| 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())
|