diff --git a/openspec/changes/perf-profiling/design.md b/openspec/changes/perf-profiling/design.md index 0d8582c..da6319f 100644 --- a/openspec/changes/perf-profiling/design.md +++ b/openspec/changes/perf-profiling/design.md @@ -157,6 +157,12 @@ | 2026-10-25 | Рендер компонента в коммите: `flags & PerformedWork` для тегов 0/1/11/15 + обход как в DevTools; `self_ms = actualDuration - сумма детей` | Единственный способ получить renders/self_ms без React DevTools extension | Приближение: не учитывает memo-bailout'ы вне обхода | | 2026-10-25 | `modals.json`: Menu/Popover-контекстные меню (RowActionMenu, StagesTable Menu, ValidationErrorsAlert) не включены; Popover выбора колонок (`sheet_columns_tree`) включён как единственный оверлей, доступный на большом листе | Меню не модалки; select_vsp/select_expense/create_* на самом большом листе rf (FORM_2/AHR) не открываются (проверено на стенде) | Неполное покрытие «модалка поверх большого листа» | | 2026-10-25 | Для открытия под admin без права на лист — роль admin только для модалок страниц /tasks, задач и админки; остальные под rf | Права строк есть у rf | Нет | +| 2026-10-26 | R-2: форм-модалки (выбор ВСП/статьи, создание программы/проекта) мерятся и на малом листе, и на «выращенном» (@big: FORM_2 AHR_RENT/UTILITY/SECURITY ~500 строк, FORM_4 AHR ~300) через perfkit/gen/grow.py, без пересоздания БД; grow запускается после скролл-сценариев | Ревью T7: на листах, где эти модалки существуют, 31–110 строк, а жалоба заказчика — на модалку большой формы; FORM_4 add_line O(n) не позволяет выращивать до 1000+ | БД после grow отличается от базовой на этих листах; цели одинаковы на масштабах 1 и 2 | +| 2026-10-26 | R-2, детали: grow растит FORM_2 AHR_UTILITY и AHR_SECURITY (rf) до 500 строк и FORM_4 CAP (rf, форма 13) до 300; AHR_RENT и FORM_4 AHR остаются малыми и служат «малым» листом пары (поэтому порядок запуска не влияет на малые замеры; на скролл-сценарии по UTILITY/SECURITY/CAP grow влияет, их запускать до grow). Цели те же на масштабах 1 и 2 | Один и тот же лист нельзя замерить и малым, и большим без жёсткого порядка; на FORM_4 add_line O(n) | После grow БД отличается от базовой; скролл-замеры этих трёх листов после grow не сравнимы с до | +| 2026-10-26 | Строки grow пустые (значения не заполняются), FORM_2: дубли ВСП допустимы (API принимает) | Для открытия модалки важно число строк в DOM/таблице, не данные | Нет | +| 2026-10-26 | Время grow на масштабе 1: FORM_2 +468/+469 строк по ~1 c (параллельно), FORM_4 CAP +190 строк 213 c (последовательно, ~1.1 c на строку, O(n)) | Замер | Для масштаба 2 цели те же - время не растёт | +| 2026-10-26 | Порядок run-all (предложение для T10): reset -> gen -> скролл -> соавтор -> grow -> модалки (малые и @big) -> память | grow меняет размер листов | Решение за T10 | + ## Decisions made with the human diff --git a/perf/README.md b/perf/README.md index 1799ff9..270ca8b 100644 --- a/perf/README.md +++ b/perf/README.md @@ -84,3 +84,5 @@ cd .. && .venv/bin/python -m perfkit.modals_inventory results/m - Переменные: `PERF_MODALS` (glob по 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,6` priority 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`: пара записей `` (малый лист: AHR_RENT, FORM_4/AHR) и `@big` (UTILITY, FORM_4/CAP). Порядок для run-all: reset -> gen -> скролл -> соавтор -> grow -> модалки; малые замеры от grow не зависят, но скролл по UTILITY/SECURITY/CAP после grow несопоставим с до него. diff --git a/perf/e2e/modals.json b/perf/e2e/modals.json index 6213032..43bfa1b 100644 --- a/perf/e2e/modals.json +++ b/perf/e2e/modals.json @@ -15,7 +15,8 @@ "css": "h2", "text": "Выбор ВСП" }, - "note": "открывается из RealtimeTable.jsx:652; только при праве править строки (rf); на самом большом листе rf (FORM_2/AHR, 1154 строк) модалка не открывается (проверено: кнопка «Добавить строку» добавляет строку, ВСП-выбор только для ветки isVspAdditionRow)" + "note": "RealtimeTable.jsx:652; только при праве править строки (rf); additionVspRowTable FORM_2 = AHR_RENT/UTILITY/SECURITY (на AHR модалка не открывается). Малый лист AHR_RENT (31 строка), @big = AHR_UTILITY, выращенный perfkit.gen.grow до ~500 строк", + "big_sheet": "FORM_2/AHR_UTILITY/null" }, { "id": "sheet_select_expense", @@ -30,7 +31,7 @@ "action": "click", "pre": [ { - "selector": "tbody tr:has-text(\"ПроектAHR\") >> nth=0 >> td >> nth=1", + "selector": "tbody tr:has-text(\"Проект\") >> nth=0 >> td >> nth=1", "action": "click" } ] @@ -39,7 +40,8 @@ "css": "h2", "text": "Добавление строки" }, - "note": "нужна выделенная строка проекта (row_type ITEM); RealtimeTable.jsx:656" + "note": "нужна выделенная строка проекта (row_type ITEM); RealtimeTable.jsx:656; малый лист FORM_4/AHR (110 строк), @big = FORM_4/CAP, выращенный perfkit.gen.grow до ~300 строк", + "big_sheet": "FORM_4/CAP/null" }, { "id": "sheet_create_program", @@ -57,7 +59,8 @@ "css": "h2", "text": "Создание программы" }, - "note": "RealtimeTable.jsx:697, только FORM_4" + "note": "RealtimeTable.jsx:697, только FORM_4; малый лист FORM_4/AHR (110 строк), @big = FORM_4/CAP, выращенный perfkit.gen.grow до ~300 строк", + "big_sheet": "FORM_4/CAP/null" }, { "id": "sheet_create_project", @@ -72,7 +75,7 @@ "action": "click", "pre": [ { - "selector": "tbody tr:has-text(\"ПрограммаAHR\") >> nth=0 >> td >> nth=1", + "selector": "tbody tr:has-text(\"Программа\") >> nth=0 >> td >> nth=1", "action": "click" } ] @@ -81,7 +84,8 @@ "css": "h2", "text": "Создание проекта" }, - "note": "нужна выделенная строка программы (GROUP); RealtimeTable.jsx:705" + "note": "нужна выделенная строка программы (GROUP); RealtimeTable.jsx:705; малый лист FORM_4/AHR (110 строк), @big = FORM_4/CAP, выращенный perfkit.gen.grow до ~300 строк", + "big_sheet": "FORM_4/CAP/null" }, { "id": "sheet_columns_tree", diff --git a/perf/perfkit/gen/grow.py b/perf/perfkit/gen/grow.py new file mode 100644 index 0000000..005cd0b --- /dev/null +++ b/perf/perfkit/gen/grow.py @@ -0,0 +1,121 @@ +"""Доращивание отдельных листов rf до «большого» размера (для замеров модалок поверх большого листа). + + cd perf && .venv/bin/python -m perfkit.gen.grow --scale 1 [--vsp-rows 500] [--form4-rows 300] + +Базовая генерация (run.py) даёт на листах, где существуют форм-модалки, 31 (FORM_2 AHR_RENT/UTILITY/ +SECURITY) и 110 (FORM_4 AHR/CAP/OPER) строк. Здесь строки донабираются через тот же API (POST .../line +под admin, как в run.py), идемпотентно: если строк уже >= цели - ничего не делается. +Растут: FORM_2 AHR_UTILITY и AHR_SECURITY (до --vsp-rows) и FORM_4 CAP (до --form4-rows) формы rf. +AHR_RENT и FORM_4 AHR остаются малыми: это пара «малый лист» для тех же модалок, а скролл-сценарии +по AHR не меняются. Порядок запуска поэтому не влияет на малые замеры; на скролл-сценарии по +UTILITY/SECURITY/CAP grow влияет - запускать после них. Цели одинаковы на масштабах 1 и 2 +(add_line на FORM_4 стоит O(n)). Манифест обновляется (Sheet.rows). +""" +from __future__ import annotations + +import argparse +import asyncio +import json +import random +import time +from collections import Counter +from datetime import datetime, timezone + +from .. import RESULTS_DIR +from ..manifest import dump, load, manifest_path +from .client import Api +from .run import CONCURRENCY, FormInfo, YEAR, add_lines, count_rows, leaf_items, log, sheet_params + +VSP_ROWS = 500 +FORM4_ROWS = 300 +# (form_type, лист, вид) +TARGETS = [("FORM_2", "AHR_UTILITY", "vsp"), ("FORM_2", "AHR_SECURITY", "vsp"), ("FORM_4", "CAP", "expense")] + + +def rows_to_add(current: int, target: int) -> int: + """Сколько строк донабрать, чтобы на листе стало >= target (current уже включает шаблонные и META-строки).""" + return max(0, target - current) + + +def _target_for(kind: str, vsp_rows: int, form4_rows: int) -> int: + return vsp_rows if kind == "vsp" else form4_rows + + +async def grow_vsp_sheet(api: Api, form_id: int, sheet: str, need: int, org_index: int) -> int: + path = f"/form/{form_id}/sheet/{sheet}" + rows = (await api.get(path))["result"] + vsp_ids = sorted({r["data"]["vsp_id"] for r in rows if r.get("row_type") == "INPUT" and r["data"].get("vsp_id")}) + if not vsp_ids: + raise RuntimeError(f"{sheet}: нет ВСП в существующих строках") + + async def one(n: int) -> None: + body = { + "vsp_id": vsp_ids[n % len(vsp_ids)], + "contract_number": f"ДГ-{org_index:02d}/{sheet[-4:]}/G{n:04d}", + "contract_end_date": datetime(YEAR + 1 + n % 3, 1 + n % 12, 1 + n % 27, tzinfo=timezone.utc).isoformat(), + } + await api.post(path + "/line", json=body) + + await asyncio.gather(*(one(n) for n in range(need))) + return need + + +async def grow_form4_sheet(api: Api, form_id: int, sheet: str, need: int) -> int: + path = f"/form/{form_id}/sheet/{sheet}" + rows = (await api.get(path))["result"] + vsp_ids = sorted({r["data"]["header"]["vsp_id"] for r in rows if r.get("row_type") == "INPUT" and r["data"]["header"].get("vsp_id")}) + project_ids = sorted({r["data"]["header"]["project_id"] for r in rows if r.get("row_type") == "ITEM"}) + if not project_ids: + raise RuntimeError(f"{sheet}: нет проектов") + leaves = await leaf_items(api, "FORM_4", sheet) + form = FormInfo(form_id, "FORM_4", 0, "rf", 1, vsp_ids) + stats: Counter = Counter() + ids = await add_lines(api, form, sheet, None, need, leaves, random.Random(f"grow-{form_id}-{sheet}"), stats, project_ids) + if stats: + log(f" статистика {dict(stats)}") + return len(ids) + + +async def main(scale: int, vsp_rows: int, form4_rows: int) -> None: + path = manifest_path(scale, RESULTS_DIR) + m = load(path) + api = Api("admin", "admin", asyncio.Semaphore(CONCURRENCY)) + report = [] + try: + await api.login() + for ftype, sheet, kind in TARGETS: + sh = next((s for s in m.sheets if s.owner == "rf" and s.form_type == ftype and s.sheet == sheet and s.direction is None), None) + if sh is None: + raise RuntimeError(f"лист {ftype}/{sheet} (rf) не найден в манифесте") + cur = await count_rows(api, f"/form/{sh.form_id}/sheet/{sheet}", sheet_params(None)) + target = _target_for(kind, vsp_rows, form4_rows) + need = rows_to_add(cur, target) + t0 = time.time() + if need: + log(f"{ftype}/{sheet} form {sh.form_id}: {cur} -> {target} (+{need})") + if kind == "vsp": + await grow_vsp_sheet(api, sh.form_id, sheet, need, 1) + else: + await grow_form4_sheet(api, sh.form_id, sheet, need) + else: + log(f"{ftype}/{sheet} form {sh.form_id}: {cur} >= {target}, ничего не делаю") + after = await count_rows(api, f"/form/{sh.form_id}/sheet/{sheet}", sheet_params(None)) + sh.rows = after + dt = time.time() - t0 + report.append({"sheet": f"{ftype}/{sheet}", "form_id": sh.form_id, "before": cur, "after": after, "added": need, "seconds": round(dt, 1)}) + log(f" итого {after} строк за {dt:.0f} c") + dump(m, path) + out = RESULTS_DIR / f"grow-s{scale}.json" + out.write_text(json.dumps(report, ensure_ascii=False, indent=2), encoding="utf-8") + log(f"манифест обновлён: {path}; отчёт {out}") + finally: + await api.aclose() + + +if __name__ == "__main__": + ap = argparse.ArgumentParser(description=__doc__) + ap.add_argument("--scale", type=int, required=True) + ap.add_argument("--vsp-rows", type=int, default=VSP_ROWS) + ap.add_argument("--form4-rows", type=int, default=FORM4_ROWS) + a = ap.parse_args() + asyncio.run(main(a.scale, a.vsp_rows, a.form4_rows)) diff --git a/perf/tests/test_gen_grow.py b/perf/tests/test_gen_grow.py new file mode 100644 index 0000000..1b4aa4e --- /dev/null +++ b/perf/tests/test_gen_grow.py @@ -0,0 +1,14 @@ +import pytest + +from perfkit.gen.grow import rows_to_add, TARGETS + + +@pytest.mark.parametrize("cur,target,exp", [(31, 500, 469), (110, 300, 190), (500, 500, 0), (700, 500, 0), (0, 5, 5)]) +def test_rows_to_add(cur, target, exp): + assert rows_to_add(cur, target) == exp + + +def test_targets_do_not_touch_small_pair_sheets(): + # AHR_RENT и FORM_4/AHR остаются малыми (пара для @big) + assert ("FORM_2", "AHR_RENT") not in [(f, s) for f, s, _ in TARGETS] + assert ("FORM_4", "AHR") not in [(f, s) for f, s, _ in TARGETS]