mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-29 03:09:36 +00:00
fix: the authoritative adoption keeps its own task (#520)
The r1 diagnosis was wrong, and the measurement in the code review proved it: removing the two declarations from `static properties` left the cold start at 19 update cycles, 4 model builds and 4 config epochs, exactly the numbers of the bug. Lit's forced first-update change does mark `_serverCfg` changed, but at that moment the body and `_cfgEpochPreservedConfig` are both null, `preserveGeometry` is true and the epoch does not move. The comment above `static properties` now says that; the declaration still stays out, because two owners of one reactivity is what #500 removed. The real cause is the `await`. Before #500 everything from `_adoptStructuralResponses` to the end of the load ran in one task: the adopted bodies, `_adoptInitialSpace`, the viewport restore, `_loadOk`, and the device seeding — whose `_syncNewDevices`/`_seedHiddenDevices` write the config back — all landed in a single Lit update. #500 made the adoption an async sequence, so the caller resumes one microtask later, after Lit has already painted the adopted config; the seeding writes then arrive as a second config epoch, a second model build and a second paint of a 60-room house. `GatedAdoptionInput` gains `afterAdopt`, the mirror of `beforeAdopt`: it runs synchronously at the end of the sequence, before the promise resolves. `_loadFromServer` moves the viewport restore, `_loadOk` and the device rebuild into it — `_syncNewDevices` refuses to write before `_loadOk`, so the order inside the hook matters — and the load tail now rebuilds devices only when nothing was adopted. `_reloadConfigOnly` takes the same route. Measured with the project's own runner, 7 samples per profile, base `a44fbd37` against this tree (Chromium 152, sandbox): interaction modelReadyMs 761.3 ≤ 950.56 (base 731.2) firstStableRenderMs 2567.2 ≤ 3000 (base 2542.7) longTask.maxSingleMs 690 ≤ 921 · cache.entries.cleanFloor 100 isometric modelReadyMs 1252.9 ≤ 1499.76 (base 1249.8) firstStableRenderMs 1378 ≤ 1610.16 (base 1341.8) Boot diagnostics on both trees: 18 update cycles, 3 model builds, 3 config epochs, with the same epoch trace — the candidate is no longer distinguishable from the base. Witnesses. `config-adoption.test.mjs` queues a microtask at the start of the adoption and pins that `afterAdopt` runs before it — the probe fails the moment the hook crosses an await; `config-adoption-ownership.test.mjs` pins the wiring in the card and the hook's place in the sequence. Mutants `adoption-tail-defers-caller-hook` (defers the hook by one microtask) and `authoritative-load-seeds-devices-after-the-await` (drops the rebuild from the hook) redden them. The initial View graph grows 40 B gzip, so the #438 ceiling is recentred 300 300 → 300 400 with the usual dated note; measured 299 812 B keeps 588 B above and 1 412 B below the band. The overall 301 066 B budget and the #367 headroom debt are untouched. Issue: #520 User-Visible: no
This commit is contained in:
@@ -7251,9 +7251,10 @@ const MUTANT_DEFINITIONS = [
|
||||
id: 'adoption-bodies-declared-reactive',
|
||||
guard: 'npx tsc -p tsconfig.test.json && node scripts/fix-test-build.mjs '
|
||||
+ '&& node --test --test-name-pattern="#520" test/config-adoption-ownership.test.mjs',
|
||||
because: 'declaring the adoption bodies as Lit properties marks them `wrapped`, and the '
|
||||
+ 'forced first-update change raises the config epoch: the model of a 60-room house is '
|
||||
+ 'built and painted a second time, +550 ms to the first stable frame (#520)',
|
||||
because: 'since #500 `_adoption` owns the reactivity of these bodies; declaring them again '
|
||||
+ 'makes Lit a second owner, marks them `wrapped` and force-writes a change on the first '
|
||||
+ 'update — today harmless only because both sides are null at that moment, and a '
|
||||
+ 'spurious config epoch as soon as a warm cache arrives earlier (#520 AC1)',
|
||||
patches: [{
|
||||
file: 'src/houseplan-card.ts',
|
||||
find: " _devices: { state: true },",
|
||||
@@ -7272,6 +7273,30 @@ const MUTANT_DEFINITIONS = [
|
||||
replace: ' this.config = next;',
|
||||
}],
|
||||
},
|
||||
{
|
||||
id: 'adoption-tail-defers-caller-hook',
|
||||
guard: 'node --test test/config-adoption.test.mjs',
|
||||
because: 'the caller hook closes the adoption task; deferring it by even one microtask lets '
|
||||
+ 'Lit paint the adopted config first, and the device seeding that follows costs a second '
|
||||
+ 'config epoch, a second model build and a second paint of a 60-room house (#520 AC6)',
|
||||
patches: [{
|
||||
file: 'src/config-adoption.ts',
|
||||
find: ' input.afterAdopt?.();',
|
||||
replace: ' void Promise.resolve().then(() => input.afterAdopt?.());',
|
||||
}],
|
||||
},
|
||||
{
|
||||
id: 'authoritative-load-seeds-devices-after-the-await',
|
||||
guard: 'node --test --test-name-pattern="#520" test/config-adoption-ownership.test.mjs',
|
||||
because: 'the cold-start regression of #520 itself: seeding the devices outside the adoption '
|
||||
+ 'task writes the config back after Lit already painted it, 19 update cycles, 4 model '
|
||||
+ 'builds and 4 config epochs against 18/3/3 (#520 AC6)',
|
||||
patches: [{
|
||||
file: 'src/houseplan-card.ts',
|
||||
find: ' this._loadOk = true;\n rebuildDevices();',
|
||||
replace: ' this._loadOk = true;',
|
||||
}],
|
||||
},
|
||||
{
|
||||
id: 'live-editor-view-mode-routes-live',
|
||||
guard: 'node --test test/live-editor.test.mjs',
|
||||
|
||||
Reference in New Issue
Block a user