33/**
44 * Benchmark runner — measures codegraph performance on itself (dogfooding).
55 *
6- * Runs both native (Rust) and WASM engines, outputs JSON to stdout
7- * with raw and per-file normalized metrics for each.
6+ * Each engine (native / WASM) runs in a forked subprocess so that a segfault
7+ * in the native addon only kills the child — the parent survives and collects
8+ * partial results from whichever engines succeeded.
89 *
910 * Usage: node scripts/benchmark.js
1011 */
@@ -15,25 +16,82 @@ import { performance } from 'node:perf_hooks';
1516import { fileURLToPath } from 'node:url' ;
1617import Database from 'better-sqlite3' ;
1718import { resolveBenchmarkSource , srcImport } from './lib/bench-config.js' ;
19+ import { isWorker , workerEngine , forkEngines } from './lib/fork-engine.js' ;
20+
21+ // ── Parent process: fork one child per engine, assemble final output ─────
22+ if ( ! isWorker ( ) ) {
23+ const { version, cleanup : versionCleanup } = await resolveBenchmarkSource ( ) ;
24+ let wasm , native ;
25+ try {
26+ ( { wasm, native } = await forkEngines ( import . meta. url , process . argv . slice ( 2 ) ) ) ;
27+ } catch ( err ) {
28+ console . error ( `Error: ${ err . message } ` ) ;
29+ versionCleanup ( ) ;
30+ process . exit ( 1 ) ;
31+ }
32+
33+ const primary = wasm || native ;
34+ if ( ! primary ) {
35+ console . error ( 'Error: Both engines failed. No results to report.' ) ;
36+ versionCleanup ( ) ;
37+ process . exit ( 1 ) ;
38+ }
39+
40+ const result = {
41+ version,
42+ date : new Date ( ) . toISOString ( ) . slice ( 0 , 10 ) ,
43+ files : primary . files ,
44+ wasm : wasm
45+ ? {
46+ buildTimeMs : wasm . buildTimeMs ,
47+ queryTimeMs : wasm . queryTimeMs ,
48+ nodes : wasm . nodes ,
49+ edges : wasm . edges ,
50+ dbSizeBytes : wasm . dbSizeBytes ,
51+ perFile : wasm . perFile ,
52+ noopRebuildMs : wasm . noopRebuildMs ,
53+ oneFileRebuildMs : wasm . oneFileRebuildMs ,
54+ oneFilePhases : wasm . oneFilePhases ,
55+ queries : wasm . queries ,
56+ phases : wasm . phases ,
57+ }
58+ : null ,
59+ native : native
60+ ? {
61+ buildTimeMs : native . buildTimeMs ,
62+ queryTimeMs : native . queryTimeMs ,
63+ nodes : native . nodes ,
64+ edges : native . edges ,
65+ dbSizeBytes : native . dbSizeBytes ,
66+ perFile : native . perFile ,
67+ noopRebuildMs : native . noopRebuildMs ,
68+ oneFileRebuildMs : native . oneFileRebuildMs ,
69+ oneFilePhases : native . oneFilePhases ,
70+ queries : native . queries ,
71+ phases : native . phases ,
72+ }
73+ : null ,
74+ } ;
75+
76+ console . log ( JSON . stringify ( result , null , 2 ) ) ;
77+ versionCleanup ( ) ;
78+ process . exit ( 0 ) ;
79+ }
80+
81+ // ── Worker process: benchmark a single engine, write JSON to stdout ──────
82+ const engine = workerEngine ( ) ;
1883
1984const __dirname = path . dirname ( fileURLToPath ( import . meta. url ) ) ;
2085const root = path . resolve ( __dirname , '..' ) ;
2186
22- const { version , srcDir, cleanup } = await resolveBenchmarkSource ( ) ;
87+ const { srcDir, cleanup } = await resolveBenchmarkSource ( ) ;
2388
2489const dbPath = path . join ( root , '.codegraph' , 'graph.db' ) ;
2590
26- // Import programmatic API (use file:// URLs for Windows compatibility)
2791const { buildGraph } = await import ( srcImport ( srcDir , 'builder.js' ) ) ;
2892const { fnDepsData, fnImpactData, pathData, rolesData, statsData } = await import (
2993 srcImport ( srcDir , 'queries.js' )
3094) ;
31- const { isNativeAvailable } = await import (
32- srcImport ( srcDir , 'native.js' )
33- ) ;
34- const { isWasmAvailable } = await import (
35- srcImport ( srcDir , 'parser.js' )
36- ) ;
3795
3896const INCREMENTAL_RUNS = 3 ;
3997const QUERY_RUNS = 5 ;
@@ -49,9 +107,6 @@ function round1(n) {
49107 return Math . round ( n * 10 ) / 10 ;
50108}
51109
52- /**
53- * Pick hub (most-connected) and leaf (least-connected) non-test symbols from the DB.
54- */
55110function selectTargets ( ) {
56111 const db = new Database ( dbPath , { readonly : true } ) ;
57112 const rows = db
@@ -67,183 +122,106 @@ function selectTargets() {
67122 db . close ( ) ;
68123
69124 if ( rows . length === 0 ) return { hub : 'buildGraph' , leaf : 'median' } ;
70-
71125 return { hub : rows [ 0 ] . name , leaf : rows [ rows . length - 1 ] . name } ;
72126}
73127
74128// Redirect console.log to stderr so only JSON goes to stdout
75129const origLog = console . log ;
76130console . log = ( ...args ) => console . error ( ...args ) ;
77131
78- async function benchmarkEngine ( engine ) {
79- // Clean DB for a full build
80- if ( fs . existsSync ( dbPath ) ) fs . unlinkSync ( dbPath ) ;
81-
82- const buildStart = performance . now ( ) ;
83- const buildResult = await buildGraph ( root , { engine, incremental : false } ) ;
84- const buildTimeMs = performance . now ( ) - buildStart ;
85-
86- const queryStart = performance . now ( ) ;
87- fnDepsData ( 'buildGraph' , dbPath ) ;
88- const queryTimeMs = performance . now ( ) - queryStart ;
89-
90- const stats = statsData ( dbPath ) ;
91- const totalFiles = stats . files . total ;
92- const totalNodes = stats . nodes . total ;
93- const totalEdges = stats . edges . total ;
94- const dbSizeBytes = fs . statSync ( dbPath ) . size ;
95-
96- // ── Incremental build tiers (reuse existing DB from full build) ─────
97- console . error ( ` [${ engine } ] Benchmarking no-op rebuild...` ) ;
98- const noopTimings = [ ] ;
132+ // Clean DB for a full build
133+ if ( fs . existsSync ( dbPath ) ) fs . unlinkSync ( dbPath ) ;
134+
135+ const buildStart = performance . now ( ) ;
136+ const buildResult = await buildGraph ( root , { engine, incremental : false } ) ;
137+ const buildTimeMs = performance . now ( ) - buildStart ;
138+
139+ const queryStart = performance . now ( ) ;
140+ fnDepsData ( 'buildGraph' , dbPath ) ;
141+ const queryTimeMs = performance . now ( ) - queryStart ;
142+
143+ const stats = statsData ( dbPath ) ;
144+ const totalFiles = stats . files . total ;
145+ const totalNodes = stats . nodes . total ;
146+ const totalEdges = stats . edges . total ;
147+ const dbSizeBytes = fs . statSync ( dbPath ) . size ;
148+
149+ // ── Incremental build tiers ─────────────────────────────────────────
150+ console . error ( ` [${ engine } ] Benchmarking no-op rebuild...` ) ;
151+ const noopTimings = [ ] ;
152+ for ( let i = 0 ; i < INCREMENTAL_RUNS ; i ++ ) {
153+ const start = performance . now ( ) ;
154+ await buildGraph ( root , { engine, incremental : true } ) ;
155+ noopTimings . push ( performance . now ( ) - start ) ;
156+ }
157+ const noopRebuildMs = Math . round ( median ( noopTimings ) ) ;
158+
159+ console . error ( ` [${ engine } ] Benchmarking 1-file rebuild...` ) ;
160+ const original = fs . readFileSync ( PROBE_FILE , 'utf8' ) ;
161+ let oneFileRebuildMs ;
162+ let oneFilePhases = null ;
163+ try {
164+ const oneFileRuns = [ ] ;
99165 for ( let i = 0 ; i < INCREMENTAL_RUNS ; i ++ ) {
166+ fs . writeFileSync ( PROBE_FILE , original + `\n// probe-${ i } \n` ) ;
100167 const start = performance . now ( ) ;
101- await buildGraph ( root , { engine, incremental : true } ) ;
102- noopTimings . push ( performance . now ( ) - start ) ;
168+ const res = await buildGraph ( root , { engine, incremental : true } ) ;
169+ oneFileRuns . push ( { ms : performance . now ( ) - start , phases : res ?. phases || null } ) ;
103170 }
104- const noopRebuildMs = Math . round ( median ( noopTimings ) ) ;
105-
106- console . error ( ` [${ engine } ] Benchmarking 1-file rebuild...` ) ;
107- const original = fs . readFileSync ( PROBE_FILE , 'utf8' ) ;
108- let oneFileRebuildMs ;
109- let oneFilePhases = null ;
110- try {
111- const oneFileRuns = [ ] ;
112- for ( let i = 0 ; i < INCREMENTAL_RUNS ; i ++ ) {
113- fs . writeFileSync ( PROBE_FILE , original + `\n// probe-${ i } \n` ) ;
114- const start = performance . now ( ) ;
115- const res = await buildGraph ( root , { engine, incremental : true } ) ;
116- oneFileRuns . push ( { ms : performance . now ( ) - start , phases : res ?. phases || null } ) ;
117- }
118- oneFileRuns . sort ( ( a , b ) => a . ms - b . ms ) ;
119- const medianRun = oneFileRuns [ Math . floor ( oneFileRuns . length / 2 ) ] ;
120- oneFileRebuildMs = Math . round ( medianRun . ms ) ;
121- oneFilePhases = medianRun . phases ;
122- } finally {
123- fs . writeFileSync ( PROBE_FILE , original ) ;
124- await buildGraph ( root , { engine, incremental : true } ) ;
125- }
126-
127- // ── Query benchmarks (median of QUERY_RUNS each) ────────────────────
128- console . error ( ` [${ engine } ] Benchmarking queries...` ) ;
129- const targets = selectTargets ( ) ;
130- console . error ( ` hub=${ targets . hub } , leaf=${ targets . leaf } ` ) ;
131-
132- function benchQuery ( fn , ...args ) {
133- const timings = [ ] ;
134- for ( let i = 0 ; i < QUERY_RUNS ; i ++ ) {
135- const start = performance . now ( ) ;
136- fn ( ...args ) ;
137- timings . push ( performance . now ( ) - start ) ;
138- }
139- return round1 ( median ( timings ) ) ;
140- }
141-
142- const queries = {
143- fnDepsMs : fnDepsData ? benchQuery ( fnDepsData , targets . hub , dbPath , { depth : 3 , noTests : true } ) : null ,
144- fnImpactMs : fnImpactData ? benchQuery ( fnImpactData , targets . hub , dbPath , { depth : 3 , noTests : true } ) : null ,
145- pathMs : pathData ? benchQuery ( pathData , targets . hub , targets . leaf , dbPath , { noTests : true } ) : null ,
146- rolesMs : rolesData ? benchQuery ( rolesData , dbPath , { noTests : true } ) : null ,
147- } ;
148-
149- return {
150- buildTimeMs : Math . round ( buildTimeMs ) ,
151- queryTimeMs : Math . round ( queryTimeMs * 10 ) / 10 ,
152- nodes : totalNodes ,
153- edges : totalEdges ,
154- files : totalFiles ,
155- dbSizeBytes,
156- perFile : {
157- buildTimeMs : Math . round ( ( buildTimeMs / totalFiles ) * 10 ) / 10 ,
158- nodes : Math . round ( ( totalNodes / totalFiles ) * 10 ) / 10 ,
159- edges : Math . round ( ( totalEdges / totalFiles ) * 10 ) / 10 ,
160- dbSizeBytes : Math . round ( dbSizeBytes / totalFiles ) ,
161- } ,
162- noopRebuildMs,
163- oneFileRebuildMs,
164- oneFilePhases,
165- queries,
166- phases : buildResult ?. phases || null ,
167- } ;
171+ oneFileRuns . sort ( ( a , b ) => a . ms - b . ms ) ;
172+ const medianRun = oneFileRuns [ Math . floor ( oneFileRuns . length / 2 ) ] ;
173+ oneFileRebuildMs = Math . round ( medianRun . ms ) ;
174+ oneFilePhases = medianRun . phases ;
175+ } finally {
176+ fs . writeFileSync ( PROBE_FILE , original ) ;
177+ await buildGraph ( root , { engine, incremental : true } ) ;
168178}
169179
170- // ── Run benchmarks ───────────────────────────────────────────────────────
171- const hasWasm = isWasmAvailable ( ) ;
172- const hasNative = isNativeAvailable ( ) ;
173-
174- if ( ! hasWasm && ! hasNative ) {
175- console . error ( 'Error: Neither WASM grammars nor native engine are available.' ) ;
176- console . error ( 'Run "npm run build:wasm" to build WASM grammars, or install the native platform package.' ) ;
177- process . exit ( 1 ) ;
178- }
180+ // ── Query benchmarks ────────────────────────────────────────────────
181+ console . error ( ` [${ engine } ] Benchmarking queries...` ) ;
182+ const targets = selectTargets ( ) ;
183+ console . error ( ` hub=${ targets . hub } , leaf=${ targets . leaf } ` ) ;
179184
180- let wasm = null ;
181- if ( hasWasm ) {
182- try {
183- wasm = await benchmarkEngine ( 'wasm' ) ;
184- } catch ( err ) {
185- console . error ( `WASM benchmark failed: ${ err ?. message ?? String ( err ) } ` ) ;
185+ function benchQuery ( fn , ... args ) {
186+ const timings = [ ] ;
187+ for ( let i = 0 ; i < QUERY_RUNS ; i ++ ) {
188+ const start = performance . now ( ) ;
189+ fn ( ... args ) ;
190+ timings . push ( performance . now ( ) - start ) ;
186191 }
187- } else {
188- console . error ( 'WASM grammars not built — skipping WASM benchmark' ) ;
192+ return round1 ( median ( timings ) ) ;
189193}
190194
191- let native = null ;
192- if ( hasNative ) {
193- try {
194- native = await benchmarkEngine ( 'native' ) ;
195- } catch ( err ) {
196- console . error ( `Native benchmark failed: ${ err ?. message ?? String ( err ) } ` ) ;
197- }
198- } else {
199- console . error ( 'Native engine not available — skipping native benchmark' ) ;
200- }
195+ const queries = {
196+ fnDepsMs : fnDepsData ? benchQuery ( fnDepsData , targets . hub , dbPath , { depth : 3 , noTests : true } ) : null ,
197+ fnImpactMs : fnImpactData ? benchQuery ( fnImpactData , targets . hub , dbPath , { depth : 3 , noTests : true } ) : null ,
198+ pathMs : pathData ? benchQuery ( pathData , targets . hub , targets . leaf , dbPath , { noTests : true } ) : null ,
199+ rolesMs : rolesData ? benchQuery ( rolesData , dbPath , { noTests : true } ) : null ,
200+ } ;
201201
202202// Restore console.log for JSON output
203203console . log = origLog ;
204204
205- const primary = wasm || native ;
206- if ( ! primary ) {
207- console . error ( 'Error: Both engines failed. No results to report.' ) ;
208- cleanup ( ) ;
209- process . exit ( 1 ) ;
210- }
211- const result = {
212- version,
213- date : new Date ( ) . toISOString ( ) . slice ( 0 , 10 ) ,
214- files : primary . files ,
215- wasm : wasm
216- ? {
217- buildTimeMs : wasm . buildTimeMs ,
218- queryTimeMs : wasm . queryTimeMs ,
219- nodes : wasm . nodes ,
220- edges : wasm . edges ,
221- dbSizeBytes : wasm . dbSizeBytes ,
222- perFile : wasm . perFile ,
223- noopRebuildMs : wasm . noopRebuildMs ,
224- oneFileRebuildMs : wasm . oneFileRebuildMs ,
225- oneFilePhases : wasm . oneFilePhases ,
226- queries : wasm . queries ,
227- phases : wasm . phases ,
228- }
229- : null ,
230- native : native
231- ? {
232- buildTimeMs : native . buildTimeMs ,
233- queryTimeMs : native . queryTimeMs ,
234- nodes : native . nodes ,
235- edges : native . edges ,
236- dbSizeBytes : native . dbSizeBytes ,
237- perFile : native . perFile ,
238- noopRebuildMs : native . noopRebuildMs ,
239- oneFileRebuildMs : native . oneFileRebuildMs ,
240- oneFilePhases : native . oneFilePhases ,
241- queries : native . queries ,
242- phases : native . phases ,
243- }
244- : null ,
205+ const workerResult = {
206+ buildTimeMs : Math . round ( buildTimeMs ) ,
207+ queryTimeMs : Math . round ( queryTimeMs * 10 ) / 10 ,
208+ nodes : totalNodes ,
209+ edges : totalEdges ,
210+ files : totalFiles ,
211+ dbSizeBytes,
212+ perFile : {
213+ buildTimeMs : Math . round ( ( buildTimeMs / totalFiles ) * 10 ) / 10 ,
214+ nodes : Math . round ( ( totalNodes / totalFiles ) * 10 ) / 10 ,
215+ edges : Math . round ( ( totalEdges / totalFiles ) * 10 ) / 10 ,
216+ dbSizeBytes : Math . round ( dbSizeBytes / totalFiles ) ,
217+ } ,
218+ noopRebuildMs,
219+ oneFileRebuildMs,
220+ oneFilePhases,
221+ queries,
222+ phases : buildResult ?. phases || null ,
245223} ;
246224
247- console . log ( JSON . stringify ( result , null , 2 ) ) ;
225+ console . log ( JSON . stringify ( workerResult ) ) ;
248226
249227cleanup ( ) ;
0 commit comments