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

35 KiB
Raw Blame History

Профилирование: 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
  1. Разбор на живой БД масштаба 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() - не запускать параллельно с замерами.

  1. Горячие точки фронта (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.