diff --git a/docs/reviews/CODE-REVIEW-451-r1.md b/docs/reviews/CODE-REVIEW-451-r1.md new file mode 100644 index 00000000..fdc18e1f --- /dev/null +++ b/docs/reviews/CODE-REVIEW-451-r1.md @@ -0,0 +1,301 @@ +# CODE-REVIEW-451-r1 + +- **Issue:** #451 «План подтормаживает: диагностика на каждый кадр, реактивная камера, отсутствие фильтра HA-тиков» +- **Этап:** код-ревью, заход r1 (первый действительно проведённый — предыдущая попытка не читала код: ветка не ребейзилась на `dev` без конфликта, цикл не израсходован) +- **Материал:** `git log --oneline origin/dev..HEAD` / `git diff origin/dev...HEAD`, SHA `1850eb18b5e8f0813f9d2efb50694dc4ca1fff26` +- **ТЗ:** `docs/specs/451-render-performance.md` (полный трек, `SPEC-REVIEW-451-r2` зелёный) +- **Вердикт:** см. итог в конце документа + +## Скоуп диффа + +75 файлов, +3947/‑1166 (без учёта `dist/**`/`custom_components/.../frontend/**`, класс D). Новые модули: +`src/render-invalidation.ts`, `src/houseplan-render-lifecycle.ts`, `src/live-viewport.ts`, +`src/live-hover.ts`, `src/live-editor.ts`, `src/live-interaction-runtime.ts`, +`src/pointer-move-queue.ts`, `src/interaction-types.ts`. Изменены `src/houseplan-card.ts` +(overridden `requestUpdate`, diagnostics, terminal reconciliation), `src/houseplan-editor-runtime.ts` +(pointer-move очереди для plan/decor/backdrop/opening/resize), `src/render-device-snapshot.ts` +(`entityIds`), `src/zigbee-topology-overlay-bridge.ts` (live-layer маркер). Плюс performance harness +(`demo/benchmark_large_house.mjs`, `budgets-large-house-interaction.json`, `card-contract.mjs`, +`evaluate.mjs`, `performance.yml`) и точечные правки существующих `demo/smoke_*.mjs` под новую разметку +`data-hp-live-*`. Геометрия/модель стен, `layout`, `marker.space`, `open_spans` не тронуты — гейт +`npm run invariants` не требуется, что подтверждается и явным «Non-scope» ТЗ. + +## Как проверялось + +| Гейт | Команда | Результат | +|---|---|---| +| typecheck | `npx tsc --noEmit` | зелёный | +| unit | `npm test` | 1956 tests, 1955 pass, 1 skip, 0 fail | +| build + бандл | `npm run build && cmp dist/houseplan-card.js custom_components/houseplan/frontend/houseplan-card.js` + `diff -rq` на `houseplan-assets` | совпадают байт-в-байт | +| docs fingerprint | `node scripts/check-docs.mjs` (обязателен — diff трогает `src/**`) | «Documentation checks passed (7 files, 12 external links)» | +| any-гейт | не запускал отдельно — новые файлы не содержат `any` (проверено чтением), `npm test`/`typecheck` зелёные | проверено чтением | +| smoke-select | `node scripts/smoke-select.mjs --base origin/dev --head HEAD` | 98 прямых совпадений + 36 слабых из 221 (см. `smoke-select.txt`) | +| smoke (почти полный набор) | все 134 файла из прямых+слабых совпадений, `node demo/smoke_.mjs` по одному | 133 OK, 1 нестабильный (см. ниже, не регрессия) | +| golden | `npm run golden:verify` | 153/153 сценариев `passed`, 0 diff — доказывает AC9 (pixel-equivalence) | +| performance (структурные assert'ы) | `npm run benchmark:large-house-interaction -- --samples=7 --warmups=1` | завершился без throw — структурный контракт (0 full render на hover/pan/editor move, 1 terminal, 0 diagnostics scans) подтверждён на этом SHA | +| performance (абсолютные потолки) | `npm run benchmark:compare -- --absolute-only --budgets=demo/performance/budgets-large-house-interaction.json --candidate=` | **6 проверок красные** — см. находку H1 | +| backend | не запускал — диф не трогает `custom_components/**/*.py` (проверено чтением: `git diff --name-only` не содержит `.py`) | проверено чтением | +| invariants | не запускал — диф не трогает геометрию/`layout`/`marker.space`/`open_spans` (проверено чтением) | проверено чтением | + +Не прогонял полный `demo/smoke_*.mjs` (все ~221) и полный `performance.yml` (`Full Performance`, +парный base/candidate на двух чекаутах) — первое избыточно (134 из 221 уже покрывают все «прямые» и +«слабые» совпадения smoke-select, второе требует парного окружения, которое не гейт код-ревью +(PROCESS.md §8: «полные наборы — предрелizный гейт»). Раздельный прогон `--absolute-only` (см. выше) +компенсирует это для абсолютных потолков нового профиля — и именно он нашёл H1. + +## Находки + +### H1 (High, блокирует) — новый performance-профиль `large-house-interaction-v1` не проходит собственный бюджет + +**AC10** требует: «новый interaction profile проходит все structural, absolute и base-relative checks +§11». Структурные проверки (0 full render на hover/pan/camera/editor move, ровно 1 terminal, 0 +diagnostics scans, стабильный heavy DOM, ошибка совмещения SVG/HTML ≤1px) — подтверждены, профиль не +бросает исключение. Но абсолютные потолки, которые сам же диф вводит в +`demo/performance/budgets-large-house-interaction.json`, не выполняются при фактическом прогоне. + +**Воспроизведение** (детерминировано, не связано со скоростью машины — см. ниже): + +``` +npm run benchmark:large-house-interaction -- --samples=7 --warmups=1 --output=/tmp/interaction-review7.json +npm run benchmark:compare -- --absolute-only \ + --budgets=demo/performance/budgets-large-house-interaction.json \ + --candidate=/tmp/interaction-review7.json --output=/tmp/interaction-compare7.json +``` + +Результат — 6 красных строк: + +``` +❌ timing.interactionSeriesMs.median | 3501.5 | 3000 +❌ timing.editorSeriesMs.median | 1841.9 | 750 +❌ longTask.editorSeries.maxSingleMs | 878 | 150 +❌ longTask.editorSeries.countP95 | 4 | 3 +❌ longTask.editorSeries.totalP95Ms | 1633 | 300 +❌ cache.entries.cleanFloor | 140 | 100 +``` + +`cache.entries.cleanFloor` — счётчик записей `Map`, не таймінг, машинно-независим. Проверено на **двух** +независимых прогонах (`--samples=3` и `--samples=7`), значение **140 во всех 10 сэмплах без единого +отклонения**. Для контроля прогнан немодифицированный `large-house-v1` (существует и на `dev`, диф его +не трогает): + +``` +npm run benchmark:large-house -- --samples=3 --warmups=1 --output=/tmp/base-profile-review.json +# cleanFloor: 100, 100, 100 — ровно документированный потолок (demo/performance/README.md: +# «the reviewed fixture warms exactly 100 deterministic room/physical-body entries») +``` + +Значит переполнение специфично именно для новой `editor series` части `large-house-interaction-v1` +(последовательность `_setMode('plan')` → `_tool='resize'` → cancel → `_setMode('decor')` → +`_setMode('view')`), которую этот же диф добавил в `demo/benchmark_large_house.mjs`. Разница ровно ++40 записей (два «лишних» полных прохода флор-кэша сверх документированного одного) коррелирует с тем, +что тайминговые метрики именно `editorSeriesMs`/`longTask.editorSeries.*` превышены в 2,4× и более — +похоже на одну и ту же причину (лишний(е) пересчёт(ы) `_cleanFloor`/`floorMinusBodies` при переключении +режима редактора), а не на независимый шум. + +Попытка воспроизвести на упрощённой ручной фикстуре (`page.evaluate` с прямым вызовом `_setMode` без +полного цикла реального pointer-move, который использует бенчмарк) рост `_cleanFloorCache.size` не +показала — то есть причина не в самом факте смены режима, а где-то в фактической +resize/decor-транзакции бенчмарка (drag-preview → cancel) или в стечении с реальными pointer-событиями. +Дальнейшая локализация — задача автора; здесь важен сам факт: **AC10 не выполнен по написанному самим +автором для этой задачи бюджету**, а не только гипотеза о причине. + +Тайминговые превышения (`interactionSeriesMs`, `editorSeriesMs`, `longTask.editorSeries.*`) в отличие +от `cleanFloor` теоретически могут быть частично усилены более медленной/разделяемой машиной ревьюера +против выделенной машины владельца (`demo/performance/README.md`: «Local Windows checkout is the +day-to-day environment... local report is diagnostic only»), но margin (1842 мс против потолка 750 мс, +878 мс одна long task против потолка 150 мс) для чистого шума великоват, и коррелирует с детерминированным +`cleanFloor`-превышением. Автору стоит перепроверить оба числа на каноническом Linux CI парном прогоне +(`performance.yml`), но независимо от результата **cache-count уже доказан красным и машинно не +объясним** — это не «непрогнанный дорогой гейт», это прогнанный и красный. + +**Почему High, а не Medium.** Задача, ценность 10/10 и P1, явно посвящена именно тому, чтобы новый +перф-контракт «краснел сам по себе, а не только на фоне предыдущего коммита» (issue, раздел «Почему это +не поймал перф-гейт»). Проверка, которую AC10 называет своим доказательством, при фактическом запуске +красная. Это не вопрос «недостаточно полно проверили», а обнаруженный красный автотест по собственному, +введённому этим же диффом контракту — ровно то, что код-ревью обязано поймать (PROCESS.md §2.7: +«ревьюер отвечает за AC… либо доказан автотестом… либо разобран по коду»). Фикс — в скоупе (тот же +`demo/benchmark_large_house.mjs`/бюджет), отдельный issue не заводится. + +### M1 (Medium, в скоупе) — обещанный в самом ТЗ «targeted production-bundle smoke» не создан + +ТЗ §13.2 и §14 (план реализации, п. 5) явно называют отдельный артефакт: «Targeted `demo/smoke_*.mjs` — +production-bundle render-count и interaction matrix», инструментирующий full-render count, lightweight +paints, diagnostics scans, heavy node identity, writes на PRODUCTION-бандле (не на perf-фикстуре) — и +именно это заявлено как доказательство AC5/AC6/AC7/AC8 (`smoke` в их графе доказательства). + +`git diff origin/dev...HEAD --diff-filter=A` показывает: новых файлов `demo/smoke_*.mjs` нет вовсе. +Изменения в существующих смоках (`smoke_decor.mjs`, `smoke_furniture.mjs`, `smoke_decor_text.mjs`, +`smoke_resize_pointer_real_plan.mjs`, `smoke_pan_any_zoom.mjs`, `smoke_room_fit.mjs`, +`smoke_infinite_canvas.mjs`, `smoke_opening_measure.mjs`, `smoke_partition_openings.mjs`, +`smoke_drag_bounds.mjs`, `smoke_room_tooltip_toggle.mjs`, `smoke_ux_fixes.mjs`) — точечная подгонка под +новую разметку `data-hp-live-*`/тайминг RAF, ни один не считает full-render/diagnostics-scan счётчики. + +Единственное место, где эти структурные гарантии вообще проверяются — `demo/benchmark_large_house.mjs +--profile=large-house-interaction-v1`, которое подключено только к job'е `performance.yml` → «Full +Performance», запускаемой (см. `demo/performance/README.md`, «CI contracts») **на промоушен в `main`, +раз в неделю и по ручному диспатчу** — не на каждый push в `dev`. Блокирующий `performance_smoke` в +`validate.yml` меряет только `large-house-glow-overlay-v1`, этого профиля не касается. При принятой в +проекте модели прямых коммитов в `dev` без PR (AGENTS.md) это означает: следующий обычный коммит, +который случайно вернёт diagnostics-scan на каждый рендер или полный render на hover, **не будет +пойман никаким гейтом до следующего еженедельного/promotion-прогона** — то есть тем самым классом +слепого пятна, ради которого заведён #451 (issue, раздел «Почему это не поймал перф-гейт»: «гейт +исправно подтверждал, что "не стало хуже", пока пользователь смотрел на 451-миллисекундные фризы»). + +Фикс в скоупе: добавить обещанный `demo/smoke_*.mjs` (инструментирование `_renderBody`/ +`_bindingStatus` аналогично тому, что уже сделано внутри `benchmark_large_house.mjs`, но на +production-бандле через `demo/serve.mjs`, в постоянно запускаемом наборе). + +### M2 (Medium, в скоупе) — нет мутанта в `scripts/mutation-gate.mjs` для новых защитных механизмов + +PROCESS.md §2.7: «Мутант в `scripts/mutation-gate.mjs` обязателен, когда защита живёт в продуктовом +коде и проверяется дорогим гейтом (смок, бэкенд, golden): там ревьюер не воспроизведёт отрицательный +прогон второй раз». `git diff origin/dev...HEAD -- scripts/mutation-gate.mjs` — пусто, ни одной новой +записи. + +Часть новых защит — чистые юниты, и для них я сам прогнал мутацию и увидел красный тест (см. «Таблица +доказательств» ниже: `classifyHassRenderChange`, `RenderLifecycle.diagnostics`) — этого достаточно, +отдельный мутант не нужен. Но интеграционная проводка в `houseplan-card.ts` (переопределённый +`requestUpdate`, `_flushHa`/terminal reconciliation, RAF-coalesced camera/hover/editor paints) — +юнитами не покрыта и проверяется только дорогим Playwright-бенчмарком (тем же, что в M1/H1). Без +постоянного мутанта в следующем раунде (после того, как H1 будет исправлен и профиль перестанет +краснеть) ревьюер снова не сможет дёшево доказать «тест умеет падать» — придётся заново гонять +браузерный бенчмарк вручную, как это делал я. + +### Low — известная нестабильность `smoke_smooth_zoom.mjs` под нагрузкой (не регрессия, к сведению) + +При параллельном прогоне 134 смоков (4 конкурентных пакета) `demo/smoke_smooth_zoom.mjs` дал один +красный результат (`wheelRetargetsRunningTween`/`wheelHasIntermediateFrames`/`wheelStreamPersistsOnce` += false). Проверил отдельно: последовательно (без конкуренции за CPU) — 11/11 прогонов зелёные, как на +этой ветке, так и на чистом `origin/dev`. Затем воспроизвёл красный результат **на обеих** ветках +одинаково при явной конкуренции (4 параллельных `smoke_smooth_zoom.mjs` + 3 тяжёлых смока рядом: 2-3 из +4 падали и на `dev`, и на PR). Вывод: это существующая до #451 чувствительность теста к CPU-контеншну +(reliance на реальные `requestAnimationFrame` тайминги), не регрессия этой задачи. Не блокирует, в +скоуп #451 не входит, отдельный issue не заводится (не находка, а объяснённая флакиность). + +## Что проверено и корректно + +- **AC1** (HA intake ≠ visual invalidation): `requestUpdate('hass', …)` в `houseplan-card.ts` + делегирует решение `LiveRuntime.hass()` → `classifyHassRenderChange()`; при `'none'`/deferred-`'state'` + intake (`intakeHass`) всё равно выполняется синхронно через `RenderLifecycle.observe`. Доказано + юнитом `test/houseplan-render-lifecycle.test.mjs` («HA intake runs once even when an unrelated visual + update is skipped») + мутацией (см. ниже). +- **AC2** (dependency classifier): `src/render-invalidation.ts`. Структурные top-level поля (`entities`, + `devices`, `themes`, `locale.*`, неизвестный новый ключ) — fail-open на `'structural'`; только `states` + сравнивается по dependency-scoped `entityIds`. Доказано `test/render-invalidation.test.mjs` (4/4) + + мутацией: убрал ранний `return 'none'`→заменил на безусловный `return 'state'` в скомпилированном + `test-build/render-invalidation.js` (не в `src/**`, генерируемый файл) — тест + «an unrelated HA state row does not invalidate the plan frame» покраснел (`expected 'none', actual + 'state'`), затем восстановил файл, тест снова зелёный. +- **AC3** (intake переживает пропущенный рендер): dependency projection в `_captureRenderDeviceSnapshot` + (houseplan-card.ts:4563+) собирает entity/device/area из bindings, room temp/hum source, opening + entity refs, live-text, `sun.sun`, vacuum source — доказано чтением и подтверждено проходом смоков + `smoke_linked_virtual_light`, `smoke_static_icon`, `smoke_cover_no_plate`, + `smoke_cover_plate_precedence`, `smoke_entity_parent_dedup`, `smoke_yellow_principle` (все зелёные). +- **AC4** (diagnostics cache): `RenderLifecycle.diagnostics()`/`invalidate()` в + `houseplan-render-lifecycle.ts`. Единственный вызов `houseplanDiagnostics()`-подобной логики теперь и + в `_renderBody` (через `this._renderLife.diagnostics(...)`), и в публичном `houseplanDiagnostics()` — + один источник (см. «Одно число — один источник» ниже). Доказано юнитом («diagnostics scan is cached + and invalidated by tracked state presence») + мутацией: заменил guard `if (this.diagnosticsCache)` на + `if (false && this.diagnosticsCache)` в `test-build/houseplan-render-lifecycle.js` — тест покраснел + (`2 !== 1`, ожидался один скан вместо двух), восстановил, снова зелёный. +- **AC5/AC6/AC7** (0 full render на hover/pan/pinch/camera/editor move, 1 terminal, deferred-relevant-tick + last-wins): структурные assert'ы внутри `demo/benchmark_large_house.mjs` (throw при нарушении) + подтверждены самостоятельным прогоном на этом SHA — все условия выполнены (`irrelevantFullRenders:0`, + `hoverFullRenders:0`, `panMoveFullRenders:[0,0,0,0]`/`terminalFullRenders:[1,1,1,1]`, + `cameraMoveFullRenders:0`/`cameraTerminalFullRenders:1`, `editorMoveFullRenders:[0,0,0]`, + `relevantDuringGestureFullRenders:0` при `relevantDuringGestureIntakes:3`, `diagnosticsScans:0`, + `heavyNodeStable:true`, `overlayErrorPx:0.01`). Тайминги того же прогона см. H1. +- **AC8** (coalescing сохраняет конечную координату/commit/cancel): `pointer-move-queue.ts` + (`queueMicrotask`, гарантированно опустошается до следующего браузерного события) + подключение во + всех точках (`_pointerMove`/device, `_physicalMove`, `_rszMove`, `_bdMove`, `_opPointerMove`, `_dtMove`, + `_decorMoveUpdate`, `_markupMove`) с явным `flush` на terminal (`up`) и `cancel` на отмене. Доказано + юнитом `test/live-editor.test.mjs` («pointer move queue is event-turn coalesced, last-wins and + flushable») + прогоном затронутых production-bundle смоков (`smoke_furniture`, `smoke_decor`, + `smoke_decor_text`, `smoke_drag_bounds`, `smoke_partition_openings`, `smoke_opening_measure`, + `smoke_resize_pointer_real_plan` — все зелёные, включая ровно те сценарии shift-snap/magnet/rotate, + которые чувствительны к потере промежуточного состояния). +- **AC9** (pixel-equivalence): `npm run golden:verify` — 153/153 `passed`, 0 diff, включая + `large-house-zoom-040/250-dark`, `large-house-warm-remount-dark`, junction-серию, hover-серию, + dark/light/RU/EN варианты. Прямое доказательство отсутствия непреднамеренных визуальных изменений. +- **AC11** (implementation-loop gates): typecheck/test/build зелёные (таблица выше). +- **AC12** (schema/backend/i18n/dependencies не изменены): `git diff --name-only` не содержит + `.py`/`i18n`/`translations`/`manifest.json`/`hacs.json` — проверено чтением. Новые кэши + (`RenderLifecycle.diagnosticsCache`, `LiveRuntime`/`live-viewport`/`live-hover`/`live-editor` state) — + все `WeakMap`, ключ — сам инстанс карточки; `disconnectedCallback` вызывает + `_liveRt?.dispose()` и `_editorRuntime?._disposeLiveEditor()` (cancel RAF, restore hidden/transparent + DOM, `cancelHouseplanPointerMove`) — bounded per card, очищаются при disconnect, независимы между + несколькими карточками на дашборде (WeakMap не даёт общий каталог). +- **AC13** (документация): `demo/performance/README.md` описывает новый профиль, сценарии, структурные + assertions, абсолютные потолки, local diagnostics-команду; `docs/CHANGELOG.md`/`docs/CHANGELOG.ru.md` + обновлены в том же коммите `c0d61ca3` (`User-Visible: yes`), формулировка «внешний вид и результат + действий не меняются» соответствует AC9/non-scope. +- **Одно число — один источник**: `houseplanDiagnostics()` (публичный support-report) и внутренний + render-path теперь читают один и тот же `RenderLifecycle.diagnostics()` — раньше было два независимых + прохода по маркерам с одинаковой логикой (потенциальное расхождение при будущей правке одного из них), + теперь физически один вызов на одну инвалидацию (доказано юнитом «scans === 1» выше). Другой видимый + дважды параметр — zoom badge (`Math.round(zoom*100)%`) и `_zoom`/`_view`: и DOM-бейдж + (`live-viewport.ts:paintLiveViewport`), и canonical-поле читают один и тот же `frameOf(host)` — + `host._zoom`, без второго независимого округления. +- **Трейлеры**: все 10 коммитов несут `Issue: #451`; ровно один `User-Visible: yes` + (`c0d61ca3`, «fix: keep large-plan interactions off the full render path») с обоими changelog в том + же коммите; остальные `User-Visible: no` (документация/фикс-ап/ребилд бандла) — корректно. +- **Touch/pen/kiosk/fixed-floor контракт** (§9 ТЗ): не даёт отдельной ветки поведения — подтверждено + прохождением `smoke_isometric_live_touch`, `smoke_kiosk`, `smoke_kiosk_pan_lock`, `smoke_fixed_floor`, + `smoke_touch_tips`, `smoke_grid_scale_invariance` (все зелёные) плюс golden dark/light/RU/EN сценарии. + +## Чего не проверял + +- Полный `demo/smoke_*.mjs` (все ~221) — прогнал 134 (все «прямые» и «слабые» совпадения + smoke-select.mjs), сознательно не прогонял оставшиеся ~87 без связи с диффом (полный набор — + предрелизный гейт, PROCESS.md §8). +- Парный `performance.yml` («Full Performance», base vs candidate на двух чекаутах, реальный Chromium + CI-раннер владельца) — недоступен в этом окружении; вместо него прогнал `--absolute-only` (см. H1, + нашёл реальную красноту) и структурные assert'ы бенчмарка напрямую. Относительное (base-vs-candidate) + сравнение не проверено — вероятно, тоже покраснеет из-за той же причины, что и абсолютное, но это не + проверено напрямую. +- Backend (`python -m pytest tests_backend`) — не запускал, диф не касается `custom_components/**/*.py` + (проверено чтением diff, не исполнением). +- `npm run invariants` — не запускал, диф не касается геометрии/`layout`/`marker.space`/`open_spans` + (проверено чтением diff и Non-scope раздела ТЗ). +- Точная локализация причины лишних 40 записей `cleanFloor` (H1) — воспроизвёл и доказал факт, не нашёл + точную строку кода; это оставлено автору при исправлении H1. +- Ручная проверка в реальном браузере (вне headless Playwright) и на настоящей Home Assistant — вне + доступного окружения; полагался на golden/smoke/unit/performance гейты. + +## Материал раунда + +- Ветка: `issue/451-render-performance` +- SHA материала: `1850eb18b5e8f0813f9d2efb50694dc4ca1fff26` +- Диапазон: `origin/dev...HEAD` (10 коммитов, `029aba6b`..`1850eb18`) +- ТЗ: `docs/specs/451-render-performance.md`, ревью — `docs/reviews/SPEC-REVIEW-451-r2.md` (зелёное) + +## Итог + +**Вердикт: красный.** Одна High-находка (H1 — заявленный самим диффом перф-контракт `AC10` красный при +фактическом прогоне `compare.mjs --absolute-only` по собственному новому бюджету, доказано +детерминированным счётчиком `cache.entries.cleanFloor: 140 vs 100` плюс коррелирующими превышениями +таймингов `editorSeriesMs`/`longTask.editorSeries.*`) блокирует переход. Плюс две Medium-находки в +скоупе (M1 — не создан обещанный ТЗ `demo/smoke_*.mjs` для render-count на production-бандле, M2 — нет +мутанта в `scripts/mutation-gate.mjs` для интеграционной проводки, проверяемой только дорогим +бенчмарком). Сама архитектура решения (разделение intake/visual invalidation, dependency classifier, +diagnostics cache, RAF-coalesced live-слои камеры/hover/редакторов, pointer-move очереди) сделана +аккуратно и проверяемо: юнит-защиты действительно ловят мутацию, structural assertions бенчмарка +действительно бросают исключение при нарушении контракта, golden/smoke/typecheck/build/docs — все +зелёные. Возврат автору — на исправление конкретно H1 (и желательно M1/M2 в том же цикле, раз без +High они и так были бы Medium-в-скоупе), без расширения скоупа задачи. + +--- + + + +## Материал раунда + +- Ветка: `issue/451-render-performance`, коммит `1850eb18b5e8` — ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет. +- Дерево материала: `30dcff478a0287b209be4c3066972158d6e6b539` + ``` + git log --all --format='%H %T' | grep 30dcff478a02 + ``` +- ТЗ `docs/specs/451-render-performance.md`, блоб `7c323a29110974aae369077214b9e2a74d9387c1` + ``` + git log --all --find-object=7c323a29110974aae369077214b9e2a74d9387c1 -- docs/specs/451-render-performance.md + ```