264 lines
35 KiB
Markdown
264 lines
35 KiB
Markdown
# Профилирование: 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`.
|