stack.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429
  1. #!/usr/bin/env python3
  2. #
  3. # Script to find stack usage at the function level. Will detect recursion and
  4. # report as infinite stack usage.
  5. #
  6. import os
  7. import glob
  8. import itertools as it
  9. import re
  10. import csv
  11. import collections as co
  12. import math as m
  13. CI_PATHS = ['*.ci']
  14. def openio(path, mode='r'):
  15. if path == '-':
  16. if 'r' in mode:
  17. return os.fdopen(os.dup(sys.stdin.fileno()), 'r')
  18. else:
  19. return os.fdopen(os.dup(sys.stdout.fileno()), 'w')
  20. else:
  21. return open(path, mode)
  22. class StackResult(co.namedtuple('StackResult', 'stack_frame,stack_limit')):
  23. __slots__ = ()
  24. def __new__(cls, stack_frame=0, stack_limit=0):
  25. return super().__new__(cls,
  26. int(stack_frame),
  27. float(stack_limit))
  28. def __add__(self, other):
  29. return self.__class__(
  30. self.stack_frame + other.stack_frame,
  31. max(self.stack_limit, other.stack_limit))
  32. def __sub__(self, other):
  33. return StackDiff(other, self)
  34. def __rsub__(self, other):
  35. return self.__class__.__sub__(other, self)
  36. def key(self, **args):
  37. if args.get('limit_sort'):
  38. return -self.stack_limit
  39. elif args.get('reverse_limit_sort'):
  40. return +self.stack_limit
  41. elif args.get('frame_sort'):
  42. return -self.stack_frame
  43. elif args.get('reverse_frame_sort'):
  44. return +self.stack_frame
  45. else:
  46. return None
  47. _header = '%7s %7s' % ('frame', 'limit')
  48. def __str__(self):
  49. return '%7d %7s' % (
  50. self.stack_frame,
  51. '∞' if m.isinf(self.stack_limit) else int(self.stack_limit))
  52. class StackDiff(co.namedtuple('StackDiff', 'old,new')):
  53. __slots__ = ()
  54. def ratio(self):
  55. old_limit = self.old.stack_limit if self.old is not None else 0
  56. new_limit = self.new.stack_limit if self.new is not None else 0
  57. return (0.0 if m.isinf(new_limit) and m.isinf(old_limit)
  58. else +float('inf') if m.isinf(new_limit)
  59. else -float('inf') if m.isinf(old_limit)
  60. else 0.0 if not old_limit and not new_limit
  61. else 1.0 if not old_limit
  62. else (new_limit-old_limit) / old_limit)
  63. def key(self, **args):
  64. return (
  65. self.new.key(**args) if self.new is not None else 0,
  66. -self.ratio())
  67. def __bool__(self):
  68. return bool(self.ratio())
  69. _header = '%15s %15s %15s' % ('old', 'new', 'diff')
  70. def __str__(self):
  71. old_frame = self.old.stack_frame if self.old is not None else 0
  72. old_limit = self.old.stack_limit if self.old is not None else 0
  73. new_frame = self.new.stack_frame if self.new is not None else 0
  74. new_limit = self.new.stack_limit if self.new is not None else 0
  75. diff_frame = new_frame - old_frame
  76. diff_limit = (0 if m.isinf(new_limit) and m.isinf(old_limit)
  77. else new_limit - old_limit)
  78. ratio = self.ratio()
  79. return '%7s %7s %7s %7s %+7d %7s%s' % (
  80. old_frame if self.old is not None else '-',
  81. ('∞' if m.isinf(old_limit) else int(old_limit))
  82. if self.old is not None else '-',
  83. new_frame if self.new is not None else '-',
  84. ('∞' if m.isinf(new_limit) else int(new_limit))
  85. if self.new is not None else '-',
  86. diff_frame,
  87. '+∞' if diff_limit > 0 and m.isinf(diff_limit)
  88. else '-∞' if diff_limit < 0 and m.isinf(diff_limit)
  89. else '%+d' % diff_limit,
  90. '' if not ratio
  91. else ' (+∞%)' if ratio > 0 and m.isinf(ratio)
  92. else ' (-∞%)' if ratio < 0 and m.isinf(ratio)
  93. else ' (%+.1f%%)' % (100*ratio))
  94. def collect(paths, **args):
  95. # parse the vcg format
  96. k_pattern = re.compile('([a-z]+)\s*:', re.DOTALL)
  97. v_pattern = re.compile('(?:"(.*?)"|([a-z]+))', re.DOTALL)
  98. def parse_vcg(rest):
  99. def parse_vcg(rest):
  100. node = []
  101. while True:
  102. rest = rest.lstrip()
  103. m = k_pattern.match(rest)
  104. if not m:
  105. return (node, rest)
  106. k, rest = m.group(1), rest[m.end(0):]
  107. rest = rest.lstrip()
  108. if rest.startswith('{'):
  109. v, rest = parse_vcg(rest[1:])
  110. assert rest[0] == '}', "unexpected %r" % rest[0:1]
  111. rest = rest[1:]
  112. node.append((k, v))
  113. else:
  114. m = v_pattern.match(rest)
  115. assert m, "unexpected %r" % rest[0:1]
  116. v, rest = m.group(1) or m.group(2), rest[m.end(0):]
  117. node.append((k, v))
  118. node, rest = parse_vcg(rest)
  119. assert rest == '', "unexpected %r" % rest[0:1]
  120. return node
  121. # collect into functions
  122. callgraph = co.defaultdict(lambda: (None, None, 0, set()))
  123. f_pattern = re.compile(
  124. r'([^\\]*)\\n([^:]*)[^\\]*\\n([0-9]+) bytes \((.*)\)')
  125. for path in paths:
  126. with open(path) as f:
  127. vcg = parse_vcg(f.read())
  128. for k, graph in vcg:
  129. if k != 'graph':
  130. continue
  131. for k, info in graph:
  132. if k == 'node':
  133. info = dict(info)
  134. m = f_pattern.match(info['label'])
  135. if m:
  136. function, file, size, type = m.groups()
  137. if not args.get('quiet') and type != 'static':
  138. print('warning: found non-static stack for %s (%s)'
  139. % (function, type))
  140. _, _, _, targets = callgraph[info['title']]
  141. callgraph[info['title']] = (
  142. file, function, int(size), targets)
  143. elif k == 'edge':
  144. info = dict(info)
  145. _, _, _, targets = callgraph[info['sourcename']]
  146. targets.add(info['targetname'])
  147. else:
  148. continue
  149. if not args.get('everything'):
  150. for source, (s_file, s_function, _, _) in list(callgraph.items()):
  151. # discard internal functions
  152. if s_file.startswith('<') or s_file.startswith('/usr/include'):
  153. del callgraph[source]
  154. # find maximum stack size recursively, this requires also detecting cycles
  155. # (in case of recursion)
  156. def find_limit(source, seen=None):
  157. seen = seen or set()
  158. if source not in callgraph:
  159. return 0
  160. _, _, frame, targets = callgraph[source]
  161. limit = 0
  162. for target in targets:
  163. if target in seen:
  164. # found a cycle
  165. return float('inf')
  166. limit_ = find_limit(target, seen | {target})
  167. limit = max(limit, limit_)
  168. return frame + limit
  169. def find_calls(targets):
  170. calls = set()
  171. for target in targets:
  172. if target in callgraph:
  173. t_file, t_function, _, _ = callgraph[target]
  174. calls.add((t_file, t_function))
  175. return calls
  176. # build results
  177. results = {}
  178. result_calls = {}
  179. for source, (s_file, s_function, frame, targets) in callgraph.items():
  180. limit = find_limit(source)
  181. calls = find_calls(targets)
  182. results[(s_file, s_function)] = StackResult(frame, limit)
  183. result_calls[(s_file, s_function)] = calls
  184. return results, result_calls
  185. def main(**args):
  186. # find sizes
  187. if not args.get('use', None):
  188. # find .ci files
  189. paths = []
  190. for path in args['ci_paths']:
  191. if os.path.isdir(path):
  192. path = path + '/*.ci'
  193. for path in glob.glob(path):
  194. paths.append(path)
  195. if not paths:
  196. print('no .ci files found in %r?' % args['ci_paths'])
  197. sys.exit(-1)
  198. results, result_calls = collect(paths, **args)
  199. else:
  200. with openio(args['use']) as f:
  201. r = csv.DictReader(f)
  202. results = {
  203. (result['file'], result['name']): StackResult(
  204. *(result[f] for f in StackResult._fields))
  205. for result in r
  206. if all(result.get(f) not in {None, ''}
  207. for f in StackResult._fields)}
  208. result_calls = {}
  209. # find previous results?
  210. if args.get('diff'):
  211. try:
  212. with openio(args['diff']) as f:
  213. r = csv.DictReader(f)
  214. prev_results = {
  215. (result['file'], result['name']): StackResult(
  216. *(result[f] for f in StackResult._fields))
  217. for result in r
  218. if all(result.get(f) not in {None, ''}
  219. for f in StackResult._fields)}
  220. except FileNotFoundError:
  221. prev_results = []
  222. # write results to CSV
  223. if args.get('output'):
  224. merged_results = co.defaultdict(lambda: {})
  225. other_fields = []
  226. # merge?
  227. if args.get('merge'):
  228. try:
  229. with openio(args['merge']) as f:
  230. r = csv.DictReader(f)
  231. for result in r:
  232. file = result.pop('file', '')
  233. func = result.pop('name', '')
  234. for f in StackResult._fields:
  235. result.pop(f, None)
  236. merged_results[(file, func)] = result
  237. other_fields = result.keys()
  238. except FileNotFoundError:
  239. pass
  240. for (file, func), result in results.items():
  241. merged_results[(file, func)] |= result._asdict()
  242. with openio(args['output'], 'w') as f:
  243. w = csv.DictWriter(f, ['file', 'name',
  244. *other_fields, *StackResult._fields])
  245. w.writeheader()
  246. for (file, func), result in sorted(merged_results.items()):
  247. w.writerow({'file': file, 'name': func, **result})
  248. # print results
  249. def print_header(by):
  250. if by == 'total':
  251. entry = lambda k: 'TOTAL'
  252. elif by == 'file':
  253. entry = lambda k: k[0]
  254. else:
  255. entry = lambda k: k[1]
  256. if not args.get('diff'):
  257. print('%-36s %s' % (by, StackResult._header))
  258. else:
  259. old = {entry(k) for k in results.keys()}
  260. new = {entry(k) for k in prev_results.keys()}
  261. print('%-36s %s' % (
  262. '%s (%d added, %d removed)' % (by,
  263. sum(1 for k in new if k not in old),
  264. sum(1 for k in old if k not in new))
  265. if by else '',
  266. StackDiff._header))
  267. def print_entries(by):
  268. # print optional tree of dependencies
  269. def print_calls(entries, entry_calls, depth,
  270. filter=lambda _: True,
  271. prefixes=('', '', '', '')):
  272. filtered_entries = {
  273. name: result for name, result in entries.items()
  274. if filter(name)}
  275. for i, (name, result) in enumerate(sorted(filtered_entries.items(),
  276. key=lambda p: (p[1].key(**args), p))):
  277. last = (i == len(filtered_entries)-1)
  278. print('%-36s %s' % (prefixes[0+last] + name, result))
  279. if depth > 0 and by != 'total':
  280. calls = entry_calls.get(name, set())
  281. print_calls(entries, entry_calls, depth-1,
  282. lambda name: name in calls,
  283. ( prefixes[2+last] + "|-> ",
  284. prefixes[2+last] + "'-> ",
  285. prefixes[2+last] + "| ",
  286. prefixes[2+last] + " "))
  287. if by == 'total':
  288. entry = lambda k: 'TOTAL'
  289. elif by == 'file':
  290. entry = lambda k: k[0]
  291. else:
  292. entry = lambda k: k[1]
  293. entries = co.defaultdict(lambda: StackResult())
  294. for k, result in results.items():
  295. entries[entry(k)] += result
  296. entry_calls = co.defaultdict(lambda: set())
  297. for k, calls in result_calls.items():
  298. entry_calls[entry(k)] |= {entry(c) for c in calls}
  299. if not args.get('diff'):
  300. print_calls(
  301. entries,
  302. entry_calls,
  303. args.get('depth', 0))
  304. else:
  305. prev_entries = co.defaultdict(lambda: StackResult())
  306. for k, result in prev_results.items():
  307. prev_entries[entry(k)] += result
  308. diff_entries = {name: entries.get(name) - prev_entries.get(name)
  309. for name in (entries.keys() | prev_entries.keys())}
  310. print_calls(
  311. {name: diff for name, diff in diff_entries.items()
  312. if diff or args.get('all')},
  313. entry_calls,
  314. args.get('depth', 0))
  315. if args.get('quiet'):
  316. pass
  317. elif args.get('summary'):
  318. print_header('')
  319. print_entries('total')
  320. elif args.get('files'):
  321. print_header('file')
  322. print_entries('file')
  323. print_entries('total')
  324. else:
  325. print_header('function')
  326. print_entries('function')
  327. print_entries('total')
  328. # catch recursion
  329. if args.get('error_on_recursion') and any(
  330. m.isinf(limit) for _, _, _, limit, _ in results):
  331. sys.exit(2)
  332. if __name__ == "__main__":
  333. import argparse
  334. import sys
  335. parser = argparse.ArgumentParser(
  336. description="Find stack usage at the function level.")
  337. parser.add_argument('ci_paths', nargs='*', default=CI_PATHS,
  338. help="Description of where to find *.ci files. May be a directory \
  339. or a list of paths. Defaults to %r." % CI_PATHS)
  340. parser.add_argument('-v', '--verbose', action='store_true',
  341. help="Output commands that run behind the scenes.")
  342. parser.add_argument('-q', '--quiet', action='store_true',
  343. help="Don't show anything, useful with -o.")
  344. parser.add_argument('-o', '--output',
  345. help="Specify CSV file to store results.")
  346. parser.add_argument('-u', '--use',
  347. help="Don't parse callgraph files, instead use this CSV file.")
  348. parser.add_argument('-d', '--diff',
  349. help="Specify CSV file to diff against.")
  350. parser.add_argument('-m', '--merge',
  351. help="Merge with an existing CSV file when writing to output.")
  352. parser.add_argument('-a', '--all', action='store_true',
  353. help="Show all functions, not just the ones that changed.")
  354. parser.add_argument('-A', '--everything', action='store_true',
  355. help="Include builtin and libc specific symbols.")
  356. parser.add_argument('--frame-sort', action='store_true',
  357. help="Sort by stack frame size.")
  358. parser.add_argument('--reverse-frame-sort', action='store_true',
  359. help="Sort by stack frame size, but backwards.")
  360. parser.add_argument('-s', '--limit-sort', action='store_true',
  361. help="Sort by stack limit.")
  362. parser.add_argument('-S', '--reverse-limit-sort', action='store_true',
  363. help="Sort by stack limit, but backwards.")
  364. parser.add_argument('-L', '--depth', default=0, type=lambda x: int(x, 0),
  365. nargs='?', const=float('inf'),
  366. help="Depth of dependencies to show.")
  367. parser.add_argument('-F', '--files', action='store_true',
  368. help="Show file-level calls.")
  369. parser.add_argument('-Y', '--summary', action='store_true',
  370. help="Only show the total stack size.")
  371. parser.add_argument('-e', '--error-on-recursion', action='store_true',
  372. help="Error if any functions are recursive.")
  373. parser.add_argument('--build-dir',
  374. help="Specify the relative build directory. Used to map object files \
  375. to the correct source files.")
  376. sys.exit(main(**{k: v
  377. for k, v in vars(parser.parse_args()).items()
  378. if v is not None}))