| 1 | #!/usr/bin/env python3
|
| 2 | """Cross-check harness timings with optional external `hyperfine`.
|
| 3 |
|
| 4 | The benchmark harness stays stdlib-only. `hyperfine` is optional: when absent,
|
| 5 | callers get either a clear diagnostic or a structured returncode-77 skip row.
|
| 6 | """
|
| 7 |
|
| 8 | from __future__ import annotations
|
| 9 |
|
| 10 | import argparse
|
| 11 | import json
|
| 12 | import os
|
| 13 | import shlex
|
| 14 | import shutil
|
| 15 | import subprocess
|
| 16 | import sys
|
| 17 | import tempfile
|
| 18 | from pathlib import Path
|
| 19 | from typing import Any, Callable, Optional, Sequence
|
| 20 |
|
| 21 | from oakbench.environment import host_home_environment
|
| 22 |
|
| 23 | SKIP_RETURNCODE = 77
|
| 24 | MEASUREMENT_SOURCE = "external_hyperfine"
|
| 25 |
|
| 26 |
|
| 27 | def hyperfine_path(executable: str = "hyperfine") -> Optional[str]:
|
| 28 | return shutil.which(executable)
|
| 29 |
|
| 30 |
|
| 31 | def skip_row(
|
| 32 | *,
|
| 33 | command: Sequence[str],
|
| 34 | reason: str,
|
| 35 | bench_id: str = "manual",
|
| 36 | scenario: str = "external-timing",
|
| 37 | operation: str = "hyperfine.crosscheck",
|
| 38 | run: int = 1,
|
| 39 | subject: str = "unknown",
|
| 40 | subject_kind: str = "unknown",
|
| 41 | ) -> dict[str, Any]:
|
| 42 | return {
|
| 43 | "schema_version": 1,
|
| 44 | "bench_id": bench_id,
|
| 45 | "profile": "crosscheck",
|
| 46 | "scenario": scenario,
|
| 47 | "operation": operation,
|
| 48 | "run": run,
|
| 49 | "subject": subject,
|
| 50 | "subject_kind": subject_kind,
|
| 51 | "elapsed_ms": 0.0,
|
| 52 | "returncode": SKIP_RETURNCODE,
|
| 53 | "command": list(command),
|
| 54 | "skipped": True,
|
| 55 | "skip_reason": reason,
|
| 56 | "measurement_source": MEASUREMENT_SOURCE,
|
| 57 | }
|
| 58 |
|
| 59 |
|
| 60 | def hyperfine_command(executable: str, command: Sequence[str], export_json: Path, *, runs: int, warmup: int) -> list[str]:
|
| 61 | return [
|
| 62 | executable,
|
| 63 | "--export-json",
|
| 64 | str(export_json),
|
| 65 | "--runs",
|
| 66 | str(runs),
|
| 67 | "--warmup",
|
| 68 | str(warmup),
|
| 69 | shlex.join(command),
|
| 70 | ]
|
| 71 |
|
| 72 |
|
| 73 | def parse_hyperfine_json(payload: str) -> dict[str, Any]:
|
| 74 | data = json.loads(payload)
|
| 75 | results = data.get("results")
|
| 76 | if not isinstance(results, list) or not results:
|
| 77 | raise ValueError("hyperfine JSON did not contain any results")
|
| 78 | first = results[0]
|
| 79 | if not isinstance(first, dict):
|
| 80 | raise ValueError("hyperfine JSON result is not an object")
|
| 81 | return first
|
| 82 |
|
| 83 |
|
| 84 | def measurement_row(
|
| 85 | *,
|
| 86 | command: Sequence[str],
|
| 87 | hyperfine_result: dict[str, Any],
|
| 88 | bench_id: str = "manual",
|
| 89 | scenario: str = "external-timing",
|
| 90 | operation: str = "hyperfine.crosscheck",
|
| 91 | run: int = 1,
|
| 92 | subject: str = "unknown",
|
| 93 | subject_kind: str = "unknown",
|
| 94 | ) -> dict[str, Any]:
|
| 95 | mean = _number_or_none(hyperfine_result.get("mean"))
|
| 96 | median = _number_or_none(hyperfine_result.get("median"))
|
| 97 | stddev = _number_or_none(hyperfine_result.get("stddev"))
|
| 98 | minimum = _number_or_none(hyperfine_result.get("min"))
|
| 99 | maximum = _number_or_none(hyperfine_result.get("max"))
|
| 100 | times = hyperfine_result.get("times")
|
| 101 | times_ms = None
|
| 102 | if isinstance(times, list):
|
| 103 | times_ms = [round(float(value) * 1000.0, 6) for value in times if _number_or_none(value) is not None]
|
| 104 | elapsed_seconds = median if median is not None else mean
|
| 105 | return {
|
| 106 | "schema_version": 1,
|
| 107 | "bench_id": bench_id,
|
| 108 | "profile": "crosscheck",
|
| 109 | "scenario": scenario,
|
| 110 | "operation": operation,
|
| 111 | "run": run,
|
| 112 | "subject": subject,
|
| 113 | "subject_kind": subject_kind,
|
| 114 | "elapsed_ms": round((elapsed_seconds or 0.0) * 1000.0, 6),
|
| 115 | "returncode": 0,
|
| 116 | "command": list(command),
|
| 117 | "measurement_source": MEASUREMENT_SOURCE,
|
| 118 | "hyperfine_command": hyperfine_result.get("command"),
|
| 119 | "hyperfine_mean_ms": _seconds_to_ms(mean),
|
| 120 | "hyperfine_median_ms": _seconds_to_ms(median),
|
| 121 | "hyperfine_stddev_ms": _seconds_to_ms(stddev),
|
| 122 | "hyperfine_min_ms": _seconds_to_ms(minimum),
|
| 123 | "hyperfine_max_ms": _seconds_to_ms(maximum),
|
| 124 | "hyperfine_times_ms": times_ms,
|
| 125 | }
|
| 126 |
|
| 127 |
|
| 128 | def run_hyperfine(
|
| 129 | command: Sequence[str],
|
| 130 | *,
|
| 131 | cwd: Path,
|
| 132 | runs: int,
|
| 133 | warmup: int,
|
| 134 | executable: str = "hyperfine",
|
| 135 | env: Optional[dict[str, str]] = None,
|
| 136 | runner: Callable[..., subprocess.CompletedProcess[str]] = subprocess.run,
|
| 137 | ) -> dict[str, Any]:
|
| 138 | resolved = hyperfine_path(executable)
|
| 139 | if resolved is None:
|
| 140 | raise HyperfineUnavailable(
|
| 141 | f"hyperfine not found on PATH; install hyperfine or emit a returncode-{SKIP_RETURNCODE} skip row"
|
| 142 | )
|
| 143 | with tempfile.TemporaryDirectory(prefix="oakbench-hyperfine-") as tmp:
|
| 144 | export_json = Path(tmp) / "hyperfine.json"
|
| 145 | proc = runner(
|
| 146 | hyperfine_command(resolved, command, export_json, runs=runs, warmup=warmup),
|
| 147 | cwd=cwd,
|
| 148 | env=host_home_environment(env),
|
| 149 | stdout=subprocess.PIPE,
|
| 150 | stderr=subprocess.PIPE,
|
| 151 | text=True,
|
| 152 | )
|
| 153 | if proc.returncode != 0:
|
| 154 | raise HyperfineFailed(proc.returncode, proc.stderr.strip() or proc.stdout.strip())
|
| 155 | return parse_hyperfine_json(export_json.read_text())
|
| 156 |
|
| 157 |
|
| 158 | class HyperfineUnavailable(RuntimeError):
|
| 159 | pass
|
| 160 |
|
| 161 |
|
| 162 | class HyperfineFailed(RuntimeError):
|
| 163 | def __init__(self, returncode: int, message: str) -> None:
|
| 164 | super().__init__(message)
|
| 165 | self.returncode = returncode
|
| 166 |
|
| 167 |
|
| 168 | def _number_or_none(value: Any) -> Optional[float]:
|
| 169 | if value is None:
|
| 170 | return None
|
| 171 | try:
|
| 172 | return float(value)
|
| 173 | except (TypeError, ValueError):
|
| 174 | return None
|
| 175 |
|
| 176 |
|
| 177 | def _seconds_to_ms(value: Optional[float]) -> Optional[float]:
|
| 178 | return None if value is None else round(value * 1000.0, 6)
|
| 179 |
|
| 180 |
|
| 181 | def parse_args(argv: Optional[Sequence[str]] = None) -> argparse.Namespace:
|
| 182 | parser = argparse.ArgumentParser(description=__doc__)
|
| 183 | parser.add_argument("--cwd", type=Path, default=Path.cwd())
|
| 184 | parser.add_argument("--runs", type=int, default=10)
|
| 185 | parser.add_argument("--warmup", type=int, default=1)
|
| 186 | parser.add_argument("--hyperfine", default="hyperfine")
|
| 187 | parser.add_argument("--jsonl", action="store_true", help="print one JSON row, including skip rows")
|
| 188 | parser.add_argument("--bench-id", default="manual")
|
| 189 | parser.add_argument("--scenario", default="external-timing")
|
| 190 | parser.add_argument("--operation", default="hyperfine.crosscheck")
|
| 191 | parser.add_argument("--run", type=int, default=1)
|
| 192 | parser.add_argument("--subject", default="unknown")
|
| 193 | parser.add_argument("--subject-kind", default="unknown")
|
| 194 | parser.add_argument("command", nargs=argparse.REMAINDER, help="command to measure, usually after --")
|
| 195 | args = parser.parse_args(argv)
|
| 196 | if args.command and args.command[0] == "--":
|
| 197 | args.command = args.command[1:]
|
| 198 | if not args.command:
|
| 199 | parser.error("command is required; pass it after --")
|
| 200 | if args.runs <= 0:
|
| 201 | parser.error("--runs must be positive")
|
| 202 | if args.warmup < 0:
|
| 203 | parser.error("--warmup must be non-negative")
|
| 204 | return args
|
| 205 |
|
| 206 |
|
| 207 | def main(argv: Optional[Sequence[str]] = None) -> int:
|
| 208 | args = parse_args(argv)
|
| 209 | row_kwargs = {
|
| 210 | "bench_id": args.bench_id,
|
| 211 | "scenario": args.scenario,
|
| 212 | "operation": args.operation,
|
| 213 | "run": args.run,
|
| 214 | "subject": args.subject,
|
| 215 | "subject_kind": args.subject_kind,
|
| 216 | }
|
| 217 | try:
|
| 218 | result = run_hyperfine(
|
| 219 | args.command,
|
| 220 | cwd=args.cwd,
|
| 221 | runs=args.runs,
|
| 222 | warmup=args.warmup,
|
| 223 | executable=args.hyperfine,
|
| 224 | env=host_home_environment(),
|
| 225 | )
|
| 226 | except HyperfineUnavailable as exc:
|
| 227 | if args.jsonl:
|
| 228 | print(json.dumps(skip_row(command=args.command, reason=str(exc), **row_kwargs), sort_keys=True))
|
| 229 | else:
|
| 230 | print(str(exc), file=sys.stderr)
|
| 231 | return SKIP_RETURNCODE
|
| 232 | except HyperfineFailed as exc:
|
| 233 | print(f"hyperfine failed with exit code {exc.returncode}: {exc}", file=sys.stderr)
|
| 234 | return exc.returncode or 1
|
| 235 |
|
| 236 | print(json.dumps(measurement_row(command=args.command, hyperfine_result=result, **row_kwargs), sort_keys=True))
|
| 237 | return 0
|
| 238 |
|
| 239 |
|
| 240 | if __name__ == "__main__":
|
| 241 | raise SystemExit(main())
|