Andrey Epifancev 29867535b0 perf: полный прогон run-all.sh и правки по ревью
- perf/run-all.sh: масштабы 1 и 2, шаги reset -> gen -> verify -> stand ->
  preflight -> scroll -> coauthor -> grow -> modals -> modals_profiling ->
  memory -> memtrend -> [control4x] -> bench -> aggregate; лог и
  run-all.status.json, возобновление (PERF_STEPS, PERF_SKIP_RESET),
  сухой режим с оценкой длительности, PERF_S2_REPEATS/PERF_S2_TARGETS,
  контроль CPU 4x (PERF_CONTROL_TARGETS).
- Основная метрика: main_thread_ms и main_thread_ms_per_step в скролле
  (CDP TaskDuration), frame_excess_ms; в summary.md первыми строками.
- SQL-статистика по шагам: PERF_SQLSTATS_TAG -> sql/<шаг>/ (sqlstats --out).
- netlog: тела /auth/ не пишутся, поля password/token/secret вырезаются.
- scroll.spec: уникальные имена целей (лист ССП с суффиксом /ssp, год в
  отчёте проекта) - иначе Playwright не запускал файл из-за дублей.
- PERF_TRACE=0 для прогонов с одним повтором.
- README: scroll-coauthor, run-all, возобновление, без CPU 4/6 в матрицах.
- design.md: уточнена формулировка R-2, решения Task 10a.

Refs #202

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 03:01:33 +04:00

100 lines
3.8 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")
# основная метрика сравнения (решение человека 2026-10-25): суммарное время main thread на действие - первыми строками
PRIMARY_METRICS = ("main_thread_ms_per_step", "main_thread_ms")
def _metric_order(m: str):
return (PRIMARY_METRICS.index(m), "") if m in PRIMARY_METRICS else (len(PRIMARY_METRICS), m)
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(), key=lambda kv: _metric_order(kv[0])):
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())