3
3
4
4
5
5
import dataclasses
6
+ import enum
6
7
import json
7
8
import logging
9
+ import math
8
10
import subprocess
9
11
import sys
10
12
import textwrap
@@ -125,11 +127,21 @@ def __call__(self, results):
125
127
126
128
127
129
130
+ class Plot (enum .Enum ):
131
+ """Scatterplot configuration options
132
+ """
133
+ OFF = 1
134
+ LINEAR = 2
135
+ LOG = 3
136
+
137
+
138
+
128
139
class dump_markdown_results_table :
129
140
"""Print Markdown-formatted tables displaying benchmark results
130
141
131
142
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.
133
145
134
146
The 'out_file' key is mandatory; specify '-' to print to stdout.
135
147
@@ -145,12 +157,16 @@ class dump_markdown_results_table:
145
157
particular combinations of values for different variants, such as
146
158
regressions or performance improvements.
147
159
160
+ 'scatterplot' takes the values 'off' (default), 'linear' (linearly scaled
161
+ axes), or 'log' (logarithmically scaled axes).
162
+
148
163
Sample configuration:
149
164
150
165
```
151
166
visualize:
152
167
- type: dump_markdown_results_table
153
168
out_file: "-"
169
+ scatterplot: linear
154
170
extra_columns:
155
171
runtime:
156
172
- column_name: ratio
@@ -187,9 +203,10 @@ class dump_markdown_results_table:
187
203
"""
188
204
189
205
190
- def __init__ (self , out_file , extra_columns = None ):
206
+ def __init__ (self , out_file , extra_columns = None , scatterplot = None ):
191
207
self .get_out_file = benchcomp .Outfile (out_file )
192
208
self .extra_columns = self ._eval_column_text (extra_columns or {})
209
+ self .scatterplot = self ._parse_scatterplot_config (scatterplot )
193
210
194
211
195
212
@staticmethod
@@ -206,12 +223,48 @@ def _eval_column_text(column_spec):
206
223
return column_spec
207
224
208
225
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
+
209
242
@staticmethod
210
243
def _get_template ():
211
244
return textwrap .dedent ("""\
212
245
{% for metric, benchmarks in d["metrics"].items() %}
213
246
## {{ metric }}
214
247
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 -%}
215
268
| Benchmark | {% for variant in d["variants"][metric] %} {{ variant }} |{% endfor %}
216
269
| --- |{% for variant in d["variants"][metric] %} --- |{% endfor -%}
217
270
{% for bench_name, bench_variants in benchmarks.items () %}
@@ -228,7 +281,48 @@ def _get_variant_names(results):
228
281
229
282
230
283
@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 ):
232
326
ret = {metric : {} for metric in results ["metrics" ]}
233
327
for bench , bench_result in results ["benchmarks" ].items ():
234
328
for variant , variant_result in bench_result ["variants" ].items ():
@@ -246,7 +340,13 @@ def _organize_results_into_metrics(results):
246
340
ret [metric ][bench ] = {
247
341
variant : variant_result ["metrics" ][metric ]
248
342
}
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 )
250
350
251
351
252
352
def _add_extra_columns (self , metrics ):
@@ -271,20 +371,34 @@ def _get_variants(metrics):
271
371
return ret
272
372
273
373
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
+
274
384
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 )
276
387
self ._add_extra_columns (metrics )
277
388
278
389
data = {
279
390
"metrics" : metrics ,
280
391
"variants" : self ._get_variants (metrics ),
392
+ "scaled_metrics" : scaled ,
393
+ "scaled_variants" : self ._get_scaled_variants (scaled ),
281
394
}
282
395
283
396
env = jinja2 .Environment (
284
397
loader = jinja2 .BaseLoader , autoescape = jinja2 .select_autoescape (
285
398
enabled_extensions = ("html" ),
286
399
default_for_string = True ))
287
400
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 ]
289
403
with self .get_out_file () as handle :
290
404
print (output , file = handle )
0 commit comments