Commit cba3a50a authored by Sigurd Schneider's avatar Sigurd Schneider Committed by Commit Bot

[tools] Improve avg.py script

- Output from console.timeEnd is now supported
- The final result is printed in table format with ; separator,
  making it easy to copy/paste into a spreadsheet.
- Various style improvements.

Change-Id: Iba00ee54720344765262b5cc44c1e939278b03a4
Notry: true
Reviewed-on: https://chromium-review.googlesource.com/c/1405030
Commit-Queue: Sigurd Schneider <sigurds@chromium.org>
Reviewed-by: 's avatarMichael Achenbach <machenbach@chromium.org>
Cr-Commit-Position: refs/heads/master@{#59030}
parent e7e61ce6
...@@ -23,49 +23,49 @@ will output ...@@ -23,49 +23,49 @@ will output
""" """
import argparse import argparse
import subprocess import math
import re import re
import numpy
import time
import sys
import signal import signal
import subprocess
import sys
parser = argparse.ArgumentParser( PARSER = argparse.ArgumentParser(
description="A script that averages numbers from another script's output", 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\"" epilog="Example:\n\tavg.py 10 bash -c \"echo A: 100; echo B 120; sleep .1\""
) )
parser.add_argument( PARSER.add_argument(
'repetitions', 'repetitions',
type=int, type=int,
help="number of times the command should be repeated") help="number of times the command should be repeated")
parser.add_argument( PARSER.add_argument(
'command', 'command',
nargs=argparse.REMAINDER, nargs=argparse.REMAINDER,
help="command to run (no quotes needed)") help="command to run (no quotes needed)")
parser.add_argument( PARSER.add_argument(
'--echo', '--echo',
'-e', '-e',
action='store_true', action='store_true',
default=False, default=False,
help="set this flag to echo the command's output") help="set this flag to echo the command's output")
args = vars(parser.parse_args()) ARGS = vars(PARSER.parse_args())
if (len(args['command']) == 0): if not ARGS['command']:
print("No command provided.") print("No command provided.")
exit(1) exit(1)
class FieldWidth: class FieldWidth:
def __init__(self, key=0, average=0, stddev=0, min=0, max=0): def __init__(self, points=0, key=0, average=0, stddev=0, min_width=0, max_width=0):
self.w = dict(key=key, average=average, stddev=stddev, min=min, max=max) self.widths = dict(points=points, key=key, average=average, stddev=stddev,
min=min_width, max=max_width)
def max_with(self, w2): def max_widths(self, other):
self.w = {k: max(v, w2.w[k]) for k, v in self.w.items()} self.widths = {k: max(v, other.widths[k]) for k, v in self.widths.items()}
def __getattr__(self, key): def __getattr__(self, key):
return self.w[key] return self.widths[key]
def fmtS(string, width=0): def fmtS(string, width=0):
...@@ -76,6 +76,27 @@ def fmtN(num, width=0): ...@@ -76,6 +76,27 @@ def fmtN(num, width=0):
return "{0:>{1},.2f}".format(num, width) return "{0:>{1},.2f}".format(num, width)
def fmt(num):
return "{0:>,.2f}".format(num)
def format_line(points, key, average, stddev, min_value, max_value,
unit_string, widths):
return "{:>{}}; {:<{}}; {:>{}}; {:>{}}; {:>{}}; {:>{}}; {}".format(
points, widths.points,
key, widths.key,
average, widths.average,
stddev, widths.stddev,
min_value, widths.min,
max_value, widths.max,
unit_string)
def fmt_reps(msrmnt):
rep_string = str(ARGS['repetitions'])
return "[{0:>{1}}/{2}]".format(msrmnt.size(), len(rep_string), rep_string)
class Measurement: class Measurement:
def __init__(self, key, unit): def __init__(self, key, unit):
...@@ -102,14 +123,21 @@ class Measurement: ...@@ -102,14 +123,21 @@ class Measurement:
except ValueError: except ValueError:
print("Ignoring non-numeric value", value) print("Ignoring non-numeric value", value)
def status(self, w): def status(self, widths):
return "{}: avg {} stddev {} ({} - {}) {}".format( return "{} {}: avg {} stddev {} ({} - {}) {}".format(
fmtS(self.key, w.key), fmtN(self.average, w.average), fmt_reps(self),
fmtN(self.stddev(), w.stddev), fmtN(self.min, w.min), fmtS(self.key, widths.key), fmtN(self.average, widths.average),
fmtN(self.max, w.max), fmtS(self.unit_string())) fmtN(self.stddev(), widths.stddev), fmtN(self.min, widths.min),
fmtN(self.max, widths.max), fmtS(self.unit_string()))
def result(self, widths):
return format_line(self.size(), self.key, fmt(self.average),
fmt(self.stddev()), fmt(self.min),
fmt(self.max), self.unit_string(),
widths)
def unit_string(self): def unit_string(self):
if self.unit == None: if not self.unit:
return "" return ""
return self.unit return self.unit
...@@ -119,21 +147,24 @@ class Measurement: ...@@ -119,21 +147,24 @@ class Measurement:
return self.M2 / (self.count - 1) return self.M2 / (self.count - 1)
def stddev(self): def stddev(self):
return numpy.sqrt(self.variance()) return math.sqrt(self.variance())
def size(self): def size(self):
return len(self.values) return len(self.values)
def widths(self): def widths(self):
return FieldWidth( return FieldWidth(
key=len(fmtS(self.key)), points=len("{}".format(self.size())) + 2,
average=len(fmtN(self.average)), key=len(self.key),
stddev=len(fmtN(self.stddev())), average=len(fmt(self.average)),
min=len(fmtN(self.min)), stddev=len(fmt(self.stddev())),
max=len(fmtN(self.max))) min_width=len(fmt(self.min)),
max_width=len(fmt(self.max)))
rep_string = str(args['repetitions']) def result_header(widths):
return format_line("#/{}".format(ARGS['repetitions']),
"id", "avg", "stddev", "min", "max", "unit", widths)
class Measurements: class Measurements:
...@@ -141,70 +172,73 @@ class Measurements: ...@@ -141,70 +172,73 @@ class Measurements:
def __init__(self): def __init__(self):
self.all = {} self.all = {}
self.default_key = '[default]' self.default_key = '[default]'
self.max_widths = FieldWidth() self.max_widths = FieldWidth(
points=len("{}".format(ARGS['repetitions'])) + 2,
key=len("id"),
average=len("avg"),
stddev=len("stddev"),
min_width=len("min"),
max_width=len("max"))
self.last_status_len = 0
def record(self, key, value, unit): def record(self, key, value, unit):
if (key == None): if not key:
key = self.default_key key = self.default_key
if key not in self.all: if key not in self.all:
self.all[key] = Measurement(key, unit) self.all[key] = Measurement(key, unit)
self.all[key].addValue(value) self.all[key].addValue(value)
self.max_widths.max_with(self.all[key].widths()) self.max_widths.max_widths(self.all[key].widths())
def any(self): def any(self):
if len(self.all) >= 1: if self.all:
return next(iter(self.all.values())) return next(iter(self.all.values()))
else: return None
return None
def format_status(self):
m = self.any()
if m == None:
return ""
return m.status(self.max_widths)
def format_num(self, m): def print_results(self):
return "[{0:>{1}}/{2}]".format(m.size(), len(rep_string), rep_string) print("{:<{}}".format("", self.last_status_len), end="\r")
print(result_header(self.max_widths), sep=" ")
for key in sorted(self.all):
print(self.all[key].result(self.max_widths), sep=" ")
def print_status(self): def print_status(self):
if len(self.all) == 0: status = "No results found. Check format?"
print("No results found. Check format?") measurement = MEASUREMENTS.any()
return if measurement:
print(self.format_num(self.any()), self.format_status(), sep=" ", end="") status = measurement.status(MEASUREMENTS.max_widths)
print("{:<{}}".format(status, self.last_status_len), end="\r")
self.last_status_len = len(status)
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()
measurements = Measurements()
def signal_handler(signum, frame):
def signal_handler(signal, frame):
print("", end="\r") print("", end="\r")
measurements.print_status() MEASUREMENTS.print_results()
print()
measurements.print_results()
sys.exit(0) sys.exit(0)
signal.signal(signal.SIGINT, signal_handler) signal.signal(signal.SIGINT, signal_handler)
for x in range(0, args['repetitions']): SCORE_REGEX = (r'\A((console.timeEnd: )?'
proc = subprocess.Popen(args['command'], stdout=subprocess.PIPE) r'(?P<key>[^\s:,]+)[,:]?)?'
r'(^\s*|\s+)'
r'(?P<value>[0-9]+(.[0-9]+)?)'
r'\ ?(?P<unit>[^\d\W]\w*)?[.\s]*\Z')
for x in range(0, ARGS['repetitions']):
proc = subprocess.Popen(ARGS['command'], stdout=subprocess.PIPE)
for line in proc.stdout: for line in proc.stdout:
if args['echo']: if ARGS['echo']:
print(line.decode(), end="") print(line.decode(), end="")
for m in re.finditer( for m in re.finditer(SCORE_REGEX, line.decode()):
r'\A((?P<key>.*[^\s\d:]+)[:]?)?\s*(?P<value>[0-9]+(.[0-9]+)?)\ ?(?P<unit>[^\d\W]\w*)?\s*\Z', MEASUREMENTS.record(m.group('key'), m.group('value'), m.group('unit'))
line.decode()):
measurements.record(m.group('key'), m.group('value'), m.group('unit'))
proc.wait() proc.wait()
if proc.returncode != 0: if proc.returncode != 0:
print("Child exited with status %d" % proc.returncode) print("Child exited with status %d" % proc.returncode)
break break
measurements.print_status()
print("", end="\r")
measurements.print_results() MEASUREMENTS.print_status()
# Print final results
MEASUREMENTS.print_results()
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment