From 7e383ca675f0a5ed5d29bde5cd76ee7fcb3fa4f3 Mon Sep 17 00:00:00 2001 From: 4gray Date: Thu, 1 Oct 2026 06:27:24 +0200 Subject: [PATCH] test(perf): refuse a J1 idle window that opened late after the settle point Addresses review: the idle window opens in the settle timer's callback while the settle point is that timer's deadline, so a late callback left ticks uncounted between the two. Co-Authored-By: Claude Opus 5.5 --- .../performance/launch-journey-record.spec.ts | 30 +++++++++++++++---- .../src/performance/launch-journey-record.ts | 14 +++++++++ docs/architecture/performance-journeys.md | 8 +++-- 3 files changed, 44 insertions(+), 8 deletions(-) diff --git a/apps/electron-backend-e2e/src/performance/launch-journey-record.spec.ts b/apps/electron-backend-e2e/src/performance/launch-journey-record.spec.ts index 90624738c..8035f7982 100644 --- a/apps/electron-backend-e2e/src/performance/launch-journey-record.spec.ts +++ b/apps/electron-backend-e2e/src/performance/launch-journey-record.spec.ts @@ -31,8 +31,8 @@ function measurement( firstCardPaintEpochMs: 2_650, idle: { domMutations: 12, - endEpochMs: 36_000.04, - startEpochMs: 6_000, + endEpochMs: 33_200.04, + startEpochMs: 3_200, status: 'done', ticks: 31, }, @@ -198,7 +198,7 @@ test('maps the probe, IPC capture and main counters to exact counters and spawn- assert.deepEqual(record.evidence['idle'], { domMutations: 12, durationMs: 30_000, - settledToIdleStartMs: 2_819.9, + settledToIdleStartMs: 19.9, }); }); @@ -278,14 +278,32 @@ test('refuses a launch without a complete, on-time idle window after the settle ), /idle-before-settle/ ); + // The settle point is 3_180.06: a window opened 120 ms after it left + // ticks uncounted in between. assert.throws( () => - toLaunchIterationRecord(0, false, withIdle({ endEpochMs: 35_000 })), + toLaunchIterationRecord( + 0, + false, + withIdle({ endEpochMs: 33_300.1, startEpochMs: 3_300.1 }) + ), + /idle-start-late/ + ); + assert.doesNotThrow(() => + toLaunchIterationRecord( + 0, + false, + withIdle({ endEpochMs: 33_250, startEpochMs: 3_250 }) + ) + ); + assert.throws( + () => + toLaunchIterationRecord(0, false, withIdle({ endEpochMs: 33_000 })), /idle-window-short/ ); assert.throws( () => - toLaunchIterationRecord(0, false, withIdle({ endEpochMs: 37_500 })), + toLaunchIterationRecord(0, false, withIdle({ endEpochMs: 34_500 })), /idle-window-late/ ); assert.equal( @@ -332,7 +350,7 @@ test('refuses a launch whose settle window did not end after the first-card cuto const capped = toLaunchIterationRecord( 0, false, - withSettle({ epochMs: 5_650, status: 'cap' }) + withSettle({ epochMs: 3_150, status: 'cap' }) ); assert.equal( (capped.evidence['settle'] as { reason: string }).reason, diff --git a/apps/electron-backend-e2e/src/performance/launch-journey-record.ts b/apps/electron-backend-e2e/src/performance/launch-journey-record.ts index 085920135..3dbf07e41 100644 --- a/apps/electron-backend-e2e/src/performance/launch-journey-record.ts +++ b/apps/electron-backend-e2e/src/performance/launch-journey-record.ts @@ -50,6 +50,14 @@ export const LAUNCH_JOURNEY_UNAVAILABLE_COUNTERS: Readonly< */ export const LAUNCH_JOURNEY_IDLE_LATE_TOLERANCE_MS = 1_000; +/** + * The idle window opens in the settle timer's callback, but the settle point + * is that timer's deadline. A callback that ran later than this left ticks + * between the two outside both windows, and means the page was still busy + * at the settle point, so the iteration is refused rather than undercounted. + */ +export const LAUNCH_JOURNEY_IDLE_START_TOLERANCE_MS = 100; + export interface LaunchJourneyMeasurement { readonly electronVersion: string; readonly gate: JourneyRendererGateState; @@ -230,6 +238,12 @@ function assertLaunchIdleWindow( if (idle.startEpochMs < settledEpochMs) { throw new Error('launch-journey-record-idle-before-settle'); } + if ( + idle.startEpochMs - settledEpochMs > + LAUNCH_JOURNEY_IDLE_START_TOLERANCE_MS + ) { + throw new Error('launch-journey-record-idle-start-late'); + } const durationMs = idle.endEpochMs - idle.startEpochMs; // Timers may fire up to a millisecond early after clamping. if (durationMs < JOURNEY_IDLE_WINDOW_MS - 1) { diff --git a/docs/architecture/performance-journeys.md b/docs/architecture/performance-journeys.md index eb13b9858..a0544dcab 100644 --- a/docs/architecture/performance-journeys.md +++ b/docs/architecture/performance-journeys.md @@ -201,9 +201,13 @@ zoneless change detection removes. The window opens when the settle window closes, so startup data still landing is not idle work, and it is timed by a renderer `setTimeout`. The record refuses an iteration whose window opened before the settle point or +more than 100 ms after it (`launch-journey-record-idle-start-late`), or lasted less than 30 s, or more than 1 s longer -(`launch-journey-record-idle-window-late`): a timer that late means the -page was busy, not idle. `evidence.idle` keeps the measured `durationMs` and +(`launch-journey-record-idle-window-late`). The window opens in the settle +timer's callback while the settle point is that timer's deadline, so a late +callback would leave ticks between the two outside both windows; either late +timer means the page was busy, not idle. Locally the window opened 1-4 ms +after the settle point. `evidence.idle` keeps the measured `durationMs` and `settledToIdleStartMs`. The main-process counters and the IPC capture are read after the window, which does not move them: they are frozen earlier. J2's launches skip the window (`runLaunchJourney` with `idleWindowMs: null`),