| #!/usr/bin/env python3 |
| # |
| # Script to summarize the outputs of other scripts. Operates on CSV files. |
| # |
| |
| import functools as ft |
| import collections as co |
| import os |
| import csv |
| import re |
| import math as m |
| |
| # displayable fields |
| Field = co.namedtuple('Field', 'name,parse,acc,key,fmt,repr,null,ratio') |
| FIELDS = [ |
| # name, parse, accumulate, fmt, print, null |
| Field('code', |
| lambda r: int(r['code_size']), |
| sum, |
| lambda r: r, |
| '%7s', |
| lambda r: r, |
| '-', |
| lambda old, new: (new-old)/old), |
| Field('data', |
| lambda r: int(r['data_size']), |
| sum, |
| lambda r: r, |
| '%7s', |
| lambda r: r, |
| '-', |
| lambda old, new: (new-old)/old), |
| Field('stack', |
| lambda r: float(r['stack_limit']), |
| max, |
| lambda r: r, |
| '%7s', |
| lambda r: '∞' if m.isinf(r) else int(r), |
| '-', |
| lambda old, new: (new-old)/old), |
| Field('structs', |
| lambda r: int(r['struct_size']), |
| sum, |
| lambda r: r, |
| '%8s', |
| lambda r: r, |
| '-', |
| lambda old, new: (new-old)/old), |
| Field('coverage', |
| lambda r: (int(r['coverage_hits']), int(r['coverage_count'])), |
| lambda rs: ft.reduce(lambda a, b: (a[0]+b[0], a[1]+b[1]), rs), |
| lambda r: r[0]/r[1], |
| '%19s', |
| lambda r: '%11s %7s' % ('%d/%d' % (r[0], r[1]), '%.1f%%' % (100*r[0]/r[1])), |
| '%11s %7s' % ('-', '-'), |
| lambda old, new: ((new[0]/new[1]) - (old[0]/old[1]))) |
| ] |
| |
| |
| def main(**args): |
| def openio(path, mode='r'): |
| if path == '-': |
| if 'r' in mode: |
| return os.fdopen(os.dup(sys.stdin.fileno()), 'r') |
| else: |
| return os.fdopen(os.dup(sys.stdout.fileno()), 'w') |
| else: |
| return open(path, mode) |
| |
| # find results |
| results = co.defaultdict(lambda: {}) |
| for path in args.get('csv_paths', '-'): |
| try: |
| with openio(path) as f: |
| r = csv.DictReader(f) |
| for result in r: |
| file = result.pop('file', '') |
| name = result.pop('name', '') |
| prev = results[(file, name)] |
| for field in FIELDS: |
| try: |
| r = field.parse(result) |
| if field.name in prev: |
| results[(file, name)][field.name] = field.acc( |
| [prev[field.name], r]) |
| else: |
| results[(file, name)][field.name] = r |
| except (KeyError, ValueError): |
| pass |
| except FileNotFoundError: |
| pass |
| |
| # find fields |
| if args.get('all_fields'): |
| fields = FIELDS |
| elif args.get('fields') is not None: |
| fields_dict = {field.name: field for field in FIELDS} |
| fields = [fields_dict[f] for f in args['fields']] |
| else: |
| fields = [] |
| for field in FIELDS: |
| if any(field.name in result for result in results.values()): |
| fields.append(field) |
| |
| # find total for every field |
| total = {} |
| for result in results.values(): |
| for field in fields: |
| if field.name in result and field.name in total: |
| total[field.name] = field.acc( |
| [total[field.name], result[field.name]]) |
| elif field.name in result: |
| total[field.name] = result[field.name] |
| |
| # find previous results? |
| if args.get('diff'): |
| prev_results = co.defaultdict(lambda: {}) |
| try: |
| with openio(args['diff']) as f: |
| r = csv.DictReader(f) |
| for result in r: |
| file = result.pop('file', '') |
| name = result.pop('name', '') |
| prev = prev_results[(file, name)] |
| for field in FIELDS: |
| try: |
| r = field.parse(result) |
| if field.name in prev: |
| prev_results[(file, name)][field.name] = field.acc( |
| [prev[field.name], r]) |
| else: |
| prev_results[(file, name)][field.name] = r |
| except (KeyError, ValueError): |
| pass |
| except FileNotFoundError: |
| pass |
| |
| prev_total = {} |
| for result in prev_results.values(): |
| for field in fields: |
| if field.name in result and field.name in prev_total: |
| prev_total[field.name] = field.acc( |
| [prev_total[field.name], result[field.name]]) |
| elif field.name in result: |
| prev_total[field.name] = result[field.name] |
| |
| # print results |
| def dedup_entries(results, by='name'): |
| entries = co.defaultdict(lambda: {}) |
| for (file, func), result in results.items(): |
| entry = (file if by == 'file' else func) |
| prev = entries[entry] |
| for field in fields: |
| if field.name in result and field.name in prev: |
| entries[entry][field.name] = field.acc( |
| [prev[field.name], result[field.name]]) |
| elif field.name in result: |
| entries[entry][field.name] = result[field.name] |
| return entries |
| |
| def sorted_entries(entries): |
| if args.get('sort') is not None: |
| field = {field.name: field for field in FIELDS}[args['sort']] |
| return sorted(entries, key=lambda x: ( |
| -(field.key(x[1][field.name])) if field.name in x[1] else -1, x)) |
| elif args.get('reverse_sort') is not None: |
| field = {field.name: field for field in FIELDS}[args['reverse_sort']] |
| return sorted(entries, key=lambda x: ( |
| +(field.key(x[1][field.name])) if field.name in x[1] else -1, x)) |
| else: |
| return sorted(entries) |
| |
| def print_header(by=''): |
| if not args.get('diff'): |
| print('%-36s' % by, end='') |
| for field in fields: |
| print((' '+field.fmt) % field.name, end='') |
| print() |
| else: |
| print('%-36s' % by, end='') |
| for field in fields: |
| print((' '+field.fmt) % field.name, end='') |
| print(' %-9s' % '', end='') |
| print() |
| |
| def print_entry(name, result): |
| print('%-36s' % name, end='') |
| for field in fields: |
| r = result.get(field.name) |
| if r is not None: |
| print((' '+field.fmt) % field.repr(r), end='') |
| else: |
| print((' '+field.fmt) % '-', end='') |
| print() |
| |
| def print_diff_entry(name, old, new): |
| print('%-36s' % name, end='') |
| for field in fields: |
| n = new.get(field.name) |
| if n is not None: |
| print((' '+field.fmt) % field.repr(n), end='') |
| else: |
| print((' '+field.fmt) % '-', end='') |
| o = old.get(field.name) |
| ratio = ( |
| 0.0 if m.isinf(o or 0) and m.isinf(n or 0) |
| else +float('inf') if m.isinf(n or 0) |
| else -float('inf') if m.isinf(o or 0) |
| else 0.0 if not o and not n |
| else +1.0 if not o |
| else -1.0 if not n |
| else field.ratio(o, n)) |
| print(' %-9s' % ( |
| '' if not ratio |
| else '(+∞%)' if ratio > 0 and m.isinf(ratio) |
| else '(-∞%)' if ratio < 0 and m.isinf(ratio) |
| else '(%+.1f%%)' % (100*ratio)), end='') |
| print() |
| |
| def print_entries(by='name'): |
| entries = dedup_entries(results, by=by) |
| |
| if not args.get('diff'): |
| print_header(by=by) |
| for name, result in sorted_entries(entries.items()): |
| print_entry(name, result) |
| else: |
| prev_entries = dedup_entries(prev_results, by=by) |
| print_header(by='%s (%d added, %d removed)' % (by, |
| sum(1 for name in entries if name not in prev_entries), |
| sum(1 for name in prev_entries if name not in entries))) |
| for name, result in sorted_entries(entries.items()): |
| if args.get('all') or result != prev_entries.get(name, {}): |
| print_diff_entry(name, prev_entries.get(name, {}), result) |
| |
| def print_totals(): |
| if not args.get('diff'): |
| print_entry('TOTAL', total) |
| else: |
| print_diff_entry('TOTAL', prev_total, total) |
| |
| if args.get('summary'): |
| print_header() |
| print_totals() |
| elif args.get('files'): |
| print_entries(by='file') |
| print_totals() |
| else: |
| print_entries(by='name') |
| print_totals() |
| |
| |
| if __name__ == "__main__": |
| import argparse |
| import sys |
| parser = argparse.ArgumentParser( |
| description="Summarize measurements") |
| parser.add_argument('csv_paths', nargs='*', default='-', |
| help="Description of where to find *.csv files. May be a directory \ |
| or list of paths. *.csv files will be merged to show the total \ |
| coverage.") |
| parser.add_argument('-d', '--diff', |
| help="Specify CSV file to diff against.") |
| parser.add_argument('-a', '--all', action='store_true', |
| help="Show all objects, not just the ones that changed.") |
| parser.add_argument('-e', '--all-fields', action='store_true', |
| help="Show all fields, even those with no results.") |
| parser.add_argument('-f', '--fields', type=lambda x: re.split('\s*,\s*', x), |
| help="Comma separated list of fields to print, by default all fields \ |
| that are found in the CSV files are printed.") |
| parser.add_argument('-s', '--sort', |
| help="Sort by this field.") |
| parser.add_argument('-S', '--reverse-sort', |
| help="Sort by this field, but backwards.") |
| parser.add_argument('-F', '--files', action='store_true', |
| help="Show file-level calls.") |
| parser.add_argument('-Y', '--summary', action='store_true', |
| help="Only show the totals.") |
| sys.exit(main(**vars(parser.parse_args()))) |