perf: модалки на малом и выращенном листе (grow)

perfkit.gen.grow доращивает FORM_2 AHR_UTILITY/AHR_SECURITY до 500 и
FORM_4 CAP до 300 строк; у форм-модалок варианты @big.

Refs #202

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Andrey Epifancev 2026-10-26 01:04:06 +04:00
parent 627ea82321
commit 6a2439239d
5 changed files with 153 additions and 6 deletions

View File

@ -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

View File

@ -84,3 +84,5 @@ cd .. && .venv/bin/python -m perfkit.modals_inventory results/m
- Переменные: `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,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`: пара записей `<id>` (малый лист: AHR_RENT, FORM_4/AHR) и `<id>@big` (UTILITY, FORM_4/CAP). Порядок для run-all: reset -> gen -> скролл -> соавтор -> grow -> модалки; малые замеры от grow не зависят, но скролл по UTILITY/SECURITY/CAP после grow несопоставим с до него.

View File

@ -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",

121
perf/perfkit/gen/grow.py Normal file
View File

@ -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))

View File

@ -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]