mirror of
https://github.com/Matysh/houseplan-card
synced 2026-10-04 13:48:57 +00:00
Profiling #694 found three costs on every View pass, paid even with the summary panel hidden. The summary panel read the safe-area probe's computed style in layout(), which the card reaches up to five times per render (renderControls, menuItems, renderPanel twice, the clock check), and its updated() measured the stage, probe and kiosk buttons after every DOM commit. The insets now live in the measured state: measureLayout is the only method that reads style or layout, and updated() calls it only when an input of the measurement changed (probe, kiosk buttons or stage element, title, language, mode, kiosk, kiosk scale, narrow, HA theme), after connect() or an identity change, on visibility, once after document.fonts.ready, and from resized() as before. A floor switch or an HA tick no longer measures. The _model getter rebuilt the config fingerprint (a walk over every space and room with JSON.stringify of room settings) on each of its dozens of reads per render. ConfigFingerprintPass remembers the whole cache key (epoch and fingerprint) from the start of willUpdate() to the end of render() while the epoch, the config object and its spaces array are unchanged. Remembering only the fingerprint and concatenating the key on every read was tried first: in 2.5D on the large house the switch cycle measured slower than without any memo, and CPU profiles showed several times more garbage collection on load and on the first visit of a floor; one remembered key per pass has neither. Outside the pass (handlers, updated(), timers) every read still builds the key, so an in-place edit without an epoch bump stays visible (HP-1454-04). No write to the fingerprinted fields is reachable from willUpdate() or render(). _isoScene read the stage box during render only to feed an aspect into the overlay fit, whose frame has not depended on the aspect since #713. It now uses the frame's own aspect and passes stageSize: null. render-layout-read.mjs now also judges _isoScene and the whole summary runtime except measureLayout, forbids layout property reads (clientWidth, offsetTop, ...) besides the two calls, and reports every violation. Two registered mutants restore the old reads. No visible change: panel caps, side, offsets and kiosk clearance are computed from the same values; the 2.5D frame is the same. Issue: #725 User-Visible: no Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
112 lines
4.1 KiB
JavaScript
112 lines
4.1 KiB
JavaScript
import assert from 'node:assert/strict';
|
|
import test from 'node:test';
|
|
|
|
import { ConfigFingerprintPass } from '../test-build/config-fingerprint-pass.js';
|
|
|
|
/** A builder that counts how often the fingerprint is walked. */
|
|
const counter = () => {
|
|
const built = { count: 0 };
|
|
return { built, build: () => { built.count += 1; return `fingerprint-${built.count}`; } };
|
|
};
|
|
|
|
test('#725 AC2: 50 reads inside one pass build the fingerprint once', () => {
|
|
const pass = new ConfigFingerprintPass();
|
|
const { built, build } = counter();
|
|
const config = { spaces: [] };
|
|
pass.begin();
|
|
const values = Array.from({ length: 50 }, () => pass.read(3, config, build));
|
|
pass.end();
|
|
assert.equal(built.count, 1);
|
|
assert.deepEqual(new Set(values), new Set(['fingerprint-1']), 'every read gets the same fingerprint');
|
|
});
|
|
|
|
test('#725 AC2: an epoch change inside the pass builds again', () => {
|
|
const pass = new ConfigFingerprintPass();
|
|
const { built, build } = counter();
|
|
const config = { spaces: [] };
|
|
pass.begin();
|
|
pass.read(3, config, build);
|
|
pass.read(3, config, build);
|
|
// willUpdate bumps `_cfgEpoch` after its first `_model` read.
|
|
assert.equal(pass.read(4, config, build), 'fingerprint-2');
|
|
pass.read(4, config, build);
|
|
pass.end();
|
|
assert.equal(built.count, 2);
|
|
});
|
|
|
|
test('#725 AC2: a new config object or a new spaces array inside the pass builds again', () => {
|
|
const pass = new ConfigFingerprintPass();
|
|
const { built, build } = counter();
|
|
const config = { spaces: [] };
|
|
pass.begin();
|
|
pass.read(3, config, build);
|
|
const replaced = { spaces: config.spaces };
|
|
pass.read(3, replaced, build);
|
|
pass.read(3, replaced, build);
|
|
assert.equal(built.count, 2, 'a replaced config object');
|
|
replaced.spaces = [{ id: 'floor-2' }];
|
|
pass.read(3, replaced, build);
|
|
pass.read(3, replaced, build);
|
|
assert.equal(built.count, 3, 'a replaced spaces array');
|
|
pass.read(3, null, build);
|
|
pass.read(3, null, build);
|
|
assert.equal(built.count, 4, 'no config at all');
|
|
pass.end();
|
|
});
|
|
|
|
test('#725 AC2: reads outside a pass build on every read', () => {
|
|
const pass = new ConfigFingerprintPass();
|
|
const { built, build } = counter();
|
|
const config = { spaces: [] };
|
|
for (let i = 0; i < 5; i++) pass.read(3, config, build);
|
|
assert.equal(built.count, 5, 'before the first pass: handlers, timers');
|
|
pass.begin();
|
|
pass.read(3, config, build);
|
|
pass.read(3, config, build);
|
|
pass.end();
|
|
assert.equal(built.count, 6);
|
|
assert.equal(pass.read(3, config, build), 'fingerprint-7', 'after end() the remembered value is not used');
|
|
pass.read(3, config, build);
|
|
assert.equal(built.count, 8, 'updated() and later handlers see an in-place mutation (HP-1454-04)');
|
|
});
|
|
|
|
test('#725 AC2: a new pass forgets the previous one', () => {
|
|
const pass = new ConfigFingerprintPass();
|
|
const { built, build } = counter();
|
|
const config = { spaces: [] };
|
|
pass.begin();
|
|
pass.read(3, config, build);
|
|
pass.begin();
|
|
pass.read(3, config, build);
|
|
pass.end();
|
|
assert.equal(built.count, 2, 'an in-place edit between passes is seen by the next pass');
|
|
});
|
|
|
|
test('#725 AC2: a pass that never reaches end() closes itself in a microtask', async () => {
|
|
const pass = new ConfigFingerprintPass();
|
|
const { built, build } = counter();
|
|
const config = { spaces: [] };
|
|
pass.begin(); // willUpdate() threw: Lit never calls render()
|
|
pass.read(3, config, build);
|
|
await null;
|
|
pass.read(3, config, build);
|
|
pass.read(3, config, build);
|
|
assert.equal(built.count, 3, 'a handler after the failed update builds on every read');
|
|
|
|
pass.begin();
|
|
pass.end(); // pass A rendered normally; its microtask is still queued
|
|
let builtInsideB = -1;
|
|
queueMicrotask(() => {
|
|
const before = built.count;
|
|
pass.read(3, config, build);
|
|
pass.read(3, config, build);
|
|
builtInsideB = built.count - before;
|
|
});
|
|
pass.begin(); // pass B opens before A's microtask runs
|
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
assert.equal(builtInsideB, 1, "A's microtask leaves pass B open");
|
|
pass.read(3, config, build);
|
|
pass.read(3, config, build);
|
|
assert.equal(built.count, 6, "B's own microtask closes B");
|
|
});
|