11/**
22 * Format benchmark comparison results into a GitHub PR comment.
33 *
4- * Reads the plain-text mitata output from bench-compare.mjs and wraps it in a
5- * GitHub-flavored markdown comment.
4+ * Reads the plain-text mitata output and (optionally) the JSON results from
5+ * the bench run, then produces a GitHub-flavored markdown comment with:
6+ * 1. A summary table (when comparison data is available)
7+ * 2. Full mitata output in a collapsible <details> section
68 *
79 * Environment variables:
810 * BENCH_OUTPUT_FILE - Path to the plain-text bench output
11+ * BENCH_JSON_OUTPUT - Path to the JSON bench results (optional)
912 * BENCH_JOB_SUCCESS - Set to "true" if the benchmark job succeeded
1013 */
1114
1215import { readFileSync } from 'node:fs' ;
1316
1417const marker = '<!-- bench-compare -->' ;
1518
19+ // ---------------------------------------------------------------------------
20+ // Read raw mitata output
21+ // ---------------------------------------------------------------------------
22+
1623let rawOutput ;
1724try {
1825 rawOutput = readFileSync ( process . env . BENCH_OUTPUT_FILE , 'utf8' ) . trim ( ) ;
@@ -21,13 +28,99 @@ try {
2128 rawOutput = '(no output — benchmark may have failed to start)' ;
2229}
2330
31+ // Strip any lines before the mitata header (safety net for leaked setup messages)
32+ const benchStart = rawOutput . search ( / ^ ( c l k : | b e n c h m a r k \b ) / m) ;
33+ if ( benchStart > 0 ) {
34+ rawOutput = rawOutput . slice ( benchStart ) ;
35+ }
36+
37+ // ---------------------------------------------------------------------------
38+ // Read JSON results (if available) and build summary
39+ // ---------------------------------------------------------------------------
40+
41+ let summarySection = '' ;
42+ const jsonPath = process . env . BENCH_JSON_OUTPUT ;
43+
44+ if ( jsonPath ) {
45+ try {
46+ const json = JSON . parse ( readFileSync ( jsonPath , 'utf8' ) ) ;
47+ summarySection = buildSummary ( json ) ;
48+ } catch {
49+ // JSON not available or malformed — skip summary
50+ }
51+ }
52+
53+ function formatTime ( ns ) {
54+ if ( ns >= 1e6 ) return `${ ( ns / 1e6 ) . toFixed ( 2 ) } ms` ;
55+ if ( ns >= 1e3 ) return `${ ( ns / 1e3 ) . toFixed ( 2 ) } µs` ;
56+ return `${ ns . toFixed ( 2 ) } ns` ;
57+ }
58+
59+ function deltaEmoji ( pct ) {
60+ const abs = Math . abs ( pct ) ;
61+ // negative pct means experiment is faster (lower time = better)
62+ if ( abs < 1 ) return '⚪' ;
63+ if ( pct <= - 5 ) return '🟢' ;
64+ if ( pct >= 5 ) return '🔴' ;
65+ return '🟡' ;
66+ }
67+
68+ function buildSummary ( json ) {
69+ const benchmarks = json . benchmarks || [ ] ;
70+
71+ // In comparison mode, benchmarks come in pairs inside summary groups.
72+ // Each benchmark alias is like "gts small (control)" / "gts small (experiment)".
73+ // Group them by stripping the suffix.
74+ const pairs = new Map ( ) ;
75+
76+ for ( const trial of benchmarks ) {
77+ for ( const r of trial . runs || [ ] ) {
78+ if ( ! r . stats ) continue ;
79+ const m = r . name . match ( / ^ ( .+ ) \s + \( ( c o n t r o l | e x p e r i m e n t ) \) $ / ) ;
80+ if ( ! m ) continue ;
81+ const [ , key , role ] = m ;
82+ if ( ! pairs . has ( key ) ) pairs . set ( key , { } ) ;
83+ pairs . get ( key ) [ role ] = r . stats ;
84+ }
85+ }
86+
87+ if ( pairs . size === 0 ) return '' ;
88+
89+ const rows = [ ] ;
90+ for ( const [ name , { control, experiment } ] of pairs ) {
91+ if ( ! control || ! experiment ) continue ;
92+ const delta = ( ( experiment . avg - control . avg ) / control . avg ) * 100 ;
93+ const emoji = deltaEmoji ( delta ) ;
94+ const sign = delta > 0 ? '+' : '' ;
95+ rows . push (
96+ `| ${ emoji } | ${ name } | ${ formatTime ( control . avg ) } | ${ formatTime ( experiment . avg ) } | ${ sign } ${ delta . toFixed ( 1 ) } % |`
97+ ) ;
98+ }
99+
100+ if ( rows . length === 0 ) return '' ;
101+
102+ return [
103+ '' ,
104+ '| | Benchmark | Control (avg) | Experiment (avg) | Δ |' ,
105+ '|---|---|---:|---:|---:|' ,
106+ ...rows ,
107+ '' ,
108+ '> 🟢 faster · 🔴 slower · 🟡 within 5% · ⚪ within 1%' ,
109+ '' ,
110+ ] . join ( '\n' ) ;
111+ }
112+
113+ // ---------------------------------------------------------------------------
114+ // Assemble comment
115+ // ---------------------------------------------------------------------------
116+
24117const success = process . env . BENCH_JOB_SUCCESS === 'true' ;
25118const heading = success ? '## 🏎️ Benchmark Comparison' : '## ❌ Benchmark Comparison (failed)' ;
26119
27120const body = [
28121 marker ,
29122 heading ,
30- '' ,
123+ summarySection ,
31124 '<details>' ,
32125 '<summary>Full mitata output</summary>' ,
33126 '' ,
0 commit comments