"""Тренд памяти по записям сценария memory (Task 8). 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 METRICS = ("heap_used_mb", "nodes", "js_event_listeners") def slope_per_cycle(values: list[float]) -> float: """Наклон прямой МНК по точкам (0, v0), (1, v1), ... Меньше двух точек - ValueError.""" n = len(values) if n < 2: raise ValueError("нужно минимум две точки") mx = (n - 1) / 2 my = sum(values) / n sxx = sum((i - mx) ** 2 for i in range(n)) sxy = sum((i - mx) * (v - my) for i, v in enumerate(values)) return sxy / sxx def load_memory(run_dir) -> list[dict]: f = Path(run_dir) / "memory.jsonl" recs = [] for line in f.read_text(encoding="utf-8").splitlines(): if not line.strip(): continue r = json.loads(line) if r.get("scenario") == "memory": recs.append(r) return sorted(recs, key=lambda r: r["metrics"]["cycle"]) 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 if __name__ == "__main__": sys.exit(main())