code.py 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205
  1. #!/usr/bin/env python3
  2. #
  3. # Script to find code size at the function level. Basically just a bit wrapper
  4. # around nm with some extra conveniences for comparing builds. Heavily inspired
  5. # by Linux's Bloat-O-Meter.
  6. #
  7. import os
  8. import glob
  9. import itertools as it
  10. import subprocess as sp
  11. import shlex
  12. import re
  13. import csv
  14. import collections as co
  15. OBJ_PATHS = ['*.o', 'bd/*.o']
  16. def collect(paths, **args):
  17. results = co.defaultdict(lambda: 0)
  18. pattern = re.compile(
  19. '^(?P<size>[0-9a-fA-F]+)' +
  20. ' (?P<type>[%s])' % re.escape(args['type']) +
  21. ' (?P<func>.+?)$')
  22. for path in paths:
  23. # note nm-tool may contain extra args
  24. cmd = args['nm_tool'] + ['--size-sort', path]
  25. if args.get('verbose'):
  26. print(' '.join(shlex.quote(c) for c in cmd))
  27. proc = sp.Popen(cmd, stdout=sp.PIPE, universal_newlines=True)
  28. for line in proc.stdout:
  29. m = pattern.match(line)
  30. if m:
  31. results[(path, m.group('func'))] += int(m.group('size'), 16)
  32. flat_results = []
  33. for (file, func), size in results.items():
  34. # map to source files
  35. if args.get('build_dir'):
  36. file = re.sub('%s/*' % re.escape(args['build_dir']), '', file)
  37. # discard internal functions
  38. if func.startswith('__'):
  39. continue
  40. # discard .8449 suffixes created by optimizer
  41. func = re.sub('\.[0-9]+', '', func)
  42. flat_results.append((file, func, size))
  43. return flat_results
  44. def main(**args):
  45. # find sizes
  46. if not args.get('use', None):
  47. # find .o files
  48. paths = []
  49. for path in args['obj_paths']:
  50. if os.path.isdir(path):
  51. path = path + '/*.o'
  52. for path in glob.glob(path):
  53. paths.append(path)
  54. if not paths:
  55. print('no .obj files found in %r?' % args['obj_paths'])
  56. sys.exit(-1)
  57. results = collect(paths, **args)
  58. else:
  59. with open(args['use']) as f:
  60. r = csv.DictReader(f)
  61. results = [
  62. ( result['file'],
  63. result['function'],
  64. int(result['size']))
  65. for result in r]
  66. total = 0
  67. for _, _, size in results:
  68. total += size
  69. # find previous results?
  70. if args.get('diff'):
  71. with open(args['diff']) as f:
  72. r = csv.DictReader(f)
  73. prev_results = [
  74. ( result['file'],
  75. result['function'],
  76. int(result['size']))
  77. for result in r]
  78. prev_total = 0
  79. for _, _, size in prev_results:
  80. prev_total += size
  81. # write results to CSV
  82. if args.get('output'):
  83. with open(args['output'], 'w') as f:
  84. w = csv.writer(f)
  85. w.writerow(['file', 'function', 'size'])
  86. for file, func, size in sorted(results):
  87. w.writerow((file, func, size))
  88. # print results
  89. def dedup_entries(results, by='function'):
  90. entries = co.defaultdict(lambda: 0)
  91. for file, func, size in results:
  92. entry = (file if by == 'file' else func)
  93. entries[entry] += size
  94. return entries
  95. def diff_entries(olds, news):
  96. diff = co.defaultdict(lambda: (0, 0, 0, 0))
  97. for name, new in news.items():
  98. diff[name] = (0, new, new, 1.0)
  99. for name, old in olds.items():
  100. _, new, _, _ = diff[name]
  101. diff[name] = (old, new, new-old, (new-old)/old if old else 1.0)
  102. return diff
  103. def print_header(by=''):
  104. if not args.get('diff'):
  105. print('%-36s %7s' % (by, 'size'))
  106. else:
  107. print('%-36s %7s %7s %7s' % (by, 'old', 'new', 'diff'))
  108. def print_entries(by='function'):
  109. entries = dedup_entries(results, by=by)
  110. if not args.get('diff'):
  111. print_header(by=by)
  112. for name, size in sorted(entries.items()):
  113. print("%-36s %7d" % (name, size))
  114. else:
  115. prev_entries = dedup_entries(prev_results, by=by)
  116. diff = diff_entries(prev_entries, entries)
  117. print_header(by='%s (%d added, %d removed)' % (by,
  118. sum(1 for old, _, _, _ in diff.values() if not old),
  119. sum(1 for _, new, _, _ in diff.values() if not new)))
  120. for name, (old, new, diff, ratio) in sorted(diff.items(),
  121. key=lambda x: (-x[1][3], x)):
  122. if ratio or args.get('all'):
  123. print("%-36s %7s %7s %+7d%s" % (name,
  124. old or "-",
  125. new or "-",
  126. diff,
  127. ' (%+.1f%%)' % (100*ratio) if ratio else ''))
  128. def print_totals():
  129. if not args.get('diff'):
  130. print("%-36s %7d" % ('TOTAL', total))
  131. else:
  132. ratio = (total-prev_total)/prev_total if prev_total else 1.0
  133. print("%-36s %7s %7s %+7d%s" % (
  134. 'TOTAL',
  135. prev_total if prev_total else '-',
  136. total if total else '-',
  137. total-prev_total,
  138. ' (%+.1f%%)' % (100*ratio) if ratio else ''))
  139. if args.get('quiet'):
  140. pass
  141. elif args.get('summary'):
  142. print_header()
  143. print_totals()
  144. elif args.get('files'):
  145. print_entries(by='file')
  146. print_totals()
  147. else:
  148. print_entries(by='function')
  149. print_totals()
  150. if __name__ == "__main__":
  151. import argparse
  152. import sys
  153. parser = argparse.ArgumentParser(
  154. description="Find code size at the function level.")
  155. parser.add_argument('obj_paths', nargs='*', default=OBJ_PATHS,
  156. help="Description of where to find *.o files. May be a directory \
  157. or a list of paths. Defaults to %r." % OBJ_PATHS)
  158. parser.add_argument('-v', '--verbose', action='store_true',
  159. help="Output commands that run behind the scenes.")
  160. parser.add_argument('-o', '--output',
  161. help="Specify CSV file to store results.")
  162. parser.add_argument('-u', '--use',
  163. help="Don't compile and find code sizes, instead use this CSV file.")
  164. parser.add_argument('-d', '--diff',
  165. help="Specify CSV file to diff code size against.")
  166. parser.add_argument('-a', '--all', action='store_true',
  167. help="Show all functions, not just the ones that changed.")
  168. parser.add_argument('--files', action='store_true',
  169. help="Show file-level code sizes. Note this does not include padding! "
  170. "So sizes may differ from other tools.")
  171. parser.add_argument('-s', '--summary', action='store_true',
  172. help="Only show the total code size.")
  173. parser.add_argument('-q', '--quiet', action='store_true',
  174. help="Don't show anything, useful with -o.")
  175. parser.add_argument('--type', default='tTrRdDbB',
  176. help="Type of symbols to report, this uses the same single-character "
  177. "type-names emitted by nm. Defaults to %(default)r.")
  178. parser.add_argument('--nm-tool', default=['nm'], type=lambda x: x.split(),
  179. help="Path to the nm tool to use.")
  180. parser.add_argument('--build-dir',
  181. help="Specify the relative build directory. Used to map object files \
  182. to the correct source files.")
  183. sys.exit(main(**vars(parser.parse_args())))