mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-28 05:16:23 -06:00
608e365ce4
* perf(scripts): report SQLite scenarios independently * perf(sqlite): benchmark production state workloads * test(sqlite): use tracked benchmark temp dirs * perf(sqlite): harden workload evidence * fix(perf): require matching SQLite workloads
166 lines
6.0 KiB
TypeScript
166 lines
6.0 KiB
TypeScript
// Bench SQLite State tests cover benchmark CLI argument safety.
|
|
import { spawnSync } from "node:child_process";
|
|
import { readFileSync } from "node:fs";
|
|
import path from "node:path";
|
|
import { afterEach, describe, expect, it } from "vitest";
|
|
import { collectSqliteQueryPlanEvidence } from "../../scripts/lib/sqlite-query-plan-evidence.js";
|
|
import { parseSqliteStateBenchmarkCli } from "../../scripts/lib/sqlite-state-benchmark-cli.js";
|
|
import { OPENCLAW_AGENT_SCHEMA_VERSION } from "../../src/state/openclaw-agent-db-contract.js";
|
|
import { OPENCLAW_STATE_SCHEMA_VERSION } from "../../src/state/openclaw-state-db-contract.js";
|
|
import { useAutoCleanupTempDirTracker } from "../helpers/temp-dir.js";
|
|
|
|
const tempDirs = useAutoCleanupTempDirTracker(afterEach);
|
|
|
|
function runBench(args: string[]) {
|
|
return spawnSync(
|
|
process.execPath,
|
|
["--import", "tsx", "scripts/bench-sqlite-state.ts", ...args],
|
|
{
|
|
cwd: process.cwd(),
|
|
encoding: "utf8",
|
|
},
|
|
);
|
|
}
|
|
|
|
describe("scripts/bench-sqlite-state", () => {
|
|
it("rejects unknown args before seeding benchmark databases", () => {
|
|
const result = runBench(["--wat"]);
|
|
|
|
expect(result.status).toBe(2);
|
|
expect(result.stdout).toBe("");
|
|
expect(result.stderr.trim()).toBe("error: Unknown argument: --wat");
|
|
});
|
|
|
|
it("rejects missing output values before seeding benchmark databases", () => {
|
|
expect(() => parseSqliteStateBenchmarkCli(["--output", "--profile", "smoke"])).toThrow(
|
|
"--output requires a value",
|
|
);
|
|
});
|
|
|
|
it("rejects short flag output values before seeding benchmark databases", () => {
|
|
expect(() => parseSqliteStateBenchmarkCli(["--output", "-h"])).toThrow(
|
|
"--output requires a value",
|
|
);
|
|
});
|
|
|
|
it("rejects invalid profiles without printing a stack trace", () => {
|
|
expect(() => parseSqliteStateBenchmarkCli(["--profile", "huge"])).toThrow(
|
|
'--profile must be one of smoke, default, large; got "huge"',
|
|
);
|
|
});
|
|
|
|
it("rejects duplicate single-value controls before seeding benchmark databases", () => {
|
|
expect(() =>
|
|
parseSqliteStateBenchmarkCli(["--profile", "smoke", "--profile", "large"]),
|
|
).toThrow("--profile was provided more than once");
|
|
expect(parseSqliteStateBenchmarkCli(["--help", "--profile", "huge"])).toEqual({
|
|
help: true,
|
|
});
|
|
});
|
|
|
|
it("reports production-shaped SQLite scenarios in the existing smoke benchmark", () => {
|
|
const outputDir = tempDirs.make("openclaw-sqlite-bench-test-");
|
|
const outputPath = path.join(outputDir, "report.json");
|
|
const result = runBench(["--profile", "smoke", "--output", outputPath]);
|
|
|
|
expect(result.status).toBe(0);
|
|
expect(result.stderr).toBe("");
|
|
expect(result.stdout).toContain("SQLITE_PERF_TRANSCRIPT_ROWS=256");
|
|
const proofLines = result.stdout
|
|
.split("\n")
|
|
.filter((line) => line.startsWith("SQLITE_PERF_SCENARIO "));
|
|
expect(proofLines).toHaveLength(12);
|
|
|
|
const report = JSON.parse(readFileSync(outputPath, "utf8")) as {
|
|
schemaVersion: number;
|
|
queries: Array<{
|
|
database: string;
|
|
id: string;
|
|
p50Ms: number;
|
|
p95Ms: number;
|
|
plan: {
|
|
fullTableScans: string[];
|
|
indexes: string[];
|
|
raw: string[];
|
|
tempSorts: string[];
|
|
};
|
|
rows: number;
|
|
runs: number;
|
|
sql: string;
|
|
}>;
|
|
versions: { agentSchema: number; sqlite: string; stateSchema: number };
|
|
};
|
|
expect(report.schemaVersion).toBe(2);
|
|
expect(report.versions).toEqual({
|
|
agentSchema: OPENCLAW_AGENT_SCHEMA_VERSION,
|
|
sqlite: expect.stringMatching(/^\d+\.\d+\.\d+$/u),
|
|
stateSchema: OPENCLAW_STATE_SCHEMA_VERSION,
|
|
});
|
|
|
|
const expectedIds = [
|
|
"cron.store.load",
|
|
"task-runs.cron.list",
|
|
"task-runs.cron-source.list",
|
|
"delivery.pending.load",
|
|
"ingress.pending.first-page",
|
|
"ingress.pending.seek-page",
|
|
"ingress.pending.id-page",
|
|
"ingress.pending.id-seek-page",
|
|
"plugin-state.namespace.live",
|
|
"agent-cache.plugin-model-catalog.list",
|
|
"transcript.tail.metadata",
|
|
"transcript.tail.payload",
|
|
];
|
|
expect(report.queries.map((query) => query.id)).toEqual(expectedIds);
|
|
expect(new Set(report.queries.map((query) => query.id)).size).toBe(expectedIds.length);
|
|
const expectedRows = new Map([
|
|
["cron.store.load", 13],
|
|
["task-runs.cron.list", 1_000],
|
|
["task-runs.cron-source.list", 250],
|
|
["delivery.pending.load", 696],
|
|
["ingress.pending.first-page", 100],
|
|
["ingress.pending.seek-page", 100],
|
|
["ingress.pending.id-page", 100],
|
|
["ingress.pending.id-seek-page", 100],
|
|
["plugin-state.namespace.live", 675],
|
|
["agent-cache.plugin-model-catalog.list", 64],
|
|
["transcript.tail.metadata", 256],
|
|
["transcript.tail.payload", 256],
|
|
]);
|
|
for (const query of report.queries) {
|
|
expect(["agent", "state"]).toContain(query.database);
|
|
expect(query.rows).toBe(expectedRows.get(query.id));
|
|
expect(query.runs).toBe(20);
|
|
expect(Number.isFinite(query.p50Ms)).toBe(true);
|
|
expect(Number.isFinite(query.p95Ms)).toBe(true);
|
|
expect(query.sql).toContain("SELECT");
|
|
expect(query.plan.raw.length).toBeGreaterThan(0);
|
|
expect(query.plan.fullTableScans).toEqual([]);
|
|
if (!query.id.startsWith("task-runs.")) {
|
|
expect(query.plan.tempSorts).toEqual([]);
|
|
}
|
|
}
|
|
});
|
|
|
|
it("normalizes modern plan forms without inventing table scans", () => {
|
|
expect(
|
|
collectSqliteQueryPlanEvidence([
|
|
"SEARCH events USING AUTOMATIC PARTIAL COVERING INDEX (status=?)",
|
|
"SCAN json_each VIRTUAL TABLE INDEX 1:",
|
|
"SCAN 2-ROW VALUES CLAUSE",
|
|
"SCAN task_runs",
|
|
]),
|
|
).toEqual({
|
|
fullTableScans: ["SCAN task_runs"],
|
|
indexes: ["AUTOMATIC PARTIAL COVERING INDEX"],
|
|
raw: [
|
|
"SEARCH events USING AUTOMATIC PARTIAL COVERING INDEX (status=?)",
|
|
"SCAN json_each VIRTUAL TABLE INDEX 1:",
|
|
"SCAN 2-ROW VALUES CLAUSE",
|
|
"SCAN task_runs",
|
|
],
|
|
tempSorts: [],
|
|
});
|
|
});
|
|
});
|