fold/bench/perf_audit_run.py
trvon e30764fb19
bench: extend fold_eval scoring with labeled negative cases
Add negative_cases_from_universe to expand an explicit file-by-rule universe
into labeled negatives (enabling TP/FP/TN/FN, not just TP/FP), plus project-eval
harness updates in run_project_eval and the score/score_fp/quality-gate helpers.
2026-08-16 16:43:51 -06:00

271 lines
9.8 KiB
Python
Executable file
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
"""Multi-run perf measurement harness for fold (see docs/internal/perf-audit.md).
Single-run wall-clock diffs at fold's curl-baseline scale carry ~15%
run-to-run variance — too noisy to attribute sub-15% optimization
wins. This script:
1. Runs `fold scan --profile ...` N times against a fixed target.
2. Captures the --profile JSON each run.
3. Reports median + IQR + min/max for total_wall_ms and the new
per-detector phase timings (walker_ms, encode_ms, hook_ms,
state_merge_ms).
4. Optionally diffs against a saved baseline JSON (recorded by an
earlier invocation with --record).
Convention: snapshots land under docs/internal/perf-audit/runs/<label>.json
so per-commit baselines are diff-able from git.
Usage:
bench/perf_audit_run.py --label baseline --runs 5
bench/perf_audit_run.py --label after-fix --runs 5 \\
--diff docs/internal/perf-audit/runs/baseline.json
"""
from __future__ import annotations
import argparse
import json
import statistics
import subprocess
import sys
import time
from pathlib import Path
from typing import Any
REPO_ROOT = Path(__file__).resolve().parent.parent
DEFAULT_FOLD = REPO_ROOT / "build" / "release" / "src" / "cli" / "fold"
DEFAULT_COMPILE_DB = REPO_ROOT.parent / "curl-source" / "curl" / "build" / "compile_commands.json"
RUNS_DIR = REPO_ROOT / "docs" / "perf-audit" / "runs"
# Metrics surfaced from --profile JSON. Keep in sync with
# include/fold/cli/profile.hpp.
TOP_METRICS = [
"total_wall_ms",
"compile_db_load_ms",
"fanout_orchestration_ms",
"serial_dispatch_ms",
"function_fingerprint_ms",
"project_index_load_ms",
"project_index_update_ms",
"call_graph_ms",
"project_graph_collect_ms",
"project_graph_merge_ms",
"project_graph_fallback_ms",
"project_index_contribution_update_ms",
"project_index_snapshot_update_ms",
"security_candidate_ms",
"analyze_ms",
"report_ms",
]
DETECTOR_METRICS = [
"wall_ms",
"walker_ms",
"encode_ms",
"hook_ms",
"state_merge_ms",
"control_walk_ms",
"range_refine_ms",
"octagon_refine_ms",
"summary_lookup_ms",
"z3_queries",
"stmt_count",
"control_stmt_count",
"candidate_sink_count",
"functions_prechecked_safe",
"expr_cache_hits",
"expr_cache_misses",
]
def run_once(fold: Path, compile_db: Path, detectors: str, processes: int,
tmp_profile: Path, tmp_report: Path) -> dict[str, Any]:
"""Run fold once with --profile. Return the parsed profile JSON."""
cmd = [
str(fold), "--quiet", "scan",
"--compile-db", str(compile_db),
"--detectors", detectors,
"--processes", str(processes),
"--profile",
"--profile-output", str(tmp_profile),
"--output", str(tmp_report),
]
started = time.monotonic()
result = subprocess.run(cmd, capture_output=True, text=True)
wall_s = time.monotonic() - started
if result.returncode not in (0, 1):
sys.stderr.write(f"fold exited {result.returncode}\n")
sys.stderr.write(result.stderr)
raise RuntimeError("fold failed; see stderr above")
if not tmp_profile.exists():
raise RuntimeError(f"fold did not write profile to {tmp_profile}")
with tmp_profile.open() as f:
profile = json.load(f)
profile["_observed_wall_s"] = wall_s
return profile
def aggregate(runs: list[dict[str, Any]]) -> dict[str, Any]:
"""Reduce N --profile JSONs to one summary with median+IQR+min+max
for every metric we care about."""
summary: dict[str, Any] = {
"n_runs": len(runs),
"observed_wall_s": _stats([r["_observed_wall_s"] for r in runs]),
"scan": {},
"detectors": {},
}
for k in TOP_METRICS:
vals = [r["scan"].get(k, 0) for r in runs]
summary["scan"][k] = _stats(vals)
detector_names = sorted({d["name"] for r in runs for d in r.get("detectors", [])})
for name in detector_names:
per_run = []
for r in runs:
for d in r.get("detectors", []):
if d["name"] == name:
per_run.append(d)
break
per_metric: dict[str, dict[str, float]] = {}
for m in DETECTOR_METRICS:
vals = [d.get(m, 0) for d in per_run]
per_metric[m] = _stats(vals)
summary["detectors"][name] = per_metric
return summary
def _stats(vals: list[float]) -> dict[str, float]:
if not vals:
return {"median": 0, "iqr": 0, "min": 0, "max": 0}
vals = sorted(vals)
n = len(vals)
q1 = vals[n // 4]
q3 = vals[(3 * n) // 4]
return {
"median": statistics.median(vals),
"iqr": q3 - q1,
"min": vals[0],
"max": vals[-1],
"n": n,
}
def fmt_ms(d: dict[str, float]) -> str:
"""Render a stats dict as a compact "median±iqr (min..max)" string."""
return f"{int(d['median']):>7} iqr={int(d['iqr']):>5} range={int(d['min'])}..{int(d['max'])}"
def print_summary(label: str, summary: dict[str, Any]) -> None:
print(f"=== {label} ({summary['n_runs']} runs) ===")
print(f" observed_wall_s {summary['observed_wall_s']['median']:.1f}s "
f"iqr={summary['observed_wall_s']['iqr']:.1f}s "
f"range={summary['observed_wall_s']['min']:.1f}..{summary['observed_wall_s']['max']:.1f}")
print()
print(f" scan timings (ms):")
for k in TOP_METRICS:
d = summary["scan"][k]
if d["median"] > 0 or d["max"] > 0:
print(f" {k:30s} {fmt_ms(d)}")
print()
print(f" detector breakdown (ms):")
print(f" {'detector':18s} {'wall':>8s} {'walker':>8s} {'encode':>7s} {'hook':>7s} {'merge':>5s} {'queries':>7s}")
print(f" {'-'*18} {'-'*8} {'-'*8} {'-'*7} {'-'*7} {'-'*5} {'-'*7}")
for name in sorted(summary["detectors"].keys(),
key=lambda n: -summary["detectors"][n]["wall_ms"]["median"]):
d = summary["detectors"][name]
row = (
f" {name:18s} "
f"{int(d['wall_ms']['median']):>8} "
f"{int(d['walker_ms']['median']):>8} "
f"{int(d['encode_ms']['median']):>7} "
f"{int(d['hook_ms']['median']):>7} "
f"{int(d['state_merge_ms']['median']):>5} "
f"{int(d['z3_queries']['median']):>7}"
)
print(row)
def print_diff(baseline: dict[str, Any], after: dict[str, Any]) -> None:
"""Side-by-side diff. Flags moves outside median±IQR as "significant"."""
print()
print("=== Diff (after vs baseline) ===")
print(f"{'metric':40s} {'baseline':>12s} {'after':>12s} {'delta':>10s} {'sig?':>5s}")
print(f"{'-'*40} {'-'*12} {'-'*12} {'-'*10} {'-'*5}")
def diff_one(label: str, b: dict[str, float], a: dict[str, float]) -> None:
bm, am = b["median"], a["median"]
delta = am - bm
# Significant if delta exceeds 1.5 × the larger IQR.
threshold = 1.5 * max(b["iqr"], a["iqr"], 1)
sig = "yes" if abs(delta) > threshold else "no"
pct = f"({delta * 100 / bm:+.1f}%)" if bm else ""
print(f"{label:40s} {int(bm):>12} {int(am):>12} {int(delta):>+8}{pct:>10s} {sig:>5}")
diff_one("scan.total_wall_ms", baseline["scan"]["total_wall_ms"], after["scan"]["total_wall_ms"])
for k in TOP_METRICS[1:]:
diff_one(f"scan.{k}", baseline["scan"][k], after["scan"][k])
for name in sorted(set(baseline["detectors"]) | set(after["detectors"])):
if name not in baseline["detectors"] or name not in after["detectors"]:
continue
b = baseline["detectors"][name]
a = after["detectors"][name]
for m in DETECTOR_METRICS:
diff_one(f"{name}.{m}", b[m], a[m])
def main() -> int:
ap = argparse.ArgumentParser(description=__doc__,
formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument("--fold", type=Path, default=DEFAULT_FOLD)
ap.add_argument("--compile-db", type=Path, default=DEFAULT_COMPILE_DB)
ap.add_argument("--detectors", default="clone,dead,equiv,null-deref")
ap.add_argument("--processes", type=int, default=4)
ap.add_argument("--runs", type=int, default=5,
help="number of fold invocations to median over (default 5)")
ap.add_argument("--label", default="run",
help="filename under docs/internal/perf-audit/runs/ to save the summary")
ap.add_argument("--diff", type=Path,
help="path to a baseline summary JSON to diff against")
args = ap.parse_args()
if not args.fold.exists():
sys.stderr.write(f"fold binary not found at {args.fold}\n")
return 2
if not args.compile_db.exists():
sys.stderr.write(f"compile-db not found at {args.compile_db}\n")
return 2
RUNS_DIR.mkdir(parents=True, exist_ok=True)
runs: list[dict[str, Any]] = []
tmp_profile = Path("/tmp/perf-audit-profile.json")
tmp_report = Path("/tmp/perf-audit-report.sarif")
for i in range(args.runs):
sys.stderr.write(f"[{i + 1}/{args.runs}] running fold...\n")
try:
profile = run_once(args.fold, args.compile_db, args.detectors,
args.processes, tmp_profile, tmp_report)
except Exception as e:
sys.stderr.write(f"run {i + 1} failed: {e}\n")
return 1
runs.append(profile)
summary = aggregate(runs)
out_path = RUNS_DIR / f"{args.label}.json"
with out_path.open("w") as f:
json.dump(summary, f, indent=2)
sys.stderr.write(f"saved summary to {out_path}\n")
print_summary(args.label, summary)
if args.diff:
if not args.diff.exists():
sys.stderr.write(f"diff baseline not found at {args.diff}\n")
return 2
with args.diff.open() as f:
baseline = json.load(f)
print_diff(baseline, summary)
return 0
if __name__ == "__main__":
sys.exit(main())