code.py 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233
  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']
  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,
  28. stdout=sp.PIPE,
  29. stderr=sp.PIPE if not args.get('verbose') else None,
  30. universal_newlines=True)
  31. for line in proc.stdout:
  32. m = pattern.match(line)
  33. if m:
  34. results[(path, m.group('func'))] += int(m.group('size'), 16)
  35. proc.wait()
  36. if proc.returncode != 0:
  37. if not args.get('verbose'):
  38. for line in proc.stderr:
  39. sys.stdout.write(line)
  40. sys.exit(-1)
  41. flat_results = []
  42. for (file, func), size in results.items():
  43. # map to source files
  44. if args.get('build_dir'):
  45. file = re.sub('%s/*' % re.escape(args['build_dir']), '', file)
  46. # discard internal functions
  47. if func.startswith('__'):
  48. continue
  49. # discard .8449 suffixes created by optimizer
  50. func = re.sub('\.[0-9]+', '', func)
  51. flat_results.append((file, func, size))
  52. return flat_results
  53. def main(**args):
  54. # find sizes
  55. if not args.get('use', None):
  56. # find .o files
  57. paths = []
  58. for path in args['obj_paths']:
  59. if os.path.isdir(path):
  60. path = path + '/*.o'
  61. for path in glob.glob(path):
  62. paths.append(path)
  63. if not paths:
  64. print('no .obj files found in %r?' % args['obj_paths'])
  65. sys.exit(-1)
  66. results = collect(paths, **args)
  67. else:
  68. with open(args['use']) as f:
  69. r = csv.DictReader(f)
  70. results = [
  71. ( result['file'],
  72. result['function'],
  73. int(result['size']))
  74. for result in r]
  75. total = 0
  76. for _, _, size in results:
  77. total += size
  78. # find previous results?
  79. if args.get('diff'):
  80. with open(args['diff']) as f:
  81. r = csv.DictReader(f)
  82. prev_results = [
  83. ( result['file'],
  84. result['function'],
  85. int(result['size']))
  86. for result in r]
  87. prev_total = 0
  88. for _, _, size in prev_results:
  89. prev_total += size
  90. # write results to CSV
  91. if args.get('output'):
  92. with open(args['output'], 'w') as f:
  93. w = csv.writer(f)
  94. w.writerow(['file', 'function', 'size'])
  95. for file, func, size in sorted(results):
  96. w.writerow((file, func, size))
  97. # print results
  98. def dedup_entries(results, by='function'):
  99. entries = co.defaultdict(lambda: 0)
  100. for file, func, size in results:
  101. entry = (file if by == 'file' else func)
  102. entries[entry] += size
  103. return entries
  104. def diff_entries(olds, news):
  105. diff = co.defaultdict(lambda: (0, 0, 0, 0))
  106. for name, new in news.items():
  107. diff[name] = (0, new, new, 1.0)
  108. for name, old in olds.items():
  109. _, new, _, _ = diff[name]
  110. diff[name] = (old, new, new-old, (new-old)/old if old else 1.0)
  111. return diff
  112. def sorted_entries(entries):
  113. if args.get('size_sort'):
  114. return sorted(entries.items(), key=lambda x: (-x[1], x))
  115. elif args.get('reverse_size_sort'):
  116. return sorted(entries.items(), key=lambda x: (+x[1], x))
  117. else:
  118. return sorted(entries.items())
  119. def sorted_diff_entries(entries):
  120. if args.get('size_sort'):
  121. return sorted(entries.items(), key=lambda x: (-x[1][1], x))
  122. elif args.get('reverse_size_sort'):
  123. return sorted(entries.items(), key=lambda x: (+x[1][1], x))
  124. else:
  125. return sorted(entries.items(), key=lambda x: (-x[1][3], x))
  126. def print_header(by=''):
  127. if not args.get('diff'):
  128. print('%-36s %7s' % (by, 'size'))
  129. else:
  130. print('%-36s %7s %7s %7s' % (by, 'old', 'new', 'diff'))
  131. def print_entries(by='function'):
  132. entries = dedup_entries(results, by=by)
  133. if not args.get('diff'):
  134. print_header(by=by)
  135. for name, size in sorted_entries(entries):
  136. print("%-36s %7d" % (name, size))
  137. else:
  138. prev_entries = dedup_entries(prev_results, by=by)
  139. diff = diff_entries(prev_entries, entries)
  140. print_header(by='%s (%d added, %d removed)' % (by,
  141. sum(1 for old, _, _, _ in diff.values() if not old),
  142. sum(1 for _, new, _, _ in diff.values() if not new)))
  143. for name, (old, new, diff, ratio) in sorted_diff_entries(diff):
  144. if ratio or args.get('all'):
  145. print("%-36s %7s %7s %+7d%s" % (name,
  146. old or "-",
  147. new or "-",
  148. diff,
  149. ' (%+.1f%%)' % (100*ratio) if ratio else ''))
  150. def print_totals():
  151. if not args.get('diff'):
  152. print("%-36s %7d" % ('TOTAL', total))
  153. else:
  154. ratio = (total-prev_total)/prev_total if prev_total else 1.0
  155. print("%-36s %7s %7s %+7d%s" % (
  156. 'TOTAL',
  157. prev_total if prev_total else '-',
  158. total if total else '-',
  159. total-prev_total,
  160. ' (%+.1f%%)' % (100*ratio) if ratio else ''))
  161. if args.get('quiet'):
  162. pass
  163. elif args.get('summary'):
  164. print_header()
  165. print_totals()
  166. elif args.get('files'):
  167. print_entries(by='file')
  168. print_totals()
  169. else:
  170. print_entries(by='function')
  171. print_totals()
  172. if __name__ == "__main__":
  173. import argparse
  174. import sys
  175. parser = argparse.ArgumentParser(
  176. description="Find code size at the function level.")
  177. parser.add_argument('obj_paths', nargs='*', default=OBJ_PATHS,
  178. help="Description of where to find *.o files. May be a directory \
  179. or a list of paths. Defaults to %r." % OBJ_PATHS)
  180. parser.add_argument('-v', '--verbose', action='store_true',
  181. help="Output commands that run behind the scenes.")
  182. parser.add_argument('-o', '--output',
  183. help="Specify CSV file to store results.")
  184. parser.add_argument('-u', '--use',
  185. help="Don't compile and find code sizes, instead use this CSV file.")
  186. parser.add_argument('-d', '--diff',
  187. help="Specify CSV file to diff code size against.")
  188. parser.add_argument('-a', '--all', action='store_true',
  189. help="Show all functions, not just the ones that changed.")
  190. parser.add_argument('-s', '--size-sort', action='store_true',
  191. help="Sort by size.")
  192. parser.add_argument('-S', '--reverse-size-sort', action='store_true',
  193. help="Sort by size, but backwards.")
  194. parser.add_argument('--files', action='store_true',
  195. help="Show file-level code sizes. Note this does not include padding! "
  196. "So sizes may differ from other tools.")
  197. parser.add_argument('--summary', action='store_true',
  198. help="Only show the total code size.")
  199. parser.add_argument('-q', '--quiet', action='store_true',
  200. help="Don't show anything, useful with -o.")
  201. parser.add_argument('--type', default='tTrRdD',
  202. help="Type of symbols to report, this uses the same single-character "
  203. "type-names emitted by nm. Defaults to %(default)r.")
  204. parser.add_argument('--nm-tool', default=['nm'], type=lambda x: x.split(),
  205. help="Path to the nm tool to use.")
  206. parser.add_argument('--build-dir',
  207. help="Specify the relative build directory. Used to map object files \
  208. to the correct source files.")
  209. sys.exit(main(**vars(parser.parse_args())))