mirror of
https://github.com/littlefs-project/littlefs.git
synced 2025-10-20 13:23:58 +08:00

This also adds coverage support to the new test framework, which due to reduction in scope, no longer needs aggregation and can be much simpler. Really all we need to do is pass --coverage to GCC, which builds its .gcda files during testing in a multi-process-safe manner. The addition of branch coverage leverages information that was available in both lcov and gcov. This was made easier with the addition of the --json-format to gcov in GCC 9.0, however the lax backwards compatibility for gcov's intermediary options is a bit concerning. Hopefully --json-format sticks around for a while.
440 lines
17 KiB
Python
Executable File
440 lines
17 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
#
|
|
# Script to find stack usage at the function level. Will detect recursion and
|
|
# report as infinite stack usage.
|
|
#
|
|
|
|
import os
|
|
import glob
|
|
import itertools as it
|
|
import re
|
|
import csv
|
|
import collections as co
|
|
import math as m
|
|
|
|
|
|
CI_PATHS = ['*.ci']
|
|
|
|
def openio(path, mode='r'):
|
|
if path == '-':
|
|
if 'r' in mode:
|
|
return os.fdopen(os.dup(sys.stdin.fileno()), 'r')
|
|
else:
|
|
return os.fdopen(os.dup(sys.stdout.fileno()), 'w')
|
|
else:
|
|
return open(path, mode)
|
|
|
|
def collect(paths, **args):
|
|
# parse the vcg format
|
|
k_pattern = re.compile('([a-z]+)\s*:', re.DOTALL)
|
|
v_pattern = re.compile('(?:"(.*?)"|([a-z]+))', re.DOTALL)
|
|
def parse_vcg(rest):
|
|
def parse_vcg(rest):
|
|
node = []
|
|
while True:
|
|
rest = rest.lstrip()
|
|
m = k_pattern.match(rest)
|
|
if not m:
|
|
return (node, rest)
|
|
k, rest = m.group(1), rest[m.end(0):]
|
|
|
|
rest = rest.lstrip()
|
|
if rest.startswith('{'):
|
|
v, rest = parse_vcg(rest[1:])
|
|
assert rest[0] == '}', "unexpected %r" % rest[0:1]
|
|
rest = rest[1:]
|
|
node.append((k, v))
|
|
else:
|
|
m = v_pattern.match(rest)
|
|
assert m, "unexpected %r" % rest[0:1]
|
|
v, rest = m.group(1) or m.group(2), rest[m.end(0):]
|
|
node.append((k, v))
|
|
|
|
node, rest = parse_vcg(rest)
|
|
assert rest == '', "unexpected %r" % rest[0:1]
|
|
return node
|
|
|
|
# collect into functions
|
|
results = co.defaultdict(lambda: (None, None, 0, set()))
|
|
f_pattern = re.compile(
|
|
r'([^\\]*)\\n([^:]*)[^\\]*\\n([0-9]+) bytes \((.*)\)')
|
|
for path in paths:
|
|
with open(path) as f:
|
|
vcg = parse_vcg(f.read())
|
|
for k, graph in vcg:
|
|
if k != 'graph':
|
|
continue
|
|
for k, info in graph:
|
|
if k == 'node':
|
|
info = dict(info)
|
|
m = f_pattern.match(info['label'])
|
|
if m:
|
|
function, file, size, type = m.groups()
|
|
if not args.get('quiet') and type != 'static':
|
|
print('warning: found non-static stack for %s (%s)'
|
|
% (function, type))
|
|
_, _, _, targets = results[info['title']]
|
|
results[info['title']] = (
|
|
file, function, int(size), targets)
|
|
elif k == 'edge':
|
|
info = dict(info)
|
|
_, _, _, targets = results[info['sourcename']]
|
|
targets.add(info['targetname'])
|
|
else:
|
|
continue
|
|
|
|
if not args.get('everything'):
|
|
for source, (s_file, s_function, _, _) in list(results.items()):
|
|
# discard internal functions
|
|
if s_file.startswith('<') or s_file.startswith('/usr/include'):
|
|
del results[source]
|
|
|
|
# find maximum stack size recursively, this requires also detecting cycles
|
|
# (in case of recursion)
|
|
def find_limit(source, seen=None):
|
|
seen = seen or set()
|
|
if source not in results:
|
|
return 0
|
|
_, _, frame, targets = results[source]
|
|
|
|
limit = 0
|
|
for target in targets:
|
|
if target in seen:
|
|
# found a cycle
|
|
return float('inf')
|
|
limit_ = find_limit(target, seen | {target})
|
|
limit = max(limit, limit_)
|
|
|
|
return frame + limit
|
|
|
|
def find_deps(targets):
|
|
deps = set()
|
|
for target in targets:
|
|
if target in results:
|
|
t_file, t_function, _, _ = results[target]
|
|
deps.add((t_file, t_function))
|
|
return deps
|
|
|
|
# flatten into a list
|
|
flat_results = []
|
|
for source, (s_file, s_function, frame, targets) in results.items():
|
|
limit = find_limit(source)
|
|
deps = find_deps(targets)
|
|
flat_results.append((s_file, s_function, frame, limit, deps))
|
|
|
|
return flat_results
|
|
|
|
def main(**args):
|
|
# find sizes
|
|
if not args.get('use', None):
|
|
# find .ci files
|
|
paths = []
|
|
for path in args['ci_paths']:
|
|
if os.path.isdir(path):
|
|
path = path + '/*.ci'
|
|
|
|
for path in glob.glob(path):
|
|
paths.append(path)
|
|
|
|
if not paths:
|
|
print('no .ci files found in %r?' % args['ci_paths'])
|
|
sys.exit(-1)
|
|
|
|
results = collect(paths, **args)
|
|
else:
|
|
with openio(args['use']) as f:
|
|
r = csv.DictReader(f)
|
|
results = [
|
|
( result['file'],
|
|
result['name'],
|
|
int(result['stack_frame']),
|
|
float(result['stack_limit']), # note limit can be inf
|
|
set())
|
|
for result in r
|
|
if result.get('stack_frame') not in {None, ''}
|
|
if result.get('stack_limit') not in {None, ''}]
|
|
|
|
total_frame = 0
|
|
total_limit = 0
|
|
for _, _, frame, limit, _ in results:
|
|
total_frame += frame
|
|
total_limit = max(total_limit, limit)
|
|
|
|
# find previous results?
|
|
if args.get('diff'):
|
|
try:
|
|
with openio(args['diff']) as f:
|
|
r = csv.DictReader(f)
|
|
prev_results = [
|
|
( result['file'],
|
|
result['name'],
|
|
int(result['stack_frame']),
|
|
float(result['stack_limit']),
|
|
set())
|
|
for result in r
|
|
if result.get('stack_frame') not in {None, ''}
|
|
if result.get('stack_limit') not in {None, ''}]
|
|
except FileNotFoundError:
|
|
prev_results = []
|
|
|
|
prev_total_frame = 0
|
|
prev_total_limit = 0
|
|
for _, _, frame, limit, _ in prev_results:
|
|
prev_total_frame += frame
|
|
prev_total_limit = max(prev_total_limit, limit)
|
|
|
|
# write results to CSV
|
|
if args.get('output'):
|
|
merged_results = co.defaultdict(lambda: {})
|
|
other_fields = []
|
|
|
|
# merge?
|
|
if args.get('merge'):
|
|
try:
|
|
with openio(args['merge']) as f:
|
|
r = csv.DictReader(f)
|
|
for result in r:
|
|
file = result.pop('file', '')
|
|
func = result.pop('name', '')
|
|
result.pop('stack_frame', None)
|
|
result.pop('stack_limit', None)
|
|
merged_results[(file, func)] = result
|
|
other_fields = result.keys()
|
|
except FileNotFoundError:
|
|
pass
|
|
|
|
for file, func, frame, limit, _ in results:
|
|
merged_results[(file, func)]['stack_frame'] = frame
|
|
merged_results[(file, func)]['stack_limit'] = limit
|
|
|
|
with openio(args['output'], 'w') as f:
|
|
w = csv.DictWriter(f, ['file', 'name', *other_fields, 'stack_frame', 'stack_limit'])
|
|
w.writeheader()
|
|
for (file, func), result in sorted(merged_results.items()):
|
|
w.writerow({'file': file, 'name': func, **result})
|
|
|
|
# print results
|
|
def dedup_entries(results, by='name'):
|
|
entries = co.defaultdict(lambda: (0, 0, set()))
|
|
for file, func, frame, limit, deps in results:
|
|
entry = (file if by == 'file' else func)
|
|
entry_frame, entry_limit, entry_deps = entries[entry]
|
|
entries[entry] = (
|
|
entry_frame + frame,
|
|
max(entry_limit, limit),
|
|
entry_deps | {file if by == 'file' else func
|
|
for file, func in deps})
|
|
return entries
|
|
|
|
def diff_entries(olds, news):
|
|
diff = co.defaultdict(lambda: (None, None, None, None, 0, 0, 0, set()))
|
|
for name, (new_frame, new_limit, deps) in news.items():
|
|
diff[name] = (
|
|
None, None,
|
|
new_frame, new_limit,
|
|
new_frame, new_limit,
|
|
1.0,
|
|
deps)
|
|
for name, (old_frame, old_limit, _) in olds.items():
|
|
_, _, new_frame, new_limit, _, _, _, deps = diff[name]
|
|
diff[name] = (
|
|
old_frame, old_limit,
|
|
new_frame, new_limit,
|
|
(new_frame or 0) - (old_frame or 0),
|
|
0 if m.isinf(new_limit or 0) and m.isinf(old_limit or 0)
|
|
else (new_limit or 0) - (old_limit or 0),
|
|
0.0 if m.isinf(new_limit or 0) and m.isinf(old_limit or 0)
|
|
else +float('inf') if m.isinf(new_limit or 0)
|
|
else -float('inf') if m.isinf(old_limit or 0)
|
|
else +0.0 if not old_limit and not new_limit
|
|
else +1.0 if not old_limit
|
|
else ((new_limit or 0) - (old_limit or 0))/(old_limit or 0),
|
|
deps)
|
|
return diff
|
|
|
|
def sorted_entries(entries):
|
|
if args.get('limit_sort'):
|
|
return sorted(entries, key=lambda x: (-x[1][1], x))
|
|
elif args.get('reverse_limit_sort'):
|
|
return sorted(entries, key=lambda x: (+x[1][1], x))
|
|
elif args.get('frame_sort'):
|
|
return sorted(entries, key=lambda x: (-x[1][0], x))
|
|
elif args.get('reverse_frame_sort'):
|
|
return sorted(entries, key=lambda x: (+x[1][0], x))
|
|
else:
|
|
return sorted(entries)
|
|
|
|
def sorted_diff_entries(entries):
|
|
if args.get('limit_sort'):
|
|
return sorted(entries, key=lambda x: (-(x[1][3] or 0), x))
|
|
elif args.get('reverse_limit_sort'):
|
|
return sorted(entries, key=lambda x: (+(x[1][3] or 0), x))
|
|
elif args.get('frame_sort'):
|
|
return sorted(entries, key=lambda x: (-(x[1][2] or 0), x))
|
|
elif args.get('reverse_frame_sort'):
|
|
return sorted(entries, key=lambda x: (+(x[1][2] or 0), x))
|
|
else:
|
|
return sorted(entries, key=lambda x: (-x[1][6], x))
|
|
|
|
def print_header(by=''):
|
|
if not args.get('diff'):
|
|
print('%-36s %7s %7s' % (by, 'frame', 'limit'))
|
|
else:
|
|
print('%-36s %15s %15s %15s' % (by, 'old', 'new', 'diff'))
|
|
|
|
def print_entry(name, frame, limit):
|
|
print("%-36s %7d %7s" % (name,
|
|
frame, '∞' if m.isinf(limit) else int(limit)))
|
|
|
|
def print_diff_entry(name,
|
|
old_frame, old_limit,
|
|
new_frame, new_limit,
|
|
diff_frame, diff_limit,
|
|
ratio):
|
|
print('%-36s %7s %7s %7s %7s %+7d %7s%s' % (name,
|
|
old_frame if old_frame is not None else "-",
|
|
('∞' if m.isinf(old_limit) else int(old_limit))
|
|
if old_limit is not None else "-",
|
|
new_frame if new_frame is not None else "-",
|
|
('∞' if m.isinf(new_limit) else int(new_limit))
|
|
if new_limit is not None else "-",
|
|
diff_frame,
|
|
('+∞' if diff_limit > 0 and m.isinf(diff_limit)
|
|
else '-∞' if diff_limit < 0 and m.isinf(diff_limit)
|
|
else '%+d' % diff_limit),
|
|
'' if not ratio
|
|
else ' (+∞%)' if ratio > 0 and m.isinf(ratio)
|
|
else ' (-∞%)' if ratio < 0 and m.isinf(ratio)
|
|
else ' (%+.1f%%)' % (100*ratio)))
|
|
|
|
def print_entries(by='name'):
|
|
# build optional tree of dependencies
|
|
def print_deps(entries, depth, print,
|
|
filter=lambda _: True,
|
|
prefixes=('', '', '', '')):
|
|
entries = entries if isinstance(entries, list) else list(entries)
|
|
filtered_entries = [(name, entry)
|
|
for name, entry in entries
|
|
if filter(name)]
|
|
for i, (name, entry) in enumerate(filtered_entries):
|
|
last = (i == len(filtered_entries)-1)
|
|
print(prefixes[0+last] + name, entry)
|
|
|
|
if depth > 0:
|
|
deps = entry[-1]
|
|
print_deps(entries, depth-1, print,
|
|
lambda name: name in deps,
|
|
( prefixes[2+last] + "|-> ",
|
|
prefixes[2+last] + "'-> ",
|
|
prefixes[2+last] + "| ",
|
|
prefixes[2+last] + " "))
|
|
|
|
entries = dedup_entries(results, by=by)
|
|
|
|
if not args.get('diff'):
|
|
print_header(by=by)
|
|
print_deps(
|
|
sorted_entries(entries.items()),
|
|
args.get('depth') or 0,
|
|
lambda name, entry: print_entry(name, *entry[:-1]))
|
|
else:
|
|
prev_entries = dedup_entries(prev_results, by=by)
|
|
diff = diff_entries(prev_entries, entries)
|
|
|
|
print_header(by='%s (%d added, %d removed)' % (by,
|
|
sum(1 for _, old, _, _, _, _, _, _ in diff.values() if old is None),
|
|
sum(1 for _, _, _, new, _, _, _, _ in diff.values() if new is None)))
|
|
print_deps(
|
|
filter(
|
|
lambda x: x[1][6] or args.get('all'),
|
|
sorted_diff_entries(diff.items())),
|
|
args.get('depth') or 0,
|
|
lambda name, entry: print_diff_entry(name, *entry[:-1]))
|
|
|
|
def print_totals():
|
|
if not args.get('diff'):
|
|
print_entry('TOTAL', total_frame, total_limit)
|
|
else:
|
|
diff_frame = total_frame - prev_total_frame
|
|
diff_limit = (
|
|
0 if m.isinf(total_limit or 0) and m.isinf(prev_total_limit or 0)
|
|
else (total_limit or 0) - (prev_total_limit or 0))
|
|
ratio = (
|
|
0.0 if m.isinf(total_limit or 0) and m.isinf(prev_total_limit or 0)
|
|
else +float('inf') if m.isinf(total_limit or 0)
|
|
else -float('inf') if m.isinf(prev_total_limit or 0)
|
|
else 0.0 if not prev_total_limit and not total_limit
|
|
else 1.0 if not prev_total_limit
|
|
else ((total_limit or 0) - (prev_total_limit or 0))/(prev_total_limit or 0))
|
|
print_diff_entry('TOTAL',
|
|
prev_total_frame, prev_total_limit,
|
|
total_frame, total_limit,
|
|
diff_frame, diff_limit,
|
|
ratio)
|
|
|
|
if args.get('quiet'):
|
|
pass
|
|
elif args.get('summary'):
|
|
print_header()
|
|
print_totals()
|
|
elif args.get('files'):
|
|
print_entries(by='file')
|
|
print_totals()
|
|
else:
|
|
print_entries(by='name')
|
|
print_totals()
|
|
|
|
# catch recursion
|
|
if args.get('error_on_recursion') and any(
|
|
m.isinf(limit) for _, _, _, limit, _ in results):
|
|
sys.exit(2)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
import argparse
|
|
import sys
|
|
parser = argparse.ArgumentParser(
|
|
description="Find stack usage at the function level.")
|
|
parser.add_argument('ci_paths', nargs='*', default=CI_PATHS,
|
|
help="Description of where to find *.ci files. May be a directory \
|
|
or a list of paths. Defaults to %r." % CI_PATHS)
|
|
parser.add_argument('-v', '--verbose', action='store_true',
|
|
help="Output commands that run behind the scenes.")
|
|
parser.add_argument('-q', '--quiet', action='store_true',
|
|
help="Don't show anything, useful with -o.")
|
|
parser.add_argument('-o', '--output',
|
|
help="Specify CSV file to store results.")
|
|
parser.add_argument('-u', '--use',
|
|
help="Don't parse callgraph files, instead use this CSV file.")
|
|
parser.add_argument('-d', '--diff',
|
|
help="Specify CSV file to diff against.")
|
|
parser.add_argument('-m', '--merge',
|
|
help="Merge with an existing CSV file when writing to output.")
|
|
parser.add_argument('-a', '--all', action='store_true',
|
|
help="Show all functions, not just the ones that changed.")
|
|
parser.add_argument('-A', '--everything', action='store_true',
|
|
help="Include builtin and libc specific symbols.")
|
|
parser.add_argument('--frame-sort', action='store_true',
|
|
help="Sort by stack frame size.")
|
|
parser.add_argument('--reverse-frame-sort', action='store_true',
|
|
help="Sort by stack frame size, but backwards.")
|
|
parser.add_argument('-s', '--limit-sort', action='store_true',
|
|
help="Sort by stack limit.")
|
|
parser.add_argument('-S', '--reverse-limit-sort', action='store_true',
|
|
help="Sort by stack limit, but backwards.")
|
|
parser.add_argument('-L', '--depth', default=0, type=lambda x: int(x, 0),
|
|
nargs='?', const=float('inf'),
|
|
help="Depth of dependencies to show.")
|
|
parser.add_argument('-F', '--files', action='store_true',
|
|
help="Show file-level calls.")
|
|
parser.add_argument('-Y', '--summary', action='store_true',
|
|
help="Only show the total stack size.")
|
|
parser.add_argument('-e', '--error-on-recursion', action='store_true',
|
|
help="Error if any functions are recursive.")
|
|
parser.add_argument('--build-dir',
|
|
help="Specify the relative build directory. Used to map object files \
|
|
to the correct source files.")
|
|
sys.exit(main(**{k: v
|
|
for k, v in vars(parser.parse_args()).items()
|
|
if v is not None}))
|