#!/usr/bin/env python3 # Copyright 2018 the V8 project authors. All rights reserved. # Use of this source code is governed by a BSD-style license that can # be found in the LICENSE file. """ This script averages numbers output from another script. It is useful to average over a benchmark that outputs one or more results of the form <key> <number> <unit> key and unit are optional, but only one number per line is processed. For example, if $ bch --allow-natives-syntax toNumber.js outputs Number('undefined'): 155763 (+'undefined'): 193050 Kps 23736 Kps then $ avg.py 10 bch --allow-natives-syntax toNumber.js will output [10/10] (+'undefined') : avg 192,240.40 stddev 6,486.24 (185,529.00 - 206,186.00) [10/10] Number('undefined') : avg 156,990.10 stddev 16,327.56 (144,718.00 - 202,840.00) Kps [10/10] [default] : avg 22,885.80 stddev 1,941.80 ( 17,584.00 - 24,266.00) Kps """ import argparse import subprocess import re import numpy import time import sys import signal parser = argparse.ArgumentParser( description="A script that averages numbers from another script's output", epilog="Example:\n\tavg.py 10 bash -c \"echo A: 100; echo B 120; sleep .1\"" ) parser.add_argument( 'repetitions', type=int, help="number of times the command should be repeated") parser.add_argument( 'command', nargs=argparse.REMAINDER, help="command to run (no quotes needed)") parser.add_argument( '--echo', '-e', action='store_true', default=False, help="set this flag to echo the command's output") args = vars(parser.parse_args()) if (len(args['command']) == 0): print("No command provided.") exit(1) class FieldWidth: def __init__(self, key=0, average=0, stddev=0, min=0, max=0): self.w = dict(key=key, average=average, stddev=stddev, min=min, max=max) def max_with(self, w2): self.w = {k: max(v, w2.w[k]) for k, v in self.w.items()} def __getattr__(self, key): return self.w[key] def fmtS(string, width=0): return "{0:<{1}}".format(string, width) def fmtN(num, width=0): return "{0:>{1},.2f}".format(num, width) class Measurement: def __init__(self, key, unit): self.key = key self.unit = unit self.values = [] self.average = 0 self.count = 0 self.M2 = 0 self.min = float("inf") self.max = -float("inf") def addValue(self, value): try: num_value = float(value) self.values.append(num_value) self.min = min(self.min, num_value) self.max = max(self.max, num_value) self.count = self.count + 1 delta = num_value - self.average self.average = self.average + delta / self.count delta2 = num_value - self.average self.M2 = self.M2 + delta * delta2 except ValueError: print("Ignoring non-numeric value", value) def status(self, w): return "{}: avg {} stddev {} ({} - {}) {}".format( fmtS(self.key, w.key), fmtN(self.average, w.average), fmtN(self.stddev(), w.stddev), fmtN(self.min, w.min), fmtN(self.max, w.max), fmtS(self.unit_string())) def unit_string(self): if self.unit == None: return "" return self.unit def variance(self): if self.count < 2: return float('NaN') return self.M2 / (self.count - 1) def stddev(self): return numpy.sqrt(self.variance()) def size(self): return len(self.values) def widths(self): return FieldWidth( key=len(fmtS(self.key)), average=len(fmtN(self.average)), stddev=len(fmtN(self.stddev())), min=len(fmtN(self.min)), max=len(fmtN(self.max))) rep_string = str(args['repetitions']) class Measurements: def __init__(self): self.all = {} self.default_key = '[default]' self.max_widths = FieldWidth() def record(self, key, value, unit): if (key == None): key = self.default_key if key not in self.all: self.all[key] = Measurement(key, unit) self.all[key].addValue(value) self.max_widths.max_with(self.all[key].widths()) def any(self): if len(self.all) >= 1: return next(iter(self.all.values())) else: return None def format_status(self): m = self.any() if m == None: return "" return m.status(self.max_widths) def format_num(self, m): return "[{0:>{1}}/{2}]".format(m.size(), len(rep_string), rep_string) def print_status(self): if len(self.all) == 0: print("No results found. Check format?") return print(self.format_num(self.any()), self.format_status(), sep=" ", end="") def print_results(self): for key in self.all: m = self.all[key] print(self.format_num(m), m.status(self.max_widths), sep=" ") measurements = Measurements() def signal_handler(signal, frame): print("", end="\r") measurements.print_status() print() measurements.print_results() sys.exit(0) signal.signal(signal.SIGINT, signal_handler) for x in range(0, args['repetitions']): proc = subprocess.Popen(args['command'], stdout=subprocess.PIPE) for line in proc.stdout: if args['echo']: print(line.decode(), end="") for m in re.finditer( r'\A((?P<key>.*[^\s\d:]+)[:]?)?\s*(?P<value>[0-9]+(.[0-9]+)?)\ ?(?P<unit>[^\d\W]\w*)?\s*\Z', line.decode()): measurements.record(m.group('key'), m.group('value'), m.group('unit')) proc.wait() if proc.returncode != 0: print("Child exited with status %d" % proc.returncode) break measurements.print_status() print("", end="\r") measurements.print_results()