Viewing files at commit 523859ca17adCommit detailsView latest

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