-
Notifications
You must be signed in to change notification settings - Fork 2.5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add start-up timing logs in frontend and backend #10407
Add start-up timing logs in frontend and backend #10407
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you for your contribution 👍
In order to accept your changes please be sure to sure to sign the eclipse contributor agreement (eca) with the same email as your authorship.
05a1731
to
cb28d08
Compare
Thanks, @vince-fugnitto ! I had already signed the ECA (being a committer on other projects) but used a different e-mail address as commit author than what is registered at Eclipse. I have fixed that. |
cb28d08
to
4a34d3f
Compare
Commit 4a34d3f rebases the branch onto a bunch of changes on the |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The approach already looks quite good. I have a few suggestions, regarding APIs and optionality.
@optional() | ||
@inject(BackendStopwatch) | ||
protected readonly backendStopwatch?: BackendStopwatch; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there a special reason why this is optional? As far as I can tell, making it optional only makes stuff more complicated. If users don't want to enable this BackendStopwatch
stuff, I would much rather have an injectable option object on which I can simply set a disabled
flag.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll look for an example elsewhere in the codebase to mimic.
return result; | ||
} | ||
|
||
private mergeLogOptions(logOptions?: Partial<LogOptions>): LogOptions { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Minor: We usually leave nothing as private
and allow everything to be overwritten by the downstream user. Please use protected
instead.
constructor(@inject(new LazyServiceIdentifer(() => Stopwatch)) protected readonly stopwatch: Stopwatch) { | ||
// Nothing else to initialize | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Suggestion: We usually try to shy away from constructor injection due to the API constraints it introduces when trying to override classes. I would make this a class field.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually, I had originally tried field injection, but the LazyServiceIdentifer
[sic] for function-based injection to break dependency cycles doesn't work for property injection1. It has to be used in a class constructor. And this case is a dependency cycle, so I'm not sure what else I can do. I could try @lazyInject
but that requires adding a new package dependency that we haven't used to date in Theia.
Footnotes
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
And this case is a dependency cycle
I'm not sure what you mean here. DefaultBackendStopwatch
depends on Stopwatch
, and on the back end, Stopwatch
is bound to NodeStopwatch
, which does not depend on DefaultBackendStopwatch
in any way, so I don't see any dependency cycle, and I was able to get this code working with simple property injection @inject(Stopwatch) protected readonly stopwatch: Stopwatch;
. What circularity do you see or foresee here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
And this case is a dependency cycle
I'm not sure what you mean here.
DefaultBackendStopwatch
depends onStopwatch
, and on the back end,Stopwatch
is bound toNodeStopwatch
, which does not depend onDefaultBackendStopwatch
in any way, so I don't see any dependency cycle, and I was able to get this code working with simple property injection@inject(Stopwatch) protected readonly stopwatch: Stopwatch;
. What circularity do you see or foresee here?
I got an injection failure exception complaining of unsatisfied dependency when I tried that initially. I tracked it down to a dependency cycle in debugging, which is why I resorted to the lazy service identifier. Perhaps subsequent changes have broken that cycle. I'll have another look.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Confirmed. The dependency cycle that we once had here is evidently no longer. So, that's better all around.
4a34d3f
to
28c7094
Compare
Thanks for the review, @msujew! I've pushed commit 28c7094 that rebases to resolve merge conflicts and addresses your comments:
|
export function bindBackendStopwatch(bind: interfaces.Bind): interfaces.BindingWhenOnSyntax<unknown> { | ||
return bind(BackendStopwatch).toDynamicValue(({ container }) => { | ||
if (container.isBound(BackendStopwatchOptions) && container.get<BackendStopwatchOptions>(BackendStopwatchOptions).disabled) { | ||
// If the back-end stopwatch service is disabled, then we don't the RPC proxy at all |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
// If the back-end stopwatch service is disabled, then we don't the RPC proxy at all | |
// If the back-end stopwatch service is disabled, then we don't need the RPC proxy at all |
Maybe?
* A service that exposes the back-end's {@link Stopwatch} to clients | ||
* via the remote API. | ||
*/ | ||
export interface BackendStopwatch extends JsonRpcServer<unknown> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It doesn't look like any of JsonRpcServer
machinery is referred to by callers of the BackendStopwatch
. Unless you foresee a use for calls to setClient
etc., you could omit extends JsonRpcServer
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see no use cases for exposing those APIs, so yes I would prefer to leave off the inherited interface. Didn't know I could do that!
constructor(@inject(new LazyServiceIdentifer(() => Stopwatch)) protected readonly stopwatch: Stopwatch) { | ||
// Nothing else to initialize | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
And this case is a dependency cycle
I'm not sure what you mean here. DefaultBackendStopwatch
depends on Stopwatch
, and on the back end, Stopwatch
is bound to NodeStopwatch
, which does not depend on DefaultBackendStopwatch
in any way, so I don't see any dependency cycle, and I was able to get this code working with simple property injection @inject(Stopwatch) protected readonly stopwatch: Stopwatch;
. What circularity do you see or foresee here?
@injectable() | ||
export class NullBackendStopwatch { | ||
|
||
protected idSequence: number = 0; | ||
|
||
start(): RemoteMeasurement { | ||
return ++this.idSequence; | ||
} | ||
|
||
stop(): void { | ||
// Nothing to stop | ||
} | ||
|
||
/** | ||
* Obtain a proxy for myself that implements the asynchronous RPC API. | ||
* | ||
* @returns the async proxy for myself | ||
*/ | ||
asyncProxy(): BackendStopwatch { | ||
return new Proxy<BackendStopwatch>(this as any, { | ||
get(target: any, p: string | symbol, receiver: any): any { | ||
if (p === 'start') { | ||
return () => Promise.resolve(target.start()); | ||
} | ||
if (p === 'stop') { | ||
return () => Promise.resolve(); | ||
} | ||
} | ||
}); | ||
|
||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since the only purpose of this class is to satisfy the BackendStopwatch
interface without doing anything, it could be as simple as
@injectable() | |
export class NullBackendStopwatch { | |
protected idSequence: number = 0; | |
start(): RemoteMeasurement { | |
return ++this.idSequence; | |
} | |
stop(): void { | |
// Nothing to stop | |
} | |
/** | |
* Obtain a proxy for myself that implements the asynchronous RPC API. | |
* | |
* @returns the async proxy for myself | |
*/ | |
asyncProxy(): BackendStopwatch { | |
return new Proxy<BackendStopwatch>(this as any, { | |
get(target: any, p: string | symbol, receiver: any): any { | |
if (p === 'start') { | |
return () => Promise.resolve(target.start()); | |
} | |
if (p === 'stop') { | |
return () => Promise.resolve(); | |
} | |
} | |
}); | |
} | |
} | |
@injectable() | |
export class NullBackendStopwatch implements BackendStopwatch { | |
start(): Promise<RemoteMeasurement> { | |
return Promise.resolve(0); | |
} | |
stop(): Promise<void> { | |
return Promise.resolve(); | |
} | |
} |
Assuming that the BackendStopwatch
interface is simplified to exclude the RPCProxy part.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, this complexity was only there to maintain the RPC protocol. Glad to drop it.
*/ | ||
context?: string; | ||
|
||
/** An optional logging level at which to emit the log message. The default-default is {@link LogLevel.INFO}. */ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
/** An optional logging level at which to emit the log message. The default-default is {@link LogLevel.INFO}. */ | |
/** An optional logging level at which to emit the log message. The default default is {@link LogLevel.INFO}. */ |
Or 'default value' or just 'default'.
/** | ||
* Injectable options for configuration of the (remote) back-end stopwatch service. | ||
*/ | ||
export interface BackendStopwatchOptions { | ||
/** | ||
* Wether to disable the back-end stopwatch service. When `disabled`, the service interface | ||
* is still fulfilled, but it just doesn't do anything. The default `disabled` state is | ||
* `false` (being enabled). | ||
*/ | ||
disabled?: boolean; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure I see the purpose of this interface. Without it, adopters who don't want a BackendStopwatch can do something like
rebind(BackendStopwatch).to(NullBackendStopwatch).inSingletonScope();
which seems at least as straightforward as
bind(BackendStopwatchOptions).toConstantValue({disabled: true});
return options?.levelOverride; | ||
} | ||
|
||
return options?.defaultLogLevel || DEFAULT_LOG_LEVEL; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
return options?.defaultLogLevel || DEFAULT_LOG_LEVEL; | |
return options?.defaultLogLevel ?? this.defaultOptions.defaultLogLevel ?? DEFAULT_LOG_LEVEL; |
This method is likely expected to be called on options have gone through mergeOptions
, but it isn't guaranteed, so it's like worth inserting the local default.
|
||
protected logLevel(elapsed: number, options?: Partial<LogOptions>): LogLevel { | ||
if (options?.levelOverride) { | ||
return options?.levelOverride; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
return options?.levelOverride; | |
return options.levelOverride; |
constructor( | ||
@inject(ContributionProvider) @named(BackendApplicationContribution) | ||
protected readonly contributionsProvider: ContributionProvider<BackendApplicationContribution>, | ||
@inject(BackendApplicationCliContribution) protected readonly cliParams: BackendApplicationCliContribution | ||
@inject(BackendApplicationCliContribution) protected readonly cliParams: BackendApplicationCliContribution, | ||
@inject(Stopwatch) protected readonly stopwatch: Stopwatch |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This can be added as a property injection.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The constructor calls initialize
, which needs to use the stopwatch to measure the initializations of contributions. But this happens before property injection, so constructor injection seemed to be the way to go. This then would also have to be added to the breaking API changes in the changelog.
I would have moved the initialize
step to the @postConstruct configure
method, but I figured there must have been a reason to call it from the constructor and so I wasn't comfortable making that change.
28c7094
to
2270385
Compare
Thanks @colin-grant-work for the review. I think this has helped to clean things up considerably! I've rebased to resolve merge conflict in the changelog and made changes to address your comments. I've not got a solution to the matter of changing the constructor of the Personally, I'd rather see that initialization step also happen in the post construction because it doesn't feel like an object constructor responsibility and it would mean that we can avoid the API-breaking changing to the constructor signature. WDYT? |
I agree here - in general we want initialization to happen in decorated |
2270385
to
07b3617
Compare
I've pushed an update that moves the initialization as discussed, to maintain the existing constructor signature. Testing the example deployment shows no outward change in behaviour. In particular, initialization and configuration of contributions are still interleaved as before (the Because now the constructor does not trigger initialization, any derivative applications that don't get the |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The changes are looking really good to me now. I have a minor remark, but except for that I don't have anything to add 👍
@cdamus, apologies for the delay. I'll take a look now. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good to me, and it's good to have a more unified style of measurement.
@@ -263,10 +273,9 @@ export class FrontendApplication { | |||
protected revealShell(host: HTMLElement): Promise<void> { | |||
const startupElem = this.getStartupIndicator(host); | |||
if (startupElem) { | |||
return new Promise(resolve => { | |||
const doRevealShell = () => new Promise<void>(resolve => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since we're already adding several measurements to the start
method, which is where this is called, it seems more consistent (and easier) to add the measurement there? I'm not sure how often we enter the else
here - in principle, it shouldn't be often.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Indeed, this was too cute by half. Thanks for the insight about the else case!
Added some finer-grained logging of timing measurements in backend start-up tasks. Includes a refactoring of similar patterns used throughout frontend and backend code, using different performance tracing APIs, into a common injected Stopwatch service providing Measurements. Also publishes the backend Stopwatch to frontends via a new BackendStopwatch RPC service, letting frontends log messages in the timeframe of the backend application. Contributed on behalf of STMicroelectronics. Signed-off-by: Christian W. Damus <[email protected]>
07b3617
to
232477a
Compare
No apologies needed @colin-grant-work. I'm very glad to have your keen eyes on this, and as you can tell I don't have a great deal of bandwidth for this, either! I've pushed an update that resolves the merge conflict and simplifies the frontend application start measurement per your suggestion. |
What it does
Refactors various functions in a few different places in the frontend into a new abstract
Stopwatch
class that provides measurement of tasks and logs their completion with messages similar to what we had before. The frontend injects a concrete implementation based on the browser performance API used previously and the backend injects a concrete implementation based on the similar, but different, node.js performance API.Additionally, as the performance of the frontend and backend applications is largely independent but also coupled at a few synchronization points, a
BackendStopwatch
RPC service is added that publishes limited access to the backend’s stopwatch, letting the frontend log timing messages in the timeframe of the backend application. This is used to log how long it took the frontend to fully start up from the backend’s perspective, which is especially useful in the Electron deployment but also makes sense in most Browser use cases.Resolves #10406.
No new package dependencies are added.
Contributed on behalf of STMicroelectronics.
How to test
Start the Electron or Browser example and see the timing log messages from both the frontend and the backend emitted on the console.
Note also that the format of log messages is slightly changed, for example removing the word "took" from the
took: xy.z ms
part of the pattern, as it caused some new messages to read strangely and was redundant.Review checklist
Reminder for reviewers