27 KiB
CODE-REVIEW-582-r1
Issue: #582 · этап: code · заход r1 ·
блокирующих циклов израсходовано 0 из 4 · материал: 0a6592f4112fd7ceef68cbd3be7c44eeb8a7ef3e
(origin/dev = bae6afaae7bac2dd45bca52b6caf64a16d96f182, origin/dev..HEAD = b9bf3daa
(документ ревью ТЗ) + 0a6592f4 (сама правка)).
Скоуп
ТЗ (в теле issue) чинит воспроизводимый в HA Companion дефект: во время pinch-zoom
пространства с фоном «Следует за Солнцем» появляются фиксированные белые/прозрачные
области, вызванные тем, что постоянный will-change: filter из #532 висит на
внутренней группе .hp-paperg, чей локальный SVG-bbox при cell_cm: 1 может достигать
тысяч единиц — Chromium промотирует слой пропорционально этому координатному размеру, а
не экранному. Контракт (п.1–7) требует: сохранить визуальный контур и его перфоманс-выигрыш
из #532, но привязать промотированный слой к экранному viewport независимо от cell_cm, и
сделать то же самое для houseplan-space-card. AC1–AC9 в теле issue.
Это основная View-поверхность (J1) и touch-контракт View/kiosk (docs/TOUCH-SUPPORT.md,
release-blocking), так что вопрос "работает ли оно на самом деле" здесь не формальность.
Материал и что сделано в коде
- Новый общий рендерер
src/render/paper-scene.ts(renderPaperShapes) — заменяет три дублированные веткиpath/poly/rectвhouseplan-card.tsиspace-render.ts. - В
houseplan-card.tsиspace-render.tsдобавлен ОТДЕЛЬНЫЙ<svg class="hp-paper-outline-svg">сосед перед основнымplan-svg/hp-static-plan-svg, содержащий копию бумажных фигур; он использует тот жеviewBox/data-hp-live-viewbox, что и основной план, и тот жеisoFloorMatrixCss()в изометрии. - В
plan.styles.tsтройнойdrop-shadowиwill-change: filterперенесены с.hp-papergна.hp-paper-outline-svg(стейдж-размерный корневой<svg>, растянутый правилом.zoomwrap > svg { width:100%; height:100% }), тем самым слой теперь действительно ограничен экранными пикселями, а не локальными SVG-единицами. space-card.tsполучил такое же правило для.hp-static-stage .hp-paper-outline-svg.- Новый браузерный смок
demo/smoke_daycycle_layer_budget.mjs(CDP LayerTree + screencast) — свидетель AC1/AC3. Новый мутантdaycycle-outline-promoted-on-inner-paper(guard — этот смок) — свидетель AC2.smoke_daycycle_raster.mjs/smoke_bg_color.mjsобновлены под новые имена классов — свидетели AC4/AC5. Новыйtest/paper-scene-contract.test.mjs— свидетель AC7.scripts/smoke-links.mjsрегистрирует связьrenderPaperShapes→ 4 смока.scripts/bundle-budget.mjsподвинул потолок на 200 Б с объяснением. Оба changelog иdocs/ARCHITECTURE.md/docs/SUN.md/docs/TOUCH-SUPPORT.md/docs/TESTING.mdобновлены согласованно с кодом. Трейлеры коммитаIssue: #582/User-Visible: yesна месте.
Как проверялось
Дешёвые гейты подтверждены зелёным Validate на точном SHA
(https://github.com/Matysh/houseplan-card/actions/runs/34940487324, workflow_dispatch,
без heavy): предполёт, классификация, «Фронтенд: типы/юниты/бандл», 6 шардов
«Мутанты по диффу» — всё success. Тяжёлые джобы (golden, smoke, performance_smoke,
hacs, hassfest, geometry_parity, backend) в этом прогоне skipped — не PR,
не Release:, не full=true, поэтому они не гейтили этот пуш. Именно эти пропущенные
джобы оказались важны (см. находки).
Я лично прогнал (то, что Validate не покрыл, и что явно относится к диффу):
| Гейт | Результат | Почему прогнан/не прогнан |
|---|---|---|
npm run bundle:sync (typecheck+build+rollup+bundle-tree) |
зелёный, git status после — чисто |
дёшево, диф трогает src/** |
node demo/smoke_daycycle_layer_budget.mjs (AC1/AC3) |
зелёный, 4 прогона подряд | прямое совпадение с диффом |
node demo/smoke_bg_color.mjs |
зелёный (все 47 проверок, включая изменённую staticCardLayersStayOrdered) |
зарегистрированная связь, CI её не гонял (heavy=false) |
node demo/smoke_live_pan_coverage.mjs |
зелёный | зарегистрированная связь, CI её не гонял |
node demo/smoke_daycycle_raster.mjs (AC4, свидетель #532) |
красный, воспроизводимо 8/8 (ratio 2.16–2.50 против потолка 2.0); на origin/dev тот же смок даёт 1.01–1.14 в 7/7 запусках в ЭТОМ ЖЕ окружении |
прямое совпадение с диффом; CI отчитался зелёным на этом SHA, но локальный результат детерминирован и объяснён (см. Находка №1) |
npm run golden:verify (AC6, полная матрица) |
4 из 4 day-cycle сцен = different, остальные ~150 сцен passed |
diff меняет рендер; CI golden был skipped на этом пуше, так что это первый реальный прогон на данном SHA |
npm run inventory |
не гонял | не требуется для этого раунда |
python -m pytest tests_backend -q, HACS, Hassfest, geometry parity |
не гонял | диф не трогает custom_components/**/*.py, манифесты или геометрию решётки/толщины стен |
performance_smoke (полный) |
не гонял | AC4 уже красный на более дешёвом целевом свидетеле; полный перф-профиль не добавит новой информации до фикса |
Плюс диагностика причины (CDP LayerTree + compositingReasons, одна и та же гарнитура
demo/serve.mjs, один и тот же демо-фикстур 1400×900, сравнение origin/dev vs HEAD
бок о бок в одном окружении) — вынесена в Находку №1.
Находки
Находка №1 (High) — AC4 не держится: день-цикл при pan рерастеризует весь план, не только контур
Файлы: src/houseplan-card.ts:11597-11603, src/styles/plan.styles.ts:97-105,
src/space-render.ts (тот же паттерн для static card).
Механизм. Раньше will-change: filter стоял на .hp-paperg — <g> ВНУТРИ
plan-svg, с малым собственным bbox. plan-svg целиком в свой отдельный
compositor-слой не попадал (в baseline origin/dev слой plan-svg в CDP LayerTree
вообще не значится: контент рисуется в общей поверхности). Патч кладёт контур в
ОТДЕЛЬНЫЙ <svg class="hp-paper-outline-svg">-СОСЕД перед plan-svg в том же
.zoomwrap. Chromium корректно ограничивает промотированный слой контура экранными
пикселями (это и было целью, AC1/AC2/AC3 — подтверждены), но побочный эффект: теперь
plan-svg (ВЕСЬ видимый план — стены, комнаты, устройства, декор) визуально
перекрывает промотированный слой-соседа и по правилам Chromium сам получает
отдельный compositor-слой с причиной "Overlaps other composited content"
(проверено CDP LayerTree.compositingReasons на обоих деревьях, один и тот же демо-стенд,
1400×900):
origin/dev (до патча): hp-paperg 920×720 (will-change: filter); НЕТ отдельного слоя plan-svg
0a6592f4 (после патча): hp-paper-outline-svg 780×724 (will-change: filter);
plan-svg 780×724 ("Overlaps other composited content") ← новый слой
Это ПОЛНЫЙ план как отдельная растеризуемая поверхность, которая теперь должна перерисовываться при любом визуальном изменении плана во время жеста — то есть именно тот класс стоимости, который #532 устранял для внешнего контура, теперь возвращается для всего плана.
Численное подтверждение (изолированный прогон, 20 шагов панорамы мышью, тот же демо-стенд, RasterTask по CDP Tracing):
origin/dev: 31.8 мс, 106 RasterTask
0a6592f4: 74.7 мс, 282 RasterTask (≈2.35× по времени, ≈2.7× по числу тасков)
Свидетель AC4 (demo/smoke_daycycle_raster.mjs), 60-шаговая панорама, то же
окружение, 8 прогонов на HEAD против 7 на origin/dev:
| Дерево | ratio (потолок 2.0) |
|---|---|
origin/dev |
1.09, 1.14, 1.03, 1.03, 1.04, 1.01 (все ≤ 1.14) |
0a6592f4 |
2.19, 2.16, 2.23, 2.18, 2.50, 2.33 (все ≥ 2.16) |
Разброс между двумя деревьями непересекается ни разу за 13 запусков — это не шум конкретной машины, а систематическая разница, воспроизводимая в контролируемом A/B внутри ОДНОГО окружения (обе половины смока меряются в одном процессе на одной странице, как и задумано автором смока).
Почему CI на этом SHA отчитался зелёным, а не потому что находки нет. Job
«Мутанты по диффу» реально исполняет node demo/smoke_daycycle_raster.mjs (лог
ok чистый прогон: node demo/smoke_daycycle_raster.mjs, 07:14:24) и код проверки
(serve.mjs: finish()) действительно ставит process.exitCode=1 при ratio > 2.0 —
то есть CI-раннер в моменте объективно получил ratio ≤ 2.0. Проблема в марже: порог
2.0 был откалиброван «примерно посередине по логарифму» между 7.9 (без подсказки
вообще) и 0.26 (подсказка на тесной .hp-paperg, комментарий в самом смоке, строка 15).
Новая реализация переехала с 0.26 на ~2.2–2.5 — то есть съела почти весь запас,
оставленный именно на случай машинной вариативности, и на GitHub-раннере это на
сегодня чуть-чуть укладывается, а на менее мощном/более шумном исполнителе — уже нет.
Целевая платформа этой самой задачи — Android HA Companion WebView — как правило
СЛАБЕЕ настольного CI-раннера, а не сильнее; то есть именно то устройство, ради
которого чинится #582, статистически более рискованно по AC4, чем моя тестовая машина.
Последствие. AC4 сформулирован как "свидетель #532 остаётся зелёным" — де-факто он не остаётся зелёным как класс поведения (плана целиком, а не только контура), просто конкретный числовой порог на конкретном раннере пока не перешагнут. Это разваливает и собственное объяснение ТЗ в "Технические ограничения": "Фильтр/промоушен должен применяться на уровне экранной scene surface, а не координатно большой внутренней группы" — технически верно для самого контура, но не учитывает, что помещение этой scene surface СОСЕДОМ (а не потомком) плана заставляет браузер промотировать и сам план.
Находка №2 (High) — AC6 не проходит: все 4 day-cycle golden-сцены отличаются от эталона на каноничной платформе
Файлы: demo/golden/baselines/day-cycle-{dawn,day,dusk,night}-dark.png (не
обновлены в этом диффе — git diff origin/dev...HEAD --stat -- demo/golden/ пуст).
npm run golden:verify (Linux, тот же движок, что CI/golden — каноничная платформа
по AGENTS.md) на точном SHA даёт:
| Сцена | статус | differing px | diffRatio | maxObservedDelta | порог diffRatio |
|---|---|---|---|---|---|
| day-cycle-dawn-dark | different | 26 903 | 0.0349 | 81 | 0.0005 |
| day-cycle-day-dark | different | 26 160 | 0.0339 | 81 | 0.0005 |
| day-cycle-dusk-dark | different | 25 667 | 0.0333 | 81 | 0.0005 |
| day-cycle-night-dark | different | 26 142 | 0.0339 | 94 | 0.0005 |
Это ~70× превышение допустимой доли различающихся пикселей и разница каналов до 94/255
при допуске 10/255 — не антиалиасинг-дребезг на границе допуска. Визуально (сравнение
demo/golden/baselines/day-cycle-day-dark.png и artifacts/golden/actual/day-cycle-day-dark.png
на глаз почти неотличимы; diff-маска в artifacts/golden/diff/day-cycle-day-dark.png
подсвечивает штриховку буквально ВСЕХ стен по всему плану, не только внешний контур)
похоже на тот же механизм, что и Находка №1: как только plan-svg стал отдельным
композитным слоем, финальная сборка кадра проходит через дополнительный проход
растеризации/блендинга этого слоя, слегка меняя суб-пиксельное позиционирование штриховки
стен по всему плану — отсюда широкий, но мелкий по амплитуде на глаз разброс.
Оставшиеся ~150 сцен матрицы прошли (passed), включая не-day-cycle сцены — регрессия
локализована именно в day-cycle рендере, как и предсказывает механизм Находки №1.
AC6 прямо требует: "четыре существующие day-cycle сцены... проходят... Любое
необходимое обновление baseline допускается только после объяснённого визуального
сравнения". Baseline не обновлён, объяснения в issue/коммите нет — сам факт диффа не
скрывается (никто не подделывал числа), но и не заявлен: ни в диффе, ни в комментариях
issue нет упоминания, что golden:verify был прогнан и дал результат. Раз diff явно
трогает рендер (src/**: houseplan-card.ts, space-render.ts, paper-scene.ts,
plan.styles.ts), golden:verify был обязателен перед S7-code-review
(AGENTS.md, "проверь по diff и AC" + сам этот процесс-документ).
Обе находки объединяет один и тот же корень: помещение дневного контура СОСЕДНИМ
<svg> перед plan-svg вместо ВНУТРИ него как раньше — технически верно решает
координатно-огромный слой (AC1–AC3 в порядке), но по пути превращает весь план в
отдельный композитный слой, что стоит и лишней растеризации (Находка №1), и лишнего
прохода блендинга, видимого в пикселях (Находка №2). Возможные направления для автора
(не предписание, только ориентир): не выносить контур отдельным сиблингом, а найти
способ ограничить экранными размерами именно промотированный слой ВНУТРИ существующего
дерева plan-svg (например, через clipPath/contain, привязанный к экранному
viewBox, а не к локальному bbox фигуры), либо явно и всегда держать plan-svg на
собственном стабильном слое ДО появления контура, чтобы контур не мог его "продавить"
через overlap-эвристику при первом появлении.
Находка №3 (Low, не блокирует, оставлена без правки)
src/houseplan-card.ts:11623-11624 — комментарий "One <g> around ALL paper shapes:
the external day-cycle outline (styles.ts) is composited once for the whole sheet"
относится к .hp-paperg, но сам фильтр туда больше не прикреплён (перенесён на
.hp-paper-outline-svg, отдельный узел). Смысл абзаца (одна группа = отсутствие швов
между комнатами) остаётся верным, но фраза про "the external day-cycle outline...
is composited" на этом самом <g> — уже не так. Мелкая правка комментария, не код.
Что проверено и корректно
- AC1/AC2/AC3 — подтверждены и CI-логом мутанта (
daycycle-outline-promoted-on-inner-paper, чистый прогон ok + красный на мутанте ok, 07:14:05–07:14:50 на точном SHA), и моим локальным повторным запускомsmoke_daycycle_layer_budget.mjs(4/4 зелёных): контур теперь 826×736 (экранный порядок) вместо потенциальных 4700×4200; ни один content-слой4096; 9/9 отснятых кадров pinch без белых пикселей.
- Единый живой viewport (#579, контракт п.3–4).
data-hp-live-viewboxстоит на.hp-paper-outline-svgтем же значением (camera/floor), что наplan-svg;live-viewport.ts:paintLiveViewportобновляет оба через один и тот жеquerySelectorAllи одну и ту же проекцию — контур и план не могут разойтись по кадру или масштабу отдельно от терминального примирения. Проверено чтением, не исполнением отдельно от смоков выше (которые это же утверждение покрывают AC3). - Основная бумага не фильтруется (контракт «Технические ограничения»).
renderPaperShapes(paperShapes)безgroupClassдаёт.hp-papergбез фильтра;test/paper-scene-contract.test.mjsпроверяет отсутствиеwill-change: filterна.hp-paperg; смокsmoke_daycycle_raster.mjsподтверждаетdayCyclePaperStaysUnfiltered: trueв моём прогоне. - Изометрия и static card не потеряли зеркальность.
<g transform=${iso ? isoFloorMatrixCss() : nothing}>оборачиваетrenderPaperShapesв контурном SVG тем же способом, что в основном;hp-static-stageполучил аналогичное правило z-index/ overflow/pointer-events;smoke_bg_color.mjs(staticCardLayersStayOrdered) зелёный. plan-svgz-index стал безусловным (былоclass=${iso ? 'plan-svg' : nothing}, сталоclass="plan-svg") — раньше во flat-режиме класс не вешался вовсе и элемент зависел от порядка DOM для стэкинга; теперь это явноеz-index: 1против0у контура. Не регрессия: до этого диффа конкурирующего элемента с явным z-index не было, после — есть (сам контур), и явный класс корректно фиксирует стэкинг.test/isometric-contract.test.mjsобновлён синхронно.- Changelog/трейлеры/документация.
Issue: #582+User-Visible: yesна терминальном коммите; обаdocs/CHANGELOG*.mdправлены в нём же;docs/ARCHITECTURE.md,docs/SUN.md,docs/TOUCH-SUPPORT.md,docs/TESTING.mdописывают именно то, что делает код (сверено построчно с диффом кода).scripts/bundle-budget.mjs— потолок подвинут на 200 Б с содержательным объяснением, общий бюджет не тронут. - Мутационная регистрация.
daycycle-outline-promoted-on-inner-paperпривязан к правильному guard-файлу и реально ловит возврат старогоwill-change(подтверждено и в CI, и локально).
Чего не проверял и почему
python -m pytest tests_backend, HACS, Hassfest, geometry-parity — diff не трогаетcustom_components/**/*.py, манифесты,layout/marker.space/толщину стен; эти гейты были в Validateskippedпо классификации файлов, а не по heavy-флагу, что и ожидаемо для чисто фронтенд-диффа.- Полный
performance_smoke/профили внеsmoke_daycycle_raster.mjs— AC4 уже провалена на целевом, более дёшевом свидетеле; гонять более тяжёлый профиль до фикса не добавляет решающей информации. npm run inventory— не требуется для вынесения вердикта этого раунда.- Полная browser-smoke матрица (250 файлов) — вне AC и вне выборки
smoke-select.mjs(4 зарегистрированные связи, все 4 прогнаны явно, см. таблицу выше). - AC9 (полевая приёмка на реальном Android WebView владельцем в следующей бете) — вне этого этапа по определению самого AC.
Вывод
Реализация корректно решает координатно-зависимый размер промотированного слоя
(AC1–AC3), сохраняет единый live-viewport и визуальный контракт контура на уровне
кода, документация и трейлеры в порядке. Но выбранная архитектура (контур —
СОСЕДНИЙ <svg> перед планом, а не элемент внутри него) имеет побочный эффект:
plan-svg целиком продавливается в отдельный композитный слой, что воспроизводимо
проваливает собственный AC4 (перф-свидетель #532) вне узкой марки конкретного
CI-раннера и приводит к measurable расхождению во всех 4 golden-сценах AC6 на
каноничной Linux-платформе, не сопровождённому обновлением baseline или объяснением.
Обе находки — High, обе в скоупе задачи (эта же ветка чинит то, что сама же
сформулировала как риск: "Исправление WebView может регрессировать
Firefox-производительность #532"). Возврат автору.
Материал раунда
- Ветка:
issue/582-webview-large-filter-layers, коммит0a6592f4112f— ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет. - Дерево материала:
9f4d95168fdda9650fa7d35f17d80c7177a7d12bgit log --all --format='%H %T' | grep 9f4d95168fdd - Тело issue:
4c1e1ecfe8ce205f3cfb3c44eb6028a35d0bb4358323e05ba60c6fd85b3b40ba - Вердикт конвейера:
red· High 2