From 285688de47ccad81b985c23c5030214c3c5eab9a Mon Sep 17 00:00:00 2001 From: Sergey Matyunin Date: Sun, 30 Aug 2026 00:45:31 +0300 Subject: [PATCH] fix: support stable performance baselines Issue: #380 User-Visible: no --- demo/benchmark_glow.mjs | 5 ++- demo/benchmark_large_house.mjs | 5 ++- demo/editor-runtime-compat.mjs | 15 ++++++++ demo/performance/card-contract.mjs | 20 +++++++++-- demo/serve.mjs | 6 +++- test/editor-runtime-compat.test.mjs | 34 ++++++++++++++++++ test/performance-contract.test.mjs | 54 ++++++++++++++++++++++++++++- 7 files changed, 132 insertions(+), 7 deletions(-) create mode 100644 demo/editor-runtime-compat.mjs create mode 100644 test/editor-runtime-compat.test.mjs diff --git a/demo/benchmark_glow.mjs b/demo/benchmark_glow.mjs index fbe998e9..adfb759c 100644 --- a/demo/benchmark_glow.mjs +++ b/demo/benchmark_glow.mjs @@ -204,7 +204,10 @@ try { }); const cacheSnapshot = (card) => ({ ...(card.localName === 'houseplan-space-card' ? { - glowClip: card._glowRuntimeState.clipCache?.size ?? 0, + // #380: the previous stable predates the shared static-card Glow + // runtime. Its default profile still has a valid empty cache, and + // the opt-in Glow profile is explicitly bootstrap-compared below. + glowClip: card._glowRuntimeState?.clipCache?.size ?? 0, } : { cleanFloor: card._cleanFloorCache?.size ?? 0, glowClip: card._glowClipCache?.size ?? 0, diff --git a/demo/benchmark_large_house.mjs b/demo/benchmark_large_house.mjs index 2dc27a65..ed64754b 100644 --- a/demo/benchmark_large_house.mjs +++ b/demo/benchmark_large_house.mjs @@ -346,7 +346,10 @@ try { target: null, }; card._rszEdgeDown(quietEvent, room.id, 1); - const plan = card._resize?.plan; + // #380: the previous stable owns the active session directly; current + // candidates own it through ResizeController. The explicit contract + // above requires one of these shapes before measurements begin. + const plan = card._resize?.plan || card._rszDrag?.plan; if (!plan) throw new Error('large-house resize plan was not created'); const target = [ plan.a[0] + plan.n[0] * card._gridPitch, diff --git a/demo/editor-runtime-compat.mjs b/demo/editor-runtime-compat.mjs new file mode 100644 index 00000000..77d10b9f --- /dev/null +++ b/demo/editor-runtime-compat.mjs @@ -0,0 +1,15 @@ +/** + * Prepare editor internals for demo harnesses across comparison baselines. + * + * Since #337 current builds keep the editor in a lazy runtime and expose an + * explicit preload method. Older stable builds are monolithic: absence of the + * method means there is nothing to preload, not that the baseline is broken. + * A present method remains fail-closed through its result or exception. + */ +export async function ensureHarnessEditorRuntime( + card = globalThis.window?.__card, +) { + const ensure = card?._ensureEditorRuntime; + if (typeof ensure !== 'function') return true; + return Boolean(await ensure.call(card)); +} diff --git a/demo/performance/card-contract.mjs b/demo/performance/card-contract.mjs index f78a99f4..4a24e3a8 100644 --- a/demo/performance/card-contract.mjs +++ b/demo/performance/card-contract.mjs @@ -34,7 +34,6 @@ export const LARGE_HOUSE_CARD_CONTRACT = Object.freeze({ '_loadOk', '_model', '_path', - '_resize', '_serverCfg', '_settingsDialog', '_tool', @@ -46,6 +45,13 @@ export const LARGE_HOUSE_CARD_CONTRACT = Object.freeze({ '_onLabsSnapshot', '_planSnapGeometryCache', '_roomDialog', '_setProjection', '_wallFaceBatch', '_wallFaceGraphCache', ]), + // #380: v1.68.1 owns the same resize session directly on the card; newer + // bundles moved it into ResizeController. A comparison target must expose + // one of the two explicit shapes; the current member retains its object + // type check and is also verified against current production source. + fieldAlternatives: Object.freeze([ + Object.freeze({ current: '_resize', legacy: '_rszDrag' }), + ]), fieldTypes: Object.freeze({ _booting: 'boolean', _cleanFloorCache: 'map', @@ -115,11 +121,19 @@ export function assertCardContract(card, contract) { .map((name) => `${name}()`); const missingFields = contract.fields .filter((name) => !(name in card) || card[name] === undefined); - const invalidFields = [...contract.fields, ...(contract.optionalFields || [])] + const missingAlternatives = (contract.fieldAlternatives || []) + .filter((choice) => !Object.values(choice) + .some((name) => name in card && card[name] !== undefined)) + .map((choice) => Object.values(choice).join('|')); + const alternativeFields = (contract.fieldAlternatives || []) + .flatMap((choice) => Object.values(choice)); + const invalidFields = [ + ...contract.fields, ...(contract.optionalFields || []), ...alternativeFields, + ] .filter((name) => name in card && contract.fieldTypes?.[name] && !matches(card[name], contract.fieldTypes[name])) .map((name) => `${name}:${contract.fieldTypes[name]}`); - const missing = [...missingMethods, ...missingFields]; + const missing = [...missingMethods, ...missingFields, ...missingAlternatives]; if (missing.length || invalidFields.length) { const details = [ missing.length ? `missing private API: ${missing.join(', ')}` : '', diff --git a/demo/serve.mjs b/demo/serve.mjs index 0819b192..6f92bca8 100644 --- a/demo/serve.mjs +++ b/demo/serve.mjs @@ -2,6 +2,7 @@ // via request interception (no HTTP server needed). Usage: const {page,browser}=await launch(); import { chromium } from 'playwright'; import { assertFreshDemoBundleUnlessAllowed } from './bundle-freshness.mjs'; +import { ensureHarnessEditorRuntime } from './editor-runtime-compat.mjs'; import { readFileSync, existsSync } from 'node:fs'; import { fileURLToPath } from 'node:url'; import { dirname } from 'node:path'; @@ -88,8 +89,11 @@ async function launchInternal( // Existing product smokes exercise editor internals directly. They preload // the new #337 runtime without changing mode; the dedicated network smoke // uses launchColdView() and proves the real cold-View boundary separately. + // Comparative performance runs also launch the previous stable through this + // file. A pre-#337 monolithic card has no preload method and is already + // ready; a current card still has to return truthy (#380). if (preloadEditorRuntime) { - const ready = await page.evaluate(() => window.__card._ensureEditorRuntime()); + const ready = await page.evaluate(ensureHarnessEditorRuntime); if (!ready) throw new Error('editor runtime did not preload for browser smoke'); } return { page, browser }; diff --git a/test/editor-runtime-compat.test.mjs b/test/editor-runtime-compat.test.mjs new file mode 100644 index 00000000..446294b8 --- /dev/null +++ b/test/editor-runtime-compat.test.mjs @@ -0,0 +1,34 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; + +import { ensureHarnessEditorRuntime } from '../demo/editor-runtime-compat.mjs'; + +test('a pre-lazy-editor baseline is already ready for the harness (#380)', async () => { + assert.equal(await ensureHarnessEditorRuntime({}), true); + assert.equal(await ensureHarnessEditorRuntime(null), true); +}); + +test('a lazy editor runtime is called with its card and must succeed (#380)', async () => { + let owner = null; + const card = { + async _ensureEditorRuntime() { + owner = this; + return true; + }, + }; + assert.equal(await ensureHarnessEditorRuntime(card), true); + assert.equal(owner, card); + + assert.equal(await ensureHarnessEditorRuntime({ + async _ensureEditorRuntime() { return false; }, + }), false); +}); + +test('a broken lazy preload remains a hard harness failure (#380)', async () => { + await assert.rejects( + ensureHarnessEditorRuntime({ + async _ensureEditorRuntime() { throw new Error('chunk failed'); }, + }), + /chunk failed/, + ); +}); diff --git a/test/performance-contract.test.mjs b/test/performance-contract.test.mjs index 71e9cf8a..4f0f6634 100644 --- a/test/performance-contract.test.mjs +++ b/test/performance-contract.test.mjs @@ -21,6 +21,12 @@ const directCardMembers = (relativePath) => { const declaredMembers = (contract) => new Set([ ...contract.methods, ...contract.fields, ...(contract.optionalFields || []), + ...(contract.fieldAlternatives || []).flatMap((choice) => Object.values(choice)), +]); + +const currentProductionMembers = (contract) => new Set([ + ...contract.methods, ...contract.fields, ...(contract.optionalFields || []), + ...(contract.fieldAlternatives || []).map((choice) => choice.current), ]); test('large-house benchmark declares every private card member it consumes', () => { @@ -42,10 +48,18 @@ test('Glow benchmark declares every private card member it consumes', () => { ); }); +test('static-card cache diagnostics accept a pre-Glow stable baseline (#380)', () => { + const source = readFileSync( + new URL('../demo/benchmark_glow.mjs', import.meta.url), 'utf8', + ); + assert.match(source, /card\._glowRuntimeState\?\.clipCache\?\.size \?\? 0/); + assert.doesNotMatch(source, /card\._glowRuntimeState\.clipCache/); +}); + test('performance contracts reference real production members', () => { const source = readHouseplanProductionSource(); for (const contract of [LARGE_HOUSE_CARD_CONTRACT, GLOW_CARD_CONTRACT]) { - for (const name of declaredMembers(contract)) { + for (const name of currentProductionMembers(contract)) { assert.match(source, new RegExp(`\\b(?:private\\s+(?:declare\\s+|get\\s+)?|get\\s+)${name}\\b`), `${contract.label} declares missing production member ${name}`); } @@ -57,6 +71,44 @@ test('performance contracts reference real production members', () => { } }); +test('large-house contract accepts only an explicit current or stable resize owner', () => { + const methods = Object.fromEntries( + LARGE_HOUSE_CARD_CONTRACT.methods.map((name) => [name, () => undefined]), + ); + const fields = Object.fromEntries( + LARGE_HOUSE_CARD_CONTRACT.fields.map((name) => [name, null]), + ); + Object.assign(fields, { + _booting: false, + _cleanFloorCache: new Map(), + _devices: [], + _glowClipCache: new Map(), + _gridPitch: 1, + _loadOk: true, + _model: [], + _path: [], + _serverCfg: {}, + _tool: 'view', + }); + + assert.doesNotThrow(() => assertCardContract( + { ...methods, ...fields, _resize: {} }, LARGE_HOUSE_CARD_CONTRACT, + )); + assert.doesNotThrow(() => assertCardContract( + { ...methods, ...fields, _rszDrag: null }, LARGE_HOUSE_CARD_CONTRACT, + )); + assert.throws( + () => assertCardContract( + { ...methods, ...fields, _resize: false }, LARGE_HOUSE_CARD_CONTRACT, + ), + /invalid private API types: _resize:object/, + ); + assert.throws( + () => assertCardContract({ ...methods, ...fields }, LARGE_HOUSE_CARD_CONTRACT), + /missing private API: _resize\|_rszDrag/, + ); +}); + test('contract accepts recent optional fields only when their runtime type is valid', () => { const base = { _cleanFloorCache: new Map(), _glowClipCache: new Map(),