@theia/core 1.71.0-next.64 → 1.71.0-next.72

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/lib/browser/catalog.json +30 -2
  2. package/lib/browser/frontend-application-contribution.d.ts +2 -2
  3. package/lib/browser/frontend-application-contribution.d.ts.map +1 -1
  4. package/lib/browser/frontend-application.d.ts +2 -0
  5. package/lib/browser/frontend-application.d.ts.map +1 -1
  6. package/lib/browser/frontend-application.js +15 -6
  7. package/lib/browser/frontend-application.js.map +1 -1
  8. package/lib/browser/performance/frontend-stopwatch.js +1 -1
  9. package/lib/browser/performance/frontend-stopwatch.js.map +1 -1
  10. package/lib/common/performance/index.d.ts +1 -0
  11. package/lib/common/performance/index.d.ts.map +1 -1
  12. package/lib/common/performance/index.js +1 -0
  13. package/lib/common/performance/index.js.map +1 -1
  14. package/lib/common/performance/simple-stopwatch.d.ts +18 -0
  15. package/lib/common/performance/simple-stopwatch.d.ts.map +1 -0
  16. package/lib/common/performance/simple-stopwatch.js +80 -0
  17. package/lib/common/performance/simple-stopwatch.js.map +1 -0
  18. package/lib/common/performance/stopwatch.d.ts +41 -0
  19. package/lib/common/performance/stopwatch.d.ts.map +1 -1
  20. package/lib/common/performance/stopwatch.js +89 -3
  21. package/lib/common/performance/stopwatch.js.map +1 -1
  22. package/lib/common/performance/stopwatch.spec.d.ts +2 -0
  23. package/lib/common/performance/stopwatch.spec.d.ts.map +1 -0
  24. package/lib/common/performance/stopwatch.spec.js +256 -0
  25. package/lib/common/performance/stopwatch.spec.js.map +1 -0
  26. package/lib/electron-main/electron-main-application-module.d.ts.map +1 -1
  27. package/lib/electron-main/electron-main-application-module.js +3 -0
  28. package/lib/electron-main/electron-main-application-module.js.map +1 -1
  29. package/lib/electron-main/electron-main-application.d.ts +2 -0
  30. package/lib/electron-main/electron-main-application.d.ts.map +1 -1
  31. package/lib/electron-main/electron-main-application.js +14 -5
  32. package/lib/electron-main/electron-main-application.js.map +1 -1
  33. package/lib/node/backend-application.d.ts +2 -0
  34. package/lib/node/backend-application.d.ts.map +1 -1
  35. package/lib/node/backend-application.js +17 -5
  36. package/lib/node/backend-application.js.map +1 -1
  37. package/lib/node/performance/node-stopwatch.js +1 -1
  38. package/lib/node/performance/node-stopwatch.js.map +1 -1
  39. package/package.json +4 -4
  40. package/src/browser/frontend-application-contribution.ts +2 -2
  41. package/src/browser/frontend-application.ts +26 -17
  42. package/src/browser/performance/frontend-stopwatch.ts +1 -1
  43. package/src/common/performance/index.ts +1 -0
  44. package/src/common/performance/simple-stopwatch.ts +91 -0
  45. package/src/common/performance/stopwatch.spec.ts +321 -0
  46. package/src/common/performance/stopwatch.ts +103 -2
  47. package/src/electron-main/electron-main-application-module.ts +3 -0
  48. package/src/electron-main/electron-main-application.ts +21 -5
  49. package/src/node/backend-application.ts +27 -10
  50. package/src/node/performance/node-stopwatch.ts +1 -1
@@ -0,0 +1,321 @@
1
+ // *****************************************************************************
2
+ // Copyright (C) 2026 STMicroelectronics and others.
3
+ //
4
+ // This program and the accompanying materials are made available under the
5
+ // terms of the Eclipse Public License v. 2.0 which is available at
6
+ // http://www.eclipse.org/legal/epl-2.0.
7
+ //
8
+ // This Source Code may also be made available under the following Secondary
9
+ // Licenses when the conditions for such availability set forth in the Eclipse
10
+ // Public License v. 2.0 are satisfied: GNU General Public License, version 2
11
+ // with the GNU Classpath Exception which is available at
12
+ // https://www.gnu.org/software/classpath/license.html.
13
+ //
14
+ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
15
+ // *****************************************************************************
16
+
17
+ import { expect } from 'chai';
18
+ import * as sinon from 'sinon';
19
+ import { Deferred } from '../promise-util';
20
+ import { Measurement, MeasurementOptions } from './measurement';
21
+ import { MeasurementContext } from './stopwatch';
22
+ import { SimpleStopwatch } from './simple-stopwatch';
23
+
24
+ /**
25
+ * A fake {@link Measurement} whose log methods are sinon spies, with a
26
+ * configurable duration returned by {@link stop}.
27
+ */
28
+ class FakeMeasurement implements Measurement {
29
+ name: string;
30
+ elapsed?: number;
31
+ duration: number;
32
+
33
+ readonly log = sinon.spy();
34
+ readonly info = sinon.spy();
35
+ readonly debug = sinon.spy();
36
+ readonly warn = sinon.spy();
37
+ readonly error = sinon.spy();
38
+
39
+ readonly stop = sinon.spy((): number => {
40
+ if (this.elapsed === undefined) {
41
+ this.elapsed = this.duration;
42
+ }
43
+ return this.elapsed;
44
+ });
45
+
46
+ constructor(name: string, duration: number = 0) {
47
+ this.name = name;
48
+ this.duration = duration;
49
+ }
50
+ }
51
+
52
+ /**
53
+ * A fake {@link Stopwatch} that creates {@link FakeMeasurement}s with
54
+ * configurable durations and records all invocations of {@link start}.
55
+ */
56
+ class FakeStopwatch extends SimpleStopwatch {
57
+ defaultDuration = 0;
58
+ readonly durationByName = new Map<string, number>();
59
+ readonly created: FakeMeasurement[] = [];
60
+
61
+ override readonly start = sinon.spy((name: string, _options?: MeasurementOptions): Measurement => {
62
+ const duration = this.durationByName.get(name) ?? this.defaultDuration;
63
+ const measurement = new FakeMeasurement(name, duration);
64
+ this.created.push(measurement);
65
+ return measurement;
66
+ });
67
+
68
+ constructor() {
69
+ super('test', () => 0);
70
+ }
71
+
72
+ /** Return the first measurement created with the given name, or `undefined`. */
73
+ measurementFor(name: string): FakeMeasurement | undefined {
74
+ return this.created.find(m => m.name === name);
75
+ }
76
+ }
77
+
78
+ class TestContribution { }
79
+ class OtherTestContribution { }
80
+
81
+ /**
82
+ * Allow any already-queued microtasks (such as `.then` callbacks attached to
83
+ * already-settled promises) to run before assertions.
84
+ */
85
+ async function flushPromises(): Promise<void> {
86
+ for (let i = 0; i < 5; i++) {
87
+ await Promise.resolve();
88
+ }
89
+ }
90
+
91
+ describe('MeasurementContext', () => {
92
+
93
+ let stopwatch: FakeStopwatch;
94
+
95
+ beforeEach(() => {
96
+ stopwatch = new FakeStopwatch();
97
+ });
98
+
99
+ describe('ensureEntry', () => {
100
+
101
+ it('starts a per-contribution measurement', () => {
102
+ const context = new MeasurementContext<TestContribution>(stopwatch, 'Frontend', 250);
103
+
104
+ context.ensureEntry(new TestContribution());
105
+
106
+ sinon.assert.calledWith(stopwatch.start, 'TestContribution.settled', sinon.match({ thresholdMillis: 250 }));
107
+ });
108
+
109
+ it('starts a per-contribution measurement only once per item', () => {
110
+ const context = new MeasurementContext<TestContribution>(stopwatch, 'Frontend', 100);
111
+ const item = new TestContribution();
112
+
113
+ context.ensureEntry(item);
114
+ context.ensureEntry(item);
115
+ context.ensureEntry(item);
116
+
117
+ const started = stopwatch.start.getCalls().filter(c => c.args[0] === 'TestContribution.settled');
118
+ expect(started).to.have.length(1);
119
+ });
120
+
121
+ it('starts independent measurements for distinct items', () => {
122
+ const context = new MeasurementContext<TestContribution>(stopwatch, 'Frontend', 100);
123
+
124
+ context.ensureEntry(new TestContribution());
125
+ context.ensureEntry(new TestContribution());
126
+
127
+ const started = stopwatch.start.getCalls().filter(c => c.args[0] === 'TestContribution.settled');
128
+ expect(started).to.have.length(2);
129
+ });
130
+ });
131
+
132
+ describe('trackSettlement', () => {
133
+
134
+ it('is a no-op for a synchronous result', async () => {
135
+ const context = new MeasurementContext<TestContribution>(stopwatch, 'Frontend', 100);
136
+ const item = new TestContribution();
137
+ context.ensureEntry(item);
138
+
139
+ context.trackSettlement(item, undefined);
140
+ context.armAllSettled();
141
+ await flushPromises();
142
+
143
+ const perContribution = stopwatch.measurementFor('TestContribution.settled')!;
144
+ sinon.assert.notCalled(perContribution.debug);
145
+ sinon.assert.notCalled(perContribution.warn);
146
+ sinon.assert.notCalled(perContribution.info);
147
+
148
+ // Synchronous results do not increment allSettledPending, so arming fires the
149
+ // aggregate message immediately.
150
+ const allSettled = stopwatch.measurementFor('frontend-all-settled')!;
151
+ sinon.assert.calledOnce(allSettled.info);
152
+ });
153
+
154
+ it('does not log a per-contribution settlement when only one promise was tracked', async () => {
155
+ // The single lifecycle measurement already describes the duration of a solo tracked
156
+ // promise, so the per-contribution aggregate must stay silent.
157
+ const context = new MeasurementContext<TestContribution>(stopwatch, 'Frontend', 100);
158
+ const item = new TestContribution();
159
+ context.ensureEntry(item);
160
+
161
+ context.trackSettlement(item, Promise.resolve());
162
+ context.armAllSettled();
163
+ await flushPromises();
164
+
165
+ const perContribution = stopwatch.measurementFor('TestContribution.settled')!;
166
+ sinon.assert.notCalled(perContribution.debug);
167
+ sinon.assert.notCalled(perContribution.warn);
168
+ sinon.assert.notCalled(perContribution.info);
169
+ });
170
+
171
+ it('logs a debug settlement message once multiple tracked promises all resolve under the threshold', async () => {
172
+ stopwatch.durationByName.set('TestContribution.settled', 50);
173
+ const context = new MeasurementContext<TestContribution>(stopwatch, 'Frontend', 100);
174
+ const item = new TestContribution();
175
+ context.ensureEntry(item);
176
+
177
+ const first = new Deferred<void>();
178
+ const second = new Deferred<void>();
179
+ context.trackSettlement(item, first.promise);
180
+ context.trackSettlement(item, second.promise);
181
+
182
+ // Before any promise resolves, nothing has been logged.
183
+ const perContribution = stopwatch.measurementFor('TestContribution.settled')!;
184
+ sinon.assert.notCalled(perContribution.debug);
185
+
186
+ first.resolve();
187
+ await flushPromises();
188
+ sinon.assert.notCalled(perContribution.debug);
189
+
190
+ second.resolve();
191
+ await flushPromises();
192
+ sinon.assert.calledOnceWithExactly(perContribution.debug, 'Frontend TestContribution settled');
193
+ sinon.assert.notCalled(perContribution.warn);
194
+ });
195
+
196
+ it('logs a warn settlement message when multiple tracked promises exceed the threshold', async () => {
197
+ stopwatch.durationByName.set('TestContribution.settled', 500);
198
+ const context = new MeasurementContext<TestContribution>(stopwatch, 'Frontend', 100);
199
+ const item = new TestContribution();
200
+ context.ensureEntry(item);
201
+
202
+ context.trackSettlement(item, Promise.resolve());
203
+ context.trackSettlement(item, Promise.resolve());
204
+ await flushPromises();
205
+
206
+ const perContribution = stopwatch.measurementFor('TestContribution.settled')!;
207
+ sinon.assert.calledOnceWithExactly(perContribution.warn, 'Frontend TestContribution took longer than expected to settle');
208
+ sinon.assert.notCalled(perContribution.debug);
209
+ });
210
+
211
+ it('treats a rejected promise as settled', async () => {
212
+ stopwatch.durationByName.set('TestContribution.settled', 10);
213
+ const context = new MeasurementContext<TestContribution>(stopwatch, 'Frontend', 100);
214
+ const item = new TestContribution();
215
+ context.ensureEntry(item);
216
+
217
+ const rejecting = new Deferred<void>();
218
+ context.trackSettlement(item, Promise.resolve());
219
+ context.trackSettlement(item, rejecting.promise);
220
+ rejecting.reject(new Error('expected failure'));
221
+ await flushPromises();
222
+
223
+ const perContribution = stopwatch.measurementFor('TestContribution.settled')!;
224
+ sinon.assert.calledOnce(perContribution.debug);
225
+ });
226
+
227
+ it('tracks promises independently for each contribution', async () => {
228
+ stopwatch.durationByName.set('TestContribution.settled', 50);
229
+ stopwatch.durationByName.set('OtherTestContribution.settled', 50);
230
+ const context = new MeasurementContext<object>(stopwatch, 'Frontend', 100);
231
+
232
+ const a = new TestContribution();
233
+ const b = new OtherTestContribution();
234
+ context.ensureEntry(a);
235
+ context.ensureEntry(b);
236
+
237
+ context.trackSettlement(a, Promise.resolve());
238
+ context.trackSettlement(a, Promise.resolve());
239
+ context.trackSettlement(b, Promise.resolve());
240
+ await flushPromises();
241
+
242
+ // a had two tracked promises: logs once.
243
+ sinon.assert.calledOnce(stopwatch.measurementFor('TestContribution.settled')!.debug);
244
+ // b had a single tracked promise: logs nothing.
245
+ sinon.assert.notCalled(stopwatch.measurementFor('OtherTestContribution.settled')!.debug);
246
+ });
247
+ });
248
+
249
+ describe('armAllSettled', () => {
250
+
251
+ it('logs the aggregate message immediately when armed with zero pending promises', () => {
252
+ const context = new MeasurementContext(stopwatch, 'Frontend', 100);
253
+
254
+ context.armAllSettled();
255
+
256
+ const allSettled = stopwatch.measurementFor('frontend-all-settled')!;
257
+ sinon.assert.calledOnceWithExactly(allSettled.info, 'All frontend contributions settled');
258
+ });
259
+
260
+ it('defers the aggregate log until the last tracked promise settles', async () => {
261
+ const context = new MeasurementContext<TestContribution>(stopwatch, 'Frontend', 100);
262
+ const item = new TestContribution();
263
+ context.ensureEntry(item);
264
+
265
+ const pending = new Deferred<void>();
266
+ context.trackSettlement(item, pending.promise);
267
+ context.armAllSettled();
268
+
269
+ const allSettled = stopwatch.measurementFor('frontend-all-settled')!;
270
+ sinon.assert.notCalled(allSettled.info);
271
+
272
+ pending.resolve();
273
+ await flushPromises();
274
+
275
+ sinon.assert.calledOnce(allSettled.info);
276
+ });
277
+
278
+ it('does not log the aggregate message when all promises settle before arming', async () => {
279
+ const context = new MeasurementContext<TestContribution>(stopwatch, 'Frontend', 100);
280
+ const item = new TestContribution();
281
+ context.ensureEntry(item);
282
+
283
+ context.trackSettlement(item, Promise.resolve());
284
+ await flushPromises();
285
+
286
+ const allSettled = stopwatch.measurementFor('frontend-all-settled')!;
287
+ sinon.assert.notCalled(allSettled.info);
288
+ });
289
+
290
+ it('logs the aggregate message when arming after all tracked promises have already settled', async () => {
291
+ const context = new MeasurementContext<TestContribution>(stopwatch, 'Frontend', 100);
292
+ const item = new TestContribution();
293
+ context.ensureEntry(item);
294
+
295
+ context.trackSettlement(item, Promise.resolve());
296
+ await flushPromises();
297
+
298
+ const allSettled = stopwatch.measurementFor('frontend-all-settled')!;
299
+ sinon.assert.notCalled(allSettled.info);
300
+
301
+ context.armAllSettled();
302
+ sinon.assert.calledOnce(allSettled.info);
303
+ });
304
+
305
+ it('logs the aggregate message exactly once when multiple contributions finish', async () => {
306
+ const context = new MeasurementContext<object>(stopwatch, 'Frontend', 100);
307
+ const a = new TestContribution();
308
+ const b = new OtherTestContribution();
309
+ context.ensureEntry(a);
310
+ context.ensureEntry(b);
311
+
312
+ context.trackSettlement(a, Promise.resolve());
313
+ context.trackSettlement(b, Promise.resolve());
314
+ context.armAllSettled();
315
+ await flushPromises();
316
+
317
+ const allSettled = stopwatch.measurementFor('frontend-all-settled')!;
318
+ sinon.assert.calledOnce(allSettled.info);
319
+ });
320
+ });
321
+ });
@@ -170,8 +170,8 @@ export abstract class Stopwatch {
170
170
  }
171
171
  }
172
172
 
173
- const start = options.owner ? `${options.owner} start` : 'start';
174
- const timeFromStart = `Finished ${(options.now() / 1000).toFixed(3)} s after ${start}`;
173
+ const origin = options.owner ?? 'application';
174
+ const timeFromStart = `${(options.now() / 1000).toFixed(3)} s since ${origin} start`;
175
175
  const whatWasMeasured = options.context ? `[${options.context}] ${activity}` : activity;
176
176
  this.logger.log(level, `${whatWasMeasured}: ${elapsed.toFixed(1)} ms [${timeFromStart}]`, ...(options.arguments ?? []));
177
177
  }
@@ -181,3 +181,104 @@ export abstract class Stopwatch {
181
181
  }
182
182
 
183
183
  }
184
+
185
+ interface SettlementEntry {
186
+ name: string;
187
+ measurement: Measurement;
188
+ pending: number;
189
+ total: number;
190
+ }
191
+
192
+ /**
193
+ * Tracks the settlement of async work initiated by contributions during application startup.
194
+ *
195
+ * A contribution "settles" when all promises it returned from lifecycle methods (initialize, configure, onStart, etc.)
196
+ * have resolved. Individual settlement is only logged when a contribution returned promises from more than one lifecycle
197
+ * method; otherwise the single lifecycle measurement already describes the work. An aggregate "all settled" message is
198
+ * logged once all tracked promises across all contributions have resolved.
199
+ *
200
+ * Typical usage:
201
+ * 1. Create the context at the start of the application lifecycle.
202
+ * 2. Before each lifecycle call, call {@link ensureEntry} to start the per-contribution clock.
203
+ * 3. After each lifecycle call, call {@link trackSettlement} with the return value.
204
+ * 4. After the startup sequence completes, call {@link armAllSettled} to enable the aggregate message.
205
+ */
206
+ export class MeasurementContext<T extends object = object> {
207
+
208
+ private readonly entries = new Map<T, SettlementEntry>();
209
+ private readonly allSettledMeasurement: Measurement;
210
+ private allSettledPending = 0;
211
+ private allSettledArmed = false;
212
+
213
+ constructor(
214
+ protected readonly stopwatch: Stopwatch,
215
+ protected readonly owner: string,
216
+ protected readonly thresholdMillis: number
217
+ ) {
218
+ this.allSettledMeasurement = this.stopwatch.start(`${owner.toLowerCase()}-all-settled`);
219
+ }
220
+
221
+ /**
222
+ * Ensure that settlement tracking has been started for the given contribution.
223
+ * Starts the per-contribution measurement clock on the first call for each contribution.
224
+ */
225
+ ensureEntry(item: T): void {
226
+ if (!this.entries.has(item)) {
227
+ const name = item.constructor.name;
228
+ this.entries.set(item, {
229
+ name,
230
+ measurement: this.stopwatch.start(`${name}.settled`, { thresholdMillis: this.thresholdMillis }),
231
+ pending: 0,
232
+ total: 0
233
+ });
234
+ }
235
+ }
236
+
237
+ /**
238
+ * Track a promise returned by a contribution's lifecycle method.
239
+ * Must be called after the corresponding {@link Stopwatch.startAsync} has completed so that
240
+ * the settlement log appears after the lifecycle measurement log.
241
+ */
242
+ trackSettlement(item: T, result: MaybePromise<unknown>): void {
243
+ if (result instanceof Promise) {
244
+ const entry = this.entries.get(item)!;
245
+ entry.pending++;
246
+ entry.total++;
247
+ this.allSettledPending++;
248
+ const onSettled = (): void => {
249
+ this.onPromiseSettled(item);
250
+ };
251
+ result.then(onSettled, onSettled);
252
+ }
253
+ }
254
+
255
+ /**
256
+ * Arm the aggregate "all settled" log message. Call this after the startup sequence has finished
257
+ * collecting all promises. If all promises have already settled, the message is logged immediately.
258
+ */
259
+ armAllSettled(): void {
260
+ this.allSettledArmed = true;
261
+ if (this.allSettledPending === 0) {
262
+ this.allSettledMeasurement.info(`All ${this.owner.toLowerCase()} contributions settled`);
263
+ }
264
+ }
265
+
266
+ private onPromiseSettled(item: T): void {
267
+ const entry = this.entries.get(item);
268
+ if (entry && --entry.pending === 0) {
269
+ const { name, measurement, total } = entry;
270
+ this.entries.delete(item);
271
+ if (total > 1) {
272
+ if (measurement.stop() > this.thresholdMillis) {
273
+ measurement.warn(`${this.owner} ${name} took longer than expected to settle`);
274
+ } else {
275
+ measurement.debug(`${this.owner} ${name} settled`);
276
+ }
277
+ }
278
+ }
279
+ if (--this.allSettledPending === 0 && this.allSettledArmed) {
280
+ this.allSettledMeasurement.info(`All ${this.owner.toLowerCase()} contributions settled`);
281
+ }
282
+ }
283
+
284
+ }
@@ -15,8 +15,10 @@
15
15
  // *****************************************************************************
16
16
 
17
17
  import { ContainerModule } from 'inversify';
18
+ import { performance } from 'perf_hooks';
18
19
  import { generateUuid } from '../common/uuid';
19
20
  import { bindRootContributionProvider } from '../common/contribution-provider';
21
+ import { Stopwatch, SimpleStopwatch } from '../common/performance';
20
22
  import { RpcConnectionHandler } from '../common/messaging/proxy-factory';
21
23
  import { ElectronSecurityToken } from '../electron-common/electron-token';
22
24
  import { ElectronMainWindowService, electronMainWindowServicePath } from '../electron-common/electron-main-window-service';
@@ -34,6 +36,7 @@ const electronSecurityToken: ElectronSecurityToken = { value: generateUuid() };
34
36
  (global as any)[ElectronSecurityToken] = electronSecurityToken;
35
37
 
36
38
  export default new ContainerModule(bind => {
39
+ bind(Stopwatch).toConstantValue(new SimpleStopwatch('electron main', () => performance.now()));
37
40
  bind(ElectronMainApplication).toSelf().inSingletonScope();
38
41
  bind(ElectronMessagingContribution).toSelf().inSingletonScope();
39
42
  bind(ElectronMainApplicationContribution).toService(ElectronMessagingContribution);
@@ -30,6 +30,7 @@ import URI from '../common/uri';
30
30
  import { FileUri } from '../common/file-uri';
31
31
  import { Deferred, timeout } from '../common/promise-util';
32
32
  import { MaybePromise } from '../common/types';
33
+ import { Stopwatch } from '../common/performance';
33
34
  import { ContributionProvider } from '../common/contribution-provider';
34
35
  import { ElectronSecurityTokenService } from './electron-security-token-service';
35
36
  import { ElectronSecurityToken } from '../electron-common/electron-token';
@@ -49,6 +50,8 @@ export { ElectronMainApplicationGlobals };
49
50
 
50
51
  const createYargs: (argv?: string[], cwd?: string) => Argv = require('yargs/yargs');
51
52
 
53
+ const ELECTRON_TIMER_WARNING_THRESHOLD = 50;
54
+
52
55
  /**
53
56
  * Options passed to the main/default command handler.
54
57
  */
@@ -171,6 +174,9 @@ export class ElectronMainApplication {
171
174
  @inject(TheiaElectronWindowFactory)
172
175
  protected readonly windowFactory: TheiaElectronWindowFactory;
173
176
 
177
+ @inject(Stopwatch)
178
+ protected readonly stopwatch: Stopwatch;
179
+
174
180
  protected isPortable = this.makePortable();
175
181
 
176
182
  protected readonly electronStore = new Storage<{
@@ -229,15 +235,19 @@ export class ElectronMainApplication {
229
235
  await fs.mkdir(args.electronUserData, { recursive: true });
230
236
  app.setPath('userData', args.electronUserData);
231
237
  }
238
+ const startupMeasurement = this.stopwatch.start('electron-main-startup');
232
239
  this.useNativeWindowFrame = this.getTitleBarStyle(config) === 'native';
233
240
  this._config = config;
234
241
  this.hookApplicationEvents();
235
242
  this.showInitialWindow(argv.includes('--open-url') ? argv[argv.length - 1] : undefined);
236
- const port = await this.startBackend();
243
+ const port = await this.stopwatch.startAsync('electron-main-start-backend', 'Starting backend', () => this.startBackend());
237
244
  this._backendPort.resolve(port);
238
245
  await app.whenReady();
239
- await this.attachElectronSecurityToken(port);
240
- await this.startContributions();
246
+ await this.stopwatch.startAsync('electron-main-security-token', 'Attaching security token',
247
+ () => this.attachElectronSecurityToken(port));
248
+ await this.stopwatch.startAsync('electron-main-start-contributions', 'Starting contributions',
249
+ () => this.startContributions());
250
+ startupMeasurement.info('Startup sequence completed');
241
251
 
242
252
  this.handleMainCommand({
243
253
  file: args.file,
@@ -920,8 +930,14 @@ export class ElectronMainApplication {
920
930
  protected async startContributions(): Promise<void> {
921
931
  const promises = [];
922
932
  for (const contribution of this.contributions.getContributions()) {
923
- if (contribution.onStart) {
924
- promises.push(contribution.onStart(this));
933
+ const onStart = contribution.onStart;
934
+ if (onStart) {
935
+ promises.push(this.stopwatch.startAsync(
936
+ `${contribution.constructor.name}.onStart`,
937
+ `${contribution.constructor.name}.onStart`,
938
+ () => onStart.call(contribution, this),
939
+ { thresholdMillis: ELECTRON_TIMER_WARNING_THRESHOLD }
940
+ ));
925
941
  }
926
942
  }
927
943
  await Promise.all(promises);
@@ -22,7 +22,7 @@ import * as express from 'express';
22
22
  import * as yargs from 'yargs';
23
23
  import * as fs from 'fs-extra';
24
24
  import { inject, named, injectable, postConstruct } from 'inversify';
25
- import { ContributionProvider, MaybePromise, Stopwatch } from '../common';
25
+ import { ContributionProvider, LogLevel, MaybePromise, MeasurementContext, Stopwatch } from '../common';
26
26
  import { CliContribution } from './cli';
27
27
  import { Deferred } from '../common/promise-util';
28
28
  import { environment } from '../common/index';
@@ -164,6 +164,8 @@ export class BackendApplication {
164
164
 
165
165
  private _configured: Promise<void>;
166
166
 
167
+ private settlementContext?: MeasurementContext<BackendApplicationContribution>;
168
+
167
169
  constructor(
168
170
  @inject(ContributionProvider) @named(BackendApplicationContribution)
169
171
  protected readonly contributionsProvider: ContributionProvider<BackendApplicationContribution>,
@@ -195,9 +197,8 @@ export class BackendApplication {
195
197
  await Promise.all(this.contributionsProvider.getContributions().map(async contribution => {
196
198
  if (contribution.initialize) {
197
199
  try {
198
- await this.measure(contribution.constructor.name + '.initialize',
199
- () => contribution.initialize!()
200
- );
200
+ await this.measureContribution(contribution, 'initialize',
201
+ () => contribution.initialize!());
201
202
  } catch (error) {
202
203
  console.error('Could not initialize contribution', error);
203
204
  }
@@ -211,6 +212,7 @@ export class BackendApplication {
211
212
 
212
213
  @postConstruct()
213
214
  protected init(): void {
215
+ this.settlementContext = new MeasurementContext(this.stopwatch, 'Backend', TIMER_WARNING_THRESHOLD);
214
216
  this._configured = this.configure();
215
217
  }
216
218
 
@@ -232,7 +234,8 @@ export class BackendApplication {
232
234
  await Promise.all(this.contributionsProvider.getContributions().map(async contribution => {
233
235
  if (contribution.configure) {
234
236
  try {
235
- await contribution.configure!(this.app);
237
+ await this.measureContribution(contribution, 'configure',
238
+ () => contribution.configure!(this.app));
236
239
  } catch (error) {
237
240
  console.error('Could not configure contribution', error);
238
241
  }
@@ -246,6 +249,8 @@ export class BackendApplication {
246
249
  }
247
250
 
248
251
  async start(port?: number, hostname?: string): Promise<http.Server | https.Server> {
252
+ const startupMeasurement = this.stopwatch.start('backend-startup');
253
+
249
254
  hostname ??= this.cliParams.hostname;
250
255
  port ??= this.cliParams.port;
251
256
 
@@ -307,15 +312,17 @@ export class BackendApplication {
307
312
  for (const contribution of this.contributionsProvider.getContributions()) {
308
313
  if (contribution.onStart) {
309
314
  try {
310
- await this.measure(contribution.constructor.name + '.onStart',
311
- () => contribution.onStart!(server)
312
- );
315
+ await this.measureContribution(contribution, 'onStart',
316
+ () => contribution.onStart!(server));
313
317
  } catch (error) {
314
318
  console.error('Could not start contribution', error);
315
319
  }
316
320
  }
317
321
  }
318
- return this.stopwatch.startAsync('server', 'Finished starting backend application', () => deferred.promise);
322
+ await deferred.promise;
323
+ startupMeasurement.info('Backend application startup sequence completed (async work may still be pending)');
324
+ this.settlementContext?.armAllSettled();
325
+ return server;
319
326
  }
320
327
 
321
328
  protected getHttpUrl({ address, port, family }: AddressInfo, ssl?: boolean): string {
@@ -358,8 +365,18 @@ export class BackendApplication {
358
365
  next();
359
366
  }
360
367
 
368
+ protected async measureContribution<T>(contribution: BackendApplicationContribution, hook: string, fn: () => MaybePromise<T>): Promise<T> {
369
+ let innerResult: MaybePromise<T>;
370
+ this.settlementContext?.ensureEntry(contribution);
371
+ const result = await this.measure(contribution.constructor.name + '.' + hook,
372
+ () => (innerResult = fn())
373
+ );
374
+ this.settlementContext?.trackSettlement(contribution, innerResult!);
375
+ return result;
376
+ }
377
+
361
378
  protected async measure<T>(name: string, fn: () => MaybePromise<T>): Promise<T> {
362
- return this.stopwatch.startAsync(name, `Backend ${name}`, fn, { thresholdMillis: TIMER_WARNING_THRESHOLD });
379
+ return this.stopwatch.startAsync(name, `Backend ${name}`, fn, { thresholdMillis: TIMER_WARNING_THRESHOLD, defaultLogLevel: LogLevel.DEBUG });
363
380
  }
364
381
 
365
382
  protected handleUncaughtError(error: Error): void {
@@ -23,7 +23,7 @@ export class NodeStopwatch extends Stopwatch {
23
23
 
24
24
  constructor() {
25
25
  super({
26
- owner: 'backend',
26
+ owner: 'backend process',
27
27
  now: () => performance.now(),
28
28
  });
29
29
  }