Files
openclaw/test/scripts/bench-task-registry-sqlite.test.ts
Vincent Koc a3b0982148 perf(tasks): benchmark durable task registration churn (#119828)
* perf(tasks): add durable registry churn benchmark

* test(tasks): cover registry churn benchmark contract

* chore(tasks): wire registry churn benchmark
2026-08-06 18:37:30 +08:00

399 lines
12 KiB
TypeScript

import { spawnSync } from "node:child_process";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { describe, expect, it } from "vitest";
import {
testing,
WORKER_RESULT_SENTINEL,
type MemorySample,
type WorkerResult,
} from "../../scripts/bench-task-registry-sqlite.ts";
function memorySample(cycle: number): MemorySample {
return {
cycle,
heapUsedBytes: 100 + cycle,
heapTotalBytes: 150 + cycle,
rssBytes: 200 + cycle,
externalBytes: 30 + cycle,
arrayBuffersBytes: 10 + cycle,
processPeakRssBytes: 250 + cycle,
};
}
function workerResult(size: number, cycles = 3, warmup = 1): WorkerResult {
return {
size,
timingsMs: {
registration: Array.from({ length: cycles }, (_, index) => index + 1),
terminal: Array.from({ length: cycles }, (_, index) => index + 2),
teardown: Array.from({ length: cycles }, (_, index) => index + 3),
},
memory: {
postGcBaseline: memorySample(-1),
postGcSamples: Array.from({ length: cycles }, (_, index) => memorySample(index)),
retainedSlopesBytesPerCycle: {
heapUsedBytes: 1,
heapTotalBytes: 1,
rssBytes: 1,
externalBytes: 1,
arrayBuffersBytes: 1,
},
retainedDeltasBytes: {
heapUsedBytes: cycles,
heapTotalBytes: cycles,
rssBytes: cycles,
externalBytes: cycles,
arrayBuffersBytes: cycles,
},
processPeakRssBytes: 300,
},
invariant: {
ok: true,
cyclesValidated: cycles + warmup,
registration: {
memory: {
taskCount: size,
deliveryStateCount: size,
runningTasks: size,
succeededTasks: 0,
pendingDeliveryTasks: size,
succeededTerminalOutcomes: 0,
},
sqlite: {
taskCount: size,
deliveryStateCount: size,
runningTasks: size,
succeededTasks: 0,
pendingDeliveryTasks: size,
succeededTerminalOutcomes: 0,
},
},
terminal: {
memory: {
taskCount: size,
deliveryStateCount: size,
runningTasks: 0,
succeededTasks: size,
pendingDeliveryTasks: size,
succeededTerminalOutcomes: size,
},
sqlite: {
taskCount: size,
deliveryStateCount: size,
runningTasks: 0,
succeededTasks: size,
pendingDeliveryTasks: size,
succeededTerminalOutcomes: size,
},
},
teardown: {
memory: {
taskCount: 0,
deliveryStateCount: 0,
runningTasks: 0,
succeededTasks: 0,
pendingDeliveryTasks: 0,
succeededTerminalOutcomes: 0,
},
sqlite: {
taskCount: 0,
deliveryStateCount: 0,
runningTasks: 0,
succeededTasks: 0,
pendingDeliveryTasks: 0,
succeededTerminalOutcomes: 0,
},
},
serializedSharedConnection: true,
},
};
}
function workerStdout(result: WorkerResult): string {
return `${WORKER_RESULT_SENTINEL}${JSON.stringify(result)}\n`;
}
describe("durable task registry churn benchmark", () => {
it("parses bounded defaults and explicit options", () => {
expect(testing.parseOptions([])).toMatchObject({
sizes: [24, 64, 128],
cycles: 20,
warmup: 3,
});
expect(
testing.parseOptions([
"--sizes",
"2,4",
"--cycles",
"5",
"--warmup",
"0",
"--output",
"bench.json",
"--json",
]),
).toMatchObject({
sizes: [2, 4],
cycles: 5,
warmup: 0,
output: "bench.json",
json: true,
});
expect(() => testing.parseOptions(["--sizes", "2,2"])).toThrow(
"--sizes contains duplicate values",
);
expect(() => testing.parseOptions(["--cycles", "201"])).toThrow("--cycles must be at most 200");
expect(() => testing.parseOptions(["--wat"])).toThrow("Unknown argument: --wat");
});
it("always launches a fresh expose-gc worker for one size", () => {
const options = testing.parseOptions(["--sizes", "8", "--cycles", "2", "--warmup", "1"]);
const stateDir = path.join(os.tmpdir(), "benchmark-state");
expect(testing.buildWorkerArgs(options, 8, stateDir)).toEqual([
"--expose-gc",
"--import",
"tsx",
"scripts/bench-task-registry-sqlite-worker.ts",
"--size",
"8",
"--cycles",
"2",
"--warmup",
"1",
"--state-dir",
stateDir,
]);
});
it("removes the parent-owned worker state directory after a timeout", () => {
const options = testing.parseOptions(["--sizes", "2", "--cycles", "1", "--warmup", "0"]);
let stateDir: string | undefined;
expect(() =>
testing.runWorker(options, 2, {
spawnWorker: (_command, args) => {
const stateDirIndex = args.indexOf("--state-dir");
stateDir = args[stateDirIndex + 1];
if (!stateDir) {
throw new Error("missing worker state directory");
}
fs.writeFileSync(path.join(stateDir, "openclaw.sqlite"), "timeout fixture");
return {
status: null,
stdout: "",
stderr: "",
error: Object.assign(new Error("spawnSync timed out"), { code: "ETIMEDOUT" }),
};
},
}),
).toThrow("timed out after 300000ms");
expect(stateDir).toBeDefined();
expect(fs.existsSync(stateDir!)).toBe(false);
});
it("aggregates schema version 1 with raw memory samples and explicit interpretation", () => {
const options = testing.parseOptions(["--sizes", "2,4", "--cycles", "3", "--warmup", "1"]);
const report = testing.aggregateWorkerResults(options, [workerResult(2), workerResult(4)]);
expect(report).toMatchObject({
schemaVersion: 1,
benchmark: "durable-task-registry-churn",
model: {
unit: "subagent task-record registrations",
isolation: "fresh --expose-gc worker process per size",
workload:
"all task records start running with pending delivery, transition to succeeded with a succeeded terminal outcome, then delete",
},
options: { sizes: [2, 4], cycles: 3, warmup: 1 },
invariants: {
ok: true,
exactRegistrationTerminalAndTeardownState: true,
zeroRowsAfterEveryTeardown: true,
},
});
expect(report.interpretation.timings).toContain("advisory only");
expect(report.interpretation.memory).toContain("neither claim nor rule out a memory leak");
expect(report.sizes[0]?.memory.postGcBaseline.cycle).toBe(-1);
expect(report.sizes[0]?.memory.postGcSamples).toHaveLength(3);
expect(report.sizes[0]?.memory.retainedDeltasBytes.heapUsedBytes).toBe(3);
expect(report.sizes[0]?.memory.retainedSlopesBytesPerCycle).not.toHaveProperty(
"processPeakRssBytes",
);
expect(report.sizes[0]?.timingsMs.registration).toEqual({
count: 3,
min: 1,
p50: 2,
max: 3,
});
expect(JSON.stringify(report)).not.toContain(process.cwd());
});
it("rejects malformed, incomplete, failed, and timed-out worker results", () => {
const expected = { size: 2, cycles: 3, warmup: 1 };
const valid = workerResult(2);
expect(
testing.parseWorkerProcessResult(
{ status: 0, stdout: workerStdout(valid), stderr: "" },
expected,
),
).toEqual(valid);
expect(() =>
testing.parseWorkerProcessResult(
{ status: 0, stdout: `${WORKER_RESULT_SENTINEL}{\n`, stderr: "" },
expected,
),
).toThrow("returned invalid JSON");
expect(() =>
testing.parseWorkerProcessResult(
{ status: 0, stdout: workerStdout(workerResult(3)), stderr: "" },
expected,
),
).toThrow("mismatched identity");
expect(() =>
testing.parseWorkerProcessResult(
{
status: 0,
stdout: workerStdout({
...valid,
memory: { ...valid.memory, postGcSamples: valid.memory.postGcSamples.slice(1) },
}),
stderr: "",
},
expected,
),
).toThrow("invalid post-GC sample count");
expect(() =>
testing.parseWorkerProcessResult(
{
status: 0,
stdout: workerStdout({
...valid,
memory: {
...valid.memory,
retainedDeltasBytes: { ...valid.memory.retainedDeltasBytes, heapUsedBytes: 99 },
},
}),
stderr: "",
},
expected,
),
).toThrow("retainedDeltasBytes.heapUsedBytes must be end minus baseline");
expect(() =>
testing.parseWorkerProcessResult(
{
status: 0,
stdout: workerStdout({
...valid,
invariant: {
...valid.invariant,
terminal: {
...valid.invariant.terminal,
sqlite: { ...valid.invariant.terminal.sqlite, succeededTasks: 1 },
},
},
}),
stderr: "",
},
expected,
),
).toThrow("invariant.terminal.sqlite.succeededTasks was unexpected");
expect(() =>
testing.parseWorkerProcessResult(
{ status: 7, stdout: "", stderr: "worker exploded" },
expected,
),
).toThrow("failed (7): worker exploded");
expect(() =>
testing.parseWorkerProcessResult(
{
status: null,
stdout: "",
stderr: "",
error: Object.assign(new Error("spawnSync timed out"), { code: "ETIMEDOUT" }),
},
expected,
),
).toThrow("timed out after 300000ms");
});
it("runs a tiny durable create, terminal, delete smoke", () => {
const smoke = spawnSync(
process.execPath,
[
"--import",
"tsx",
"scripts/bench-task-registry-sqlite.ts",
"--sizes",
"2",
"--cycles",
"1",
"--warmup",
"0",
"--json",
],
{
cwd: process.cwd(),
encoding: "utf8",
env: { ...process.env, NODE_NO_WARNINGS: "1" },
timeout: 120_000,
},
);
expect(smoke.status, smoke.stderr).toBe(0);
expect(smoke.stderr).toContain("worker 1/1 complete size=2");
const report = JSON.parse(smoke.stdout) as {
invariants: { ok: boolean };
sizes: Array<{ invariant: WorkerResult["invariant"] }>;
};
expect(report.invariants.ok).toBe(true);
expect(report.sizes[0]?.invariant).toMatchObject({
registration: {
memory: { taskCount: 2, deliveryStateCount: 2, runningTasks: 2 },
sqlite: { taskCount: 2, deliveryStateCount: 2, runningTasks: 2 },
},
terminal: {
memory: {
taskCount: 2,
deliveryStateCount: 2,
succeededTasks: 2,
pendingDeliveryTasks: 2,
succeededTerminalOutcomes: 2,
},
sqlite: {
taskCount: 2,
deliveryStateCount: 2,
succeededTasks: 2,
pendingDeliveryTasks: 2,
succeededTerminalOutcomes: 2,
},
},
teardown: {
memory: { taskCount: 0, deliveryStateCount: 0 },
sqlite: { taskCount: 0, deliveryStateCount: 0 },
},
});
});
it("supports help and ends failures with the marker", () => {
const help = spawnSync(
process.execPath,
["--import", "tsx", "scripts/bench-task-registry-sqlite.ts", "--help"],
{ cwd: process.cwd(), encoding: "utf8", env: { ...process.env, NODE_NO_WARNINGS: "1" } },
);
expect(help.status).toBe(0);
expect(help.stdout).toContain("OpenClaw durable task registry churn benchmark");
expect(help.stdout).toContain("--sizes <list>");
expect(help.stderr).toBe("");
const failure = spawnSync(
process.execPath,
["--import", "tsx", "scripts/bench-task-registry-sqlite.ts", "--wat"],
{ cwd: process.cwd(), encoding: "utf8", env: { ...process.env, NODE_NO_WARNINGS: "1" } },
);
expect(failure.status).toBe(1);
expect(failure.stdout).toBe("");
expect(failure.stderr.trim().split("\n").at(-1)).toBe(
"[bench-task-registry-sqlite] FAILED (exit 1)",
);
});
});