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
This commit is contained in:
Claude
2026-10-01 15:41:48 +00:00
committed by claude[bot]
parent 134e3e79da
commit b18d366e8c
8 changed files with 252 additions and 9 deletions
+8
View File
@@ -40,6 +40,7 @@ jobs:
profile:
- large-house
- isometric
- isometric-backdrop
- isometric-stage3
- plan-snap
- interaction
@@ -134,6 +135,10 @@ jobs:
npm run benchmark:large-house-isometric -- --target-root=../baseline --samples=7 --warmups=1 --output=../artifacts/performance/isometric-baseline.json
npm run benchmark:large-house-isometric -- --target-root=. --samples=7 --warmups=1 --output=../artifacts/performance/isometric-candidate.json
;;
isometric-backdrop)
npm run benchmark:large-house-isometric-backdrop -- --target-root=../baseline --samples=7 --warmups=1 --output=../artifacts/performance/isometric-backdrop-baseline.json
npm run benchmark:large-house-isometric-backdrop -- --target-root=. --samples=7 --warmups=1 --output=../artifacts/performance/isometric-backdrop-candidate.json
;;
isometric-stage3)
npm run benchmark:isometric-stage3-dense -- --allow-stage2-base --target-root=../baseline --samples=7 --warmups=1 --output=../artifacts/performance/isometric-stage3-baseline.json
npm run benchmark:isometric-stage3-dense -- --target-root=. --samples=7 --warmups=1 --output=../artifacts/performance/isometric-stage3-candidate.json
@@ -188,6 +193,9 @@ jobs:
isometric)
npm run benchmark:compare -- --budgets=demo/performance/budgets-large-house-isometric.json --baseline=../artifacts/performance/isometric-baseline.json --candidate=../artifacts/performance/isometric-candidate.json --baseline-sha="$(git -C ../baseline rev-parse HEAD)" --candidate-sha="$(git rev-parse HEAD)" --output=../artifacts/performance/isometric-comparison.json
;;
isometric-backdrop)
npm run benchmark:compare -- --budgets=demo/performance/budgets-large-house-isometric-backdrop.json --baseline=../artifacts/performance/isometric-backdrop-baseline.json --candidate=../artifacts/performance/isometric-backdrop-candidate.json --baseline-sha="$(git -C ../baseline rev-parse HEAD)" --candidate-sha="$(git rev-parse HEAD)" --output=../artifacts/performance/isometric-backdrop-comparison.json
;;
isometric-stage3)
npm run benchmark:compare -- --budgets=demo/performance/budgets-isometric-stage3-dense.json --baseline=../artifacts/performance/isometric-stage3-baseline.json --candidate=../artifacts/performance/isometric-stage3-candidate.json --baseline-sha="$(git -C ../baseline rev-parse HEAD)" --candidate-sha="$(git rev-parse HEAD)" --output=../artifacts/performance/isometric-stage3-comparison.json
;;
+42 -5
View File
@@ -22,7 +22,7 @@ const targetRoot = resolve(valueArg('target-root') ?? '.');
const profile = valueArg('profile') ?? 'large-house-v1';
if (![
'large-house-v1', 'large-house-isometric-v1', ISOMETRIC_STAGE3_DENSE_PROFILE,
'large-house-plan-snap-v1', 'large-house-interaction-v1',
'large-house-plan-snap-v1', 'large-house-interaction-v1', 'large-house-isometric-backdrop-v1',
].includes(profile))
throw new Error(`unknown large-house profile: ${profile}`);
const stage3Dense = profile === ISOMETRIC_STAGE3_DENSE_PROFILE;
@@ -30,7 +30,8 @@ const allowStage2Base = process.argv.includes('--allow-stage2-base');
if (allowStage2Base && !stage3Dense)
throw new Error('--allow-stage2-base is valid only for the Stage 3 dense profile');
const requireStage3 = stage3Dense && !allowStage2Base;
const isometric = profile === 'large-house-isometric-v1' || stage3Dense;
const backdrop = profile === 'large-house-isometric-backdrop-v1';
const isometric = profile === 'large-house-isometric-v1' || stage3Dense || backdrop;
const planSnap = profile === 'large-house-plan-snap-v1';
const interaction = profile === 'large-house-interaction-v1';
const requiresIsometric = isometric && existsSync(resolve(targetRoot, 'src/iso-projection.ts'));
@@ -42,7 +43,20 @@ const requiresWallFace = planSnap && existsSync(resolve(targetRoot, 'src/wall-fa
const requiresInteraction = interaction && existsSync(resolve(targetRoot, 'src/live-viewport.ts'));
const requiresStairs = existsSync(resolve(targetRoot, 'src/stairs.ts'));
const fixture = stage3Dense ? makeIsometricStage3DenseFixture() : makeLargeHouseFixture();
const fixtureCounts = stage3Dense ? fixture.counts : LARGE_HOUSE_COUNTS;
if (backdrop) {
// #743: the 2.5D twin with a backdrop image on every floor, so the imagePlan
// path (#739 K1) is measured at all. One shipped picture (demo/srv/assets
// since v1.14.0, present in every base) under a URL of its own per floor, so
// each floor decodes its own image; plan_aspect 1 lays it over the whole
// plan square and leaves the room geometry alone. The variant lives here, as
// plan-snap's does: demo/fixtures/** and the bundle fingerprint stay as they are.
for (const space of fixture.config.spaces) {
space.plan_url = `/assets/f1.svg?${space.id}`;
space.plan_aspect = 1;
}
fixture.counts = { ...fixture.counts, backdrops: fixture.config.spaces.length };
}
const fixtureCounts = stage3Dense || backdrop ? fixture.counts : LARGE_HOUSE_COUNTS;
const sourceSha = (() => {
try {
return execFileSync('git', ['-C', targetRoot, 'rev-parse', 'HEAD'], { encoding: 'utf8' }).trim();
@@ -101,7 +115,7 @@ try {
const row = await page.evaluate(async ({
fixture, sample, cardContract, isometric, requiresIsometric, planSnap, requiresPlanSnap,
requiresWallFace, interaction, requiresInteraction, stage3Dense, requireStage3,
requiresIsoStructuralBuildCounter, requiresStairs, profile,
requiresIsoStructuralBuildCounter, requiresStairs, profile, backdrop,
}) => {
const frame = () => new Promise((done) => requestAnimationFrame(() => requestAnimationFrame(done)));
const until = async (predicate, timeout = 10000) => {
@@ -406,6 +420,10 @@ try {
&& card.renderRoot.querySelectorAll('[data-hp="stair"]').length !== fixture.counts.stairs) {
throw new Error(`${profile} did not render the bounded maximum stair collection`);
}
// #743 precondition (docs/TESTING.md rule 3): the fixture carries the
// layer the profile guards, so a stable frame without the backdrop fails.
if (backdrop && !card.renderRoot.querySelector('.stage svg image.hp-backdrop'))
throw new Error(`${profile} rendered no backdrop image`);
const firstStableRenderMs = Number((performance.now() - loadStarted).toFixed(2));
const bootDiag = {
updates: card.__diag.updates,
@@ -1163,6 +1181,24 @@ try {
if (switchCycleBuilt.length)
throw new Error(`${profile} switchCycle built a floor inside the window: ${switchCycleBuilt.join(', ')}`);
// #743 structural probe, untimed and outside every Long Task window: six
// warm switches round the floors (the cycle ended on the last floor, so
// each one is a real switch). Each counts the update passes from the
// pick until updateComplete reports quiescence (resolves true). Counting
// right after the first await would miss the #739 K1 second pass: it
// starts after that promise resolves, in the same task. The wait is
// bounded so an endless update loop yields a large count instead of a
// hung job. evaluate.mjs judges the candidate; a base is only reported.
const floorSwitchPasses = [];
if (backdrop) {
for (let index = 0; index < 6; index++) {
const passesBefore = card.__diag.updates;
card._pickSpace(`perf-floor-${(index % fixture.counts.floors) + 1}`);
for (let wait = 0; wait < 20 && !(await card.updateComplete); wait++);
floorSwitchPasses.push(card.__diag.updates - passesBefore);
}
}
await forceGc();
const cacheBefore = cacheSnapshot(card);
const heapBefore = performance.memory?.usedJSHeapSize ?? null;
@@ -1239,6 +1275,7 @@ try {
? null : afterStateIsoStructuralBuilds - steadyIsoStructuralBuilds,
},
} : {}),
...(backdrop ? { floorSwitchPasses: { supported: true, perSwitch: floorSwitchPasses } } : {}),
...(finalStage3 ? {
isoStageRevision: finalStage3.isoStageRevision,
stage3Diagnostics: {
@@ -1256,7 +1293,7 @@ try {
fixture, sample: measuredSample, cardContract: LARGE_HOUSE_CARD_CONTRACT,
isometric, requiresIsometric, planSnap, requiresPlanSnap, requiresWallFace,
interaction, requiresInteraction, stage3Dense, requireStage3,
requiresIsoStructuralBuildCounter, requiresStairs, profile,
requiresIsoStructuralBuildCounter, requiresStairs, profile, backdrop,
});
// #520: диагностика печатается в лог прогона и в запись не попадает.
const { bootDiag, ...measured } = row;
+32
View File
@@ -90,6 +90,37 @@ on both sides of the change are recorded in #735. Budgets and `hardMaxMs` did
not change in #735; #747 then brought the `switchCycleMs` ceilings down to the
warmed level (see "CI contracts" below).
Issue #743 adds `large-house-isometric-backdrop-v1`, the 2.5D twin of
`large-house-isometric-v1` with a loaded plan picture on every floor. Before it
no Full Performance profile walked the backdrop (`imagePlan`) path, so the
double render of #739 K1 — every warm floor switch cleared the ready paper,
inserted the first-frame veil, probed the card background and asked for a
second full update — was invisible to CI both by time and structurally; a
temporary probe found it. The runner derives the fixture as it does for
plan-snap: `large-house-v1` plus `plan_url: '/assets/f1.svg?<space id>'` and
`plan_aspect: 1` on each floor. One shipped picture (in `demo/srv/assets` since
v1.14.0, so every base has it) under a URL of its own per floor decodes once per
floor; it covers the whole plan square and the room geometry is unchanged.
`demo/fixtures/**` and the bundle fingerprint stay as they are, and the report's
fixture counts gain `backdrops: 3`. A sample fails unless its first stable frame
has the backdrop `<image>` in the stage SVG (`docs/TESTING.md` rule 3). The
profile runs every window of the historical isometric profile; after the
`switchCycle` window and its #735 guard, before forced GC and outside every
timed and Long Task window, a structural probe makes six warm switches round the
floors and counts the `performUpdate` passes of each, from the pick until
`updateComplete` resolves `true`. Counting has to run to that quiescence: the K1
second pass starts after the first `updateComplete` has resolved, in the same
task, so a count taken right after the first `await` reads one pass on both
sides. Late updates after the frame are not counted. Each row reports
`floorSwitchPasses: { supported, perSwitch }`, and `evaluate.mjs` rejects a
candidate unless every element is 1 (`candidate: a warm 2.5D floor switch with a
backdrop took <n> update passes`, #739 AC2 and `docs/ISOMETRIC.md`). The base is
reported, not judged: bases before #739, v1.78.0 among them, take two passes.
`budgets-large-house-isometric-backdrop.json` is a copy of the historical
isometric budget that differs only in `profile`, so the twin shares every
ceiling (as #160 does); the historical profile keeps its meaning as the #124
witness. The profile runs in Full Performance only, not in the Validate smoke.
Every report is tied to the source fingerprint embedded by Rollup. A stale
bundle is a hard failure.
@@ -296,6 +327,7 @@ Build and copy a fresh demo bundle first, then run:
```bash
npm run benchmark:large-house -- --samples=7 --warmups=1 --output=artifacts/performance/local.json
npm run benchmark:large-house-isometric -- --samples=7 --warmups=1 --output=artifacts/performance/isometric-local.json
npm run benchmark:large-house-isometric-backdrop -- --samples=7 --warmups=1 --output=artifacts/performance/isometric-backdrop-local.json
npm run benchmark:isometric-stage3-dense -- --samples=7 --warmups=1 --output=artifacts/performance/isometric-stage3-local.json
npm run benchmark:large-house-plan-snap -- --samples=7 --warmups=1 --output=artifacts/performance/plan-snap-local.json
npm run benchmark:large-house-interaction -- --samples=7 --warmups=1 --output=artifacts/performance/interaction-local.json
@@ -0,0 +1,49 @@
{
"schema": 1,
"profile": "large-house-isometric-backdrop-v1",
"minimumSamples": 7,
"timings": {
"modelReadyMs": { "stat": "median", "maxRegressionRatio": 0.2, "noiseAllowanceMs": 200, "hardMaxMs": 3000 },
"firstStableRenderMs": { "stat": "median", "maxRegressionRatio": 0.2, "noiseAllowanceMs": 250, "hardMaxMs": 3500 },
"spaceSwitchMs": { "stat": "median", "maxRegressionRatio": 0.2, "noiseAllowanceMs": 100, "hardMaxMs": 2200 },
"stateUpdateMs": { "stat": "median", "maxRegressionRatio": 0.2, "noiseAllowanceMs": 75, "hardMaxMs": 1000 },
"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 }
},
"longTasks": {
"maxSingleMs": 3000,
"maxSingleRegressionRatio": 0.2,
"maxSingleNoiseAllowanceMs": 250,
"maxCountP95": 30,
"maxCountRegressionRatio": 0.2,
"countNoiseAllowance": 5,
"maxTotalP95Ms": 12000,
"maxTotalRegressionRatio": 0.2,
"noiseAllowanceMs": 150
},
"heap": {
"required": true,
"hardMaxGrowthBytes": 67108864,
"maxRegressionRatio": 0.2,
"noiseAllowanceBytes": 16777216
},
"cacheEntries": {
"cleanFloor": 100,
"glowClip": 200,
"wallUnion": 1,
"openingTunnel": 1,
"openingWallIndex": 1,
"isoGeometry": 8
},
"cacheGrowth": {
"cleanFloor": 0,
"glowClip": 0,
"wallUnion": 0,
"openingTunnel": 0,
"openingWallIndex": 0,
"isoGeometry": 0
},
"renderedDevices": 200
}
+15 -1
View File
@@ -38,8 +38,9 @@ export const summarizeLongTasks = (rows) => {
const sameJson = (a, b) => JSON.stringify(a) === JSON.stringify(b);
const exactGitSha = (value) => typeof value === 'string' && /^[0-9a-f]{40}$/i.test(value);
const ISOMETRIC_BACKDROP_PROFILE = 'large-house-isometric-backdrop-v1';
const isIsometricProfile = (profile) => profile === 'large-house-isometric-v1'
|| profile === 'isometric-stage3-dense-v1';
|| profile === 'isometric-stage3-dense-v1' || profile === ISOMETRIC_BACKDROP_PROFILE;
const requireReport = (report, budgets, label, expectedSha = null) => {
if (!report || report.schema !== 2) throw new Error(`${label}: unsupported report schema`);
@@ -61,6 +62,19 @@ const requireReport = (report, budgets, label, expectedSha = null) => {
|| row.isoStructuralBuilds?.haUpdateDelta !== 0)) {
throw new Error(`${label}: Iso structural build count is missing or changed on HA-only update`);
}
if (label === 'candidate' && report.profile === ISOMETRIC_BACKDROP_PROFILE) {
// #743: a warm floor switch with a backdrop renders once (#739 AC2,
// docs/ISOMETRIC.md). The base is not judged: bases before #739,
// v1.78.0 among them, take two passes.
for (const row of report.rows) {
const passes = row.floorSwitchPasses;
if (passes?.supported !== true || !Array.isArray(passes.perSwitch) || !passes.perSwitch.length)
throw new Error(`${label}: floor switch update passes are missing`);
const extra = passes.perSwitch.find((count) => count !== 1);
if (extra !== undefined)
throw new Error(`${label}: a warm 2.5D floor switch with a backdrop took ${extra} update passes`);
}
}
if (label === 'candidate' && report.profile === 'isometric-stage3-dense-v1') {
// The profile/schema name is intentionally stable for baseline continuity;
// the candidate DOM revision tracks the current experimental Stage 4.
+1
View File
@@ -26,6 +26,7 @@
"audit:config": "node scripts/config-audit.mjs",
"benchmark:large-house": "node demo/benchmark_large_house.mjs",
"benchmark:large-house-isometric": "node demo/benchmark_large_house.mjs --profile=large-house-isometric-v1",
"benchmark:large-house-isometric-backdrop": "node demo/benchmark_large_house.mjs --profile=large-house-isometric-backdrop-v1",
"benchmark:isometric-stage3-dense": "node demo/benchmark_large_house.mjs --profile=isometric-stage3-dense-v1",
"benchmark:large-house-plan-snap": "node demo/benchmark_large_house.mjs --profile=large-house-plan-snap-v1",
"benchmark:large-house-interaction": "node demo/benchmark_large_house.mjs --profile=large-house-interaction-v1",
+50
View File
@@ -142,6 +142,55 @@ test('isometric reports fail closed on exact SHA, effective projection and curre
}), /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,
@@ -424,6 +473,7 @@ test('#720: the 2.5D view toggle is reported, not budgeted (owner decision in #6
'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`);
+55 -3
View File
@@ -41,6 +41,7 @@ test('full performance is isolated to stable, scheduled and manual entry points'
'profile:',
'- large-house',
'- isometric',
'- isometric-backdrop',
'- isometric-stage3',
'- plan-snap',
'- interaction',
@@ -54,12 +55,13 @@ test('full performance is isolated to stable, scheduled and manual entry points'
]) assert.ok(workflow.includes(contract), `missing full-gate contract: ${contract}`);
assert.ok(workflow.includes('if [ -f baseline/scripts/bundle-sync.mjs ]; then'));
assert.equal((workflow.match(/--samples=7 --warmups=1/g) || []).length, 18);
assert.equal((workflow.match(/--samples=7 --warmups=1/g) || []).length, 20);
assert.equal((workflow.match(/--allow-stage2-base/g) || []).length, 1,
'only the Stage 3 comparison base may bypass the candidate-only DOM contract');
assert.ok(workflow.includes('budgets-isometric-stage3-dense.json'));
assert.equal((workflow.match(/--baseline-sha=/g) || []).length, 2);
assert.equal((workflow.match(/--candidate-sha=/g) || []).length, 2);
assert.ok(workflow.includes('budgets-large-house-isometric-backdrop.json'));
assert.equal((workflow.match(/--baseline-sha=/g) || []).length, 3);
assert.equal((workflow.match(/--candidate-sha=/g) || []).length, 3);
const release = readWorkflow('release.yml');
// #540: признак стабильного — тег кандидата, не поле события: тот же гейт
@@ -129,6 +131,56 @@ test('#160 Stage 3 budget preserves every historical common ceiling', () => {
}
});
test('#743 backdrop budget is the historical isometric budget under its own profile id', () => {
// A new fixture meaning gets a new id (README, "Changing budgets"); the twin
// shares every ceiling with large-house-isometric-v1, as #160 does.
const historical = readJson('../demo/performance/budgets-large-house-isometric.json');
const backdrop = readJson('../demo/performance/budgets-large-house-isometric-backdrop.json');
assert.equal(backdrop.profile, 'large-house-isometric-backdrop-v1');
assert.deepEqual({ ...backdrop, profile: historical.profile }, historical,
'the twin differs from the historical isometric budget only in profile');
assert.equal('viewToggleMs' in backdrop.timings, false, '#720: the toggle is reported, not budgeted');
const workflow = readWorkflow('performance.yml');
const capture = workflow.slice(workflow.indexOf(' isometric-backdrop)'));
assert.match(capture,
/^ {12}isometric-backdrop\)\n {14}npm run benchmark:large-house-isometric-backdrop -- --target-root=\.\.\/baseline --samples=7 --warmups=1 [^\n]+\n {14}npm run benchmark:large-house-isometric-backdrop -- --target-root=\. --samples=7 --warmups=1 /,
'base and candidate are captured by the same runner, base first');
assert.match(workflow,
/--budgets=demo\/performance\/budgets-large-house-isometric-backdrop\.json [^\n]*--baseline-sha="[^\n]*--candidate-sha="/,
'the comparison pins both exact SHAs, as the isometric profile does');
const pkg = readJson('../package.json');
assert.equal(pkg.scripts['benchmark:large-house-isometric-backdrop'],
'node demo/benchmark_large_house.mjs --profile=large-house-isometric-backdrop-v1');
});
test('#743 backdrop profile walks the imagePlan path and probes update passes after the #735 guard', () => {
const runner = readFileSync(new URL('../demo/benchmark_large_house.mjs', import.meta.url), 'utf8');
assert.match(runner, /const isometric = [^;\n]*\|\| backdrop;/,
'the backdrop twin runs every 2.5D window of large-house-isometric-v1');
assert.match(runner,
/space\.plan_url = `\/assets\/f1\.svg\?\$\{space\.id\}`;\s*space\.plan_aspect = 1;/,
'every floor gets the shipped picture under a URL of its own over the whole plan square');
assert.match(runner, /backdrops: fixture\.config\.spaces\.length/);
assert.match(runner,
/if \(backdrop && !card\.renderRoot\.querySelector\('\.stage svg image\.hp-backdrop'\)\)\s*throw new Error\(`\$\{profile\} rendered no backdrop image`\);\s*const firstStableRenderMs/,
'a stable frame without the backdrop layer fails the sample (docs/TESTING.md rule 3)');
const cycleStart = runner.indexOf('const switchCycle = await duration(');
const guard = runner.indexOf('switchCycle built a floor inside the window', cycleStart);
const probe = runner.indexOf('const floorSwitchPasses = [];', cycleStart);
const gc = runner.indexOf('await forceGc();', cycleStart);
assert.ok(cycleStart > 0 && guard > cycleStart && probe > guard && gc > probe,
'the probe follows the switchCycle window and its #735 guard and precedes forced GC');
const probeBody = runner.slice(probe, gc);
assert.ok(!probeBody.includes('duration(') && !probeBody.includes('startLongTaskWindow('),
'the probe stays outside every timed and Long Task window');
assert.match(probeBody,
/for \(let index = 0; index < 6; index\+\+\) \{\s*const passesBefore = card\.__diag\.updates;\s*card\._pickSpace\(`perf-floor-\$\{\(index % fixture\.counts\.floors\) \+ 1\}`\);\s*for \(let wait = 0; wait < 20 && !\(await card\.updateComplete\); wait\+\+\);\s*floorSwitchPasses\.push\(card\.__diag\.updates - passesBefore\);/,
'six warm switches, each counted until updateComplete reports quiescence');
assert.match(runner,
/\.\.\.\(backdrop \? \{ floorSwitchPasses: \{ supported: true, perSwitch: floorSwitchPasses \} \} : \{\}\)/,
'only the backdrop profile reports the passes');
});
test('#570 current Stage 4 runner fails closed on the agreed observable DOM contract', () => {
const runner = readFileSync(new URL('../demo/benchmark_large_house.mjs', import.meta.url), 'utf8');
for (const contract of [