| 1 | #!/usr/bin/env python3
|
| 2 | """External cross-check against git's own ``t/perf`` suite.
|
| 3 |
|
| 4 | License boundary (GPLv2): git's ``t/perf`` scripts are GPLv2 and are NEVER
|
| 5 | vendored, copied, or translated into this repository. This module drives the
|
| 6 | upstream suite at arm's length only β a subprocess invocation of
|
| 7 | ``<git-src>/t/perf/run`` inside the user's own git source tree β and then
|
| 8 | aggregates the raw numeric output files (``t/test-results/*.result``) that the
|
| 9 | suite leaves behind. Reading numbers out of result files is data aggregation,
|
| 10 | not derivation of GPL code; no shell logic crosses the boundary.
|
| 11 |
|
| 12 | What this gives the harness: git's own perf numbers for the same corpus
|
| 13 | fixture (``GIT_PERF_REPO``), labeled ``measurement_source:
|
| 14 | "git_tperf_external"``. t/perf reports min-of-N while our lanes report
|
| 15 | medians, so the comparison is a sanity band (see ``sanity_band``), never an
|
| 16 | equality check.
|
| 17 |
|
| 18 | Result-file format (observed, liberal): each ``<script>.<n>.result`` file
|
| 19 | holds one line per repetition of whitespace-separated floats β GNU-time-style
|
| 20 | ``real user sys`` seconds. We take the first float as real seconds and any
|
| 21 | second/third as user/sys; malformed lines are skipped, never fatal.
|
| 22 | ``<script>.<n>.descr`` (when present) holds the human description;
|
| 23 | ``<script>.subtests`` lists test numbers and is not needed for parsing.
|
| 24 |
|
| 25 | Modes:
|
| 26 |
|
| 27 | - run mode (default): requires ``--git-src`` pointing at a git source tree
|
| 28 | containing ``t/perf/run``. Missing or not such a tree -> exit 3 with skip
|
| 29 | JSON ``{"status": "skipped", "reason": "git_src_missing:<path>"}`` on
|
| 30 | stdout (skip honesty: no git source tree is a coverage gap, not an error).
|
| 31 | A nonzero exit from ``./run`` is recorded in a run-record row, never a hard
|
| 32 | failure β partial result files are still parsed.
|
| 33 | - ``--parse-only DIR``: skip running; parse an existing ``t/test-results``
|
| 34 | directory. This is the testable path (unittest runs without a git tree).
|
| 35 |
|
| 36 | Output: JSONL under ``--results`` (a directory): one row per repetition plus
|
| 37 | one ``min_of_n`` summary row per test, all stamped with runner identity
|
| 38 | (ADR-0007) via ``oakbench.runner``.
|
| 39 |
|
| 40 | Exit codes: 0 on parsed rows; 3 skip (git source tree or results missing);
|
| 41 | 2 usage errors.
|
| 42 | """
|
| 43 |
|
| 44 | from __future__ import annotations
|
| 45 |
|
| 46 | import argparse
|
| 47 | import json
|
| 48 | import os
|
| 49 | import subprocess
|
| 50 | import sys
|
| 51 | from pathlib import Path
|
| 52 | from typing import Any, Optional
|
| 53 |
|
| 54 | sys.path.insert(0, str(Path(__file__).resolve().parent))
|
| 55 |
|
| 56 | from oakbench.runner import runner_fields, stamp_row
|
| 57 |
|
| 58 | SCHEMA_VERSION = 1
|
| 59 | MEASUREMENT_SOURCE = "git_tperf_external"
|
| 60 | RESULTS_FILENAME = "tperf_external.jsonl"
|
| 61 | DEFAULT_REPEAT_COUNT = 10
|
| 62 |
|
| 63 | EXIT_OK = 0
|
| 64 | EXIT_USAGE = 2
|
| 65 | EXIT_SKIP = 3
|
| 66 |
|
| 67 |
|
| 68 | def parse_args(argv: Optional[list[str]] = None) -> argparse.Namespace:
|
| 69 | parser = argparse.ArgumentParser(
|
| 70 | description="Run git's t/perf suite at arm's length and aggregate its raw .result files."
|
| 71 | )
|
| 72 | parser.add_argument(
|
| 73 | "--git-src",
|
| 74 | type=Path,
|
| 75 | default=None,
|
| 76 | help="Git source tree containing t/perf (missing/not-a-git-tree -> exit 3 skip JSON)",
|
| 77 | )
|
| 78 | parser.add_argument(
|
| 79 | "--perf-repo",
|
| 80 | type=Path,
|
| 81 | default=None,
|
| 82 | help="Corpus fixture repository exported as GIT_PERF_REPO",
|
| 83 | )
|
| 84 | parser.add_argument(
|
| 85 | "--repeat-count",
|
| 86 | type=int,
|
| 87 | default=DEFAULT_REPEAT_COUNT,
|
| 88 | help=f"Exported as GIT_PERF_REPEAT_COUNT (default {DEFAULT_REPEAT_COUNT})",
|
| 89 | )
|
| 90 | parser.add_argument(
|
| 91 | "--tests",
|
| 92 | type=str,
|
| 93 | default=None,
|
| 94 | help="Optional CSV subset of perf scripts, e.g. p0001-rev-list.sh",
|
| 95 | )
|
| 96 | parser.add_argument(
|
| 97 | "--results",
|
| 98 | type=Path,
|
| 99 | required=True,
|
| 100 | help=f"Directory for the output JSONL ({RESULTS_FILENAME})",
|
| 101 | )
|
| 102 | parser.add_argument(
|
| 103 | "--parse-only",
|
| 104 | type=Path,
|
| 105 | default=None,
|
| 106 | metavar="DIR",
|
| 107 | help="Skip running; parse an existing t/test-results directory",
|
| 108 | )
|
| 109 | return parser.parse_args(argv)
|
| 110 |
|
| 111 |
|
| 112 | def _floats(tokens: list[str]) -> Optional[list[float]]:
|
| 113 | try:
|
| 114 | return [float(token) for token in tokens]
|
| 115 | except ValueError:
|
| 116 | return None
|
| 117 |
|
| 118 |
|
| 119 | def _split_result_stem(stem: str) -> tuple[str, Optional[str]]:
|
| 120 | """``p0001-rev-list.1`` -> (``p0001-rev-list``, ``1``)."""
|
| 121 | if "." in stem:
|
| 122 | script, number = stem.rsplit(".", 1)
|
| 123 | return script, number
|
| 124 | return stem, None
|
| 125 |
|
| 126 |
|
| 127 | def _description_for(results_dir: Path, script: str, number: Optional[str]) -> Optional[str]:
|
| 128 | if number is None:
|
| 129 | return None
|
| 130 | descr_path = results_dir / f"{script}.{number}.descr"
|
| 131 | try:
|
| 132 | text = descr_path.read_text(errors="replace").strip()
|
| 133 | except OSError:
|
| 134 | return None
|
| 135 | return text or None
|
| 136 |
|
| 137 |
|
| 138 | def _ms(seconds: Optional[float]) -> Optional[float]:
|
| 139 | if seconds is None:
|
| 140 | return None
|
| 141 | return round(seconds * 1000.0, 6)
|
| 142 |
|
| 143 |
|
| 144 | def parse_tperf_results(results_dir: Path) -> list[dict[str, Any]]:
|
| 145 | """Parse every ``*.result`` file in a t/test-results directory into rows.
|
| 146 |
|
| 147 | One row per repetition line plus one min-of-N summary row per test.
|
| 148 | Liberal: each line is N whitespace-separated floats; the first is real
|
| 149 | seconds, optional second/third are user/sys. Malformed lines are skipped.
|
| 150 | """
|
| 151 | rows: list[dict[str, Any]] = []
|
| 152 | for result_path in sorted(results_dir.glob("*.result")):
|
| 153 | script, number = _split_result_stem(result_path.stem)
|
| 154 | tperf_test = f"{script}.{number}" if number is not None else script
|
| 155 | description = _description_for(results_dir, script, number)
|
| 156 | try:
|
| 157 | text = result_path.read_text(errors="replace")
|
| 158 | except OSError:
|
| 159 | continue
|
| 160 |
|
| 161 | reals_ms: list[float] = []
|
| 162 | repetition = 0
|
| 163 | for line in text.splitlines():
|
| 164 | tokens = line.split()
|
| 165 | if not tokens:
|
| 166 | continue
|
| 167 | values = _floats(tokens)
|
| 168 | if not values:
|
| 169 | continue # malformed line: skipped, never fatal
|
| 170 | repetition += 1
|
| 171 | real_ms = _ms(values[0])
|
| 172 | reals_ms.append(real_ms)
|
| 173 | rows.append(
|
| 174 | {
|
| 175 | "schema_version": SCHEMA_VERSION,
|
| 176 | "measurement_source": MEASUREMENT_SOURCE,
|
| 177 | "tperf_script": script,
|
| 178 | "tperf_test": tperf_test,
|
| 179 | "tperf_description": description,
|
| 180 | "repetition": repetition,
|
| 181 | "elapsed_ms": real_ms,
|
| 182 | "user_ms": _ms(values[1]) if len(values) > 1 else None,
|
| 183 | "sys_ms": _ms(values[2]) if len(values) > 2 else None,
|
| 184 | "subject": "git",
|
| 185 | "subject_kind": "git",
|
| 186 | }
|
| 187 | )
|
| 188 | if reals_ms:
|
| 189 | rows.append(
|
| 190 | {
|
| 191 | "schema_version": SCHEMA_VERSION,
|
| 192 | "measurement_source": MEASUREMENT_SOURCE,
|
| 193 | "tperf_script": script,
|
| 194 | "tperf_test": tperf_test,
|
| 195 | "tperf_description": description,
|
| 196 | "tperf_statistic": "min_of_n",
|
| 197 | "elapsed_ms": min(reals_ms),
|
| 198 | "n": len(reals_ms),
|
| 199 | "subject": "git",
|
| 200 | "subject_kind": "git",
|
| 201 | }
|
| 202 | )
|
| 203 | return rows
|
| 204 |
|
| 205 |
|
| 206 | def sanity_band(
|
| 207 | tperf_min_ms: Optional[float],
|
| 208 | our_median_ms: Optional[float],
|
| 209 | *,
|
| 210 | tolerance_pct: float = 50,
|
| 211 | ) -> dict[str, Any]:
|
| 212 | """Sanity band between t/perf's min-of-N and our median for one operation.
|
| 213 |
|
| 214 | ``ratio`` is ``our_median_ms / tperf_min_ms``; ``within_band`` is true when
|
| 215 | the ratio sits inside ``1 +/- tolerance_pct/100``. Guards (documented):
|
| 216 | when either input is None or ``tperf_min_ms <= 0`` (zero-division guard),
|
| 217 | ``ratio`` is None and ``within_band`` is False β unmeasured never passes.
|
| 218 | """
|
| 219 | result: dict[str, Any] = {
|
| 220 | "tperf_min_ms": tperf_min_ms,
|
| 221 | "our_median_ms": our_median_ms,
|
| 222 | "tolerance_pct": tolerance_pct,
|
| 223 | "ratio": None,
|
| 224 | "within_band": False,
|
| 225 | }
|
| 226 | if tperf_min_ms is None or our_median_ms is None:
|
| 227 | return result
|
| 228 | if tperf_min_ms <= 0:
|
| 229 | return result
|
| 230 | ratio = float(our_median_ms) / float(tperf_min_ms)
|
| 231 | result["ratio"] = ratio
|
| 232 | result["within_band"] = abs(ratio - 1.0) * 100.0 <= tolerance_pct
|
| 233 | return result
|
| 234 |
|
| 235 |
|
| 236 | def _skip(reason: str) -> int:
|
| 237 | print(json.dumps({"status": "skipped", "reason": reason}))
|
| 238 | return EXIT_SKIP
|
| 239 |
|
| 240 |
|
| 241 | def run_tperf(git_src: Path, args: argparse.Namespace) -> dict[str, Any]:
|
| 242 | """Invoke ``./run`` inside <git-src>/t/perf at arm's length; record only."""
|
| 243 | perf_dir = git_src / "t" / "perf"
|
| 244 | command = ["./run"]
|
| 245 | if args.tests:
|
| 246 | command.extend(name.strip() for name in args.tests.split(",") if name.strip())
|
| 247 | env = dict(os.environ)
|
| 248 | if args.perf_repo is not None:
|
| 249 | env["GIT_PERF_REPO"] = str(args.perf_repo.resolve())
|
| 250 | env["GIT_PERF_REPEAT_COUNT"] = str(args.repeat_count)
|
| 251 | try:
|
| 252 | proc = subprocess.run(
|
| 253 | command,
|
| 254 | cwd=str(perf_dir),
|
| 255 | env=env,
|
| 256 | capture_output=True,
|
| 257 | text=True,
|
| 258 | )
|
| 259 | returncode: Optional[int] = proc.returncode
|
| 260 | error: Optional[str] = None
|
| 261 | except OSError as exc:
|
| 262 | returncode = None
|
| 263 | error = str(exc)
|
| 264 | # Nonzero/failed runs are recorded, never fatal: partial .result files
|
| 265 | # are still worth aggregating.
|
| 266 | return {
|
| 267 | "schema_version": SCHEMA_VERSION,
|
| 268 | "measurement_source": MEASUREMENT_SOURCE,
|
| 269 | "record_kind": "tperf_run",
|
| 270 | "command": command,
|
| 271 | "cwd": str(perf_dir),
|
| 272 | "git_perf_repo": str(args.perf_repo) if args.perf_repo is not None else None,
|
| 273 | "git_perf_repeat_count": args.repeat_count,
|
| 274 | "returncode": returncode,
|
| 275 | "error": error,
|
| 276 | "subject": "git",
|
| 277 | "subject_kind": "git",
|
| 278 | }
|
| 279 |
|
| 280 |
|
| 281 | def main(argv: Optional[list[str]] = None) -> int:
|
| 282 | args = parse_args(argv)
|
| 283 |
|
| 284 | rows: list[dict[str, Any]] = []
|
| 285 |
|
| 286 | if args.parse_only is not None:
|
| 287 | results_dir = args.parse_only
|
| 288 | if not results_dir.is_dir():
|
| 289 | return _skip(f"results_dir_missing:{results_dir}")
|
| 290 | else:
|
| 291 | git_src = args.git_src
|
| 292 | if git_src is None:
|
| 293 | return _skip("git_src_missing:")
|
| 294 | run_script = git_src / "t" / "perf" / "run"
|
| 295 | if not git_src.is_dir() or not run_script.is_file():
|
| 296 | return _skip(f"git_src_missing:{git_src}")
|
| 297 | rows.append(run_tperf(git_src, args))
|
| 298 | results_dir = git_src / "t" / "test-results"
|
| 299 | if not results_dir.is_dir():
|
| 300 | return _skip(f"results_dir_missing:{results_dir}")
|
| 301 |
|
| 302 | rows.extend(parse_tperf_results(results_dir))
|
| 303 |
|
| 304 | fields = runner_fields()
|
| 305 | stamped = [stamp_row(row, fields) for row in rows]
|
| 306 |
|
| 307 | args.results.mkdir(parents=True, exist_ok=True)
|
| 308 | out_path = args.results / RESULTS_FILENAME
|
| 309 | with out_path.open("w", encoding="utf-8") as fh:
|
| 310 | for row in stamped:
|
| 311 | fh.write(json.dumps(row, sort_keys=True) + "\n")
|
| 312 | print(f"[result] {out_path} ({len(stamped)} rows)", file=sys.stderr)
|
| 313 |
|
| 314 | measured = [row for row in stamped if "elapsed_ms" in row]
|
| 315 | return EXIT_OK if measured else EXIT_SKIP
|
| 316 |
|
| 317 |
|
| 318 | if __name__ == "__main__":
|
| 319 | raise SystemExit(main())
|