Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 23 additions & 5 deletions libs/act-pg/bench/notify-perf.scenario.bench.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,10 @@ async function setupReader(notifyEnabled: boolean, rec: LatencyRecorder) {
.build();

let pollTimer: NodeJS.Timeout | undefined;
// The pump is best-effort — the pool can close mid-flight at teardown —
// but a swallowed error is also the only trace left when every pass
// fails, so the last one is kept for the report.
const pump: { errors: number; last?: unknown } = { errors: 0 };
if (!notifyEnabled) {
// Tear down the auto-wired notify subscription and drive reactions
// with an explicit correlate→drain pump — the classic poll-based
Expand All @@ -120,23 +124,24 @@ async function setupReader(notifyEnabled: boolean, rec: LatencyRecorder) {
try {
await reader.correlate({ limit: 100 });
await reader.drain({ streamLimit: 50, eventLimit: 100 });
} catch {
// best-effort — pool may close mid-flight at teardown
} catch (e) {
pump.errors++;
pump.last = e;
}
}, POLL_INTERVAL_MS);
} else {
reader.on("notified", () => reader.settle({ debounceMs: 0 }));
}

return { reader, pollTimer };
return { reader, pollTimer, pump };
}

async function runScenario(
prefix: string,
notifyEnabled: boolean
): Promise<number[]> {
const rec = recorder();
const { reader, pollTimer } = await setupReader(notifyEnabled, rec);
const { reader, pollTimer, pump } = await setupReader(notifyEnabled, rec);
const writer = new PostgresStore({
port: PORT,
schema: SCHEMA,
Expand All @@ -162,7 +167,20 @@ async function runScenario(
await new Promise((r) =>
setTimeout(r, notifyEnabled ? 500 : POLL_INTERVAL_MS * 8)
);
return rec.drain();
const samples = rec.drain();
// An arm that recorded nothing is not a slow arm, it is a run that
// never happened — and the percentile helper reports an empty sample
// set as `0 ms`, which reads as "infinitely fast" in the comparison
// below. Fail here instead, where the cause can still be named.
if (samples.length === 0)
throw new Error(
`The ${prefix} arm recorded 0 of ${COMMITS} reactions. ` +
(pump.errors
? `The poll pump failed ${pump.errors} times; last error: ${pump.last}`
: "Nothing failed, so the reader never got to run — " +
"most likely starved of CPU by a bench running alongside it.")
);
return samples;
} finally {
if (pollTimer) clearInterval(pollTimer);
reader.stop_correlations();
Expand Down
9 changes: 9 additions & 0 deletions vitest.bench.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,15 @@ import base from "./vite.config.js";
export default mergeConfig(base, {
test: {
include: ["libs/*/bench/**/*.scenario.bench.ts"],
// One bench file at a time. These measure wall-clock latency, and as
// concurrent workers they measure each other instead: a CPU-bound
// bench next door starves a 50ms polling pump into collecting no
// samples at all, and inflates every percentile the latency benches
// assert on — one CI run reported an idle p95 of 516 ms against 7.6 ms
// for the same code run alone. Serializing costs about 30% wall time
// (the overlap parallelism was buying), which is the right trade when
// the output feeds PERFORMANCE.md: slow numbers beat contaminated ones.
fileParallelism: false,
// Default reporter hides `console.log` from passing tests, so the
// `console.table` blocks scenario benches emit never reach stdout
// (and CI's step-summary parser captures nothing).
Expand Down
Loading