"""Таблица циклов сценария памяти (memory.jsonl) с длительностью цикла - для report-data (сырой jsonl в git не попадает). cd perf && .venv/bin/python analysis/memory_cycles.py results/20261026-s1 --out report-data/20261026-s1/analysis Вывод: /memory-cycles.md - по циклу heap_used_mb, nodes, js_event_listeners, ws_open, documents, cycle_s, relogin; наклон МНК cycle_s по циклам 1..N (секунд на цикл) и рост последнего цикла к первому. """ from __future__ import annotations import argparse import json from pathlib import Path def slope(xs, ys): n = len(xs) mx, my = sum(xs) / n, sum(ys) / n den = sum((x - mx) ** 2 for x in xs) return sum((x - mx) * (y - my) for x, y in zip(xs, ys)) / den if den else 0.0 def main(argv=None): ap = argparse.ArgumentParser() ap.add_argument("run_dir") ap.add_argument("--out", required=True) a = ap.parse_args(argv) rows = [json.loads(x) for x in (Path(a.run_dir) / "memory.jsonl").read_text().splitlines()] rows.sort(key=lambda r: r["metrics"]["cycle"]) md = [f"# Циклы сценария памяти ({Path(a.run_dir).name}, memory_cycles.py)", "", "| цикл | heap_used_mb | nodes | js_event_listeners | ws_open | documents | cycle_s | relogin |", "|---|---|---|---|---|---|---|---|"] for r in rows: m = r["metrics"] md.append(f"| {m['cycle']} | {m['heap_used_mb']:.2f} | {m['nodes']} | {m['js_event_listeners']} | {m['ws_open']} | {m.get('documents')} | " f"{m.get('cycle_s', 0):.0f} | {m.get('relogin', '')} |") c = [r for r in rows if r["metrics"]["cycle"] >= 1] xs = [r["metrics"]["cycle"] for r in c] ys = [r["metrics"].get("cycle_s", 0) for r in c] md += ["", f"cycle_s: наклон МНК по циклам {xs[0]}-{xs[-1]} - {slope(xs, ys):.2f} с на цикл; первый {ys[0]:.0f} с, последний {ys[-1]:.0f} с " f"(+{100 * (ys[-1] / ys[0] - 1):.0f} %)."] out = Path(a.out) out.mkdir(parents=True, exist_ok=True) (out / "memory-cycles.md").write_text("\n".join(md) + "\n") print("\n".join(md[-3:])) if __name__ == "__main__": main()