perf: исправления run-all по ревью (раунд 1)
- C1: trace только на повторе 0 при числе повторов > 1 (lib/trace.ts shouldTrace) в scroll и scroll-coauthor: admin-контроль, control4x и PERF_S2_REPEATS=1 больше не теряют единственную запись (traced=1). - I1: масштаб 2 - PERF_LOAD_TIMEOUT_MS=40 мин и PERF_TEST_TIMEOUT_MS=90 мин, если не заданы. - I2: шаг aggregate падает при сбое perfkit.aggregate/копирования и если summary.json не создан; тесты run-all (perf/tests/test_run_all.py). - M2: после modals_profiling останавливается preview :8081. - README, design.md: решения раунда. Refs #202 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
ab3d9e89a3
commit
05d1586006
@ -177,7 +177,7 @@
|
||||
| 2026-10-26 | SQL-статистика по шагам: `globalTeardown` пишет `sql_top.json`/`explain/` в `<run_dir>/sql/<PERF_SQLSTATS_TAG>/` (`sqlstats dump --out`); run-all задаёт тег на каждый вызов Playwright (scroll, coauthor, modals, modals_profiling, memory, control4x_*). Параметры EXPLAIN берутся из общего `<run_dir>/network.jsonl` | Каждый вызов Playwright делает `pg_stat_statements_reset()` в globalSetup и dump в teardown: в одном run_id они перезатирали бы друг друга; общий dump на весь масштаб смешал бы сценарии и захватил бы SQL grow (add_line FORM_4 O(n)) | Для сравнения «весь масштаб» T10b складывает несколько sql_top; параметры EXPLAIN шага могут прийти из запроса другого сценария (берётся самый долгий подходящий) |
|
||||
| 2026-10-26 | `netlog.ts`: тело не-GET для путей с `/auth/` не пишется (`post_data: null`), из остальных JSON/form-urlencoded тел вырезаются поля `password`/`passwd`/`pass`/`*token*`/`*secret*`; неразбираемое тело с таким словом - null | Ревью T9 (I-a): логин и пароль попадали в `network.jsonl`; bench повторяет только GET и идемпотентный PATCH ячеек, тела логина ему не нужны | Поле с «token» в имени у ячейки потерялось бы в логе (сейчас таких нет) |
|
||||
| 2026-10-26 | Имена целей скролла: лист не-rf владельца с тем же именем, что у rf-листа, получает суффикс владельца (`FORM_2/AHR/null/ssp`); отчёт проекта - `project/<report_type>/<project_id>/<year>` (было без года) | Найдено при проверке `--list`: FORM_2 есть у ССП-01 и РФ-01, отчёты проекта за 2026 и 2027 одного типа - Playwright отказывался запускать `scroll.spec.ts` целиком из-за одинаковых названий тестов, а записи смешались бы в одну группу | Другие имена target, чем в прогонах t5-*; glob `project/LIMIT/1` больше не совпадает (нужен `project/LIMIT/1/*`) |
|
||||
| 2026-10-26 | `PERF_TRACE=0` в scroll и scroll-coauthor отключает trace на повторе 0; run-all ставит его при 1 повторе (control4x, `PERF_S2_REPEATS=1`) | aggregate исключает `traced=1` (O-9): при единственном повторе с трейсом в статистике не осталось бы ни одной записи | На таких прогонах нет trace-файла для разбора |
|
||||
| 2026-10-26 | Trace в scroll и scroll-coauthor снимается только на повторе 0 и только если повторов > 1 (`lib/trace.ts` `shouldTrace`); `PERF_TRACE=0` отключает trace совсем (run-all дополнительно ставит его при 1 повторе). Modals и memory trace не снимают | aggregate исключает `traced=1` (O-9): при единственном повторе (admin-контроль - 61 тест на масштабе 1, control4x, `PERF_S2_REPEATS=1`) группа осталась бы без метрик (ревью T10a, C1) | Нет trace-файлов admin-контроля и одноповторных прогонов; для разбора трасс есть rf/своды с 3 повторами |
|
||||
| 2026-10-26 | `perf/run-all.sh`: канонический порядок reset -> gen -> verify -> stand -> preflight -> scroll -> coauthor -> grow -> modals -> modals_profiling -> memory -> memtrend -> [control4x] -> bench -> aggregate; run_id `<PERF_RUN_TAG>-s<scale>` (тег по умолчанию - дата); фатальны только reset, gen и stand (остаток масштаба SKIP, следующий масштаб идёт); verify не фатален | Порядок - R-2/T7 (grow после скролла и соавтора); verify может споткнуться о допуски масштаба 1 на масштабе 2 (T3.1), а данные уже есть | Прогон на «плохих» данных при ложно-нефатальном verify - видно по FAIL в status.json |
|
||||
| 2026-10-26 | Шаг stand проверяет, что `:8080` отдаёт именно prod-сборку (`index.html` совпадает с `perf/.build/prod/index.html`), поднимает api/web при необходимости и гоняет smoke; шаг preflight делает `playwright test --list` всех сценариев масштаба | Профилирующая сборка на :8080 исказила бы основные цифры; ошибки манифеста и дубли названий ловятся за секунды, а не через часы | Ложный FAIL при пересборке prod без перезапуска preview |
|
||||
| 2026-10-26 | Профилирующий прогон модалок (только масштаб 1) - `PERF_PRIORITY=1` (все цели приоритета 1, включая @big), 1 повтор, `:8081` (`stand.sh web-profiling`); модалки на обоих масштабах - `PERF_PRIORITY=1,2` одним вызовом (p1 x5, p2 x3) | Счётчики рендеров нужны для форм-модалок и страниц задач/проектов (p1); p2 - админка, вторым приоритетом | Нет render counts для модалок админки |
|
||||
@ -186,6 +186,9 @@
|
||||
| 2026-10-26 | В `perf/report-data/<run_id>/` (коммитится) шаг aggregate кладёт summary.json/md, modals-inventory.md, `sql/<шаг>/{sql_top.json,explain/}`, memtrend.txt, manifest/gen-stats/grow масштаба и run-all.status.json; traces, heap snapshots и сырые jsonl остаются в `perf/results/` | Каждая цифра REPORT.md должна ссылаться на файл в report-data (дизайн); sql_top ~110 КБ на шаг | Рост репозитория на ~1-2 МБ |
|
||||
| 2026-10-26 | `sqlstats dump` берёт пользователей из манифеста `PERF_SCALE` (было всегда s1) | Масштаб 2 - свой манифест | Нет (логины одинаковые) |
|
||||
| 2026-10-26 | R-4: на масштабе 2 повторы сокращены до 1 (`PERF_S2_REPEATS=1`), масштаб 1 — 3 повтора | Оценка прогона масштаба 2 с 3 повторами ≈ 13 ч, с 1 повтором ≈ 6,3 ч; правило человека: при > ~6 ч сокращать повторы только на масштабе 2 | Масштаб 2 без разброса и p95: годится для оценки роста с объёмом, не для абсолютных цифр |
|
||||
| 2026-10-26 | Масштаб 2: run-all передаёт сценариям `PERF_LOAD_TIMEOUT_MS=2400000` (40 мин) и `PERF_TEST_TIMEOUT_MS=5400000` (90 мин), если пользователь не задал свои; свод 2 по-прежнему `PERF_TEST_TIMEOUT_SVOD2_MS` 3 ч | Ответ свода 2 на масштабе 2 ~150-190 МБ (x4 от 31-47 МБ): 15 мин ожидания дали бы `incomplete`; 45 мин на тест меньше 40 мин загрузки + скролл (ревью T10a, I1) | Зависший замер обнаружится позже (до 90 мин) |
|
||||
| 2026-10-26 | Шаг aggregate строгий: цепочка `&&`/`|| exit 1`, перед запуском удаляется прежний `report-data/<run_id>/summary.json`, в конце `test -f summary.json`; `PERF_PY` подменяет python perfkit (только для тестов run-all, `perf/tests/test_run_all.py`), статус пишется всегда venv-питоном | Иначе rc шага определял `ls -R`, и падение `perfkit.aggregate` при возобновлении (каталог report-data уже есть) маскировалось (ревью T10a, I2) | FAIL шага при сбое копирования необязательного файла |
|
||||
| 2026-10-26 | После шага modals_profiling run-all останавливает preview профилирующей сборки на :8081 (по `perf/.run/web-profiling.pid`), :8080 и :8000 не трогает; код возврата - Playwright | Профилирующий preview не нужен остальным шагам и держит память/CPU во время memory и bench (ревью T10a, M2) | Ручной запуск профилирования после run-all требует снова `stand.sh web-profiling` |
|
||||
|
||||
## Decisions made with the human
|
||||
|
||||
|
||||
@ -65,9 +65,9 @@ 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 на повторе 0).
|
||||
- Переменные: `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 исключает его из статистики (поэтому при 1 повторе нужен `PERF_TRACE=0`). Trace первого повтора - в `perf/results/<run_id>/artifacts/` (30-70 МБ каждый, в git не попадают).
|
||||
- Метрики: `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`.
|
||||
|
||||
### Скролл при соавторе
|
||||
@ -77,7 +77,7 @@ cd .. && .venv/bin/python -m perfkit.aggregate results/s1
|
||||
```
|
||||
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 PERF_TRACE=0 npx playwright test scenarios/scroll-coauthor.spec.ts
|
||||
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 # самопроверка бота (нужен стенд)
|
||||
```
|
||||
|
||||
@ -165,7 +165,7 @@ nohup perf/run-all.sh > /tmp/run-all.out 2>&1 & # боевой прогон (м
|
||||
| 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 | ~3 мин | — |
|
||||
| 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 мин на цель | |
|
||||
@ -173,7 +173,7 @@ nohup perf/run-all.sh > /tmp/run-all.out 2>&1 & # боевой прогон (м
|
||||
| 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.
|
||||
- Лог `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` передаются как есть.
|
||||
|
||||
@ -186,7 +186,7 @@ PERF_SCALES=2 PERF_RUN_TAG=20261026 PERF_SKIP_RESET=1 PERF_STEPS=scroll,grow,mod
|
||||
|
||||
- `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_S2_REPEATS` (по умолчанию 3, как на масштабе 1; при 1 trace не снимается) и `PERF_S2_TARGETS` (glob целей скролла) — только для скролла масштаба 2. Оценка: 3 повтора ~13 ч, 2 — ~9,6 ч, 1 — ~6,3 ч; основная часть — свод 2 (строк ~в 4 раза больше, ответ ~150-190 МБ, при `PERF_LOAD_TIMEOUT_MS` 15 мин возможны `incomplete`).
|
||||
- Масштаб 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 повтор, цифры в отчёте помечаются как контрольные):
|
||||
|
||||
@ -195,6 +195,6 @@ PERF_SCALES=1 PERF_RUN_TAG=20261026 PERF_SKIP_RESET=1 PERF_STEPS=control4x,aggre
|
||||
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 повтор, `PERF_TRACE=0`; записи идут в тот же run_id (группы с `cpu_rate=4`), SQL — в `sql/control4x_*`.
|
||||
- `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.
|
||||
|
||||
|
||||
11
perf/e2e/lib/trace.ts
Normal file
11
perf/e2e/lib/trace.ts
Normal file
@ -0,0 +1,11 @@
|
||||
/**
|
||||
* Трассировать ли замер (scroll, scroll-coauthor). Trace снимается только на повторе 0 и только если повторов > 1:
|
||||
* трассированная запись помечается traced=1 и aggregate её исключает (O-9), поэтому при единственном повторе
|
||||
* (admin-контроль, control4x, PERF_S2_REPEATS=1) группа осталась бы без данных. Смена фильтра свода не трассируется.
|
||||
* PERF_TRACE=0 отключает trace полностью.
|
||||
*/
|
||||
export interface TraceOpts { repeat: number; repeats: number; filter?: boolean }
|
||||
|
||||
export function shouldTrace(o: TraceOpts, env: Record<string, string | undefined> = process.env): boolean {
|
||||
return o.repeat === 0 && o.repeats > 1 && !o.filter && env.PERF_TRACE !== '0';
|
||||
}
|
||||
@ -4,7 +4,7 @@
|
||||
*
|
||||
* Параметры (env): PERF_SCALE, PERF_TARGETS (glob по `<form_type>/<sheet>/<direction|null>`), PERF_RUN_ID,
|
||||
* PERF_REPEATS (3), PERF_CPU_RATES ('1'; троттлинг отключён решением человека, PERF_CPU_RATES=6 включает вручную), PERF_COAUTHOR_RATES ('1,5'), PERF_LOAD_TIMEOUT_MS, PERF_MAX_STEPS,
|
||||
* PERF_TEST_TIMEOUT_MS (45 мин), PERF_PYTHON (по умолчанию perf/.venv/bin/python), PERF_TRACE=0 (без trace на повторе 0).
|
||||
* PERF_TEST_TIMEOUT_MS (45 мин), PERF_PYTHON (по умолчанию perf/.venv/bin/python), PERF_TRACE=0 (без trace; по умолчанию trace на повторе 0, если повторов > 1).
|
||||
* Цели: самый большой лист каждой формы из доступных роли rf (owner=rf).
|
||||
* Запись не пишется (тест падает), если бот не получил эха своих правок или страница не увидела ни одного cell_updated.
|
||||
*/
|
||||
@ -18,6 +18,7 @@ import { throttle, startTrace, stopTrace } from '../lib/cdp';
|
||||
import { attachNetlog } from '../lib/netlog';
|
||||
import { append, runDir } from '../lib/results';
|
||||
import { scrollThrough } from '../lib/scroll';
|
||||
import { shouldTrace } from '../lib/trace';
|
||||
|
||||
const CONTAINER = '.MuiTableContainer-root';
|
||||
const MIN_ROWS = 20;
|
||||
@ -113,7 +114,7 @@ async function measure(browser: import('@playwright/test').Browser, s: Sheet, cp
|
||||
await throttle(page, cpu);
|
||||
|
||||
bot = await startBot(s, rate);
|
||||
const trace = repeat === 0 && process.env.PERF_TRACE !== '0'; // PERF_TRACE=0 - без трейса (1 повтор)
|
||||
const trace = shouldTrace({ repeat, repeats: REPEATS }); // повтор 0 при REPEATS > 1; PERF_TRACE=0 - никогда
|
||||
const traceFile = path.join(runDir(), 'artifacts', `${safe(targetName)}-rf-cpu${cpu}.trace.json`);
|
||||
if (trace) await startTrace(page);
|
||||
const pr = await startProbe(page);
|
||||
|
||||
@ -13,7 +13,8 @@
|
||||
* PERF_LOAD_TIMEOUT_MS ожидание данных после навигации (по умолчанию 900000)
|
||||
* PERF_MAX_STEPS лимит шагов прямого прохода (по умолчанию 5000)
|
||||
* PERF_TEST_TIMEOUT_MS / PERF_TEST_TIMEOUT_SVOD2_MS таймаут теста (45 мин / 3 ч для свода 2)
|
||||
* PERF_TRACE=0 не снимать trace на повторе 0 (по умолчанию снимается; запись traced=1 aggregate исключает)
|
||||
* PERF_TRACE=0 не снимать trace вообще (по умолчанию - на повторе 0, если повторов > 1: запись traced=1 aggregate
|
||||
* исключает, поэтому при 1 повторе, как у admin-контроля, trace не снимается)
|
||||
*
|
||||
* Основная метрика: main_thread_ms (CDP TaskDuration за скролл) и main_thread_ms_per_step (на шаг колеса 200 px);
|
||||
* для смены фильтра - main_thread_ms на действие (шагов нет).
|
||||
@ -27,6 +28,7 @@ import { throttle, startTrace, stopTrace } from '../lib/cdp';
|
||||
import { attachNetlog } from '../lib/netlog';
|
||||
import { append, runDir } from '../lib/results';
|
||||
import { scrollThrough, type Axis } from '../lib/scroll';
|
||||
import { shouldTrace } from '../lib/trace';
|
||||
|
||||
const CONTAINER = '.MuiTableContainer-root'; // MRT: muiTableContainerProps (RealtimeTable.jsx), SummaryTable, Svod2Page
|
||||
const MIN_ROWS = 20;
|
||||
@ -156,10 +158,10 @@ const signature = (page: Page) =>
|
||||
|
||||
const safe = (s: string) => s.replace(/[^\w.-]+/g, '_');
|
||||
|
||||
interface RunOpts { t: Target; role: string; axis: Axis; cpu: number; repeat: number; filter?: boolean }
|
||||
interface RunOpts { t: Target; role: string; axis: Axis; cpu: number; repeat: number; repeats: number; filter?: boolean }
|
||||
|
||||
async function measure(browser: import('@playwright/test').Browser, o: RunOpts) {
|
||||
const { t, role, axis, cpu, repeat, filter } = o;
|
||||
const { t, role, axis, cpu, repeat, repeats: nRepeats, filter } = o;
|
||||
const m = loadManifest();
|
||||
const user: User = m.users[role];
|
||||
const targetName = (filter ? `${t.name}/filter` : t.name) + `@${axis}`;
|
||||
@ -211,8 +213,8 @@ async function measure(browser: import('@playwright/test').Browser, o: RunOpts)
|
||||
await net.send('Network.disable').catch(() => {});
|
||||
await throttle(page, cpu);
|
||||
|
||||
// PERF_TRACE=0 - без трейса (1 повтор: control4x, PERF_S2_REPEATS=1), иначе единственная запись была бы traced=1
|
||||
const trace = repeat === 0 && !filter && process.env.PERF_TRACE !== '0';
|
||||
// trace только на повторе 0 при повторах > 1 (lib/trace.ts): при одном повторе запись traced=1 была бы единственной
|
||||
const trace = shouldTrace({ repeat, repeats: nRepeats, filter });
|
||||
const traceFile = path.join(runDir(), 'artifacts', `${safe(targetName)}-${role}-cpu${cpu}.trace.json`);
|
||||
if (trace) await startTrace(page);
|
||||
let complete = true, steps = 0, probe: ProbeResult, stable = true, reason: string | undefined, effect = true;
|
||||
@ -267,7 +269,7 @@ test.describe('scroll', () => {
|
||||
for (let repeat = 0; repeat < ra.repeats; repeat++) {
|
||||
test(`${t.name} ${ra.role} ${axis} cpu${cpu} #${repeat}`, async ({ browser }) => {
|
||||
test.setTimeout(testTimeout(t));
|
||||
await measure(browser, { t, role: ra.role, axis, cpu, repeat });
|
||||
await measure(browser, { t, role: ra.role, axis, cpu, repeat, repeats: ra.repeats });
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -275,7 +277,7 @@ test.describe('scroll', () => {
|
||||
if (t.kind !== 'sheet' && t.kind !== 'project') {
|
||||
test(`${t.name} ${ra.role} filter cpu${FILTER_CPU}`, async ({ browser }) => {
|
||||
test.setTimeout(testTimeout(t));
|
||||
await measure(browser, { t, role: ra.role, axis, cpu: FILTER_CPU, repeat: 0, filter: true });
|
||||
await measure(browser, { t, role: ra.role, axis, cpu: FILTER_CPU, repeat: 0, repeats: 1, filter: true });
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -6,6 +6,7 @@ import path from 'node:path';
|
||||
import { PROBE_SCRIPT, startProbe } from '../lib/probe';
|
||||
import { throttle } from '../lib/cdp';
|
||||
import { append } from '../lib/results';
|
||||
import { shouldTrace } from '../lib/trace';
|
||||
|
||||
const BUSY = 'file://' + path.resolve(__dirname, '../fixtures/busy.html');
|
||||
const PERF_DIR = path.resolve(__dirname, '../..');
|
||||
@ -102,3 +103,12 @@ test('globalTeardown: PERF_SQLSTATS_TAG -> dump --out <run_dir>/sql/<tag>', asyn
|
||||
delete process.env.PERF_RESULTS_DIR;
|
||||
}
|
||||
});
|
||||
|
||||
test('shouldTrace: trace только на повторе 0 и только если повторов > 1 (иначе aggregate исключил бы единственную запись)', () => {
|
||||
expect(shouldTrace({ repeat: 0, repeats: 3 }, {})).toBe(true);
|
||||
expect(shouldTrace({ repeat: 1, repeats: 3 }, {})).toBe(false);
|
||||
expect(shouldTrace({ repeat: 0, repeats: 1 }, {})).toBe(false); // admin-контроль, control4x, PERF_S2_REPEATS=1
|
||||
expect(shouldTrace({ repeat: 0, repeats: 2 }, {})).toBe(true);
|
||||
expect(shouldTrace({ repeat: 0, repeats: 3, filter: true }, {})).toBe(false); // смена фильтра не трассируется
|
||||
expect(shouldTrace({ repeat: 0, repeats: 3 }, { PERF_TRACE: '0' })).toBe(false);
|
||||
});
|
||||
|
||||
@ -32,7 +32,8 @@ set -uo pipefail
|
||||
|
||||
PERF="$(cd "$(dirname "$0")" && pwd)"
|
||||
E2E="$PERF/e2e"
|
||||
PY="$PERF/.venv/bin/python"
|
||||
PY="${PERF_PY:-$PERF/.venv/bin/python}" # python для perfkit (подмена - только тесты run-all)
|
||||
STATUS_PY="$PERF/.venv/bin/python"
|
||||
WEB_URL="http://localhost:8080"
|
||||
PROF_URL="http://localhost:8081"
|
||||
API_URL="${PERF_API:-http://localhost:8000}"
|
||||
@ -91,7 +92,10 @@ hours() { echo "$(( $1 / 60 )).$(( ($1 % 60) * 10 / 60 ))"; }
|
||||
ts() { date '+%Y-%m-%dT%H:%M:%S%z'; }
|
||||
|
||||
pw() { # "<env assignments>" <spec> -> строка команды Playwright
|
||||
echo "cd '$E2E' && $CLEAN_ENV PERF_SCALE=$SCALE PERF_RUN_ID=$RID PERF_WEB=${3:-$WEB_URL} $1 npx playwright test $2"
|
||||
# масштаб 2: свод 2 ~150-190 МБ - ожидание загрузки 40 мин и таймаут замера 90 мин, если не заданы пользователем
|
||||
local s2=""
|
||||
[ "$SCALE" = 2 ] && s2="PERF_LOAD_TIMEOUT_MS=${PERF_LOAD_TIMEOUT_MS:-2400000} PERF_TEST_TIMEOUT_MS=${PERF_TEST_TIMEOUT_MS:-5400000} "
|
||||
echo "cd '$E2E' && $CLEAN_ENV PERF_SCALE=$SCALE PERF_RUN_ID=$RID PERF_WEB=${3:-$WEB_URL} $s2$1 npx playwright test $2"
|
||||
}
|
||||
|
||||
# Команда шага (строка для eval). Пустая строка - шаг не применим (SKIP).
|
||||
@ -126,7 +130,10 @@ step_cmd() {
|
||||
grow) echo "cd '$PERF' && '$PY' -m perfkit.gen.grow --scale $SCALE" ;;
|
||||
modals) pw "PERF_SQLSTATS_TAG=modals PERF_PRIORITY=1,2" scenarios/modals.spec.ts ;;
|
||||
modals_profiling)
|
||||
echo "'$PERF/stand.sh' web-profiling && $(pw "PERF_SQLSTATS_TAG=modals_profiling PERF_PROFILING=1 PERF_PRIORITY=1" scenarios/modals.spec.ts "$PROF_URL")" ;;
|
||||
# после прогона профилирующий preview на :8081 останавливается (prod :8080 и api :8000 не трогаются)
|
||||
local pid="$PERF/.run/web-profiling.pid"
|
||||
echo "rc=0; '$PERF/stand.sh' web-profiling && ( $(pw "PERF_SQLSTATS_TAG=modals_profiling PERF_PROFILING=1 PERF_PRIORITY=1" scenarios/modals.spec.ts "$PROF_URL") ) || rc=\$?;" \
|
||||
"if [ -f '$pid' ]; then pkill -P \$(cat '$pid') 2>/dev/null; kill \$(cat '$pid') 2>/dev/null; rm -f '$pid'; fi; exit \$rc" ;;
|
||||
memory) pw "PERF_SQLSTATS_TAG=memory" scenarios/memory.spec.ts ;;
|
||||
memtrend) echo "cd '$PERF' && '$PY' -m perfkit.memtrend 'results/$RID' | tee 'results/$RID/memtrend.txt'" ;;
|
||||
control4x) control_cmd ;;
|
||||
@ -135,13 +142,16 @@ step_cmd() {
|
||||
# в report-data/<run_id>/ (коммитится): summary.json/md, modals-inventory.md, sql/<шаг>/{sql_top.json,explain/},
|
||||
# memtrend.txt, manifest/gen-stats/grow масштаба, run-all.status.json. Сырое (traces, snapshots, jsonl) - в results/.
|
||||
local out="$PERF/report-data/$RID"
|
||||
echo "cd '$PERF' && '$PY' -m perfkit.aggregate 'results/$RID'" \
|
||||
# Любой сбой - FAIL шага: цепочка через && и || exit 1, в конце проверка, что summary.json создан этим шагом
|
||||
# (каталог report-data при возобновлении может остаться от прошлого прогона, поэтому старый summary.json удаляется).
|
||||
echo "cd '$PERF' && rm -f '$out/summary.json' && '$PY' -m perfkit.aggregate 'results/$RID'" \
|
||||
"&& { [ ! -f 'results/$RID/modal.jsonl' ] || '$PY' -m perfkit.modals_inventory 'results/$RID'; }" \
|
||||
"&& mkdir -p '$out' && for d in results/$RID/sql/*/; do [ -d \"\$d\" ] || continue; t=\$(basename \"\$d\");" \
|
||||
"mkdir -p '$out/sql/'\$t && cp \"\$d\"sql_top.json '$out/sql/'\$t/ 2>/dev/null;" \
|
||||
"[ -d \"\$d\"explain ] && cp -R \"\$d\"explain '$out/sql/'\$t/; done;" \
|
||||
"for f in results/$RID/memtrend.txt results/manifest-s$SCALE.json results/gen-stats-s$SCALE.json results/grow-s$SCALE.json results/$RID/run-all.status.json;" \
|
||||
"do [ -f \"\$f\" ] && cp \"\$f\" '$out/'; done; ls -R '$out'" ;;
|
||||
"&& mkdir -p '$out' && for d in results/$RID/sql/*/; do if [ -d \"\$d\" ]; then t=\$(basename \"\$d\");" \
|
||||
"mkdir -p '$out/sql/'\$t || exit 1; if [ -f \"\$d\"sql_top.json ]; then cp \"\$d\"sql_top.json '$out/sql/'\$t/ || exit 1; fi;" \
|
||||
"if [ -d \"\$d\"explain ]; then cp -R \"\$d\"explain '$out/sql/'\$t/ || exit 1; fi; fi; done" \
|
||||
"&& for f in results/$RID/memtrend.txt results/manifest-s$SCALE.json results/gen-stats-s$SCALE.json results/grow-s$SCALE.json results/$RID/run-all.status.json;" \
|
||||
"do if [ -f \"\$f\" ]; then cp \"\$f\" '$out/' || exit 1; fi; done" \
|
||||
"&& ls -R '$out' && test -f '$out/summary.json'" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
@ -169,7 +179,7 @@ control_cmd() {
|
||||
}
|
||||
|
||||
status_update() { # step status rc start end cmd
|
||||
"$PY" - "$STATUS" "$RID" "$SCALE" "$@" <<'EOF'
|
||||
"$STATUS_PY" - "$STATUS" "$RID" "$SCALE" "$@" <<'EOF'
|
||||
import json, sys, pathlib
|
||||
f, rid, scale, step, status, rc, start, end, dur, cmd = sys.argv[1:]
|
||||
p = pathlib.Path(f)
|
||||
|
||||
88
perf/tests/test_run_all.py
Normal file
88
perf/tests/test_run_all.py
Normal file
@ -0,0 +1,88 @@
|
||||
"""run-all.sh: сухой режим и шаг aggregate (стенд не нужен: aggregate работает только с файлами)."""
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import uuid
|
||||
|
||||
import pytest
|
||||
|
||||
from perfkit import PERF_DIR, REPORT_DATA_DIR, RESULTS_DIR
|
||||
|
||||
SCRIPT = PERF_DIR / "run-all.sh"
|
||||
|
||||
|
||||
def _run(**env):
|
||||
e = {k: v for k, v in os.environ.items() if not k.startswith("PERF_")}
|
||||
e.update(env)
|
||||
return subprocess.run(["bash", str(SCRIPT)], env=e, capture_output=True, text=True, timeout=120, stdin=subprocess.DEVNULL)
|
||||
|
||||
|
||||
def _step_line(out: str, scale: int, step: str) -> str:
|
||||
block = out.split(f"=== масштаб {scale},", 1)[1].split("ИТОГО масштаб", 1)[0]
|
||||
return next(l for l in block.splitlines() if l.strip().startswith(step + " "))
|
||||
|
||||
|
||||
def test_dry_run_plan_and_s2_load_timeout():
|
||||
p = _run(PERF_DRY_RUN="1", PERF_RUN_TAG="t")
|
||||
assert p.returncode == 0, p.stderr
|
||||
assert "ИТОГО масштаб 1" in p.stdout and "ИТОГО масштаб 2" in p.stdout
|
||||
# свод 2 на масштабе 2 (~150-190 МБ): ожидание загрузки 40 мин, если не задано пользователем
|
||||
assert "PERF_LOAD_TIMEOUT_MS=2400000" in _step_line(p.stdout, 2, "scroll")
|
||||
assert "PERF_LOAD_TIMEOUT_MS" not in _step_line(p.stdout, 1, "scroll")
|
||||
p2 = _run(PERF_DRY_RUN="1", PERF_RUN_TAG="t", PERF_SCALES="2", PERF_LOAD_TIMEOUT_MS="123")
|
||||
assert "PERF_LOAD_TIMEOUT_MS=123" in _step_line(p2.stdout, 2, "scroll")
|
||||
|
||||
|
||||
def test_dry_run_modals_profiling_stops_web_profiling():
|
||||
p = _run(PERF_DRY_RUN="1", PERF_RUN_TAG="t", PERF_SCALES="1", PERF_STEPS="modals_profiling")
|
||||
line = _step_line(p.stdout, 1, "modals_profiling")
|
||||
assert "web-profiling.pid" in line and "kill" in line
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def run_id():
|
||||
rid = f"pytest-runall-{uuid.uuid4().hex[:8]}"
|
||||
yield rid
|
||||
shutil.rmtree(RESULTS_DIR / rid, ignore_errors=True)
|
||||
shutil.rmtree(REPORT_DATA_DIR / rid, ignore_errors=True)
|
||||
|
||||
|
||||
def _write(rid: str, text: str):
|
||||
d = RESULTS_DIR / rid
|
||||
d.mkdir(parents=True)
|
||||
(d / "scroll.jsonl").write_text(text, encoding="utf-8")
|
||||
|
||||
|
||||
def _status(rid: str) -> dict:
|
||||
return json.loads((RESULTS_DIR / rid / "run-all.status.json").read_text(encoding="utf-8"))["steps"]
|
||||
|
||||
|
||||
def test_aggregate_step_fails_when_aggregate_fails(run_id):
|
||||
_write(run_id, "не json\n")
|
||||
(REPORT_DATA_DIR / run_id).mkdir(parents=True) # возобновление: каталог report-data уже есть от прошлого aggregate
|
||||
p = _run(PERF_SCALES="1", PERF_SKIP_RESET="1", PERF_STEPS="aggregate", PERF_RUN_ID_S1=run_id)
|
||||
assert p.returncode == 1, p.stdout
|
||||
assert _status(run_id)["aggregate"]["status"] == "FAIL"
|
||||
|
||||
|
||||
def test_aggregate_step_ok_and_summary_in_report_data(run_id):
|
||||
rec = {"run_id": run_id, "scenario": "scroll", "target": "x@y", "role": "rf", "scale": 1, "cpu_rate": 1, "repeat": 1,
|
||||
"metrics": {"main_thread_ms": 10.0}, "artifacts": [], "incomplete": False, "ts": "x"}
|
||||
_write(run_id, json.dumps(rec) + "\n")
|
||||
p = _run(PERF_SCALES="1", PERF_SKIP_RESET="1", PERF_STEPS="aggregate", PERF_RUN_ID_S1=run_id)
|
||||
assert p.returncode == 0, p.stdout
|
||||
assert _status(run_id)["aggregate"]["status"] == "OK"
|
||||
assert (REPORT_DATA_DIR / run_id / "summary.json").exists()
|
||||
|
||||
|
||||
def test_aggregate_step_fails_without_summary(run_id, monkeypatch):
|
||||
"""Если aggregate не создал summary.json (например, отработал «пусто» не туда), шаг - FAIL."""
|
||||
_write(run_id, "")
|
||||
(REPORT_DATA_DIR / run_id).mkdir(parents=True)
|
||||
fake = RESULTS_DIR / run_id / "fakepy"
|
||||
fake.write_text("#!/bin/sh\nexit 0\n")
|
||||
fake.chmod(0o755)
|
||||
p = _run(PERF_SCALES="1", PERF_SKIP_RESET="1", PERF_STEPS="aggregate", PERF_RUN_ID_S1=run_id, PERF_PY=str(fake))
|
||||
assert p.returncode == 1, p.stdout
|
||||
assert _status(run_id)["aggregate"]["status"] == "FAIL"
|
||||
Loading…
x
Reference in New Issue
Block a user