|
| 1 | +#!/usr/bin/env python |
| 2 | +"""Compare 2 perf runs. |
| 3 | +
|
| 4 | +To use, specify the local directories that contain |
| 5 | +the run information:: |
| 6 | +
|
| 7 | + $ ./perfcmp /results/2016-01-01-1111/ /results/2016-01-01-2222/ |
| 8 | +
|
| 9 | +""" |
| 10 | +import os |
| 11 | +import json |
| 12 | +import argparse |
| 13 | + |
| 14 | +from colorama import Fore, Style |
| 15 | +from tabulate import tabulate |
| 16 | + |
| 17 | + |
| 18 | +class RunComparison(object): |
| 19 | + |
| 20 | + MEMORY_FIELDS = ['average_memory', 'max_memory'] |
| 21 | + TIME_FIELDS = ['total_time'] |
| 22 | + # Fields that aren't memory or time fields, they require |
| 23 | + # no special formatting. |
| 24 | + OTHER_FIELDS = ['average_cpu'] |
| 25 | + |
| 26 | + def __init__(self, old_summary, new_summary): |
| 27 | + self.old_summary = old_summary |
| 28 | + self.new_summary = new_summary |
| 29 | + |
| 30 | + def iter_field_names(self): |
| 31 | + for field in self.TIME_FIELDS + self.MEMORY_FIELDS + self.OTHER_FIELDS: |
| 32 | + yield field |
| 33 | + |
| 34 | + def old(self, field): |
| 35 | + value = self.old_summary[field] |
| 36 | + return self._format(field, value) |
| 37 | + |
| 38 | + def old_suffix(self, field): |
| 39 | + value = self.old_summary[field] |
| 40 | + return self._format_suffix(field, value) |
| 41 | + |
| 42 | + def new_suffix(self, field): |
| 43 | + value = self.new_summary[field] |
| 44 | + return self._format_suffix(field, value) |
| 45 | + |
| 46 | + def _format_suffix(self, field, value): |
| 47 | + if field in self.TIME_FIELDS: |
| 48 | + return 'sec' |
| 49 | + elif field in self.OTHER_FIELDS: |
| 50 | + return '' |
| 51 | + else: |
| 52 | + # The suffix depends on the actual value. |
| 53 | + return self._human_readable_size(value)[1] |
| 54 | + |
| 55 | + def old_stddev(self, field): |
| 56 | + real_field = 'std_dev_%s' % field |
| 57 | + return self.old(real_field) |
| 58 | + |
| 59 | + def new(self, field): |
| 60 | + value = self.new_summary[field] |
| 61 | + return self._format(field, value) |
| 62 | + |
| 63 | + def new_stddev(self, field): |
| 64 | + real_field = 'std_dev_%s' % field |
| 65 | + return self.new(real_field) |
| 66 | + |
| 67 | + def _format(self, field, value): |
| 68 | + if field.startswith('std_dev_'): |
| 69 | + field = field[len('std_dev_'):] |
| 70 | + if field in self.MEMORY_FIELDS: |
| 71 | + return self._human_readable_size(value)[0] |
| 72 | + elif field in self.TIME_FIELDS: |
| 73 | + return '%-3.2f' % value |
| 74 | + else: |
| 75 | + return '%.2f' % value |
| 76 | + |
| 77 | + def _human_readable_size(self, value): |
| 78 | + hummanize_suffixes = ('KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB') |
| 79 | + base = 1024 |
| 80 | + bytes_int = float(value) |
| 81 | + |
| 82 | + if bytes_int == 1: |
| 83 | + return '1 Byte' |
| 84 | + elif bytes_int < base: |
| 85 | + return '%d Bytes' % bytes_int |
| 86 | + |
| 87 | + for i, suffix in enumerate(hummanize_suffixes): |
| 88 | + unit = base ** (i+2) |
| 89 | + if round((bytes_int / unit) * base) < base: |
| 90 | + return ['%.2f' % (base * bytes_int / unit), suffix] |
| 91 | + |
| 92 | + def diff_percent(self, field): |
| 93 | + diff_percent = ( |
| 94 | + (self.new_summary[field] - self.old_summary[field]) / |
| 95 | + float(self.old_summary[field])) * 100 |
| 96 | + return diff_percent |
| 97 | + |
| 98 | + |
| 99 | +def compare_runs(old_dir, new_dir): |
| 100 | + for dirname in os.listdir(old_dir): |
| 101 | + old_run_dir = os.path.join(old_dir, dirname) |
| 102 | + new_run_dir = os.path.join(new_dir, dirname) |
| 103 | + if not os.path.isdir(old_run_dir): |
| 104 | + continue |
| 105 | + old_summary = get_summary(old_run_dir) |
| 106 | + new_summary = get_summary(new_run_dir) |
| 107 | + comp = RunComparison(old_summary, new_summary) |
| 108 | + header = [Style.BRIGHT + dirname + Style.RESET_ALL, |
| 109 | + Style.BRIGHT + 'old' + Style.RESET_ALL, |
| 110 | + # Numeric suffix (MiB, GiB, sec). |
| 111 | + '', |
| 112 | + 'std_dev', |
| 113 | + Style.BRIGHT + 'new' + Style.RESET_ALL, |
| 114 | + # Numeric suffix (MiB, GiB, sec). |
| 115 | + '', |
| 116 | + 'std_dev', |
| 117 | + Style.BRIGHT + 'delta' + Style.RESET_ALL] |
| 118 | + rows = [] |
| 119 | + for field in comp.iter_field_names(): |
| 120 | + row = [field, comp.old(field), comp.old_suffix(field), |
| 121 | + comp.old_stddev(field), comp.new(field), |
| 122 | + comp.new_suffix(field), comp.new_stddev(field)] |
| 123 | + diff_percent = comp.diff_percent(field) |
| 124 | + diff_percent_str = '%.2f%%' % diff_percent |
| 125 | + if diff_percent < 0: |
| 126 | + diff_percent_str = ( |
| 127 | + Fore.GREEN + diff_percent_str + Style.RESET_ALL) |
| 128 | + else: |
| 129 | + diff_percent_str = ( |
| 130 | + Fore.RED + diff_percent_str + Style.RESET_ALL) |
| 131 | + row.append(diff_percent_str) |
| 132 | + rows.append(row) |
| 133 | + print(tabulate(rows, headers=header, tablefmt='plain')) |
| 134 | + print('') |
| 135 | + |
| 136 | + |
| 137 | +def get_summary(benchmark_dir): |
| 138 | + summary_json = os.path.join(benchmark_dir, 'summary.json') |
| 139 | + with open(summary_json) as f: |
| 140 | + return json.load(f) |
| 141 | + |
| 142 | + |
| 143 | +def main(): |
| 144 | + parser = argparse.ArgumentParser(description='__doc__') |
| 145 | + parser.add_argument('oldrunid', help='Path to old run idir') |
| 146 | + parser.add_argument('newrunid', help='Local to new run dir') |
| 147 | + parser.add_argument('--local', action='store_true') |
| 148 | + args = parser.parse_args() |
| 149 | + compare_runs(args.oldrunid, args.newrunid) |
| 150 | + |
| 151 | + |
| 152 | +if __name__ == '__main__': |
| 153 | + main() |
0 commit comments