diff --git a/perf/README.md b/perf/README.md index 184974d..ba107c6 100644 --- a/perf/README.md +++ b/perf/README.md @@ -268,3 +268,51 @@ cd .. `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`. + +## Свод 2 после AURORA-1843 + +Замер «до/после» свода 2 (серверная постраничная загрузка, компактный ответ, варианты фильтров). Цели и критерии: `openspec/changes/aurora-1843-svod2-server-paging/design.md`, раздел «Замер до/после». + +**Порядок на одних данных** (масштаб 1, затем масштаб 2): + +1. `perf/stand.sh reset`, `perfkit.gen.run --scale 1`, `perfkit.gen.verify --scale 1`. Своды мерить до `grow`. +2. Скопировать `results/manifest-s1.json` в оба worktree («до» из AURORA-1842, «после» из ветки PR 3). +3. «До»: поднять `api`, `web-build`, `web` в worktree «до», снять замеры (команды ниже). +4. Остановить сервисы. +5. «После»: `alembic upgrade head`, затем `api`, `web-build`, `web`; перед замером выполнить сверку (ниже); снять замеры. +6. То же на масштабе 2 (`PERF_S2_REPEATS=1`, см. «Полный прогон»). + +Между «до» и «после» не вливать AURORA-1917 и AURORA-1836: на стенде не должно быть других изменений кода. + +**Сверка перед замером «после»** (только чтение, на данных стенда): + +``` +cd perf +.venv/bin/python analysis/svod2_paging_check.py --scale 1 # склейка страниц, total, проекция compact +node analysis/svod2_search_oracle.mjs --check --api http://localhost:8000 \ + --username --password <пароль> --year <год из манифеста> # поиск и варианты фильтров; CHECK_OK +``` + +Код выхода 1 или `CHECK_FAILED` означает расхождение с экраном «до»: замер «после» не снимать, сначала чинить код. + +**Команды замера** (из `perf/e2e`, затем `perfkit.aggregate`; `` и `` подставить по этапу и масштабу): + +| что | команда | время на сторону (оценка) | +|---|---|---| +| скролл, время до первых строк | `PERF_RUN_ID=1843--s PERF_TARGETS='svod/svod2_*' PERF_ROLES=dfip PERF_TRACE=0 PERF_REPEATS=2 npx playwright test scenarios/scroll.spec.ts --grep-invert filter`; на масштабе 2 ещё `PERF_MAX_STEPS=10000` | s1: «до» ~35 мин, «после» ~15 мин; s2 ×3–4 | +| фильтр | тот же запуск с `--grep filter` | ~5 мин | +| память | `PERF_RUN_ID=1843-mem-<…>-s PERF_ROLES=dfip PERF_REPEATS=3 [PERF_SVOD2_MEM_SCROLL=1 на s1] npx playwright test scenarios/svod2-memory.spec.ts` | s1 ~10 мин, s2 ~20–30 мин | +| байты и время API | `.venv/bin/python analysis/api_split.py --scale ` | ~3–5 мин | +| планы SQL | `.venv/bin/python analysis/explain_manual.py --scale --out results/1843-explain-s` | ~3 мин | +| время API по URL (bench) | `.venv/bin/python -m perfkit.bench results/ --repeats 10 --only 'GET /api/v1/svod/2/'` | зависит от числа URL | + +Скрипты `api_split.py` и `explain_manual.py` работают на живом стенде (порты 8000 и БД `perf`); `scale_compare.py` читает уже снятые прогоны (`--s1 --s2 `). + +**Ограничение `perfkit.bench`.** Без фильтра бенч повторяет все уникальные GET прогона. Для свода 2 в одиночку: + +- `--only REGEX` — регулярное выражение по строке `METHOD url` (например `'GET /api/v1/svod/2/'`, или `'svod/2/RF.*facets'` для вариантов фильтров); +- `--repeats N` — число повторов после прогрева (по умолчанию 20; для быстрой проверки достаточно 3–5, для замера цифр в PR — 10–20). + +**Сравнение масштабов и разбор.** `scale_compare.py` выводит строки свода 2 тремя ключами: `GET свод 2 <лист>` (полный ответ), `GET свод 2 <лист> страница` (`limit=` в query), `GET свод 2 <лист> facets` (варианты фильтров). + +**Обновить цифры в PR** (`feat/236-aurora-1843-svod2-server-paging-perf`): PR открываются без цифр; цифры дописываются по готовности из `report-data//analysis/`. diff --git a/perf/analysis/api_split.py b/perf/analysis/api_split.py index 4753277..035fede 100644 --- a/perf/analysis/api_split.py +++ b/perf/analysis/api_split.py @@ -50,6 +50,13 @@ def main(argv=None): ("dfip", "свод 1 MAIN", f"/api/v1/svod/1/MAIN/{year}"), ("dfip", "свод 2 GO (все организации)", f"/api/v1/svod/2/GO/{year}"), ("dfip", "свод 2 RF (все организации)", f"/api/v1/svod/2/RF/{year}"), + ("dfip", "свод 2 GO полный, страница 300", f"/api/v1/svod/2/GO/{year}?view=full&offset=0&limit=300"), + ("dfip", "свод 2 RF полный, страница 300", f"/api/v1/svod/2/RF/{year}?view=full&offset=0&limit=300"), + ("dfip", "свод 2 GO compact, страница 300", f"/api/v1/svod/2/GO/{year}?view=compact&offset=0&limit=300"), + ("dfip", "свод 2 RF compact, страница 300", f"/api/v1/svod/2/RF/{year}?view=compact&offset=0&limit=300"), + ("dfip", "свод 2 GO варианты фильтров", f"/api/v1/svod/2/GO/{year}/facets"), + ("dfip", "свод 2 RF варианты фильтров", f"/api/v1/svod/2/RF/{year}/facets"), + ("dfip", "свод 2 RF compact, страница 300, поиск 1", f"/api/v1/svod/2/RF/{year}?view=compact&offset=0&limit=300&search=1"), ("dfip", "сводные проектов", f"/api/v1/projects/plan-summary?year={year}"), ] tokens = {} diff --git a/perf/analysis/explain_manual.py b/perf/analysis/explain_manual.py index 8a9bde3..478c38d 100644 --- a/perf/analysis/explain_manual.py +++ b/perf/analysis/explain_manual.py @@ -56,7 +56,8 @@ def subst(sql: str, form_id: int, sheet: str) -> str: return sql -def queries(f2: int, f4: int) -> list[dict]: +def queries(f2: int, f4: int, year: int) -> list[dict]: + svod2_variants = [("", {}), (".jit_off", {"jit": "off"})] return [ {"name": "form2_ahr_tw", "what": "FORM_2/AHR: тело v3.v_form2_sheet_sections (запрос «WITH tw», дерево статей + агрегаты)", "src": "api/alembic/versions/sql/0033_form2_old_sort.sql:500-1155", @@ -78,6 +79,22 @@ def queries(f2: int, f4: int) -> list[dict]: "src": "api/src/repository (sheet_repository)", "sql": f"SELECT row_type, depth, sort_order, data FROM v3.v_form_view({f4}, 'CAP', NULL::text[], NULL::varchar)", "variants": [("", {}), (".jit_off", {"jit": "off"})], "user": True}, + {"name": "svod2_go_page", "what": "свод 2 GO: страница v3.v_svod2_go (300 строк, сжатый ответ, сортировка org_name)", + "src": "api/alembic/versions/sql/0053_svod2_server_paging.sql (v3.v_svod2_go)", + "sql": f"SELECT * FROM v3.v_svod2_go({year}, NULL, 0, 300, 'org_name', 'asc', NULL, NULL, NULL, NULL, true)", + "variants": svod2_variants, "user": True}, + {"name": "svod2_rf_page", "what": "свод 2 RF: страница v3.v_svod2_rf (300 строк, сжатый ответ, сортировка org_name)", + "src": "api/alembic/versions/sql/0053_svod2_server_paging.sql (v3.v_svod2_rf)", + "sql": f"SELECT * FROM v3.v_svod2_rf({year}, NULL, 0, 300, 'org_name', 'asc', NULL, NULL, NULL, NULL, true)", + "variants": svod2_variants, "user": True}, + {"name": "svod2_rf_search", "what": "свод 2 RF: страница с поиском '1' (v3.v_svod2_rf, отбор svod2_lines по тексту)", + "src": "api/alembic/versions/sql/0053_svod2_server_paging.sql (v3.v_svod2_rf, v3.svod2_lines)", + "sql": f"SELECT * FROM v3.v_svod2_rf({year}, NULL, 0, 300, 'org_name', 'asc', '1', NULL, NULL, NULL, true)", + "variants": svod2_variants, "user": True}, + {"name": "svod2_go_facets", "what": "свод 2 GO: варианты фильтров v3.v_svod2_facets (без поиска и фильтров)", + "src": "api/alembic/versions/sql/0053_svod2_server_paging.sql (v3.v_svod2_facets)", + "sql": f"SELECT * FROM v3.v_svod2_facets('GO', {year}, NULL, NULL, NULL, NULL, NULL)", + "variants": svod2_variants, "user": True}, ] @@ -122,7 +139,8 @@ async def run(dsn: str, scale: int, out: Path): md = ["# EXPLAIN (ANALYZE, BUFFERS) вложенных запросов (explain_manual.py)", "", f"Postgres {pg}, work_mem по умолчанию {wm}; масштаб {scale}; FORM_2/AHR form_id={f2}, FORM_4/CAP form_id={f4}; " f"app.user_id={uid} (perf_rf). Каждый запрос - в транзакции с ROLLBACK.", ""] - for q in queries(f2, f4): + year = next(s["year"] for s in rf if s["form_type"] == "FORM_2" and s["sheet"] == "AHR") + for q in queries(f2, f4, year): sql = q["sql"] if sql is None: sql = subst(func_body(*q["func_body"]), q["form_id"], q["sheet"]) diff --git a/perf/analysis/scale_compare.py b/perf/analysis/scale_compare.py index 4ec56b4..48e8d51 100644 --- a/perf/analysis/scale_compare.py +++ b/perf/analysis/scale_compare.py @@ -30,6 +30,17 @@ def med(g, k): return m["median"] if m else None +def svod2_key(path: str) -> str: + """Ключ свода 2 в таблице API: полный ответ, страница (limit в query) или варианты фильтров (/facets).""" + m = re.match(r"/api/v1/svod/2/(\w+)/", path) + base = "GET свод 2 " + m.group(1) + if "/facets" in path: + return base + " facets" + if "limit=" in path: + return base + " страница" + return base + + def api_by_sheet(run: str, scale: int) -> dict: man = json.loads((PERF / "report-data" / run / f"manifest-s{scale}.json").read_text()) ft = {s["form_id"]: s["form_type"] for s in man["sheets"]} @@ -41,7 +52,7 @@ def api_by_sheet(run: str, scale: int) -> dict: if m: key = f"GET лист {ft.get(int(m.group(1)), '?')}/{m.group(2)}" elif re.match(r"/api/v1/svod/2/(\w+)/", path): - key = "GET свод 2 " + re.match(r"/api/v1/svod/2/(\w+)/", path).group(1) + key = svod2_key(path) elif re.match(r"/api/v1/svod/1/(\w+)/", path): key = "GET свод 1 " + re.match(r"/api/v1/svod/1/(\w+)/", path).group(1) elif "plan-summary" in path: diff --git a/perf/tests/test_svod2_analysis.py b/perf/tests/test_svod2_analysis.py new file mode 100644 index 0000000..ef38a69 --- /dev/null +++ b/perf/tests/test_svod2_analysis.py @@ -0,0 +1,19 @@ +"""Самопроверка разборщиков свода 2 в perf/analysis (без стенда и БД).""" +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "analysis")) + +import explain_manual # noqa: E402 +import scale_compare # noqa: E402 + + +def test_svod2_key(): + assert scale_compare.svod2_key("/api/v1/svod/2/GO/2026") == "GET свод 2 GO" + assert scale_compare.svod2_key("/api/v1/svod/2/RF/2026?view=compact&offset=0&limit=300") == "GET свод 2 RF страница" + assert scale_compare.svod2_key("/api/v1/svod/2/GO/2026/facets?search=1") == "GET свод 2 GO facets" + + +def test_explain_has_svod2_queries(): + names = {q["name"] for q in explain_manual.queries(1, 2, 2026)} + assert {"svod2_go_page", "svod2_rf_page", "svod2_rf_search", "svod2_go_facets"} <= names