166 lines
5.5 KiB
Python
166 lines
5.5 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Validate benchmark-vs-TA-Lib results against guardrail thresholds.
|
|
|
|
This is intentionally conservative: it catches severe regressions and incomplete
|
|
benchmark outputs, without overfitting to one machine.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
from pathlib import Path
|
|
|
|
|
|
def _parse_threshold_items(items: list[str]) -> dict[int, float]:
|
|
thresholds: dict[int, float] = {}
|
|
for item in items:
|
|
if "=" not in item:
|
|
raise ValueError(f"Invalid threshold '{item}', expected SIZE=VALUE")
|
|
size_s, value_s = item.split("=", 1)
|
|
thresholds[int(size_s)] = float(value_s)
|
|
return thresholds
|
|
|
|
|
|
def _percentile(values: list[float], q: float) -> float:
|
|
"""Return the q percentile using linear interpolation."""
|
|
if not values:
|
|
raise ValueError("Cannot compute percentile of empty sequence")
|
|
if q <= 0:
|
|
return min(values)
|
|
if q >= 100:
|
|
return max(values)
|
|
|
|
values = sorted(values)
|
|
rank = (len(values) - 1) * (q / 100.0)
|
|
lower = int(rank)
|
|
upper = min(lower + 1, len(values) - 1)
|
|
weight = rank - lower
|
|
return values[lower] * (1.0 - weight) + values[upper] * weight
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser(
|
|
description="Check TA-Lib benchmark JSON against regression thresholds."
|
|
)
|
|
parser.add_argument(
|
|
"--input",
|
|
default="benchmark_vs_talib.json",
|
|
help="Path to benchmark JSON produced by benchmarks/bench_vs_talib.py",
|
|
)
|
|
parser.add_argument(
|
|
"--min-rows",
|
|
type=int,
|
|
default=10,
|
|
help="Minimum benchmark rows required per size",
|
|
)
|
|
parser.add_argument(
|
|
"--median-floor",
|
|
action="append",
|
|
default=["10000=0.35", "100000=0.35"],
|
|
help="Required minimum median speedup per size, e.g. 100000=0.5 (repeatable)",
|
|
)
|
|
parser.add_argument(
|
|
"--min-speedup-floor",
|
|
action="append",
|
|
default=["10000=0.10", "100000=0.10"],
|
|
help="Hard minimum per-row speedup floor per size, e.g. 100000=0.1 (repeatable)",
|
|
)
|
|
parser.add_argument(
|
|
"--tail-percentile",
|
|
type=float,
|
|
default=10.0,
|
|
help="Tail percentile used for distribution-based slowdown checks (default: 10)",
|
|
)
|
|
parser.add_argument(
|
|
"--tail-speedup-floor",
|
|
action="append",
|
|
default=["10000=0.20", "100000=0.20"],
|
|
help="Required minimum tail percentile speedup per size, e.g. 100000=0.2 (repeatable)",
|
|
)
|
|
args = parser.parse_args()
|
|
|
|
path = Path(args.input)
|
|
if not path.exists():
|
|
print(f"ERROR: benchmark file not found: {path}")
|
|
return 1
|
|
|
|
data = json.loads(path.read_text(encoding="utf-8"))
|
|
if not data.get("talib_available", False):
|
|
print(
|
|
"ERROR: TA-Lib was not available; cannot enforce TA-Lib regression policy."
|
|
)
|
|
return 1
|
|
|
|
summary_by_size = {
|
|
int(entry.get("size")): entry
|
|
for entry in data.get("summary", {}).get("by_size", [])
|
|
if entry.get("size") is not None
|
|
}
|
|
results_by_size: dict[int, list[dict[str, object]]] = {}
|
|
for row in data.get("results", []):
|
|
if "speedup" not in row or row.get("size") is None:
|
|
continue
|
|
size = int(row["size"])
|
|
results_by_size.setdefault(size, []).append(row)
|
|
|
|
median_floor = _parse_threshold_items(args.median_floor)
|
|
min_speedup_floor = _parse_threshold_items(args.min_speedup_floor)
|
|
tail_speedup_floor = _parse_threshold_items(args.tail_speedup_floor)
|
|
required_sizes = sorted(
|
|
set(median_floor) | set(min_speedup_floor) | set(tail_speedup_floor)
|
|
)
|
|
|
|
failures: list[str] = []
|
|
for size in required_sizes:
|
|
entry = summary_by_size.get(size)
|
|
if entry is None:
|
|
failures.append(f"missing summary for size={size}")
|
|
continue
|
|
rows_for_size = results_by_size.get(size, [])
|
|
if not rows_for_size:
|
|
failures.append(f"missing detailed rows for size={size}")
|
|
continue
|
|
|
|
rows = int(entry.get("rows", 0))
|
|
med = float(entry.get("median_speedup", 0.0))
|
|
min_s = float(entry.get("min_speedup", 0.0))
|
|
speedups = [float(row["speedup"]) for row in rows_for_size]
|
|
tail_s = _percentile(speedups, args.tail_percentile)
|
|
print(
|
|
"size="
|
|
f"{size}: rows={rows}, median_speedup={med:.4f}, "
|
|
f"p{args.tail_percentile:g}_speedup={tail_s:.4f}, min_speedup={min_s:.4f}"
|
|
)
|
|
|
|
if rows < args.min_rows:
|
|
failures.append(f"size={size} rows {rows} < min_rows {args.min_rows}")
|
|
if med < median_floor.get(size, float("-inf")):
|
|
failures.append(
|
|
f"size={size} median_speedup {med:.4f} < floor {median_floor[size]:.4f}"
|
|
)
|
|
if tail_s < tail_speedup_floor.get(size, float("-inf")):
|
|
failures.append(
|
|
"size="
|
|
f"{size} p{args.tail_percentile:g}_speedup {tail_s:.4f} "
|
|
f"< floor {tail_speedup_floor[size]:.4f}"
|
|
)
|
|
if min_s < min_speedup_floor.get(size, float("-inf")):
|
|
failures.append(
|
|
f"size={size} min_speedup {min_s:.4f} < floor {min_speedup_floor[size]:.4f}"
|
|
)
|
|
|
|
if failures:
|
|
print("FAILED benchmark regression policy:")
|
|
for failure in failures:
|
|
print(f" - {failure}")
|
|
return 1
|
|
|
|
print("PASS benchmark regression policy.")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|