Log in
scripts/perf_scorecard.py 100 lines · 3.9 KB · python Blame
1
#!/usr/bin/env python3
2
"""Run or read the focused oak-vs-git bench and emit a JSON scorecard."""
3
4
from __future__ import annotations
5
6
import argparse
7
import json
8
import subprocess
9
import sys
10
from datetime import datetime, timezone
11
from pathlib import Path
12
13
ROOT = Path(__file__).resolve().parents[1]
14
sys.path.insert(0, str(ROOT / "scripts"))
15
16
from oakbench.perf_loop import (  # noqa: E402
17
    build_scorecard,
18
    load_jsonl,
19
    scorecard_exit_code,
20
    write_json,
21
)
22
23
24
def parse_args() -> argparse.Namespace:
25
    parser = argparse.ArgumentParser(description=__doc__)
26
    parser.add_argument("--rows", nargs="*", type=Path, help="Existing JSONL rows to score; skips bench.py.")
27
    parser.add_argument("--profile", default="standard", help="bench.py profile when --rows is omitted.")
28
    parser.add_argument("--runs", type=int, default=None, help="bench.py --runs override.")
29
    parser.add_argument("--results", type=Path, default=ROOT / "results" / "perf-scorecard")
30
    parser.add_argument("--subjects", default="git,oak_installed", help="bench.py subjects.")
31
    parser.add_argument("--oak-subject", default="oak_installed", help="Oak subject to compare against git.")
32
    parser.add_argument("--git-subject", default="git")
33
    parser.add_argument("--git-bin", type=Path)
34
    parser.add_argument("--oak-installed-bin", type=Path)
35
    parser.add_argument("--oak-local-bin", type=Path)
36
    parser.add_argument("--skip-remote", action="store_true", default=True)
37
    parser.add_argument("--include-remote", action="store_true", help="Do not pass --skip-remote to bench.py.")
38
    parser.add_argument("--baseline", type=Path, help="Previous scorecard JSON to diff against.")
39
    parser.add_argument("--write-baseline", type=Path, help="Write this scorecard as a stored baseline.")
40
    parser.add_argument("--output", type=Path, help="Write JSON here instead of stdout.")
41
    parser.add_argument("--change-threshold-pct", type=float, default=3.0)
42
    return parser.parse_args()
43
44
45
def run_bench(args: argparse.Namespace) -> list[Path]:
46
    bench_id = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ")
47
    out = args.results / bench_id
48
    command = [
49
        sys.executable,
50
        str(ROOT / "scripts" / "bench.py"),
51
        "--profile",
52
        args.profile,
53
        "--subjects",
54
        args.subjects,
55
        "--randomize-subject-order",
56
        "--results",
57
        str(out),
58
    ]
59
    if args.runs is not None:
60
        command.extend(["--runs", str(args.runs)])
61
    if args.git_bin:
62
        command.extend(["--git-bin", str(args.git_bin)])
63
    if args.oak_installed_bin:
64
        command.extend(["--oak-installed-bin", str(args.oak_installed_bin)])
65
    if args.oak_local_bin:
66
        command.extend(["--oak-local-bin", str(args.oak_local_bin)])
67
    if args.skip_remote and not args.include_remote:
68
        command.append("--skip-remote")
69
    proc = subprocess.run(command, cwd=ROOT, check=False)
70
    if proc.returncode not in (0, 1):
71
        raise SystemExit(f"bench.py failed to run (exit {proc.returncode})")
72
    latest = out / "latest.jsonl"
73
    if not latest.exists():
74
        raise SystemExit(f"bench.py did not write {latest}")
75
    return [latest]
76
77
78
def main() -> int:
79
    args = parse_args()
80
    row_paths = args.rows if args.rows else run_bench(args)
81
    baseline = json.loads(args.baseline.read_text()) if args.baseline else None
82
    scorecard = build_scorecard(
83
        load_jsonl(row_paths),
84
        oak_subject=args.oak_subject,
85
        git_subject=args.git_subject,
86
        baseline=baseline,
87
        change_threshold_pct=args.change_threshold_pct,
88
    )
89
    scorecard["source_rows"] = [str(path) for path in row_paths]
90
    if args.write_baseline:
91
        write_json(args.write_baseline, scorecard)
92
    if args.output:
93
        write_json(args.output, scorecard)
94
    else:
95
        print(json.dumps(scorecard, indent=2, sort_keys=True))
96
    return scorecard_exit_code(str(scorecard["summary"]["gate_verdict"]))
97
98
99
if __name__ == "__main__":
100
    raise SystemExit(main())