Skip to content

Commit 22dba1b

Browse files
committed
benchmark: introduce python script for benchmark comparison
This script can be used to compare peformance of two commits. It checks out, builds and compares benchmark outputs of both commits. How to use: python3 benchmark_compare.py abc123 def456 python3 benchmark_compare.py master your/feature/branch
1 parent 9d9b791 commit 22dba1b

File tree

1 file changed

+186
-0
lines changed

1 file changed

+186
-0
lines changed

bin/admin/benchmark_compare.py

Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
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

Comments
 (0)