blob: b9ceb0d9072feefa14920ec34d610283d44cec5f [file] [log] [blame]
#!/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()