From 2079b59a66be3097fc37e5d73d49b4f5cb80aec7 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 28 Sep 2026 06:18:09 +0000 Subject: [PATCH] fix(performance): treat a counter/wall-clock type switch as a weakening Turning `{ value: 100, slack: 10 }` into `{ value: 105, toleranceRatio: 1 }` skipped the raised-counter-value rule and was reported as a lowered limit. Switching an entry between counter and wall-clock now needs the perf-baseline-increase label. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_0166PWobUjpoiWt8E9bBdsCs --- docs/architecture/performance-journeys.md | 5 +-- .../performance/check-baseline-direction.mjs | 8 +++++ .../check-baseline-direction.test.mjs | 31 +++++++++++++++++++ 3 files changed, 42 insertions(+), 2 deletions(-) diff --git a/docs/architecture/performance-journeys.md b/docs/architecture/performance-journeys.md index fbc445d14..5c79decab 100644 --- a/docs/architecture/performance-journeys.md +++ b/docs/architecture/performance-journeys.md @@ -359,8 +359,9 @@ The job also refuses a weakened baselines file: entry's enforced limit (`value × toleranceRatio` or `value + slack`) went up, a tolerance or slack widened or an entry disappeared, so a PR cannot grow the payload and raise the baseline to match. A counter's `value` may not go up -either, even when narrower slack lowers its limit. Lowered limits and new -entries pass. +either, even when narrower slack lowers its limit, and switching an entry +between counter and wall-clock (adding or removing `toleranceRatio`) counts +as a weakening too. Lowered limits and new entries pass. Baselines only move down. Lower `value` in the same PR as the change that earned it, set `updatedAt` and `evidencePr`, and paste the measurement output diff --git a/tools/performance/check-baseline-direction.mjs b/tools/performance/check-baseline-direction.mjs index b0b8b9949..41816767c 100644 --- a/tools/performance/check-baseline-direction.mjs +++ b/tools/performance/check-baseline-direction.mjs @@ -9,6 +9,8 @@ * or `value + slack`) went up, whose tolerance or slack widened, or that * disappeared, is a failure. A counter's `value` may not go up either, even * when narrower slack lowers its limit: the value is the measured evidence. + * Switching an entry between counter and wall-clock (adding or removing + * `toleranceRatio`) is a weakening too, so that rule cannot be sidestepped. * New entries and lowered limits pass. * * `--allow-increase` turns those failures into printed "allowed" lines. CI @@ -81,6 +83,8 @@ export function compareBaselineDirection({ ); continue; } + const kind = (entry) => + entry.toleranceRatio === undefined ? 'counter' : 'wall-clock'; const baseTolerance = baseEntry.toleranceRatio ?? 1; const headTolerance = headEntry.toleranceRatio ?? 1; const baseSlack = baseEntry.slack ?? 0; @@ -91,6 +95,10 @@ export function compareBaselineDirection({ weakened.push( `${label}: toleranceRatio widened from ${baseTolerance} to ${headTolerance}. Tolerances are a maintainer decision (the ${BASELINE_INCREASE_LABEL} label); a PR may only narrow them.` ); + } else if (kind(baseEntry) !== kind(headEntry)) { + weakened.push( + `${label}: changed from a ${kind(baseEntry)} entry to a ${kind(headEntry)} entry. The two are read from different summary sections and compared differently, so a type change is a maintainer decision (the ${BASELINE_INCREASE_LABEL} label).` + ); } else if (headSlack > baseSlack) { weakened.push( `${label}: slack widened from ${formatNumber(baseSlack)} to ${formatNumber(headSlack)}${unit}. Slack is a maintainer decision (the ${BASELINE_INCREASE_LABEL} label); a PR may only narrow it.` diff --git a/tools/performance/check-baseline-direction.test.mjs b/tools/performance/check-baseline-direction.test.mjs index 54b1346a2..86f2a7e03 100644 --- a/tools/performance/check-baseline-direction.test.mjs +++ b/tools/performance/check-baseline-direction.test.mjs @@ -213,6 +213,37 @@ test('a counter value raised behind narrower slack still fails', () => { assert.equal(allowed.allowed.length, 1); }); +test('switching an entry between counter and wall-clock fails', () => { + const counterToWallClock = compareBaselineDirection({ + base: file(100, { x: { value: 100, slack: 10 } }), + head: file(100, { x: { value: 105, toleranceRatio: 1 } }), + }); + assert.equal(counterToWallClock.failures.length, 1); + assert.deepEqual(counterToWallClock.lowered, []); + assert.match( + counterToWallClock.failures[0], + /launch\/x: changed from a counter entry to a wall-clock entry/ + ); + + const wallClockToCounter = compareBaselineDirection({ + base: file(100, { x: { value: 100, toleranceRatio: 1.25 } }), + head: file(100, { x: { value: 90 } }), + }); + assert.equal(wallClockToCounter.failures.length, 1); + assert.match( + wallClockToCounter.failures[0], + /changed from a wall-clock entry to a counter entry/ + ); + + const allowed = compareBaselineDirection({ + base: file(100, { x: { value: 100, slack: 10 } }), + head: file(100, { x: { value: 105, toleranceRatio: 1 } }), + allowIncrease: true, + }); + assert.deepEqual(allowed.failures, []); + assert.equal(allowed.allowed.length, 1); +}); + test('allowIncrease reports every weakening as allowed instead of failing', () => { const result = compareBaselineDirection({ base: file(100, { cdTicks: { value: 5 } }),