-
Notifications
You must be signed in to change notification settings - Fork 794
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Added scripts/code_size.py, for more in-depth code-size reporting
Inspired by Linux's Bloat-O-Meter, code_size.py wraps nm to provide function-level code size, and supports detailed comparison between different builds. One difference is that code_size.py invokes littlefs's build system similarly to test.py, creating a duplicate build in the "sizes" directory. This makes it easy to monitor a cross-compiled build size while simultaneously testing on the host machine.
- Loading branch information
Showing
2 changed files
with
334 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,328 @@ | ||
#!/usr/bin/env python3 | ||
# | ||
# This script finds the code size at the function level, with/without | ||
# static functions, and has some conveniences for comparing different | ||
# versions. It's basically one big wrapper around nm, and may or may | ||
# not have been written out of jealousy of Linux's Bloat-O-Meter. | ||
# | ||
# Here's a useful bash script to use while developing: | ||
# ./scripts/code_size.py -qo old.csv | ||
# while true ; do ./code_scripts/size.py -d old.csv ; inotifywait -rqe modify * ; done | ||
# | ||
# Or even better, to automatically update results on commit: | ||
# ./scripts/code_size.py -qo commit.csv | ||
# while true ; do ./scripts/code_size.py -d commit.csv -o current.csv ; git diff --exit-code --quiet && cp current.csv commit.csv ; inotifywait -rqe modify * ; done | ||
# | ||
# Or my personal favorite: | ||
# ./scripts/code_size.py -qo master.csv && cp master.csv commit.csv | ||
# while true ; do ( ./scripts/code_size.py -i commit.csv -d master.csv -s ; ./scripts/code_size.py -i current.csv -d master.csv -s ; ./scripts/code_size.py -d master.csv -o current.csv -s ) | awk 'BEGIN {printf "%-16s %7s %7s %7s\n","","old","new","diff"} (NR==2 && $1="commit") || (NR==4 && $1="prev") || (NR==6 && $1="current") {printf "%-16s %7s %7s %7s %s\n",$1,$2,$3,$5,$6}' ; git diff --exit-code --quiet && cp current.csv commit.csv ; inotifywait -rqe modify * ; done | ||
# | ||
|
||
import os | ||
import itertools as it | ||
import subprocess as sp | ||
import shlex | ||
import re | ||
import csv | ||
import collections as co | ||
|
||
SIZEDIR = 'sizes' | ||
RULES = """ | ||
define FLATTEN | ||
%(sizedir)s/%(build)s.$(subst /,.,$(target)): $(target) | ||
( echo "#line 1 \\"$$<\\"" ; %(cat)s $$< ) > $$@ | ||
%(sizedir)s/%(build)s.$(subst /,.,$(target:.c=.size)): \\ | ||
%(sizedir)s/%(build)s.$(subst /,.,$(target:.c=.o)) | ||
$(NM) --size-sort $$^ | sed 's/^/$(subst /,\\/,$(target:.c=.o)):/' > $$@ | ||
endef | ||
$(foreach target,$(SRC),$(eval $(FLATTEN))) | ||
-include %(sizedir)s/*.d | ||
.SECONDARY: | ||
%%.size: $(foreach t,$(subst /,.,$(SRC:.c=.size)),%%.$t) | ||
cat $^ > $@ | ||
""" | ||
CATS = { | ||
'code': 'cat', | ||
'code_inlined': 'sed \'s/^static\( inline\)\?//\'', | ||
} | ||
|
||
def build(**args): | ||
# mkdir -p sizedir | ||
os.makedirs(args['sizedir'], exist_ok=True) | ||
|
||
if args.get('inlined', False): | ||
builds = ['code', 'code_inlined'] | ||
else: | ||
builds = ['code'] | ||
|
||
# write makefiles for the different types of builds | ||
makefiles = [] | ||
targets = [] | ||
for build in builds: | ||
path = args['sizedir'] + '/' + build | ||
with open(path + '.mk', 'w') as mk: | ||
mk.write(RULES.replace(4*' ', '\t') % dict( | ||
sizedir=args['sizedir'], | ||
build=build, | ||
cat=CATS[build])) | ||
mk.write('\n') | ||
|
||
# pass on defines | ||
for d in args['D']: | ||
mk.write('%s: override CFLAGS += -D%s\n' % ( | ||
path+'.size', d)) | ||
|
||
makefiles.append(path + '.mk') | ||
targets.append(path + '.size') | ||
|
||
# build in parallel | ||
cmd = (['make', '-f', 'Makefile'] + | ||
list(it.chain.from_iterable(['-f', m] for m in makefiles)) + | ||
[target for target in targets]) | ||
if args.get('verbose', False): | ||
print(' '.join(shlex.quote(c) for c in cmd)) | ||
proc = sp.Popen(cmd, | ||
stdout=sp.DEVNULL if not args.get('verbose', False) else None) | ||
proc.wait() | ||
if proc.returncode != 0: | ||
sys.exit(-1) | ||
|
||
# find results | ||
build_results = co.defaultdict(lambda: 0) | ||
# notes | ||
# - filters type | ||
# - discards internal/debug functions (leading __) | ||
pattern = re.compile( | ||
'^(?P<file>[^:]+)' + | ||
':(?P<size>[0-9a-fA-F]+)' + | ||
' (?P<type>[%s])' % re.escape(args['type']) + | ||
' (?!__)(?P<name>.+?)$') | ||
for build in builds: | ||
path = args['sizedir'] + '/' + build | ||
with open(path + '.size') as size: | ||
for line in size: | ||
match = pattern.match(line) | ||
if match: | ||
file = match.group('file') | ||
# discard .8449 suffixes created by optimizer | ||
name = re.sub('\.[0-9]+', '', match.group('name')) | ||
size = int(match.group('size'), 16) | ||
build_results[(build, file, name)] += size | ||
|
||
results = [] | ||
for (build, file, name), size in build_results.items(): | ||
if build == 'code': | ||
results.append((file, name, size, False)) | ||
elif (build == 'code_inlined' and | ||
('inlined', file, name) not in results): | ||
results.append((file, name, size, True)) | ||
|
||
return results | ||
|
||
def main(**args): | ||
# find results | ||
if not args.get('input', None): | ||
results = build(**args) | ||
else: | ||
with open(args['input']) as f: | ||
r = csv.DictReader(f) | ||
results = [ | ||
( result['file'], | ||
result['name'], | ||
int(result['size']), | ||
bool(int(result.get('inlined', 0)))) | ||
for result in r | ||
if (not bool(int(result.get('inlined', 0))) or | ||
args.get('inlined', False))] | ||
|
||
total = 0 | ||
for _, _, size, inlined in results: | ||
if not inlined: | ||
total += size | ||
|
||
# find previous results? | ||
if args.get('diff', None): | ||
with open(args['diff']) as f: | ||
r = csv.DictReader(f) | ||
prev_results = [ | ||
( result['file'], | ||
result['name'], | ||
int(result['size']), | ||
bool(int(result.get('inlined', 0)))) | ||
for result in r | ||
if (not bool(int(result.get('inlined', 0))) or | ||
args.get('inlined', False))] | ||
|
||
prev_total = 0 | ||
for _, _, size, inlined in prev_results: | ||
if not inlined: | ||
prev_total += size | ||
|
||
# write results to CSV | ||
if args.get('output', None): | ||
results.sort(key=lambda x: (-x[2], x)) | ||
with open(args['output'], 'w') as f: | ||
w = csv.writer(f) | ||
if args.get('inlined', False): | ||
w.writerow(['file', 'name', 'size', 'inlined']) | ||
for file, name, size, inlined in results: | ||
w.writerow((file, name, size, int(inlined))) | ||
else: | ||
w.writerow(['file', 'name', 'size']) | ||
for file, name, size, inlined in results: | ||
w.writerow((file, name, size)) | ||
|
||
# print results | ||
def dedup_functions(results): | ||
functions = co.defaultdict(lambda: (0, True)) | ||
for _, name, size, inlined in results: | ||
if not inlined: | ||
functions[name] = (functions[name][0] + size, False) | ||
for _, name, size, inlined in results: | ||
if inlined and functions[name][1]: | ||
functions[name] = (functions[name][0] + size, True) | ||
return functions | ||
|
||
def dedup_files(results): | ||
files = co.defaultdict(lambda: 0) | ||
for file, _, size, inlined in results: | ||
if not inlined: | ||
files[file] += size | ||
return files | ||
|
||
def diff_sizes(olds, news): | ||
diff = co.defaultdict(lambda: (None, None, None)) | ||
for name, new in news.items(): | ||
diff[name] = (None, new, new) | ||
for name, old in olds.items(): | ||
new = diff[name][1] or 0 | ||
diff[name] = (old, new, new-old) | ||
return diff | ||
|
||
def print_header(name=''): | ||
if not args.get('diff', False): | ||
print('%-40s %7s' % (name, 'size')) | ||
else: | ||
print('%-40s %7s %7s %7s' % (name, 'old', 'new', 'diff')) | ||
|
||
def print_functions(): | ||
functions = dedup_functions(results) | ||
functions = { | ||
name+' (inlined)' if inlined else name: size | ||
for name, (size, inlined) in functions.items()} | ||
|
||
if not args.get('diff', None): | ||
print_header('function') | ||
for name, size in sorted(functions.items(), | ||
key=lambda x: (-x[1], x)): | ||
print("%-40s %7d" % (name, size)) | ||
else: | ||
prev_functions = dedup_functions(prev_results) | ||
prev_functions = { | ||
name+' (inlined)' if inlined else name: size | ||
for name, (size, inlined) in prev_functions.items()} | ||
diff = diff_sizes(functions, prev_functions) | ||
print_header('function (%d added, %d removed)' % ( | ||
sum(1 for old, _, _ in diff.values() if not old), | ||
sum(1 for _, new, _ in diff.values() if not new))) | ||
for name, (old, new, diff) in sorted(diff.items(), | ||
key=lambda x: (-(x[1][2] or 0), x)): | ||
if diff or args.get('all', False): | ||
print("%-40s %7s %7s %+7d%s" % ( | ||
name, old or "-", new or "-", diff, | ||
' (%+.2f%%)' % (100*((new-old)/old)) | ||
if old and new else | ||
'')) | ||
|
||
def print_files(): | ||
files = dedup_files(results) | ||
|
||
if not args.get('diff', None): | ||
print_header('file') | ||
for file, size in sorted(files.items(), | ||
key=lambda x: (-x[1], x)): | ||
print("%-40s %7d" % (file, size)) | ||
else: | ||
prev_files = dedup_files(prev_results) | ||
diff = diff_sizes(files, prev_files) | ||
print_header('file (%d added, %d removed)' % ( | ||
sum(1 for old, _, _ in diff.values() if not old), | ||
sum(1 for _, new, _ in diff.values() if not new))) | ||
for name, (old, new, diff) in sorted(diff.items(), | ||
key=lambda x: (-(x[1][2] or 0), x)): | ||
if diff or args.get('all', False): | ||
print("%-40s %7s %7s %+7d%s" % ( | ||
name, old or "-", new or "-", diff, | ||
' (%+.2f%%)' % (100*((new-old)/old)) | ||
if old and new else | ||
'')) | ||
|
||
def print_totals(): | ||
if not args.get('diff', None): | ||
print("%-40s %7d" % ('TOTALS', total)) | ||
else: | ||
print("%-40s %7s %7s %+7d%s" % ( | ||
'TOTALS', prev_total, total, total-prev_total, | ||
' (%+.2f%%)' % (100*((total-prev_total)/total)) | ||
if prev_total and total else | ||
'')) | ||
|
||
def print_status(): | ||
if not args.get('diff', None): | ||
print(total) | ||
else: | ||
print("%d (%+.2f%%)" % (total, 100*((total-prev_total)/total))) | ||
|
||
if args.get('quiet', False): | ||
pass | ||
elif args.get('status', False): | ||
print_status() | ||
elif args.get('summary', False): | ||
print_header() | ||
print_totals() | ||
elif args.get('files', False): | ||
print_files() | ||
print_totals() | ||
else: | ||
print_functions() | ||
print_totals() | ||
|
||
if __name__ == "__main__": | ||
import argparse | ||
import sys | ||
parser = argparse.ArgumentParser( | ||
description="Find code size at the function level.") | ||
parser.add_argument('sizedir', nargs='?', default=SIZEDIR, | ||
help="Directory to store intermediary results. Defaults " | ||
"to \"%s\"." % SIZEDIR) | ||
parser.add_argument('-D', action='append', default=[], | ||
help="Specify compile-time define.") | ||
parser.add_argument('-v', '--verbose', action='store_true', | ||
help="Output commands that run behind the scenes.") | ||
parser.add_argument('-i', '--input', | ||
help="Don't compile and find code sizes, instead use this CSV file.") | ||
parser.add_argument('-o', '--output', | ||
help="Specify CSV file to store results.") | ||
parser.add_argument('-d', '--diff', | ||
help="Specify CSV file to diff code size against.") | ||
parser.add_argument('-a', '--all', action='store_true', | ||
help="Show all functions, not just the ones that changed.") | ||
parser.add_argument('--inlined', action='store_true', | ||
help="Run a second compilation to find the sizes of functions normally " | ||
"removed by optimizations. These will be shown as \"*.inlined\" " | ||
"functions, and will not be included in the total.") | ||
parser.add_argument('--files', action='store_true', | ||
help="Show file-level code sizes. Note this does not include padding! " | ||
"So sizes may differ from other tools.") | ||
parser.add_argument('-s', '--summary', action='store_true', | ||
help="Only show the total code size.") | ||
parser.add_argument('-S', '--status', action='store_true', | ||
help="Show minimum info useful for a single-line status.") | ||
parser.add_argument('-q', '--quiet', action='store_true', | ||
help="Don't show anything, useful with -o.") | ||
parser.add_argument('--type', default='tTrRdDbB', | ||
help="Type of symbols to report, this uses the same single-character " | ||
"type-names emitted by nm. Defaults to %(default)r.") | ||
sys.exit(main(**vars(parser.parse_args()))) |