"""Агрегатор: python -m perfkit.aggregate -> perf/report-data//summary.{json,md}.""" from __future__ import annotations import json import math import sys from pathlib import Path from . import REPORT_DATA_DIR from .results import read_all GROUP_KEYS = ("scenario", "target", "role", "scale", "cpu_rate") def _p95(sorted_vals): """Nearest-rank.""" k = math.ceil(0.95 * len(sorted_vals)) return sorted_vals[max(k, 1) - 1] def _median(s): n = len(s) return s[n // 2] if n % 2 else (s[n // 2 - 1] + s[n // 2]) / 2 def _num(v): return isinstance(v, (int, float)) and not isinstance(v, bool) def summarize(run_dir) -> dict: run_dir = Path(run_dir) groups: dict[tuple, dict] = {} for r in read_all(run_dir): key = tuple(r[k] for k in GROUP_KEYS) g = groups.setdefault(key, {"vals": {}, "n": 0, "incomplete_n": 0, "traced_n": 0}) if r["metrics"].get("traced") == 1: # повтор под трассировкой (O-9): искажён накладными расходами trace, в статистику не входит g["traced_n"] += 1 continue if r.get("incomplete"): g["incomplete_n"] += 1 continue g["n"] += 1 for m, v in r["metrics"].items(): if m != "traced" and _num(v): g["vals"].setdefault(m, []).append(v) out = [] for key in sorted(groups, key=lambda k: tuple(str(x) for x in k)): g = groups[key] metrics = {} for m, vals in sorted(g["vals"].items()): s = sorted(vals) metrics[m] = {"median": _median(s), "p95": _p95(s), "min": s[0], "max": s[-1], "n": len(s)} out.append({**dict(zip(GROUP_KEYS, key)), "n": g["n"], "incomplete_n": g["incomplete_n"], "traced_n": g["traced_n"], "metrics": metrics}) return {"run_id": run_dir.name, "groups": out} def _fmt(x): return f"{x:.1f}" if isinstance(x, float) and not x.is_integer() else str(int(x)) if isinstance(x, float) else str(x) def render_md(summary: dict) -> str: lines = [f"# Summary: {summary['run_id']}", ""] for g in summary["groups"]: lines.append(f"## {g['scenario']} | target={g['target']} | role={g['role']} | scale={g['scale']} | cpu_rate={g['cpu_rate']}") lines.append("") lines.append(f"Замеров: {g['n']}, незавершённых: {g['incomplete_n']}" + (f", исключено трассировочных: {g['traced_n']}" if g.get("traced_n") else "")) lines.append("") lines.append("| metric | median | p95 | min | max | n |") lines.append("|---|---|---|---|---|---|") for m, s in g["metrics"].items(): lines.append(f"| {m} | {_fmt(s['median'])} | {_fmt(s['p95'])} | {_fmt(s['min'])} | {_fmt(s['max'])} | {s['n']} |") lines.append("") return "\n".join(lines) def main(argv=None) -> int: argv = sys.argv[1:] if argv is None else argv if len(argv) != 1: print("usage: python -m perfkit.aggregate ", file=sys.stderr) return 2 run_dir = Path(argv[0]) s = summarize(run_dir) out = REPORT_DATA_DIR / s["run_id"] out.mkdir(parents=True, exist_ok=True) (out / "summary.json").write_text(json.dumps(s, ensure_ascii=False, indent=2), encoding="utf-8") (out / "summary.md").write_text(render_md(s), encoding="utf-8") print(out) return 0 if __name__ == "__main__": raise SystemExit(main())