1#!/usr/bin/env python
2
3# Copyright 2016 Google Inc.
4#
5# Use of this source code is governed by a BSD-style license that can be
6# found in the LICENSE file.
7
8from __future__ import print_function
9from _benchresult import BenchResult
10from argparse import ArgumentParser
11from collections import defaultdict, namedtuple
12from datetime import datetime
13import operator
14import os
15import sys
16import tempfile
17import urllib
18import urlparse
19import webbrowser
20
21__argparse = ArgumentParser(description="""
22
23Formats skpbench.py outputs as csv.
24
25This script can also be used to generate a Google sheet:
26
27(1) Install the "Office Editing for Docs, Sheets & Slides" Chrome extension:
28    https://chrome.google.com/webstore/detail/office-editing-for-docs-s/gbkeegbaiigmenfmjfclcdgdpimamgkj
29
30(2) Update your global OS file associations to use Chrome for .csv files.
31
32(3) Run parseskpbench.py with the --open flag.
33
34""")
35
36__argparse.add_argument('-r', '--result',
37  choices=['accum', 'median', 'max', 'min'], default='accum',
38  help="result to use for cell values")
39__argparse.add_argument('-f', '--force',
40  action='store_true', help='silently ignore warnings')
41__argparse.add_argument('-o', '--open',
42  action='store_true',
43  help="generate a temp file and open it (theoretically in a web browser)")
44__argparse.add_argument('-n', '--name',
45  default='skpbench_%s' % datetime.now().strftime('%Y-%m-%d_%H.%M.%S.csv'),
46  help="if using --open, a name for the temp file")
47__argparse.add_argument('sources',
48  nargs='+', help="source files that contain skpbench results ('-' for stdin)")
49
50FLAGS = __argparse.parse_args()
51
52RESULT_QUALIFIERS = ('sample_ms', 'clock', 'metric')
53
54class FullConfig(namedtuple('fullconfig', ('config',) + RESULT_QUALIFIERS)):
55  def qualified_name(self, qualifiers=RESULT_QUALIFIERS):
56    return get_qualified_name(self.config.replace(',', ' '),
57                              {x:getattr(self, x) for x in qualifiers})
58
59def get_qualified_name(name, qualifiers):
60  if not qualifiers:
61    return name
62  else:
63    args = ('%s=%s' % (k,v) for k,v in qualifiers.iteritems())
64    return '%s (%s)' % (name, ' '.join(args))
65
66class Parser:
67  def __init__(self):
68    self.sheet_qualifiers = {x:None for x in RESULT_QUALIFIERS}
69    self.config_qualifiers = set()
70    self.fullconfigs = list() # use list to preserve the order.
71    self.rows = defaultdict(dict)
72    self.cols = defaultdict(dict)
73
74  def parse_file(self, infile):
75    for line in infile:
76      match = BenchResult.match(line)
77      if not match:
78        continue
79
80      fullconfig = FullConfig(*(match.get_string(x)
81                                for x in FullConfig._fields))
82      if not fullconfig in self.fullconfigs:
83        self.fullconfigs.append(fullconfig)
84
85      for qualifier, value in self.sheet_qualifiers.items():
86        if value is None:
87          self.sheet_qualifiers[qualifier] = match.get_string(qualifier)
88        elif value != match.get_string(qualifier):
89          del self.sheet_qualifiers[qualifier]
90          self.config_qualifiers.add(qualifier)
91
92      self.rows[match.bench][fullconfig] = match.get_string(FLAGS.result)
93      self.cols[fullconfig][match.bench] = getattr(match, FLAGS.result)
94
95  def print_csv(self, outfile=sys.stdout):
96    # Write the title.
97    print(get_qualified_name(FLAGS.result, self.sheet_qualifiers), file=outfile)
98
99    # Write the header.
100    outfile.write('bench,')
101    for fullconfig in self.fullconfigs:
102      outfile.write('%s,' % fullconfig.qualified_name(self.config_qualifiers))
103    outfile.write('\n')
104
105    # Write the rows.
106    for bench, row in self.rows.iteritems():
107      outfile.write('%s,' % bench)
108      for fullconfig in self.fullconfigs:
109        if fullconfig in row:
110          outfile.write('%s,' % row[fullconfig])
111        elif FLAGS.force:
112          outfile.write('NULL,')
113        else:
114          raise ValueError("%s: missing value for %s. (use --force to ignore)" %
115                           (bench,
116                            fullconfig.qualified_name(self.config_qualifiers)))
117      outfile.write('\n')
118
119    # Add simple, literal averages.
120    if len(self.rows) > 1:
121      outfile.write('\n')
122      self._print_computed_row('MEAN',
123        lambda col: reduce(operator.add, col.values()) / len(col),
124        outfile=outfile)
125      self._print_computed_row('GEOMEAN',
126        lambda col: reduce(operator.mul, col.values()) ** (1.0 / len(col)),
127        outfile=outfile)
128
129  def _print_computed_row(self, name, func, outfile=sys.stdout):
130    outfile.write('%s,' % name)
131    for fullconfig in self.fullconfigs:
132      if len(self.cols[fullconfig]) != len(self.rows):
133        outfile.write('NULL,')
134        continue
135      outfile.write('%.4g,' % func(self.cols[fullconfig]))
136    outfile.write('\n')
137
138def main():
139  parser = Parser()
140
141  # Parse the input files.
142  for src in FLAGS.sources:
143    if src == '-':
144      parser.parse_file(sys.stdin)
145    else:
146      with open(src, mode='r') as infile:
147        parser.parse_file(infile)
148
149  # Print the csv.
150  if not FLAGS.open:
151    parser.print_csv()
152  else:
153    dirname = tempfile.mkdtemp()
154    basename = FLAGS.name
155    if os.path.splitext(basename)[1] != '.csv':
156      basename += '.csv';
157    pathname = os.path.join(dirname, basename)
158    with open(pathname, mode='w') as tmpfile:
159      parser.print_csv(outfile=tmpfile)
160    fileuri = urlparse.urljoin('file:', urllib.pathname2url(pathname))
161    print('opening %s' % fileuri)
162    webbrowser.open(fileuri)
163
164
165if __name__ == '__main__':
166  main()
167