#!/usr/bin/env python3 """Compare two `run_all.py` catalogs (e.g. RustPython vs. CPython) side by side. Usage ----- # Catalog a real CPython too (no psutil stub needed -- it has a real one): python3 scripts/pyperformance/run_all.py --python "$(which python3.13)" \\ --label cpython3.13 --no-psutil-stub # RustPython catalog, if not already generated: python3 scripts/pyperformance/run_all.py --python target/release/rustpython \\ --label rustpython # Compare them: python3 scripts/pyperformance/compare.py --baseline cpython3.13 --candidate rustpython Writes `scripts/pyperformance/results/COMPARE--vs-.md`. """ from __future__ import annotations import argparse import json import re from pathlib import Path SCRIPT_DIR = Path(__file__).resolve().parent DEFAULT_RESULTS_DIR = SCRIPT_DIR / "results" TIME_RE = re.compile(r"^\s*([\d.]+)\s*(ns|us|ms|sec)\b") UNIT_SECONDS = {"ns": 1e-9, "us": 1e-6, "ms": 1e-3, "sec": 1.0} def parse_mean_seconds(mean: str | None) -> float | None: """Parse a pyperf " +- " string into seconds.""" if not mean: return None m = TIME_RE.match(mean) if not m: return None value, unit = m.groups() return float(value) * UNIT_SECONDS[unit] def load_catalog(results_dir: Path, label: str) -> dict[str, dict]: catalog_json = results_dir / label / "catalog.json" if not catalog_json.exists(): raise SystemExit( f"no catalog for label {label!r} at {catalog_json} -- run " f"`run_all.py --label {label} ...` first" ) return {r["benchmark"]: r for r in json.loads(catalog_json.read_text())} def main() -> None: parser = argparse.ArgumentParser( description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter ) parser.add_argument( "--baseline", required=True, help="label of the baseline catalog, e.g. cpython3.13", ) parser.add_argument( "--candidate", required=True, help="label of the catalog to compare against it, e.g. rustpython", ) parser.add_argument( "--results-dir", type=Path, default=DEFAULT_RESULTS_DIR, help="parent directory containing