From 0e6cb7570ba8f763f1a6fef538f6a57d2f8c90c0 Mon Sep 17 00:00:00 2001 From: Sergey Matyunin Date: Fri, 14 Aug 2026 22:19:50 +0300 Subject: [PATCH] docs: specify v1.64 performance repairs Issue: #156 User-Visible: no --- .../specs/156-full-performance-regressions.md | 312 ++++++++++++++++++ docs/specs/README.md | 1 + 2 files changed, 313 insertions(+) create mode 100644 docs/specs/156-full-performance-regressions.md diff --git a/docs/specs/156-full-performance-regressions.md b/docs/specs/156-full-performance-regressions.md new file mode 100644 index 00000000..5aa2bb69 --- /dev/null +++ b/docs/specs/156-full-performance-regressions.md @@ -0,0 +1,312 @@ +# Issue #156 — регрессии Full Performance перед v1.64.0 stable + +- **Issue:** https://github.com/Matysh/houseplan-card/issues/156 +- **Редакция:** первая редакция для независимого ревью; статус определяется только метками issue +- **Тип / приоритет:** bug / P1 +- **Оценка:** пользовательская ценность 8/10; ценность для разработки 10/10; + сложность и риск 6/10 +- **Область:** runtime physical geometry, Plan snap hover/render, release performance gate +- **Модель данных:** без новых полей, миграции и backend-изменений +- **Связано:** #137, #141, #153, #124, `docs/specs/137-plan-snap-overlay.md`, + `docs/specs/141-wall-junctions.md`, `demo/performance/README.md` + +## 1. Контекст и симптом + +Exact-SHA stable-кандидат `321d153c22dfe2087e31437720919f872d9cd47c` +дважды воспроизводимо провалил обязательный Full Performance: + +- run: https://github.com/Matysh/houseplan-card/actions/runs/31829284992; +- обычный и изометрический профили превысили бюджеты полного цикла из 12 + переключений этажей и суммарных Long Tasks; +- Plan snap профиль дополнительно потратил около 2,65 секунды на 120 pointermove + до первого клика, включая один Long Task около 2,54 секунды; +- оба Glow-профиля прошли; +- `viewToggleMs` прошёл, поэтому #124 не является причиной этого падения. + +Attempt 1 и повтор на свежем runner-е показали один класс превышений. Ослабление +budget запрещено; стабильный релиз #153 заблокирован до зелёного exact-SHA +Full Performance. + +## 2. Пользовательский сценарий + +**Персона:** администратор дома, который переключает этажи в View или точно +рисует контур/перегородку в desktop-редакторе Плана. + +**До исправления:** релизная линия beta выполняет лишнюю polygon-union работу при +переключении пространства. В Плане движение мыши над snap topology до первой +точки инициирует полный render большой сцены; на крупном плане серия движений +может заблокировать main thread на секунды. + +**После исправления:** переключение не строит неиспользуемую unioned geometry, а +до первой точки hover меняет только один SVG-маркер. Линии, точки, snap-result, +rubber-band, click и сохранённая геометрия визуально и семантически прежние. + +## 3. Установленные причины + +### 3.1 Лишняя unioned physical geometry + +После #141 `physicalBodySet()` всегда вычисляет `geometry` через polygon union. +Полный card runtime и `physicalBodies()` используют только `drafts`, +`partitions`, `columns`, `patches` и `all`; `geometry` в production-пути не +читается. Одноэлементный cache полного card меняет ключ при каждом переходе на +другой этаж, поэтому ненужный union повторяется в каждом switch. + +### 3.2 Полный render на initial snap hover + +#137 сохраняет результат hover в `_planSnapHover`, а координату — в реактивный +`_cursorPt`. Даже когда `_path` пуст и rubber-band ещё отсутствует, каждый +pointermove назначает `_cursorPt` и запускает полный Lit render 60 комнат, +200 устройств и всей статической snap topology. В benchmark 120 обновлений идут +в одном interaction window и образуют наблюдаемый длинный блок. + +## 4. Scope + +В issue входят: + +1. Разделение построения raw/join-patch physical bodies и необязательной + unioned geometry так, чтобы production consumers без union не платили за неё. +2. Сохранение текущего pure API/семантики unioned geometry для unit-проверок и + любого consumer, которому она действительно нужна. +3. Разделение Plan snap overlay на неизменную topology и единственный активный + marker. +4. Обновление active marker до первого клика без полного render карточки. +5. Сохранение обычного реактивного rubber-band после появления anchor, без + повторного создания статической topology при неизменном snapshot. +6. Regression unit/smoke и прохождение неизменённых Full Performance budgets. +7. Запись исправления в оба changelog и выпуск через дополнительную beta перед + повторным stable promotion. + +## 5. Non-scope + +Не входят: + +- изменение цветов, радиусов, толщины линий, layering или видимости overlay; +- изменение tolerance, endpoint-first приоритета, grid/45°/line quantization; +- изменение click/tap, T-join, autoclose, room/draft/partition или wall geometry; +- новый spatial index для resolver или общая декомпозиция большого render; +- изменение fixture, samples, budget, baseline selection или compare policy; +- исправление отдельного изометрического долга #124; +- schema, backend, storage, import/export, i18n, network или HA permissions; +- публичная функция, настройка или feature flag. + +## 6. Контракт physical geometry + +1. Pure-построитель physical parts возвращает тот же детерминированный набор + `drafts`, `partitions`, `columns`, `patches` и `all`, что текущий + `physicalBodySet()` до union. +2. `physicalBodySet()` либо эквивалентный явный union-consumer сохраняет текущее + значение `geometry`, порядок-независимость, bounded mitre/bevel и fail-closed + поведение malformed inputs. +3. Full card и static-card consumers, которым нужны только bodies, используют + non-union путь. Они не вычисляют `geometry` скрыто через spread, getter или + wrapper side effect. +4. Cache keys и invalidation остаются структурными: пространство, config epoch, + `cell_cm` и grid/scale. HA state, hover и theme не добавляются в ключ. +5. Raw identity для editor hit/selection и joined patches для render/physics + сохраняются. Результирующие SVG/clean-floor/light/iso pixels не меняются. + +## 7. Контракт snap hover и DOM + +### 7.1 Статическая topology + +- Статические `plan-snap-line` и endpoint nodes строятся из существующего + immutable geometry snapshot. +- При pointermove с тем же snapshot их DOM nodes и атрибуты не пересоздаются и + не обходятся ради выбора active styling. +- Cache invalidation остаётся прежним: structural config, space, active draft + и другие уже входящие в geometry key факторы перестраивают topology. + +### 7.2 Один активный marker + +- Overlay содержит не более одного active marker. +- Для endpoint marker расположен точно поверх статической точки, имеет прежний + active endpoint style и радиус 10 см. +- Для line candidate marker имеет прежний dynamic style, wall-bound/grid-bound + координату и радиус 10 см. +- При miss, pointerleave, смене инструмента/пространства/режима, pan/pinch cancel + marker очищается. +- Marker остаётся `pointer-events:none`, `aria-hidden` через родительскую группу + и не становится focusable. + +### 7.3 Initial hover без полного render + +Пока `_path` пуст: + +1. pointermove использует тот же canonical `resolvePlanSnap()`; +2. `_cursorPt` остаётся пустым, потому что rubber-band ещё нечего рисовать; +3. меняется только dedicated active marker внутри уже существующего overlay; +4. полный Lit update карточки не запрашивается только ради hover; +5. click повторно вызывает resolver из координат события и не доверяет DOM или + устаревшему hover state. + +После первого click текущий reactive path/cursor render сохраняется: live +rubber-band должен двигаться вместе с marker. Следующий внешний Lit render обязан +согласовать dedicated marker с текущим state, поэтому оптимизация не создаёт +второго авторитетного snap-result. + +## 8. Совместимость, touch и accessibility + +- Persisted config и layout побайтно не меняются от hover/render/cache warm-up. +- Старые планы получают оптимизацию без migration и write-back. +- Desktop hover остаётся полным контрактом #137. +- Touch editor остаётся best effort: tap без hover повторно resolves candidate; + pinch/pan/suppressed click не сохраняют сегмент. View и kiosk не получают + новых interaction branches. +- Новых доступных элементов, текстов и keyboard semantics нет. Forced colours, + dark/light theme и reduced motion сохраняют текущий вид. + +## 9. Производительность и наблюдаемость + +1. Budgets и runner не ослабляются и не меняют смысл. +2. `large-house-v1`, `large-house-isometric-v1` и + `large-house-plan-snap-v1` проходят относительные и абсолютные thresholds на + одном exact SHA; оба Glow-профиля остаются зелёными. +3. Plan snap diagnostics по-прежнему доказывают cache/DOM/config/ws stability, + endpoint и line paths и единственный active candidate. +4. Технический regression test отдельно доказывает отсутствие полного Lit + update для initial hover; один зелёный timing без structural assertion + недостаточен. +5. Full Performance запускается после публикации исправленной beta и нового + promotion SHA в `main`, как требует stable runbook. + +## 10. Acceptance criteria + +- **AC1 (`unit` + code review; разработчик/ревьюер):** pure physical-parts path + возвращает те же raw bodies и junction patches, что текущая реализация, но не + вызывает polygon union; explicit union path сохраняет прежнюю `geometry` и + permutation/area assertions #141. +- **AC2 (`unit` + `smoke`; разработчик):** full/static runtime использует + non-union parts без скрытого eager вычисления; View/Plan/static/hidden iso + сохраняют прежний physical footprint, clean floor и wall-junction pixels. +- **AC3 (`smoke`; разработчик):** при пустом `_path` endpoint, line и miss + pointermove обновляют ровно один active marker без вызова полного Lit update, + не меняют static-node identity/count, geometry cache, config и websocket. +- **AC4 (`smoke` + `golden`; разработчик):** endpoint marker остаётся прежнего + цвета/радиуса 10 см поверх статического endpoint, line marker — прежнего + dynamic вида; lines/endpoints, layering, forced-colours и pointer transparency + визуально не меняются. +- **AC5 (`unit` + `smoke`; разработчик):** click без предварительного hover, + click после initial hover и следующие clicks используют canonical resolver; + endpoint/line coordinates, Shift/grid priority, T-join, autoclose и + no-split-target semantics #137/#138/#141 сохраняются. +- **AC6 (`smoke`; разработчик):** после первого anchor rubber-band и marker + следуют pointer, а pointerleave, tool/space/mode change, pan/pinch и cancel + очищают marker без ghost state или лишнего commit. +- **AC7 (`performance`; разработчик):** exact-SHA Full Performance проходит без + правок пяти budget files, fixture semantics, sample count и compare logic; + артефакт содержит зелёные ordinary, isometric, plan-snap, blend и overlay + comparisons. +- **AC8 (`unit` + backend/schema review; разработчик/ревьюер):** schema, + serialized config/layout, backend, import/export, i18n, HA calls, permissions + и зависимости не меняются; hover и cache не создают writes. +- **AC9 (`typecheck` + `unit` + `build` + documentation review; разработчик):** + implementation-loop gates зелёные; RU/EN changelog обновлены в том же + `User-Visible: yes` commit; три bundle snapshots после release build идентичны. +- **AC10 (`release gate`; релиз-менеджер):** исправление опубликовано в новой + `v1.64.0-beta.*`, beta issue закрыты штатно, затем новый promotion-only stable + commit получает зелёные exact-SHA Validate и Full Performance до тега + `v1.64.0`. + +## 11. План тестирования + +### 11.1 Unit + +- выделить pure physical-parts builder и сравнить `drafts/partitions/columns`, + patches и `all` с explicit union wrapper; +- сохранить #141 matrix: endpoint/line joins, unequal thickness, permutation, + near-miss, malformed/zero-length и geometry area; +- добавить assertion, что production parts API не требует/не материализует + `geometry`; +- существующие resolver tests #137/#138 остаются без изменения ожидаемых точек. + +### 11.2 Targeted production-bundle smoke + +Расширить `demo/smoke_plan_snap_overlay.mjs` либо добавить узкий smoke: + +1. после initial render перехватить/посчитать card update requests; +2. выполнить endpoint → line → miss pointermove при пустом path; +3. доказать ноль full updates, один active marker, неизменные static nodes/cache + и отсутствие writes; +4. выполнить click без hover и после hover, затем move с непустым path; +5. проверить rubber-band, exact coordinates и очистку marker; +6. сохранить существующие gesture, gaps, draft exclusion и T-join проверки. + +Существующие smoke/golden #137/#141 являются визуальным и поведенческим +контрактом. Новые golden не требуются, потому что pixels должны остаться +прежними; любой непредусмотренный diff исследуется и не принимается автоматически. + +### 11.3 Performance и release + +- implementation loop: `npm run typecheck`, `npm test`, `npm run build`; +- перед beta: полный smoke/golden/HA/performance gate по runbook; +- после promotion в main: exact-SHA Validate и Full Performance; +- сравнить все пять comparison reports, а не останавливаться на первом зелёном + профиле; +- не переиспользовать красный artifact как доказательство и не ослаблять budget. + +Backend не меняется; targeted backend test не нужен, полный Linux harness остаётся +release gate. + +## 12. План реализации + +1. Выделить из `physicalBodySet()` построитель physical parts без union; явный + set/geometry consumer строит union только по запросу. +2. Перевести full/static body consumers на parts path и убрать `geometry` из + runtime cache shape, если оно не читается. +3. Разделить static snap topology и dedicated active marker; исключить обход + всех endpoints ради active styling. +4. Добавить синхронизацию marker с canonical hover state и fast path при пустом + `_path`; сохранить reactive path после anchor. +5. Дополнить unit/smoke, прогнать implementation gates. +6. Обновить changelog и release metadata, пересобрать bundle snapshots в + prerelease commit, выпустить beta и повторить stable promotion/gates. + +Точные имена helpers и способ локального DOM-handle (`ref`, controller или +эквивалент) не являются продуктовым контрактом. + +## 13. Release-артефакты + +Исправление пользовательски заметно как восстановление отзывчивости, поэтому +implementation commit имеет `User-Visible: yes` и обновляет: + +- `docs/CHANGELOG.md`; +- `docs/CHANGELOG.ru.md`. + +`docs/USER-GUIDE.ru.md`, `docs/CANVAS.md`, `docs/WALL-THICKNESS.md` и i18n не +меняются: описанный пользовательский контракт остаётся прежним. Новые golden +не ожидаются. Обязательны targeted smoke, полный beta gate, exact-SHA artifacts +и повторный stable Full Performance. Security report не требуется. + +## 14. Риски и снижение + +| Риск | Вероятность / ущерб | Снижение | +|---|---|---| +| Dedicated marker расходится с Lit state после внешнего render | средняя / высокий | один canonical hover state; render всегда повторно синхронизирует marker; transition smoke | +| Fast path оставляет ghost marker при навигации/gesture | средняя / средний | единый clear helper и matrix pointerleave/tool/space/mode/pinch | +| Endpoint меняет yellow style на dynamic green | средняя / средний | отдельный kind/class contract и golden/smoke radius/style assertions | +| Удаление eager union меняет physical footprint | низкая / высокий | parts equality, existing #141 unit/smoke/golden, explicit union wrapper unchanged | +| Локальный runner зелёный, Linux всё ещё красный | средняя / высокий | только exact-SHA Full Performance является release evidence | +| Оптимизация маскируется ослаблением gate | низкая / высокий | budget/fixture/compare files вне implementation diff | + +## 15. Откат + +Откат — revert user-visible implementation commit #156 вместе с тестами, +changelog и соответствующими prerelease bundle snapshots. Persisted data и +schema не меняются, migration/data rollback не нужны. После отката возвращается +performance-регрессия, поэтому стабильный релиз остаётся заблокированным. + +## 16. Принятые технические предположения + +1. Внешний snap/junction контракт полностью наследуется из #137/#138/#141; + новых продуктовых решений нет. +2. Рекомендуемый physical-parts helper может называться иначе, если eager union + доказуемо отсутствует у production consumers. +3. Active marker может обновляться через локальный DOM handle внутри overlay; + SVG DOM не становится источником snap geometry или commit coordinates. +4. Static topology может быть защищена Lit `guard`, вынесена в child component + или сохранена эквивалентным способом; node identity/count обязаны быть стабильны. +5. Fast path применяется только когда live rubber-band отсутствует. После anchor + допустим полный reactive update, если Full Performance и структурные AC зелёные. +6. Любая дополнительная оптимизация вне двух установленных горячих путей требует + отдельного обоснования в issue и не должна расширять пользовательский scope. +7. Открытых продуктовых вопросов нет. diff --git a/docs/specs/README.md b/docs/specs/README.md index 941efd2e..14e8aaaa 100644 --- a/docs/specs/README.md +++ b/docs/specs/README.md @@ -48,6 +48,7 @@ GitHub Issues и GitHub Projects (v2) остаются единственным | [#131](https://github.com/Matysh/houseplan-card/issues/131) Полный первый кадр View у read-only-пользователя | [131-readonly-cold-start.md](131-readonly-cold-start.md) | | [#138](https://github.com/Matysh/houseplan-card/issues/138) Автозамыкание комнаты по существующей стене | [138-adjacent-room-autoclose.md](138-adjacent-room-autoclose.md) | | [#146](https://github.com/Matysh/houseplan-card/issues/146) Четырёхфазный фон «Следует за Солнцем» | [146-four-phase-sun-background.md](146-four-phase-sun-background.md) | +| [#156](https://github.com/Matysh/houseplan-card/issues/156) Регрессии Full Performance перед v1.64.0 stable | [156-full-performance-regressions.md](156-full-performance-regressions.md) | ## P2