From 1aef1ec6625f2c32cbd62cfbeb02cffb43a275d2 Mon Sep 17 00:00:00 2001 From: "paseo-bot[bot]" <266920839+paseo-bot[bot]@users.noreply.github.com> Date: Thu, 29 Oct 2026 12:21:29 +0000 Subject: [PATCH] test(server): isolate timeline fetch responsiveness from runner work (#8197) * test(server): isolate timeline fetch responsiveness from runner work Measure the existing cold tail workload in a separate process after fixture collection. Keep the 50ms gate and row assertions, and reproduce unrelated callback CPU contaminating the old measurement. * test(server): keep timeline probe private and exercise overlapping CPU --------- Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com> --- .../server/agent/agent-timeline-store.test.ts | 61 ++++++++++++++----- .../test-utils/timeline-responsiveness.ts | 25 ++++++++ 2 files changed, 71 insertions(+), 15 deletions(-) create mode 100644 packages/server/src/server/agent/test-utils/timeline-responsiveness.ts diff --git a/packages/server/src/server/agent/agent-timeline-store.test.ts b/packages/server/src/server/agent/agent-timeline-store.test.ts index 4baaf89edb..5d3c836e65 100644 --- a/packages/server/src/server/agent/agent-timeline-store.test.ts +++ b/packages/server/src/server/agent/agent-timeline-store.test.ts @@ -1,4 +1,6 @@ -import { measureMainThreadWork } from "../../test-utils/main-thread-work.js"; +import { execFile } from "node:child_process"; +import { promisify } from "node:util"; +import { fileURLToPath } from "node:url"; import { describe, expect, it } from "vitest"; import { selectReferenceProjectedTimelinePage } from "./test-utils/timeline-page-reference.js"; import type { AgentTimelineItem } from "./agent-sdk-types.js"; @@ -251,23 +253,52 @@ it("includes transitive tool updates in a contiguous projected tail", () => { expect(tail).toMatchObject({ startSeq: 1, endSeq: 6, hasOlder: false }); }); +async function measureTimelineTail(wholeTurns: boolean) { + const fixture = fileURLToPath( + new URL("./test-utils/timeline-responsiveness.ts", import.meta.url), + ); + const { stdout, stderr } = await promisify(execFile)( + process.execPath, + ["--expose-gc", "--import", "tsx", fixture, String(wholeTurns)], + { timeout: 15_000 }, + ); + process.stderr.write(stderr); + return JSON.parse(stdout) as { + result: ReturnType; + blockingMs: number; + }; +} + +it("excludes unrelated test-runner CPU from the timeline fetch budget", async () => { + let unrelatedWork: ReturnType; + const burnCpu = () => { + const threadCpuUsage: (previous?: NodeJS.CpuUsage) => NodeJS.CpuUsage = Reflect.get( + process, + "threadCpuUsage", + ); + const start = threadCpuUsage(); + let elapsed: NodeJS.CpuUsage; + do { + elapsed = threadCpuUsage(start); + } while (elapsed.user + elapsed.system < 80_000); + unrelatedWork = setImmediate(burnCpu); + }; + unrelatedWork = setImmediate(burnCpu); + try { + const { result: tail, blockingMs } = await measureTimelineTail(false); + expect(tail.rows).toHaveLength(200); + expect(tail.startSeq).toBe(99_801); + expect(tail.endSeq).toBe(100_000); + expect(blockingMs).toBeLessThan(50); + } finally { + clearImmediate(unrelatedWork); + } +}); + it.each([false, true])( "serves tail 200 from 100,000 retained rows without stalling the loop (wholeTurns=%s)", async (wholeTurns) => { - const store = new InMemoryAgentTimelineStore(); - store.initialize("large"); - for (let index = 0; index < 100_000; index++) { - store.append("large", { type: "user_message", text: String(index) }); - } - const { result: tail, blockingMs } = await measureMainThreadWork( - `timeline tail main-thread work (wholeTurns=${wholeTurns})`, - () => { - let page = store.fetch("large", { limit: 200, wholeTurns }); - for (let repeat = 1; repeat < 8; repeat++) - page = store.fetch("large", { limit: 200, wholeTurns }); - return page; - }, - ); + const { result: tail, blockingMs } = await measureTimelineTail(wholeTurns); expect(tail.rows).toHaveLength(200); expect(tail.startSeq).toBe(99_801); expect(tail.endSeq).toBe(100_000); diff --git a/packages/server/src/server/agent/test-utils/timeline-responsiveness.ts b/packages/server/src/server/agent/test-utils/timeline-responsiveness.ts new file mode 100644 index 0000000000..bfe5ee59c4 --- /dev/null +++ b/packages/server/src/server/agent/test-utils/timeline-responsiveness.ts @@ -0,0 +1,25 @@ +import { setImmediate as nextTurn } from "node:timers/promises"; +import { measureMainThreadWork } from "../../../test-utils/main-thread-work.js"; +import { InMemoryAgentTimelineStore } from "../agent-timeline-store.js"; + +// Measure a cold fetch on its own thread, without Vitest reporting or callbacks. +const wholeTurns = process.argv[2] === "true"; +const store = new InMemoryAgentTimelineStore(); +store.initialize("large"); +for (let index = 0; index < 100_000; index++) { + store.append("large", { type: "user_message", text: String(index) }); +} +// Finish fixture allocation and collect its temporary garbage before measuring. +globalThis.gc!(); +await nextTurn(); +const measured = await measureMainThreadWork( + `timeline tail main-thread work (wholeTurns=${wholeTurns})`, + () => { + let page = store.fetch("large", { limit: 200, wholeTurns }); + for (let repeat = 1; repeat < 8; repeat++) + page = store.fetch("large", { limit: 200, wholeTurns }); + return page; + }, +); + +process.stdout.write(JSON.stringify(measured));