Files
2026-07-13 11:59:58 +08:00

194 lines
5.6 KiB
Python
Executable File

#!/usr/bin/env python3
"""Aggregate JSONL benchmark results into summary tables.
Usage::
python scripts/benchmark/summarize_bench.py results.jsonl
python scripts/benchmark/summarize_bench.py results/*.jsonl --group provider,scenario,workload
python scripts/benchmark/summarize_bench.py results.jsonl --csv > summary.csv
"""
from __future__ import annotations
import argparse
import json
import sys
from collections import defaultdict
from pathlib import Path
from typing import Any
def _p(arr: list[float], pct: float) -> float:
if not arr:
return 0.0
s = sorted(arr)
if len(s) == 1:
return s[0]
rank = (len(s) - 1) * (pct / 100)
lower = int(rank)
upper = min(lower + 1, len(s) - 1)
weight = rank - lower
return s[lower] * (1 - weight) + s[upper] * weight
def _load_jsonl(paths: list[Path]) -> list[dict[str, Any]]:
rows: list[dict[str, Any]] = []
errors: list[str] = []
for p in paths:
with p.open("r", encoding="utf-8") as f:
for line_no, line in enumerate(f, start=1):
line = line.strip()
if not line or line.startswith("#"):
continue
try:
rows.append(json.loads(line))
except json.JSONDecodeError as exc:
errors.append(f"{p}:{line_no}: {exc.msg}")
if errors:
raise ValueError("Malformed JSONL row(s):\n" + "\n".join(errors))
return rows
def _group_key(row: dict[str, Any], group_by: list[str]) -> tuple:
return tuple(row.get(k, "?") for k in group_by)
def _summarize(rows: list[dict[str, Any]], group_by: list[str]) -> list[dict[str, Any]]:
groups: dict[tuple, list[dict[str, Any]]] = defaultdict(list)
for r in rows:
groups[_group_key(r, group_by)].append(r)
summary: list[dict[str, Any]] = []
for key, group in sorted(groups.items()):
ok = [r for r in group if r.get("success")]
a = [r["acquire_ms"] for r in ok]
ru = [r.get("run_ms", 0) for r in ok]
rel = [r.get("release_ms", 0) for r in ok]
t = [r["total_ms"] for r in ok]
warm_hits = sum(1 for r in ok if r.get("warm_hit"))
errors = len(group) - len(ok)
entry: dict[str, Any] = {}
for i, k in enumerate(group_by):
entry[k] = key[i]
entry["count"] = len(group)
entry["ok"] = len(ok)
entry["errors"] = errors
entry["warm_hit_rate"] = round(warm_hits / len(ok), 3) if ok else 0
entry["acquire_p50"] = round(_p(a, 50), 1)
entry["acquire_p95"] = round(_p(a, 95), 1)
entry["acquire_p99"] = round(_p(a, 99), 1)
entry["acquire_mean"] = round(sum(a) / len(a), 1) if a else 0.0
entry["run_p50"] = round(_p(ru, 50), 1)
entry["run_p95"] = round(_p(ru, 95), 1)
entry["release_p50"] = round(_p(rel, 50), 1)
entry["total_p50"] = round(_p(t, 50), 1)
entry["total_p95"] = round(_p(t, 95), 1)
entry["total_p99"] = round(_p(t, 99), 1)
entry["total_mean"] = round(sum(t) / len(t), 1) if t else 0.0
summary.append(entry)
return summary
_COLUMNS = [
"provider",
"scenario",
"workload",
"concurrency",
"count",
"ok",
"errors",
"warm_hit_rate",
"acquire_p50",
"acquire_p95",
"acquire_p99",
"acquire_mean",
"run_p50",
"run_p95",
"release_p50",
"total_p50",
"total_p95",
"total_p99",
"total_mean",
]
def _print_table(rows: list[dict[str, Any]], fmt: str = "plain") -> None:
if fmt == "csv":
import csv as _csv
w = _csv.DictWriter(sys.stdout, fieldnames=_COLUMNS, extrasaction="ignore")
w.writeheader()
w.writerows(rows)
return
# Plain text table
if not rows:
print("(no data)")
return
headers = [c for c in _COLUMNS if any(r.get(c) is not None for r in rows)]
col_widths = {h: len(h) for h in headers}
for r in rows:
for h in headers:
v = str(r.get(h, ""))
col_widths[h] = max(col_widths[h], len(v))
def _fmt_row(vals: list[str]) -> str:
parts = [v.rjust(col_widths[h]) for h, v in zip(headers, vals)]
return " ".join(parts)
print(_fmt_row(headers))
for r in rows:
vals = [str(r.get(h, "")) for h in headers]
print(_fmt_row(vals))
def main(argv: list[str] | None = None) -> int:
p = argparse.ArgumentParser(description="Aggregate JSONL benchmark results")
p.add_argument("inputs", nargs="+", help="JSONL file(s) from bench_sandbox_provider.py")
p.add_argument(
"--group",
default="provider,scenario,workload,concurrency",
help="Comma-separated grouping dimensions (default: provider,scenario,workload,concurrency)",
)
p.add_argument(
"--csv",
action="store_true",
help="Output CSV instead of aligned text",
)
p.add_argument(
"--json",
action="store_true",
help="Output JSON instead of aligned text",
)
args = p.parse_args(argv)
paths = [Path(i) for i in args.inputs]
try:
rows = _load_jsonl(paths)
except ValueError as exc:
print(str(exc), file=sys.stderr)
return 1
if not rows:
print("No valid JSONL rows found.", file=sys.stderr)
return 1
group_by = [g.strip() for g in args.group.split(",") if g.strip()]
summary = _summarize(rows, group_by)
if args.json:
json.dump(summary, sys.stdout, indent=2)
elif args.csv:
_print_table(summary, fmt="csv")
else:
_print_table(summary, fmt="plain")
return 0
if __name__ == "__main__":
sys.exit(main())