• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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