1#!/usr/bin/env python3 2# 3# Script to summarize the outputs of other scripts. Operates on CSV files. 4# 5 6import functools as ft 7import collections as co 8import os 9import csv 10import re 11import math as m 12 13# displayable fields 14Field = co.namedtuple('Field', 'name,parse,acc,key,fmt,repr,null,ratio') 15FIELDS = [ 16 # name, parse, accumulate, fmt, print, null 17 Field('code', 18 lambda r: int(r['code_size']), 19 sum, 20 lambda r: r, 21 '%7s', 22 lambda r: r, 23 '-', 24 lambda old, new: (new-old)/old), 25 Field('data', 26 lambda r: int(r['data_size']), 27 sum, 28 lambda r: r, 29 '%7s', 30 lambda r: r, 31 '-', 32 lambda old, new: (new-old)/old), 33 Field('stack', 34 lambda r: float(r['stack_limit']), 35 max, 36 lambda r: r, 37 '%7s', 38 lambda r: '∞' if m.isinf(r) else int(r), 39 '-', 40 lambda old, new: (new-old)/old), 41 Field('structs', 42 lambda r: int(r['struct_size']), 43 sum, 44 lambda r: r, 45 '%8s', 46 lambda r: r, 47 '-', 48 lambda old, new: (new-old)/old), 49 Field('coverage', 50 lambda r: (int(r['coverage_hits']), int(r['coverage_count'])), 51 lambda rs: ft.reduce(lambda a, b: (a[0]+b[0], a[1]+b[1]), rs), 52 lambda r: r[0]/r[1], 53 '%19s', 54 lambda r: '%11s %7s' % ('%d/%d' % (r[0], r[1]), '%.1f%%' % (100*r[0]/r[1])), 55 '%11s %7s' % ('-', '-'), 56 lambda old, new: ((new[0]/new[1]) - (old[0]/old[1]))) 57] 58 59 60def main(**args): 61 def openio(path, mode='r'): 62 if path == '-': 63 if 'r' in mode: 64 return os.fdopen(os.dup(sys.stdin.fileno()), 'r') 65 else: 66 return os.fdopen(os.dup(sys.stdout.fileno()), 'w') 67 else: 68 return open(path, mode) 69 70 # find results 71 results = co.defaultdict(lambda: {}) 72 for path in args.get('csv_paths', '-'): 73 try: 74 with openio(path) as f: 75 r = csv.DictReader(f) 76 for result in r: 77 file = result.pop('file', '') 78 name = result.pop('name', '') 79 prev = results[(file, name)] 80 for field in FIELDS: 81 try: 82 r = field.parse(result) 83 if field.name in prev: 84 results[(file, name)][field.name] = field.acc( 85 [prev[field.name], r]) 86 else: 87 results[(file, name)][field.name] = r 88 except (KeyError, ValueError): 89 pass 90 except FileNotFoundError: 91 pass 92 93 # find fields 94 if args.get('all_fields'): 95 fields = FIELDS 96 elif args.get('fields') is not None: 97 fields_dict = {field.name: field for field in FIELDS} 98 fields = [fields_dict[f] for f in args['fields']] 99 else: 100 fields = [] 101 for field in FIELDS: 102 if any(field.name in result for result in results.values()): 103 fields.append(field) 104 105 # find total for every field 106 total = {} 107 for result in results.values(): 108 for field in fields: 109 if field.name in result and field.name in total: 110 total[field.name] = field.acc( 111 [total[field.name], result[field.name]]) 112 elif field.name in result: 113 total[field.name] = result[field.name] 114 115 # find previous results? 116 if args.get('diff'): 117 prev_results = co.defaultdict(lambda: {}) 118 try: 119 with openio(args['diff']) as f: 120 r = csv.DictReader(f) 121 for result in r: 122 file = result.pop('file', '') 123 name = result.pop('name', '') 124 prev = prev_results[(file, name)] 125 for field in FIELDS: 126 try: 127 r = field.parse(result) 128 if field.name in prev: 129 prev_results[(file, name)][field.name] = field.acc( 130 [prev[field.name], r]) 131 else: 132 prev_results[(file, name)][field.name] = r 133 except (KeyError, ValueError): 134 pass 135 except FileNotFoundError: 136 pass 137 138 prev_total = {} 139 for result in prev_results.values(): 140 for field in fields: 141 if field.name in result and field.name in prev_total: 142 prev_total[field.name] = field.acc( 143 [prev_total[field.name], result[field.name]]) 144 elif field.name in result: 145 prev_total[field.name] = result[field.name] 146 147 # print results 148 def dedup_entries(results, by='name'): 149 entries = co.defaultdict(lambda: {}) 150 for (file, func), result in results.items(): 151 entry = (file if by == 'file' else func) 152 prev = entries[entry] 153 for field in fields: 154 if field.name in result and field.name in prev: 155 entries[entry][field.name] = field.acc( 156 [prev[field.name], result[field.name]]) 157 elif field.name in result: 158 entries[entry][field.name] = result[field.name] 159 return entries 160 161 def sorted_entries(entries): 162 if args.get('sort') is not None: 163 field = {field.name: field for field in FIELDS}[args['sort']] 164 return sorted(entries, key=lambda x: ( 165 -(field.key(x[1][field.name])) if field.name in x[1] else -1, x)) 166 elif args.get('reverse_sort') is not None: 167 field = {field.name: field for field in FIELDS}[args['reverse_sort']] 168 return sorted(entries, key=lambda x: ( 169 +(field.key(x[1][field.name])) if field.name in x[1] else -1, x)) 170 else: 171 return sorted(entries) 172 173 def print_header(by=''): 174 if not args.get('diff'): 175 print('%-36s' % by, end='') 176 for field in fields: 177 print((' '+field.fmt) % field.name, end='') 178 print() 179 else: 180 print('%-36s' % by, end='') 181 for field in fields: 182 print((' '+field.fmt) % field.name, end='') 183 print(' %-9s' % '', end='') 184 print() 185 186 def print_entry(name, result): 187 print('%-36s' % name, end='') 188 for field in fields: 189 r = result.get(field.name) 190 if r is not None: 191 print((' '+field.fmt) % field.repr(r), end='') 192 else: 193 print((' '+field.fmt) % '-', end='') 194 print() 195 196 def print_diff_entry(name, old, new): 197 print('%-36s' % name, end='') 198 for field in fields: 199 n = new.get(field.name) 200 if n is not None: 201 print((' '+field.fmt) % field.repr(n), end='') 202 else: 203 print((' '+field.fmt) % '-', end='') 204 o = old.get(field.name) 205 ratio = ( 206 0.0 if m.isinf(o or 0) and m.isinf(n or 0) 207 else +float('inf') if m.isinf(n or 0) 208 else -float('inf') if m.isinf(o or 0) 209 else 0.0 if not o and not n 210 else +1.0 if not o 211 else -1.0 if not n 212 else field.ratio(o, n)) 213 print(' %-9s' % ( 214 '' if not ratio 215 else '(+∞%)' if ratio > 0 and m.isinf(ratio) 216 else '(-∞%)' if ratio < 0 and m.isinf(ratio) 217 else '(%+.1f%%)' % (100*ratio)), end='') 218 print() 219 220 def print_entries(by='name'): 221 entries = dedup_entries(results, by=by) 222 223 if not args.get('diff'): 224 print_header(by=by) 225 for name, result in sorted_entries(entries.items()): 226 print_entry(name, result) 227 else: 228 prev_entries = dedup_entries(prev_results, by=by) 229 print_header(by='%s (%d added, %d removed)' % (by, 230 sum(1 for name in entries if name not in prev_entries), 231 sum(1 for name in prev_entries if name not in entries))) 232 for name, result in sorted_entries(entries.items()): 233 if args.get('all') or result != prev_entries.get(name, {}): 234 print_diff_entry(name, prev_entries.get(name, {}), result) 235 236 def print_totals(): 237 if not args.get('diff'): 238 print_entry('TOTAL', total) 239 else: 240 print_diff_entry('TOTAL', prev_total, total) 241 242 if args.get('summary'): 243 print_header() 244 print_totals() 245 elif args.get('files'): 246 print_entries(by='file') 247 print_totals() 248 else: 249 print_entries(by='name') 250 print_totals() 251 252 253if __name__ == "__main__": 254 import argparse 255 import sys 256 parser = argparse.ArgumentParser( 257 description="Summarize measurements") 258 parser.add_argument('csv_paths', nargs='*', default='-', 259 help="Description of where to find *.csv files. May be a directory \ 260 or list of paths. *.csv files will be merged to show the total \ 261 coverage.") 262 parser.add_argument('-d', '--diff', 263 help="Specify CSV file to diff against.") 264 parser.add_argument('-a', '--all', action='store_true', 265 help="Show all objects, not just the ones that changed.") 266 parser.add_argument('-e', '--all-fields', action='store_true', 267 help="Show all fields, even those with no results.") 268 parser.add_argument('-f', '--fields', type=lambda x: re.split('\s*,\s*', x), 269 help="Comma separated list of fields to print, by default all fields \ 270 that are found in the CSV files are printed.") 271 parser.add_argument('-s', '--sort', 272 help="Sort by this field.") 273 parser.add_argument('-S', '--reverse-sort', 274 help="Sort by this field, but backwards.") 275 parser.add_argument('-F', '--files', action='store_true', 276 help="Show file-level calls.") 277 parser.add_argument('-Y', '--summary', action='store_true', 278 help="Only show the totals.") 279 sys.exit(main(**vars(parser.parse_args()))) 280