Home | History | Annotate | Download | only in bm_diff
      1 #!/usr/bin/env python2.7
      2 #
      3 # Copyright 2017 gRPC authors.
      4 #
      5 # Licensed under the Apache License, Version 2.0 (the "License");
      6 # you may not use this file except in compliance with the License.
      7 # You may obtain a copy of the License at
      8 #
      9 #     http://www.apache.org/licenses/LICENSE-2.0
     10 #
     11 # Unless required by applicable law or agreed to in writing, software
     12 # distributed under the License is distributed on an "AS IS" BASIS,
     13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     14 # See the License for the specific language governing permissions and
     15 # limitations under the License.
     16 """ Computes the diff between two bm runs and outputs significant results """
     17 
     18 import bm_constants
     19 import bm_speedup
     20 
     21 import sys
     22 import os
     23 
     24 sys.path.append(os.path.join(os.path.dirname(sys.argv[0]), '..'))
     25 import bm_json
     26 
     27 import json
     28 import tabulate
     29 import argparse
     30 import collections
     31 import subprocess
     32 
     33 verbose = False
     34 
     35 
     36 def _median(ary):
     37     assert (len(ary))
     38     ary = sorted(ary)
     39     n = len(ary)
     40     if n % 2 == 0:
     41         return (ary[(n - 1) / 2] + ary[(n - 1) / 2 + 1]) / 2.0
     42     else:
     43         return ary[n / 2]
     44 
     45 
     46 def _args():
     47     argp = argparse.ArgumentParser(
     48         description='Perform diff on microbenchmarks')
     49     argp.add_argument(
     50         '-t',
     51         '--track',
     52         choices=sorted(bm_constants._INTERESTING),
     53         nargs='+',
     54         default=sorted(bm_constants._INTERESTING),
     55         help='Which metrics to track')
     56     argp.add_argument(
     57         '-b',
     58         '--benchmarks',
     59         nargs='+',
     60         choices=bm_constants._AVAILABLE_BENCHMARK_TESTS,
     61         default=bm_constants._AVAILABLE_BENCHMARK_TESTS,
     62         help='Which benchmarks to run')
     63     argp.add_argument(
     64         '-l',
     65         '--loops',
     66         type=int,
     67         default=20,
     68         help=
     69         'Number of times to loops the benchmarks. Must match what was passed to bm_run.py'
     70     )
     71     argp.add_argument(
     72         '-r',
     73         '--regex',
     74         type=str,
     75         default="",
     76         help='Regex to filter benchmarks run')
     77     argp.add_argument('--counters', dest='counters', action='store_true')
     78     argp.add_argument('--no-counters', dest='counters', action='store_false')
     79     argp.set_defaults(counters=True)
     80     argp.add_argument('-n', '--new', type=str, help='New benchmark name')
     81     argp.add_argument('-o', '--old', type=str, help='Old benchmark name')
     82     argp.add_argument(
     83         '-v', '--verbose', type=bool, help='Print details of before/after')
     84     args = argp.parse_args()
     85     global verbose
     86     if args.verbose: verbose = True
     87     assert args.new
     88     assert args.old
     89     return args
     90 
     91 
     92 def _maybe_print(str):
     93     if verbose: print str
     94 
     95 
     96 class Benchmark:
     97 
     98     def __init__(self):
     99         self.samples = {
    100             True: collections.defaultdict(list),
    101             False: collections.defaultdict(list)
    102         }
    103         self.final = {}
    104 
    105     def add_sample(self, track, data, new):
    106         for f in track:
    107             if f in data:
    108                 self.samples[new][f].append(float(data[f]))
    109 
    110     def process(self, track, new_name, old_name):
    111         for f in sorted(track):
    112             new = self.samples[True][f]
    113             old = self.samples[False][f]
    114             if not new or not old: continue
    115             mdn_diff = abs(_median(new) - _median(old))
    116             _maybe_print('%s: %s=%r %s=%r mdn_diff=%r' %
    117                          (f, new_name, new, old_name, old, mdn_diff))
    118             s = bm_speedup.speedup(new, old, 1e-5)
    119             if abs(s) > 3:
    120                 if mdn_diff > 0.5 or 'trickle' in f:
    121                     self.final[f] = '%+d%%' % s
    122         return self.final.keys()
    123 
    124     def skip(self):
    125         return not self.final
    126 
    127     def row(self, flds):
    128         return [self.final[f] if f in self.final else '' for f in flds]
    129 
    130 
    131 def _read_json(filename, badjson_files, nonexistant_files):
    132     stripped = ".".join(filename.split(".")[:-2])
    133     try:
    134         with open(filename) as f:
    135             r = f.read()
    136             return json.loads(r)
    137     except IOError, e:
    138         if stripped in nonexistant_files:
    139             nonexistant_files[stripped] += 1
    140         else:
    141             nonexistant_files[stripped] = 1
    142         return None
    143     except ValueError, e:
    144         print r
    145         if stripped in badjson_files:
    146             badjson_files[stripped] += 1
    147         else:
    148             badjson_files[stripped] = 1
    149         return None
    150 
    151 
    152 def fmt_dict(d):
    153     return ''.join(["    " + k + ": " + str(d[k]) + "\n" for k in d])
    154 
    155 
    156 def diff(bms, loops, regex, track, old, new, counters):
    157     benchmarks = collections.defaultdict(Benchmark)
    158 
    159     badjson_files = {}
    160     nonexistant_files = {}
    161     for bm in bms:
    162         for loop in range(0, loops):
    163             for line in subprocess.check_output([
    164                     'bm_diff_%s/opt/%s' % (old, bm), '--benchmark_list_tests',
    165                     '--benchmark_filter=%s' % regex
    166             ]).splitlines():
    167                 stripped_line = line.strip().replace("/", "_").replace(
    168                     "<", "_").replace(">", "_").replace(", ", "_")
    169                 js_new_opt = _read_json('%s.%s.opt.%s.%d.json' %
    170                                         (bm, stripped_line, new, loop),
    171                                         badjson_files, nonexistant_files)
    172                 js_old_opt = _read_json('%s.%s.opt.%s.%d.json' %
    173                                         (bm, stripped_line, old, loop),
    174                                         badjson_files, nonexistant_files)
    175                 if counters:
    176                     js_new_ctr = _read_json('%s.%s.counters.%s.%d.json' %
    177                                             (bm, stripped_line, new, loop),
    178                                             badjson_files, nonexistant_files)
    179                     js_old_ctr = _read_json('%s.%s.counters.%s.%d.json' %
    180                                             (bm, stripped_line, old, loop),
    181                                             badjson_files, nonexistant_files)
    182                 else:
    183                     js_new_ctr = None
    184                     js_old_ctr = None
    185 
    186                 for row in bm_json.expand_json(js_new_ctr, js_new_opt):
    187                     name = row['cpp_name']
    188                     if name.endswith('_mean') or name.endswith('_stddev'):
    189                         continue
    190                     benchmarks[name].add_sample(track, row, True)
    191                 for row in bm_json.expand_json(js_old_ctr, js_old_opt):
    192                     name = row['cpp_name']
    193                     if name.endswith('_mean') or name.endswith('_stddev'):
    194                         continue
    195                     benchmarks[name].add_sample(track, row, False)
    196 
    197     really_interesting = set()
    198     for name, bm in benchmarks.items():
    199         _maybe_print(name)
    200         really_interesting.update(bm.process(track, new, old))
    201     fields = [f for f in track if f in really_interesting]
    202 
    203     headers = ['Benchmark'] + fields
    204     rows = []
    205     for name in sorted(benchmarks.keys()):
    206         if benchmarks[name].skip(): continue
    207         rows.append([name] + benchmarks[name].row(fields))
    208     note = None
    209     if len(badjson_files):
    210         note = 'Corrupt JSON data (indicates timeout or crash): \n%s' % fmt_dict(
    211             badjson_files)
    212     if len(nonexistant_files):
    213         if note:
    214             note += '\n\nMissing files (indicates new benchmark): \n%s' % fmt_dict(
    215                 nonexistant_files)
    216         else:
    217             note = '\n\nMissing files (indicates new benchmark): \n%s' % fmt_dict(
    218                 nonexistant_files)
    219     if rows:
    220         return tabulate.tabulate(rows, headers=headers, floatfmt='+.2f'), note
    221     else:
    222         return None, note
    223 
    224 
    225 if __name__ == '__main__':
    226     args = _args()
    227     diff, note = diff(args.benchmarks, args.loops, args.regex, args.track,
    228                       args.old, args.new, args.counters)
    229     print('%s\n%s' % (note, diff if diff else "No performance differences"))
    230