Andrey Epifancev eb5e40e9ef perf: README — порядок разбора и отчёта, web-sourcemap, hotspots
Refs #202

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 13:08:12 +04:00

264 lines
35 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Профилирование: perf/
## Стенд
Требования: Docker, python3.12, Node 24, npm.
```
perf/stand.sh all # up, migrate, bootstrap, web-build, api, web, smoke
perf/stand.sh down # остановить бэк, фронт и Postgres
```
Подкоманды: `up | migrate | bootstrap | api | web-build | web | web-profiling | web-sourcemap | smoke | reset | down | all` (`web-profiling` — профилирующая сборка на :8081, `web-sourcemap` — prod-сборка с source maps на :8082 для CPU-профилей, раздел «Анализ и отчёт»).
`reset` — чистая БД: останавливает бэк, сносит volume Postgres, поднимает заново (`up`, `migrate`, `bootstrap`, `api`); фронт не трогает.
- Postgres 16 (`pg_stat_statements`) на `localhost:55432`, `perf/perf/perf`.
- Бэк (`DEBUG=true`) на `http://localhost:8000`, логи и PID в `perf/.run/`.
- Фронт (prod-сборка, `rsbuild preview`) на `http://localhost:8080`; `/api` (включая WebSocket) проксируется на бэк.
- Профилирующая сборка (`PERF_PROFILING=1`, `react-dom/profiling`) лежит в `perf/.build/profiling`, обычная в `perf/.build/prod`.
Для запуска preview с профилирующей сборкой: `PERF_PROFILING=1 perf/stand.sh web` (после остановки обычного `web`).
- Админ: `admin` / `admin`.
- Переменные: `PERF_API`, `PERF_WEB`, `PERF_DB_DSN`.
- Зависимости Python ставятся в `perf/.venv` (`api/requirements-dev.txt` + `perf/requirements.txt`).
- npm берёт пакеты с `https://registry.npmjs.org` (`npm_config_registry`), `web/.npmrc` не нужен.
## Данные
```
perf/stand.sh reset # пустая БД (генератор на непустой БД откажется работать)
cd perf && .venv/bin/python -m perfkit.gen.run --scale 1 # ~12 мин на масштабе 1 (факт 11.4), ~45-70 мин на 2; пишет results/manifest-s1.json
cd perf && .venv/bin/python -m perfkit.gen.verify --scale 1 # должен напечатать VERIFY OK
```
Генератор работает через HTTP-API под `admin` (WebSocket — только для программ и проектов FORM_4,
у них нет HTTP-ручки). Пользователи: `perf_rf`, `perf_rf_coauthor` (одна РФ, роль 3) и `perf_dfip`
(роль 2, все оргединицы), пароль `perf`; в манифесте они под ключами `rf`, `rf_coauthor`, `dfip`.
Числа плана и их обоснование — в `perf/perfkit/gen/plan.py`.
## Фронт: замеры
Каркас: `perf/e2e` (Playwright + CDP). Версии: `@playwright/test` 1.64.1, Chromium 156.0.8078.4 (headless shell, revision 1248).
```
cd perf/e2e && npm install && npx playwright install chromium # npm_config_registry=https://registry.npmjs.org
npx playwright test tests/harness.spec.ts # самопроверка каркаса
```
- `PERF_RUN_ID` — имя каталога прогона `perf/results/<run_id>/` (по умолчанию `YYYYMMDD-HHMM`).
- `PERF_SCALE` — масштаб манифеста `perf/results/manifest-s<scale>.json` (по умолчанию 1).
- `PERF_API`, `PERF_WEB` — адреса стенда (`http://localhost:8000`, `http://localhost:8080`).
- Агрегация: `cd perf && .venv/bin/python -m perfkit.aggregate results/<run_id>`.
- Библиотеки `perf/e2e/lib/`: `auth.loginAs`, `cdp.*` (throttle, trace, gc, heap, `mainThreadMs`), `probe` (long tasks > 50 мс, кадры rAF; тяжёлый кадр — интервал > 50 мс; `frame_excess_ms` — сумма превышений кадра над 16,7 мс; `startProbe(page)`/`stop()` добавляет `main_thread_ms`), `results.append`, `manifest.loadManifest`, `netlog.attachNetlog`.
- Троттлинг CPU выключен по решению человека (все матрицы по умолчанию на CPU 1); включается вручную `PERF_CPU_RATES` (например `1,4`), контроль 4x — шаг `control4x` в `run-all.sh`.
- Основная метрика сравнения (решение человека): суммарное время main thread на действие — `main_thread_ms` (прирост CDP `Performance.TaskDuration`: скрипт, стили, layout, задачи короче 50 мс тоже) и для скролла `main_thread_ms_per_step` (на шаг колеса 200 px). В `summary.md` они первые строки каждой группы.
### Сценарии скролла
`perf/e2e/scenarios/scroll.spec.ts`, библиотека `lib/scroll.ts` (`scrollThrough`: колесо мыши шагами по 200 px вниз/вправо до конца и обратно, `complete` = `scrollTop + clientHeight >= scrollHeight - 2`). Самопроверка: `npx playwright test tests/scroll-lib.spec.ts`.
```
cd perf/e2e
PERF_RUN_ID=s1 PERF_SCALE=1 npx playwright test scenarios/scroll.spec.ts # вся матрица масштаба (долго!)
PERF_RUN_ID=t PERF_TARGETS='FORM_2/AHR/*' PERF_CPU_RATES=1 PERF_REPEATS=1 npx playwright test scenarios/scroll.spec.ts
cd .. && .venv/bin/python -m perfkit.aggregate results/s1
```
- Матрица (CPU 1): роль `rf` x листы с `owner=rf` и отчёты проектов x оси y,x x 3 повтора; `admin` x все листы x ось y x 1 повтор (контроль, только масштаб 1); `admin` и `dfip` x своды (svod1_MAIN/FORM_1..4, svod2_GO/RF, projects_summaries) x ось y x 3 повтора + одна смена фильтра (target `.../filter`, на максимальном из `PERF_CPU_RATES`). На масштабе 1 — 249 тестов (rf 156, admin 61, dfip 32).
- target: `FORM_x/<sheet>/<direction|null>` (лист ССП с тем же именем, что у листа РФ, — с суффиксом владельца: `FORM_2/AHR/null/ssp`), `project/<report_type>/<project_id>/<year>`, `svod/<name>`; к записи добавляется суффикс оси `@y` / `@x` (агрегатор группирует по target).
- Переменные: `PERF_TARGETS` (glob через запятую; сопоставляется с target без суффикса оси и с полным), `PERF_CPU_RATES` (по умолчанию `1`; `1,4,6` включает троттлинг вручную), `PERF_REPEATS` (число повторов вместо матричного), `PERF_ROLES` (`rf,admin,dfip`), `PERF_AXES` (`y,x`), `PERF_LOAD_TIMEOUT_MS` (ожидание данных, по умолчанию 15 мин; ответ свода 2 до 47 МБ), `PERF_MAX_STEPS`, `PERF_TEST_TIMEOUT_MS` (таймаут одного замера, по умолчанию 45 мин), `PERF_TEST_TIMEOUT_SVOD2_MS` (для свода 2, по умолчанию 3 ч: загрузка до 15 мин + скролл ~7 мин на CPU 1, с запасом на ручной троттлинг), `PERF_TRACE=0` (не снимать trace вообще).
- Каждый замер - новый browser context; CPU-троттлинг включается после загрузки данных (до скролла). Перед замером ждём >= 20 строк `tbody tr`; если не дождались - запись `incomplete: true` с `skip_reason`.
- Метрики: `main_thread_ms`, `main_thread_ms_per_step`, probe (long tasks, кадры, `frame_excess_ms`), `axis` (0 = y, 1 = x), `rows_visible_before/after`, `json_bytes_sheet` (размер распакованного ответа загрузки данных из сетевого лога), `steps`. Записи с `skip_reason` (`no_overflow`, `rows<20`, `filter_no_effect`, `scroll_incomplete`...) всегда `incomplete:true`; в конце прогона они выводятся списком. Метрика `traced=1` - замер с включённым трейсом (повтор 0), aggregate исключает его из статистики. Поэтому trace снимается только если повторов больше одного (`lib/trace.ts` `shouldTrace`): у admin-контроля, control4x и при `PERF_REPEATS=1` trace нет, а единственная запись идёт в статистику. Trace первого повтора - в `perf/results/<run_id>/artifacts/` (30-70 МБ каждый, в git не попадают).
- Сайт свода не читает `#hash` из URL: вкладка (`FORM_1`, `GO`...) выбирается кликом по `role=tab`.
### Скролл при соавторе
`perf/e2e/scenarios/scroll-coauthor.spec.ts`, бот `perf/perfkit/coauthor.py`. Автор `perf_rf` (роль rf) скроллит лист по оси y, пока бот `perf_rf_coauthor` правит числовые ячейки того же листа по WebSocket с частотой `rate` правок/с. Цели — самый большой лист каждой формы среди `owner=rf` (FORM_2/AHR 1154 строки, FORM_4/STRUCTURE 775; FORM_1 у rf нет). Бот стартует после загрузки листа и ждёт эха первой правки (READY), работает до SIGTERM после скролла. Только масштаб 1 (так в плане).
```
cd perf/e2e
PERF_RUN_ID=co npx playwright test scenarios/scroll-coauthor.spec.ts # 2 цели x rate 1,5 x 3 повтора, ~12 мин
PERF_RUN_ID=co PERF_TARGETS='FORM_2/AHR/*' PERF_COAUTHOR_RATES=5 PERF_REPEATS=1 npx playwright test scenarios/scroll-coauthor.spec.ts
cd .. && .venv/bin/python -m pytest -m stand tests/test_coauthor.py # самопроверка бота (нужен стенд)
```
- target `<form_type>/<sheet>/<direction|null>/coauthor<rate>@y`, scenario `scroll_coauthor`; метрики как у скролла + `coauthor_rate`, `coauthor_sent`, `coauthor_echoed`, `coauthor_errors`, `page_ws_updates` (сколько чужих `cell_updated` получила страница автора).
- Переменные: `PERF_COAUTHOR_RATES` (`1,5`), `PERF_TARGETS` (glob по имени листа), `PERF_REPEATS` (3), `PERF_CPU_RATES` (`1`), `PERF_TRACE`, `PERF_LOAD_TIMEOUT_MS`, `PERF_MAX_STEPS`, `PERF_TEST_TIMEOUT_MS`, `PERF_PYTHON`.
- Запись не пишется (тест падает), если бот не получил ни одного эха или страница не увидела ни одного `cell_updated` соавтора: иначе замер был бы «без соавтора».
- На rate 5 сервер подтверждает ~1,2 правки/с (одно WS-соединение обрабатывается последовательно): фактическую нагрузку показывают `coauthor_echoed`/`page_ws_updates`, а не номинальный rate.
### Модалки
Перечень - `perf/e2e/modals.json` (id, page, source `file:line`, `open{selector, action, pre}`, priority, роль; `skip_reason` для недостижимых из UI). `lib/modal.ts` (`measureOpen`: `click_to_visible_ms`, `long_task_n`, `long_task_ms`, `main_thread_ms` = прирост CDP `Performance.TaskDuration` от клика до отрисовки), `lib/reactProfile.ts` (минимальный `__REACT_DEVTOOLS_GLOBAL_HOOK__`: renders и self_ms по компонентам, нужна профилирующая сборка). Самопроверка: `npx playwright test tests/modal-lib.spec.ts`.
```
cd perf/e2e
PERF_RUN_ID=m PERF_PRIORITY=1 PERF_REPEATS=2 npx playwright test scenarios/modals.spec.ts # priority 1 (по умолчанию 5 повторов; priority 2 - 3)
PERF_RUN_ID=m PERF_PRIORITY=2 npx playwright test scenarios/modals.spec.ts
../stand.sh web-profiling # профилирующая сборка на :8081
PERF_WEB=http://localhost:8081 PERF_PROFILING=1 PERF_RUN_ID=m PERF_MODALS='sheet_*,projects_create' npx playwright test scenarios/modals.spec.ts # scenario modal_renders
cd .. && .venv/bin/python -m perfkit.modals_inventory results/m # report-data/m/modals-inventory.md
```
- Переменные: `PERF_MODALS` (glob по id; `<id>@big` - вариант на самом большом листе), `PERF_PRIORITY` (`1`, `2`, `1,2`), `PERF_ROLES`, `PERF_REPEATS`, `PERF_CPU_RATES` (по умолчанию `1`, троттлинг выключен).
- Каждый замер - новый browser context. Записи: `scenario: modal` (метрики `measureOpen`) и `modal_renders` (`metrics.renders_top` - топ-20 по self_ms, `commits`, `total_renders`).
- Оценка полной матрицы на CPU 1: priority 1 (17 целей x 5) около 6 мин, priority 2 (15 x 3) около 2 мин, profiling около 2 мин; всего 10-12 мин (150 тестов при `PERF_PRIORITY=1,2`, 21 в профилирующем прогоне приоритета 1).
Малый и большой лист. Форм-модалки (выбор ВСП/статьи, создание программы/проекта) существуют только на листах FORM_2 AHR_RENT/UTILITY/SECURITY (31 строка) и FORM_4 AHR/CAP/OPER (110 строк). Для зависимости от числа строк `perfkit.gen.grow` (`cd perf && .venv/bin/python -m perfkit.gen.grow --scale 1`, идемпотентно, ~4 мин, FORM_4 add_line O(n)) растит FORM_2 AHR_UTILITY/AHR_SECURITY до 500 и FORM_4 CAP до 300 строк и обновляет манифест. В `modals.json` у этих модалок `big_sheet`: пара записей `<id>` (малый лист: AHR_RENT, FORM_4/AHR) и `<id>@big` (UTILITY, FORM_4/CAP). Порядок для run-all: reset -> gen -> скролл -> соавтор -> grow -> модалки; малые замеры от grow не зависят, но скролл по UTILITY/SECURITY/CAP после grow несопоставим с до него.
### Память
`perf/e2e/scenarios/memory.spec.ts` - одна SPA-сессия роли `rf` на CPU 1 без перезагрузки страницы, 20 циклов + базовая точка (цикл 0) = 21 запись `scenario: memory`. Цикл: самый большой лист FORM_2 owner=rf (AHR) -> скролл вниз/обратно -> 3 правки ячеек двойным кликом (идут через WS, ждём эхо) -> `/tasks` -> самый большой лист FORM_4 owner=rf (STRUCTURE) -> скролл -> `/tasks` -> gc -> метрики. Навигация - history API (клиентский роутер). Листы FORM_1 у `rf` недоступны, поэтому вместо них FORM_2/FORM_4.
```
cd perf/e2e
PERF_RUN_ID=mem npx playwright test scenarios/memory.spec.ts # полный прогон, около часа
PERF_RUN_ID=memsmall PERF_MEM_CYCLES=2 PERF_MEM_SNAPSHOTS=0,2 npx playwright test scenarios/memory.spec.ts # малый
cd .. && .venv/bin/python -m perfkit.memtrend results/mem [--from-cycle 1] # наклоны МНК на цикл
```
- Метрики записи: `cycle`, `heap_used_mb` (после gc), `documents`, `nodes`, `js_event_listeners` (CDP `Memory.getDOMCounters`), `ws_open` (обёртка над `window.WebSocket`), `ws_created`, `edits_ok`, `cycle_s`, `relogin`.
- Heap snapshots на циклах 0, 10, 20 (`PERF_MEM_SNAPSHOTS`) - `perf/results/<run_id>/artifacts/memory-cycleNN.heapsnapshot`, в git не попадают; открываются в Chrome DevTools (Memory). Разбор retainers - Task 10.
- Переменные: `PERF_MEM_CYCLES` (20), `PERF_MEM_SHEET1/2` (`FORM_x/SHEET/direction|null`), `PERF_TEST_TIMEOUT_MS` (3 ч). Токен (60 мин) перевыпускается по API без перезагрузки, если до конца < 10 мин.
- Любая неудачная правка (нет эха, отказ сервера, нет редактируемых ячеек) - ошибка сценария.
Как читать `perfkit.memtrend`:
- Цикл 0 - базовая точка: страница `/tasks` до первого открытия листа (heap 7 МБ, nodes 289). Скачок цикл 0 -> 1 - загрузка кода/данных первого листа, а не рост. Поэтому МНК по умолчанию считается с цикла 1 (`--from-cycle N`); вывод печатает использованные циклы и число точек.
- Вторым блоком печатается наклон по второй половине (циклы 11-20) - индикатор плато: если он заметно меньше общего, рост затухает.
- Snapshots на циклах 10 и 20 слегка влияют на heap следующего замера (после снимка остаются служебные структуры/GC), поэтому единичные скачки в точках 11 и 21 не интерпретировать.
- Результат прогона t8-mem (с цикла 1): heap 0.046 МБ/цикл (вторая половина 0.027), nodes 0, listeners 0, ws_open = 1 всегда. DOM, listeners и WS стабильны; остаточный рост JS-heap ~27-46 КБ/цикл неубедителен на 20 циклах - кандидат на diff retainers снимков 10 и 20 (Task 10).
### Бэк
Сценарии Task 5-8 пишут сетевой лог (`perf/e2e/lib/netlog.ts`, подключён в каждом сценарии после `newContext`) в `perf/results/<run_id>/network.jsonl`: метод, полный URL с query, путь, статус, `duration_ms`/`ttfb_ms`, `body_size` (сжатый, по CDP), тело не-GET (для путей с `/auth/` не пишется, из остальных вырезаются поля `password`/`token`/`secret`), `scenario`/`target`/`role`. Фронт ходит в API через proxy `:8080/api`, оба origin учитываются. `globalSetup` Playwright фиксирует `PERF_RUN_ID` и делает `pg_stat_statements_reset()`, `globalTeardown` (если был сетевой лог) пишет `sql_top.json` и `explain/*.json` в каталог прогона, а при `PERF_SQLSTATS_TAG=<tag>` — в `<run_dir>/sql/<tag>/` (так делает `run-all.sh`: каждый вызов Playwright сбрасывает статистику, без тега следующий вызов перезатёр бы предыдущий); `PERF_SQLSTATS=0` отключает, при недоступном Postgres печатается предупреждение, прогон не падает.
```
cd perf
.venv/bin/python -m perfkit.bench results/<run_id> --repeats 20 # api.jsonl: scenario api, metrics ms/bytes
.venv/bin/python -m perfkit.aggregate results/<run_id> # таблицы, в т.ч. scenario api
.venv/bin/python -m perfkit.sqlstats dump results/<run_id> [--out results/<run_id>/sql/<tag>] # вручную
```
- `bench`: повторяет уникальные GET (роль и токен из манифеста), `target` = `METHOD шаблон` (числовые сегменты -> `{id}`; разные form_id сливаются в одну группу, полный URL - в поле `url` записи). `ms` - полное время ответа с чтением тела, `bytes` - размер после распаковки (в логе `body_size` сжатый). PATCH ячеек повторяются только при `"idempotent": true` в строке лога; сценарии такой пометки не ставят (правки идут по WebSocket), поэтому PATCH не повторяются. POST/DELETE - никогда. Запросы, длившиеся в логе дольше `--max-logged-s` (60), пропускаются (свод 2). Повторный запуск заменяет `api.jsonl`.
- `sql_top.json`: топ по `total_exec_time` и `mean_exec_time`; поле `toplevel=false` - вложенные запросы внутри plpgsql-функций (их `p_*` в тексте - переменные функции, отдельно не выполнить). Собственные запросы статистики (EXPLAIN, `perf-sqlstats`) исключены.
- `explain/<n>.json`: топ-5 по `total_exec_time` среди top-level SELECT (не `v3.add_/upd_/del_`, не DML), в транзакции с ROLLBACK. Параметры: для `v3.v_form_view` и `v3.v_summary_view` берутся из сетевого лога (тот же разбор query, что у роутера), режим `analyze` (`EXPLAIN (ANALYZE, BUFFERS)`, `SET LOCAL app.user_id` роли из лога). Для остальных - `generic_plan` (PREPARE + `force_generic_plan` + EXPLAIN без выполнения), поле `mode`. План вызова функции - один Function Scan: внутренности функции видны только в `sql_top.json` (nested-строки).
- `aggregate` исключает записи с `metrics.traced == 1` (повтор под трассировкой, O-9) и не выводит метрику `traced`; число исключённых печатается в заголовке группы.
## Полный прогон: `perf/run-all.sh`
Все замеры на масштабах 1 и 2 одной командой (долго: оценка ~6,5 ч на масштаб 1 и ~13 ч на масштаб 2 при 3 повторах). Стенд должен быть собран (`perf/stand.sh all` хотя бы раз); `reset` сносит только volume `perf-pg` этого стенда.
```
PERF_DRY_RUN=1 perf/run-all.sh # план: шаги, команды, оценка по шагам и итог; ничего не запускает
nohup perf/run-all.sh > /tmp/run-all.out 2>&1 & # боевой прогон (масштаб 1, затем 2)
```
Порядок шагов на каждом масштабе (`grow` меняет размеры листов FORM_2 AHR_UTILITY/AHR_SECURITY и FORM_4 CAP, поэтому скролл и соавтор — до него, модалки — после):
| шаг | что делает | масштаб 1 | масштаб 2 |
|---|---|---|---|
| reset | `stand.sh reset`: новый volume Postgres, миграции, bootstrap, api | ~2 мин | ~2 мин |
| gen | `perfkit.gen.run --scale S` | ~12 мин | ~45-70 мин |
| verify | `perfkit.gen.verify --scale S` (не фатален) | ~1 мин | ~2 мин |
| stand | api на :8000, prod-сборка на :8080 (сверка `index.html` с `.build/prod`), smoke | ~1 мин | ~1 мин |
| preflight | `playwright test --list` всех сценариев масштаба (манифест, дубли названий) | секунды | секунды |
| scroll | `scroll.spec.ts`, полная матрица (admin-контроль только на масштабе 1) | ~4,2 ч | ~10,8 ч (3 повтора) |
| coauthor | `scroll-coauthor.spec.ts`, rate 1 и 5 | ~13 мин | — |
| grow | `perfkit.gen.grow --scale S` | ~4 мин | ~4 мин |
| modals | `modals.spec.ts`, `PERF_PRIORITY=1,2` (p1 x5, p2 x3, включая `@big`) | ~10 мин | ~12 мин |
| modals_profiling | `stand.sh web-profiling` + профилирующий прогон p1 на :8081; затем preview :8081 останавливается (:8080 и :8000 не трогаются) | ~3 мин | — |
| memory | `memory.spec.ts`, 20 циклов | ~60 мин | — |
| memtrend | `perfkit.memtrend` -> `memtrend.txt` | секунды | — |
| control4x | только по запросу (см. ниже) | ~5 мин на цель | |
| bench | `perfkit.bench <run_dir> --repeats 20 --scale S` | ~30 мин | ~45 мин |
| aggregate | `perfkit.aggregate`, `modals_inventory`, копирование в `report-data/<run_id>/` | ~1 мин | ~1 мин |
- run_id масштаба — `<PERF_RUN_TAG>-s<S>` (тег по умолчанию — сегодняшняя дата `YYYYMMDD`) или явный `PERF_RUN_ID_S1`/`PERF_RUN_ID_S2`; все сценарии масштаба пишут в один каталог `perf/results/<run_id>/`, SQL-статистика — по шагам в `sql/<шаг>/`.
- Лог `perf/results/<run_id>/run-all.log`: `START`/`END` шага со временем, кодом возврата и длительностью, вывод команд. Статус `run-all.status.json`: по шагу `OK`/`FAIL`/`SKIP`, `rc`, время, команда. Падение шага не останавливает прогон; падение `reset`, `gen` или `stand` пропускает остаток масштаба (следующий масштаб идёт). Код выхода 1, если был FAIL. Шаг aggregate падает при любом сбое копирования и если `summary.json` не создан заново.
- В `perf/report-data/<run_id>/` (коммитится): `summary.json`, `summary.md`, `modals-inventory.md`, `sql/<шаг>/{sql_top.json,explain/}`, `memtrend.txt`, `manifest-sS.json`, `gen-stats-sS.json`, `grow-sS.json`, `run-all.status.json`. Traces, heap snapshots и сырые jsonl остаются в `perf/results/` (в git не попадают).
- Матричные переменные сценариев (`PERF_TARGETS`, `PERF_REPEATS`, `PERF_CPU_RATES`, `PERF_ROLES`, `PERF_MODALS`, `PERF_PRIORITY`, ...) из окружения не наследуются — их задаёт шаг; `PERF_LOAD_TIMEOUT_MS`, `PERF_TEST_TIMEOUT_MS`, `PERF_TEST_TIMEOUT_SVOD2_MS`, `PERF_MAX_STEPS` передаются как есть.
Возобновление (данные не пересоздаются, шаги выполняются в каноническом порядке):
```
PERF_SCALES=1 PERF_RUN_TAG=20261026 PERF_SKIP_RESET=1 PERF_STEPS=memory,memtrend,bench,aggregate perf/run-all.sh
PERF_SCALES=2 PERF_RUN_TAG=20261026 PERF_SKIP_RESET=1 PERF_STEPS=scroll,grow,modals,bench,aggregate perf/run-all.sh
```
- `PERF_SCALES` (`1 2`), `PERF_STEPS` (через запятую), `PERF_SKIP_RESET=1` (пропустить `reset` и `gen`), `PERF_RUN_TAG`, `PERF_BENCH_REPEATS` (20).
- Перезапуск `scroll`/`coauthor` после `grow` даёт несопоставимые цифры для FORM_2 AHR_UTILITY/AHR_SECURITY и FORM_4 CAP; повторно запущенный шаг дописывает записи в те же jsonl (агрегатор сложит их с прежними) — для чистого повтора удалить `<scenario>.jsonl` шага.
- Масштаб 2: сценарии получают `PERF_LOAD_TIMEOUT_MS=2400000` (40 мин на загрузку, свод 2 ~150-190 МБ) и `PERF_TEST_TIMEOUT_MS=5400000` (90 мин), если они не заданы в окружении. `PERF_S2_REPEATS` (по умолчанию 3, как на масштабе 1; при 1 trace не снимается) и `PERF_S2_TARGETS` (glob целей скролла) — только для скролла масштаба 2. Оценка: 3 повтора ~13 ч, 2 — ~9,6 ч, 1 — ~6,3 ч; основная часть — свод 2 (строк ~в 4 раза больше, ответ ~150-190 МБ, при загрузке дольше 40 мин — `incomplete`).
Контроль на CPU 4x (решение человека: только для топ-3–4 находок, 1 повтор, цифры в отчёте помечаются как контрольные):
```
PERF_SCALES=1 PERF_RUN_TAG=20261026 PERF_SKIP_RESET=1 PERF_STEPS=control4x,aggregate \
PERF_CONTROL_TARGETS='FORM_2/AHR/null@y,svod/svod1_MAIN,modal:sheet_select_vsp@big,coauthor:FORM_2/AHR/null' perf/run-all.sh
```
- `PERF_CONTROL_TARGETS`: `modal:<id>` — модалка, `coauthor:<glob>` — скролл при соавторе, остальное — glob целей скролла; `PERF_CONTROL_ROLES` (`rf,dfip`) — роли скролла. CPU 4, 1 повтор (без trace); записи идут в тот же run_id (группы с `cpu_rate=4`), SQL — в `sql/control4x_*`.
- Контроль выполняется после `grow`: для FORM_2 AHR_UTILITY/AHR_SECURITY и FORM_4 CAP цифры 4x несопоставимы со скроллом 1x.
## Анализ и отчёт: `perf/analysis/`, `perf/REPORT.md`
Отчёт — `perf/REPORT.md` (каждая цифра ссылается на файл в `perf/report-data/<run_id>/`). Порядок воспроизведения:
1. Полный прогон: `PERF_S2_REPEATS=1 perf/run-all.sh` -> `results/<дата>-s1|s2`, `report-data/<дата>-s1|s2` (summary, sql, memtrend...).
2. После прогона в БД масштаб 2. Для ручного разбора и контроля CPU 4x восстановить масштаб 1 (~20 мин):
`perf/stand.sh reset && cd perf && .venv/bin/python -m perfkit.gen.run --scale 1 && .venv/bin/python -m perfkit.gen.verify --scale 1 && .venv/bin/python -m perfkit.gen.grow --scale 1`.
Состав данных эквивалентен замеренному (тот же план), но не побитово; `results/manifest-s1.json` перезаписывается (копия прогона - в `report-data/<run_id>/`).
3. Разбор сырых данных прогона (без живой БД):
```
cd perf
.venv/bin/python analysis/timeline_breakdown.py results/20261026-s1 --out report-data/20261026-s1/analysis # trace -> scripting/rendering/gc на шаг, точки входа JS
.venv/bin/python analysis/heapdiff.py results/20261026-s1/artifacts/memory-cycle10.heapsnapshot \
results/20261026-s1/artifacts/memory-cycle20.heapsnapshot --base results/20261026-s1/artifacts/memory-cycle00.heapsnapshot \
--out report-data/20261026-s1/analysis # рост классов, retainers новых объектов
.venv/bin/python analysis/memory_cycles.py results/20261026-s1 --out report-data/20261026-s1/analysis # таблица циклов памяти (cycle_s)
.venv/bin/python analysis/scale_compare.py --s1 20261026-s1 --s2 20261026-s2 --out report-data/20261026-s2/analysis # масштаб 1 против 2
```
4. Разбор на живой БД масштаба 1 (шаг 2):
```
.venv/bin/python analysis/explain_manual.py --scale 1 --out report-data/20261026-s1/explain-manual # EXPLAIN вложенных запросов, jit/work_mem, pg_stat_statements
.venv/bin/python analysis/api_split.py --scale 1 --out report-data/20261026-s1/analysis # полное время API против X-DB-Time-Ms
```
`explain_manual.py` берёт тексты запросов из последних версий функций в `api/alembic/versions/sql/` по диапазонам строк
с якорями (при изменении функции скрипт падает с сообщением), подставляет параметры литералами, выполняет EXPLAIN
(ANALYZE, BUFFERS) в транзакции с ROLLBACK и делает `pg_stat_statements_reset()` - не запускать параллельно с замерами.
5. Горячие точки фронта (`perf/e2e/scenarios/hotspots.spec.ts`, в run-all не входит): CPU-профиль на prod-сборке с source maps
и счётчики рендеров React на профилирующей сборке.
```
perf/stand.sh web-sourcemap # prod-сборка с source maps (PERF_SOURCEMAP=1, perf/.build/prod-sourcemap) на :8082; код = :8080 с точностью до хешей чанков
perf/stand.sh web-profiling # профилирующая сборка на :8081
cd perf/e2e
PERF_RUN_ID=20261026-hot PERF_SQLSTATS=0 PERF_HOT_MODE=cpu PERF_WEB=http://localhost:8082 \
PERF_HOT_TARGETS='FORM_2/AHR/null:y,FORM_2/AHR/null:x,FORM_4/STRUCTURE/null:y,project/LIMIT/1/2026:y,svod/svod1_MAIN:y,svod/projects_summaries:y,svod/svod2_GO:y,svod/svod2_GO:filter,svod/svod2_RF:filter,svod/svod1_MAIN:filter' \
npx playwright test scenarios/hotspots.spec.ts
PERF_RUN_ID=20261026-hot PERF_SQLSTATS=0 PERF_HOT_MODE=react PERF_WEB=http://localhost:8081 \
PERF_HOT_TARGETS='FORM_2/AHR/null:y,FORM_2/AHR/null:x,FORM_4/STRUCTURE/null:y,project/LIMIT/1/2026:y,svod/svod1_MAIN:y,svod/projects_summaries:y,svod/svod2_GO:y,svod/svod2_GO:filter' \
npx playwright test scenarios/hotspots.spec.ts
PERF_RUN_ID=20261026-hot PERF_SQLSTATS=0 PERF_HOT_MODE=cpu PERF_WEB=http://localhost:8080 PERF_HOT_CYCLES=15 \
PERF_HOT_SHEET2=FORM_4/STRUCTURE/null PERF_HOT_TARGETS='FORM_2/AHR/null:cssgrowth' npx playwright test scenarios/hotspots.spec.ts
PERF_RUN_ID=20261026-hot PERF_SQLSTATS=0 PERF_WEB=http://localhost:8081 PERF_PROFILING=1 PERF_PRIORITY=2 PERF_REPEATS=1 \
PERF_MODALS='dict_vsp_*' npx playwright test scenarios/modals.spec.ts # рендеры модалок справочника ВСП
cd ..
.venv/bin/python analysis/cpuprofile_top.py results/20261026-hot --maps .build/prod-sourcemap --out report-data/20261026-s1/analysis
.venv/bin/python analysis/hotspots_summary.py results/20261026-hot --out report-data/20261026-s1/analysis
```
- `hotspots.spec.ts`: `PERF_HOT_TARGETS` - `<target>:<action>` (target как в scroll.spec без оси; action `y`, `x`, `filter`,
`cssgrowth`), `PERF_HOT_MODE` (`cpu` - `artifacts/hot-*.cpuprofile`; `react` - рендеры по компонентам), `PERF_HOT_STEPS` (40 шагов
вперёд и столько же назад), `PERF_HOT_FILTER_MS` (30 с наблюдения за мутациями DOM после ввода фильтра), `PERF_HOT_CYCLES`,
`PERF_HOT_SHEET2`, `PERF_HOT_ROLE`. Записи - `hotspot_cpu.jsonl` / `hotspot_react.jsonl`; под профайлером абсолютное время выше,
сравнивать доли.
- `cpuprofile_top.py` декодирует source maps сам (VLQ, без зависимостей): self time по функциям и пакетам, «ближайший кадр
`web/src`» в стеке каждого сэмпла (кто в коде приложения вызвал работу библиотек) и inclusive time функций `web/src`.
- Имена выходных файлов без «trace» (`timeline-breakdown.*`): шаблон `.gitignore` `perf/**/*trace*.json` исключает трассы.
- py-spy (`perf/requirements.txt`) на macOS требует root - не запускается без sudo; вместо него `analysis/api_split.py`.