summary.py 11 KB

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