mirror of
https://github.com/Matysh/houseplan-card
synced 2026-10-01 12:18:51 +00:00
docs: spec #330 — junction limits performance
Four cuts, zero verdict changes: the ws_config_set validator chain moves to
the executor, the previous-document violation counts are cached by
config_rev, П3 builds its node index once per check in both mirrors, and the
frontend baseline is cached per config epoch. A new benchmark with budgets
pins the class of regression (O(n²) returning) in CI.
Measured on dev 2c20f2dc: a 576-atom plan costs 2.8 s in the HA event loop
per config write today; the spec's acceptance bar is ≤50 ms of loop time.
Issue: #330
User-Visible: no
This commit is contained in:
@@ -0,0 +1,148 @@
|
||||
# Issue #330 — производительность ограничений стыков (#329)
|
||||
|
||||
Статус: ревизия 1. Родитель: #329 (лимиты выпущены, вердикты корректны —
|
||||
подтверждено паритет-тестом и код-ревью r1–r3). Эта задача меняет ТОЛЬКО
|
||||
стоимость вычисления вердиктов; ни один вердикт не меняется ни на бит.
|
||||
Смежный #331 (пограничная точность) сознательно вне скоупа.
|
||||
|
||||
## 1. Сценарий и пользовательский результат
|
||||
|
||||
Замеры на `dev` `2c20f2dc`, синтетическая сетка комнат 300×300 см, стены
|
||||
15 см, легаси-хранение (худший и одновременно типичный случай: первый
|
||||
структурный write после обновления карточки):
|
||||
|
||||
| План | Бэкенд `validate_junction_limits` | из них 1 миграция | П3 | П4 |
|
||||
|---|---|---|---|---|
|
||||
| 6×6, 144 атома | 189 мс | 53 мс | 16 мс | 23 мс |
|
||||
| 12×12, 576 атомов | **2 835 мс** | 754 мс | 285 мс | 376 мс |
|
||||
|
||||
Вся цепочка валидаторов `ws_config_set` выполняется синхронно в event loop
|
||||
HA под write_lock — на 576 атомах каждое сохранение замораживает ВЕСЬ Home
|
||||
Assistant почти на 3 секунды. `MAX_CONFIG_BYTES` = 2 МБ допускает документы
|
||||
ещё на порядок больше. Гайдлайн HA: >50 мс в event loop недопустимо.
|
||||
|
||||
Фронтенд, те же 576 атомов: `checkSegmentLengths` — 289 мс/вызов,
|
||||
`checkNodeDistances` — 105 мс; в ресайзе проверка кандидата зовётся на
|
||||
каждый pointermove, а baseline (клон конфига + миграция + П1–П5 + polyclip
|
||||
по комнатам) пересчитывается там же, хотя в течение жеста неизменен.
|
||||
|
||||
Результат для пользователя: сохранение большого плана не подвешивает HA;
|
||||
ресайз на большом плане держит существующий кадровый бюджет.
|
||||
|
||||
## 2. Корни (по убыванию вклада)
|
||||
|
||||
1. **Две полные миграции на запись.** `validate_junction_limits` гонит и
|
||||
`previous`, и `candidate` через `commit_wall_segment_model` (deepcopy +
|
||||
каноникализация) — ~1.5 с из 2.8 на 12×12. `previous` между записями не
|
||||
меняется: его миграция и подсчёт нарушений выбрасываются и повторяются.
|
||||
2. **П3 квадратичен в обеих реализациях.** `collinearRunLengthUnits` /
|
||||
`collinear_run_length_units` строит индекс узлов заново для КАЖДОГО
|
||||
сегмента → O(n²) с тяжёлой константой. Зеркала честно скопировали дефект
|
||||
друг друга.
|
||||
3. **Фронтовый baseline без кэша.** `_junctionLimitsIntroduced` строит
|
||||
baseline-нарушения из previousConfig на каждый вызов; в жесте ресайза —
|
||||
на каждый move.
|
||||
|
||||
## 3. Границы
|
||||
|
||||
- Вердикты П1–П5 байт-идентичны текущим на любом входе. Существующие юниты
|
||||
границ, паритет-тест TS↔Python и все три мутанта #329 проходят без правок
|
||||
их семантики (мутантам разрешено обновить якоря, если строки сместились).
|
||||
- Спека #329 (наследование по правилу, обе стороны после одной миграции,
|
||||
скоуп применения §3) не меняется.
|
||||
- Вне скоупа: итеративный walk, точность ключей узлов, 0°-дубль (#331);
|
||||
optimize/import-лазейка (#333); пропуск миграции для уже-v9 документов —
|
||||
рассмотрен и отклонён: он меняет сравниваемое представление (кандидат от
|
||||
клиента не каноникализован) и рискует паритетом ради экономии, которую
|
||||
даёт rev-кэш.
|
||||
|
||||
## 4. Решение
|
||||
|
||||
### 4.1 Бэкенд: цепочка валидаторов в executor
|
||||
|
||||
В `ws_config_set` вынести CPU-часть цепочки (validate_wall_model_transition,
|
||||
CONFIG_SCHEMA, marker/passage/host-валидаторы, validate_junction_limits) в
|
||||
`async_add_executor_job` одной функцией над данными: вход — `msg["config"]`
|
||||
и снапшот `data.get("config")`, выход — validated config либо типизированная
|
||||
ошибка. Применение результата (`msg["config"].clear()/update`) и отправка
|
||||
ответа остаются в loop. write_lock сохраняется — исключается только
|
||||
блокировка loop, не сериализация записей. Паттерн в кодовой базе есть:
|
||||
parse/merge импорта уже выполняются в executor.
|
||||
|
||||
`ws_plan_optimize` — тем же способом (он уже страдает той же синхронной
|
||||
CONFIG_SCHEMA + миграцией на больших планах).
|
||||
|
||||
### 4.2 Бэкенд: кэш baseline по config_rev
|
||||
|
||||
На runtime-объекте — один слот `_junction_baseline: (config_rev, counts)`,
|
||||
где counts — счётчики нарушений previous ПО ПРАВИЛАМ (то, что и потребляет
|
||||
сравнение; сами документы не хранятся). Заполняется лениво при первом
|
||||
validate, инвалидируется несовпадением rev. Повторные записи одного клиента
|
||||
перестают мигрировать previous вовсе. `validate_junction_limits` получает
|
||||
опциональный параметр `baseline_counts`; его отсутствие — прежний путь
|
||||
(миграция previous), чтобы прямые вызовы и тесты не изменились.
|
||||
|
||||
### 4.3 Обе реализации: линейный П3
|
||||
|
||||
`checkSegmentLengths` / `check_segment_lengths` строит byNode-индекс один
|
||||
раз и передаёт его в прогон коллинеарной цепочки; публичная сигнатура
|
||||
`collinearRunLengthUnits(segment, segments)` сохраняется (индекс — третий
|
||||
опциональный аргумент; без него строится как раньше — для прямых вызовов
|
||||
из тестов).
|
||||
|
||||
### 4.4 Фронтенд: кэш baseline на конфиг-эпоху
|
||||
|
||||
`_junctionLimitsIntroduced` кэширует baseline-нарушения по ключу
|
||||
`${spaceId}|${cfgEpoch}` (один слот, как _wallUnionCache). `_cfgEpoch`
|
||||
инкрементится при каждом изменении конфига, поэтому инвалидация бесплатна и
|
||||
консервативна: любой реальный write сбрасывает кэш. Жест ресайза считает
|
||||
baseline один раз вместо «на каждый move».
|
||||
|
||||
## 5. Перф-контракт (новый, в CI)
|
||||
|
||||
`demo/benchmark_junction_limits.mjs` по образцу `benchmark_safe_resize.mjs`:
|
||||
сетка 12×12 (576 атомов), бюджеты в файле бенча:
|
||||
|
||||
- фронт: `checkSegmentLengths` ≤ 25 мс, полный `_junctionLimitViolations`
|
||||
кандидата ≤ 60 мс (p95 из N прогонов);
|
||||
- бэкенд (запускается тем же бенчем через python3): полный
|
||||
`validate_junction_limits` с тёплым rev-кэшем ≤ 150 мс, холодный ≤ 900 мс.
|
||||
|
||||
Бюджеты — 2–3× от замеренного после фикса, чтобы бенч ловил регресс класса
|
||||
«вернули O(n²)», а не дрожание раннера. Включается в перф-джобу CI рядом с
|
||||
существующими бенчами.
|
||||
|
||||
## 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, паритет-тест,
|
||||
смоки `smoke_junction_limits`/`smoke_island_rooms`/`smoke_room_resize` и
|
||||
мутанты `junction-limit-*` зелёные без изменения ожиданий.
|
||||
- **AC6 (перф-контракт).** Бенч §5 в CI, красный при возврате O(n²)
|
||||
(проверяется мутантом на «строить индекс на каждый сегмент»).
|
||||
|
||||
## 7. План тестов
|
||||
|
||||
Юниты: rev-кэш (заполнение, инвалидация, эквивалентность вердиктов),
|
||||
baseline-кэш фронта (одно вычисление на эпоху), эквивалентность П3 с
|
||||
индексом и без на фикстурах границ. Бенч §5. Мутанты: `junction-limit-p3-
|
||||
quadratic-again` (индекс на каждый сегмент — бенч красный),
|
||||
`junction-limit-baseline-cache-stale` (кэш не инвалидируется по rev — тест
|
||||
эквивалентности красный). Бэкенд: тест синхронного времени AC1.
|
||||
|
||||
## 8. Откат
|
||||
|
||||
Чистый revert: изменения не трогают формат данных, конфиг и контракт
|
||||
WebSocket-команд; кэши — только в памяти процесса.
|
||||
Reference in New Issue
Block a user