11import { spawn } from "node:child_process" ;
2- import { mkdirSync , mkdtempSync , rmSync , writeFileSync } from "node:fs" ;
2+ import { mkdirSync , mkdtempSync , readFileSync , rmSync , writeFileSync } from "node:fs" ;
33import os from "node:os" ;
44import 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+
5670type 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
724740function 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
750791async 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+
834883async 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