From 6f0477c3f5f16b681e99828dcd459ec888a11e97 Mon Sep 17 00:00:00 2001 From: 4gray Date: Tue, 29 Sep 2026 17:58:20 +0200 Subject: [PATCH] test(perf): sample both J2 settle captures in one main-process snapshot The settle sample read J1's capture (pending calls) and J2's capture (call count) in two evaluate calls, so a call starting in between was counted with a stale zero in flight and its completion went unseen. Both states are now read in one synchronous pass, where no ipcMain event can be handled in between. Co-Authored-By: Claude Opus 5.5 --- .../src/journeys/open-source-journey-app.ts | 20 +++++----- .../journey-main-ipc-capture.spec.ts | 37 +++++++++++++++++++ .../performance/journey-main-ipc-capture.ts | 33 +++++++++++++---- 3 files changed, 71 insertions(+), 19 deletions(-) diff --git a/apps/electron-backend-e2e/src/journeys/open-source-journey-app.ts b/apps/electron-backend-e2e/src/journeys/open-source-journey-app.ts index 5273d385d..15a7295ed 100644 --- a/apps/electron-backend-e2e/src/journeys/open-source-journey-app.ts +++ b/apps/electron-backend-e2e/src/journeys/open-source-journey-app.ts @@ -7,7 +7,7 @@ import { installJourneyMainIpcCapture, JOURNEY_MAIN_IPC_STATE_KEY, JOURNEY_RENDERER_API_TRACE_CHANNEL, - peekJourneyMainIpcCapture, + peekJourneyMainIpcCaptures, readJourneyMainIpcCapture, } from '../performance/journey-main-ipc-capture'; import type { JourneyMockRequestLedger } from '../performance/journey-mock-request-ledger'; @@ -90,10 +90,13 @@ async function waitForQuiet( }> { const armMark = ledger.mark(); const sample = async (): Promise => { - const launchCapture = await peekJourneyMainIpcCapture( - electronApp, - JOURNEY_MAIN_IPC_STATE_KEY - ); + // Both captures in one snapshot: a call counted by J2's capture is + // then also pending in J1's, never counted with a stale in-flight 0. + const [launchCapture, openSourceCapture] = + await peekJourneyMainIpcCaptures(electronApp, [ + JOURNEY_MAIN_IPC_STATE_KEY, + OPEN_SOURCE_JOURNEY_MAIN_IPC_STATE_KEY, + ]); if (launchCapture.unmatchedCompletions > 0) { throw new Error('open-source-journey-bridge-completions-unmatched'); } @@ -101,12 +104,7 @@ async function waitForQuiet( domMutations: await readPreStartMutations(page, probeStateKey), httpInFlight: ledger.inFlight(), httpRequests: ledger.mark(), - ipcCalls: ( - await peekJourneyMainIpcCapture( - electronApp, - OPEN_SOURCE_JOURNEY_MAIN_IPC_STATE_KEY - ) - ).callsBeforeStart, + ipcCalls: openSourceCapture.callsBeforeStart, ipcInFlight: countJourneyMainIpcInFlight(launchCapture), }; }; diff --git a/apps/electron-backend-e2e/src/performance/journey-main-ipc-capture.spec.ts b/apps/electron-backend-e2e/src/performance/journey-main-ipc-capture.spec.ts index 219bc041f..00633261b 100644 --- a/apps/electron-backend-e2e/src/performance/journey-main-ipc-capture.spec.ts +++ b/apps/electron-backend-e2e/src/performance/journey-main-ipc-capture.spec.ts @@ -13,6 +13,7 @@ import { installJourneyMainIpcCapture, JOURNEY_RENDERER_API_TRACE_CHANNEL, peekJourneyMainIpcCapture, + peekJourneyMainIpcCaptures, type JourneyMainIpcCaptureOptions, type JourneyMainIpcCaptureState, } from './journey-main-ipc-capture'; @@ -372,3 +373,39 @@ test('a detached capture stops listening and keeps its last state', async () => ); }); }); + +test('reads several captures in one main-process pass', async () => { + const fake = createFakeElectronApp(); + let evaluations = 0; + const counting = { + evaluate: (...args: Parameters) => { + evaluations += 1; + return fake.app.evaluate(...args); + }, + } as unknown as ElectronApplication; + const target = globalThis as unknown as Record; + const [first, second] = ['__journeyPeekA', '__journeyPeekB']; + target[first] = { callsBeforeStart: 1 }; + target[second] = { callsBeforeStart: 2 }; + try { + const states = await peekJourneyMainIpcCaptures(counting, [ + first, + second, + ]); + assert.equal(evaluations, 1); + assert.deepEqual( + states.map((state) => state.callsBeforeStart), + [1, 2] + ); + await assert.rejects( + peekJourneyMainIpcCaptures(counting, [ + first, + '__journeyPeekMissing', + ]), + /capture-missing/ + ); + } finally { + delete target[first]; + delete target[second]; + } +}); diff --git a/apps/electron-backend-e2e/src/performance/journey-main-ipc-capture.ts b/apps/electron-backend-e2e/src/performance/journey-main-ipc-capture.ts index 8fc3d7c7f..a8c13ad6d 100644 --- a/apps/electron-backend-e2e/src/performance/journey-main-ipc-capture.ts +++ b/apps/electron-backend-e2e/src/performance/journey-main-ipc-capture.ts @@ -213,20 +213,37 @@ export async function peekJourneyMainIpcCapture( electronApp: ElectronApplication, stateKey: string ): Promise { - const state = (await electronApp.evaluate( - (_electron, key) => + const [state] = await peekJourneyMainIpcCaptures(electronApp, [stateKey]); + return state as JourneyMainIpcCaptureState; +} + +/** + * Several captures read in one synchronous pass in the main process. No + * `ipcMain` event can be handled in between, so the states are one coherent + * snapshot: a call counted by one capture is also pending in the other. + */ +export async function peekJourneyMainIpcCaptures( + electronApp: ElectronApplication, + stateKeys: readonly string[] +): Promise { + const states = (await electronApp.evaluate( + (_electron, keys) => JSON.parse( JSON.stringify( - (globalThis as unknown as Record)[key] ?? - null + keys.map( + (key) => + (globalThis as unknown as Record)[ + key + ] ?? null + ) ) ) as unknown, - stateKey - )) as JourneyMainIpcCaptureState | null; - if (!state) { + [...stateKeys] + )) as (JourneyMainIpcCaptureState | null)[]; + if (states.some((state) => !state)) { throw new Error('journey-main-ipc-capture-missing'); } - return state; + return states as JourneyMainIpcCaptureState[]; } export async function readJourneyMainIpcCapture(