Skip to content

Commit ec2b8b3

Browse files
authored
Add optional scatterplot to benchcomp output (rust-lang#3077)
Scatterplots should make it easier to immediately spot performance trends (and indeed any differences) rather than having to process a (large) number of table rows. Uses mermaid-js to produce markdown-embedded plots that will display on the job summary page. Scatterplots are not directly supported by mermaid-js at this point (xycharts only do line or bar charts), so quadrant plots are employed with various diagram items drawn in white to make them disappear.
1 parent 54786ad commit ec2b8b3

File tree

4 files changed

+138
-7
lines changed

4 files changed

+138
-7
lines changed

tools/benchcomp/benchcomp/visualizers/__init__.py

Lines changed: 120 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@
33

44

55
import dataclasses
6+
import enum
67
import json
78
import logging
9+
import math
810
import subprocess
911
import sys
1012
import textwrap
@@ -125,11 +127,21 @@ def __call__(self, results):
125127

126128

127129

130+
class Plot(enum.Enum):
131+
"""Scatterplot configuration options
132+
"""
133+
OFF = 1
134+
LINEAR = 2
135+
LOG = 3
136+
137+
138+
128139
class dump_markdown_results_table:
129140
"""Print Markdown-formatted tables displaying benchmark results
130141
131142
For each metric, this visualization prints out a table of benchmarks,
132-
showing the value of the metric for each variant.
143+
showing the value of the metric for each variant, combined with an optional
144+
scatterplot.
133145
134146
The 'out_file' key is mandatory; specify '-' to print to stdout.
135147
@@ -145,12 +157,16 @@ class dump_markdown_results_table:
145157
particular combinations of values for different variants, such as
146158
regressions or performance improvements.
147159
160+
'scatterplot' takes the values 'off' (default), 'linear' (linearly scaled
161+
axes), or 'log' (logarithmically scaled axes).
162+
148163
Sample configuration:
149164
150165
```
151166
visualize:
152167
- type: dump_markdown_results_table
153168
out_file: "-"
169+
scatterplot: linear
154170
extra_columns:
155171
runtime:
156172
- column_name: ratio
@@ -187,9 +203,10 @@ class dump_markdown_results_table:
187203
"""
188204

189205

190-
def __init__(self, out_file, extra_columns=None):
206+
def __init__(self, out_file, extra_columns=None, scatterplot=None):
191207
self.get_out_file = benchcomp.Outfile(out_file)
192208
self.extra_columns = self._eval_column_text(extra_columns or {})
209+
self.scatterplot = self._parse_scatterplot_config(scatterplot)
193210

194211

195212
@staticmethod
@@ -206,12 +223,48 @@ def _eval_column_text(column_spec):
206223
return column_spec
207224

208225

226+
@staticmethod
227+
def _parse_scatterplot_config(scatterplot_config_string):
228+
if (scatterplot_config_string is None or
229+
scatterplot_config_string == "off"):
230+
return Plot.OFF
231+
elif scatterplot_config_string == "linear":
232+
return Plot.LINEAR
233+
elif scatterplot_config_string == "log":
234+
return Plot.LOG
235+
else:
236+
logging.error(
237+
"Invalid scatterplot configuration '%s'",
238+
scatterplot_config_string)
239+
sys.exit(1)
240+
241+
209242
@staticmethod
210243
def _get_template():
211244
return textwrap.dedent("""\
212245
{% for metric, benchmarks in d["metrics"].items() %}
213246
## {{ metric }}
214247
248+
{% if scatterplot and metric in d["scaled_metrics"] and d["scaled_variants"][metric]|length == 2 -%}
249+
```mermaid
250+
%%{init: { "quadrantChart": { "titlePadding": 15, "xAxisLabelPadding": 20, "yAxisLabelPadding": 20, "quadrantLabelFontSize": 0, "pointRadius": 2, "pointLabelFontSize": 2 }, "themeVariables": { "quadrant1Fill": "#FFFFFF", "quadrant2Fill": "#FFFFFF", "quadrant3Fill": "#FFFFFF", "quadrant4Fill": "#FFFFFF", "quadrant1TextFill": "#FFFFFF", "quadrant2TextFill": "#FFFFFF", "quadrant3TextFill": "#FFFFFF", "quadrant4TextFill": "#FFFFFF", "quadrantInternalBorderStrokeFill": "#FFFFFF" } } }%%
251+
quadrantChart
252+
title {{ metric }}
253+
x-axis {{ d["scaled_variants"][metric][0] }}
254+
y-axis {{ d["scaled_variants"][metric][1] }}
255+
quadrant-1 1
256+
quadrant-2 2
257+
quadrant-3 3
258+
quadrant-4 4
259+
{%- for bench_name, bench_variants in d["scaled_metrics"][metric]["benchmarks"].items () %}
260+
{% set v0 = bench_variants[d["scaled_variants"][metric][0]] -%}
261+
{% set v1 = bench_variants[d["scaled_variants"][metric][1]] -%}
262+
"{{ bench_name }}": [{{ v0|round(3) }}, {{ v1|round(3) }}]
263+
{%- endfor %}
264+
```
265+
Scatterplot axis ranges are {{ d["scaled_metrics"][metric]["min_value"] }} (bottom/left) to {{ d["scaled_metrics"][metric]["max_value"] }} (top/right).
266+
267+
{% endif -%}
215268
| Benchmark | {% for variant in d["variants"][metric] %} {{ variant }} |{% endfor %}
216269
| --- |{% for variant in d["variants"][metric] %} --- |{% endfor -%}
217270
{% for bench_name, bench_variants in benchmarks.items () %}
@@ -228,7 +281,48 @@ def _get_variant_names(results):
228281

229282

230283
@staticmethod
231-
def _organize_results_into_metrics(results):
284+
def _compute_scaled_metric(data_for_metric, log_scaling):
285+
min_value = math.inf
286+
max_value = -math.inf
287+
for bench, bench_result in data_for_metric.items():
288+
for variant, variant_result in bench_result.items():
289+
if isinstance(variant_result, (bool, str)):
290+
return None
291+
if not isinstance(variant_result, (int, float)):
292+
return None
293+
if variant_result < min_value:
294+
min_value = variant_result
295+
if variant_result > max_value:
296+
max_value = variant_result
297+
ret = {
298+
"benchmarks": {bench: {} for bench in data_for_metric.keys()},
299+
"min_value": "log({})".format(min_value) if log_scaling else min_value,
300+
"max_value": "log({})".format(max_value) if log_scaling else max_value,
301+
}
302+
# 1.0 is not a permissible value for mermaid, so make sure all scaled
303+
# results stay below that by use 0.99 as hard-coded value or
304+
# artificially increasing the range by 10 per cent
305+
if min_value == math.inf or min_value == max_value:
306+
for bench, bench_result in data_for_metric.items():
307+
ret["benchmarks"][bench] = {variant: 0.99 for variant in bench_result.keys()}
308+
else:
309+
if log_scaling:
310+
min_value = math.log(min_value, 10)
311+
max_value = math.log(max_value, 10)
312+
value_range = max_value - min_value
313+
value_range = value_range * 1.1
314+
for bench, bench_result in data_for_metric.items():
315+
for variant, variant_result in bench_result.items():
316+
if log_scaling:
317+
abs_value = math.log(variant_result, 10)
318+
else:
319+
abs_value = variant_result
320+
ret["benchmarks"][bench][variant] = (abs_value - min_value) / value_range
321+
return ret
322+
323+
324+
@staticmethod
325+
def _organize_results_into_metrics(results, log_scaling):
232326
ret = {metric: {} for metric in results["metrics"]}
233327
for bench, bench_result in results["benchmarks"].items():
234328
for variant, variant_result in bench_result["variants"].items():
@@ -246,7 +340,13 @@ def _organize_results_into_metrics(results):
246340
ret[metric][bench] = {
247341
variant: variant_result["metrics"][metric]
248342
}
249-
return ret
343+
ret_scaled = {}
344+
for metric, bench_result in ret.items():
345+
scaled = dump_markdown_results_table._compute_scaled_metric(
346+
bench_result, log_scaling)
347+
if scaled is not None:
348+
ret_scaled[metric] = scaled
349+
return (ret, ret_scaled)
250350

251351

252352
def _add_extra_columns(self, metrics):
@@ -271,20 +371,34 @@ def _get_variants(metrics):
271371
return ret
272372

273373

374+
@staticmethod
375+
def _get_scaled_variants(metrics):
376+
ret = {}
377+
for metric, entries in metrics.items():
378+
for bench, variants in entries["benchmarks"].items():
379+
ret[metric] = list(variants.keys())
380+
break
381+
return ret
382+
383+
274384
def __call__(self, results):
275-
metrics = self._organize_results_into_metrics(results)
385+
(metrics, scaled) = self._organize_results_into_metrics(
386+
results, self.scatterplot == Plot.LOG)
276387
self._add_extra_columns(metrics)
277388

278389
data = {
279390
"metrics": metrics,
280391
"variants": self._get_variants(metrics),
392+
"scaled_metrics": scaled,
393+
"scaled_variants": self._get_scaled_variants(scaled),
281394
}
282395

283396
env = jinja2.Environment(
284397
loader=jinja2.BaseLoader, autoescape=jinja2.select_autoescape(
285398
enabled_extensions=("html"),
286399
default_for_string=True))
287400
template = env.from_string(self._get_template())
288-
output = template.render(d=data)[:-1]
401+
include_scatterplot = self.scatterplot != Plot.OFF
402+
output = template.render(d=data, scatterplot=include_scatterplot)[:-1]
289403
with self.get_out_file() as handle:
290404
print(output, file=handle)

tools/benchcomp/configs/perf-regression.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ visualize:
3333

3434
- type: dump_markdown_results_table
3535
out_file: '-'
36+
scatterplot: linear
3637
extra_columns:
3738

3839
# For these two metrics, display the difference between old and new and

tools/benchcomp/test/test_regression.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -436,6 +436,7 @@ def test_markdown_results_table(self):
436436
"visualize": [{
437437
"type": "dump_markdown_results_table",
438438
"out_file": "-",
439+
"scatterplot": "linear",
439440
"extra_columns": {
440441
"runtime": [{
441442
"column_name": "ratio",
@@ -461,6 +462,21 @@ def test_markdown_results_table(self):
461462
run_bc.stdout, textwrap.dedent("""
462463
## runtime
463464
465+
```mermaid
466+
%%{init: { "quadrantChart": { "titlePadding": 15, "xAxisLabelPadding": 20, "yAxisLabelPadding": 20, "quadrantLabelFontSize": 0, "pointRadius": 2, "pointLabelFontSize": 2 }, "themeVariables": { "quadrant1Fill": "#FFFFFF", "quadrant2Fill": "#FFFFFF", "quadrant3Fill": "#FFFFFF", "quadrant4Fill": "#FFFFFF", "quadrant1TextFill": "#FFFFFF", "quadrant2TextFill": "#FFFFFF", "quadrant3TextFill": "#FFFFFF", "quadrant4TextFill": "#FFFFFF", "quadrantInternalBorderStrokeFill": "#FFFFFF" } } }%%
467+
quadrantChart
468+
title runtime
469+
x-axis variant_1
470+
y-axis variant_2
471+
quadrant-1 1
472+
quadrant-2 2
473+
quadrant-3 3
474+
quadrant-4 4
475+
"bench_1": [0.0, 0.909]
476+
"bench_2": [0.909, 0.0]
477+
```
478+
Scatterplot axis ranges are 5 (bottom/left) to 10 (top/right).
479+
464480
| Benchmark | variant_1 | variant_2 | ratio |
465481
| --- | --- | --- | --- |
466482
| bench_1 | 5 | 10 | **2.0** |

0 commit comments

Comments
 (0)