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