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