@@ -73,13 +73,22 @@ const DEFAULT_NOISE_SIGMAS = 3;
7373const COMMENT_MARKER = '<!-- celerity-benchmarks-comment -->' ;
7474
7575function formatNs ( ns ) {
76- if ( ns == null ) return 'n/a' ;
76+ if ( ns == null || ! Number . isFinite ( ns ) ) return 'n/a' ;
7777 if ( ns < 1000 ) return `${ ns . toFixed ( 1 ) } ns` ;
7878 if ( ns < 1_000_000 ) return `${ ( ns / 1000 ) . toFixed ( 2 ) } μs` ;
7979 if ( ns < 1_000_000_000 ) return `${ ( ns / 1_000_000 ) . toFixed ( 2 ) } ms` ;
8080 return `${ ( ns / 1_000_000_000 ) . toFixed ( 2 ) } s` ;
8181}
8282
83+ // A mean has to be finite and positive for the ratio between two of them to mean anything.
84+ // A zero or missing base makes the ratio Infinity, which clears the regression gate; a zero
85+ // PR mean makes it 0, which clears the improvement gate. Either way the row would be
86+ // flagged on the strength of a broken measurement, which is precisely the failure mode this
87+ // file exists to remove.
88+ function isComparable ( prMean , baseMean ) {
89+ return Number . isFinite ( prMean ) && prMean > 0 && Number . isFinite ( baseMean ) && baseMean > 0 ;
90+ }
91+
8392// The flag decision for one paired row, kept as a named function so the self-test can
8493// exercise it directly against the real measurements that motivated it.
8594//
@@ -88,6 +97,8 @@ function formatNs(ns) {
8897// behaviour the previous rule had for the same input — this decides how much evidence a
8998// delta needs, and it must not silently drop a row it cannot judge.
9099function classifyDelta ( prMean , prStdDev , baseMean , baseStdDev , thresholdRatio , noiseSigmas ) {
100+ if ( ! isComparable ( prMean , baseMean ) ) return null ;
101+
91102 const ratio = prMean / baseMean ;
92103 const ps = Number . isFinite ( prStdDev ) ? prStdDev : 0 ;
93104 const bs = Number . isFinite ( baseStdDev ) ? baseStdDev : 0 ;
@@ -128,6 +139,25 @@ function noiseProfile(deltaPercents) {
128139 } ;
129140}
130141
142+ // A setting that silently falls back is how a gate gets weakened without anyone noticing,
143+ // and a setting that silently *accepts* nonsense is worse. `ALERT_THRESHOLD_RATIO=1` flags
144+ // every row in the run; `0.9` inverts the two gates so a speed-up is called a regression;
145+ // a negative `ALERT_NOISE_SIGMAS` makes the noise guard vacuously true and restores exactly
146+ // the behaviour this file was written to remove. None of those announce themselves in the
147+ // output, so they are refused here instead.
148+ //
149+ // An unset or empty variable is not an error — it means "use the calibrated default".
150+ function readSetting ( name , raw , fallback , isValid , expectation ) {
151+ const text = ( raw ?? '' ) . trim ( ) ;
152+ if ( text === '' ) return fallback ;
153+
154+ const value = Number ( text ) ;
155+ if ( ! Number . isFinite ( value ) || ! isValid ( value ) ) {
156+ throw new Error ( `${ name } =${ JSON . stringify ( text ) } is not usable: expected ${ expectation } .` ) ;
157+ }
158+ return value ;
159+ }
160+
131161function buildComment ( prReport , baseReport , options = { } ) {
132162 const thresholdRatio = options . thresholdRatio ?? DEFAULT_THRESHOLD_RATIO ;
133163 const noiseSigmas = options . noiseSigmas ?? DEFAULT_NOISE_SIGMAS ;
@@ -174,16 +204,22 @@ function buildComment(prReport, baseReport, options = {}) {
174204
175205 if ( baseStats ) {
176206 const baseMean = baseStats . Mean ;
177- const pct = ( prMean / baseMean - 1 ) * 100 ;
178- deltaCell = `${ pct >= 0 ? '+' : '' } ${ pct . toFixed ( 1 ) } %` ;
179- // A degenerate base (a zero or missing mean) yields Infinity or NaN, which would
180- // sort to the end of the noise profile and drag its upper percentiles with it. The
181- // row still gets a delta cell and a flag; it just does not calibrate anything.
182- if ( Number . isFinite ( pct ) ) deltaPercents . push ( Math . abs ( pct ) ) ;
183-
184- flag = classifyDelta ( prMean , prStdDev , baseMean , baseStats . StandardDeviation , thresholdRatio , noiseSigmas ) ;
185- if ( flag === 'regression' ) { deltaCell += ' ⚠️' ; regressions ++ ; }
186- else if ( flag === 'improvement' ) { deltaCell += ' ✅' ; improvements ++ ; }
207+ // A row whose ratio cannot mean anything is reported as such rather than rendered as
208+ // `+Infinity%` and flagged. It is counted with the errored rows because it is the
209+ // same thing from a reviewer's point of view — a case that produced no usable
210+ // comparison — and a silently uncounted anomaly reads exactly like a clean report.
211+ if ( ! isComparable ( prMean , baseMean ) ) {
212+ errored ++ ;
213+ deltaCell = '⚠️ not comparable' ;
214+ } else {
215+ const pct = ( prMean / baseMean - 1 ) * 100 ;
216+ deltaCell = `${ pct >= 0 ? '+' : '' } ${ pct . toFixed ( 1 ) } %` ;
217+ deltaPercents . push ( Math . abs ( pct ) ) ;
218+
219+ flag = classifyDelta ( prMean , prStdDev , baseMean , baseStats . StandardDeviation , thresholdRatio , noiseSigmas ) ;
220+ if ( flag === 'regression' ) { deltaCell += ' ⚠️' ; regressions ++ ; }
221+ else if ( flag === 'improvement' ) { deltaCell += ' ✅' ; improvements ++ ; }
222+ }
187223 }
188224
189225 entries . push ( {
@@ -210,7 +246,7 @@ function buildComment(prReport, baseReport, options = {}) {
210246 subtitle = `No significant change vs main (all within ±${ thresholdPct } % or inside ${ noiseSigmas } σ of the measurement noise).` ;
211247 }
212248 if ( errored > 0 ) {
213- subtitle += ` ⚠️ ${ errored } benchmark${ errored === 1 ? '' : 's' } errored ( no measurements) — see the table.` ;
249+ subtitle += ` ⚠️ ${ errored } benchmark${ errored === 1 ? '' : 's' } produced no usable comparison — see the table.` ;
214250 }
215251
216252 // The script is runnable outside Actions (that is the point of extracting it), where
@@ -448,14 +484,27 @@ function selfTest() {
448484 { Benchmarks : [
449485 { FullName : 'ABenchmark.A_Ok(ItemCount: 8)' , Statistics : statsFor ( 102 , 1 ) } ,
450486 { FullName : 'ABenchmark.A_ZeroBase(ItemCount: 8)' , Statistics : statsFor ( 100 , 1 ) } ,
487+ { FullName : 'ABenchmark.A_ZeroPr(ItemCount: 8)' , Statistics : statsFor ( 0 , 0 ) } ,
488+ { FullName : 'ABenchmark.A_NanPr(ItemCount: 8)' , Statistics : statsFor ( NaN , 1 ) } ,
451489 ] } ,
452490 { Benchmarks : [
453491 { FullName : 'ABenchmark.A_Ok(ItemCount: 8)' , Statistics : statsFor ( 100 , 1 ) } ,
454492 { FullName : 'ABenchmark.A_ZeroBase(ItemCount: 8)' , Statistics : statsFor ( 0 , 0 ) } ,
493+ { FullName : 'ABenchmark.A_ZeroPr(ItemCount: 8)' , Statistics : statsFor ( 100 , 1 ) } ,
494+ { FullName : 'ABenchmark.A_NanPr(ItemCount: 8)' , Statistics : statsFor ( 100 , 1 ) } ,
455495 ] } ,
456496 { } ,
457497 ) ;
458498 check ( 'a non-finite delta is kept out of the noise profile' , [ degenerate . noise . n , degenerate . noise . p95 . toFixed ( 1 ) ] , [ 1 , '2.0' ] ) ;
499+ // A zero base makes the ratio Infinity, a zero PR mean makes it 0. Both used to clear a
500+ // gate; neither may now be reported as a change.
501+ check ( 'a broken comparison is never flagged' , [ degenerate . regressions , degenerate . improvements ] , [ 0 , 0 ] ) ;
502+ check ( 'a broken comparison is counted and shown' , [ degenerate . errored , ( degenerate . body . match ( / n o t c o m p a r a b l e / g) || [ ] ) . length ] , [ 3 , 3 ] ) ;
503+ check ( 'no Infinity or NaN reaches the table' , / I n f i n i t y | N a N / . test ( degenerate . body ) , false ) ;
504+ check ( 'the subtitle owns up to it' , degenerate . body . includes ( '3 benchmarks produced no usable comparison' ) , true ) ;
505+ check ( 'classifyDelta refuses a zero base' , classifyDelta ( 100 , 1 , 0 , 0 , 1.10 , 3 ) , null ) ;
506+ check ( 'classifyDelta refuses a zero pr mean' , classifyDelta ( 0 , 0 , 100 , 1 , 1.10 , 3 ) , null ) ;
507+ check ( 'formatNs(NaN)' , formatNs ( NaN ) , 'n/a' ) ;
459508
460509 // Run outside Actions, neither BASE_SHA nor SHARD_TOTAL is set. The footer has to stay
461510 // readable rather than rendering an empty backtick pair and a shard count of nothing.
@@ -470,6 +519,26 @@ function selfTest() {
470519 check ( 'an empty base still renders' , noBase . regressions , 0 ) ;
471520 check ( 'an empty base reports every row as new' , ( noBase . body . match ( / 🆕 n e w / g) || [ ] ) . length , 5 ) ;
472521
522+ // ---- settings ----
523+ // Unset means "use the calibrated default"; a value that would quietly change what a
524+ // flag means is refused rather than accepted.
525+ const ratio = ( raw ) => readSetting ( 'ALERT_THRESHOLD_RATIO' , raw , DEFAULT_THRESHOLD_RATIO , ( v ) => v > 1 , 'x' ) ;
526+ const sigmas = ( raw ) => readSetting ( 'ALERT_NOISE_SIGMAS' , raw , DEFAULT_NOISE_SIGMAS , ( v ) => v >= 0 , 'x' ) ;
527+ const refused = ( fn , raw ) => { try { fn ( raw ) ; return false ; } catch { return true ; } } ;
528+
529+ check ( 'unset falls back to the default' , [ ratio ( undefined ) , ratio ( '' ) , ratio ( ' ' ) ] , [ 1.10 , 1.10 , 1.10 ] ) ;
530+ check ( 'a valid override is taken' , [ ratio ( '1.25' ) , sigmas ( '2.5' ) ] , [ 1.25 , 2.5 ] ) ;
531+ check ( 'zero sigmas survives the parser' , sigmas ( '0' ) , 0 ) ;
532+ // 1 makes every row past the gate; below 1 swaps the regression and improvement arms.
533+ check ( 'a threshold of 1 is refused' , refused ( ratio , '1' ) , true ) ;
534+ check ( 'a threshold below 1 is refused' , refused ( ratio , '0.9' ) , true ) ;
535+ // Negative sigmas makes the noise guard vacuously true — the defect this file fixes.
536+ check ( 'negative sigmas is refused' , refused ( sigmas , '-1' ) , true ) ;
537+ check ( 'a non-numeric setting is refused' , [ refused ( ratio , 'high' ) , refused ( sigmas , 'lots' ) ] , [ true , true ] ) ;
538+ // parseFloat('1.10abc') is 1.10; Number() is NaN. The stricter reading is the right one
539+ // for a setting that decides what gets called a regression.
540+ check ( 'a partly-numeric setting is refused' , refused ( ratio , '1.10abc' ) , true ) ;
541+
473542 if ( failures > 0 ) {
474543 console . error ( `\n${ failures } of ${ checks } check(s) failed.` ) ;
475544 process . exit ( 1 ) ;
@@ -506,18 +575,28 @@ function main() {
506575 process . exit ( 1 ) ;
507576 }
508577
509- const thresholdRatio = parseFloat ( process . env . ALERT_THRESHOLD_RATIO || '' ) ;
510- const noiseSigmas = parseFloat ( process . env . ALERT_NOISE_SIGMAS || '' ) ;
578+ let thresholdRatio ;
579+ let noiseSigmas ;
580+ try {
581+ thresholdRatio = readSetting (
582+ 'ALERT_THRESHOLD_RATIO' , process . env . ALERT_THRESHOLD_RATIO , DEFAULT_THRESHOLD_RATIO ,
583+ ( v ) => v > 1 , 'a ratio greater than 1 (1.10 flags a row that moved by a tenth)' ) ;
584+ // Zero is allowed and means "ratio gate only" — a legitimate way to see every row past
585+ // the threshold regardless of its spread.
586+ noiseSigmas = readSetting (
587+ 'ALERT_NOISE_SIGMAS' , process . env . ALERT_NOISE_SIGMAS , DEFAULT_NOISE_SIGMAS ,
588+ ( v ) => v >= 0 , 'zero or more sigmas' ) ;
589+ } catch ( err ) {
590+ console . error ( err . message ) ;
591+ process . exit ( 1 ) ;
592+ }
511593
512594 const result = buildComment (
513595 JSON . parse ( fs . readFileSync ( prPath , 'utf8' ) ) ,
514596 JSON . parse ( fs . readFileSync ( basePath , 'utf8' ) ) ,
515597 {
516- // Number.isFinite, not `||`: setting ALERT_NOISE_SIGMAS to 0 is a legitimate way to
517- // turn the noise guard off and leave the ratio gate alone, and `0 || 3` would
518- // silently reinstate the guard instead.
519- thresholdRatio : Number . isFinite ( thresholdRatio ) ? thresholdRatio : DEFAULT_THRESHOLD_RATIO ,
520- noiseSigmas : Number . isFinite ( noiseSigmas ) ? noiseSigmas : DEFAULT_NOISE_SIGMAS ,
598+ thresholdRatio,
599+ noiseSigmas,
521600 missingShards : process . env . MISSING_SHARDS ,
522601 baseSha : process . env . BASE_SHA ,
523602 shardTotal : process . env . SHARD_TOTAL ,
@@ -539,4 +618,4 @@ if (require.main === module) {
539618 main ( ) ;
540619}
541620
542- module . exports = { buildComment, classifyDelta, formatNs, noiseProfile, COMMENT_MARKER } ;
621+ module . exports = { buildComment, classifyDelta, formatNs, isComparable , noiseProfile, readSetting , COMMENT_MARKER } ;
0 commit comments