mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-27 12:56:01 -06:00
344 lines
12 KiB
TypeScript
344 lines
12 KiB
TypeScript
// Selected memory runtime benchmark tests cover CLI parsing and report-budget semantics.
|
|
import { execFileSync } 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 } from "../../scripts/bench-memory-selected-runtime.mts";
|
|
|
|
function withReport<T>(payload: unknown, run: (reportPath: string) => T): T {
|
|
const reportPath = path.join(
|
|
os.tmpdir(),
|
|
`openclaw-memory-selected-runtime-${Date.now()}-${Math.random().toString(16).slice(2)}.json`,
|
|
);
|
|
fs.writeFileSync(
|
|
reportPath,
|
|
typeof payload === "string" ? payload : `${JSON.stringify(payload)}\n`,
|
|
);
|
|
try {
|
|
return run(reportPath);
|
|
} finally {
|
|
fs.rmSync(reportPath, { force: true });
|
|
}
|
|
}
|
|
|
|
const samples = Array.from({ length: 20 }, (_, index) => index + 1);
|
|
const baselineCommit = "a".repeat(40);
|
|
const candidateCommit = "b".repeat(40);
|
|
|
|
type CandidateReport = Parameters<typeof testing.compareP95AgainstBaseline>[0]["candidate"];
|
|
|
|
function baselineReport(overrides: Record<string, unknown> = {}) {
|
|
return {
|
|
batchSize: 100,
|
|
fixture: "memory-core-selected-runtime",
|
|
mode: "selected-memory-runtime",
|
|
runs: 20,
|
|
runtime: { arch: "x64", node: "v24.15.0", platform: "linux" },
|
|
source: { commit: baselineCommit },
|
|
summary: testing.summarizeLatency(samples),
|
|
version: 2,
|
|
warmLookupNsPerLookup: samples,
|
|
warmup: 1,
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function candidateReport(overrides: Partial<CandidateReport> = {}): CandidateReport {
|
|
return {
|
|
batchSize: 100,
|
|
fixture: "memory-core-selected-runtime",
|
|
firstLookupNs: 1,
|
|
generatedAt: "2026-08-10T00:00:00.000Z",
|
|
lookup: "getSelectedMemoryRuntime",
|
|
mode: "selected-memory-runtime",
|
|
repo: "/candidate",
|
|
runs: 20,
|
|
runtime: { arch: "x64", node: "v24.15.0", platform: "linux" },
|
|
source: { commit: candidateCommit },
|
|
summary: testing.summarizeLatency(samples),
|
|
version: 2,
|
|
warmLookupNsPerLookup: samples,
|
|
warmup: 1,
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function candidateWithField(field: string, value: unknown): CandidateReport {
|
|
const candidate = candidateReport();
|
|
const record = candidate as unknown as Record<string, unknown>;
|
|
if (field.startsWith("runtime.")) {
|
|
const runtime = record.runtime as Record<string, unknown>;
|
|
runtime[field.slice("runtime.".length)] = value;
|
|
} else {
|
|
record[field] = value;
|
|
}
|
|
return candidate;
|
|
}
|
|
|
|
describe("selected memory runtime benchmark script", () => {
|
|
it("uses the legacy fixture without authorization and the P0A fixture with it", () => {
|
|
const runtime = {};
|
|
const authorization = {};
|
|
|
|
expect(testing.createMemoryCapabilityFixture(runtime)).toEqual({ runtime });
|
|
expect(testing.createMemoryCapabilityFixture(runtime, authorization)).toEqual({
|
|
authorization,
|
|
runtime,
|
|
});
|
|
});
|
|
|
|
it("finds the authorization module only when the target source tree provides it", () => {
|
|
const repoRoot = fs.mkdtempSync(
|
|
path.join(os.tmpdir(), "openclaw-memory-selected-runtime-repo-"),
|
|
);
|
|
const authorizationPath = path.join(
|
|
repoRoot,
|
|
"src",
|
|
"memory-host-sdk",
|
|
"host",
|
|
"authorization.ts",
|
|
);
|
|
try {
|
|
expect(testing.findLegacyAuthorizationModule(repoRoot)).toBeUndefined();
|
|
fs.mkdirSync(path.dirname(authorizationPath), { recursive: true });
|
|
fs.writeFileSync(authorizationPath, "export {};\n");
|
|
expect(testing.findLegacyAuthorizationModule(repoRoot)).toBe(authorizationPath);
|
|
} finally {
|
|
fs.rmSync(repoRoot, { force: true, recursive: true });
|
|
}
|
|
});
|
|
|
|
it("records a source commit only for a clean target worktree", () => {
|
|
const repoRoot = fs.mkdtempSync(
|
|
path.join(os.tmpdir(), "openclaw-memory-selected-runtime-git-"),
|
|
);
|
|
const trackedPath = path.join(repoRoot, "tracked.txt");
|
|
try {
|
|
execFileSync("git", ["init", "--quiet"], { cwd: repoRoot });
|
|
execFileSync("git", ["config", "user.email", "test@example.com"], { cwd: repoRoot });
|
|
execFileSync("git", ["config", "user.name", "Benchmark Test"], { cwd: repoRoot });
|
|
fs.writeFileSync(trackedPath, "initial\n");
|
|
execFileSync("git", ["add", "tracked.txt"], { cwd: repoRoot });
|
|
execFileSync("git", ["commit", "--quiet", "-m", "initial"], { cwd: repoRoot });
|
|
|
|
expect(testing.resolveSourceCommit(repoRoot)).toMatch(/^[0-9a-f]{40}$/u);
|
|
|
|
fs.writeFileSync(trackedPath, "modified\n");
|
|
expect(() => testing.resolveSourceCommit(repoRoot)).toThrow(
|
|
"must be clean to report its commit",
|
|
);
|
|
|
|
execFileSync("git", ["checkout", "--", "tracked.txt"], { cwd: repoRoot });
|
|
fs.writeFileSync(path.join(repoRoot, "untracked.txt"), "untracked\n");
|
|
expect(() => testing.resolveSourceCommit(repoRoot)).toThrow(
|
|
"must be clean to report its commit",
|
|
);
|
|
} finally {
|
|
fs.rmSync(repoRoot, { force: true, recursive: true });
|
|
}
|
|
});
|
|
|
|
it("parses bounded benchmark controls and defaults", () => {
|
|
expect(testing.parseArgs([], "/repo")).toEqual({
|
|
batchSize: 100_000,
|
|
json: false,
|
|
maxP95RegressionPct: 15,
|
|
repo: "/repo",
|
|
runs: 20,
|
|
warmup: 3,
|
|
});
|
|
expect(
|
|
testing.parseArgs(
|
|
[
|
|
"--repo",
|
|
"candidate",
|
|
"--batch-size",
|
|
"200",
|
|
"--runs",
|
|
"20",
|
|
"--warmup",
|
|
"1",
|
|
"--output",
|
|
"report.json",
|
|
"--baseline",
|
|
"baseline.json",
|
|
"--max-p95-regression-pct",
|
|
"12.5",
|
|
"--json",
|
|
],
|
|
"/repo",
|
|
),
|
|
).toEqual({
|
|
baseline: "/repo/baseline.json",
|
|
batchSize: 200,
|
|
json: true,
|
|
maxP95RegressionPct: 12.5,
|
|
output: "/repo/report.json",
|
|
repo: "/repo/candidate",
|
|
runs: 20,
|
|
warmup: 1,
|
|
});
|
|
expect(testing.parseArgs(["--help"], "/repo")).toEqual({ help: true });
|
|
});
|
|
|
|
it("rejects invalid, missing, duplicate, and unknown flags before running a benchmark", () => {
|
|
expect(() => testing.parseArgs(["--batch-size", "0"])).toThrow(
|
|
"--batch-size must be at least 1",
|
|
);
|
|
expect(() => testing.parseArgs(["--runs", "1.5"])).toThrow("--runs must be an integer");
|
|
expect(() => testing.parseArgs(["--runs", "19"])).toThrow("--runs must be at least 20");
|
|
expect(() => testing.parseArgs(["--warmup", "1.5"])).toThrow("--warmup must be an integer");
|
|
expect(() => testing.parseArgs(["--max-p95-regression-pct", "wat"])).toThrow(
|
|
"--max-p95-regression-pct must be a non-negative number",
|
|
);
|
|
expect(() => testing.parseArgs(["--output"])).toThrow("--output requires a value");
|
|
expect(() => testing.parseArgs(["--json", "--json"])).toThrow(
|
|
"--json was provided more than once",
|
|
);
|
|
expect(() => testing.parseArgs(["--runs", "20", "--runs", "21"])).toThrow(
|
|
"--runs was provided more than once",
|
|
);
|
|
expect(() => testing.parseArgs(["--wat"])).toThrow("Unknown argument: --wat");
|
|
});
|
|
|
|
it("reports nearest-rank p50 and p95 warm lookup summaries", () => {
|
|
expect(testing.summarizeLatency(samples)).toEqual({
|
|
count: 20,
|
|
maxNsPerLookup: 20,
|
|
minNsPerLookup: 1,
|
|
p50NsPerLookup: 10,
|
|
p95NsPerLookup: 19,
|
|
});
|
|
expect(() => testing.summarizeLatency(samples.slice(0, 19))).toThrow(
|
|
"p95 requires at least 20 warm lookup samples; received 19",
|
|
);
|
|
});
|
|
|
|
it("reads only complete, internally consistent baseline reports", () => {
|
|
const baseline = withReport(baselineReport(), testing.readBaselineReport);
|
|
expect(baseline.source.commit).toBe(baselineCommit);
|
|
expect(baseline.summary).toMatchObject({ count: 20, p95NsPerLookup: 19 });
|
|
|
|
const invalidReports: Array<{ expected: string; payload: unknown }> = [
|
|
{ expected: "must contain version >= 0", payload: { summary: { p95NsPerLookup: 19 } } },
|
|
{
|
|
expected: "must contain runs >= 20",
|
|
payload: baselineReport({ runs: 19, warmLookupNsPerLookup: samples.slice(0, 19) }),
|
|
},
|
|
{
|
|
expected: "warmLookupNsPerLookup length must equal runs",
|
|
payload: baselineReport({ warmLookupNsPerLookup: samples.slice(0, 19) }),
|
|
},
|
|
{
|
|
expected: "warmLookupNsPerLookup must contain numbers",
|
|
payload: baselineReport({ warmLookupNsPerLookup: [...samples.slice(0, 19), -1] }),
|
|
},
|
|
{
|
|
expected: "must contain summary.maxNsPerLookup",
|
|
payload: baselineReport({ summary: { count: 20, p95NsPerLookup: 19 } }),
|
|
},
|
|
{
|
|
expected: "source.commit must be a full lowercase SHA",
|
|
payload: baselineReport({ source: { commit: "A".repeat(40) } }),
|
|
},
|
|
];
|
|
for (const { expected, payload } of invalidReports) {
|
|
withReport(payload, (reportPath) => {
|
|
expect(() => testing.readBaselineReport(reportPath)).toThrow(expected);
|
|
});
|
|
}
|
|
for (const [field, value] of [
|
|
["count", 19],
|
|
["maxNsPerLookup", 21],
|
|
["minNsPerLookup", 2],
|
|
["p50NsPerLookup", 11],
|
|
["p95NsPerLookup", 20],
|
|
] as const) {
|
|
const summary = { ...testing.summarizeLatency(samples) } as Record<string, number>;
|
|
summary[field] = value;
|
|
withReport(baselineReport({ summary }), (reportPath) => {
|
|
expect(() => testing.readBaselineReport(reportPath)).toThrow(
|
|
"summary must match warmLookupNsPerLookup",
|
|
);
|
|
});
|
|
}
|
|
withReport("{", (reportPath) => {
|
|
expect(() => testing.readBaselineReport(reportPath)).toThrow(
|
|
"failed to read baseline report",
|
|
);
|
|
});
|
|
});
|
|
|
|
it("compares comparable reports across source commits and records both identities", () => {
|
|
const baseline = withReport(baselineReport(), testing.readBaselineReport);
|
|
const comparison = testing.compareP95AgainstBaseline({
|
|
baseline,
|
|
candidate: candidateReport(),
|
|
maxP95RegressionPct: 15,
|
|
});
|
|
|
|
expect(comparison).toMatchObject({
|
|
baselineCommit,
|
|
candidateCommit,
|
|
candidateP95NsPerLookup: 19,
|
|
passed: true,
|
|
});
|
|
expect(() => testing.assertP95RegressionWithinBudget(comparison)).not.toThrow();
|
|
});
|
|
|
|
it("rejects baseline comparisons with incompatible benchmark identities", () => {
|
|
const baseline = withReport(baselineReport(), testing.readBaselineReport);
|
|
const mismatches = [
|
|
["version", 3],
|
|
["mode", "other-mode"],
|
|
["fixture", "other-fixture"],
|
|
["batchSize", 101],
|
|
["runs", 21],
|
|
["warmup", 2],
|
|
["runtime.node", "v24.16.0"],
|
|
["runtime.platform", "darwin"],
|
|
["runtime.arch", "arm64"],
|
|
] as const;
|
|
|
|
for (const [field, value] of mismatches) {
|
|
expect(() =>
|
|
testing.compareP95AgainstBaseline({
|
|
baseline,
|
|
candidate: candidateWithField(field, value),
|
|
maxP95RegressionPct: 15,
|
|
}),
|
|
).toThrow(`baseline report is not comparable: ${field}`);
|
|
}
|
|
});
|
|
|
|
it("enforces the p95 budget only after compatibility succeeds", () => {
|
|
const baseline = withReport(baselineReport(), testing.readBaselineReport);
|
|
const slowerSamples = Array.from({ length: 20 }, (_, index) => index + 4);
|
|
const slowerCandidate = candidateReport({
|
|
summary: testing.summarizeLatency(slowerSamples),
|
|
warmLookupNsPerLookup: slowerSamples,
|
|
});
|
|
|
|
const comparison = testing.compareP95AgainstBaseline({
|
|
baseline,
|
|
candidate: slowerCandidate,
|
|
maxP95RegressionPct: 15,
|
|
});
|
|
expect(comparison.passed).toBe(false);
|
|
expect(() => testing.assertP95RegressionWithinBudget(comparison)).toThrow(
|
|
"warm selected-memory-runtime p95",
|
|
);
|
|
|
|
const incompatibleCandidate = candidateWithField("version", 3);
|
|
incompatibleCandidate.summary = testing.summarizeLatency(slowerSamples);
|
|
expect(() =>
|
|
testing.compareP95AgainstBaseline({
|
|
baseline,
|
|
candidate: incompatibleCandidate,
|
|
maxP95RegressionPct: 15,
|
|
}),
|
|
).toThrow("baseline report is not comparable: version");
|
|
});
|
|
});
|