Сетевой лог сценариев, bench запросов, pg_stat_statements и EXPLAIN, aggregate исключает трассировочные повторы (O-9). Refs #202 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Профилирование: 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 | smoke | reset | down | all.
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 # ~20-40 мин, пишет 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),probe(long tasks > 50 мс, кадры rAF; тяжёлый кадр — интервал > 50 мс),results.append,manifest.loadManifest.
Сценарии скролла
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
- Матрица: роль
rfx листы сowner=rfи отчёты проектов x оси y,x x CPU 1 (без троттлинга, решение человека) x 3 повтора;adminx все листы x ось y x CPU x 1 повтор (только масштаб 1);adminиdfipx своды (svod1_MAIN/FORM_1..4, svod2_GO/RF, projects_summaries) x ось y x CPU x 3 повтора + одна смена фильтра на CPU 6 (target.../filter). - target:
FORM_x/<sheet>/<direction|null>,project/<report_type>/<project_id>,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 мин + скролл на CPU 6 порядка 40 мин). - Каждый замер - новый browser context; CPU-троттлинг включается после загрузки данных (до скролла). Перед замером ждём >= 20 строк
tbody tr; если не дождались - записьincomplete: trueсskip_reason. - Метрики: probe (long tasks, кадры),
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), Task 10 может исключать его из статистики. Trace первого повтора - вperf/results/<run_id>/artifacts/(30-70 МБ каждый, в git не попадают). - Сайт свода не читает
#hashиз URL: вкладка (FORM_1,GO...) выбирается кликом поrole=tab.
Модалки
Перечень - 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 мин. С
PERF_CPU_RATES=1,4,6priority 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(CDPMemory.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, 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=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> # вручную, если нужно перегенерировать
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; число исключённых печатается в заголовке группы.