mirror of
https://github.com/Matysh/houseplan-card
synced 2026-10-02 12:49:56 +00:00
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:
@@ -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
|
||||
;;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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`);
|
||||
|
||||
@@ -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 [
|
||||
|
||||
Reference in New Issue
Block a user