Skip to content

Commit 2810a48

Browse files
committed
[libc++] Use pandas.DataFrame in compare-benchmarks
This opens the door to performing more advanced computations on the data we're comparing.
1 parent cc0fecf commit 2810a48

File tree

2 files changed

+67
-70
lines changed

2 files changed

+67
-70
lines changed

libcxx/utils/compare-benchmarks

Lines changed: 61 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -7,28 +7,33 @@ import statistics
77
import sys
88
import tempfile
99

10-
import plotly
10+
import numpy
11+
import pandas
12+
import plotly.express
1113
import 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-
9085
def 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:

libcxx/utils/visualize-historical

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -158,28 +158,28 @@ def parse_lnt(lines, aggregate=statistics.median):
158158
If a metric has multiple values associated to it, they are aggregated into a single
159159
value using the provided aggregation function.
160160
"""
161-
results = []
161+
results = {}
162162
for line in lines:
163163
line = line.strip()
164164
if not line:
165165
continue
166166

167167
(identifier, value) = line.split(' ')
168168
(benchmark, metric) = identifier.split('.')
169-
if not any(x['benchmark'] == benchmark for x in results):
170-
results.append({'benchmark': benchmark})
169+
if benchmark not in results:
170+
results[benchmark] = {'benchmark': benchmark}
171171

172-
entry = next(x for x in results if x['benchmark'] == benchmark)
172+
entry = results[benchmark]
173173
if metric not in entry:
174174
entry[metric] = []
175175
entry[metric].append(float(value))
176176

177-
for entry in results:
177+
for (bm, entry) in results.items():
178178
for metric in entry:
179179
if isinstance(entry[metric], list):
180180
entry[metric] = aggregate(entry[metric])
181181

182-
return results
182+
return list(results.values())
183183

184184
def sorted_revlist(git_repo, commits):
185185
"""

0 commit comments

Comments
 (0)