mirror of
https://github.com/Matysh/houseplan-card
synced 2026-10-01 04:09:17 +00:00
docs: spec #330 revision 2 — budgets from the profile, П4 bucket, no re-migration for current-version documents
r1-H1 was right twice: the rev cache never touched the candidate's migration, and П4 is architecturally quadratic. Profiled instead of guessing: the money is not in deepcopy (3 ms) but in _atomize (663k distance calls), and it runs even for a document that already carries the current catalogue — 815 ms python / 69 ms TS for a no-op migration. Two new cuts follow: §4.5 bucket index for П4 (prototype: 372→44 ms, identical verdicts) and §4.6 current- version documents are used as-is (an explicit revision of the "both sides through one migration" wording, guarded by a new parity case: v9 input gives the same verdict with and without migration). r1-H2: AC4 now rests on the new benchmark that actually exercises the junction code; benchmark_safe_resize is named as a non-proof. r1-M1: §9 adds the mandatory i18n/touch/risks/release sections. Budgets in §5 are recomputed from measured post-fix prototypes with a 2-3x allowance, including an honest row for the one-off cold legacy case. Issue: #330 User-Visible: no
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
# Issue #330 — производительность ограничений стыков (#329)
|
||||
|
||||
Статус: ревизия 1. Родитель: #329 (лимиты выпущены, вердикты корректны —
|
||||
Статус: ревизия 2 (r1: H1 — бюджеты пересчитаны от профиля, добавлены срезы
|
||||
§4.5 bucket-П4 и §4.6 «документ текущей версии без повторной миграции»;
|
||||
H2 — AC4 опирается на новый бенч; M1 — §9). Родитель: #329 (лимиты выпущены, вердикты корректны —
|
||||
подтверждено паритет-тестом и код-ревью r1–r3). Эта задача меняет ТОЛЬКО
|
||||
стоимость вычисления вердиктов; ни один вердикт не меняется ни на бит.
|
||||
Смежный #331 (пограничная точность) сознательно вне скоупа.
|
||||
@@ -29,17 +31,20 @@ Assistant почти на 3 секунды. `MAX_CONFIG_BYTES` = 2 МБ допу
|
||||
Результат для пользователя: сохранение большого плана не подвешивает HA;
|
||||
ресайз на большом плане держит существующий кадровый бюджет.
|
||||
|
||||
## 2. Корни (по убыванию вклада)
|
||||
## 2. Корни (по убыванию вклада; профиль cProfile/бенч, 576 атомов)
|
||||
|
||||
1. **Две полные миграции на запись.** `validate_junction_limits` гонит и
|
||||
`previous`, и `candidate` через `commit_wall_segment_model` (deepcopy +
|
||||
каноникализация) — ~1.5 с из 2.8 на 12×12. `previous` между записями не
|
||||
меняется: его миграция и подсчёт нарушений выбрасываются и повторяются.
|
||||
1. **Миграция обоих документов на каждую запись.** По профилю деньги НЕ в
|
||||
deepcopy (3 мс): 1.7 с одной миграции — это `_migrate_space → _atomize`
|
||||
(663 тыс. вызовов `_distance_to_segment`, квадратичная атомизация), и она
|
||||
выполняется **даже для документа, уже несущего актуальный каталог**:
|
||||
migrate(v9) = 815 мс python / 69 мс TS. `previous` к тому же неизменен
|
||||
между записями — его миграция и подсчёт выбрасываются и повторяются.
|
||||
2. **П3 квадратичен в обеих реализациях.** `collinearRunLengthUnits` /
|
||||
`collinear_run_length_units` строит индекс узлов заново для КАЖДОГО
|
||||
сегмента → O(n²) с тяжёлой константой. Зеркала честно скопировали дефект
|
||||
друг друга.
|
||||
3. **Фронтовый baseline без кэша.** `_junctionLimitsIntroduced` строит
|
||||
сегмента → O(n²): 285 мс py / 290 мс TS.
|
||||
3. **П4 квадратичен архитектурно** (все пары узлов + узел×сегмент):
|
||||
372 мс py / 104 мс TS. Линеаризация П3 его не касается (r1-H1).
|
||||
4. **Фронтовый baseline без кэша.** `_junctionLimitsIntroduced` строит
|
||||
baseline-нарушения из previousConfig на каждый вызов; в жесте ресайза —
|
||||
на каждый move.
|
||||
|
||||
@@ -51,10 +56,10 @@ Assistant почти на 3 секунды. `MAX_CONFIG_BYTES` = 2 МБ допу
|
||||
- Спека #329 (наследование по правилу, обе стороны после одной миграции,
|
||||
скоуп применения §3) не меняется.
|
||||
- Вне скоупа: итеративный walk, точность ключей узлов, 0°-дубль (#331);
|
||||
optimize/import-лазейка (#333); пропуск миграции для уже-v9 документов —
|
||||
рассмотрен и отклонён: он меняет сравниваемое представление (кандидат от
|
||||
клиента не каноникализован) и рискует паритетом ради экономии, которую
|
||||
даёт rev-кэш.
|
||||
optimize/import-лазейка (#333); ускорение самой атомизации
|
||||
(`_atomize`/`atomize` квадратичны — общесистемная правка ядра модели с
|
||||
требованием байт-паритета зеркал; отдельная задача, если §4.6 окажется
|
||||
недостаточным).
|
||||
|
||||
## 4. Решение
|
||||
|
||||
@@ -98,51 +103,107 @@ validate, инвалидируется несовпадением rev. Повт
|
||||
консервативна: любой реальный write сбрасывает кэш. Жест ресайза считает
|
||||
baseline один раз вместо «на каждый move».
|
||||
|
||||
### 4.5 Обе реализации: bucket-индекс для П4 (r1-H1)
|
||||
|
||||
П4 переводится с «все пары узлов + каждый узел × каждый сегмент» на
|
||||
пространственную сетку с шагом `minDistance`: узлы и bbox сегментов
|
||||
(расширенные на порог) раскладываются по ячейкам, кандидаты берутся из
|
||||
9-окрестности. Прототип на python: 372 → 44 мс на 576 атомах, вердикты
|
||||
идентичны. Реализуется в обоих зеркалах симметрично; вердикты байт-идентичны
|
||||
(паритет-тест).
|
||||
|
||||
### 4.6 Документ текущей версии не мигрируется повторно (r1-H1)
|
||||
|
||||
Профиль показал: `commit_wall_segment_model` тратит 0.8 с python / 70 мс TS
|
||||
даже на документе, который УЖЕ несёт актуальный каталог (`model_version ==
|
||||
WALL_SEGMENT_MODEL_VERSION`) — атомизация гоняется заново ради нулевой
|
||||
дельты. В путях лимитов такой документ используется как есть, без повторной
|
||||
миграции; легаси-документ мигрируется прежним путём.
|
||||
|
||||
Это ОСОЗНАННАЯ ревизия формулировки «обе стороны после одной миграции»
|
||||
(#329, CONFIG-COMPATIBILITY): её цель — не сравнивать документ без каталога
|
||||
с мигрированным; у v9-документа каталог уже есть, цель достигнута без
|
||||
работы. Риск «клиент прислал неканонический v9» закрывается требованием
|
||||
эквивалентности: паритет-тест дополняется кейсом «v9-вход даёт тот же
|
||||
вердикт с миграцией и без» на фикстурах границ, а барьер модели ниже по
|
||||
конвейеру по-прежнему валидирует документ целиком.
|
||||
|
||||
## 5. Перф-контракт (новый, в CI)
|
||||
|
||||
`demo/benchmark_junction_limits.mjs` по образцу `benchmark_safe_resize.mjs`:
|
||||
сетка 12×12 (576 атомов), бюджеты в файле бенча:
|
||||
`demo/benchmark_junction_limits.mjs` по образцу остальных бенчей: сетка
|
||||
12×12 (576 атомов, легаси и v9-варианты), бюджеты в файле бенча. Бенч меряет
|
||||
ИМЕННО код лимитов (r1-H2): TS-проверки П1–П5 напрямую и полный
|
||||
`_junctionLimitViolations` на смонтированной карточке, плюс python-часть
|
||||
через дочерний процесс.
|
||||
|
||||
- фронт: `checkSegmentLengths` ≤ 25 мс, полный `_junctionLimitViolations`
|
||||
кандидата ≤ 60 мс (p95 из N прогонов);
|
||||
- бэкенд (запускается тем же бенчем через python3): полный
|
||||
`validate_junction_limits` с тёплым rev-кэшем ≤ 150 мс, холодный ≤ 900 мс.
|
||||
Бюджеты — ~2–3× от замеренного после фикса (ловим возврат O(n²), не
|
||||
дрожание раннера); замеры после фикса по прототипам:
|
||||
|
||||
Бюджеты — 2–3× от замеренного после фикса, чтобы бенч ловил регресс класса
|
||||
«вернули O(n²)», а не дрожание раннера. Включается в перф-джобу CI рядом с
|
||||
существующими бенчами.
|
||||
| Метрика | После фикса (ожид.) | Бюджет |
|
||||
|---|---|---|
|
||||
| TS `checkSegmentLengths` (П3, 576) | ~15 мс | ≤ 40 мс |
|
||||
| TS `checkNodeDistances` (П4, 576) | ~15 мс | ≤ 40 мс |
|
||||
| TS полный набор П1–П5 кандидата v9 (без повторной миграции, §4.6) | ~40 мс | ≤ 100 мс |
|
||||
| py `validate_junction_limits`, тёплый (v9 + rev-кэш) | ~100 мс | ≤ 250 мс |
|
||||
| py `validate_junction_limits`, холодный (легаси-обе-стороны) | ~1.7 с | ≤ 3.5 с — только в executor (AC1), одноразовый случай первой записи после обновления |
|
||||
|
||||
## 6. Acceptance criteria
|
||||
|
||||
- **AC1 (event loop).** Запись 576-атомного конфига блокирует event loop
|
||||
≤ 50 мс: backend-тест меряет время синхронной части `ws_config_set`
|
||||
(инструментированный вызов); полная цепочка живёт в executor.
|
||||
- **AC2 (П3 линейный).** 576 атомов: `checkSegmentLengths` ≤ 25 мс (фронт),
|
||||
`check_segment_lengths` ≤ 100 мс (python) — бенч из §5; вердикты на
|
||||
фикстурах паритет-теста не изменились (существующий тест).
|
||||
- **AC3 (rev-кэш).** Две подряд записи с одним previous: вторая не вызывает
|
||||
`commit_wall_segment_model` для previous (тест со счётчиком вызовов через
|
||||
monkeypatch); смена rev инвалидирует кэш; вердикты идентичны пути без
|
||||
кэша на обеих фикстурах (унаследованное нарушение / новое нарушение).
|
||||
- **AC4 (фронт-кэш).** В одном жесте ресайза baseline считается один раз:
|
||||
юнит на `_junctionLimitsIntroduced` со счётчиком, плюс существующий
|
||||
`benchmark_safe_resize` держит прежний pointer-бюджет на large-house.
|
||||
- **AC5 (семантика не тронута).** Все существующие юниты #329, паритет-тест,
|
||||
≤ 50 мс: backend-тест меряет синхронную часть `ws_config_set`
|
||||
(инструментированный вызов); CPU-цепочка живёт в executor. Холодный
|
||||
легаси-случай подчиняется тому же лимиту loop-времени.
|
||||
- **AC2 (П3+П4 линейные).** Бюджеты §5 для `checkSegmentLengths` и
|
||||
`checkNodeDistances` в обоих зеркалах; вердикты на фикстурах паритет-теста
|
||||
не изменились (существующий тест).
|
||||
- **AC3 (rev-кэш).** Две подряд записи с одним previous: вторая не считает
|
||||
нарушения previous заново (тест со счётчиком через monkeypatch); смена rev
|
||||
инвалидирует кэш; вердикты идентичны пути без кэша на обеих фикстурах
|
||||
(унаследованное нарушение / новое нарушение).
|
||||
- **AC4 (фронт-кэш).** В одном жесте ресайза baseline считается один раз —
|
||||
юнит со счётчиком на `_junctionLimitsIntroduced`; бюджет полного набора
|
||||
П1–П5 кандидата — по НОВОМУ бенчу §5 (r1-H2: `benchmark_safe_resize`
|
||||
код лимитов не вызывает и доказательством не является).
|
||||
- **AC5 (эквивалентность §4.6).** Для документа текущей версии вердикты
|
||||
«как есть» и «через миграцию» совпадают: паритет-тест дополняется этим
|
||||
кейсом на фикстурах границ (14°/15°, 19/20 см, T-стык, доборный атом);
|
||||
легаси-документ по-прежнему мигрируется (существующий тест H1 из #329
|
||||
остаётся зелёным).
|
||||
- **AC6 (семантика не тронута).** Все существующие юниты #329, паритет-тест,
|
||||
смоки `smoke_junction_limits`/`smoke_island_rooms`/`smoke_room_resize` и
|
||||
мутанты `junction-limit-*` зелёные без изменения ожиданий.
|
||||
- **AC6 (перф-контракт).** Бенч §5 в CI, красный при возврате O(n²)
|
||||
(проверяется мутантом на «строить индекс на каждый сегмент»).
|
||||
- **AC7 (перф-контракт).** Бенч §5 в CI, красный при возврате O(n²) —
|
||||
доказано мутантом «индекс на каждый сегмент».
|
||||
|
||||
## 7. План тестов
|
||||
|
||||
Юниты: rev-кэш (заполнение, инвалидация, эквивалентность вердиктов),
|
||||
baseline-кэш фронта (одно вычисление на эпоху), эквивалентность П3 с
|
||||
индексом и без на фикстурах границ. Бенч §5. Мутанты: `junction-limit-p3-
|
||||
quadratic-again` (индекс на каждый сегмент — бенч красный),
|
||||
`junction-limit-baseline-cache-stale` (кэш не инвалидируется по rev — тест
|
||||
эквивалентности красный). Бэкенд: тест синхронного времени AC1.
|
||||
Юниты: rev-кэш (заполнение, инвалидация, эквивалентность вердиктов);
|
||||
baseline-кэш фронта (одно вычисление на эпоху); эквивалентность П3/П4 с
|
||||
индексами и без на фикстурах границ; эквивалентность §4.6 (v9 как есть ==
|
||||
v9 через миграцию). Бенч §5 в перф-джобе. Мутанты:
|
||||
`junction-limit-p3-quadratic-again` (индекс П3 на каждый сегмент — бенч
|
||||
красный), `junction-limit-p4-bruteforce-again` (bucket отключён — бенч
|
||||
красный), `junction-limit-baseline-cache-stale` (кэш не инвалидируется по
|
||||
rev — тест эквивалентности красный). Бэкенд: тест синхронного времени AC1.
|
||||
|
||||
## 8. Откат
|
||||
|
||||
Чистый revert: изменения не трогают формат данных, конфиг и контракт
|
||||
WebSocket-команд; кэши — только в памяти процесса.
|
||||
|
||||
## 9. Обязательные разделы (r1-M1)
|
||||
|
||||
- **i18n**: новых ключей нет — задача не меняет ни один пользовательский
|
||||
текст.
|
||||
- **Touch**: не задет — меняется стоимость вычислений, не жесты и не
|
||||
пороги; touch-контракт (docs/TOUCH-SUPPORT.md) не упоминает лимиты.
|
||||
- **Риски**: (1) executor под write_lock — блокировки loop нет, но время
|
||||
ответа записи сохраняется; смягчение — AC1 меряет именно loop-время;
|
||||
(2) §4.6 меняет представление, на котором считаются лимиты для
|
||||
v9-документов — закрыто AC5-эквивалентностью; (3) bucket-П4 —
|
||||
геометрическая эквивалентность brute-force закрыта юнитами границ (4/5 см)
|
||||
и паритетом.
|
||||
- **Release-артефакты**: обычная бета; CHANGELOG (EN+RU) — одна строка
|
||||
user-visible («сохранение и ресайз больших планов быстрее, HA не
|
||||
замирает»); фингерпринт бандла пересобирается штатно; миграций конфига
|
||||
нет, откат §8 — чистый revert.
|
||||
|
||||
Reference in New Issue
Block a user