@@ -7,28 +7,33 @@ import statistics
7
7
import sys
8
8
import tempfile
9
9
10
- import plotly
10
+ import numpy
11
+ import pandas
12
+ import plotly .express
11
13
import tabulate
12
14
13
- def parse_lnt (lines ):
15
+ def parse_lnt (lines , aggregate = statistics . median ):
14
16
"""
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:
16
18
17
- {
18
- 'benchmark1': {
19
- 'metric1': [float],
20
- 'metric2': [float],
19
+ [
20
+ {
21
+ 'benchmark': <benchmark1>,
22
+ <metric1>: float,
23
+ <metric2>: float,
21
24
...
22
25
},
23
- 'benchmark2': {
24
- 'metric1': [float],
25
- 'metric2': [float],
26
+ {
27
+ 'benchmark': <benchmark2>,
28
+ <metric1>: float,
29
+ <metric2>: float,
26
30
...
27
31
},
28
32
...
29
- }
33
+ ]
30
34
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.
32
37
"""
33
38
results = {}
34
39
for line in lines :
@@ -37,61 +42,51 @@ def parse_lnt(lines):
37
42
continue
38
43
39
44
(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 ):
49
62
"""
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 .
51
64
"""
65
+ data = data .replace (numpy .nan , None ).sort_values (by = 'benchmark' ) # avoid NaNs in tabulate output
52
66
headers = ['Benchmark' , baseline_name , candidate_name , 'Difference' , '% Difference' ]
53
67
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' )
60
69
return tabulate .tabulate (table , headers = headers , floatfmt = fmt , numalign = 'right' )
61
70
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 ):
63
72
"""
64
- Create a bar chart comparing ' baseline' and ' candidate' .
73
+ Create a bar chart comparing the given metric between the baseline and the candidate.
65
74
"""
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
71
78
})
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 = '' )
74
83
return figure
75
84
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
-
90
85
def main (argv ):
91
86
parser = argparse .ArgumentParser (
92
87
prog = 'compare-benchmarks' ,
93
88
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` .' )
95
90
parser .add_argument ('baseline' , type = argparse .FileType ('r' ),
96
91
help = 'Path to a LNT format file containing the benchmark results for the baseline.' )
97
92
parser .add_argument ('candidate' , type = argparse .FileType ('r' ),
@@ -127,26 +122,28 @@ def main(argv):
127
122
if args .format == 'text' and args .open :
128
123
parser .error ('Passing --open makes no sense with --format=text' )
129
124
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 () ))
132
127
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' ])
137
132
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 )]
139
136
140
137
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 )
144
141
do_open = args .output is None or args .open
145
142
output = args .output or tempfile .NamedTemporaryFile (suffix = '.html' ).name
146
143
plotly .io .write_html (figure , file = output , auto_open = do_open )
147
144
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 )
150
147
diff += '\n '
151
148
if args .output is not None :
152
149
with open (args .output , 'w' ) as out :
0 commit comments