1717from tinydb import TinyDB
1818from tqdm import tqdm
1919
20-
2120TABLE_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
261363if __name__ == "__main__" :
0 commit comments