|
| 1 | +#!/usr/bin/env python3 |
| 2 | +# -*- coding: utf-8 -*- |
| 3 | + |
| 4 | +import os |
| 5 | +import sys |
| 6 | +import subprocess |
| 7 | +import json |
| 8 | +import argparse |
| 9 | + |
| 10 | +SILENT_OUTPUT = True # Set to True to suppress command line output |
| 11 | + |
| 12 | +def run_command(command): |
| 13 | + subprocess.run(command, shell=True, check=True, capture_output=SILENT_OUTPUT, text=True) |
| 14 | + |
| 15 | +def get_current_git_state(): |
| 16 | + try: |
| 17 | + # try branch name |
| 18 | + result = subprocess.run("git symbolic-ref --short HEAD", shell=True, |
| 19 | + capture_output=True, text=True, check=True) |
| 20 | + return result.stdout.strip() |
| 21 | + except subprocess.CalledProcessError: |
| 22 | + # else try commit hash (for detached state) |
| 23 | + result = subprocess.run("git rev-parse HEAD", shell=True, check=True, capture_output=True, text=True) |
| 24 | + return result.stdout.strip() |
| 25 | + |
| 26 | +def restore_git_state(original_git_state): |
| 27 | + print(f"\nRestoring original git state: {original_git_state}") |
| 28 | + run_command(f"git checkout {original_git_state}") |
| 29 | + |
| 30 | +def configure_and_build(commit, cmake_variables, benchmark_target, build_dir): |
| 31 | + print(f"\nConfiguring and building commit: {commit}\n") |
| 32 | + |
| 33 | + run_command(f"git checkout {commit}") |
| 34 | + |
| 35 | + cmake_vars_str = " ".join(cmake_variables) |
| 36 | + command = f"cmake -S . -B {build_dir} -DCMAKE_BUILD_TYPE=Release {cmake_vars_str}" |
| 37 | + run_command(command) |
| 38 | + |
| 39 | + command = f"cmake --build {build_dir} --target {benchmark_target} --clean-first" |
| 40 | + run_command(command) |
| 41 | + |
| 42 | +def run_benchmarks(commit, benchmark_target, build_dir): |
| 43 | + print(f"Running benchmarks for commit: {commit}\n") |
| 44 | + command = f"./{build_dir}/benchmarks/{benchmark_target} --benchmark_out_format=json --benchmark_time_unit=us --benchmark_out={commit}-results.json" |
| 45 | + run_command(command) |
| 46 | + print(f"Benchmarks for commit {commit} completed and results saved to {commit}-results.json\n") |
| 47 | + |
| 48 | +def compare_benchmarks(base_commit, head_commit, metric="cpu_time"): |
| 49 | + base_file = f"{base_commit}-results.json" |
| 50 | + new_file = f"{head_commit}-results.json" |
| 51 | + output_file = f"{base_commit}-{head_commit}-comparison.txt" |
| 52 | + |
| 53 | + print(f"\nComparing benchmarks between {base_commit} and {head_commit} using metric: {metric}\n") |
| 54 | + |
| 55 | + # Validate files exist and load JSON data |
| 56 | + try: |
| 57 | + if not os.path.exists(base_file): |
| 58 | + raise FileNotFoundError(f"Base file not found: {base_file}") |
| 59 | + if not os.path.exists(new_file): |
| 60 | + raise FileNotFoundError(f"New file not found: {new_file}") |
| 61 | + |
| 62 | + with open(base_file, 'r') as f: |
| 63 | + base_data = json.load(f) |
| 64 | + with open(new_file, 'r') as f: |
| 65 | + new_data = json.load(f) |
| 66 | + |
| 67 | + except json.JSONDecodeError as e: |
| 68 | + raise ValueError(f"Invalid JSON format: {e}") |
| 69 | + except Exception as e: |
| 70 | + raise RuntimeError(f"Error reading files: {e}") |
| 71 | + |
| 72 | + # index benchmarks by name |
| 73 | + base_benchmarks = {b['name']: b for b in base_data['benchmarks']} |
| 74 | + new_benchmarks = {b['name']: b for b in new_data['benchmarks']} |
| 75 | + |
| 76 | + # get time unit |
| 77 | + time_unit = base_data['benchmarks'][0].get('time_unit', 'ns') |
| 78 | + |
| 79 | + # only compare benchmarks that are present in both files |
| 80 | + common_names = [] |
| 81 | + new_benchmark_names = set(new_benchmarks.keys()) |
| 82 | + for benchmark in base_data['benchmarks']: |
| 83 | + if benchmark['name'] in new_benchmark_names: |
| 84 | + common_names.append(benchmark['name']) |
| 85 | + |
| 86 | + # output details |
| 87 | + output_lines = [] |
| 88 | + output_lines.append(f"Benchmark Comparison: {base_commit} vs {head_commit}") |
| 89 | + output_lines.append(f"Metric: {metric}") |
| 90 | + output_lines.append(f"Time Unit: {time_unit}") |
| 91 | + output_lines.append(f"Date: {os.popen('date').read().strip()}") |
| 92 | + output_lines.append("") |
| 93 | + |
| 94 | + if 'context' in base_data: |
| 95 | + output_lines.append("Base Benchmark Context:") |
| 96 | + output_lines.append(json.dumps(base_data['context'], indent=2)) |
| 97 | + output_lines.append("") |
| 98 | + |
| 99 | + if 'context' in new_data: |
| 100 | + output_lines.append("New Benchmark Context:") |
| 101 | + output_lines.append(json.dumps(new_data['context'], indent=2)) |
| 102 | + output_lines.append("") |
| 103 | + |
| 104 | + output_lines.append(f"{'Name':<60} {f'Base ({time_unit})':<15} {f'New ({time_unit})':<15} {f'Diff ({time_unit})':<15} {'% Diff':<10}") |
| 105 | + output_lines.append("-" * 125) |
| 106 | + |
| 107 | + # compare each benchmark |
| 108 | + for name in common_names: |
| 109 | + base_value = base_benchmarks[name][metric] |
| 110 | + new_value = new_benchmarks[name][metric] |
| 111 | + |
| 112 | + diff = new_value - base_value |
| 113 | + percentage_diff = ((new_value - base_value) / base_value) * 100.0 if base_value != 0 else 0.0 |
| 114 | + sign = "+" if percentage_diff > 0 else "" |
| 115 | + |
| 116 | + line = f"{name:<60} {base_value:<15.2f} {new_value:<15.2f} {diff:<15.2f} {sign}{percentage_diff:<9.2f}" |
| 117 | + output_lines.append(line) |
| 118 | + |
| 119 | + # Write to file |
| 120 | + with open(output_file, 'w') as f: |
| 121 | + f.write('\n'.join(output_lines)) |
| 122 | + |
| 123 | + print(f"\nComparison results written to: {output_file}") |
| 124 | + |
| 125 | + |
| 126 | +if __name__ == "__main__": |
| 127 | + parser = argparse.ArgumentParser( |
| 128 | + description="Compare SeQuant benchmarks between two commits", |
| 129 | + formatter_class=argparse.RawDescriptionHelpFormatter, |
| 130 | + ) |
| 131 | + |
| 132 | + parser.add_argument("base_commit", help="Base commit SHA to compare against") |
| 133 | + parser.add_argument("head_commit", help="Head commit SHA to compare") |
| 134 | + parser.add_argument("--benchmark-target", "-t", |
| 135 | + default="sequant_benchmarks", |
| 136 | + help="Benchmark target to build and run (default: sequant_benchmarks)") |
| 137 | + parser.add_argument("--build-dir", "-b", |
| 138 | + default="build", |
| 139 | + help="Build directory for CMake (default: build)") |
| 140 | + |
| 141 | + args = parser.parse_args() |
| 142 | + |
| 143 | + # print info |
| 144 | + print("**" * 50) |
| 145 | + print("SeQuant Benchmark Comparison Script\n") |
| 146 | + print(f"Base commit: {args.base_commit}") |
| 147 | + print(f"Head commit: {args.head_commit}") |
| 148 | + print(f"Benchmark target: {args.benchmark_target}") |
| 149 | + print(f"Build directory: {args.build_dir}") |
| 150 | + print("**" * 50) |
| 151 | + |
| 152 | + original_ref = get_current_git_state() |
| 153 | + print(f"Original git reference: {original_ref}") |
| 154 | + |
| 155 | + # Define CMake variables |
| 156 | + cmake_variables = ["-G Ninja", |
| 157 | + "-DCMAKE_INTERPROCEDURAL_OPTIMIZATION=ON", |
| 158 | + "-DSEQUANT_TESTS=OFF", |
| 159 | + "-DSEQUANT_EVAL_TESTS=OFF", |
| 160 | + "-DSEQUANT_BENCHMARKS=ON", |
| 161 | + "-DSEQUANT_MIMALLOC=ON", |
| 162 | + "-DSEQUANT_CONTEXT_MANIPULATION_THREADSAFE=ON"] |
| 163 | + |
| 164 | + print("\nCMake variables:") |
| 165 | + for var in cmake_variables: |
| 166 | + print(f"{var}") |
| 167 | + |
| 168 | + try: |
| 169 | + # base commit |
| 170 | + configure_and_build(args.base_commit, cmake_variables, args.benchmark_target, args.build_dir) |
| 171 | + run_benchmarks(args.base_commit, args.benchmark_target, args.build_dir) |
| 172 | + |
| 173 | + # head commit |
| 174 | + configure_and_build(args.head_commit, cmake_variables, args.benchmark_target, args.build_dir) |
| 175 | + run_benchmarks(args.head_commit, args.benchmark_target, args.build_dir) |
| 176 | + |
| 177 | + # Compare benchmarks |
| 178 | + compare_benchmarks(args.base_commit, args.head_commit) |
| 179 | + print("Benchmark comparison completed successfully.") |
| 180 | + |
| 181 | + except Exception as e: |
| 182 | + print(f"Error during benchmark execution: {e}") |
| 183 | + sys.exit(1) |
| 184 | + finally: |
| 185 | + # restore original git state even if script fails |
| 186 | + restore_git_state(original_ref) |
0 commit comments