Files
houseplan-card/test/performance-budget.test.mjs
T
Claudeandclaude[bot] b18d366e8c test(perf): 2.5D backdrop twin profile judges one update pass per floor switch (#743)
No Full Performance profile walked the backdrop (imagePlan) path: every
large-house fixture has plan_url null. So the #739 K1 double render -- a
warm 2.5D floor switch with a backdrop cleared the ready paper, inserted
the veil, probed the card background and rendered a second time -- was
invisible to CI by time and structurally; a temporary probe found it.

large-house-isometric-backdrop-v1 is the twin of large-house-isometric-v1
with the shipped f1.svg under a URL of its own on every floor
(plan_aspect 1, room geometry unchanged). The variant is derived in the
runner as plan-snap's is, so demo/fixtures and the bundle fingerprint do
not change. A first stable frame without the backdrop image fails the
sample. After the switchCycle window and its #735 guard, before forced
GC and outside every timed window, a probe makes six warm switches and
counts performUpdate passes until updateComplete resolves true: the K1
second pass starts after the first updateComplete resolves, so a count
taken right after the first await reads one on both sides.

evaluate.mjs rejects a candidate of this profile unless every switch took
one pass, or when the probe is missing; the base is reported, not judged
(v1.78.0 and dev before #739 take two). The budget is a copy of the
historical isometric budget under the new profile id. performance.yml
gains the isometric-backdrop matrix entry with exact-SHA comparison.

Witness: a tree with #739 reverted reads perSwitch 2 in every sample and
benchmark:compare against the branch report throws on the pass count;
v1.78.0 reads 2, the branch reads 1.

Issue: #743
User-Visible: no
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
2026-10-01 15:41:48 +00:00

488 lines
26 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import test from 'node:test';
import assert from 'node:assert/strict';
import { evaluatePerformanceBudget } from '../demo/performance/evaluate.mjs';
const budgets = {
schema: 1,
profile: 'large-house-v1',
minimumSamples: 2,
timings: {
firstStableRenderMs: {
stat: 'median', maxRegressionRatio: 0.25, noiseAllowanceMs: 10, hardMaxMs: 500,
},
},
longTasks: {
maxSingleMs: 200, maxCountP95: 10, maxTotalP95Ms: 500,
maxSingleRegressionRatio: 0.5, maxSingleNoiseAllowanceMs: 20,
maxCountRegressionRatio: 0.5, countNoiseAllowance: 2,
maxTotalRegressionRatio: 0.5, noiseAllowanceMs: 20,
},
heap: {
required: true, hardMaxGrowthBytes: 1000, maxRegressionRatio: 0.5, noiseAllowanceBytes: 100,
},
cacheEntries: { cleanFloor: 3 },
cacheGrowth: { cleanFloor: 0 },
renderedDevices: 200,
};
const report = ({ timing = 100, heap = 100, cache = 3, growth = 0, long = 20 } = {}) => ({
schema: 2,
profile: 'large-house-v1',
sourceSha: '1'.repeat(40),
buildFingerprint: `fixture-${timing}`,
runtime: { node: 'v22.0.0', chromium: '1.2.3', platform: 'linux', arch: 'x64' },
fixture: { rooms: 60 },
summary: { firstStableRenderMs: { median: timing, p95: timing, min: timing, max: timing } },
longTasks: { maxSingleMs: long, countP95: 1, totalP95Ms: long },
rows: [0, 1].map(() => ({
heapGrowthBytes: heap,
preciseGc: true,
longTasks: { load: { supported: true, count: 1, maxMs: long, totalMs: long } },
cacheEntries: { cleanFloor: cache },
cacheGrowth: { cleanFloor: growth },
renderedDevices: 200,
})),
});
test('performance budget accepts a candidate inside relative and absolute limits', () => {
const result = evaluatePerformanceBudget({
baseline: report(), candidate: report({ timing: 120, heap: 150 }), budgets,
});
assert.equal(result.pass, true);
assert.deepEqual(result.failures, []);
});
test('performance budget rejects a timing regression even below the hard ceiling', () => {
const result = evaluatePerformanceBudget({
baseline: report(), candidate: report({ timing: 150 }), budgets,
});
assert.equal(result.pass, false);
assert.ok(result.failures.some((check) => check.id === 'timing.firstStableRenderMs.median'));
});
test('performance budget rejects long tasks, heap growth, cache growth and missing devices', () => {
const candidate = report({ heap: 2000, cache: 4, growth: 1, long: 300 });
candidate.rows.forEach((row) => { row.renderedDevices = 199; });
const result = evaluatePerformanceBudget({ baseline: report(), candidate, budgets });
assert.equal(result.pass, false);
assert.deepEqual(
new Set(result.failures.map((check) => check.id)),
new Set([
'longTask.maxSingleMs',
'longTask.totalP95Ms',
'heap.growthP95Bytes',
'cache.entries.cleanFloor',
'cache.growth.cleanFloor',
'renderedDevices',
]),
);
});
test('performance budget compares single/count Long Tasks to the same-runner baseline', () => {
const baseline = report({ long: 100 });
baseline.longTasks.countP95 = 4;
const candidate = report({ long: 130 });
candidate.longTasks.countP95 = 6;
const result = evaluatePerformanceBudget({ baseline, candidate, budgets });
assert.equal(result.pass, true);
assert.equal(result.checks.find((check) => check.id === 'longTask.maxSingleMs')?.baseline, 100);
assert.equal(result.checks.find((check) => check.id === 'longTask.countP95')?.baseline, 4);
});
test('performance budget refuses incomparable runtime profiles', () => {
const candidate = report();
candidate.runtime.chromium = 'different';
assert.throws(
() => evaluatePerformanceBudget({ baseline: report(), candidate, budgets }),
/runtime mismatch for chromium/,
);
});
test('isometric reports fail closed on exact SHA, effective projection and current Stage 4 metadata', () => {
const isoBudgets = { ...budgets, profile: 'isometric-stage3-dense-v1' };
const valid = report();
Object.assign(valid, {
profile: isoBudgets.profile,
effectiveProjection: ['iso'],
isoStageRevision: ['4'],
stage3Required: true,
});
valid.rows.forEach((row) => Object.assign(row, {
effectiveProjection: 'iso', isoStageRevision: '4',
isoStructuralBuilds: {
supported: true, initial: 1, beforeHaUpdate: 2, afterHaUpdate: 2, haUpdateDelta: 0,
},
}));
const baseline = structuredClone(valid);
baseline.sourceSha = '2'.repeat(40);
baseline.stage3Required = false;
assert.equal(evaluatePerformanceBudget({
baseline, candidate: valid, budgets: isoBudgets,
baselineSha: baseline.sourceSha, candidateSha: valid.sourceSha,
}).pass, true);
for (const mutate of [
(candidate) => { candidate.sourceSha = null; },
(candidate) => { candidate.effectiveProjection = ['flat']; },
(candidate) => { candidate.rows[0].effectiveProjection = 'flat'; },
(candidate) => { candidate.stage3Required = false; },
(candidate) => { candidate.isoStageRevision = ['2']; },
(candidate) => { candidate.rows[0].isoStageRevision = '2'; },
(candidate) => { candidate.rows[0].isoStructuralBuilds.haUpdateDelta = 1; },
(candidate) => { delete candidate.rows[0].isoStructuralBuilds; },
]) {
const candidate = structuredClone(valid);
mutate(candidate);
assert.throws(() => evaluatePerformanceBudget({
baseline, candidate, budgets: isoBudgets,
}), /sourceSha|effectiveProjection|Stage 4|structural build count/);
}
assert.throws(() => evaluatePerformanceBudget({
baseline, candidate: valid, budgets: isoBudgets, candidateSha: 'f'.repeat(40),
}), /sourceSha does not match/);
});
test('#743 backdrop candidate renders a warm 2.5D floor switch in one update pass', () => {
const backdropBudgets = { ...budgets, profile: 'large-house-isometric-backdrop-v1' };
const withPasses = (sourceSha, perSwitch) => {
const value = report();
Object.assign(value, { profile: backdropBudgets.profile, sourceSha, effectiveProjection: ['iso'] });
value.rows.forEach((row) => Object.assign(row, {
effectiveProjection: 'iso',
isoStructuralBuilds: {
supported: true, initial: 1, beforeHaUpdate: 2, afterHaUpdate: 2, haUpdateDelta: 0,
},
...(perSwitch ? { floorSwitchPasses: { supported: true, perSwitch: [...perSwitch] } } : {}),
}));
return value;
};
const evaluate = (candidate, baseline) => evaluatePerformanceBudget({
baseline, candidate, budgets: backdropBudgets,
baselineSha: baseline.sourceSha, candidateSha: candidate.sourceSha,
});
// The base is reported, not judged: before #739 (v1.78.0 too) it takes two passes.
const base = withPasses('2'.repeat(40), [2, 2]);
assert.equal(evaluate(withPasses('1'.repeat(40), [1, 1, 1, 1, 1, 1]), base).pass, true);
const twice = withPasses('1'.repeat(40), [1, 2]);
assert.throws(() => evaluate(twice, base),
/^Error: candidate: a warm 2\.5D floor switch with a backdrop took 2 update passes$/);
const oneRowTwice = withPasses('1'.repeat(40), [1, 1]);
oneRowTwice.rows[1].floorSwitchPasses.perSwitch = [1, 2];
assert.throws(() => evaluate(oneRowTwice, base), /took 2 update passes/, 'every sample is judged');
for (const mutate of [
(candidate) => { delete candidate.rows[0].floorSwitchPasses; },
(candidate) => { candidate.rows[0].floorSwitchPasses.supported = false; },
(candidate) => { candidate.rows[0].floorSwitchPasses.perSwitch = []; },
]) {
const candidate = withPasses('1'.repeat(40), [1, 1]);
mutate(candidate);
assert.throws(() => evaluate(candidate, base), /candidate: floor switch update passes are missing/);
}
assert.throws(() => evaluate(withPasses('1'.repeat(40), null), base),
/candidate: floor switch update passes are missing/, 'a candidate report without the probe fails closed');
// The twin is an isometric profile: exact SHA and effective Iso are required as well.
const flat = withPasses('1'.repeat(40), [1, 1]);
flat.effectiveProjection = ['flat'];
assert.throws(() => evaluate(flat, base), /effectiveProjection is not exclusively iso/);
const unpinned = withPasses(null, [1, 1]);
assert.throws(() => evaluatePerformanceBudget({ baseline: base, candidate: unpinned, budgets: backdropBudgets }),
/candidate: missing exact sourceSha/);
});
test('absolute performance smoke needs no baseline and enforces hard ceilings', () => {
const accepted = evaluatePerformanceBudget({
candidate: report({ timing: 450, heap: 900, long: 190 }), budgets, absoluteOnly: true,
});
assert.equal(accepted.pass, true);
assert.equal(accepted.mode, 'absolute');
assert.equal(accepted.checks.find((check) => check.id === 'timing.firstStableRenderMs.median')?.baseline, undefined);
const rejected = evaluatePerformanceBudget({
candidate: report({ timing: 501 }), budgets, absoluteOnly: true,
});
assert.equal(rejected.pass, false);
assert.ok(rejected.failures.some((check) => check.id === 'timing.firstStableRenderMs.median'));
});
test('named interaction windows enforce their own absolute Long Task limits', () => {
const windowBudgets = {
...budgets,
longTaskWindows: {
editorSeries: { maxSingleMs: 150, maxCountP95: 3, maxTotalP95Ms: 300 },
},
};
const candidate = report();
candidate.rows[0].longTasks.editorSeries = {
supported: true, count: 3, maxMs: 149, totalMs: 299,
};
candidate.rows[1].longTasks.editorSeries = {
supported: true, count: 4, maxMs: 151, totalMs: 301,
};
const result = evaluatePerformanceBudget({ baseline: report(), candidate, budgets: windowBudgets });
assert.deepEqual(
new Set(result.failures.map((check) => check.id)),
new Set([
'longTask.editorSeries.maxSingleMs',
'longTask.editorSeries.countP95',
'longTask.editorSeries.totalP95Ms',
]),
);
});
// #473 AC4: smoke-бюджеты диффозависимых профилей повторяют абсолютные потолки
// полных профилей и пригодны для `compare --absolute-only` на трёх образцах.
import { readFileSync } from 'node:fs';
const readBudget = (name) => JSON.parse(readFileSync(new URL(`../demo/performance/${name}`, import.meta.url), 'utf8'));
// Синтетический отчёт для `compare --absolute-only`: все метрики по 1 мс,
// кроме переданных в `timings`.
const absoluteSmokeReport = (smoke, timings = {}) => ({
schema: 2, profile: smoke.profile, sourceSha: '1'.repeat(40), buildFingerprint: 'fixture',
runtime: { node: 'v22.0.0', chromium: '1.2.3', platform: 'linux', arch: 'x64' },
fixture: { rooms: 60 },
summary: Object.fromEntries(Object.keys(smoke.timings).map((metric) => {
const value = timings[metric] ?? 1;
return [metric, { median: value, p95: value, min: value, max: value }];
})),
longTasks: { maxSingleMs: 1, countP95: 1, totalP95Ms: 1 },
...(smoke.profile === 'large-house-isometric-v1' ? { effectiveProjection: ['iso'] } : {}),
rows: [0, 1, 2].map(() => ({
heapGrowthBytes: 1, preciseGc: true,
longTasks: Object.fromEntries(['load', ...Object.keys(smoke.longTaskWindows ?? {})]
.map((name) => [name, { supported: true, count: 1, maxMs: 1, totalMs: 1 }])),
cacheEntries: { ...smoke.cacheEntries },
cacheGrowth: Object.fromEntries(Object.keys(smoke.cacheGrowth).map((key) => [key, 0])),
renderedDevices: smoke.renderedDevices,
...(smoke.profile === 'large-house-isometric-v1'
? {
effectiveProjection: 'iso',
isoStructuralBuilds: { supported: true, initial: 1, beforeHaUpdate: 2, afterHaUpdate: 2, haUpdateDelta: 0 },
} : {}),
})),
});
for (const [smokeName, fullName] of [
['budgets-isometric-smoke.json', 'budgets-large-house-isometric.json'],
['budgets-interaction-smoke.json', 'budgets-large-house-interaction.json'],
]) {
test(`${smokeName} повторяет hardMaxMs полного профиля и держит 3 образца (#473 AC4)`, () => {
const smoke = readBudget(smokeName);
const full = readBudget(fullName);
assert.equal(smoke.profile, full.profile);
assert.equal(smoke.minimumSamples, 3);
assert.deepEqual(Object.keys(smoke.timings), Object.keys(full.timings), 'набор метрик тот же');
for (const [metric, budget] of Object.entries(smoke.timings)) {
assert.equal(budget.hardMaxMs, full.timings[metric].hardMaxMs, `${metric}: потолок отличается от полного`);
assert.equal(budget.stat, full.timings[metric].stat);
// Регрессионных коэффициентов в смоке нет: сравнивать не с чем (§5).
assert.equal(budget.maxRegressionRatio, undefined, `${metric}: в смоке нет относительных лимитов`);
}
assert.equal(smoke.longTasks.maxSingleMs, full.longTasks.maxSingleMs);
assert.equal(smoke.longTasks.maxCountP95, full.longTasks.maxCountP95);
assert.equal(smoke.longTasks.maxTotalP95Ms, full.longTasks.maxTotalP95Ms);
assert.deepEqual(smoke.longTaskWindows, full.longTaskWindows);
assert.equal(smoke.heap.hardMaxGrowthBytes, full.heap.hardMaxGrowthBytes);
assert.deepEqual(smoke.cacheEntries, full.cacheEntries);
assert.deepEqual(smoke.renderedDevices, full.renderedDevices);
// Пригодность для --absolute-only: синтетический отчёт под потолками
// проходит, первый кадр как у de215578 (9 870 мс) — красный.
const ok = evaluatePerformanceBudget({ candidate: absoluteSmokeReport(smoke), budgets: smoke, absoluteOnly: true });
assert.deepEqual(ok.failures, [], 'отчёт под потолками обязан проходить');
const regressed = evaluatePerformanceBudget({
candidate: absoluteSmokeReport(smoke, { firstStableRenderMs: 9870 }), budgets: smoke, absoluteOnly: true,
});
assert.ok(regressed.failures.some((check) => check.id === 'timing.firstStableRenderMs.median'),
'первый кадр 9 870 мс обязан краснеть');
});
}
test('#692 первый стабильный кадр interaction: потолок над уровнем 1.78 с запасом на шум раннера', () => {
const smoke = readBudget('budgets-interaction-smoke.json');
const full = readBudget('budgets-large-house-interaction.json');
const ceiling = full.timings.firstStableRenderMs.hardMaxMs;
assert.equal(ceiling, 3400);
assert.equal(smoke.timings.firstStableRenderMs.hardMaxMs, ceiling, 'смок = полный профиль (#473 AC4)');
// Медианы полного профиля (7 образцов) из performance.yml: линия 1.77 и v1.78.0.
const level177 = [2769.0, 2838.2, 2777.3, 2801.7];
const level178 = [2925.0, 2925.2];
const worst = 3144.8; // тот же SHA v1.78.0, соседний прогон — шум раннера до 7,5 %
for (const median of [...level177, ...level178, worst]) assert.ok(median < ceiling, `${median} краснеет на ${ceiling}`);
assert.ok(ceiling >= Math.max(...level178) * 1.15, 'запас над уровнем 1.78 не меньше 15 %');
assert.ok(ceiling <= Math.max(...level178) * 1.2, 'и не больше 20 %: потолок остаётся гардом');
// Относительное сравнение полного профиля не ослаблено.
assert.equal(full.timings.firstStableRenderMs.maxRegressionRatio, 0.3);
assert.equal(full.timings.firstStableRenderMs.noiseAllowanceMs, 250);
});
test('interaction aggregate keeps hosted-runner headroom without weakening component gates (#483)', () => {
const smoke = readBudget('budgets-interaction-smoke.json');
const full = readBudget('budgets-large-house-interaction.json');
assert.equal(smoke.timings.interactionSeriesMs.hardMaxMs, 3300);
assert.equal(full.timings.interactionSeriesMs.hardMaxMs, 3300);
assert.equal(smoke.timings.hoverSeriesMs.hardMaxMs, 500);
assert.equal(smoke.timings.panSeriesMs.hardMaxMs, 500);
assert.equal(smoke.timings.cameraSeriesMs.hardMaxMs, 500);
assert.equal(smoke.timings.editorSeriesMs.hardMaxMs, 750);
});
// #675: обоснование потолка и ряд замеров — demo/performance/README.md,
// «CI contracts». Три файла держат одно число: смок повторяет полный профиль
// (#473 AC4), плотный двойник — исторический (#160).
test('isometric space switch ceiling covers the 2.5D runner level and still catches #583 (#675)', () => {
const files = [
'budgets-isometric-smoke.json',
'budgets-large-house-isometric.json',
'budgets-isometric-stage3-dense.json',
];
for (const file of files)
assert.equal(readBudget(file).timings.spaceSwitchMs.hardMaxMs, 2200, `${file}: общий потолок spaceSwitchMs`);
const smoke = readBudget('budgets-isometric-smoke.json');
const red = (ms) => evaluatePerformanceBudget({
candidate: absoluteSmokeReport(smoke, { spaceSwitchMs: ms }), budgets: smoke, absoluteOnly: true,
}).failures.some((check) => check.id === 'timing.spaceSwitchMs.median');
assert.equal(red(1867.7), false, 'максимум уровня 2.5D на hosted-раннере (прогон 36306131709) проходит');
assert.equal(red(2719.6), true, 'провал #583 до решётки (прогон 35070397356) краснеет и по spaceSwitchMs');
assert.equal(red(2 * 1798.3), true, 'удвоение текущего уровня краснеет');
// Потолок — не детектор тренда: полный прогон по-прежнему сравнивает с базой
// того же раннера, и рычагом не стали ни коэффициент, ни допуск.
for (const file of files.slice(1)) {
const budget = readBudget(file).timings.spaceSwitchMs;
assert.equal(budget.maxRegressionRatio, 0.2, `${file}: коэффициент не рычаг`);
assert.equal(budget.noiseAllowanceMs, 100, `${file}: допуск не рычаг`);
}
});
// #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');
// v1.76.0 temporarily widened exactly these three allowances after #583.
// The boundary-event search retires that exception; the twin profiles keep
// their shared Stage 3/4 contract and the ordinary pre-exception values.
assert.equal(isometric.timings.resizePreviewMs.noiseAllowanceMs, 150);
assert.equal(isometric.timings.panZoomMs.noiseAllowanceMs, 60);
assert.equal(isometric.timings.stateUpdateMs.noiseAllowanceMs, 75,
'профили-близнецы обязаны совпадать по общим метрикам (#160)');
assert.equal(dense.timings.stateUpdateMs.noiseAllowanceMs, 75);
assert.equal(dense.timings.resizePreviewMs.noiseAllowanceMs, 150);
assert.equal(dense.timings.panZoomMs.noiseAllowanceMs, 60);
for (const budget of [isometric, dense]) {
for (const metric of ['resizePreviewMs', 'panZoomMs', 'stateUpdateMs']) {
assert.equal(budget.timings[metric].maxRegressionRatio, 0.2, 'коэффициент не рычаг');
}
assert.equal(budget.timings.resizePreviewMs.hardMaxMs, 2200, 'абсолютный потолок не двигался');
assert.equal(budget.timings.panZoomMs.hardMaxMs, 600);
assert.equal(budget.timings.modelReadyMs.noiseAllowanceMs, 200, 'загрузка допуска не получала');
assert.equal(budget.timings.spaceSwitchMs.noiseAllowanceMs, 100);
}
for (const file of ['budgets.json', 'budgets-large-house-plan-snap.json', 'budgets-large-house-interaction.json']) {
const other = readBudget(file);
assert.equal(other.timings.panZoomMs.noiseAllowanceMs, 60, `${file} допуска жеста не получал`);
assert.equal(other.timings.resizePreviewMs.noiseAllowanceMs, 150, `${file} допуска ресайза не получал`);
}
});
test('isometric long-task count allowance covers the lazy iso-chunk split, nothing else (#507)', () => {
const isometric = readBudget('budgets-large-house-isometric.json');
assert.equal(isometric.longTasks.countNoiseAllowance, 5, 'owner-accepted +2 tasks of the lazy iso-scene-render split plus jitter');
assert.equal(isometric.longTasks.maxCountRegressionRatio, 0.2, 'the ratio is not the lever');
assert.equal(isometric.longTasks.maxCountP95, 30);
assert.equal(isometric.longTasks.maxTotalRegressionRatio, 0.2, 'total work is still gated as before');
assert.equal(isometric.longTasks.maxSingleRegressionRatio, 0.2);
assert.equal(readBudget('budgets-isometric-stage3-dense.json').longTasks.countNoiseAllowance, 5, 'the dense twin shares the lazy iso chunk and the #160 common-ceiling contract');
for (const file of ['budgets.json', 'budgets-large-house-plan-snap.json', 'budgets-large-house-interaction.json']) {
assert.equal(readBudget(file).longTasks.countNoiseAllowance, 3, `${file} keeps the ordinary allowance`);
}
// The v1.73.0 stable comparison against v1.72.0: 16 → 20 passes, 22 does not.
const limit = Math.max(16 * (1 + isometric.longTasks.maxCountRegressionRatio), 16 + isometric.longTasks.countNoiseAllowance);
assert.equal(limit, 21);
});
test('#720: the 2.5D view toggle is reported, not budgeted (owner decision in #694)', () => {
// A one-off settings operation, and since #649 the runner measures a full
// config reload for the candidate but a per-device projection flip for
// v1.77.0 — not the same operation. Load, floor switch and the single
// long-task ceiling keep gating the 2.5D scene build and UI freezes.
for (const name of [
'budgets-isometric-smoke.json',
'budgets-large-house-isometric.json',
'budgets-isometric-stage3-dense.json',
'budgets-large-house-isometric-backdrop.json',
]) {
const budget = readBudget(name);
assert.equal('viewToggleMs' in budget.timings, false, `${name} budgets viewToggleMs`);
for (const metric of ['modelReadyMs', 'firstStableRenderMs', 'spaceSwitchMs'])
assert.ok(budget.timings[metric], `${name} keeps ${metric}`);
assert.ok(budget.longTasks.maxSingleMs > 0, `${name} keeps the single long-task ceiling`);
}
const runner = readFileSync(new URL('../demo/benchmark_large_house.mjs', import.meta.url), 'utf8');
assert.match(runner, /if \(isometric\) metricNames\.splice\(2, 0, 'viewToggleMs'\);/,
'the runner still reports the toggle');
});