test(server): keep loader messages out of config responsiveness probes

This commit is contained in:
paseo-bot[bot]
2026-10-29 15:36:28 +00:00
parent 5ab6ff5676
commit 6db22f79e8
3 changed files with 66 additions and 45 deletions
@@ -24,8 +24,6 @@ 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;
@@ -48,10 +46,6 @@ 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++;
@@ -66,13 +60,7 @@ export async function measureMainThreadWork<T>(label: string, action: () => T |
const start = callbacks.get(id);
if (!start) return;
callbacks.delete(id);
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",
);
}
record(performance.now() - start.wall, threadCpuUsage(start.cpu));
},
}).enable();
const sample = () => {
@@ -5,7 +5,8 @@ import { fileURLToPath } from "node:url";
import { writeFile, truncate, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
import { build } from "esbuild";
import { isPlatform } from "../test-utils/platform.js";
import {
readPaseoConfig,
@@ -20,21 +21,40 @@ import {
writePaseoConfigForEdit,
} from "./paseo-config-file.js";
let probeDirectory: string;
let probePath: string;
beforeAll(async () => {
probeDirectory = mkdtempSync(join(tmpdir(), "paseo-config-probe-"));
probePath = join(probeDirectory, "config-responsiveness.mjs");
// Compile before starting the measured process. A runtime TypeScript loader
// owns MessagePorts that the responsiveness probe otherwise charges as work.
await build({
entryPoints: [
fileURLToPath(new URL("./test-fixtures/config-responsiveness.ts", import.meta.url)),
],
outfile: probePath,
bundle: true,
platform: "node",
format: "esm",
});
});
afterAll(() => {
rmSync(probeDirectory, { recursive: true, force: true });
});
async function measureConfigRead(directory: string) {
const fixture = fileURLToPath(
new URL("./test-fixtures/config-responsiveness.ts", import.meta.url),
);
const { stdout, stderr } = await promisify(execFile)(
process.execPath,
["--import", "tsx", fixture, directory],
{ timeout: 15_000 },
);
const { stdout, stderr } = await promisify(execFile)(process.execPath, [probePath, directory], {
timeout: 15_000,
});
process.stderr.write(stderr);
return JSON.parse(stdout) as {
result: Awaited<ReturnType<typeof readPaseoConfigForEdit>>;
blockingMs: number;
filesystemRequests: number;
yields: number;
workerMessages: number;
};
}
@@ -84,7 +104,7 @@ describe("paseo config file substrate", () => {
);
});
it("excludes unrelated test-runner CPU from the config read budget", async () => {
it("excludes test-runner and loader activity from the config read budget", async () => {
await writeFile(join(tempDir, "paseo.json"), JSON.stringify({ worktree: { setup: "npm ci" } }));
const unrelatedWork = new Promise<void>((resolve) => {
setImmediate(() => {
@@ -100,8 +120,10 @@ describe("paseo config file substrate", () => {
resolve();
});
});
const { result, blockingMs, filesystemRequests } = await measureConfigRead(tempDir);
const { result, blockingMs, filesystemRequests, workerMessages } =
await measureConfigRead(tempDir);
await unrelatedWork;
expect(workerMessages).toBe(0);
expect(result).toEqual({
ok: true,
config: { worktree: { setup: "npm ci" } },
@@ -111,32 +133,33 @@ describe("paseo config file substrate", () => {
expect(blockingMs).toBeLessThan(20);
});
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("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, workerMessages } =
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);
expect(workerMessages).toBe(0);
});
it("rejects a 32MiB config with main-thread work below 20ms", async () => {
await writeFile(
join(tempDir, "paseo.json"),
JSON.stringify({ extra: "x".repeat(32 * 1024 * 1024) }),
);
const { result, blockingMs, filesystemRequests } = await measureConfigRead(tempDir);
const { result, blockingMs, filesystemRequests, workerMessages } =
await measureConfigRead(tempDir);
expect(blockingMs).toBeLessThan(20);
expect(filesystemRequests).toBeGreaterThan(0);
expect(workerMessages).toBe(0);
expect(result).toEqual({ ok: false, error: { code: "invalid_project_config" } });
});
@@ -1,14 +1,24 @@
import { createHook, executionAsyncResource } from "node:async_hooks";
import { MessagePort } from "node:worker_threads";
import assert from "node:assert/strict";
import { setImmediate as nextTurn } from "node:timers/promises";
import { measureMainThreadWork } from "../../test-utils/main-thread-work.js";
import { readPaseoConfigForEdit } from "../paseo-config-file.js";
// Keep Vitest transforms, reporting, and unrelated callbacks outside the measured
// thread. Read the real file once, without warming the config snapshot or schema.
// The parent bundles this fixture before launching plain Node, keeping test
// runner callbacks and runtime loader messages outside the measured thread.
// Read the real file once, without warming the config snapshot or schema.
const directory = process.argv[2];
assert.ok(directory, "Config directory is required");
await nextTurn();
let workerMessages = 0;
const activity = createHook({
before() {
if (executionAsyncResource() instanceof MessagePort) workerMessages++;
},
}).enable();
const measured = await measureMainThreadWork("project config main-thread work", () =>
readPaseoConfigForEdit(directory),
);
process.stdout.write(JSON.stringify(measured));
activity.disable();
process.stdout.write(JSON.stringify({ ...measured, workerMessages }));