UNPKG

@theia/core

Version:

Theia is a cloud & desktop IDE framework implemented in TypeScript.

256 lines • 13.1 kB
"use strict"; // ***************************************************************************** // Copyright (C) 2026 STMicroelectronics and others. // // This program and the accompanying materials are made available under the // terms of the Eclipse Public License v. 2.0 which is available at // http://www.eclipse.org/legal/epl-2.0. // // This Source Code may also be made available under the following Secondary // Licenses when the conditions for such availability set forth in the Eclipse // Public License v. 2.0 are satisfied: GNU General Public License, version 2 // with the GNU Classpath Exception which is available at // https://www.gnu.org/software/classpath/license.html. // // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 // ***************************************************************************** Object.defineProperty(exports, "__esModule", { value: true }); const chai_1 = require("chai"); const sinon = require("sinon"); const promise_util_1 = require("../promise-util"); const stopwatch_1 = require("./stopwatch"); const simple_stopwatch_1 = require("./simple-stopwatch"); /** * A fake {@link Measurement} whose log methods are sinon spies, with a * configurable duration returned by {@link stop}. */ class FakeMeasurement { constructor(name, duration = 0) { this.log = sinon.spy(); this.info = sinon.spy(); this.debug = sinon.spy(); this.warn = sinon.spy(); this.error = sinon.spy(); this.stop = sinon.spy(() => { if (this.elapsed === undefined) { this.elapsed = this.duration; } return this.elapsed; }); this.name = name; this.duration = duration; } } /** * A fake {@link Stopwatch} that creates {@link FakeMeasurement}s with * configurable durations and records all invocations of {@link start}. */ class FakeStopwatch extends simple_stopwatch_1.SimpleStopwatch { constructor() { super('test', () => 0); this.defaultDuration = 0; this.durationByName = new Map(); this.created = []; this.start = sinon.spy((name, _options) => { const duration = this.durationByName.get(name) ?? this.defaultDuration; const measurement = new FakeMeasurement(name, duration); this.created.push(measurement); return measurement; }); } /** Return the first measurement created with the given name, or `undefined`. */ measurementFor(name) { return this.created.find(m => m.name === name); } } class TestContribution { } class OtherTestContribution { } /** * Allow any already-queued microtasks (such as `.then` callbacks attached to * already-settled promises) to run before assertions. */ async function flushPromises() { for (let i = 0; i < 5; i++) { await Promise.resolve(); } } describe('MeasurementContext', () => { let stopwatch; beforeEach(() => { stopwatch = new FakeStopwatch(); }); describe('ensureEntry', () => { it('starts a per-contribution measurement', () => { const context = new stopwatch_1.MeasurementContext(stopwatch, 'Frontend', 250); context.ensureEntry(new TestContribution()); sinon.assert.calledWith(stopwatch.start, 'TestContribution.settled', sinon.match({ thresholdMillis: 250 })); }); it('starts a per-contribution measurement only once per item', () => { const context = new stopwatch_1.MeasurementContext(stopwatch, 'Frontend', 100); const item = new TestContribution(); context.ensureEntry(item); context.ensureEntry(item); context.ensureEntry(item); const started = stopwatch.start.getCalls().filter(c => c.args[0] === 'TestContribution.settled'); (0, chai_1.expect)(started).to.have.length(1); }); it('starts independent measurements for distinct items', () => { const context = new stopwatch_1.MeasurementContext(stopwatch, 'Frontend', 100); context.ensureEntry(new TestContribution()); context.ensureEntry(new TestContribution()); const started = stopwatch.start.getCalls().filter(c => c.args[0] === 'TestContribution.settled'); (0, chai_1.expect)(started).to.have.length(2); }); }); describe('trackSettlement', () => { it('is a no-op for a synchronous result', async () => { const context = new stopwatch_1.MeasurementContext(stopwatch, 'Frontend', 100); const item = new TestContribution(); context.ensureEntry(item); context.trackSettlement(item, undefined); context.armAllSettled(); await flushPromises(); const perContribution = stopwatch.measurementFor('TestContribution.settled'); sinon.assert.notCalled(perContribution.debug); sinon.assert.notCalled(perContribution.warn); sinon.assert.notCalled(perContribution.info); // Synchronous results do not increment allSettledPending, so arming fires the // aggregate message immediately. const allSettled = stopwatch.measurementFor('frontend-all-settled'); sinon.assert.calledOnce(allSettled.info); }); it('does not log a per-contribution settlement when only one promise was tracked', async () => { // The single lifecycle measurement already describes the duration of a solo tracked // promise, so the per-contribution aggregate must stay silent. const context = new stopwatch_1.MeasurementContext(stopwatch, 'Frontend', 100); const item = new TestContribution(); context.ensureEntry(item); context.trackSettlement(item, Promise.resolve()); context.armAllSettled(); await flushPromises(); const perContribution = stopwatch.measurementFor('TestContribution.settled'); sinon.assert.notCalled(perContribution.debug); sinon.assert.notCalled(perContribution.warn); sinon.assert.notCalled(perContribution.info); }); it('logs a debug settlement message once multiple tracked promises all resolve under the threshold', async () => { stopwatch.durationByName.set('TestContribution.settled', 50); const context = new stopwatch_1.MeasurementContext(stopwatch, 'Frontend', 100); const item = new TestContribution(); context.ensureEntry(item); const first = new promise_util_1.Deferred(); const second = new promise_util_1.Deferred(); context.trackSettlement(item, first.promise); context.trackSettlement(item, second.promise); // Before any promise resolves, nothing has been logged. const perContribution = stopwatch.measurementFor('TestContribution.settled'); sinon.assert.notCalled(perContribution.debug); first.resolve(); await flushPromises(); sinon.assert.notCalled(perContribution.debug); second.resolve(); await flushPromises(); sinon.assert.calledOnceWithExactly(perContribution.debug, 'Frontend TestContribution settled'); sinon.assert.notCalled(perContribution.warn); }); it('logs a warn settlement message when multiple tracked promises exceed the threshold', async () => { stopwatch.durationByName.set('TestContribution.settled', 500); const context = new stopwatch_1.MeasurementContext(stopwatch, 'Frontend', 100); const item = new TestContribution(); context.ensureEntry(item); context.trackSettlement(item, Promise.resolve()); context.trackSettlement(item, Promise.resolve()); await flushPromises(); const perContribution = stopwatch.measurementFor('TestContribution.settled'); sinon.assert.calledOnceWithExactly(perContribution.warn, 'Frontend TestContribution took longer than expected to settle'); sinon.assert.notCalled(perContribution.debug); }); it('treats a rejected promise as settled', async () => { stopwatch.durationByName.set('TestContribution.settled', 10); const context = new stopwatch_1.MeasurementContext(stopwatch, 'Frontend', 100); const item = new TestContribution(); context.ensureEntry(item); const rejecting = new promise_util_1.Deferred(); context.trackSettlement(item, Promise.resolve()); context.trackSettlement(item, rejecting.promise); rejecting.reject(new Error('expected failure')); await flushPromises(); const perContribution = stopwatch.measurementFor('TestContribution.settled'); sinon.assert.calledOnce(perContribution.debug); }); it('tracks promises independently for each contribution', async () => { stopwatch.durationByName.set('TestContribution.settled', 50); stopwatch.durationByName.set('OtherTestContribution.settled', 50); const context = new stopwatch_1.MeasurementContext(stopwatch, 'Frontend', 100); const a = new TestContribution(); const b = new OtherTestContribution(); context.ensureEntry(a); context.ensureEntry(b); context.trackSettlement(a, Promise.resolve()); context.trackSettlement(a, Promise.resolve()); context.trackSettlement(b, Promise.resolve()); await flushPromises(); // a had two tracked promises: logs once. sinon.assert.calledOnce(stopwatch.measurementFor('TestContribution.settled').debug); // b had a single tracked promise: logs nothing. sinon.assert.notCalled(stopwatch.measurementFor('OtherTestContribution.settled').debug); }); }); describe('armAllSettled', () => { it('logs the aggregate message immediately when armed with zero pending promises', () => { const context = new stopwatch_1.MeasurementContext(stopwatch, 'Frontend', 100); context.armAllSettled(); const allSettled = stopwatch.measurementFor('frontend-all-settled'); sinon.assert.calledOnceWithExactly(allSettled.info, 'All frontend contributions settled'); }); it('defers the aggregate log until the last tracked promise settles', async () => { const context = new stopwatch_1.MeasurementContext(stopwatch, 'Frontend', 100); const item = new TestContribution(); context.ensureEntry(item); const pending = new promise_util_1.Deferred(); context.trackSettlement(item, pending.promise); context.armAllSettled(); const allSettled = stopwatch.measurementFor('frontend-all-settled'); sinon.assert.notCalled(allSettled.info); pending.resolve(); await flushPromises(); sinon.assert.calledOnce(allSettled.info); }); it('does not log the aggregate message when all promises settle before arming', async () => { const context = new stopwatch_1.MeasurementContext(stopwatch, 'Frontend', 100); const item = new TestContribution(); context.ensureEntry(item); context.trackSettlement(item, Promise.resolve()); await flushPromises(); const allSettled = stopwatch.measurementFor('frontend-all-settled'); sinon.assert.notCalled(allSettled.info); }); it('logs the aggregate message when arming after all tracked promises have already settled', async () => { const context = new stopwatch_1.MeasurementContext(stopwatch, 'Frontend', 100); const item = new TestContribution(); context.ensureEntry(item); context.trackSettlement(item, Promise.resolve()); await flushPromises(); const allSettled = stopwatch.measurementFor('frontend-all-settled'); sinon.assert.notCalled(allSettled.info); context.armAllSettled(); sinon.assert.calledOnce(allSettled.info); }); it('logs the aggregate message exactly once when multiple contributions finish', async () => { const context = new stopwatch_1.MeasurementContext(stopwatch, 'Frontend', 100); const a = new TestContribution(); const b = new OtherTestContribution(); context.ensureEntry(a); context.ensureEntry(b); context.trackSettlement(a, Promise.resolve()); context.trackSettlement(b, Promise.resolve()); context.armAllSettled(); await flushPromises(); const allSettled = stopwatch.measurementFor('frontend-all-settled'); sinon.assert.calledOnce(allSettled.info); }); }); }); //# sourceMappingURL=stopwatch.spec.js.map