1#!/usr/bin/env python3 2# Copyright 2018 the V8 project authors. All rights reserved. 3# Use of this source code is governed by a BSD-style license that can 4# be found in the LICENSE file. 5""" 6This script averages numbers output from another script. It is useful 7to average over a benchmark that outputs one or more results of the form 8 <key> <number> <unit> 9key and unit are optional, but only one number per line is processed. 10 11For example, if 12 $ bch --allow-natives-syntax toNumber.js 13outputs 14 Number('undefined'): 155763 15 (+'undefined'): 193050 Kps 16 23736 Kps 17then 18 $ avg.py 10 bch --allow-natives-syntax toNumber.js 19will output 20 [10/10] (+'undefined') : avg 192,240.40 stddev 6,486.24 (185,529.00 - 206,186.00) 21 [10/10] Number('undefined') : avg 156,990.10 stddev 16,327.56 (144,718.00 - 202,840.00) Kps 22 [10/10] [default] : avg 22,885.80 stddev 1,941.80 ( 17,584.00 - 24,266.00) Kps 23""" 24 25import argparse 26import subprocess 27import re 28import numpy 29import time 30import sys 31import signal 32 33parser = argparse.ArgumentParser( 34 description="A script that averages numbers from another script's output", 35 epilog="Example:\n\tavg.py 10 bash -c \"echo A: 100; echo B 120; sleep .1\"" 36) 37parser.add_argument( 38 'repetitions', 39 type=int, 40 help="number of times the command should be repeated") 41parser.add_argument( 42 'command', 43 nargs=argparse.REMAINDER, 44 help="command to run (no quotes needed)") 45parser.add_argument( 46 '--echo', 47 '-e', 48 action='store_true', 49 default=False, 50 help="set this flag to echo the command's output") 51 52args = vars(parser.parse_args()) 53 54if (len(args['command']) == 0): 55 print("No command provided.") 56 exit(1) 57 58 59class FieldWidth: 60 61 def __init__(self, key=0, average=0, stddev=0, min=0, max=0): 62 self.w = dict(key=key, average=average, stddev=stddev, min=min, max=max) 63 64 def max_with(self, w2): 65 self.w = {k: max(v, w2.w[k]) for k, v in self.w.items()} 66 67 def __getattr__(self, key): 68 return self.w[key] 69 70 71def fmtS(string, width=0): 72 return "{0:<{1}}".format(string, width) 73 74 75def fmtN(num, width=0): 76 return "{0:>{1},.2f}".format(num, width) 77 78 79class Measurement: 80 81 def __init__(self, key, unit): 82 self.key = key 83 self.unit = unit 84 self.values = [] 85 self.average = 0 86 self.count = 0 87 self.M2 = 0 88 self.min = float("inf") 89 self.max = -float("inf") 90 91 def addValue(self, value): 92 try: 93 num_value = float(value) 94 self.values.append(num_value) 95 self.min = min(self.min, num_value) 96 self.max = max(self.max, num_value) 97 self.count = self.count + 1 98 delta = num_value - self.average 99 self.average = self.average + delta / self.count 100 delta2 = num_value - self.average 101 self.M2 = self.M2 + delta * delta2 102 except ValueError: 103 print("Ignoring non-numeric value", value) 104 105 def status(self, w): 106 return "{}: avg {} stddev {} ({} - {}) {}".format( 107 fmtS(self.key, w.key), fmtN(self.average, w.average), 108 fmtN(self.stddev(), w.stddev), fmtN(self.min, w.min), 109 fmtN(self.max, w.max), fmtS(self.unit_string())) 110 111 def unit_string(self): 112 if self.unit == None: 113 return "" 114 return self.unit 115 116 def variance(self): 117 if self.count < 2: 118 return float('NaN') 119 return self.M2 / (self.count - 1) 120 121 def stddev(self): 122 return numpy.sqrt(self.variance()) 123 124 def size(self): 125 return len(self.values) 126 127 def widths(self): 128 return FieldWidth( 129 key=len(fmtS(self.key)), 130 average=len(fmtN(self.average)), 131 stddev=len(fmtN(self.stddev())), 132 min=len(fmtN(self.min)), 133 max=len(fmtN(self.max))) 134 135 136rep_string = str(args['repetitions']) 137 138 139class Measurements: 140 141 def __init__(self): 142 self.all = {} 143 self.default_key = '[default]' 144 self.max_widths = FieldWidth() 145 146 def record(self, key, value, unit): 147 if (key == None): 148 key = self.default_key 149 if key not in self.all: 150 self.all[key] = Measurement(key, unit) 151 self.all[key].addValue(value) 152 self.max_widths.max_with(self.all[key].widths()) 153 154 def any(self): 155 if len(self.all) >= 1: 156 return next(iter(self.all.values())) 157 else: 158 return None 159 160 def format_status(self): 161 m = self.any() 162 if m == None: 163 return "" 164 return m.status(self.max_widths) 165 166 def format_num(self, m): 167 return "[{0:>{1}}/{2}]".format(m.size(), len(rep_string), rep_string) 168 169 def print_status(self): 170 if len(self.all) == 0: 171 print("No results found. Check format?") 172 return 173 print(self.format_num(self.any()), self.format_status(), sep=" ", end="") 174 175 def print_results(self): 176 for key in self.all: 177 m = self.all[key] 178 print(self.format_num(m), m.status(self.max_widths), sep=" ") 179 180 181measurements = Measurements() 182 183 184def signal_handler(signal, frame): 185 print("", end="\r") 186 measurements.print_status() 187 print() 188 measurements.print_results() 189 sys.exit(0) 190 191 192signal.signal(signal.SIGINT, signal_handler) 193 194for x in range(0, args['repetitions']): 195 proc = subprocess.Popen(args['command'], stdout=subprocess.PIPE) 196 for line in proc.stdout: 197 if args['echo']: 198 print(line.decode(), end="") 199 for m in re.finditer( 200 r'\A((?P<key>.*[^\s\d:]+)[:]?)?\s*(?P<value>[0-9]+(.[0-9]+)?)\ ?(?P<unit>[^\d\W]\w*)?\s*\Z', 201 line.decode()): 202 measurements.record(m.group('key'), m.group('value'), m.group('unit')) 203 proc.wait() 204 if proc.returncode != 0: 205 print("Child exited with status %d" % proc.returncode) 206 break 207 measurements.print_status() 208 print("", end="\r") 209 210measurements.print_results() 211