Skip to content

Commit 5a52b6d

Browse files
fix(ci): refuse a comparison, and a setting, that cannot mean anything
Two suppressed review findings on #353, both real: - A zero or missing base mean makes the ratio Infinity, which clears the regression gate; a zero PR mean makes it 0, which clears the improvement gate. Either way a broken measurement was reported as a change. Such a row is now shown as 'not comparable', counted with the errored rows and never flagged. - ALERT_THRESHOLD_RATIO=1 flags every row in the run, below 1 swaps the two arms, and a negative ALERT_NOISE_SIGMAS makes the noise guard vacuously true - restoring the behaviour this PR removes. All three were accepted silently. They now fail the step with the expectation stated. Unset still means the calibrated default. 61 self-test checks. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
1 parent fe99379 commit 5a52b6d

1 file changed

Lines changed: 99 additions & 20 deletions

File tree

scripts/benchmark_comment.js

Lines changed: 99 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -73,13 +73,22 @@ const DEFAULT_NOISE_SIGMAS = 3;
7373
const COMMENT_MARKER = '<!-- celerity-benchmarks-comment -->';
7474

7575
function 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.
9099
function 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+
131161
function 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(/not comparable/g) || []).length], [3, 3]);
503+
check('no Infinity or NaN reaches the table', /Infinity|NaN/.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(/🆕 new/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

Comments
 (0)