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