Skip to content

Commit 77414e4

Browse files
committed
test(perf): compare saved CLI startup benchmarks
1 parent dcfc7e5 commit 77414e4

1 file changed

Lines changed: 79 additions & 12 deletions

File tree

scripts/bench-cli-startup.ts

Lines changed: 79 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { spawn } from "node:child_process";
2-
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
2+
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
33
import os from "node:os";
44
import path from "node:path";
55

@@ -53,8 +53,24 @@ type SuiteResult = {
5353
}>;
5454
};
5555

56+
type BenchmarkReport = {
57+
primary: SuiteResult;
58+
secondary?: SuiteResult | null;
59+
};
60+
61+
type CaseDelta = {
62+
id: string;
63+
name: string;
64+
durationAvgDeltaMs: number;
65+
durationAvgDeltaPct: number;
66+
maxRssAvgDeltaMb: number | null;
67+
maxRssAvgDeltaPct: number | null;
68+
};
69+
5670
type CliOptions = {
5771
cases: CommandCase[];
72+
compareBaseline?: string;
73+
compareCandidate?: string;
5874
entryPrimary: string;
5975
entrySecondary?: string;
6076
runs: number;
@@ -722,8 +738,26 @@ function printSuite(result: SuiteResult): void {
722738
}
723739

724740
function printDelta(primary: SuiteResult, secondary: SuiteResult): void {
725-
const primaryById = new Map(primary.cases.map((commandCase) => [commandCase.id, commandCase]));
741+
const deltas = buildCaseDeltas(primary, secondary);
726742
console.log("Delta (secondary - primary, avg)");
743+
for (const delta of deltas) {
744+
const durationDelta = delta.durationAvgDeltaMs;
745+
const durationPct = delta.durationAvgDeltaPct;
746+
const durationSign = durationDelta > 0 ? "+" : "";
747+
let line = `${delta.name.padEnd(24)} ${durationSign}${formatMs(durationDelta)} (${durationSign}${durationPct.toFixed(1)}%)`;
748+
if (delta.maxRssAvgDeltaMb != null && delta.maxRssAvgDeltaPct != null) {
749+
const rssDelta = delta.maxRssAvgDeltaMb;
750+
const rssPct = delta.maxRssAvgDeltaPct;
751+
const rssSign = rssDelta > 0 ? "+" : "";
752+
line += ` rss ${rssSign}${formatMb(rssDelta)} (${rssSign}${rssPct.toFixed(1)}%)`;
753+
}
754+
console.log(line);
755+
}
756+
}
757+
758+
function buildCaseDeltas(primary: SuiteResult, secondary: SuiteResult): CaseDelta[] {
759+
const primaryById = new Map(primary.cases.map((commandCase) => [commandCase.id, commandCase]));
760+
const deltas: CaseDelta[] = [];
727761
for (const commandCase of secondary.cases) {
728762
const baseline = primaryById.get(commandCase.id);
729763
if (!baseline) {
@@ -734,17 +768,24 @@ function printDelta(primary: SuiteResult, secondary: SuiteResult): void {
734768
baseline.summary.durationMs.avg > 0
735769
? (durationDelta / baseline.summary.durationMs.avg) * 100
736770
: 0;
737-
const durationSign = durationDelta > 0 ? "+" : "";
738-
let line = `${commandCase.name.padEnd(24)} ${durationSign}${formatMs(durationDelta)} (${durationSign}${durationPct.toFixed(1)}%)`;
739-
if (baseline.summary.maxRssMb && commandCase.summary.maxRssMb) {
740-
const rssDelta = commandCase.summary.maxRssMb.avg - baseline.summary.maxRssMb.avg;
741-
const rssPct =
742-
baseline.summary.maxRssMb.avg > 0 ? (rssDelta / baseline.summary.maxRssMb.avg) * 100 : 0;
743-
const rssSign = rssDelta > 0 ? "+" : "";
744-
line += ` rss ${rssSign}${formatMb(rssDelta)} (${rssSign}${rssPct.toFixed(1)}%)`;
745-
}
746-
console.log(line);
771+
const rssDelta =
772+
baseline.summary.maxRssMb && commandCase.summary.maxRssMb
773+
? commandCase.summary.maxRssMb.avg - baseline.summary.maxRssMb.avg
774+
: null;
775+
const rssPct =
776+
rssDelta != null && baseline.summary.maxRssMb && baseline.summary.maxRssMb.avg > 0
777+
? (rssDelta / baseline.summary.maxRssMb.avg) * 100
778+
: null;
779+
deltas.push({
780+
id: commandCase.id,
781+
name: commandCase.name,
782+
durationAvgDeltaMs: durationDelta,
783+
durationAvgDeltaPct: durationPct,
784+
maxRssAvgDeltaMb: rssDelta,
785+
maxRssAvgDeltaPct: rssPct,
786+
});
747787
}
788+
return deltas;
748789
}
749790

750791
async function buildSuiteResult(params: {
@@ -793,6 +834,8 @@ function parseOptions(): CliOptions {
793834
});
794835
return {
795836
cases,
837+
compareBaseline: parseFlagValue("--compare-baseline"),
838+
compareCandidate: parseFlagValue("--compare-candidate"),
796839
entryPrimary: parseFlagValue("--entry-primary") ?? parseFlagValue("--entry") ?? DEFAULT_ENTRY,
797840
entrySecondary: parseFlagValue("--entry-secondary"),
798841
runs: parsePositiveInt(parseFlagValue("--runs"), DEFAULT_RUNS),
@@ -821,6 +864,8 @@ Options:
821864
--warmup <n> Warmup runs per case (default: ${DEFAULT_WARMUP})
822865
--timeout-ms <ms> Per-run timeout (default: ${DEFAULT_TIMEOUT_MS})
823866
--output <path> Write machine-readable JSON to a file
867+
--compare-baseline <path> Read a saved JSON report as the baseline
868+
--compare-candidate <path> Read a saved JSON report as the candidate and print deltas
824869
--cpu-prof-dir <dir> Write V8 CPU profiles for each run
825870
--heap-prof-dir <dir> Write V8 heap profiles for each run
826871
--json Emit machine-readable JSON
@@ -831,13 +876,35 @@ Case ids:
831876
`);
832877
}
833878

879+
function readBenchmarkReport(filePath: string): BenchmarkReport {
880+
return JSON.parse(readFileSync(filePath, "utf8")) as BenchmarkReport;
881+
}
882+
834883
async function main(): Promise<void> {
835884
if (hasFlag("--help")) {
836885
printUsage();
837886
return;
838887
}
839888

840889
const options = parseOptions();
890+
if (options.compareBaseline || options.compareCandidate) {
891+
if (!options.compareBaseline || !options.compareCandidate) {
892+
throw new Error("--compare-baseline and --compare-candidate must be provided together");
893+
}
894+
const baseline = readBenchmarkReport(options.compareBaseline);
895+
const candidate = readBenchmarkReport(options.compareCandidate);
896+
const comparison = {
897+
baseline: options.compareBaseline,
898+
candidate: options.compareCandidate,
899+
deltas: buildCaseDeltas(baseline.primary, candidate.primary),
900+
};
901+
if (options.json) {
902+
console.log(JSON.stringify(comparison, null, 2));
903+
return;
904+
}
905+
printDelta(baseline.primary, candidate.primary);
906+
return;
907+
}
841908
const tmpDir = mkdtempSync(path.join(os.tmpdir(), "openclaw-cli-bench-"));
842909
const rssHookPath = buildRssHook(tmpDir);
843910
try {

0 commit comments

Comments
 (0)