mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-08 17:06:15 -08:00
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:
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user