Run benchmarks with deterministic seeds and generate checksums for artifacts.
This script ensures reproducibility by:
#!/usr/bin/env python3
"""Run benchmarks with deterministic seeds and generate checksums for artifacts.
This script ensures reproducibility by:
1. Setting global seeds for all benchmarks
2. Running selected benchmarks with consistent parameters
3. Generating checksums (SHA256) for all output artifacts
4. Creating a manifest file with checksums for verification
"""
from __future__ import annotations
import argparse
import hashlib
import json
import os
import subprocess
import sys
from pathlib import Path
from typing import Any
# Add src to path for security module
_SCRIPT_DIR = Path(__file__).resolve().parent
_REPO_ROOT = _SCRIPT_DIR.parent
if str(_REPO_ROOT / "src") not in sys.path:
sys.path.insert(0, str(_REPO_ROOT / "src"))
from tnfr.security import run_command_safely, validate_path_safe
# Default global seed for all benchmarks
DEFAULT_SEED = 42
# Output truncation limits
MAX_STDERR_LENGTH = 500
MAX_STDOUT_PREVIEW_LENGTH = 200
# Benchmark configurations with their default parameters
BENCHMARK_CONFIGS = {
"comprehensive_cache_profiler": {
"script": "benchmarks/comprehensive_cache_profiler.py",
"args": ["--nodes", "100", "--steps", "20"],
"output_arg": "--output",
"output_format": "json",
"output_suffix": ".json",
"supports_seed": True,
},
"full_pipeline_profile": {
"script": "benchmarks/full_pipeline_profile.py",
"args": ["--nodes", "100", "--edge-probability", "0.25", "--loops", "3"],
"output_arg": "--output-dir",
"output_format": "dir",
"output_suffix": "",
"supports_seed": True,
},
"cache_hot_path_profiler": {
"script": "benchmarks/cache_hot_path_profiler.py",
"args": ["--nodes", "100", "--steps", "20"],
"output_arg": "--output",
"output_format": "json",
"output_suffix": ".json",
"supports_seed": True,
},
"compute_si_profile": {
"script": "benchmarks/compute_si_profile.py",
"args": ["--nodes", "100", "--loops", "5"],
"output_arg": "--output-dir",
"output_format": "dir",
"output_suffix": "",
"extra_args": ["--format", "json"],
"supports_seed": False, # Uses deterministic chord graph
},
}
def compute_checksum(file_path: Path) -> str:
"""Compute SHA256 checksum of a file."""
sha256_hash = hashlib.sha256()
with open(file_path, "rb") as f:
for byte_block in iter(lambda: f.read(4096), b""):
sha256_hash.update(byte_block)
return sha256_hash.hexdigest()
def run_benchmark(
name: str,
config: dict[str, Any],
output_dir: Path,
seed: int,
verbose: bool = False,
) -> dict[str, Any]:
"""Run a single benchmark and return its results."""
# Validate benchmark name to prevent injection
if name not in BENCHMARK_CONFIGS:
return {"status": "skipped", "reason": "invalid benchmark name"}
script_path = Path(config["script"])
if not script_path.exists():
print(f"Warning: Benchmark script {script_path} not found, skipping.")
return {"status": "skipped", "reason": "script not found"}
# Prepare output file/directory
output_suffix = config.get("output_suffix", ".json")
output_path = output_dir / f"{name}_seed{seed}{output_suffix}"
# Build command - all arguments are validated strings
cmd = [
sys.executable,
str(script_path),
*[str(arg) for arg in config["args"]],
]
# Add seed if supported
if config.get("supports_seed", True):
cmd.extend(["--seed", str(seed)])
# Add extra args if specified
if "extra_args" in config:
cmd.extend([str(arg) for arg in config["extra_args"]])
# Add output argument if benchmark supports it
output_arg = config.get("output_arg")
if output_arg:
cmd.extend([str(output_arg), str(output_path)])
if verbose:
print(f"Running: {' '.join(cmd)}")
try:
# Set PYTHONPATH to include src directory
env = os.environ.copy()
env["PYTHONPATH"] = str(Path.cwd() / "src")
result = run_command_safely(
cmd,
check=False,
timeout=300, # 5 minute timeout
env=env,
)
if result.returncode != 0:
print(f"Error running {name}:")
print(result.stderr)
return {
"status": "failed",
"returncode": result.returncode,
"stderr": result.stderr[:MAX_STDERR_LENGTH], # First N chars
}
# Compute checksum based on output format
checksums = {}
output_files = []
output_format = config.get("output_format", "none")
if output_format == "dir" and output_path.exists() and output_path.is_dir():
# Directory output: checksum all JSON files
for json_file in output_path.glob("*.json"):
filename = json_file.name
checksums[filename] = compute_checksum(json_file)
output_files.append(str(json_file))
elif output_path.exists() and output_path.is_file():
# Single file output
filename = output_path.name
checksums[filename] = compute_checksum(output_path)
output_files.append(str(output_path))
# If no output file was created, save stdout
if not output_files and result.stdout:
stdout_file = output_dir / f"{name}_seed{seed}_stdout.txt"
stdout_file.write_text(result.stdout)
checksums["stdout.txt"] = compute_checksum(stdout_file)
output_files.append(str(stdout_file))
return {
"status": "success",
"output_files": output_files,
"checksums": checksums,
"stdout_preview": (
result.stdout[-MAX_STDOUT_PREVIEW_LENGTH:] if verbose else ""
),
}
except subprocess.TimeoutExpired:
return {"status": "timeout"}
except Exception as e:
return {"status": "error", "message": str(e)}
def main() -> int:
"""Main entry point."""
parser = argparse.ArgumentParser(
description=__doc__,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
parser.add_argument(
"--seed",
type=int,
default=DEFAULT_SEED,
help=f"Global seed for all benchmarks (default: {DEFAULT_SEED})",
)
parser.add_argument(
"--output-dir",
type=Path,
default=Path("artifacts"),
help="Directory to store benchmark outputs (default: artifacts)",
)
parser.add_argument(
"--benchmarks",
nargs="+",
choices=list(BENCHMARK_CONFIGS.keys()) + ["all"],
default=["all"],
help="Which benchmarks to run (default: all)",
)
parser.add_argument(
"--manifest",
type=Path,
help="Path to output manifest file with checksums (default: <output-dir>/manifest.json)",
)
parser.add_argument(
"--verify",
type=Path,
help="Verify checksums against an existing manifest file",
)
parser.add_argument(
"-v",
"--verbose",
action="store_true",
help="Verbose output",
)
args = parser.parse_args()
# Verify mode
if args.verify:
return verify_manifest(args.verify, args.verbose)
# Create output directory
args.output_dir.mkdir(parents=True, exist_ok=True)
# Determine which benchmarks to run
benchmarks_to_run = (
list(BENCHMARK_CONFIGS.keys()) if "all" in args.benchmarks else args.benchmarks
)
# Run benchmarks
print(f"Running {len(benchmarks_to_run)} benchmark(s) with seed={args.seed}")
results: dict[str, Any] = {
"seed": args.seed,
"benchmarks": {},
}
for name in benchmarks_to_run:
print(f"\n[{name}]")
config = BENCHMARK_CONFIGS[name]
result = run_benchmark(name, config, args.output_dir, args.seed, args.verbose)
results["benchmarks"][name] = result
status_symbol = "✓" if result["status"] == "success" else "✗"
print(f"{status_symbol} {result['status']}")
if result["status"] == "success":
checksums = result.get("checksums", {})
if checksums:
print(f" Generated {len(checksums)} artifact(s)")
for name_part, checksum in list(checksums.items())[:3]:
print(f" {name_part}: {checksum[:16]}...")
if len(checksums) > 3:
print(f" ... and {len(checksums) - 3} more")
# Save manifest
manifest_path = args.manifest or (args.output_dir / "manifest.json")
with open(manifest_path, "w") as f:
json.dump(results, f, indent=2)
print(f"\nManifest saved to: {manifest_path}")
# Count successes
successes = sum(
1 for r in results["benchmarks"].values() if r["status"] == "success"
)
print(f"Successfully ran {successes}/{len(benchmarks_to_run)} benchmarks")
return 0 if successes == len(benchmarks_to_run) else 1
def verify_manifest(manifest_path: Path, verbose: bool = False) -> int:
"""Verify checksums against a manifest file."""
if not manifest_path.exists():
print(f"Error: Manifest file {manifest_path} not found")
return 1
with open(manifest_path) as f:
manifest = json.load(f)
print(f"Verifying checksums from: {manifest_path}")
print(f"Manifest seed: {manifest.get('seed', 'unknown')}")
mismatches = []
missing = []
verified = 0
for benchmark_name, result in manifest.get("benchmarks", {}).items():
if result.get("status") != "success":
continue
checksums = result.get("checksums", {})
output_files = result.get("output_files", [])
for file_name, expected_checksum in checksums.items():
# Find the actual file by matching filename
actual_file = None
for output_path in output_files:
file_path = Path(output_path)
if file_path.name == file_name or file_path.name.endswith(file_name):
actual_file = file_path
break
if not actual_file or not actual_file.exists():
missing.append(f"{benchmark_name}/{file_name}")
print(f"✗ {benchmark_name}/{file_name}: file missing")
continue
actual_checksum = compute_checksum(actual_file)
if actual_checksum == expected_checksum:
verified += 1
if verbose:
print(f"✓ {benchmark_name}/{file_name}: verified")
else:
mismatches.append(f"{benchmark_name}/{file_name}")
print(f"✗ {benchmark_name}/{file_name}: checksum mismatch")
if verbose:
print(f" Expected: {expected_checksum}")
print(f" Actual: {actual_checksum}")
print(f"\nVerified {verified} checksum(s)")
if mismatches:
print(f"Mismatches: {', '.join(mismatches)}")
if missing:
print(f"Missing files: {', '.join(missing)}")
return 0 if not (mismatches or missing) else 1
if __name__ == "__main__":
sys.exit(main())