• Home
  • History
  • Annotate
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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