- 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>
100 lines
3.8 KiB
Python
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())
|