@@ -7,28 +7,33 @@ import statistics
77import sys
88import tempfile
99
10- import plotly
10+ import numpy
11+ import pandas
12+ import plotly .express
1113import tabulate
1214
13- def parse_lnt (lines ):
15+ def parse_lnt (lines , aggregate = statistics . median ):
1416 """
15- Parse lines in LNT format and return a dictionnary of the form:
17+ Parse lines in LNT format and return a list of dictionnaries of the form:
1618
17- {
18- 'benchmark1': {
19- 'metric1': [float],
20- 'metric2': [float],
19+ [
20+ {
21+ 'benchmark': <benchmark1>,
22+ <metric1>: float,
23+ <metric2>: float,
2124 ...
2225 },
23- 'benchmark2': {
24- 'metric1': [float],
25- 'metric2': [float],
26+ {
27+ 'benchmark': <benchmark2>,
28+ <metric1>: float,
29+ <metric2>: float,
2630 ...
2731 },
2832 ...
29- }
33+ ]
3034
31- Each metric may have multiple values.
35+ If a metric has multiple values associated to it, they are aggregated into a single
36+ value using the provided aggregation function.
3237 """
3338 results = {}
3439 for line in lines :
@@ -37,61 +42,51 @@ def parse_lnt(lines):
3742 continue
3843
3944 (identifier , value ) = line .split (' ' )
40- (name , metric ) = identifier .split ('.' )
41- if name not in results :
42- results [name ] = {}
43- if metric not in results [name ]:
44- results [name ][metric ] = []
45- results [name ][metric ].append (float (value ))
46- return results
47-
48- def plain_text_comparison (benchmarks , baseline , candidate , baseline_name = None , candidate_name = None ):
45+ (benchmark , metric ) = identifier .split ('.' )
46+ if benchmark not in results :
47+ results [benchmark ] = {'benchmark' : benchmark }
48+
49+ entry = results [benchmark ]
50+ if metric not in entry :
51+ entry [metric ] = []
52+ entry [metric ].append (float (value ))
53+
54+ for (bm , entry ) in results .items ():
55+ for metric in entry :
56+ if isinstance (entry [metric ], list ):
57+ entry [metric ] = aggregate (entry [metric ])
58+
59+ return list (results .values ())
60+
61+ def plain_text_comparison (data , metric , baseline_name = None , candidate_name = None ):
4962 """
50- Create a tabulated comparison of the baseline and the candidate.
63+ Create a tabulated comparison of the baseline and the candidate for the given metric .
5164 """
65+ data = data .replace (numpy .nan , None ).sort_values (by = 'benchmark' ) # avoid NaNs in tabulate output
5266 headers = ['Benchmark' , baseline_name , candidate_name , 'Difference' , '% Difference' ]
5367 fmt = (None , '.2f' , '.2f' , '.2f' , '.2f' )
54- table = []
55- for (bm , base , cand ) in zip (benchmarks , baseline , candidate ):
56- diff = (cand - base ) if base and cand else None
57- percent = 100 * (diff / base ) if base and cand else None
58- row = [bm , base , cand , diff , percent ]
59- table .append (row )
68+ table = data [['benchmark' , f'{ metric } _baseline' , f'{ metric } _candidate' , 'difference' , 'percent' ]].set_index ('benchmark' )
6069 return tabulate .tabulate (table , headers = headers , floatfmt = fmt , numalign = 'right' )
6170
62- def create_chart (benchmarks , baseline , candidate , subtitle = None , baseline_name = None , candidate_name = None ):
71+ def create_chart (data , metric , subtitle = None , baseline_name = None , candidate_name = None ):
6372 """
64- Create a bar chart comparing ' baseline' and ' candidate' .
73+ Create a bar chart comparing the given metric between the baseline and the candidate.
6574 """
66- figure = plotly .graph_objects .Figure (layout = {
67- 'title' : {
68- 'text' : f'{ baseline_name } vs { candidate_name } ' ,
69- 'subtitle' : {'text' : subtitle }
70- }
75+ data = data .sort_values (by = 'benchmark' ).rename (columns = {
76+ f'{ metric } _baseline' : baseline_name ,
77+ f'{ metric } _candidate' : candidate_name
7178 })
72- figure .add_trace (plotly .graph_objects .Bar (x = benchmarks , y = baseline , name = baseline_name ))
73- figure .add_trace (plotly .graph_objects .Bar (x = benchmarks , y = candidate , name = candidate_name ))
79+ figure = plotly .express .bar (data , title = f'{ baseline_name } vs { candidate_name } ' ,
80+ subtitle = subtitle ,
81+ x = 'benchmark' , y = [baseline_name , candidate_name ], barmode = 'group' )
82+ figure .update_layout (xaxis_title = '' , yaxis_title = '' , legend_title = '' )
7483 return figure
7584
76- def prepare_series (baseline , candidate , metric , aggregate = statistics .median ):
77- """
78- Prepare the data for being formatted or displayed as a chart.
79-
80- Metrics that have more than one value are aggregated using the given aggregation function.
81- """
82- all_benchmarks = sorted (list (set (baseline .keys ()) | set (candidate .keys ())))
83- baseline_series = []
84- candidate_series = []
85- for bm in all_benchmarks :
86- baseline_series .append (aggregate (baseline [bm ][metric ]) if bm in baseline and metric in baseline [bm ] else None )
87- candidate_series .append (aggregate (candidate [bm ][metric ]) if bm in candidate and metric in candidate [bm ] else None )
88- return (all_benchmarks , baseline_series , candidate_series )
89-
9085def main (argv ):
9186 parser = argparse .ArgumentParser (
9287 prog = 'compare-benchmarks' ,
9388 description = 'Compare the results of two sets of benchmarks in LNT format.' ,
94- epilog = 'This script requires the `tabulate` and the `plotly` Python modules .' )
89+ epilog = 'This script depends on the modules listed in `libcxx/utils/requirements.txt` .' )
9590 parser .add_argument ('baseline' , type = argparse .FileType ('r' ),
9691 help = 'Path to a LNT format file containing the benchmark results for the baseline.' )
9792 parser .add_argument ('candidate' , type = argparse .FileType ('r' ),
@@ -127,26 +122,28 @@ def main(argv):
127122 if args .format == 'text' and args .open :
128123 parser .error ('Passing --open makes no sense with --format=text' )
129124
130- baseline = parse_lnt (args .baseline .readlines ())
131- candidate = parse_lnt (args .candidate .readlines ())
125+ baseline = pandas . DataFrame ( parse_lnt (args .baseline .readlines () ))
126+ candidate = pandas . DataFrame ( parse_lnt (args .candidate .readlines () ))
132127
133- if args . filter is not None :
134- regex = re . compile ( args . filter )
135- baseline = { k : v for ( k , v ) in baseline . items () if regex . search ( k )}
136- candidate = { k : v for ( k , v ) in candidate . items () if regex . search ( k )}
128+ # Join the baseline and the candidate into a single dataframe and add some new columns
129+ data = baseline . merge ( candidate , how = 'outer' , on = 'benchmark' , suffixes = ( '_baseline' , '_candidate' ) )
130+ data [ 'difference' ] = data [ f' { args . metric } _candidate' ] - data [ f' { args . metric } _baseline' ]
131+ data [ 'percent' ] = 100 * ( data [ 'difference' ] / data [ f' { args . metric } _baseline' ])
137132
138- (benchmarks , baseline_series , candidate_series ) = prepare_series (baseline , candidate , args .metric )
133+ if args .filter is not None :
134+ keeplist = [b for b in data ['benchmark' ] if re .search (args .filter , b ) is not None ]
135+ data = data [data ['benchmark' ].isin (keeplist )]
139136
140137 if args .format == 'chart' :
141- figure = create_chart (benchmarks , baseline_series , candidate_series , subtitle = args .subtitle ,
142- baseline_name = args .baseline_name ,
143- candidate_name = args .candidate_name )
138+ figure = create_chart (data , args . metric , subtitle = args .subtitle ,
139+ baseline_name = args .baseline_name ,
140+ candidate_name = args .candidate_name )
144141 do_open = args .output is None or args .open
145142 output = args .output or tempfile .NamedTemporaryFile (suffix = '.html' ).name
146143 plotly .io .write_html (figure , file = output , auto_open = do_open )
147144 else :
148- diff = plain_text_comparison (benchmarks , baseline_series , candidate_series , baseline_name = args .baseline_name ,
149- candidate_name = args .candidate_name )
145+ diff = plain_text_comparison (data , args . metric , baseline_name = args .baseline_name ,
146+ candidate_name = args .candidate_name )
150147 diff += '\n '
151148 if args .output is not None :
152149 with open (args .output , 'w' ) as out :
0 commit comments