- REPORT: правки FORM_4 — вывод по коду, не замер (I-1); цена фильтра свода по записям основного прогона, hotspots — верхняя граница (I-2); М6 поднята до P1, пояснение состава топ-5 (I-4); формулировки кадров, малых листов, admin-контроля, М8, время прогона (M-1, M-2, M-9, M-11); раздел 6 дополнен, новый раздел 6а с находками при подготовке стенда (M-10, M-12, T7.3) - README: контроль 4x с отдельным тегом и предупреждением (I-3), команды modal-renders и netlog-sizes (M-4), гипотезы в «Как читать» (T8.7) - design.md: R-7, правило приоритетов и топ-5, решения волны исправлений - план: заметка о замене CPU [1,4,6] решением человека (M-12) Refs #202 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
34 KiB
Профилирование фронта и бэка — план исполнения
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Воспроизводимый стенд в perf/ и отчёт perf/REPORT.md с доказанными проблемными местами по скроллу, модалкам, памяти и бэку (issue #202).
Architecture:
- Postgres 16 в docker (с
pg_stat_statements), бэк —uvicornизapi/в одном процессе. - Prod-сборка фронта собирается из
web/собственным конфигомperf/web/rsbuild.perf.config.tsвperf/.build/и раздаётсяrsbuild previewна :8080 с прокси/api(включая WS) на :8000. - Python-пакет
perf/perfkitгенерирует данные через HTTP-API, гоняет бота-соавтора и замеры бэка; Playwright-проектperf/e2eснимает метрики фронта через CDP. - Все сценарии пишут записи единого формата в
perf/results/<run_id>/, агрегатор сводит их вsummary.jsonи markdown-таблицы для отчёта.
Tech Stack: Python 3.12 (httpx, asyncpg, websockets, argon2-cffi, pytest), Node 24 + @playwright/test (Chromium), docker compose, Postgres 16, Rsbuild (из web/node_modules).
Spec: openspec/changes/perf-profiling/design.md (+ proposal.md)
Global Constraints
- Не менять
api/src,api/alembic,web/src,web/rsbuild.config.ts,web/package.json,api/requirements*.txt,api/web. Всё новое — только вperf/и строки в корневом.gitignore. - Не исправлять код приложения, даже очевидные баги: находка идёт в отчёт.
- Фронт мерить только на prod-сборке (
rsbuild build), режимы CPU —[1, 4, 6]черезEmulation.setCPUThrottlingRate.- Заменено решением человека 2026-10-25: основные замеры без троттлинга (CPU 1×); CPU 4× — только контрольный прогон топ-3–4 находок, 1 повтор (
openspec/changes/perf-profiling/design.md, «Decisions made with the human»). Упоминания[1,4,6]и CPU 6 ниже по плану — исходная редакция.
- Заменено решением человека 2026-10-25: основные замеры без троттлинга (CPU 1×); CPU 4× — только контрольный прогон топ-3–4 находок, 1 повтор (
- Бэк и Postgres — без лимитов CPU и памяти. В отчёте пометка: «быстрое железо, прод ≈ 1 vCPU / 4 GB».
- Postgres 16, схема
v3, миграции —cd api && alembic upgrade head, бэк сDEBUG=true,DATABASE_URL=postgresql+asyncpg://perf:perf@localhost:55432/perf. - Порты: Postgres 55432, API 8000, фронт 8080.
- Основная роль замеров — EXECUTOR_RF (role_id 3) с открытым окном этапа на правку. Контрольная — ADMIN (1), только скролл на масштабе 1.
- Масштабы данных:
1(базовый) и2(удвоенный). - Long task — задача длиннее 50 мс (
PerformanceObserverтипаlongtask). Тяжёлый кадр — интервал междуrequestAnimationFrameбольше 50 мс. - Сырые артефакты (
*.jsontrace,*.heapsnapshot,perf/results/**,perf/.build/,perf/.venv/,perf/e2e/node_modules/) — в.gitignore. Коммитятся код стенда,perf/README.md,perf/REPORT.mdиperf/report-data/<run_id>/summary.json. - Отчёт и README — на русском.
- Каждая цифра отчёта получается командой из
perf/README.md.
Review Focus
- Prod-сборка с прокси: WS
/api/v1/ws/...через :8080 должен работать; иначе таблица не загрузится, а сценарии «зелёные» на пустой странице. Проверяется smoke в Task 1 и ассертом «в таблице ≥ N видимых строк» перед каждым замером в Task 5. - Окно этапа закрыто на дату замера: EXECUTOR_RF видит лист read-only, и замер идёт не на той конфигурации. Генератор ставит окна
opens_at = now - 1 day,closes_at = now + 30 days; verify в Task 3 проверяет это. - Скролл не дошёл до конца (виртуализация или ленивая подгрузка): сценарий обязан проверить, что
scrollTop + clientHeight ≥ scrollHeight - 2в конце, иначе запись получает"incomplete": true. Тест в Task 5. - Перекрёстное влияние прогонов (кэш браузера, открытые WS, накопленная память): каждый замер скролла и модалок — в новом browser context. Проверяется в Task 4 тестом изоляции контекста.
- Неверный токен в WS-боте: бот молча не шлёт правки, и сценарий 1a измеряет «без соавтора». Бот обязан дождаться эха
cell_updatedхотя бы на первую правку, иначе падает. Тест в Task 6.
Task 1: Стенд — Postgres, миграции, админ, бэк, prod-сборка фронта с прокси
Модель: sonnet
Files:
- Create:
perf/docker-compose.yml— сервисpgизpostgres:16, порт55432:5432,POSTGRES_USER/PASSWORD/DB=perf,command: postgres -c shared_preload_libraries=pg_stat_statements -c pg_stat_statements.track=all, volumeperf-pg. - Create:
perf/stand.sh— подкомандыup | migrate | bootstrap | api | web-build | web | smoke | down | all. - Create:
perf/stand/bootstrap.py—CREATE EXTENSION IF NOT EXISTS pg_stat_statements; админadmin/admin(admin@perf.local, role_id 1, argon2-хеш); идемпотентно. - Create:
perf/stand/smoke.py. - Create:
perf/web/rsbuild.perf.config.ts—mergeRsbuildConfigповерхweb/rsbuild.config.ts:output.distPath.root=perf/.build/prodилиperf/.build/profilingприPERF_PROFILING=1; приPERF_PROFILING=1aliasreact-dom$→react-dom/profiling;server.port 8080,server.proxy/api→http://localhost:8000сws: true. - Create:
perf/requirements.txt— httpx, asyncpg, websockets, argon2-cffi, pytest. - Create:
perf/README.md— раздел «Стенд». - Modify:
.gitignore— строки из Global Constraints.
Interfaces:
-
Produces:
perf/stand.sh all— поднимает всё, бэк и фронт в фоне, PID вperf/.run/*.pid, логи вperf/.run/*.log.- venv
perf/.venv(зависимостиapi/requirements-dev.txt+perf/requirements.txt). - Переменные
PERF_API=http://localhost:8000,PERF_WEB=http://localhost:8080,PERF_DB_DSN=postgresql://perf:perf@localhost:55432/perf. - Админ
admin/admin.
-
Step 1: Написать
perf/stand/smoke.py, который проверяет:GET $PERF_API/healthz→ 200;POST $PERF_API/api/v1/auth/login {"username":"admin","password":"admin"}→ 200 иaccess_token;GET $PERF_WEB/→ 200, HTML содержит<script;GET $PERF_WEB/api/v1/users/meс Bearer → 200 (прокси);- WS
ws://localhost:8080/api/v1/ws/exportоткрывается (прокси WS).
Печатает
SMOKE OKили первую ошибку и выходит с кодом 1. -
Step 2: Прогнать
perf/stand.sh smokeдо создания стенда Expected: FAIL (соединение отклонено). -
Step 3: Реализовать compose,
stand.sh,bootstrap.py,rsbuild.perf.config.tsweb-build:cd web && npm ci && npx rsbuild build -c ../perf/web/rsbuild.perf.config.ts, затем то же сPERF_PROFILING=1.web:npx rsbuild preview -c ../perf/web/rsbuild.perf.config.ts.api:cd api && DATABASE_URL=… DEBUG=true ../perf/.venv/bin/python run_server.py.
-
Step 4: Прогнать
perf/stand.sh all && perf/stand.sh smokeExpected:SMOKE OK.git statusне показывает изменений внеperf/и.gitignore. -
Step 5: Commit —
perf: стенд для профилирования (postgres, api, prod-сборка фронта).
Task 2: perfkit — формат манифеста и результатов, агрегатор
Модель: sonnet
Files:
- Create:
perf/perfkit/__init__.py,perf/perfkit/manifest.py,perf/perfkit/results.py,perf/perfkit/aggregate.py. - Test:
perf/tests/test_manifest.py,perf/tests/test_aggregate.py,perf/pytest.ini(pythonpath = .).
Interfaces:
-
Produces:
perfkit.manifest: dataclassesSheet(form_id:int, form_type:str, sheet:str, direction:str|None, year:int, url:str, rows:int),ProjectReport(project_id:int, year:int, report_type:str, url:str, rows:int),Page(name:str, url:str),User(username:str, password:str, role_id:int),Manifest(scale:int, users:dict[str,User], sheets:list[Sheet], project_reports:list[ProjectReport], svods:list[Page], modal_pages:list[Page]);load(path) -> Manifest,dump(m, path). Путь:perf/results/manifest-s{scale}.json. Ключиusers:"admin","rf","rf_coauthor","dfip". URL листа —/table/form/{form_id}/form-type/{form_type}/{sheet}/{direction or "null"}/{year}. Реальное значение сегмента для «нет direction» исполнитель берёт изweb/srcи фиксирует в коде.perfkit.results: запись результата (одна JSON-строка) —{"run_id","scenario","target","role","scale","cpu_rate","repeat","metrics":{…},"artifacts":[…],"incomplete":bool,"ts"}. Функцияappend(run_dir, record)пишет вperf/results/<run_id>/<scenario>.jsonl. Node-сторона (Task 4) пишет тот же формат.perfkit.aggregate:summarize(run_dir) -> dictгруппирует по(scenario, target, role, scale, cpu_rate)и считает для каждой числовой метрикиmedian,p95,min,max,n. CLI:python -m perfkit.aggregate <run_dir>пишетsummary.jsonиsummary.mdвperf/report-data/<run_id>/.
-
Step 1: Тесты
test_manifest_roundtrip— dump и load дают равный объект.test_summarize_median_p95: метрикаlong_task_msсо значениями[10,20,30,40,100]→median == 30,p95 == 100(nearest-rank),n == 5.test_summarize_groups_by_cpu_rate— записи сcpu_rate1 и 6 дают две группы.test_incomplete_records_counted_separately—incomplete:trueне попадает в статистику; в группе естьincomplete_n.
-
Step 2:
cd perf && .venv/bin/pytest tests -q→ FAIL (нет модулей). -
Step 3: Реализовать модули по интерфейсам.
-
Step 4:
cd perf && .venv/bin/pytest tests -q→ PASS. -
Step 5: Commit —
perf: формат манифеста и результатов, агрегатор.
Task 3: Генератор данных через HTTP-API
Модель: sonnet
Files:
- Create:
perf/perfkit/gen/__init__.py,perf/perfkit/gen/plan.py,perf/perfkit/gen/run.py(CLIpython -m perfkit.gen.run --scale N),perf/perfkit/gen/verify.py(CLIpython -m perfkit.gen.verify --scale N). - Test:
perf/tests/test_gen_plan.py.
Interfaces:
- Consumes: стенд (Task 1),
perfkit.manifest(Task 2). - Produces:
plan_for_scale(scale:int) -> GenPlan(dataclass с полямиorg_units_ssp:int,org_units_rf:int,vsp_per_rf:int,extra_lines_per_sheet:int,projects_per_rf:int,fill_ratio:float).- Манифест
perf/results/manifest-s{scale}.json.
Генератор всё создаёт через API под admin, в таком порядке:
- оргединицы (
POST /api/v1/org-unit/); - пользователи
rf,rf_coauthor(role 3, оба на одну РФ) иdfip(role 2) с паролемperf(POST /api/v1/users/,PUT /users/dfip-many-ssp/); - ВСП (
POST /api/v1/dict/info); - формы FORM_1, FORM_2, FORM_4 за 2026 год по оргединицам (
POST /api/v1/form/) — строки из шаблона создаёт сама БД; - дополнительные строки (
POST /form/{id}/sheet/{sheet}/line); - заполнение ячеек долей
fill_ratio(PATCH .../cellsпачками); - этапы с окнами
opens_at = now - 1 day,closes_at = now + 30 daysдля ролей EXECUTOR_RF и EXECUTOR_DFIP на всех листах (POST /api/v1/stages/form/{id}); - проекты РФ с годами и отчётами (FORM_3) и этапы проектов.
Значения plan_for_scale(1) исполнитель выбирает так, чтобы самый большой лист у rf имел ≥ 1000 строк, а своды были заполнены по всем оргединицам. Обоснование — комментарием в plan.py, оно попадёт в отчёт.
-
Step 1: Тесты
test_plan_scale2_doubles_volume: уplan_for_scale(2)поляorg_units_rf,vsp_per_rf,extra_lines_per_sheet,projects_per_rfравны удвоенным значениям изplan_for_scale(1),fill_ratioтот же.test_plan_rejects_zero_scale—ValueError.
-
Step 2: pytest → FAIL.
-
Step 3: Реализовать
plan.pyиrun.py. Повторный запуск на непустой БД падает с понятным сообщением «сначала perf/stand.sh reset». Для этого в Task 1 нужна подкомандаreset: drop volume + up + migrate + bootstrap; добавить её в этом таске. -
Step 4: Реализовать
verify.py. Он проверяет в БД (asyncpg,PERF_DB_DSN):- число форм, строк и проектов соответствует плану;
- у
rfсамый большой лист ≥ 1000 строк; - на каждом листе из манифеста есть этап с
opens_at < now() < closes_atдля роли EXECUTOR_RF; - каждый URL манифеста отвечает 200 через
$PERF_WEB.
Печатает
VERIFY OK. -
Step 5:
perf/stand.sh reset && .venv/bin/python -m perfkit.gen.run --scale 1 && .venv/bin/python -m perfkit.gen.verify --scale 1→VERIFY OK; pytest → PASS. -
Step 6: Commit —
perf: генератор данных через API.
Task 4: Каркас замеров фронта (Playwright + CDP)
Модель: sonnet
Files:
- Create:
perf/e2e/package.json(@playwright/test, точная версия),perf/e2e/playwright.config.ts(Chromium,workers: 1,fullyParallel: false, без ретраев, timeout 10 мин). - Create:
perf/e2e/lib/manifest.ts— чтение манифеста Task 2. - Create:
perf/e2e/lib/auth.ts—loginAs(context, user): токен через API, затем вlocalStorageпод ключами, которые читаетweb/src/app/context/AuthProvider.jsx; ключи исполнитель берёт оттуда. - Create:
perf/e2e/lib/cdp.ts—throttle(page, rate),startTrace(page),stopTrace(page, path),gc(page),domCounters(page),heapUsed(page),heapSnapshot(page, path). - Create:
perf/e2e/lib/probe.ts— init-script, встраиваемый в страницу:PerformanceObserver('longtask')и счётчик кадров черезrequestAnimationFrame;window.__perf.start()/window.__perf.stop()возвращают{long_task_n, long_task_ms, frame_n, heavy_frame_n, frame_p50_ms, frame_p95_ms, duration_ms}. - Create:
perf/e2e/lib/results.ts—append(record)в формате Task 2.run_idизPERF_RUN_ID, по умолчаниюYYYYMMDD-HHMM. - Create:
perf/e2e/fixtures/busy.html— страница, которая по кнопке крутит синхронный цикл 120 мс. - Test:
perf/e2e/tests/harness.spec.ts.
Interfaces:
-
Produces: перечисленные функции с этими именами. Каждая следующая e2e-задача импортирует их из
../lib/*. -
Step 1: Тесты в
harness.spec.tsprobe detects long task: наbusy.htmlнажатие кнопки междуstartиstopдаётlong_task_n >= 1иlong_task_ms >= 100.throttle slows script: время того же цикла приthrottle(page, 4)≥ 2× времени приthrottle(page, 1).fresh context isolation:localStorage, выставленный в одном контексте, не виден в новом.results format:appendпишет строку, которуюperfkit.resultsиз Task 2 читает (проверка черезpython -m perfkit.aggregateна временном каталоге даётn == 1).
-
Step 2:
cd perf/e2e && npx playwright test tests/harness.spec.ts→ FAIL. -
Step 3: Реализовать
lib/*. -
Step 4: Тот же прогон → PASS.
-
Step 5: Commit —
perf: каркас замеров фронта.
Task 5: Сценарии скролла — листы форм, отчёты проектов, своды
Модель: sonnet
Files:
- Create:
perf/e2e/lib/scroll.ts—scrollThrough(page, containerSelector, axis: 'y'|'x', stepPx=200) -> {complete: boolean, steps: number}: колесо мыши шагами по 200 px, после каждого шага ждатьrequestAnimationFrame, вниз до конца и обратно.complete— правило из Review Focus. - Create:
perf/e2e/scenarios/scroll.spec.ts. - Test:
perf/e2e/tests/scroll-lib.spec.tsнаperf/e2e/fixtures/long-list.html(div 20 000 px).
Interfaces:
- Consumes: Task 4 lib, манифест (
sheets,project_reports,svods). - Produces: записи
scenario: "scroll",target—"{form_type}/{sheet}/{direction}","project/{report_type}"или"svod/{name}"; метрики probe плюсaxis,rows_visible_before,json_bytes_sheet(размер ответа загрузки листа из сетевого лога). Trace на каждый первый повтор — вartifacts.
Матрица:
- роль
rf× все листы и отчёты проектов × оси y и x × CPU[1,4,6]× 3 повтора; - роль
admin× листы × ось y × CPU[1,4,6]× 1 повтор, только масштаб 1; - своды (свод 1 по каждому
sheet, свод 2 GO и RF,/projects/summaries) подadminиdfip× ось y × CPU[1,4,6]× 3 повтора, плюс одна смена фильтра на CPU 6 с замером probe от клика до стабилизации (2 кадра без long task).
Перед замером: дождаться ≥ 20 видимых строк таблицы, иначе тест падает (Review Focus). Селектор контейнера скролла исполнитель берёт из DOM material-react-table.
- Step 1: Тесты
scroll-lib.spec.ts: наlong-list.htmlscrollThroughдаётcomplete === true; на странице, где контейнер обрезан до 5000 px приmaxSteps = 10, —complete === false. - Step 2: Прогон → FAIL.
- Step 3: Реализовать
scroll.tsиscroll.spec.ts. Сценарий параметризуетсяPERF_SCALEи фильтромPERF_TARGETS(glob поtarget) для быстрых прогонов. - Step 4:
npx playwright test tests/scroll-lib.spec.ts→ PASS.PERF_SCALE=1 PERF_TARGETS='FORM_1/*' npx playwright test scenarios/scroll.spec.ts→ записи безincomplete;python -m perfkit.aggregateстроит таблицу. - Step 5: Commit —
perf: сценарии скролла.
Task 6: Бот-соавтор и сценарий «скролл при соавторе»
Модель: sonnet
Files:
- Create:
perf/perfkit/coauthor.py— CLIpython -m perfkit.coauthor --form-id N --sheet S [--direction D] --rate 1|5 --duration SEC. - Create:
perf/e2e/scenarios/scroll-coauthor.spec.ts. - Test:
perf/tests/test_coauthor.py(интеграционный, помечен@pytest.mark.stand; вpytest.ini—markers = stand: нужен поднятый стенд).
Interfaces:
- Consumes: манифест (
users.rf_coauthor,sheets), Task 4/5 lib. - Produces: записи
scenario: "scroll_coauthor",metrics+coauthor_rate,coauthor_sent,coauthor_echoed.
Протокол — api/src/api/v1/websocket.py:
- подключение
ws://localhost:8000/api/v1/ws/form/{id}/sheet/{sheet}?direction=; - первое сообщение
{"event":"user_login","data":{"token":…}}; - правка — событие
cell_updated(форматdataвзять из обработчика__update_cellи из того, что шлёт фронт вweb/src/components/RealtimeTable/hooks/useWebSocket.js).
Бот правит случайные редактируемые ячейки листа в колонках, открытых этапом для EXECUTOR_RF. Перед правкой шлёт cell_edit_start, после — cell_edit_end.
- Step 1: Тест
test_coauthor_first_edit_echoed: бот с--rate 1 --duration 3на первом листе FORM_1 получает эхоcell_updatedсвоей первой правки и завершается кодом 0. С неверным токеном завершается кодом ≠ 0 за ≤ 5 с (Review Focus). - Step 2:
.venv/bin/pytest -m stand tests/test_coauthor.py→ FAIL. - Step 3: Реализовать бота и сценарий:
- бот стартует как дочерний процесс на время скролла;
- роль
rf, масштаб 1, CPU 6, rate 1 и 5, 3 повтора; - цели — самый большой лист каждой формы.
- Step 4: Тест → PASS. Сценарий на FORM_1 даёт записи с
coauthor_echoed > 0. - Step 5: Commit —
perf: бот-соавтор и скролл при соавторе.
Task 7: Модалки — перечень, время открытия, что перерисовывается
Модель: sonnet
Files:
- Create:
perf/e2e/modals.json— перечень модалок:{id, page: "sheet"|"task"|"project"|"projects"|"admin_*"|"svod", source: "web/src/...:line", open: {selector, action}, priority: 1|2}. Источник — поиск поweb/src(Dialog,Modal,common/Modal) по страницам форм, задачи и проекта (priority 1) и админки (priority 2). Для модалки без воспроизводимого пути открытия ставится"skip_reason". - Create:
perf/e2e/lib/modal.ts—measureOpen(page, opener) -> {click_to_visible_ms, long_task_n, long_task_ms}: отметкаperformance.now()в обработчикеpointerdown(capture);MutationObserverловит появление видимого[role=dialog]; конец — следующийrequestAnimationFrameпосле появления. - Create:
perf/e2e/lib/reactProfile.ts— для profiling-сборки: через__REACT_DEVTOOLS_GLOBAL_HOOK__(init-script с минимальным хукомonCommitFiberRoot) считает число зафиксированных fiber-рендеров поdisplayNameза окно замера. Возвращает топ-20{component, renders, self_ms}. - Create:
perf/e2e/scenarios/modals.spec.ts. - Test:
perf/e2e/tests/modal-lib.spec.tsнаfixtures/modal.html: кнопка открываетrole=dialogчерезsetTimeout(150).
Interfaces:
- Consumes: Task 4 lib, манифест (
modal_pages,sheets). - Produces: записи
scenario: "modal",target: modal.id, метрикиmeasureOpen; для profiling-прогона —scenario: "modal_renders",metrics.renders_top(массив). Кроме того, сводная таблицаperf/report-data/<run_id>/modals-inventory.md(id, source, priority, skip_reason).
Матрица:
-
priority 1 × роль
rf(admin, если модалка доступна только ему) × CPU[1,4,6]× 5 повторов; -
модалки страницы листа — дополнительно при открытом самом большом листе;
-
priority 2 — CPU 6 × 3 повтора;
-
profiling-сборка (сервер на
PERF_WEB_PROFILING,perf/stand.sh web-profilingна :8081) — каждая модалка priority 1 × CPU 1 × 1 повтор. -
Step 1: Тест:
measureOpenнаmodal.htmlдаётclick_to_visible_msв диапазоне[150, 400]; на кнопке без модалки падает по таймауту 5 с с сообщением, где есть id модалки. -
Step 2: Прогон → FAIL.
-
Step 3: Составить
modals.json. Реализовать lib, сценарий и подкомандуweb-profilingвstand.sh. -
Step 4: Тест → PASS. Сценарий на priority 1 при CPU 6 даёт записи для всех модалок без
skip_reason. -
Step 5: Commit —
perf: замеры открытия модалок.
Task 8: Память при длительной работе
Модель: sonnet
Files:
- Create:
perf/e2e/scenarios/memory.spec.ts. - Create:
perf/perfkit/memtrend.py—slope_per_cycle(values: list[float]) -> float(наклон МНК); CLI печатает наклон поheap_used_mb,nodes,js_event_listenersиз записейmemory. - Test:
perf/tests/test_memtrend.py.
Interfaces:
- Consumes: Task 4 lib, манифест. Правки — через UI (двойной клик по ячейке и ввод числа), чтобы шли через фронтовый WS.
- Produces: записи
scenario: "memory", по одной на цикл:metrics: {cycle, heap_used_mb, documents, nodes, js_event_listeners, ws_open}.ws_open— счётчик открытых WebSocket, его ведёт init-script с обёрткой надwindow.WebSocket. Heap snapshots на циклах 0, 10, 20 — вartifacts.
Один цикл:
- открыть самый большой лист FORM_1;
- проскроллить вниз;
- отредактировать 3 ячейки через UI;
- перейти на
/tasks; - открыть самый большой лист FORM_2;
- проскроллить;
- вернуться на
/tasks; gc, затем снять метрики.
Всего 20 циклов, роль rf, CPU 1 (память от троттлинга не зависит), масштаб 1, одна SPA-сессия без перезагрузки страницы.
- Step 1: Тесты:
slope_per_cycle([10,11,12,13]) == 1.0;slope_per_cycle([5,5,5]) == 0.0; меньше двух точек →ValueError. - Step 2: pytest → FAIL.
- Step 3: Реализовать
memtrend.pyи сценарий. - Step 4: pytest → PASS. Сценарий даёт 21 запись (цикл 0 — базовая точка) и 3 heap snapshot.
python -m perfkit.memtrend <run_dir>печатает три наклона. - Step 5: Commit —
perf: сценарий памяти.
Task 9: Замеры бэка — запросы сценариев, pg_stat_statements, EXPLAIN
Модель: sonnet
Files:
- Modify:
perf/e2e/lib/results.ts— сохранять сетевой лог (page.on('requestfinished'): метод, путь, статус,timing, размер тела) вperf/results/<run_id>/network.jsonlво всех сценариях Task 5–8 (через общую фикстуру). - Create:
perf/perfkit/bench.py— CLIpython -m perfkit.bench <run_dir> --repeats 20: берёт уникальныеGETиPATCH/POST(только идемпотентные PATCH ячеек с тем же значением) изnetwork.jsonl, повторяет их через httpx под той же ролью. Пишет записиscenario: "api",target: "<METHOD> <path-template>", метрикиmsиbytes. Шаблон пути — числа заменены на{id}. - Create:
perf/perfkit/sqlstats.py—reset()иdump(run_dir, top=15): топ поtotal_exec_timeиmean_exec_timeизpg_stat_statementsвsql_top.json. Для топ-5 поtotal_exec_time, где текст — вызовv3.*-функции или SELECT, выполняетсяEXPLAIN (ANALYZE, BUFFERS, FORMAT JSON)с параметрами из сетевого лога, результат вexplain/<n>.json. DML не EXPLAIN'ится. - Test:
perf/tests/test_bench.py.
Interfaces:
-
Consumes: сетевой лог, манифест (пользователи),
PERF_DB_DSN. -
Produces:
api-записи,sql_top.json,explain/*.jsonв каталоге прогона. -
Step 1: Тесты:
test_path_template:"/api/v1/form/123/sheet/AHR/cells"→"/api/v1/form/{id}/sheet/AHR/cells";test_select_requests_skips_non_idempotent:POST .../lineне попадает в набор повторов.
-
Step 2: pytest → FAIL.
-
Step 3: Реализовать.
sqlstats.reset()вызывается вglobalSetupPlaywright,dump— вglobalTeardown. -
Step 4: pytest → PASS. После прогона сценария скролла по FORM_1 есть
network.jsonl,sql_top.jsonи ≥ 1explain/*.json;benchпишетapi-записи. -
Step 5: Commit —
perf: замеры бэка.
Task 10: Полные прогоны, статический разбор, отчёт
Модель: opus
Files:
- Create:
perf/run-all.sh— масштаб 1: reset + gen + все сценарии + bench + memtrend + aggregate; то же для масштаба 2 (без admin-контроля и profiling-прогона). - Create:
perf/REPORT.md,perf/report-data/<run_id>/summary.json,summary.md,modals-inventory.md. - Modify:
perf/README.md— полный порядок: стенд, данные, прогоны, где результаты.
Interfaces:
-
Consumes: всё выше.
-
Step 1:
perf/run-all.shпроходит целиком на масштабах 1 и 2. Записей сincompleteнет, а если есть — в отчёте раздел «не проверено» с причиной. -
Step 2: Разбор:
- traces первых повторов: топ функций по self time в long tasks при скролле;
- render counts модалок;
- retainers растущих объектов в heap snapshots (циклы 10 и 20);
sql_topи EXPLAIN;- сравнение масштабов 1 и 2;
- для эндпоинтов, где медиана
api.msбольше суммарного времени их SQL изsql_top.jsonв 2 и более раза, — профильpy-spy recordбэка на повтореbenchпо этому эндпоинту (py-spyставится вperf/.venv).
Каждую горячую точку свести к
file:lineвweb/srcилиapi/. -
Step 3: Статический разбор кода по найденным точкам: нестабильные значения контекстов, пересоздание колонок и конфигов на рендер, слушатели, таймеры и WS без очистки,
console.logв горячих обработчиках. Пометка «подтверждено замером: да/нет». -
Step 4: Написать
perf/REPORT.mdпо структуре дизайна (резюме и топ-5, методика и окружение, включая версию Chromium и оговорки про Яндекс.Браузер и прод-ресурсы, обоснование масштаба изplan.py, таблица мест, подробности, не проверено). Каждая цифра — со ссылкой на файл вperf/report-data/<run_id>/. -
Step 5: Проверка: каждая строка таблицы мест имеет
file:line, доказательство и приоритет;git statusчист внеperf/и.gitignore; вapi/src,web/src,api/alembicнет изменений (git diff origin/test --stat -- api/src web/src api/alembicпуст). -
Step 6: Commit —
perf: отчёт о проблемах производительности.