mirror of
https://github.com/libretro/RetroArch.git
synced 2026-11-10 17:27:30 -10:00
rthreads.c is the only user of the GEKKO and 3DS pthread wrappers, so they now live inline in its platform include section rather than as separate headers. psp_pthread.h is referenced by nothing - PSP threading comes from the toolchain's pthreads - and is removed outright. Comments across the tree that point at thread stack sizes now name rthreads.c.
160 lines
5.6 KiB
Python
160 lines
5.6 KiB
Python
#!/usr/bin/env python3
|
|
"""Worst-case cumulative stack per call chain, from a set of roots.
|
|
|
|
tools/stack_budget.py checks one frame at a time, which is necessary
|
|
and not sufficient: a frame does not run alone, it runs on top of
|
|
everything that called it. config_file_write() was caught by that
|
|
check only because the offending 16 KiB was in its own frame. Ten
|
|
1.5 KiB frames in a row pass every per-frame check ever written and
|
|
still overflow an 8 KiB thread stack.
|
|
|
|
This walks the call graph GCC emits with -fcallgraph-info=su and sums
|
|
frames along it, reporting the deepest chain from each root. Task
|
|
handlers are the roots that matter: retro_task_regular_gather() runs
|
|
them on the main thread, but a threaded task queue runs them on a
|
|
worker, and on GEKKO that worker has 8 KiB (STACKSIZE in
|
|
rthreads/rthreads.c).
|
|
|
|
WHAT THIS IS NOT
|
|
|
|
A verdict. Three things keep it a screening tool rather than a proof:
|
|
|
|
- The deepest path may be infeasible. Nothing here knows that two
|
|
branches are mutually exclusive, so the sum can describe a chain
|
|
that cannot occur.
|
|
- Indirect calls are invisible to the call graph, so a real chain
|
|
can be deeper than anything reported here. The number is not an
|
|
upper bound either.
|
|
- It measures what the *given* build configuration compiles. Run
|
|
with a host config.h and it will sum codecs the target never
|
|
builds - the same roots measure 73 KiB with everything enabled
|
|
and 20 KiB with a console-shaped feature set, and neither figure
|
|
describes a platform nobody built.
|
|
|
|
So it is deliberately not wired into CI: it would report numbers that
|
|
need a real target configuration to mean anything, and a check that
|
|
cries wolf gets an allowlist and then gets ignored. Point it at a
|
|
tree configured for the target you care about, and read the chains
|
|
rather than the totals.
|
|
|
|
USAGE
|
|
|
|
# emit call graphs for the translation units of interest
|
|
mkdir ci && cd ci
|
|
gcc -O2 -DGEKKO -fcallgraph-info=su -c <sources> -I... -include config.h
|
|
|
|
# roots: one symbol per line
|
|
tools/stack_chain.py ci roots.txt
|
|
|
|
A roots file can be built from the task handlers with:
|
|
|
|
grep -rhoE '(\\.|->)handler[[:space:]]*=[[:space:]]*[a-zA-Z_][a-zA-Z0-9_]*' \\
|
|
tasks/*.c *.c menu/*.c | sed 's/.*=[[:space:]]*//' | sort -u
|
|
"""
|
|
import collections
|
|
import glob
|
|
import os
|
|
import re
|
|
import sys
|
|
|
|
NODE = re.compile(r'node:\s*\{\s*title:\s*"([^"]+)"\s*label:\s*"([^"]*?)"', re.S)
|
|
EDGE = re.compile(r'edge:\s*\{\s*sourcename:\s*"([^"]+)"\s*targetname:\s*"([^"]+)"')
|
|
SIZE = re.compile(r'(\d+)\s+bytes')
|
|
# GCC suffixes clones; the roots file names the original.
|
|
CLONE = re.compile(r'\.(isra|constprop|part|cold|lto_priv)[0-9.]*$')
|
|
|
|
|
|
def load(ci_dir):
|
|
"""size[node], edges[node] -> set(node), byname[symbol] -> [node...]"""
|
|
size = {}
|
|
edges = collections.defaultdict(set)
|
|
byname = collections.defaultdict(list)
|
|
|
|
files = glob.glob(os.path.join(ci_dir, '*.ci'))
|
|
if not files:
|
|
print('error: no .ci files in %s - was -fcallgraph-info=su passed?'
|
|
% ci_dir, file=sys.stderr)
|
|
sys.exit(2)
|
|
|
|
for path in files:
|
|
with open(path, encoding='utf-8', errors='replace') as f:
|
|
text = f.read()
|
|
for title, label in NODE.findall(text):
|
|
m = SIZE.search(label)
|
|
if not m:
|
|
continue # declaration only, no body here
|
|
size[title] = int(m.group(1))
|
|
byname[CLONE.sub('', title.split(':')[-1])].append(title)
|
|
for src, dst in EDGE.findall(text):
|
|
edges[src].add(dst)
|
|
return size, edges, byname
|
|
|
|
|
|
def analyse(size, edges, byname, roots, top):
|
|
def targets(node):
|
|
"""Resolve a node's callees, including cross-TU ones.
|
|
|
|
A call to a function defined in another translation unit is
|
|
emitted as a bare symbol, so it has to be matched back to
|
|
whichever node defines it."""
|
|
out = set()
|
|
for dst in edges.get(node, ()):
|
|
if dst in size:
|
|
out.add(dst)
|
|
else:
|
|
out.update(byname.get(CLONE.sub('', dst.split(':')[-1]), ()))
|
|
return out
|
|
|
|
memo = {}
|
|
|
|
def worst(node, on_stack):
|
|
# Recursion is reported rather than summed: its depth is a
|
|
# runtime property this cannot see.
|
|
if node in on_stack:
|
|
return 0, ['<recursion>']
|
|
if node in memo:
|
|
return memo[node]
|
|
best, best_path = 0, []
|
|
for dst in targets(node):
|
|
cost, path = worst(dst, on_stack | {node})
|
|
if cost > best:
|
|
best, best_path = cost, path
|
|
result = (size.get(node, 0) + best,
|
|
[CLONE.sub('', node.split(':')[-1])] + best_path)
|
|
memo[node] = result
|
|
return result
|
|
|
|
found = []
|
|
for root in roots:
|
|
for node in byname.get(root, ()):
|
|
total, path = worst(node, frozenset())
|
|
found.append((total, root, path))
|
|
|
|
if not found:
|
|
print('error: none of the given roots were found in the call graph',
|
|
file=sys.stderr)
|
|
return 2
|
|
|
|
for total, root, path in sorted(found, reverse=True)[:top]:
|
|
print('%7d %s' % (total, root))
|
|
print(' %s' % ' -> '.join(path[:8]))
|
|
return 0
|
|
|
|
|
|
def main():
|
|
if len(sys.argv) < 3:
|
|
print(__doc__, file=sys.stderr)
|
|
return 2
|
|
ci_dir = sys.argv[1]
|
|
with open(sys.argv[2]) as f:
|
|
roots = [l.strip() for l in f if l.strip()]
|
|
top = int(sys.argv[3]) if len(sys.argv) > 3 else 15
|
|
size, edges, byname = load(ci_dir)
|
|
print('call graph: %d nodes with frames, %d roots requested\n'
|
|
% (len(size), len(roots)))
|
|
return analyse(size, edges, byname, roots, top)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
sys.exit(main())
|