11'use strict' ;
22
33const { spawn, fork } = require ( 'node:child_process' ) ;
4+ const { closeSync, openSync, writeSync } = require ( 'node:fs' ) ;
45const { inspect } = require ( 'util' ) ;
56const path = require ( 'path' ) ;
67const CLI = require ( './_cli.js' ) ;
@@ -27,7 +28,9 @@ const cli = new CLI(`usage: ./node compare.js [options] [--] <category> ...
2728 --no-progress don't show benchmark progress indicator
2829 --analyze perform statistical analysis after benchmarks
2930 complete (Welch's t-test, effect size) instead
30- of printing csv output
31+ of printing csv output to stdout
32+ --csv filename write csv output to filename (can be combined
33+ with --analyze). Use ` - ` to write to stdout.
3134 --scale 1000 rate-to-integer multiplier for histogram
3235 precision when using --analyze (default: 1000)
3336 --max-regression N exit with code 1 if any statistically
@@ -60,6 +63,20 @@ if (benchmarks.length === 0) {
6063 return ;
6164}
6265
66+ const cvsToStdout = cli . optional . csv === '-' ;
67+ const csvFd = cli . optional . csv === undefined || cvsToStdout ?
68+ null :
69+ openSync ( cli . optional . csv , 'w' ) ;
70+ const outputCsv = ! analyze || csvFd !== null || cvsToStdout ;
71+
72+ function writeCsv ( line ) {
73+ if ( csvFd === null ) {
74+ writeSync ( process . stdout . fd , `${ line } \n` ) ;
75+ } else {
76+ writeSync ( csvFd , `${ line } \n` ) ;
77+ }
78+ }
79+
6380// When --analyze is set, collect results for statistical analysis.
6481const results = analyze ? new Map ( ) : null ;
6582
@@ -78,17 +95,19 @@ for (const filename of benchmarks) {
7895}
7996// queue.length = binary.length * runs * benchmarks.length
8097
81- // Print csv header ( unless analyzing inline) .
82- if ( ! analyze ) {
83- console . log ( '"binary","filename","configuration","rate","time"' ) ;
98+ // Print csv header unless only analyzing inline.
99+ if ( outputCsv ) {
100+ writeCsv ( '"binary","filename","configuration","rate","time"' ) ;
84101}
85102
86103const kStartOfQueue = 0 ;
87104
88- const showProgress = ! cli . optional [ 'no-progress' ] ;
105+ const showProgress = ! cli . optional [ 'no-progress' ] && ! cvsToStdout ;
89106let progress ;
90107if ( showProgress ) {
91- progress = new BenchmarkProgress ( queue , benchmarks , { analyze } ) ;
108+ progress = new BenchmarkProgress ( queue , benchmarks , {
109+ analyze : analyze || csvFd !== null ,
110+ } ) ;
92111 progress . startQueue ( kStartOfQueue ) ;
93112}
94113
@@ -126,11 +145,13 @@ if (showProgress) {
126145 results . set ( name , { old : [ ] , new : [ ] } ) ;
127146 }
128147 results . get ( name ) [ job . binary ] . push ( data . rate ) ;
129- } else {
148+ }
149+
150+ if ( outputCsv ) {
130151 // Escape quotes (") for correct csv formatting
131- conf = conf . replace ( / " / g, '""' ) ;
132- console . log ( `"${ job . binary } ","${ job . filename } ","${ conf } ",` +
133- `${ data . rate } ,${ data . time } ` ) ;
152+ const csvConf = conf . replace ( / " / g, '""' ) ;
153+ writeCsv ( `"${ job . binary } ","${ job . filename } ","${ csvConf } ",` +
154+ `${ data . rate } ,${ data . time } ` ) ;
134155 }
135156 if ( showProgress ) {
136157 // One item in the subqueue has been completed.
@@ -153,8 +174,9 @@ if (showProgress) {
153174 // If there are more benchmarks execute the next
154175 if ( i + 1 < queue . length ) {
155176 recursive ( i + 1 ) ;
156- } else if ( analyze ) {
157- printAnalysis ( results , scale , maxRegression ) ;
177+ } else {
178+ if ( csvFd !== null ) closeSync ( csvFd ) ;
179+ if ( analyze ) printAnalysis ( results , scale , maxRegression ) ;
158180 }
159181 } ) ;
160182} ) ( kStartOfQueue ) ;
@@ -261,40 +283,40 @@ function printAnalysis(results, scale, maxRegression) {
261283 const pad = ( s , n ) => s + ' ' . repeat ( Math . max ( 0 , n - s . length ) ) ;
262284 const rpad = ( s , n ) => ' ' . repeat ( Math . max ( 0 , n - s . length ) ) + s ;
263285
264- console . log ( `${ pad ( '' , maxNameLen ) } confidence` +
265- ` improvement accuracy (*) (**) (***)` ) ;
286+ writeSync ( process . stdout . fd , `${ pad ( '' , maxNameLen ) } confidence` +
287+ ` improvement accuracy (*) (**) (***)\n ` ) ;
266288
267289 for ( const row of rows ) {
268290 const imp = `${ row . improvement >= 0 ? '+' : '' } ${ row . improvement . toFixed ( 2 ) } %` ;
269- console . log (
291+ writeSync ( process . stdout . fd ,
270292 `${ pad ( row . name , maxNameLen ) } ${ pad ( row . stars , 10 ) } ` +
271293 ` ${ rpad ( imp , 11 ) } ` +
272294 ` ±${ row . ci95 . toFixed ( 2 ) } %` +
273295 ` ±${ row . ci99 . toFixed ( 2 ) } %` +
274296 ` ±${ row . ci999 . toFixed ( 2 ) } %` +
275- `${ row . inconclusive ? ' (inconclusive)' : '' } ` ,
297+ `${ row . inconclusive ? ' (inconclusive)' : '' } \n ` ,
276298 ) ;
277299 }
278300
279301 if ( skipped > 0 ) {
280- console . log ( ' ') ;
281- console . log (
302+ writeSync ( process . stdout . fd , '\n ') ;
303+ writeSync ( process . stdout . fd ,
282304 `Note: ${ skipped } configuration${ skipped === 1 ? ' was' : 's were' } ` +
283305 ` skipped because Welch's t-test requires at least 2 samples per` +
284- ` binary. Use --runs 2 or higher.` ,
306+ ` binary. Use --runs 2 or higher.\n ` ,
285307 ) ;
286308 }
287309
288310 // --- Bar chart visualization ---
289311 printChart ( rows , maxNameLen ) ;
290312
291- console . log ( ' ') ;
292- console . log (
313+ writeSync ( process . stdout . fd , '\n ') ;
314+ writeSync ( process . stdout . fd ,
293315 `Rates were scaled by ${ scale } x into HdrHistogram (3 significant figures).\n` +
294- `Use --scale to adjust precision if needed.\n` ,
316+ `Use --scale to adjust precision if needed.\n\n ` ,
295317 ) ;
296318 const anyFamilyWise = rows . filter ( ( r ) => r . pAdjusted < 0.05 ) . length ;
297- console . log (
319+ writeSync ( process . stdout . fd ,
298320 `Be aware that when doing many comparisons the risk of a false-positive\n` +
299321 `result increases. In this case, there are ${ rows . length } comparisons, ` +
300322 `you can thus\nexpect the following amount of false-positive results:\n` +
@@ -307,19 +329,19 @@ function printAnalysis(results, scale, maxRegression) {
307329 `\nThe stars above are per-benchmark and uncorrected. Adjusting for the ` +
308330 `size of\nthis comparison set (Holm-Bonferroni), ${ anyFamilyWise } ` +
309331 `comparison${ anyFamilyWise === 1 ? '' : 's' } remain${ anyFamilyWise === 1 ? 's' : '' } ` +
310- `significant at 5%.\n--max-regression uses the corrected values.` ,
332+ `significant at 5%.\n--max-regression uses the corrected values.\n ` ,
311333 ) ;
312334
313335 // Gate: exit with error if any regression is shown to exceed the limit.
314336 if ( maxRegression > 0 ) {
315337 if ( underpowered > 0 ) {
316- console . log ( ' ') ;
317- console . log (
338+ writeSync ( process . stdout . fd , '\n ') ;
339+ writeSync ( process . stdout . fd ,
318340 `Note: ${ underpowered } of ${ rows . length } comparison` +
319341 `${ rows . length === 1 ? '' : 's' } could not resolve an effect as ` +
320342 `small as ${ maxRegression } %, and are marked (inconclusive). They are ` +
321343 `not\nevidence of no regression -- the samples are too noisy to tell. ` +
322- `Raise --runs,\nor pin cores with --set CPUSET, to narrow them.` ,
344+ `Raise --runs,\nor pin cores with --set CPUSET, to narrow them.\n ` ,
323345 ) ;
324346 }
325347
@@ -340,18 +362,18 @@ function printAnalysis(results, scale, maxRegression) {
340362 ) ;
341363
342364 if ( failures . length > 0 ) {
343- console . log ( ' ') ;
344- console . log (
365+ writeSync ( process . stdout . fd , '\n ') ;
366+ writeSync ( process . stdout . fd ,
345367 `FAIL: ${ failures . length } benchmark${ failures . length === 1 ? '' : 's' } ` +
346368 ` regressed by more than ${ maxRegression } %` +
347369 ` (interval excludes the threshold,\n` +
348- `family-wise corrected across ${ rows . length } comparisons):` ,
370+ `family-wise corrected across ${ rows . length } comparisons):\n ` ,
349371 ) ;
350372 for ( const f of failures ) {
351- console . log (
373+ writeSync ( process . stdout . fd ,
352374 ` ${ f . name } ${ f . improvement . toFixed ( 2 ) } % ` +
353375 `(95% CI up to ${ ( f . improvement + f . ci95 ) . toFixed ( 2 ) } %, ` +
354- `adjusted p=${ f . pAdjusted . toExponential ( 2 ) } )` ,
376+ `adjusted p=${ f . pAdjusted . toExponential ( 2 ) } )\n ` ,
355377 ) ;
356378 }
357379 process . exitCode = 1 ;
@@ -388,8 +410,8 @@ function printChart(rows, maxNameLen) {
388410 axisCenter +
389411 ' ' . repeat ( Math . max ( 0 , halfWidth - Math . ceil ( axisCenter . length / 2 ) - axisRight . length ) ) +
390412 axisRight ;
391- console . log ( ' ') ;
392- console . log ( leftLabel ) ;
413+ writeSync ( process . stdout . fd , '\n ') ;
414+ writeSync ( process . stdout . fd , ` ${ leftLabel } \n` ) ;
393415
394416 for ( const row of rows ) {
395417 const imp = row . improvement ;
@@ -421,6 +443,6 @@ function printChart(rows, maxNameLen) {
421443
422444 const label = `${ row . improvement >= 0 ? '+' : '' } ${ row . improvement . toFixed ( 2 ) } %` ;
423445 const sig = row . stars . trim ( ) ;
424- console . log ( `${ pad ( row . name , maxNameLen ) } ${ chars . join ( '' ) } ${ label } ${ sig } ` ) ;
446+ writeSync ( process . stdout . fd , `${ pad ( row . name , maxNameLen ) } ${ chars . join ( '' ) } ${ label } ${ sig } \n ` ) ;
425447 }
426448}
0 commit comments