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