test(perf): restart the J2 quiet period when pending work completes

Both waits in the open-source journey (settling before the click, closing
the mock window after the terminal) now use one waitForJourneyQuiet
helper that compares whole samples, in-flight counts included. The poll
that first sees a request or bridge call complete restarts the quiet
period, so the window is never measured from a poll at which work was
still pending. A fake-clock spec covers the in-flight to zero case.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
4grayandClaude Opus 5.5 committed 2026-09-29 08:58:36 +02:00
1 parent 27e989f204
commit 9bfd47df04
3 files changed
+156 -47

No files matched your search

@@ -10,6 +10,7 @@ import {
readJourneyMainIpcCapture,
} from '../performance/journey-main-ipc-capture';
import type { JourneyMockRequestLedger } from '../performance/journey-mock-request-ledger';
import { waitForJourneyQuiet } from '../performance/journey-quiet-wait';
import {
armJourneyRendererProbe,
createOpenSourceJourneyProbeOptions,
@@ -76,7 +77,6 @@ async function waitForQuiet(
ledger: JourneyMockRequestLedger,
probeStateKey: string
): Promise<OpenSourceJourneySettle> {
const startedMs = Date.now();
const armMark = ledger.mark();
const sample = async (): Promise<ActivitySample> => {
const launchCapture = await peekJourneyMainIpcCapture(
@@ -99,36 +99,24 @@ async function waitForQuiet(
ipcInFlight: countJourneyMainIpcInFlight(launchCapture),
};
};
let last = await sample();
let quietSinceMs = Date.now();
for (;;) {
await new Promise((resolve) => setTimeout(resolve, POLL_MS));
const next = await sample();
const now = Date.now();
if (
next.domMutations !== last.domMutations ||
next.httpInFlight > 0 ||
next.httpRequests !== last.httpRequests ||
next.ipcCalls !== last.ipcCalls ||
next.ipcInFlight > 0
) {
last = next;
quietSinceMs = now;
} else if (now - quietSinceMs >= QUIET_MS) {
return {
preStartDomMutations: next.domMutations,
preStartHttpRequests: next.httpRequests - armMark,
preStartIpcCalls: next.ipcCalls,
quietMs: QUIET_MS,
waitedMs: now - startedMs,
};
}
if (now - startedMs > SETTLE_TIMEOUT_MS) {
throw new Error(
`open-source-journey-not-quiet: ${JSON.stringify(next)}`
);
}
}
const { sample: quiet, waitedMs } = await waitForJourneyQuiet({
inFlight: (activity) => activity.httpInFlight + activity.ipcInFlight,
pollMs: POLL_MS,
quietMs: QUIET_MS,
sample,
timeoutError: (activity) =>
new Error(
`open-source-journey-not-quiet: ${JSON.stringify(activity)}`
),
timeoutMs: SETTLE_TIMEOUT_MS,
});
return {
preStartDomMutations: quiet.domMutations,
preStartHttpRequests: quiet.httpRequests - armMark,
preStartIpcCalls: quiet.ipcCalls,
quietMs: QUIET_MS,
waitedMs,
};
}
/**
@@ -139,22 +127,17 @@ async function waitForQuiet(
async function waitForMockQuiet(
ledger: JourneyMockRequestLedger
): Promise<void> {
const startedMs = Date.now();
let count = ledger.mark();
let quietSinceMs = Date.now();
for (;;) {
await new Promise((resolve) => setTimeout(resolve, POLL_MS));
const now = Date.now();
if (ledger.mark() !== count || ledger.inFlight() > 0) {
count = ledger.mark();
quietSinceMs = now;
} else if (now - quietSinceMs >= QUIET_MS) {
return;
}
if (now - startedMs > SETTLE_TIMEOUT_MS) {
throw new Error('open-source-journey-mock-not-quiet');
}
}
await waitForJourneyQuiet({
inFlight: (activity) => activity.inFlight,
pollMs: POLL_MS,
quietMs: QUIET_MS,
sample: async () => ({
inFlight: ledger.inFlight(),
requests: ledger.mark(),
}),
timeoutError: () => new Error('open-source-journey-mock-not-quiet'),
timeoutMs: SETTLE_TIMEOUT_MS,
});
}
/**
@@ -0,0 +1,72 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import { waitForJourneyQuiet } from './journey-quiet-wait';
interface Activity {
readonly inFlight: number;
readonly requests: number;
}
/**
* Runs the waiter on a fake clock: each poll advances time by `pollMs`, and
* `script(ms)` says what the app looks like at that time.
*/
async function run(
script: (atMs: number) => Activity,
quietMs = 1_000,
timeoutMs = 30_000
) {
let clock = 0;
const polls: number[] = [];
const result = await waitForJourneyQuiet<Activity>({
inFlight: (activity) => activity.inFlight,
now: () => clock,
pollMs: 100,
quietMs,
sample: async () => {
polls.push(clock);
return script(clock);
},
sleep: async (ms) => {
clock += ms;
},
timeoutError: (activity) =>
new Error(`not-quiet ${JSON.stringify(activity)}`),
timeoutMs,
});
return { ...result, polls };
}
test('returns once nothing changed for the whole quiet period', async () => {
const result = await run(() => ({ inFlight: 0, requests: 3 }));
assert.equal(result.waitedMs, 1_000);
assert.deepEqual(result.sample, { inFlight: 0, requests: 3 });
});
test('restarts the quiet period at every new request', async () => {
const result = await run((atMs) => ({
inFlight: 0,
requests: atMs < 500 ? 1 : 2,
}));
// The second request is first seen at 500 ms.
assert.equal(result.waitedMs, 1_500);
});
test('counts the quiet period from the poll that saw pending work complete', async () => {
// One request is in flight until 750 ms; the count never changes. The
// poll at 800 ms is the first to see it done, so the app is quiet only
// from 800 ms on, not from the 700 ms poll at which it was pending.
const result = await run((atMs) => ({
inFlight: atMs < 750 ? 1 : 0,
requests: 1,
}));
assert.equal(result.waitedMs, 1_800);
});
test('never returns while work stays in flight and fails at the timeout', async () => {
await assert.rejects(
run(() => ({ inFlight: 1, requests: 1 }), 1_000, 3_000),
/not-quiet \{"inFlight":1,"requests":1\}/
);
});
@@ -0,0 +1,54 @@
/**
* Polls activity until nothing has changed for `quietMs` and nothing is in
* flight. Used by the click-started journeys to settle the app before the
* click and to close the mock request window after the terminal.
*
* A sample is compared as a whole, in-flight counts included, so the poll
* that first sees pending work complete restarts the quiet period: the
* window is always at least `quietMs` after the last observed activity,
* never measured from a poll at which work was still pending.
*/
export interface JourneyQuietWaitOptions<T> {
/** Requests or calls still pending in a sample; any makes it busy. */
readonly inFlight: (sample: T) => number;
readonly now?: () => number;
readonly pollMs: number;
readonly quietMs: number;
readonly sample: () => Promise<T>;
readonly sleep?: (ms: number) => Promise<void>;
readonly timeoutError: (last: T) => Error;
readonly timeoutMs: number;
}
export interface JourneyQuietWaitResult<T> {
readonly sample: T;
readonly waitedMs: number;
}
export async function waitForJourneyQuiet<T>(
options: JourneyQuietWaitOptions<T>
): Promise<JourneyQuietWaitResult<T>> {
const now = options.now ?? Date.now;
const sleep =
options.sleep ??
((ms: number) =>
new Promise<void>((resolve) => setTimeout(resolve, ms)));
const startedMs = now();
let lastKey = JSON.stringify(await options.sample());
let quietSinceMs = now();
for (;;) {
await sleep(options.pollMs);
const next = await options.sample();
const nextKey = JSON.stringify(next);
const sampledMs = now();
if (nextKey !== lastKey || options.inFlight(next) > 0) {
lastKey = nextKey;
quietSinceMs = sampledMs;
} else if (sampledMs - quietSinceMs >= options.quietMs) {
return { sample: next, waitedMs: sampledMs - startedMs };
}
if (sampledMs - startedMs > options.timeoutMs) {
throw options.timeoutError(next);
}
}
}