add benchmark scriptr
This commit is contained in:
@@ -0,0 +1,194 @@
|
||||
#!/usr/bin/env python
|
||||
"""
|
||||
benchmark cold-start import time for every top-level import in src/*.py and simulation/*.py.
|
||||
|
||||
spawns a fresh python subprocess per import, mimicking the cold start of sloppy.py,
|
||||
and prints a sorted, color-coded listing with outliers highlighted.
|
||||
|
||||
usage: uv run python scripts/benchmark_imports.py [--runs N] [--timeout SEC] [--top N]
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import ast
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
from collections import defaultdict
|
||||
from pathlib import Path
|
||||
from statistics import median
|
||||
from typing import Iterable
|
||||
|
||||
GREEN = "\033[32m"
|
||||
YELLOW = "\033[33m"
|
||||
RED = "\033[31m"
|
||||
BOLD = "\033[1m"
|
||||
DIM = "\033[2m"
|
||||
RESET = "\033[0m"
|
||||
|
||||
DEFAULT_SCAN_DIRS = ("./src", "./simulation")
|
||||
DEFAULT_RUNS = 3
|
||||
DEFAULT_TIMEOUT = 30
|
||||
DEFAULT_TOP = 10
|
||||
DEFAULT_SLOW_MS = 200.0
|
||||
DEFAULT_MODERATE_MS = 50.0
|
||||
|
||||
|
||||
def gather_imports(scan_dirs: Iterable[str]) -> dict[str, list[str]]:
|
||||
imports: dict[str, set[str]] = defaultdict(set)
|
||||
for scan_dir in scan_dirs:
|
||||
for py_file in Path(scan_dir).rglob("*.py"):
|
||||
try:
|
||||
tree = ast.parse(py_file.read_text(encoding="utf-8", errors="replace"))
|
||||
except (SyntaxError, OSError):
|
||||
continue
|
||||
for node in tree.body:
|
||||
if isinstance(node, ast.Import):
|
||||
for alias in node.names:
|
||||
if alias.name == "__future__":
|
||||
continue
|
||||
imports[alias.name].add(str(py_file))
|
||||
elif isinstance(node, ast.ImportFrom):
|
||||
if not node.module or node.level != 0:
|
||||
continue
|
||||
if node.module == "__future__":
|
||||
continue
|
||||
imports[node.module].add(str(py_file))
|
||||
return {k: sorted(v) for k, v in imports.items()}
|
||||
|
||||
|
||||
def measure_import(module: str, sys_path: list[str], runs: int, timeout: int) -> tuple[float, str]:
|
||||
times: list[float] = []
|
||||
last_err = "no runs"
|
||||
path_setup = ";".join(f"sys.path.insert(0, {p!r})" for p in sys_path)
|
||||
for _ in range(runs):
|
||||
script = (
|
||||
"import sys, time;"
|
||||
+ path_setup + ";"
|
||||
+ f"t=time.perf_counter();"
|
||||
+ f"__import__({module!r});"
|
||||
+ f"print(time.perf_counter()-t)"
|
||||
)
|
||||
try:
|
||||
result = subprocess.run(
|
||||
[sys.executable, "-c", script],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=timeout,
|
||||
)
|
||||
except subprocess.TimeoutExpired:
|
||||
last_err = f"timeout>{timeout}s"
|
||||
continue
|
||||
if result.returncode != 0:
|
||||
err_lines = (result.stderr or "").strip().splitlines()
|
||||
last_err = (err_lines[-1] if err_lines else "non-zero exit")[:120]
|
||||
continue
|
||||
try:
|
||||
times.append(float((result.stdout or "").strip()))
|
||||
except ValueError:
|
||||
last_err = f"parse: {(result.stdout or '').strip()[:80]}"
|
||||
if not times:
|
||||
return (float("inf"), last_err)
|
||||
return (median(times), "ok")
|
||||
|
||||
|
||||
def color_for(t: float, slow_ms: float, moderate_ms: float) -> str:
|
||||
if t == float("inf"):
|
||||
return DIM
|
||||
if t * 1000 > slow_ms:
|
||||
return RED
|
||||
if t * 1000 > moderate_ms:
|
||||
return YELLOW
|
||||
return GREEN
|
||||
|
||||
|
||||
def main() -> int:
|
||||
ap = argparse.ArgumentParser(description="Benchmark cold-start import times for src/ and simulation/ files")
|
||||
ap.add_argument("--runs", type=int, default=DEFAULT_RUNS, help=f"subprocess runs per import (default {DEFAULT_RUNS})")
|
||||
ap.add_argument("--timeout", type=int, default=DEFAULT_TIMEOUT, help=f"per-subprocess timeout in seconds (default {DEFAULT_TIMEOUT})")
|
||||
ap.add_argument("--top", type=int, default=DEFAULT_TOP, help=f"top-N recommendations to list (default {DEFAULT_TOP})")
|
||||
ap.add_argument("--slow-ms", type=float, default=DEFAULT_SLOW_MS, help=f"slow threshold in ms (default {DEFAULT_SLOW_MS})")
|
||||
ap.add_argument("--moderate-ms", type=float, default=DEFAULT_MODERATE_MS, help=f"moderate threshold in ms (default {DEFAULT_MODERATE_MS})")
|
||||
ap.add_argument("--no-color", action="store_true", help="disable ANSI color output (deprecated, prefer --color=never)")
|
||||
ap.add_argument("--color", choices=("auto", "always", "never"), default="auto", help="color output mode (default auto: TTY only)")
|
||||
ap.add_argument("--scan-dir", action="append", default=None, help="additional scan directory (repeatable)")
|
||||
args = ap.parse_args()
|
||||
|
||||
if args.no_color:
|
||||
args.color = "never"
|
||||
no_color_env = os.environ.get("NO_COLOR", "").strip().lower() in ("1", "true", "yes")
|
||||
force_color_env = os.environ.get("FORCE_COLOR", "").strip().lower() in ("1", "true", "yes")
|
||||
if args.color == "always" or force_color_env:
|
||||
use_color = True
|
||||
elif args.color == "never" or no_color_env:
|
||||
use_color = False
|
||||
else:
|
||||
use_color = sys.stdout.isatty()
|
||||
if not use_color:
|
||||
global GREEN, YELLOW, RED, BOLD, DIM, RESET
|
||||
GREEN = YELLOW = RED = BOLD = DIM = RESET = ""
|
||||
|
||||
project_root = os.path.abspath(".")
|
||||
thirdparty = os.path.join(project_root, "thirdparty")
|
||||
sys_path = [project_root, thirdparty]
|
||||
|
||||
scan_dirs: tuple[str, ...] = tuple(args.scan_dir) if args.scan_dir else DEFAULT_SCAN_DIRS
|
||||
|
||||
print(f"{BOLD}scanning imports in: {', '.join(scan_dirs)}{RESET}")
|
||||
print(f"project root: {project_root}")
|
||||
print(f"sys.path: {sys_path}\n")
|
||||
|
||||
imports = gather_imports(scan_dirs)
|
||||
print(f"found {len(imports)} unique importable module paths. benchmarking ({args.runs} runs each, timeout {args.timeout}s)...\n")
|
||||
|
||||
started = time.perf_counter()
|
||||
results: list[tuple[str, float, str, int]] = []
|
||||
total = len(imports)
|
||||
for i, module in enumerate(sorted(imports), 1):
|
||||
t, status = measure_import(module, sys_path, args.runs, args.timeout)
|
||||
n = len(imports[module])
|
||||
results.append((module, t, status, n))
|
||||
ms = f"{t*1000:8.2f}ms" if t != float("inf") else " FAIL"
|
||||
col = color_for(t, args.slow_ms, args.moderate_ms)
|
||||
print(f" [{i:>3}/{total}] {module:<42} {col}{ms:<12}{RESET} ({n} files) {DIM}{status}{RESET}", end="\r")
|
||||
print()
|
||||
|
||||
results.sort(key=lambda r: (r[1] == float("inf"), -r[1] if r[1] != float("inf") else 0))
|
||||
|
||||
valid = sorted(t for _, t, _, _ in results if t != float("inf") and t > 0)
|
||||
med = median(valid) if valid else 0.0
|
||||
p90 = valid[int(len(valid) * 0.9)] if len(valid) >= 10 else (valid[-1] if valid else 0.0)
|
||||
total_elapsed = time.perf_counter() - started
|
||||
|
||||
bar = "=" * 110
|
||||
print(f"\n{BOLD}{bar}{RESET}")
|
||||
print(f"{BOLD}import time rankings (cold start, sorted slowest first){RESET}")
|
||||
print(f"thresholds: {RED}red > {args.slow_ms:.0f}ms{RESET} {YELLOW}yellow > {args.moderate_ms:.0f}ms{RESET} {GREEN}green <= {args.moderate_ms:.0f}ms{RESET}")
|
||||
print(f"stats: median={med*1000:.1f}ms p90={p90*1000:.1f}ms n={len(valid)} ok, {total - len(valid)} failed benchmark wall={total_elapsed:.1f}s")
|
||||
print(f"{BOLD}{bar}{RESET}\n")
|
||||
|
||||
print(f"{'module':<44} {'time':>12} {'files':>6} {'rank':>5} status")
|
||||
print("-" * 95)
|
||||
for rank, (mod, t, status, n) in enumerate(results, 1):
|
||||
col = color_for(t, args.slow_ms, args.moderate_ms)
|
||||
time_s = f"{t*1000:9.2f}ms" if t != float("inf") else " --"
|
||||
print(f"{col}{mod:<44} {time_s:>12} {n:>6} {rank:>5} {status}{RESET}")
|
||||
|
||||
top_n = [(m, t) for m, t, _, _ in results if t != float("inf") and t > args.slow_ms / 1000.0][:args.top]
|
||||
if top_n:
|
||||
print(f"\n{BOLD}top {len(top_n)} candidates for lazy / deferred loading (>= {args.slow_ms:.0f}ms):{RESET}")
|
||||
for m, t in top_n:
|
||||
print(f" {RED}->{RESET} {m:<44} {t*1000:8.2f}ms")
|
||||
|
||||
failed = [m for m, t, s, _ in results if t == float("inf")]
|
||||
if failed:
|
||||
print(f"\n{DIM}failed imports ({len(failed)}):{RESET}")
|
||||
for m, t, status, _ in results:
|
||||
if t == float("inf"):
|
||||
print(f" {DIM}{m:<44} {status}{RESET}")
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user