mirror of
				https://github.com/eledio-devices/thirdparty-littlefs.git
				synced 2025-10-31 16:14:16 +01:00 
			
		
		
		
	Mostly taken from .travis.yml, biggest changes were around how to get the status updates to work. We can't use a token on PRs the same way we could in Travis, so instead we use a second workflow that checks every pull request for "status" artifacts, and create the actual statuses in the "workflow_run" event, where we have full access to repo secrets.
		
			
				
	
	
		
			329 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Python
		
	
	
		
			Executable File
		
	
	
	
	
			
		
		
	
	
			329 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Python
		
	
	
		
			Executable File
		
	
	
	
	
| #!/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 /,.,$(OBJ:.o=.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())))
 |