Skip to content

Commit f5340b0

Browse files
committed
chore: add --normalize / --baseline for benchmark query.py
1 parent ef75041 commit f5340b0

1 file changed

Lines changed: 110 additions & 8 deletions

File tree

.benchmark/query.py

Lines changed: 110 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717
from tinydb import TinyDB
1818
from tqdm import tqdm
1919

20-
2120
TABLE_NAME = "benchmarks"
2221

2322

@@ -56,7 +55,45 @@ def import_data(db_path, *args):
5655
print(f"Skipped {skipped} files that were already in the database.")
5756

5857

59-
def walltime_chart(db_path, arch, binary_type, version, exclude):
58+
def resolve_baseline(columns, meta, version, baseline):
59+
"""
60+
Pick the column (a version_object) to normalize against.
61+
62+
- If 'baseline' is None, use the smallest version (deterministic).
63+
- If '--version' was not given, 'baseline' is a version string
64+
(optionally "version+commit").
65+
- If '--version' was given, 'baseline' is a config string.
66+
"""
67+
cols = list(columns)
68+
if baseline is None:
69+
# Deterministic default: the smallest version.
70+
return min(cols)
71+
72+
if version is None:
73+
# 'baseline' is a version, optionally with a "+commit" suffix.
74+
target = Version(baseline)
75+
matches = [c for c in cols if c == target]
76+
if not matches:
77+
# Fall back to matching the release, ignoring any commit suffix.
78+
matches = [c for c in cols if c.base_version == target.base_version]
79+
if not matches:
80+
available = ", ".join(sorted(str(c) for c in cols))
81+
raise SystemExit(
82+
f"Baseline version '{baseline}' not found. Available: {available}"
83+
)
84+
return min(matches)
85+
86+
# 'baseline' is a config (a single version has been selected).
87+
matches = [c for c in cols if meta.at[c, "config"] == baseline]
88+
if not matches:
89+
available = ", ".join(sorted(meta.at[c, "config"] for c in cols))
90+
raise SystemExit(
91+
f"Baseline config '{baseline}' not found. Available: {available}"
92+
)
93+
return min(matches)
94+
95+
96+
def walltime_chart(db_path, arch, binary_type, version, exclude, normalize, baseline):
6097
db = TinyDB(db_path)
6198
table = db.table(TABLE_NAME)
6299

@@ -159,6 +196,10 @@ def query(doc):
159196
df.at[row.Index, "version_object"] = Version(v)
160197
# df["version_object"] = df["version"].apply(Version)
161198

199+
# Lookup used to resolve a --baseline argument (version or config) to the
200+
# corresponding version_object column.
201+
meta = df.drop_duplicates("version_object").set_index("version_object")
202+
162203
# Pivot the DataFrame so that "name" becomes the row index and different "config"
163204
# values become separate columns for the "mean" and "stddev" values.
164205
df_pivot = df.pivot(
@@ -169,11 +210,40 @@ def query(doc):
169210
mean_df = df_pivot["mean"]
170211
stddev_df = df_pivot["stddev"]
171212

213+
# === Normalization ===
214+
215+
# Divide every benchmark's times by its value in the baseline column, so the
216+
# baseline sits at 1.0 and other bars read directly as relative factors
217+
# (e.g. 1.10 == a 10% regression). Each benchmark is normalized against its
218+
# own baseline, putting all benchmarks on a comparable linear scale.
219+
baseline_col = None
220+
if normalize:
221+
baseline_col = resolve_baseline(mean_df.columns, meta, version, baseline)
222+
base_mean = mean_df[baseline_col].copy()
223+
224+
missing = base_mean.isna()
225+
if missing.any():
226+
names = ", ".join(mean_df.index[missing])
227+
print(
228+
f"Warning: no baseline ({baseline_col}) measurement for: {names}. "
229+
"These benchmarks will be blank."
230+
)
231+
232+
# Scale stddevs by the same per-benchmark factor (baseline treated as an
233+
# exact reference).
234+
stddev_df = stddev_df.div(base_mean, axis=0)
235+
mean_df = mean_df.div(base_mean, axis=0)
236+
172237
# === Plotting ===
173238

174239
# Plot the normalized horizontal grouped bar chart.
175240
ax = mean_df.plot(
176-
kind="bar", yerr=stddev_df, capsize=3, figsize=(10, 10), log=True, width=0.8
241+
kind="bar",
242+
yerr=stddev_df,
243+
capsize=3,
244+
figsize=(10, 10),
245+
log=not normalize,
246+
width=0.8,
177247
)
178248

179249
for container in ax.containers:
@@ -190,13 +260,22 @@ def query(doc):
190260
ax.set_title(f"Benchmark results for arch={arch}, type={binary_type}")
191261
# Rotate x-axis labels for better readability.
192262
ax.set_xticklabels(ax.get_xticklabels(), rotation=20, ha="right")
193-
ax.set_ylabel("Benchmark Time")
263+
if normalize:
264+
ax.set_ylabel(f"Benchmark Time (normalized to {baseline_col})")
265+
else:
266+
ax.set_ylabel("Benchmark Time")
194267
ax.set_xlabel("Benchmark Name")
195268
# Add grid lines in the background
196269
ax.set_axisbelow(True)
197-
ax.grid(axis="y", linestyle="-", alpha=0.5, which="both")
198-
# Set y-axis range (1ms to 100s)
199-
ax.set_ylim(0.001, 100.0)
270+
if normalize:
271+
ax.grid(axis="y", linestyle="-", alpha=0.5, which="major")
272+
# Reference line at the baseline (1.0).
273+
ax.axhline(1.0, color="black", linewidth=0.8, alpha=0.6)
274+
ax.set_ylim(bottom=0.0)
275+
else:
276+
ax.grid(axis="y", linestyle="-", alpha=0.5, which="both")
277+
# Set y-axis range (1ms to 100s)
278+
ax.set_ylim(0.001, 100.0)
200279

201280
# Maximize the space for the figure.
202281
plt.subplots_adjust(left=0.06, right=0.99, top=0.97, bottom=0.12)
@@ -249,13 +328,36 @@ def main():
249328
default=None,
250329
help="Regex matching the names of the benchmarks to exclude.",
251330
)
331+
bar_parser.add_argument(
332+
"--normalize",
333+
action="store_true",
334+
help="Normalize each benchmark's times to a baseline and plot on a "
335+
"linear scale (so a 10%% regression reads as 1.10).",
336+
)
337+
bar_parser.add_argument(
338+
"--baseline",
339+
type=str,
340+
default=None,
341+
help="Baseline to normalize against: a version (default) or a config "
342+
"if --version is given. Defaults to the smallest version.",
343+
)
252344

253345
args = parser.parse_args()
254346

255347
if args.command == "import":
256348
import_data(args.db_path, *args.json_dirs)
257349
elif args.command == "walltime":
258-
walltime_chart(args.db_path, args.arch, args.binary, args.version, args.exclude)
350+
if args.baseline is not None and not args.normalize:
351+
print("Note: --baseline has no effect without --normalize.")
352+
walltime_chart(
353+
args.db_path,
354+
args.arch,
355+
args.binary,
356+
args.version,
357+
args.exclude,
358+
args.normalize,
359+
args.baseline,
360+
)
259361

260362

261363
if __name__ == "__main__":

0 commit comments

Comments
 (0)