test(perf): pull the switchCycleMs ceilings down to the warmed level (#747)

Since #735 switchCycleMs times the warmed twelve-switch cycle, and the
absolute ceilings of 7000 ms (flat) and 8000 ms (2.5D) sat 7.6-10.2
times above the level. performance_smoke judges only these ceilings, so
between full runs the warm floor switch that #694/#725 just sped up was
guarded only against a several-fold collapse.

Series: every Full Performance run after #735, both sides (the base is
measured by the candidate runner, so it is warm too), 7 samples each -
36821241343 (#735, base 76558bf2), 36838891001 (#740), 36838952536
(#742) and 36839009721 (#739), the last three against dev 7ff2b5ae.

  flat  large-house-v1 / plan-snap / interaction   666.9-812.7 ms
  2.5D  large-house-isometric / stage3-dense       766.5-1333.9 ms

The 2.5D maximum is the dense pair of 36838952536, whose base on the
same runner read 1249.7 ms against 849.9-982.4 ms elsewhere: runner
noise the series is meant to contain. No 3-sample performance_smoke
median is in the series yet; those profiles join Validate only on a
src/** diff.

Rule (as #692, #675 falls in the same band): one number per family, the
first multiple of 50 ms at or above 1.15 x M and no higher than 1.2 x M,
M being the family's maximum median: flat 1.15 x 812.7 = 934.6 -> 950
(+16.9 %), 2.5D 1.15 x 1333.9 = 1534.0 -> 1550 (+16.2 %). One number
per family keeps the smoke = full (#473 AC4), plan-snap/interaction
"every original ceiling" and dense = historical (#160) contracts; the
price is wider headroom for the faster profiles. The base-relative
comparison of the full workflow (0.35 / 0.2, 250 ms) is unchanged and
stays the detector for smaller growth.

The new test pins both families: one ceiling in every file of a family,
every point of the series passes the smoke budget with --absolute-only,
the ceiling follows the rule and stays inside [1.15, 1.2] x M, doubling
the level fails, and the full profiles' ratio and noise allowance are
unchanged. 7000 left in any flat file or a ceiling under 1.15 x M reds
it. The README records the series and the reasoning.

Issue: #747
User-Visible: no
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
This commit is contained in:
Claude
2026-10-01 13:54:35 +03:00
parent 7ce3f6629f
commit 51e2f2b510
9 changed files with 98 additions and 8 deletions
+28 -1
View File
@@ -87,7 +87,8 @@ profile's Long Task sums before and after #735 are not comparable. Local
diagnostics showed the median falling by roughly 1.6–2.8 times (for example
`large-house-v1` about 2.0 s to 0.9 s); the exact-SHA Full Performance medians
on both sides of the change are recorded in #735. Budgets and `hardMaxMs` did
not change.
not change in #735; #747 then brought the `switchCycleMs` ceilings down to the
warmed level (see "CI contracts" below).
Every report is tied to the source fingerprint embedded by Rollup. A stale
bundle is a hard failure.
@@ -146,6 +147,32 @@ wide margin. Occasional hosted runners are about a quarter faster, which a
ceiling does not mind. Smaller growth stays the job of the full workflow's
base-relative comparison.
The `switchCycleMs` ceilings are 950 ms for the flat family (`budgets.json`,
plan-snap, interaction and the interaction smoke) and 1550 ms for the 2.5D
family (both isometric profiles and the isometric smoke), one number per family
(#747). Since #735 the metric is the warmed twelve-switch cycle, and the former
7000 and 8000 ms sat 7.6–10.2 times above the #735 medians. The series is every
Full Performance run after #735, both sides with 7 samples — the base is
measured by the candidate runner, so it is warm too: 36821241343 (#735, base
`76558bf2`), 36838891001 (#740), 36838952536 (#742) and 36839009721 (#739), the
last three against `dev` `7ff2b5ae`. Flat medians span 666.9–812.7 ms (maximum:
`large-house-v1`, the base of 36838891001). 2.5D medians span 766.5–1333.9 ms;
the maximum is `isometric-stage3-dense-v1` in 36838952536, whose base on the
same runner read 1249.7 ms against 849.9–982.4 ms in the other runs — runner
noise of 27–47 %, which the series is meant to contain. Each ceiling is the
first multiple of 50 ms at or above 1.15 × that maximum and no higher than 1.2 ×
(the #692 band, which #675 also falls into): +16.9 % and +16.2 %. Doubling the
level fails by a wide margin. The price of one number per family is wider
headroom for the faster profiles (`large-house-isometric-v1`, maximum 1135.7 ms,
gets +36 %); it keeps the plan-snap and interaction contracts of "every original
ceiling" and the dense twin equal to the historical profile. Smaller growth
stays the job of the full workflow's base-relative comparison (unchanged: 0.35
flat, 0.2 2.5D, 250 ms noise allowance), and a floor built inside the window
already fails the sample through the #735 structural guard, not through time. No
3-sample `performance_smoke` median is in the series yet: those profiles join
Validate only on a `src/**` diff, so the first beta candidate after #747 is
their check.
The 2.5D View toggle (`viewToggleMs`) is reported by the isometric profiles but
budgeted by none of them (#720, owner decision in #694 on 2026-09-30). Switching
Flat ↔ 2.5D is a one-off General settings change, not something View or kiosk
@@ -61,7 +61,7 @@
},
"switchCycleMs": {
"stat": "median",
"hardMaxMs": 7000
"hardMaxMs": 950
}
},
"longTasks": {
@@ -33,7 +33,7 @@
},
"switchCycleMs": {
"stat": "median",
"hardMaxMs": 8000
"hardMaxMs": 1550
}
},
"longTasks": {
@@ -12,7 +12,7 @@
"resizePreviewMs": { "stat": "median", "maxRegressionRatio": 0.2, "noiseAllowanceMs": 150, "hardMaxMs": 2200 },
"panZoomMs": { "stat": "median", "maxRegressionRatio": 0.2, "noiseAllowanceMs": 60, "hardMaxMs": 600 },
"settingsDialogMs": { "stat": "median", "maxRegressionRatio": 0.2, "noiseAllowanceMs": 100, "hardMaxMs": 1200 },
"switchCycleMs": { "stat": "median", "maxRegressionRatio": 0.2, "noiseAllowanceMs": 250, "hardMaxMs": 8000 }
"switchCycleMs": { "stat": "median", "maxRegressionRatio": 0.2, "noiseAllowanceMs": 250, "hardMaxMs": 1550 }
},
"longTasks": {
"maxSingleMs": 3000,
@@ -91,7 +91,7 @@
"stat": "median",
"maxRegressionRatio": 0.35,
"noiseAllowanceMs": 250,
"hardMaxMs": 7000
"hardMaxMs": 950
}
},
"longTasks": {
@@ -10,7 +10,7 @@
"resizePreviewMs": { "stat": "median", "maxRegressionRatio": 0.2, "noiseAllowanceMs": 150, "hardMaxMs": 2200 },
"panZoomMs": { "stat": "median", "maxRegressionRatio": 0.2, "noiseAllowanceMs": 60, "hardMaxMs": 600 },
"settingsDialogMs": { "stat": "median", "maxRegressionRatio": 0.2, "noiseAllowanceMs": 100, "hardMaxMs": 1200 },
"switchCycleMs": { "stat": "median", "maxRegressionRatio": 0.2, "noiseAllowanceMs": 250, "hardMaxMs": 8000 }
"switchCycleMs": { "stat": "median", "maxRegressionRatio": 0.2, "noiseAllowanceMs": 250, "hardMaxMs": 1550 }
},
"longTasks": {
"maxSingleMs": 3000,
@@ -55,7 +55,7 @@
"stat": "median",
"maxRegressionRatio": 0.35,
"noiseAllowanceMs": 250,
"hardMaxMs": 7000
"hardMaxMs": 950
}
},
"longTasks": {
+1 -1
View File
@@ -49,7 +49,7 @@
"stat": "median",
"maxRegressionRatio": 0.35,
"noiseAllowanceMs": 250,
"hardMaxMs": 7000
"hardMaxMs": 950
}
},
"longTasks": {
+63
View File
@@ -307,6 +307,69 @@ test('isometric space switch ceiling covers the 2.5D runner level and still catc
}
});
// #747: обоснование потолков `switchCycleMs` и ряд — demo/performance/README.md,
// «CI contracts». Ряд — медианы Full Performance после #735 (7 образцов, обе
// стороны: база меряется раннером кандидата, окно тёплое) в порядке
// [база, кандидат] прогонов 36821241343 (#735, база 76558bf2), 36838891001
// (#740), 36838952536 (#742), 36839009721 (#739); у трёх последних база —
// `dev` 7ff2b5ae. Смоковых медиан (3 образца) в ряду пока нет.
const SWITCH_CYCLE_FAMILIES = {
flat: {
ceiling: 950,
smoke: 'budgets-interaction-smoke.json',
files: ['budgets.json', 'budgets-large-house-plan-snap.json', 'budgets-large-house-interaction.json',
'budgets-interaction-smoke.json'],
full: { maxRegressionRatio: 0.35, noiseAllowanceMs: 250 },
medians: {
'large-house-v1': [733.4, 683.3, 812.7, 749.9, 750.3, 800, 716.7, 717.9],
'large-house-plan-snap-v1': [716.7, 700.1, 762.6, 701, 733.3, 783.2, 749.9, 750.8],
'large-house-interaction-v1': [716.1, 754.1, 766.5, 666.9, 720.3, 750.1, 700.4, 703.4],
},
},
isometric: {
ceiling: 1550,
smoke: 'budgets-isometric-smoke.json',
files: ['budgets-large-house-isometric.json', 'budgets-isometric-stage3-dense.json', 'budgets-isometric-smoke.json'],
full: { maxRegressionRatio: 0.2, noiseAllowanceMs: 250 },
medians: {
'large-house-isometric-v1': [1069.5, 1051, 866.5, 766.5, 1134.9, 1100.4, 1135.7, 1089],
'isometric-stage3-dense-v1': [966.7, 983.2, 982.4, 916.5, 1249.7, 1333.9, 849.9, 803.6],
},
},
};
for (const [family, spec] of Object.entries(SWITCH_CYCLE_FAMILIES)) {
test(`#747 switchCycleMs (${family}): потолок над тёплым уровнем после #735 с запасом на шум раннера`, () => {
// Одно число на семью: смок = полный (#473 AC4), plan-snap и interaction
// сохраняют потолки large-house-v1, плотный двойник — исторические (#160).
for (const file of spec.files) {
const budget = readBudget(file).timings.switchCycleMs;
assert.equal(budget.hardMaxMs, spec.ceiling, `${file}: общий потолок switchCycleMs семьи ${family}`);
assert.equal(budget.stat, 'median', `${file}: метрика — медиана, как у соседей`);
}
const smoke = readBudget(spec.smoke);
const red = (ms) => evaluatePerformanceBudget({
candidate: absoluteSmokeReport(smoke, { switchCycleMs: ms }), budgets: smoke, absoluteOnly: true,
}).failures.some((check) => check.id === 'timing.switchCycleMs.median');
const series = Object.values(spec.medians).flat();
for (const median of series) assert.equal(red(median), false, `${median} из ряда краснеет на ${spec.ceiling}`);
const level = Math.max(...series);
// Правило #747: первое кратное 50 мс не ниже 1.15 × M и не выше 1.2 × M —
// полоса #692, в которую попадает и #675.
assert.equal(spec.ceiling, Math.ceil((level * 1.15) / 50) * 50, `потолок — первое кратное 50 над 1.15 × ${level}`);
assert.ok(spec.ceiling >= level * 1.15, 'запас над максимумом ряда не меньше 15 %');
assert.ok(spec.ceiling <= level * 1.2, 'и не больше 20 %: потолок остаётся гардом');
assert.equal(red(2 * level), true, 'удвоение уровня краснеет');
// Меньшее ловит относительное сравнение полного прогона: его коэффициент и
// допуск не рычаг.
for (const file of spec.files.filter((name) => name !== spec.smoke)) {
const budget = readBudget(file).timings.switchCycleMs;
assert.equal(budget.maxRegressionRatio, spec.full.maxRegressionRatio, `${file}: коэффициент не рычаг`);
assert.equal(budget.noiseAllowanceMs, spec.full.noiseAllowanceMs, `${file}: допуск не рычаг`);
}
});
}
test('boundary collision search restores the ordinary isometric allowances (#585)', () => {
const isometric = readBudget('budgets-large-house-isometric.json');
const dense = readBudget('budgets-isometric-stage3-dense.json');