test(server): trace recurring config responsiveness failures

This commit is contained in:
paseo-bot[bot]
2026-10-29 15:09:21 +00:00
parent 1aef1ec662
commit 5ab6ff5676
2 changed files with 30 additions and 15 deletions
@@ -24,6 +24,8 @@ export async function measureMainThreadWork<T>(label: string, action: () => T |
let previousCpu = threadCpuUsage();
let probe: ReturnType<typeof setInterval>;
let drain: ReturnType<typeof setImmediate>;
const types = new Map<number, string>();
const stacks = new Map<number, string>();
const callbacks = new Map<number, { wall: number; cpu: NodeJS.CpuUsage }>();
const record = (wallMs: number, cpu: NodeJS.CpuUsage) => {
const cpuMs = (cpu.user + cpu.system) / 1_000;
@@ -46,6 +48,10 @@ export async function measureMainThreadWork<T>(label: string, action: () => T |
gc.observe({ entryTypes: ["gc"] });
const activity = createHook({
init(_id, type) {
if (label === "project config main-thread work") {
types.set(_id, type);
stacks.set(_id, new Error().stack ?? "");
}
if (type.startsWith("FSREQ")) filesystemRequests++;
if (!draining && type === "Immediate") yields++;
if (type === "WORKER") workerEvents++;
@@ -60,7 +66,13 @@ export async function measureMainThreadWork<T>(label: string, action: () => T |
const start = callbacks.get(id);
if (!start) return;
callbacks.delete(id);
record(performance.now() - start.wall, threadCpuUsage(start.cpu));
const wall = performance.now() - start.wall;
record(wall, threadCpuUsage(start.cpu));
if (wall > 10 && label === "project config main-thread work") {
process.stderr.write(
JSON.stringify({ wall, type: types.get(id), stack: stacks.get(id) }) + "\n",
);
}
},
}).enable();
const sample = () => {
@@ -111,20 +111,23 @@ describe("paseo config file substrate", () => {
expect(blockingMs).toBeLessThan(20);
});
it("keeps main-thread work below 20ms while admitting large configs", async () => {
const config = {
scripts: Object.fromEntries(
Array.from({ length: 7_000 }, (_, i) => [`script${i}`, { command: "echo hi" }]),
),
worktree: { setup: "npm ci" },
};
await writeFile(join(tempDir, "paseo.json"), JSON.stringify(config));
const { result, blockingMs, filesystemRequests, yields } = await measureConfigRead(tempDir);
expect(result).toEqual({ ok: true, config, revision: await statPaseoConfigPath(tempDir) });
expect(blockingMs).toBeLessThan(20);
expect(filesystemRequests).toBeGreaterThan(0);
expect(yields).toBeGreaterThan(0);
});
it.each(Array.from({ length: 25 }, (_, i) => i))(
"keeps main-thread work below 20ms while admitting large configs (%i)",
async () => {
const config = {
scripts: Object.fromEntries(
Array.from({ length: 7_000 }, (_, i) => [`script${i}`, { command: "echo hi" }]),
),
worktree: { setup: "npm ci" },
};
await writeFile(join(tempDir, "paseo.json"), JSON.stringify(config));
const { result, blockingMs, filesystemRequests, yields } = await measureConfigRead(tempDir);
expect(result).toEqual({ ok: true, config, revision: await statPaseoConfigPath(tempDir) });
expect(blockingMs).toBeLessThan(20);
expect(filesystemRequests).toBeGreaterThan(0);
expect(yields).toBeGreaterThan(0);
},
);
it("rejects a 32MiB config with main-thread work below 20ms", async () => {
await writeFile(