Andrey Epifancev fdc8ecb2a3 perf: замеры бэка
Сетевой лог сценариев, bench запросов, pg_stat_statements и EXPLAIN,
aggregate исключает трассировочные повторы (O-9).

Refs #202

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 02:36:46 +04:00

94 lines
3.4 KiB
Python

"""Агрегатор: python -m perfkit.aggregate <run_dir> -> perf/report-data/<run_id>/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 <run_dir>", 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())