structs.py 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250
  1. #!/usr/bin/env python3
  2. #
  3. # Script to find struct sizes.
  4. #
  5. import os
  6. import glob
  7. import itertools as it
  8. import subprocess as sp
  9. import shlex
  10. import re
  11. import csv
  12. import collections as co
  13. OBJ_PATHS = ['*.o']
  14. def collect(paths, **args):
  15. results = co.defaultdict(lambda: 0)
  16. pattern = re.compile(
  17. '^(?:.*DW_TAG_(?P<tag>[a-z_]+).*'
  18. '|^.*DW_AT_name.*:\s*(?P<name>[^:\s]+)\s*'
  19. '|^.*DW_AT_byte_size.*:\s*(?P<size>[0-9]+)\s*)$')
  20. for path in paths:
  21. # collect structs as we parse dwarf info
  22. found = False
  23. name = None
  24. size = None
  25. # note objdump-tool may contain extra args
  26. cmd = args['objdump_tool'] + ['--dwarf=info', path]
  27. if args.get('verbose'):
  28. print(' '.join(shlex.quote(c) for c in cmd))
  29. proc = sp.Popen(cmd,
  30. stdout=sp.PIPE,
  31. stderr=sp.PIPE if not args.get('verbose') else None,
  32. universal_newlines=True)
  33. for line in proc.stdout:
  34. # state machine here to find structs
  35. m = pattern.match(line)
  36. if m:
  37. if m.group('tag'):
  38. if name is not None and size is not None:
  39. results[(path, name)] = size
  40. found = (m.group('tag') == 'structure_type')
  41. name = None
  42. size = None
  43. elif found and m.group('name'):
  44. name = m.group('name')
  45. elif found and name and m.group('size'):
  46. size = int(m.group('size'))
  47. proc.wait()
  48. if proc.returncode != 0:
  49. if not args.get('verbose'):
  50. for line in proc.stderr:
  51. sys.stdout.write(line)
  52. sys.exit(-1)
  53. flat_results = []
  54. for (file, struct), size in results.items():
  55. # map to source files
  56. if args.get('build_dir'):
  57. file = re.sub('%s/*' % re.escape(args['build_dir']), '', file)
  58. flat_results.append((file, struct, size))
  59. return flat_results
  60. def main(**args):
  61. # find sizes
  62. if not args.get('use', None):
  63. # find .o files
  64. paths = []
  65. for path in args['obj_paths']:
  66. if os.path.isdir(path):
  67. path = path + '/*.o'
  68. for path in glob.glob(path):
  69. paths.append(path)
  70. if not paths:
  71. print('no .obj files found in %r?' % args['obj_paths'])
  72. sys.exit(-1)
  73. results = collect(paths, **args)
  74. else:
  75. with open(args['use']) as f:
  76. r = csv.DictReader(f)
  77. results = [
  78. ( result['file'],
  79. result['struct'],
  80. int(result['struct_size']))
  81. for result in r]
  82. total = 0
  83. for _, _, size in results:
  84. total += size
  85. # find previous results?
  86. if args.get('diff'):
  87. try:
  88. with open(args['diff']) as f:
  89. r = csv.DictReader(f)
  90. prev_results = [
  91. ( result['file'],
  92. result['struct'],
  93. int(result['struct_size']))
  94. for result in r]
  95. except FileNotFoundError:
  96. prev_results = []
  97. prev_total = 0
  98. for _, _, size in prev_results:
  99. prev_total += size
  100. # write results to CSV
  101. if args.get('output'):
  102. with open(args['output'], 'w') as f:
  103. w = csv.writer(f)
  104. w.writerow(['file', 'struct', 'struct_size'])
  105. for file, struct, size in sorted(results):
  106. w.writerow((file, struct, size))
  107. # print results
  108. def dedup_entries(results, by='struct'):
  109. entries = co.defaultdict(lambda: 0)
  110. for file, struct, size in results:
  111. entry = (file if by == 'file' else struct)
  112. entries[entry] += size
  113. return entries
  114. def diff_entries(olds, news):
  115. diff = co.defaultdict(lambda: (0, 0, 0, 0))
  116. for name, new in news.items():
  117. diff[name] = (0, new, new, 1.0)
  118. for name, old in olds.items():
  119. _, new, _, _ = diff[name]
  120. diff[name] = (old, new, new-old, (new-old)/old if old else 1.0)
  121. return diff
  122. def sorted_entries(entries):
  123. if args.get('size_sort'):
  124. return sorted(entries, key=lambda x: (-x[1], x))
  125. elif args.get('reverse_size_sort'):
  126. return sorted(entries, key=lambda x: (+x[1], x))
  127. else:
  128. return sorted(entries)
  129. def sorted_diff_entries(entries):
  130. if args.get('size_sort'):
  131. return sorted(entries, key=lambda x: (-x[1][1], x))
  132. elif args.get('reverse_size_sort'):
  133. return sorted(entries, key=lambda x: (+x[1][1], x))
  134. else:
  135. return sorted(entries, key=lambda x: (-x[1][3], x))
  136. def print_header(by=''):
  137. if not args.get('diff'):
  138. print('%-36s %7s' % (by, 'size'))
  139. else:
  140. print('%-36s %7s %7s %7s' % (by, 'old', 'new', 'diff'))
  141. def print_entry(name, size):
  142. print("%-36s %7d" % (name, size))
  143. def print_diff_entry(name, old, new, diff, ratio):
  144. print("%-36s %7s %7s %+7d%s" % (name,
  145. old or "-",
  146. new or "-",
  147. diff,
  148. ' (%+.1f%%)' % (100*ratio) if ratio else ''))
  149. def print_entries(by='struct'):
  150. entries = dedup_entries(results, by=by)
  151. if not args.get('diff'):
  152. print_header(by=by)
  153. for name, size in sorted_entries(entries.items()):
  154. print_entry(name, size)
  155. else:
  156. prev_entries = dedup_entries(prev_results, by=by)
  157. diff = diff_entries(prev_entries, entries)
  158. print_header(by='%s (%d added, %d removed)' % (by,
  159. sum(1 for old, _, _, _ in diff.values() if not old),
  160. sum(1 for _, new, _, _ in diff.values() if not new)))
  161. for name, (old, new, diff, ratio) in sorted_diff_entries(
  162. diff.items()):
  163. if ratio or args.get('all'):
  164. print_diff_entry(name, old, new, diff, ratio)
  165. def print_totals():
  166. if not args.get('diff'):
  167. print_entry('TOTAL', total)
  168. else:
  169. ratio = (0.0 if not prev_total and not total
  170. else 1.0 if not prev_total
  171. else (total-prev_total)/prev_total)
  172. print_diff_entry('TOTAL',
  173. prev_total, total,
  174. total-prev_total,
  175. ratio)
  176. if args.get('quiet'):
  177. pass
  178. elif args.get('summary'):
  179. print_header()
  180. print_totals()
  181. elif args.get('files'):
  182. print_entries(by='file')
  183. print_totals()
  184. else:
  185. print_entries(by='struct')
  186. print_totals()
  187. if __name__ == "__main__":
  188. import argparse
  189. import sys
  190. parser = argparse.ArgumentParser(
  191. description="Find code size at the function level.")
  192. parser.add_argument('obj_paths', nargs='*', default=OBJ_PATHS,
  193. help="Description of where to find *.o files. May be a directory \
  194. or a list of paths. Defaults to %r." % OBJ_PATHS)
  195. parser.add_argument('-v', '--verbose', action='store_true',
  196. help="Output commands that run behind the scenes.")
  197. parser.add_argument('-o', '--output',
  198. help="Specify CSV file to store results.")
  199. parser.add_argument('-u', '--use',
  200. help="Don't compile and find struct sizes, instead use this CSV file.")
  201. parser.add_argument('-d', '--diff',
  202. help="Specify CSV file to diff struct size against.")
  203. parser.add_argument('-a', '--all', action='store_true',
  204. help="Show all functions, not just the ones that changed.")
  205. parser.add_argument('-A', '--everything', action='store_true',
  206. help="Include builtin and libc specific symbols.")
  207. parser.add_argument('-s', '--size-sort', action='store_true',
  208. help="Sort by size.")
  209. parser.add_argument('-S', '--reverse-size-sort', action='store_true',
  210. help="Sort by size, but backwards.")
  211. parser.add_argument('--files', action='store_true',
  212. help="Show file-level struct sizes.")
  213. parser.add_argument('--summary', action='store_true',
  214. help="Only show the total struct size.")
  215. parser.add_argument('-q', '--quiet', action='store_true',
  216. help="Don't show anything, useful with -o.")
  217. parser.add_argument('--objdump-tool', default=['objdump'], type=lambda x: x.split(),
  218. help="Path to the objdump tool to use.")
  219. parser.add_argument('--build-dir',
  220. help="Specify the relative build directory. Used to map object files \
  221. to the correct source files.")
  222. sys.exit(main(**vars(parser.parse_args())))