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,16 @@ 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+ writeSync ( csvFd || process . stdout . fd , `${ line } \n` ) ;
74+ }
75+
6376// When --analyze is set, collect results for statistical analysis.
6477const results = analyze ? new Map ( ) : null ;
6578
@@ -78,17 +91,19 @@ for (const filename of benchmarks) {
7891}
7992// queue.length = binary.length * runs * benchmarks.length
8093
81- // Print csv header ( unless analyzing inline) .
82- if ( ! analyze ) {
83- console . log ( '"binary","filename","configuration","rate","time"' ) ;
94+ // Print csv header unless only analyzing inline.
95+ if ( outputCsv ) {
96+ writeCsv ( '"binary","filename","configuration","rate","time"' ) ;
8497}
8598
8699const kStartOfQueue = 0 ;
87100
88- const showProgress = ! cli . optional [ 'no-progress' ] ;
101+ const showProgress = ! cli . optional [ 'no-progress' ] && ! cvsToStdout ;
89102let progress ;
90103if ( showProgress ) {
91- progress = new BenchmarkProgress ( queue , benchmarks , { analyze } ) ;
104+ progress = new BenchmarkProgress ( queue , benchmarks , {
105+ analyze : analyze || csvFd !== null ,
106+ } ) ;
92107 progress . startQueue ( kStartOfQueue ) ;
93108}
94109
@@ -126,11 +141,13 @@ if (showProgress) {
126141 results . set ( name , { old : [ ] , new : [ ] } ) ;
127142 }
128143 results . get ( name ) [ job . binary ] . push ( data . rate ) ;
129- } else {
144+ }
145+
146+ if ( outputCsv ) {
130147 // Escape quotes (") for correct csv formatting
131- conf = conf . replace ( / " / g, '""' ) ;
132- console . log ( `"${ job . binary } ","${ job . filename } ","${ conf } ",` +
133- `${ data . rate } ,${ data . time } ` ) ;
148+ const csvConf = conf . replace ( / " / g, '""' ) ;
149+ writeCsv ( `"${ job . binary } ","${ job . filename } ","${ csvConf } ",` +
150+ `${ data . rate } ,${ data . time } ` ) ;
134151 }
135152 if ( showProgress ) {
136153 // One item in the subqueue has been completed.
@@ -153,8 +170,9 @@ if (showProgress) {
153170 // If there are more benchmarks execute the next
154171 if ( i + 1 < queue . length ) {
155172 recursive ( i + 1 ) ;
156- } else if ( analyze ) {
157- printAnalysis ( results , scale , maxRegression ) ;
173+ } else {
174+ if ( csvFd !== null ) closeSync ( csvFd ) ;
175+ if ( analyze ) printAnalysis ( results , scale , maxRegression ) ;
158176 }
159177 } ) ;
160178} ) ( kStartOfQueue ) ;
@@ -261,41 +279,41 @@ function printAnalysis(results, scale, maxRegression) {
261279 const pad = ( s , n ) => s + ' ' . repeat ( Math . max ( 0 , n - s . length ) ) ;
262280 const rpad = ( s , n ) => ' ' . repeat ( Math . max ( 0 , n - s . length ) ) + s ;
263281
264- console . log ( `${ pad ( '' , maxNameLen ) } confidence` +
265- ` improvement accuracy (*) (**) (***)` ) ;
282+ writeSync ( process . stdout . fd , `${ pad ( '' , maxNameLen ) } confidence` +
283+ ` improvement accuracy (*) (**) (***)\n ` ) ;
266284
267285 for ( const row of rows ) {
268286 const imp = `${ row . improvement >= 0 ? '+' : '' } ${ row . improvement . toFixed ( 2 ) } %` ;
269- console . log (
270- `${ pad ( row . name , maxNameLen ) } ${ pad ( row . stars , 10 ) } ` +
287+ writeSync ( process . stdout . fd ,
288+ `${ pad ( row . name , maxNameLen ) } ${ pad ( row . stars , 10 ) } ` +
271289 ` ${ rpad ( imp , 11 ) } ` +
272290 ` ±${ row . ci95 . toFixed ( 2 ) } %` +
273291 ` ±${ row . ci99 . toFixed ( 2 ) } %` +
274292 ` ±${ row . ci999 . toFixed ( 2 ) } %` +
275- `${ row . inconclusive ? ' (inconclusive)' : '' } ` ,
293+ `${ row . inconclusive ? ' (inconclusive)' : '' } \n ` ,
276294 ) ;
277295 }
278296
279297 if ( skipped > 0 ) {
280- console . log ( ' ') ;
281- console . log (
282- `Note: ${ skipped } configuration${ skipped === 1 ? ' was' : 's were' } ` +
298+ writeSync ( process . stdout . fd , '\n ') ;
299+ writeSync ( process . stdout . fd ,
300+ `Note: ${ skipped } configuration${ skipped === 1 ? ' was' : 's were' } ` +
283301 ` skipped because Welch's t-test requires at least 2 samples per` +
284- ` binary. Use --runs 2 or higher.` ,
302+ ` binary. Use --runs 2 or higher.\n ` ,
285303 ) ;
286304 }
287305
288306 // --- Bar chart visualization ---
289307 printChart ( rows , maxNameLen ) ;
290308
291- console . log ( ' ') ;
292- console . log (
293- `Rates were scaled by ${ scale } x into HdrHistogram (3 significant figures).\n` +
294- `Use --scale to adjust precision if needed.\n` ,
309+ writeSync ( process . stdout . fd , '\n ') ;
310+ writeSync ( process . stdout . fd ,
311+ `Rates were scaled by ${ scale } x into HdrHistogram (3 significant figures).\n` +
312+ `Use --scale to adjust precision if needed.\n\n ` ,
295313 ) ;
296314 const anyFamilyWise = rows . filter ( ( r ) => r . pAdjusted < 0.05 ) . length ;
297- console . log (
298- `Be aware that when doing many comparisons the risk of a false-positive\n` +
315+ writeSync ( process . stdout . fd ,
316+ `Be aware that when doing many comparisons the risk of a false-positive\n` +
299317 `result increases. In this case, there are ${ rows . length } comparisons, ` +
300318 `you can thus\nexpect the following amount of false-positive results:\n` +
301319 ` ${ ( rows . length * 0.05 ) . toFixed ( 2 ) } false positives, when considering ` +
@@ -307,19 +325,19 @@ function printAnalysis(results, scale, maxRegression) {
307325 `\nThe stars above are per-benchmark and uncorrected. Adjusting for the ` +
308326 `size of\nthis comparison set (Holm-Bonferroni), ${ anyFamilyWise } ` +
309327 `comparison${ anyFamilyWise === 1 ? '' : 's' } remain${ anyFamilyWise === 1 ? 's' : '' } ` +
310- `significant at 5%.\n--max-regression uses the corrected values.` ,
328+ `significant at 5%.\n--max-regression uses the corrected values.\n ` ,
311329 ) ;
312330
313331 // Gate: exit with error if any regression is shown to exceed the limit.
314332 if ( maxRegression > 0 ) {
315333 if ( underpowered > 0 ) {
316- console . log ( ' ') ;
317- console . log (
318- `Note: ${ underpowered } of ${ rows . length } comparison` +
334+ writeSync ( process . stdout . fd , '\n ') ;
335+ writeSync ( process . stdout . fd ,
336+ `Note: ${ underpowered } of ${ rows . length } comparison` +
319337 `${ rows . length === 1 ? '' : 's' } could not resolve an effect as ` +
320338 `small as ${ maxRegression } %, and are marked (inconclusive). They are ` +
321339 `not\nevidence of no regression -- the samples are too noisy to tell. ` +
322- `Raise --runs,\nor pin cores with --set CPUSET, to narrow them.` ,
340+ `Raise --runs,\nor pin cores with --set CPUSET, to narrow them.\n ` ,
323341 ) ;
324342 }
325343
@@ -340,18 +358,18 @@ function printAnalysis(results, scale, maxRegression) {
340358 ) ;
341359
342360 if ( failures . length > 0 ) {
343- console . log ( ' ') ;
344- console . log (
345- `FAIL: ${ failures . length } benchmark${ failures . length === 1 ? '' : 's' } ` +
361+ writeSync ( process . stdout . fd , '\n ') ;
362+ writeSync ( process . stdout . fd ,
363+ `FAIL: ${ failures . length } benchmark${ failures . length === 1 ? '' : 's' } ` +
346364 ` regressed by more than ${ maxRegression } %` +
347365 ` (interval excludes the threshold,\n` +
348- `family-wise corrected across ${ rows . length } comparisons):` ,
366+ `family-wise corrected across ${ rows . length } comparisons):\n ` ,
349367 ) ;
350368 for ( const f of failures ) {
351- console . log (
352- ` ${ f . name } ${ f . improvement . toFixed ( 2 ) } % ` +
369+ writeSync ( process . stdout . fd ,
370+ ` ${ f . name } ${ f . improvement . toFixed ( 2 ) } % ` +
353371 `(95% CI up to ${ ( f . improvement + f . ci95 ) . toFixed ( 2 ) } %, ` +
354- `adjusted p=${ f . pAdjusted . toExponential ( 2 ) } )` ,
372+ `adjusted p=${ f . pAdjusted . toExponential ( 2 ) } )\n ` ,
355373 ) ;
356374 }
357375 process . exitCode = 1 ;
@@ -388,8 +406,8 @@ function printChart(rows, maxNameLen) {
388406 axisCenter +
389407 ' ' . repeat ( Math . max ( 0 , halfWidth - Math . ceil ( axisCenter . length / 2 ) - axisRight . length ) ) +
390408 axisRight ;
391- console . log ( ' ') ;
392- console . log ( leftLabel ) ;
409+ writeSync ( process . stdout . fd , '\n ') ;
410+ writeSync ( process . stdout . fd , ` ${ leftLabel } \n` ) ;
393411
394412 for ( const row of rows ) {
395413 const imp = row . improvement ;
@@ -421,6 +439,6 @@ function printChart(rows, maxNameLen) {
421439
422440 const label = `${ row . improvement >= 0 ? '+' : '' } ${ row . improvement . toFixed ( 2 ) } %` ;
423441 const sig = row . stars . trim ( ) ;
424- console . log ( `${ pad ( row . name , maxNameLen ) } ${ chars . join ( '' ) } ${ label } ${ sig } ` ) ;
442+ writeSync ( process . stdout . fd , `${ pad ( row . name , maxNameLen ) } ${ chars . join ( '' ) } ${ label } ${ sig } \n ` ) ;
425443 }
426444}
0 commit comments