#!/usr/bin/env python3 # Performance regression check. # # Runs bench/run.sh (or consumes its output from stdin / a file), parses # the throughput table and the per-arm latency stanzas, and diffs every # metric against bench/baseline.json. Exits 0 if every metric is within # its per-metric tolerance; exits 1 with a per-metric diff table on any # regression. # # Tolerances are one-sided: only metrics that got *worse* than baseline # (slower wall-time, larger RSS, larger latency, larger ratio) trigger a # regression. Improvements never fire — they show in the diff with a "+" # sign and a note, so an intentional improvement can be ratified into # the baseline with `--update-baseline`. # # Usage: # bench/check.py # spawn bench/run.sh -n 5 # bench/check.py --from-file out.txt # consume captured output # cat out.txt | bench/check.py --stdin # bench/check.py --update-baseline # re-run, write new baseline # bench/check.py --baseline path.json # alternate baseline location from __future__ import annotations import argparse import json import re import subprocess import sys from dataclasses import dataclass from pathlib import Path ROOT = Path(__file__).resolve().parent.parent DEFAULT_BASELINE = ROOT / "bench" / "baseline.json" RUN_SH = ROOT / "bench" / "run.sh" @dataclass class Measurement: metric_id: str baseline: float tolerance_pct: float actual: float @property def diff_pct(self) -> float: if self.baseline == 0: return 0.0 return 100.0 * (self.actual - self.baseline) / self.baseline @property def regressed(self) -> bool: return self.diff_pct > self.tolerance_pct def parse_throughput_table(text: str) -> dict[str, dict[str, float]]: # The throughput table has a header row, a separator row, then one row # per workload. Columns are pipe-separated. out: dict[str, dict[str, float]] = {} in_table = False for line in text.splitlines(): if line.startswith("workload") and "gc(s)" in line: in_table = True continue if in_table and line.startswith("---"): continue if in_table: if not line.strip() or "|" not in line: in_table = False continue cells = [c.strip() for c in line.split("|")] if len(cells) != 9: continue workload = cells[0] try: out[workload] = { "gc_s": float(cells[1]), "bump_s": float(cells[2]), "rc_s": float(cells[3]), "gc_over_bump": float(cells[4].rstrip("x")), "rc_over_bump": float(cells[5].rstrip("x")), "gc_rss_kb": float(cells[6]), "bump_rss_kb": float(cells[7]), "rc_rss_kb": float(cells[8]), } except ValueError: continue return out # Latency arm header looks like: "=== implicit @ gc (Boehm-fair) ===". # We map the leading prose label to the canonical arm key in baseline.json. ARM_LABEL_TO_KEY = { "implicit @ gc": "implicit_at_gc", "explicit @ rc": "explicit_at_rc", "implicit @ rc": "implicit_at_rc", } # Inside each arm stanza, lines that report a metric look like: # " median median= 96.4 range=[..., ...] us" # or: # " p99/median median=73.92x range=[...]" # We pick out the metric name and its "median=NN" value. LINE_RE = re.compile( r"^\s+(?P[a-zA-Z0-9./_]+)\s+median=\s*(?P[\d.]+)x?" ) HEADER_RE = re.compile(r"^=== (?P