diff --git a/perf/README.md b/perf/README.md index c186c40..29d0010 100644 --- a/perf/README.md +++ b/perf/README.md @@ -95,10 +95,16 @@ cd .. && .venv/bin/python -m perfkit.modals_inventory results/m cd perf/e2e PERF_RUN_ID=mem npx playwright test scenarios/memory.spec.ts # полный прогон, около часа PERF_RUN_ID=memsmall PERF_MEM_CYCLES=2 PERF_MEM_SNAPSHOTS=0,2 npx playwright test scenarios/memory.spec.ts # малый -cd .. && .venv/bin/python -m perfkit.memtrend results/mem # наклоны МНК на цикл +cd .. && .venv/bin/python -m perfkit.memtrend results/mem [--from-cycle 1] # наклоны МНК на цикл ``` - Метрики записи: `cycle`, `heap_used_mb` (после gc), `documents`, `nodes`, `js_event_listeners` (CDP `Memory.getDOMCounters`), `ws_open` (обёртка над `window.WebSocket`), `ws_created`, `edits_ok`, `cycle_s`, `relogin`. - Heap snapshots на циклах 0, 10, 20 (`PERF_MEM_SNAPSHOTS`) - `perf/results//artifacts/memory-cycleNN.heapsnapshot`, в git не попадают; открываются в Chrome DevTools (Memory). Разбор retainers - Task 10. - Переменные: `PERF_MEM_CYCLES` (20), `PERF_MEM_SHEET1/2` (`FORM_x/SHEET/direction|null`), `PERF_TEST_TIMEOUT_MS` (3 ч). Токен (60 мин) перевыпускается по API без перезагрузки, если до конца < 10 мин. - Любая неудачная правка (нет эха, отказ сервера, нет редактируемых ячеек) - ошибка сценария. + +Как читать `perfkit.memtrend`: +- Цикл 0 - базовая точка: страница `/tasks` до первого открытия листа (heap 7 МБ, nodes 289). Скачок цикл 0 -> 1 - загрузка кода/данных первого листа, а не рост. Поэтому МНК по умолчанию считается с цикла 1 (`--from-cycle N`); вывод печатает использованные циклы и число точек. +- Вторым блоком печатается наклон по второй половине (циклы 11-20) - индикатор плато: если он заметно меньше общего, рост затухает. +- Snapshots на циклах 10 и 20 слегка влияют на heap следующего замера (после снимка остаются служебные структуры/GC), поэтому единичные скачки в точках 11 и 21 не интерпретировать. +- Результат прогона t8-mem (с цикла 1): heap 0.046 МБ/цикл (вторая половина 0.027), nodes 0, listeners 0, ws_open = 1 всегда. DOM, listeners и WS стабильны; остаточный рост JS-heap ~27-46 КБ/цикл неубедителен на 20 циклах - кандидат на diff retainers снимков 10 и 20 (Task 10). diff --git a/perf/perfkit/memtrend.py b/perf/perfkit/memtrend.py index 7fc053e..0eb1e68 100644 --- a/perf/perfkit/memtrend.py +++ b/perf/perfkit/memtrend.py @@ -1,11 +1,12 @@ """Тренд памяти по записям сценария memory (Task 8). -CLI: python -m perfkit.memtrend -Читает /memory.jsonl, сортирует записи по metrics.cycle и печатает наклон МНК +CLI: python -m perfkit.memtrend [--from-cycle N] +Читает /memory.jsonl (по умолчанию с цикла 1, цикл 0 - базовая точка), сортирует записи по metrics.cycle и печатает наклон МНК (единиц метрики на цикл) для heap_used_mb, nodes, js_event_listeners. """ from __future__ import annotations +import argparse import json import sys from pathlib import Path @@ -37,16 +38,28 @@ def load_memory(run_dir) -> list[dict]: return sorted(recs, key=lambda r: r["metrics"]["cycle"]) -def main(argv: list[str] | None = None) -> int: - argv = sys.argv[1:] if argv is None else argv - if len(argv) != 1: - print("usage: python -m perfkit.memtrend ", file=sys.stderr) - return 2 - recs = load_memory(argv[0]) - print(f"циклов (записей): {len(recs)}") +def _print_slopes(title: str, recs: list[dict]) -> None: + cs = [r["metrics"]["cycle"] for r in recs] + print(f"{title}: циклы {cs[0]}-{cs[-1]}, {len(recs)} точек") for m in METRICS: vals = [float(r["metrics"][m]) for r in recs] print(f"{m}: {slope_per_cycle(vals):.3f} на цикл (первый {vals[0]:.1f}, последний {vals[-1]:.1f})") + + +def main(argv: list[str] | None = None) -> int: + ap = argparse.ArgumentParser(prog="python -m perfkit.memtrend") + ap.add_argument("run_dir") + ap.add_argument("--from-cycle", type=int, default=1, + help="первый цикл в МНК (по умолчанию 1: цикл 0 - базовая точка, страница /tasks до открытия листа)") + args = ap.parse_args(sys.argv[1:] if argv is None else argv) + recs = load_memory(args.run_dir) + used = [r for r in recs if r["metrics"]["cycle"] >= args.from_cycle] + print(f"записей memory: {len(recs)}") + _print_slopes(f"наклон с цикла {args.from_cycle}", used) + last = used[-1]["metrics"]["cycle"] + half = [r for r in used if r["metrics"]["cycle"] > last // 2] + if len(half) >= 2 and len(half) < len(used): + _print_slopes(f"вторая половина (циклы {half[0]['metrics']['cycle']}-{last}), индикатор плато", half) return 0 diff --git a/perf/tests/test_memtrend.py b/perf/tests/test_memtrend.py index 857d370..3fd6bea 100644 --- a/perf/tests/test_memtrend.py +++ b/perf/tests/test_memtrend.py @@ -36,3 +36,35 @@ def test_cli_prints_three_slopes(tmp_path, capsys): out = capsys.readouterr().out assert "heap_used_mb" in out and "nodes" in out and "js_event_listeners" in out assert "2.000" in out and "5.000" in out and "0.000" in out + + +def _write(tmp_path, heap): + rows = [{"scenario": "memory", "metrics": {"cycle": c, "heap_used_mb": h, "nodes": 350 if c else 289, "js_event_listeners": 336 if c else 322}} for c, h in enumerate(heap)] + (tmp_path / "memory.jsonl").write_text("\n".join(json.dumps(r) for r in rows) + "\n") + + +def test_cli_skips_baseline_step_by_default(tmp_path, capsys): + # ступенька на цикле 0 (7 -> 11), дальше +1 на цикл, 0 для nodes/listeners + _write(tmp_path, [7, 11, 12, 13, 14, 15, 16]) + assert main([str(tmp_path)]) == 0 + out = capsys.readouterr().out + assert "циклы 1-6" in out and "6 точек" in out + assert "heap_used_mb: 1.000" in out + assert "nodes: 0.000" in out and "js_event_listeners: 0.000" in out + + +def test_cli_from_cycle_zero_includes_step(tmp_path, capsys): + _write(tmp_path, [7, 11, 12, 13, 14, 15, 16]) + assert main([str(tmp_path), "--from-cycle", "0"]) == 0 + out = capsys.readouterr().out + assert "циклы 0-6" in out and "7 точек" in out + assert "heap_used_mb: 1.000" not in out.split("вторая половина")[0] + + +def test_cli_second_half_slope(tmp_path, capsys): + # рост до цикла 10, потом плато: вторая половина (циклы 11-20) = 0 + _write(tmp_path, [7] + [11 + min(c, 10) for c in range(0, 20)]) + assert main([str(tmp_path)]) == 0 + out = capsys.readouterr().out + assert "вторая половина (циклы 11-20)" in out + assert "heap_used_mb: 0.000" in out.split("вторая половина")[1]