25 KiB
SPEC-REVIEW-694-r1 — «Исследовать и устранить performance-регрессии v1.78 относительно v1.77»
Issue: #694
Этап: spec (§2.4)
Трек: ask (производительность, метка track:ask)
Заход: r1 (первый; разделы «Унаследовано из r0» и «Закрытие раунда r0» не нужны — §2.10 применяется со второго захода)
Вердикт
Жёлтый. High: 0. Medium в скоупе: 3 (возвращаются автору, отдельный issue не заводится, #202). Medium вне скоупа: 0. Low: 1 (снята решением ревьюера с записью ниже, доработка не обязательна).
Скоуп разбора
Полный разбор: тело issue #694 целиком (описание принятого performance-долга
до раздела ## ТЗ, сам раздел ## ТЗ — «Что остаётся», «Причины и правки»,
«Критерии приёмки», «Не входит», «Принятые предположения») и все пять
комментариев (два от владельца о выпуске v1.78.0 и решении по viewToggleMs,
три от автора — атрибуция регрессий по бетам, прогон после #711/#713, разбор
long tasks, финальная «Оценка»). Дополнительно прочитаны docs/SCOPE.md,
AGENTS.md, docs/process/REVIEWER.md, PROCESS.md §1, §2.3–§2.5, §2.10, §4,
§5, §7.1, §7.2.
Поскольку все три названные в ТЗ правки (П1–П3) — точные утверждения о текущем
коде, а не только план на будущее, разбор включает чтение dev на SHA
материала (84ed38e3, рабочая копия детач на нём, продуктовый код ещё не
менялся — задача только начинается, ветка issue/694-… не заведена):
src/houseplan-card.ts:2542–2567(connectedCallback,_pointerHoverObserver) — подтверждает П1 буквально: для каждой записи мутации локальная функцияinDeviceLayerвызываетelement.matches(...)иelement.querySelector('.devlayer')по всему поддереву цели заново, без дедупликации между записями одной пачки;for (const node of record.addedNodes) this._syncPointerHoverSubtree(node)вызывается безусловно для каждой записи — именно то, что AC1 требует оставить без изменений;src/stairs-view.ts:63–77(renderLayer) — подтверждает П2:this.owner._modelчитается один раз вне цикла (строка 64, дляspaceIds) и второй раз внутри.mapпо лестницам (строка 76, дляtargetTitle) — то есть геттер вызывается1 + количество активных лестницраз за рендер, а не один;src/houseplan-card.ts:3822подтверждает, что_model— геттер с внутренним кэшем по_cfgEpoch + _cfgFingerprint(), так что лишние вызовы — лишняя пересборка отпечатка, как и написано в ТЗ;src/i18n/language-runtime.ts:105–120(languageRenderGate) — подтверждает П3:host.setAttribute('lang', …)вызывается безусловно на каждом проходе сstate !== 'pending', без сравнения с уже установленным значением;test/core-file-budget.test.mjs:52—'src/houseplan-card.ts': 12896, фактwc -l src/houseplan-card.ts= 12895: запас ровно одна строка, ограничение «П1 компенсируется в той же функции, потолок не поднимается» в «Принятых предположениях» — не декларация, а точное описание реального состояния храповика;src/stairs-view.tsиsrc/i18n/language-runtime.tsвCAPSне числятся, бюджетного ограничения для П2/П3 нет, и ТЗ корректно не упоминает его для них;demo/smoke_stairs.mjs,demo/smoke_device_hit_capsules.mjs,demo/smoke_french_locale.mjs— все три существуют, AC5 не ссылается на несуществующие смоки;test/device-hit-owner.test.mjs,test/device-hit-owner-contract.test.mjs— существуют,_invalidateDeviceHitGeometry(строка 5561) — та же функция, которую вызывает наблюдатель П1 приdeviceGeometryChanged, то есть регресс по AC1 действительно ловится этими тестами, а не случайным соседом;demo/performance/evaluate.mjs:34,149–157— метрикаlongTask.countP95из AC4 реальна и вычисляется именно так, как названо в разборе комментариев;demo/performance/budgets-{isometric,isometric-stage3-dense,large-house-*,interaction}*.json— все профили из AC4 (isometric,isometric-stage3,interaction,plan-snap,large-house) существуют как отдельные бюджеты;git log --oneline -1=84ed38e3 test(perf): the Stage 4 dense contract requires no nudged overlay (#719)— подтверждает пункт ТЗ «уже сделано: … #719 (контракт бенчмарка плотной сцены)»: это не будущее намерение, а факт на материале ревью.
Проверка §7.1 — комплектность
Присутствуют: проблема («Что остаётся» — точные цифры превышений с
источником-прогоном) · контракт поведения (П1–П3 явно: «поведение не
меняется», подтверждено чтением кода выше) · скоуп и не-скоуп («Не входит»
— архитектура DOM лестниц, прочие кандидаты профилирования, методика
бенчмарка — с явной мотивировкой каждого пункта) · критерии приёмки
AC1–AC5 с доказательством (unit/perf/smoke + golden у каждого) ·
затронутые файлы (src/houseplan-card.ts, src/stairs-view.ts,
src/i18n/language-runtime.ts названы поимённо в П1–П3) · release-артефакты
для User-Visible: no (явно: «строка CHANGELOG не нужна»; AC4 требует приложить
in-issue цифры по окнам, если часть бюджетов останется красной).
Отсутствуют или не выделены явным разделом (находки ниже):
- сценарий и что человек увидит до и после — оба обязательных
продуктовых раздела §7.1 отсутствуют в самом
## ТЗ; - риски — не собраны отдельным разделом;
- откат — не назван вообще, ни как раздел, ни как строка.
UX, модель данных/миграция, i18n и touch-влияние явно не названы «нет», но по содержанию ТЗ однозначно читаются как неприменимые (три внутренние оптимизации без новых экранов, полей конфигурации, i18n-ключей или touch-путей) — это Low, не Medium (см. находки).
Проверка AC1–AC5 на однозначность и доказуемость
- AC1 — однозначен: наблюдаемый oracle назван явно (счётчик запросов
поддерева на инструментированном узле), граница фикса точна (только дедуп
проверки
inDeviceLayer,_syncPointerHoverSubtreeостаётся безусловным), подтверждено чтением реального кода наблюдателя (см. «Скоуп разбора»). - AC2 — однозначен: spy на геттере
_model, ожидание «ровно один раз за любое число лестниц», инвариант «подписи целевого этажа не меняются» — измеримо и falsifiable. - AC3 — однозначен: условие «не вызывает
setAttribute, если значение уже равно нужному» плюс два позитивных случая (смена языка, fallback наen) — полная таблица истинности для этой функции. - AC4 — доказательство названо точно (Full Performance,
v1.77.0, пять профилей, включаяlongTask.countP95), все имена профилей и метрика существуют вdemo/performance/**(см. «Скоуп разбора»). Ambiguity — см. находку Medium ниже: оговорка про лестницы не называет способ подтвердить саму причину. - AC5 — однозначен, все три названных smoke-файла существуют, требование «DOM и пиксели не меняются, golden без новых кадров» — стандартная и проверяемая формулировка для behavior-preserving рефакторинга.
Ни один AC не описывает решение, для которого в issue/каноне нет опоры: П1–П3 и AC1–AC3 линейно соответствуют друг другу, AC4/AC5 соответствуют разделу «Критерии готовности» исходного описания issue.
Находки
Medium (в скоупе, возврат автору)
-
Отсутствуют разделы «Сценарий» и «Что человек увидит до и после», обязательные по §7.1. ТЗ описывает исключительно техническую сторону регрессии (метрики, коммиты, причины) и ни разу не формулирует, какая персона
docs/SCOPE.mdи на какой поверхности сталкивается с проблемой, которую закрывает задача. Сам материал это знает — комментарий от 30.09 прямо называет: «Это частое действие (киоск, вкладки этажей) … главная оставшаяся цель задачи» — то есть медленное тёплое переключение этажей на вкладках, которое видят household members на кухонном планшете и владелец на десктопе (J1docs/SCOPE.md). Но эта формулировка осталась в комментарии-анализе и не перенесена в## ТЗкак отдельный раздел. §7.1 прямо предупреждает: «ТЗ, которое не может ответить на эти два вопроса, описывает работу, а не изменение продукта» — сейчас формально это так. Почему это не техническая деталь: без явного сценария код-ревьюер на следующем этапе не может проверить, что производительность улучшилась именно там, где это видно пользователю (а не только в синтетическом бенчмарке) — а именно этот вопрос уже поднимался в комментариях автора («это частое действие … главная оставшаяся цель»). Правка: двумя предложениями перенести в## ТЗто, что уже есть в комментарии 30.09 — кто и когда встречает регрессию (кухонный планшет/киоск, тёплое переключение вкладок этажей, админ при проверке плотных сцен) и что человек увидит после фикса («переключение этажей снова быстрое, экран не меняется ни на пиксель»). -
Отсутствуют разделы «Риски» и «Откат», оба обязательны по §7.1, «Откат» также обязателен по DoR (§2.5: «откат: как выключить или вернуть назад»). Ни в
## ТЗ, ни в «Принятых предположениях» нет ни одной строки о том, как выключить или отменить правку, если П1–П3 после мержа проявят регресс, который не поймали unit/smoke/golden (например, дедуп в П1 пропустит реальное изменение геометрии устройства на редкой комбинации мутаций). Риск-материал у задачи есть и весьма содержателен — «запас в core-file-budget ровно одна строка» (Принятые предположения), «бенчмаркisometric-stage3был сломан до #719» (Что остаётся / история комментариев), «часть бюджетов может остаться красной по причине лестниц, не по причине этой задачи» (AC4) — но он рассыпан по разным разделам и не назван риском нигде явно. Как стоит сейчас, ТЗ не может пройти чеклист DoR §2.5 буквально: пункт «откат» не закрыт ни фактом, ни явным «нет» (а «нет» здесь и не подошло бы — любая продуктовая функция может быть отменена обычнымgit revert, но это тоже нужно написать хотя бы одной строкой, поскольку правка №1 лежит в файле на пределе бюджета строк и в самом чувствительном к производительности пути карточки). Правка: добавить раздел «Риски» (собрать три пункта выше) и раздел «Откат» (для внутренних поведенчески-нейтральных правок это может быть одна строка: обычный revert коммита, флагов/миграций нет — но она должна быть написана, а не подразумеваться). -
AC4 содержит исключение без названного способа его подтвердить. Формулировка: «Если профиль остаётся красным только из-за этажа с лестницами (цена самой фичи, причина 4 разбора), в issue — цифры по окнам и предложение владельцу». Эскалация владельцу здесь корректна по духу §7.1 (продуктовый компромисс — решение владельца, не автора), но сам ТЗ не называет, как отличить «редness только из-за лестниц» от «правки недостаточны». В комментариях уже есть готовый метод («Если убрать лестницы из фикстуры, dev в 2D совпадает с базой» — «Что остаётся»), но он не зафиксирован как обязательный шаг проверки в самом AC4. Без этого код-ревьюер следующего этапа не сможет отличить обоснованную эскалацию от недоделанного фикса, выданного за «фичевую цену» — а это ровно тот вопрос, который §7.1 требует не оставлять на усмотрение читателя. Правка: в AC4 явно назвать метод атрибуции (например: «прогон с той же фикстурой без этажа лестниц воспроизводит бюджет v1.77.0 → редность отнесена на лестницы»), а не оставлять решение «только из-за лестниц» недоказуемым утверждением.
Low (снята решением ревьюера, доработка не обязательна)
- UX, модель данных/миграция, i18n и touch-влияние не названы явно «нет»
отдельными строками (DoR §2.5). По содержанию всех трёх правок (внутренний
дедуп MutationObserver, кэш чтения геттера, условный
setAttribute) это однозначно неприменимые разделы — ни новых экранов, ни новых полей конфигурации, ни i18n-ключей, ни touch-специфичного кода П1–П3 не затрагивают, а AC1/AC3 явно фиксируют «поведение не меняется». Не поднимаю до Medium: содержание уже отвечает на все четыре вопроса имплицитно и однозначно, добавление четырёх строк «нет» — гигиена оформления, а не пробел в контракте. Автору стоит добавить эти строки при следующей правке ТЗ заодно с находками Medium выше, отдельного цикла ради этого не нужно.
Что проверено и корректно
- Трек
askобоснован верно: это P1-перф-долг с продуктовыми развилками (эскалация владельцу по AC4), не подходит подshow/ship(§5). - Все технические утверждения о текущем коде (П1–П3) сверены построчно с
dev@84ed38e3и совпадают дословно, включая точный номер строки геттера_model, механику_cfgFingerprintи безусловностьsetAttribute— ни одна причина не оказалась устаревшей или неточной. - Ограничение
core-file-budget(запас 1 строка) численно подтверждено (test/core-file-budget.test.mjs:52= 12896, факт 12895). - Все имена тестов, смоков и performance-профилей, на которые ссылаются AC1–AC5, существуют в дереве материала — ни один AC не указывает на несуществующий артефакт.
- «Уже сделано» (#711, #713, #719, #720) соответствует состоянию
dev: HEAD-коммит материала — именно #719 («no nudged overlay»), заявленный как выполненный. - Продуктовых вопросов владельцу в тексте ТЗ не осталось: предыдущий открытый вопрос из комментария 30.09 («раскладывать иконки по размеру, не зависящему от состояния?») закрыт по факту — автор нашёл поведенчески-нейтральное решение (отсечение по лексикографической границе, #711/#713), и в текущее ТЗ этот вопрос не попал как нерешённый; повторно поднимать его не нужно.
- AC4 корректно направляет решение о недостижимых бюджетах владельцу, а не принимает его самостоятельно (единственный найденный пробел — не в этом принципе, а в способе доказать применимость исключения, находка Medium 3).
- Раздел «Не входит» мотивирует каждый пункт (архитектура DOM лестниц, прочие кандидаты профилирования, методика бенчмарка) и не пытается тихо расширить или сузить скоуп относительно исходного описания issue.
Чего не проверял
- Не запускал
npx tsc --noEmit,npm test,npm run build, гейты производительности или golden — на этапе ТЗ продуктового диффа ещё нет (веткаissue/694-…не создана, детач-копия наdev@84ed38e3без изменений), гонять гейты не над чем; зелёный Validate на этом SHA (материал дешёвых гейтов) сюда не относится — это гейт code-review, не spec-review. - Не проверял историческую точность процентов и медиан в таблицах комментариев (например, точные +164%/+131% по бетам) — это принятый владельцем анализ предыдущего цикла (#692 поправлен и подтверждён самим автором), не предмет ревью текущего ТЗ.
- Не выполнял предложенный в комментариях локальный прототип (лексикографическое отсечение, кэш «иконка у стены/в комнате») — эти правки уже слиты отдельными задачами (#711, #713) до открытия текущего issue и не входят в скоуп П1–П3.
- Не оценивал техническую реализуемость самого механизма «инструментированный счётчик запросов поддерева» из AC1 — это деталь исполнения, оставленная автору («принято предположительно, поменять свободно»), а не часть контракта, который проверяет ревью ТЗ.
Материал раунда
Issue #694, тело на момент вынесения вердикта (описание принятого
performance-долга плюс раздел ## ТЗ, включая пять комментариев с атрибуцией
регрессий, промежуточными замерами и финальной оценкой). Рабочая копия — detached
HEAD на dev@84ed38e3d5d511687de48e8ca4dae714f82efe2f без продуктовых
изменений (ветка issue/694-… не заведена); код читался с этого дерева
исключительно как контекст для проверки фактических утверждений ТЗ (П1–П3,
core-file-budget, имена тестов/профилей), а не как материал стадии code-review.
sha256 тела issue на момент проверки: efc82985614a54c4fc89e485322018a7f5525fbc0bc3a0304f231fe8453c9c4b.
Материал раунда
- Ветка:
dev, коммит84ed38e3d5d5— ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет. - Дерево материала:
51385b4bd49434fbfe510a14c8824a1c48b01d0bgit log --all --format='%H %T' | grep 51385b4bd494 - Тело issue:
3e716989f90524ce41441b1b6ac157229af3aec237befdc309a8e08b66ad6816 - Вердикт конвейера:
yellow· High 0