mirror of
https://github.com/getpaseo/paseo.git
synced 2026-10-30 13:13:25 +00:00
test(server): keep loader messages out of config responsiveness probes
This commit is contained in:
@@ -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 }));
|
||||
|
||||
Reference in New Issue
Block a user