stack.py 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651
  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. # Example:
  7. # ./scripts/stack.py lfs.ci lfs_util.ci -S
  8. #
  9. # Copyright (c) 2022, The littlefs authors.
  10. # SPDX-License-Identifier: BSD-3-Clause
  11. #
  12. import collections as co
  13. import csv
  14. import glob
  15. import itertools as it
  16. import math as m
  17. import os
  18. import re
  19. CI_PATHS = ['*.ci']
  20. # integer fields
  21. class IntField(co.namedtuple('IntField', 'x')):
  22. __slots__ = ()
  23. def __new__(cls, x=0):
  24. if isinstance(x, IntField):
  25. return x
  26. if isinstance(x, str):
  27. try:
  28. x = int(x, 0)
  29. except ValueError:
  30. # also accept +-∞ and +-inf
  31. if re.match('^\s*\+?\s*(?:∞|inf)\s*$', x):
  32. x = m.inf
  33. elif re.match('^\s*-\s*(?:∞|inf)\s*$', x):
  34. x = -m.inf
  35. else:
  36. raise
  37. assert isinstance(x, int) or m.isinf(x), x
  38. return super().__new__(cls, x)
  39. def __str__(self):
  40. if self.x == m.inf:
  41. return '∞'
  42. elif self.x == -m.inf:
  43. return '-∞'
  44. else:
  45. return str(self.x)
  46. def __int__(self):
  47. assert not m.isinf(self.x)
  48. return self.x
  49. def __float__(self):
  50. return float(self.x)
  51. none = '%7s' % '-'
  52. def table(self):
  53. return '%7s' % (self,)
  54. diff_none = '%7s' % '-'
  55. diff_table = table
  56. def diff_diff(self, other):
  57. new = self.x if self else 0
  58. old = other.x if other else 0
  59. diff = new - old
  60. if diff == +m.inf:
  61. return '%7s' % '+∞'
  62. elif diff == -m.inf:
  63. return '%7s' % '-∞'
  64. else:
  65. return '%+7d' % diff
  66. def ratio(self, other):
  67. new = self.x if self else 0
  68. old = other.x if other else 0
  69. if m.isinf(new) and m.isinf(old):
  70. return 0.0
  71. elif m.isinf(new):
  72. return +m.inf
  73. elif m.isinf(old):
  74. return -m.inf
  75. elif not old and not new:
  76. return 0.0
  77. elif not old:
  78. return 1.0
  79. else:
  80. return (new-old) / old
  81. def __add__(self, other):
  82. return IntField(self.x + other.x)
  83. def __sub__(self, other):
  84. return IntField(self.x - other.x)
  85. def __mul__(self, other):
  86. return IntField(self.x * other.x)
  87. def __lt__(self, other):
  88. return self.x < other.x
  89. def __gt__(self, other):
  90. return self.__class__.__lt__(other, self)
  91. def __le__(self, other):
  92. return not self.__gt__(other)
  93. def __ge__(self, other):
  94. return not self.__lt__(other)
  95. # size results
  96. class StackResult(co.namedtuple('StackResult',
  97. 'file,function,stack_frame,stack_limit')):
  98. __slots__ = ()
  99. def __new__(cls, file, function, stack_frame, stack_limit):
  100. return super().__new__(cls, file, function,
  101. IntField(stack_frame), IntField(stack_limit))
  102. def __add__(self, other):
  103. return StackResult(self.file, self.function,
  104. self.stack_frame + other.stack_frame,
  105. max(self.stack_limit, other.stack_limit))
  106. def openio(path, mode='r'):
  107. if path == '-':
  108. if mode == 'r':
  109. return os.fdopen(os.dup(sys.stdin.fileno()), 'r')
  110. else:
  111. return os.fdopen(os.dup(sys.stdout.fileno()), 'w')
  112. else:
  113. return open(path, mode)
  114. def collect(paths, *,
  115. everything=False,
  116. **args):
  117. # parse the vcg format
  118. k_pattern = re.compile('([a-z]+)\s*:', re.DOTALL)
  119. v_pattern = re.compile('(?:"(.*?)"|([a-z]+))', re.DOTALL)
  120. def parse_vcg(rest):
  121. def parse_vcg(rest):
  122. node = []
  123. while True:
  124. rest = rest.lstrip()
  125. m = k_pattern.match(rest)
  126. if not m:
  127. return (node, rest)
  128. k, rest = m.group(1), rest[m.end(0):]
  129. rest = rest.lstrip()
  130. if rest.startswith('{'):
  131. v, rest = parse_vcg(rest[1:])
  132. assert rest[0] == '}', "unexpected %r" % rest[0:1]
  133. rest = rest[1:]
  134. node.append((k, v))
  135. else:
  136. m = v_pattern.match(rest)
  137. assert m, "unexpected %r" % rest[0:1]
  138. v, rest = m.group(1) or m.group(2), rest[m.end(0):]
  139. node.append((k, v))
  140. node, rest = parse_vcg(rest)
  141. assert rest == '', "unexpected %r" % rest[0:1]
  142. return node
  143. # collect into functions
  144. callgraph = co.defaultdict(lambda: (None, None, 0, set()))
  145. f_pattern = re.compile(
  146. r'([^\\]*)\\n([^:]*)[^\\]*\\n([0-9]+) bytes \((.*)\)')
  147. for path in paths:
  148. with open(path) as f:
  149. vcg = parse_vcg(f.read())
  150. for k, graph in vcg:
  151. if k != 'graph':
  152. continue
  153. for k, info in graph:
  154. if k == 'node':
  155. info = dict(info)
  156. m = f_pattern.match(info['label'])
  157. if m:
  158. function, file, size, type = m.groups()
  159. if (not args.get('quiet')
  160. and 'static' not in type
  161. and 'bounded' not in type):
  162. print('warning: found non-static stack for %s (%s)'
  163. % (function, type, size))
  164. _, _, _, targets = callgraph[info['title']]
  165. callgraph[info['title']] = (
  166. file, function, int(size), targets)
  167. elif k == 'edge':
  168. info = dict(info)
  169. _, _, _, targets = callgraph[info['sourcename']]
  170. targets.add(info['targetname'])
  171. else:
  172. continue
  173. if not everything:
  174. for source, (s_file, s_function, _, _) in list(callgraph.items()):
  175. # discard internal functions
  176. if s_file.startswith('<') or s_file.startswith('/usr/include'):
  177. del callgraph[source]
  178. # find maximum stack size recursively, this requires also detecting cycles
  179. # (in case of recursion)
  180. def find_limit(source, seen=None):
  181. seen = seen or set()
  182. if source not in callgraph:
  183. return 0
  184. _, _, frame, targets = callgraph[source]
  185. limit = 0
  186. for target in targets:
  187. if target in seen:
  188. # found a cycle
  189. return float('inf')
  190. limit_ = find_limit(target, seen | {target})
  191. limit = max(limit, limit_)
  192. return frame + limit
  193. def find_calls(targets):
  194. calls = set()
  195. for target in targets:
  196. if target in callgraph:
  197. t_file, t_function, _, _ = callgraph[target]
  198. calls.add((t_file, t_function))
  199. return calls
  200. # build results
  201. results = []
  202. calls = {}
  203. for source, (s_file, s_function, frame, targets) in callgraph.items():
  204. limit = find_limit(source)
  205. cs = find_calls(targets)
  206. results.append(StackResult(s_file, s_function, frame, limit))
  207. calls[(s_file, s_function)] = cs
  208. return results, calls
  209. def fold(results, *,
  210. by=['file', 'function'],
  211. **_):
  212. folding = co.OrderedDict()
  213. for r in results:
  214. name = tuple(getattr(r, k) for k in by)
  215. if name not in folding:
  216. folding[name] = []
  217. folding[name].append(r)
  218. folded = []
  219. for rs in folding.values():
  220. folded.append(sum(rs[1:], start=rs[0]))
  221. return folded
  222. def fold_calls(calls, *,
  223. by=['file', 'function'],
  224. **_):
  225. def by_(name):
  226. file, function = name
  227. return (((file,) if 'file' in by else ())
  228. + ((function,) if 'function' in by else ()))
  229. folded = {}
  230. for name, cs in calls.items():
  231. name = by_(name)
  232. if name not in folded:
  233. folded[name] = set()
  234. folded[name] |= {by_(c) for c in cs}
  235. return folded
  236. def table(results, calls, diff_results=None, *,
  237. by_file=False,
  238. limit_sort=False,
  239. reverse_limit_sort=False,
  240. frame_sort=False,
  241. reverse_frame_sort=False,
  242. summary=False,
  243. all=False,
  244. percent=False,
  245. tree=False,
  246. depth=None,
  247. **_):
  248. all_, all = all, __builtins__.all
  249. # tree doesn't really make sense with depth=0, assume depth=inf
  250. if depth is None:
  251. depth = float('inf') if tree else 0
  252. # fold
  253. results = fold(results, by=['file' if by_file else 'function'])
  254. calls = fold_calls(calls, by=['file' if by_file else 'function'])
  255. if diff_results is not None:
  256. diff_results = fold(diff_results,
  257. by=['file' if by_file else 'function'])
  258. table = {
  259. r.file if by_file else r.function: r
  260. for r in results}
  261. diff_table = {
  262. r.file if by_file else r.function: r
  263. for r in diff_results or []}
  264. # sort, note that python's sort is stable
  265. names = list(table.keys() | diff_table.keys())
  266. names.sort()
  267. if diff_results is not None:
  268. names.sort(key=lambda n: -IntField.ratio(
  269. table[n].stack_frame if n in table else None,
  270. diff_table[n].stack_frame if n in diff_table else None))
  271. if limit_sort:
  272. names.sort(key=lambda n: (table[n].stack_limit,) if n in table else (),
  273. reverse=True)
  274. elif reverse_limit_sort:
  275. names.sort(key=lambda n: (table[n].stack_limit,) if n in table else (),
  276. reverse=False)
  277. elif frame_sort:
  278. names.sort(key=lambda n: (table[n].stack_frame,) if n in table else (),
  279. reverse=True)
  280. elif reverse_frame_sort:
  281. names.sort(key=lambda n: (table[n].stack_frame,) if n in table else (),
  282. reverse=False)
  283. # adjust the name width based on the expected call depth, note that we
  284. # can't always find the depth due to recursion
  285. width = 36 + (4*depth if not m.isinf(depth) else 0)
  286. # print header
  287. if not tree:
  288. print('%-*s' % (width, '%s%s' % (
  289. 'file' if by_file else 'function',
  290. ' (%d added, %d removed)' % (
  291. sum(1 for n in table if n not in diff_table),
  292. sum(1 for n in diff_table if n not in table))
  293. if diff_results is not None and not percent else '')
  294. if not summary else ''),
  295. end='')
  296. if diff_results is None:
  297. print(' %s %s' % (
  298. 'frame'.rjust(len(IntField.none)),
  299. 'limit'.rjust(len(IntField.none))))
  300. elif percent:
  301. print(' %s %s' % (
  302. 'frame'.rjust(len(IntField.diff_none)),
  303. 'limit'.rjust(len(IntField.diff_none))))
  304. else:
  305. print(' %s %s %s %s %s %s' % (
  306. 'oframe'.rjust(len(IntField.diff_none)),
  307. 'olimit'.rjust(len(IntField.diff_none)),
  308. 'nframe'.rjust(len(IntField.diff_none)),
  309. 'nlimit'.rjust(len(IntField.diff_none)),
  310. 'dframe'.rjust(len(IntField.diff_none)),
  311. 'dlimit'.rjust(len(IntField.diff_none))))
  312. # print entries
  313. if not summary:
  314. # print the tree recursively
  315. def table_calls(names_, depth,
  316. prefixes=('', '', '', '')):
  317. for i, name in enumerate(names_):
  318. r = table.get(name)
  319. if diff_results is not None:
  320. diff_r = diff_table.get(name)
  321. ratio = IntField.ratio(
  322. r.stack_limit if r else None,
  323. diff_r.stack_limit if diff_r else None)
  324. if not ratio and not all_:
  325. continue
  326. is_last = (i == len(names_)-1)
  327. print('%-*s' % (width, prefixes[0+is_last] + name), end='')
  328. if tree:
  329. print()
  330. elif diff_results is None:
  331. print(' %s %s' % (
  332. r.stack_frame.table()
  333. if r else IntField.none,
  334. r.stack_limit.table()
  335. if r else IntField.none))
  336. elif percent:
  337. print(' %s %s%s' % (
  338. r.stack_frame.diff_table()
  339. if r else IntField.diff_none,
  340. r.stack_limit.diff_table()
  341. if r else IntField.diff_none,
  342. ' (%s)' % (
  343. '+∞%' if ratio == +m.inf
  344. else '-∞%' if ratio == -m.inf
  345. else '%+.1f%%' % (100*ratio))))
  346. else:
  347. print(' %s %s %s %s %s %s%s' % (
  348. diff_r.stack_frame.diff_table()
  349. if diff_r else IntField.diff_none,
  350. diff_r.stack_limit.diff_table()
  351. if diff_r else IntField.diff_none,
  352. r.stack_frame.diff_table()
  353. if r else IntField.diff_none,
  354. r.stack_limit.diff_table()
  355. if r else IntField.diff_none,
  356. IntField.diff_diff(
  357. r.stack_frame if r else None,
  358. diff_r.stack_frame if diff_r else None)
  359. if r or diff_r else IntField.diff_none,
  360. IntField.diff_diff(
  361. r.stack_limit if r else None,
  362. diff_r.stack_limit if diff_r else None)
  363. if r or diff_r else IntField.diff_none,
  364. ' (%s)' % (
  365. '+∞%' if ratio == +m.inf
  366. else '-∞%' if ratio == -m.inf
  367. else '%+.1f%%' % (100*ratio))
  368. if ratio else ''))
  369. # recurse?
  370. if depth > 0:
  371. cs = calls.get((name,), set())
  372. table_calls(
  373. [n for n in names if (n,) in cs],
  374. depth-1,
  375. ( prefixes[2+is_last] + "|-> ",
  376. prefixes[2+is_last] + "'-> ",
  377. prefixes[2+is_last] + "| ",
  378. prefixes[2+is_last] + " "))
  379. table_calls(names, depth)
  380. # print total
  381. if not tree:
  382. total = fold(results, by=[])
  383. r = total[0] if total else None
  384. if diff_results is not None:
  385. diff_total = fold(diff_results, by=[])
  386. diff_r = diff_total[0] if diff_total else None
  387. ratio = IntField.ratio(
  388. r.stack_limit if r else None,
  389. diff_r.stack_limit if diff_r else None)
  390. print('%-*s' % (width, 'TOTAL'), end='')
  391. if diff_results is None:
  392. print(' %s %s' % (
  393. r.stack_frame.table()
  394. if r else IntField.none,
  395. r.stack_limit.table()
  396. if r else IntField.none))
  397. elif percent:
  398. print(' %s %s%s' % (
  399. r.stack_frame.diff_table()
  400. if r else IntField.diff_none,
  401. r.stack_limit.diff_table()
  402. if r else IntField.diff_none,
  403. ' (%s)' % (
  404. '+∞%' if ratio == +m.inf
  405. else '-∞%' if ratio == -m.inf
  406. else '%+.1f%%' % (100*ratio))))
  407. else:
  408. print(' %s %s %s %s %s %s%s' % (
  409. diff_r.stack_frame.diff_table()
  410. if diff_r else IntField.diff_none,
  411. diff_r.stack_limit.diff_table()
  412. if diff_r else IntField.diff_none,
  413. r.stack_frame.diff_table()
  414. if r else IntField.diff_none,
  415. r.stack_limit.diff_table()
  416. if r else IntField.diff_none,
  417. IntField.diff_diff(
  418. r.stack_frame if r else None,
  419. diff_r.stack_frame if diff_r else None)
  420. if r or diff_r else IntField.diff_none,
  421. IntField.diff_diff(
  422. r.stack_limit if r else None,
  423. diff_r.stack_limit if diff_r else None)
  424. if r or diff_r else IntField.diff_none,
  425. ' (%s)' % (
  426. '+∞%' if ratio == +m.inf
  427. else '-∞%' if ratio == -m.inf
  428. else '%+.1f%%' % (100*ratio))
  429. if ratio else ''))
  430. def main(ci_paths, **args):
  431. # find sizes
  432. if not args.get('use', None):
  433. # find .ci files
  434. paths = []
  435. for path in ci_paths:
  436. if os.path.isdir(path):
  437. path = path + '/*.ci'
  438. for path in glob.glob(path):
  439. paths.append(path)
  440. if not paths:
  441. print('no .ci files found in %r?' % ci_paths)
  442. sys.exit(-1)
  443. results, calls = collect(paths, **args)
  444. else:
  445. results = []
  446. with openio(args['use']) as f:
  447. reader = csv.DictReader(f, restval='')
  448. for r in reader:
  449. try:
  450. results.append(StackResult(**{
  451. k: v for k, v in r.items()
  452. if k in StackResult._fields}))
  453. except TypeError:
  454. pass
  455. calls = {}
  456. # fold to remove duplicates
  457. results = fold(results)
  458. # sort because why not
  459. results.sort()
  460. # write results to CSV
  461. if args.get('output'):
  462. with openio(args['output'], 'w') as f:
  463. writer = csv.DictWriter(f, StackResult._fields)
  464. writer.writeheader()
  465. for r in results:
  466. writer.writerow(r._asdict())
  467. # find previous results?
  468. if args.get('diff'):
  469. diff_results = []
  470. try:
  471. with openio(args['diff']) as f:
  472. reader = csv.DictReader(f, restval='')
  473. for r in reader:
  474. try:
  475. diff_results.append(StackResult(**{
  476. k: v for k, v in r.items()
  477. if k in StackResult._fields}))
  478. except TypeError:
  479. pass
  480. except FileNotFoundError:
  481. pass
  482. # fold to remove duplicates
  483. diff_results = fold(diff_results)
  484. # print table
  485. if not args.get('quiet'):
  486. table(
  487. results,
  488. calls,
  489. diff_results if args.get('diff') else None,
  490. **args)
  491. # error on recursion
  492. if args.get('error_on_recursion') and any(
  493. m.isinf(float(r.stack_limit)) for r in results):
  494. sys.exit(2)
  495. if __name__ == "__main__":
  496. import argparse
  497. import sys
  498. parser = argparse.ArgumentParser(
  499. description="Find stack usage at the function level.")
  500. parser.add_argument(
  501. 'ci_paths',
  502. nargs='*',
  503. default=CI_PATHS,
  504. help="Description of where to find *.ci files. May be a directory "
  505. "or a list of paths. Defaults to %r." % CI_PATHS)
  506. parser.add_argument(
  507. '-v', '--verbose',
  508. action='store_true',
  509. help="Output commands that run behind the scenes.")
  510. parser.add_argument(
  511. '-q', '--quiet',
  512. action='store_true',
  513. help="Don't show anything, useful with -o.")
  514. parser.add_argument(
  515. '-o', '--output',
  516. help="Specify CSV file to store results.")
  517. parser.add_argument(
  518. '-u', '--use',
  519. help="Don't parse anything, use this CSV file.")
  520. parser.add_argument(
  521. '-d', '--diff',
  522. help="Specify CSV file to diff against.")
  523. parser.add_argument(
  524. '-a', '--all',
  525. action='store_true',
  526. help="Show all, not just the ones that changed.")
  527. parser.add_argument(
  528. '-p', '--percent',
  529. action='store_true',
  530. help="Only show percentage change, not a full diff.")
  531. parser.add_argument(
  532. '-t', '--tree',
  533. action='store_true',
  534. help="Only show the function call tree.")
  535. parser.add_argument(
  536. '-b', '--by-file',
  537. action='store_true',
  538. help="Group by file.")
  539. parser.add_argument(
  540. '-s', '--limit-sort',
  541. action='store_true',
  542. help="Sort by stack limit.")
  543. parser.add_argument(
  544. '-S', '--reverse-limit-sort',
  545. action='store_true',
  546. help="Sort by stack limit, but backwards.")
  547. parser.add_argument(
  548. '--frame-sort',
  549. action='store_true',
  550. help="Sort by stack frame.")
  551. parser.add_argument(
  552. '--reverse-frame-sort',
  553. action='store_true',
  554. help="Sort by stack frame, but backwards.")
  555. parser.add_argument(
  556. '-Y', '--summary',
  557. action='store_true',
  558. help="Only show the total size.")
  559. parser.add_argument(
  560. '-L', '--depth',
  561. nargs='?',
  562. type=lambda x: int(x, 0),
  563. const=float('inf'),
  564. help="Depth of function calls to show.")
  565. parser.add_argument(
  566. '-e', '--error-on-recursion',
  567. action='store_true',
  568. help="Error if any functions are recursive.")
  569. parser.add_argument(
  570. '-A', '--everything',
  571. action='store_true',
  572. help="Include builtin and libc specific symbols.")
  573. parser.add_argument(
  574. '--build-dir',
  575. help="Specify the relative build directory. Used to map object files "
  576. "to the correct source files.")
  577. sys.exit(main(**{k: v
  578. for k, v in vars(parser.parse_intermixed_args()).items()
  579. if v is not None}))