#!/usr/bin/env python3 import argparse import re import statistics import sys import plotly import tabulate def parse_lnt(lines): """ Parse lines in LNT format and return a dictionnary of the form: { 'benchmark1': { 'metric1': [float], 'metric2': [float], ... }, 'benchmark2': { 'metric1': [float], 'metric2': [float], ... }, ... } Each metric may have multiple values. """ results = {} for line in lines: line = line.strip() if not line: continue (identifier, value) = line.split(' ') (name, metric) = identifier.split('.') if name not in results: results[name] = {} if metric not in results[name]: results[name][metric] = [] results[name][metric].append(float(value)) return results def plain_text_comparison(benchmarks, baseline, candidate): """ Create a tabulated comparison of the baseline and the candidate. """ headers = ['Benchmark', 'Baseline', 'Candidate', 'Difference', '% Difference'] fmt = (None, '.2f', '.2f', '.2f', '.2f') table = [] for (bm, base, cand) in zip(benchmarks, baseline, candidate): diff = (cand - base) if base and cand else None percent = 100 * (diff / base) if base and cand else None row = [bm, base, cand, diff, percent] table.append(row) return tabulate.tabulate(table, headers=headers, floatfmt=fmt, numalign='right') def create_chart(benchmarks, baseline, candidate): """ Create a bar chart comparing 'baseline' and 'candidate'. """ figure = plotly.graph_objects.Figure() figure.add_trace(plotly.graph_objects.Bar(x=benchmarks, y=baseline, name='Baseline')) figure.add_trace(plotly.graph_objects.Bar(x=benchmarks, y=candidate, name='Candidate')) return figure def prepare_series(baseline, candidate, metric, aggregate=statistics.median): """ Prepare the data for being formatted or displayed as a chart. Metrics that have more than one value are aggregated using the given aggregation function. """ all_benchmarks = sorted(list(set(baseline.keys()) | set(candidate.keys()))) baseline_series = [] candidate_series = [] for bm in all_benchmarks: baseline_series.append(aggregate(baseline[bm][metric]) if bm in baseline and metric in baseline[bm] else None) candidate_series.append(aggregate(candidate[bm][metric]) if bm in candidate and metric in candidate[bm] else None) return (all_benchmarks, baseline_series, candidate_series) def main(argv): parser = argparse.ArgumentParser( prog='compare-benchmarks', description='Compare the results of two sets of benchmarks in LNT format.', epilog='This script requires the `tabulate` and the `plotly` Python modules.') parser.add_argument('baseline', type=argparse.FileType('r'), help='Path to a LNT format file containing the benchmark results for the baseline.') parser.add_argument('candidate', type=argparse.FileType('r'), help='Path to a LNT format file containing the benchmark results for the candidate.') parser.add_argument('--output', '-o', type=argparse.FileType('w'), default=sys.stdout, help='Path of a file where to output the resulting comparison. Default to stdout.') parser.add_argument('--metric', type=str, default='execution_time', help='The metric to compare. LNT data may contain multiple metrics (e.g. code size, execution time, etc) -- ' 'this option allows selecting which metric is being analyzed. The default is "execution_time".') parser.add_argument('--filter', type=str, required=False, help='An optional regular expression used to filter the benchmarks included in the comparison. ' 'Only benchmarks whose names match the regular expression will be included.') parser.add_argument('--format', type=str, choices=['text', 'chart'], default='text', help='Select the output format. "text" generates a plain-text comparison in tabular form, and "chart" ' 'generates a self-contained HTML graph that can be opened in a browser. The default is text.') args = parser.parse_args(argv) baseline = parse_lnt(args.baseline.readlines()) candidate = parse_lnt(args.candidate.readlines()) if args.filter is not None: regex = re.compile(args.filter) baseline = {k: v for (k, v) in baseline.items() if regex.search(k)} candidate = {k: v for (k, v) in candidate.items() if regex.search(k)} (benchmarks, baseline_series, candidate_series) = prepare_series(baseline, candidate, args.metric) if args.format == 'chart': figure = create_chart(benchmarks, baseline_series, candidate_series) plotly.io.write_html(figure, file=args.output) else: diff = plain_text_comparison(benchmarks, baseline_series, candidate_series) args.output.write(diff) args.output.write('\n') if __name__ == '__main__': main(sys.argv[1:])