stack.py 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730
  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 Int(co.namedtuple('Int', 'x')):
  22. __slots__ = ()
  23. def __new__(cls, x=0):
  24. if isinstance(x, Int):
  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 self.__class__(self.x + other.x)
  83. def __sub__(self, other):
  84. return self.__class__(self.x - other.x)
  85. def __mul__(self, other):
  86. return self.__class__(self.x * other.x)
  87. # size results
  88. class StackResult(co.namedtuple('StackResult', [
  89. 'file', 'function', 'frame', 'limit', 'calls'])):
  90. _by = ['file', 'function']
  91. _fields = ['frame', 'limit']
  92. _types = {'frame': Int, 'limit': Int}
  93. __slots__ = ()
  94. def __new__(cls, file='', function='',
  95. frame=0, limit=0, calls=set()):
  96. return super().__new__(cls, file, function,
  97. Int(frame), Int(limit),
  98. calls)
  99. def __add__(self, other):
  100. return StackResult(self.file, self.function,
  101. self.frame + other.frame,
  102. max(self.limit, other.limit),
  103. self.calls | other.calls)
  104. def openio(path, mode='r'):
  105. if path == '-':
  106. if mode == 'r':
  107. return os.fdopen(os.dup(sys.stdin.fileno()), 'r')
  108. else:
  109. return os.fdopen(os.dup(sys.stdout.fileno()), 'w')
  110. else:
  111. return open(path, mode)
  112. def collect(paths, *,
  113. everything=False,
  114. **args):
  115. # parse the vcg format
  116. k_pattern = re.compile('([a-z]+)\s*:', re.DOTALL)
  117. v_pattern = re.compile('(?:"(.*?)"|([a-z]+))', re.DOTALL)
  118. def parse_vcg(rest):
  119. def parse_vcg(rest):
  120. node = []
  121. while True:
  122. rest = rest.lstrip()
  123. m_ = k_pattern.match(rest)
  124. if not m_:
  125. return (node, rest)
  126. k, rest = m_.group(1), rest[m_.end(0):]
  127. rest = rest.lstrip()
  128. if rest.startswith('{'):
  129. v, rest = parse_vcg(rest[1:])
  130. assert rest[0] == '}', "unexpected %r" % rest[0:1]
  131. rest = rest[1:]
  132. node.append((k, v))
  133. else:
  134. m_ = v_pattern.match(rest)
  135. assert m_, "unexpected %r" % rest[0:1]
  136. v, rest = m_.group(1) or m_.group(2), rest[m_.end(0):]
  137. node.append((k, v))
  138. node, rest = parse_vcg(rest)
  139. assert rest == '', "unexpected %r" % rest[0:1]
  140. return node
  141. # collect into functions
  142. callgraph = co.defaultdict(lambda: (None, None, 0, set()))
  143. f_pattern = re.compile(
  144. r'([^\\]*)\\n([^:]*)[^\\]*\\n([0-9]+) bytes \((.*)\)')
  145. for path in paths:
  146. with open(path) as f:
  147. vcg = parse_vcg(f.read())
  148. for k, graph in vcg:
  149. if k != 'graph':
  150. continue
  151. for k, info in graph:
  152. if k == 'node':
  153. info = dict(info)
  154. m_ = f_pattern.match(info['label'])
  155. if m_:
  156. function, file, size, type = m_.groups()
  157. if (not args.get('quiet')
  158. and 'static' not in type
  159. and 'bounded' not in type):
  160. print("warning: found non-static stack for %s (%s)"
  161. % (function, type, size))
  162. _, _, _, targets = callgraph[info['title']]
  163. callgraph[info['title']] = (
  164. file, function, int(size), targets)
  165. elif k == 'edge':
  166. info = dict(info)
  167. _, _, _, targets = callgraph[info['sourcename']]
  168. targets.add(info['targetname'])
  169. else:
  170. continue
  171. if not everything:
  172. for source, (s_file, s_function, _, _) in list(callgraph.items()):
  173. # discard internal functions
  174. if s_file.startswith('<') or s_file.startswith('/usr/include'):
  175. del callgraph[source]
  176. # find maximum stack size recursively, this requires also detecting cycles
  177. # (in case of recursion)
  178. def find_limit(source, seen=None):
  179. seen = seen or set()
  180. if source not in callgraph:
  181. return 0
  182. _, _, frame, targets = callgraph[source]
  183. limit = 0
  184. for target in targets:
  185. if target in seen:
  186. # found a cycle
  187. return m.inf
  188. limit_ = find_limit(target, seen | {target})
  189. limit = max(limit, limit_)
  190. return frame + limit
  191. def find_calls(targets):
  192. calls = set()
  193. for target in targets:
  194. if target in callgraph:
  195. t_file, t_function, _, _ = callgraph[target]
  196. calls.add((t_file, t_function))
  197. return calls
  198. # build results
  199. results = []
  200. for source, (s_file, s_function, frame, targets) in callgraph.items():
  201. limit = find_limit(source)
  202. calls = find_calls(targets)
  203. results.append(StackResult(s_file, s_function, frame, limit, calls))
  204. return results
  205. def fold(Result, results, *,
  206. by=None,
  207. defines=None,
  208. **_):
  209. if by is None:
  210. by = Result._by
  211. for k in it.chain(by or [], (k for k, _ in defines or [])):
  212. if k not in Result._by and k not in Result._fields:
  213. print("error: could not find field %r?" % k)
  214. sys.exit(-1)
  215. # filter by matching defines
  216. if defines is not None:
  217. results_ = []
  218. for r in results:
  219. if all(getattr(r, k) in vs for k, vs in defines):
  220. results_.append(r)
  221. results = results_
  222. # organize results into conflicts
  223. folding = co.OrderedDict()
  224. for r in results:
  225. name = tuple(getattr(r, k) for k in by)
  226. if name not in folding:
  227. folding[name] = []
  228. folding[name].append(r)
  229. # merge conflicts
  230. folded = []
  231. for name, rs in folding.items():
  232. folded.append(sum(rs[1:], start=rs[0]))
  233. return folded
  234. def table(Result, results, diff_results=None, *,
  235. by=None,
  236. fields=None,
  237. sort=None,
  238. summary=False,
  239. all=False,
  240. percent=False,
  241. tree=False,
  242. depth=None,
  243. **_):
  244. all_, all = all, __builtins__.all
  245. if by is None:
  246. by = Result._by
  247. if fields is None:
  248. fields = Result._fields
  249. types = Result._types
  250. # fold again
  251. results = fold(Result, results, by=by)
  252. if diff_results is not None:
  253. diff_results = fold(Result, diff_results, by=by)
  254. # organize by name
  255. table = {
  256. ','.join(str(getattr(r, k) or '') for k in by): r
  257. for r in results}
  258. diff_table = {
  259. ','.join(str(getattr(r, k) or '') for k in by): r
  260. for r in diff_results or []}
  261. names = list(table.keys() | diff_table.keys())
  262. # sort again, now with diff info, note that python's sort is stable
  263. names.sort()
  264. if diff_results is not None:
  265. names.sort(key=lambda n: tuple(
  266. types[k].ratio(
  267. getattr(table.get(n), k, None),
  268. getattr(diff_table.get(n), k, None))
  269. for k in fields),
  270. reverse=True)
  271. if sort:
  272. for k, reverse in reversed(sort):
  273. names.sort(key=lambda n: (getattr(table[n], k),)
  274. if getattr(table.get(n), k, None) is not None else (),
  275. reverse=reverse ^ (not k or k in Result._fields))
  276. # build up our lines
  277. lines = []
  278. # header
  279. line = []
  280. line.append('%s%s' % (
  281. ','.join(by),
  282. ' (%d added, %d removed)' % (
  283. sum(1 for n in table if n not in diff_table),
  284. sum(1 for n in diff_table if n not in table))
  285. if diff_results is not None and not percent else '')
  286. if not summary else '')
  287. if diff_results is None:
  288. for k in fields:
  289. line.append(k)
  290. elif percent:
  291. for k in fields:
  292. line.append(k)
  293. else:
  294. for k in fields:
  295. line.append('o'+k)
  296. for k in fields:
  297. line.append('n'+k)
  298. for k in fields:
  299. line.append('d'+k)
  300. line.append('')
  301. lines.append(line)
  302. # entries
  303. if not summary:
  304. for name in names:
  305. r = table.get(name)
  306. if diff_results is not None:
  307. diff_r = diff_table.get(name)
  308. ratios = [
  309. types[k].ratio(
  310. getattr(r, k, None),
  311. getattr(diff_r, k, None))
  312. for k in fields]
  313. if not any(ratios) and not all_:
  314. continue
  315. line = []
  316. line.append(name)
  317. if diff_results is None:
  318. for k in fields:
  319. line.append(getattr(r, k).table()
  320. if getattr(r, k, None) is not None
  321. else types[k].none)
  322. elif percent:
  323. for k in fields:
  324. line.append(getattr(r, k).diff_table()
  325. if getattr(r, k, None) is not None
  326. else types[k].diff_none)
  327. else:
  328. for k in fields:
  329. line.append(getattr(diff_r, k).diff_table()
  330. if getattr(diff_r, k, None) is not None
  331. else types[k].diff_none)
  332. for k in fields:
  333. line.append(getattr(r, k).diff_table()
  334. if getattr(r, k, None) is not None
  335. else types[k].diff_none)
  336. for k in fields:
  337. line.append(types[k].diff_diff(
  338. getattr(r, k, None),
  339. getattr(diff_r, k, None)))
  340. if diff_results is None:
  341. line.append('')
  342. elif percent:
  343. line.append(' (%s)' % ', '.join(
  344. '+∞%' if t == +m.inf
  345. else '-∞%' if t == -m.inf
  346. else '%+.1f%%' % (100*t)
  347. for t in ratios))
  348. else:
  349. line.append(' (%s)' % ', '.join(
  350. '+∞%' if t == +m.inf
  351. else '-∞%' if t == -m.inf
  352. else '%+.1f%%' % (100*t)
  353. for t in ratios
  354. if t)
  355. if any(ratios) else '')
  356. lines.append(line)
  357. # total
  358. r = next(iter(fold(Result, results, by=[])), None)
  359. if diff_results is not None:
  360. diff_r = next(iter(fold(Result, diff_results, by=[])), None)
  361. ratios = [
  362. types[k].ratio(
  363. getattr(r, k, None),
  364. getattr(diff_r, k, None))
  365. for k in fields]
  366. line = []
  367. line.append('TOTAL')
  368. if diff_results is None:
  369. for k in fields:
  370. line.append(getattr(r, k).table()
  371. if getattr(r, k, None) is not None
  372. else types[k].none)
  373. elif percent:
  374. for k in fields:
  375. line.append(getattr(r, k).diff_table()
  376. if getattr(r, k, None) is not None
  377. else types[k].diff_none)
  378. else:
  379. for k in fields:
  380. line.append(getattr(diff_r, k).diff_table()
  381. if getattr(diff_r, k, None) is not None
  382. else types[k].diff_none)
  383. for k in fields:
  384. line.append(getattr(r, k).diff_table()
  385. if getattr(r, k, None) is not None
  386. else types[k].diff_none)
  387. for k in fields:
  388. line.append(types[k].diff_diff(
  389. getattr(r, k, None),
  390. getattr(diff_r, k, None)))
  391. if diff_results is None:
  392. line.append('')
  393. elif percent:
  394. line.append(' (%s)' % ', '.join(
  395. '+∞%' if t == +m.inf
  396. else '-∞%' if t == -m.inf
  397. else '%+.1f%%' % (100*t)
  398. for t in ratios))
  399. else:
  400. line.append(' (%s)' % ', '.join(
  401. '+∞%' if t == +m.inf
  402. else '-∞%' if t == -m.inf
  403. else '%+.1f%%' % (100*t)
  404. for t in ratios
  405. if t)
  406. if any(ratios) else '')
  407. lines.append(line)
  408. # find the best widths, note that column 0 contains the names and column -1
  409. # the ratios, so those are handled a bit differently
  410. widths = [
  411. ((max(it.chain([w], (len(l[i]) for l in lines)))+1+4-1)//4)*4-1
  412. for w, i in zip(
  413. it.chain([23], it.repeat(7)),
  414. range(len(lines[0])-1))]
  415. # adjust the name width based on the expected call depth, though
  416. # note this doesn't really work with unbounded recursion
  417. if not summary:
  418. # it doesn't really make sense to not have a depth with tree,
  419. # so assume depth=inf if tree by default
  420. if depth is None:
  421. depth = m.inf if tree else 0
  422. elif depth == 0:
  423. depth = m.inf
  424. if not m.isinf(depth):
  425. widths[0] += 4*depth
  426. # print our table with optional call info
  427. #
  428. # note we try to adjust our name width based on expected call depth, but
  429. # this doesn't work if there's unbounded recursion
  430. if not tree:
  431. print('%-*s %s%s' % (
  432. widths[0], lines[0][0],
  433. ' '.join('%*s' % (w, x)
  434. for w, x, in zip(widths[1:], lines[0][1:-1])),
  435. lines[0][-1]))
  436. # print the tree recursively
  437. if not summary:
  438. line_table = {n: l for n, l in zip(names, lines[1:-1])}
  439. def recurse(names_, depth_, prefixes=('', '', '', '')):
  440. for i, name in enumerate(names_):
  441. if name not in line_table:
  442. continue
  443. line = line_table[name]
  444. is_last = (i == len(names_)-1)
  445. print('%s%-*s ' % (
  446. prefixes[0+is_last],
  447. widths[0] - (
  448. len(prefixes[0+is_last])
  449. if not m.isinf(depth) else 0),
  450. line[0]),
  451. end='')
  452. if not tree:
  453. print(' %s%s' % (
  454. ' '.join('%*s' % (w, x)
  455. for w, x, in zip(widths[1:], line[1:-1])),
  456. line[-1]),
  457. end='')
  458. print()
  459. # recurse?
  460. if name in table and depth_ > 0:
  461. calls = {
  462. ','.join(str(getattr(Result(*c), k) or '') for k in by)
  463. for c in table[name].calls}
  464. recurse(
  465. # note we're maintaining sort order
  466. [n for n in names if n in calls],
  467. depth_-1,
  468. (prefixes[2+is_last] + "|-> ",
  469. prefixes[2+is_last] + "'-> ",
  470. prefixes[2+is_last] + "| ",
  471. prefixes[2+is_last] + " "))
  472. recurse(names, depth)
  473. if not tree:
  474. print('%-*s %s%s' % (
  475. widths[0], lines[-1][0],
  476. ' '.join('%*s' % (w, x)
  477. for w, x, in zip(widths[1:], lines[-1][1:-1])),
  478. lines[-1][-1]))
  479. def main(ci_paths,
  480. by=None,
  481. fields=None,
  482. defines=None,
  483. sort=None,
  484. **args):
  485. # find sizes
  486. if not args.get('use', None):
  487. # find .ci files
  488. paths = []
  489. for path in ci_paths:
  490. if os.path.isdir(path):
  491. path = path + '/*.ci'
  492. for path in glob.glob(path):
  493. paths.append(path)
  494. if not paths:
  495. print("error: no .ci files found in %r?" % ci_paths)
  496. sys.exit(-1)
  497. results = collect(paths, **args)
  498. else:
  499. results = []
  500. with openio(args['use']) as f:
  501. reader = csv.DictReader(f, restval='')
  502. for r in reader:
  503. try:
  504. results.append(StackResult(
  505. **{k: r[k] for k in StackResult._by
  506. if k in r and r[k].strip()},
  507. **{k: r['stack_'+k] for k in StackResult._fields
  508. if 'stack_'+k in r and r['stack_'+k].strip()}))
  509. except TypeError:
  510. pass
  511. # fold
  512. results = fold(StackResult, results, by=by, defines=defines)
  513. # sort, note that python's sort is stable
  514. results.sort()
  515. if sort:
  516. for k, reverse in reversed(sort):
  517. results.sort(key=lambda r: (getattr(r, k),)
  518. if getattr(r, k) is not None else (),
  519. reverse=reverse ^ (not k or k in StackResult._fields))
  520. # write results to CSV
  521. if args.get('output'):
  522. with openio(args['output'], 'w') as f:
  523. writer = csv.DictWriter(f, StackResult._by
  524. + ['stack_'+k for k in StackResult._fields])
  525. writer.writeheader()
  526. for r in results:
  527. writer.writerow(
  528. {k: getattr(r, k) for k in StackResult._by}
  529. | {'stack_'+k: getattr(r, k) for k in StackResult._fields})
  530. # find previous results?
  531. if args.get('diff'):
  532. diff_results = []
  533. try:
  534. with openio(args['diff']) as f:
  535. reader = csv.DictReader(f, restval='')
  536. for r in reader:
  537. try:
  538. diff_results.append(StackResult(
  539. **{k: r[k] for k in StackResult._by
  540. if k in r and r[k].strip()},
  541. **{k: r['stack_'+k] for k in StackResult._fields
  542. if 'stack_'+k in r and r['stack_'+k].strip()}))
  543. except TypeError:
  544. raise
  545. except FileNotFoundError:
  546. pass
  547. # fold
  548. diff_results = fold(StackResult, diff_results, by=by, defines=defines)
  549. # print table
  550. if not args.get('quiet'):
  551. table(StackResult, results,
  552. diff_results if args.get('diff') else None,
  553. by=by if by is not None else ['function'],
  554. fields=fields,
  555. sort=sort,
  556. **args)
  557. # error on recursion
  558. if args.get('error_on_recursion') and any(
  559. m.isinf(float(r.limit)) for r in results):
  560. sys.exit(2)
  561. if __name__ == "__main__":
  562. import argparse
  563. import sys
  564. parser = argparse.ArgumentParser(
  565. description="Find stack usage at the function level.")
  566. parser.add_argument(
  567. 'ci_paths',
  568. nargs='*',
  569. default=CI_PATHS,
  570. help="Description of where to find *.ci files. May be a directory "
  571. "or a list of paths. Defaults to %r." % CI_PATHS)
  572. parser.add_argument(
  573. '-v', '--verbose',
  574. action='store_true',
  575. help="Output commands that run behind the scenes.")
  576. parser.add_argument(
  577. '-q', '--quiet',
  578. action='store_true',
  579. help="Don't show anything, useful with -o.")
  580. parser.add_argument(
  581. '-o', '--output',
  582. help="Specify CSV file to store results.")
  583. parser.add_argument(
  584. '-u', '--use',
  585. help="Don't parse anything, use this CSV file.")
  586. parser.add_argument(
  587. '-d', '--diff',
  588. help="Specify CSV file to diff against.")
  589. parser.add_argument(
  590. '-a', '--all',
  591. action='store_true',
  592. help="Show all, not just the ones that changed.")
  593. parser.add_argument(
  594. '-p', '--percent',
  595. action='store_true',
  596. help="Only show percentage change, not a full diff.")
  597. parser.add_argument(
  598. '-b', '--by',
  599. action='append',
  600. choices=StackResult._by,
  601. help="Group by this field.")
  602. parser.add_argument(
  603. '-f', '--field',
  604. dest='fields',
  605. action='append',
  606. choices=StackResult._fields,
  607. help="Show this field.")
  608. parser.add_argument(
  609. '-D', '--define',
  610. dest='defines',
  611. action='append',
  612. type=lambda x: (lambda k,v: (k, set(v.split(','))))(*x.split('=', 1)),
  613. help="Only include results where this field is this value.")
  614. class AppendSort(argparse.Action):
  615. def __call__(self, parser, namespace, value, option):
  616. if namespace.sort is None:
  617. namespace.sort = []
  618. namespace.sort.append((value, True if option == '-S' else False))
  619. parser.add_argument(
  620. '-s', '--sort',
  621. action=AppendSort,
  622. help="Sort by this fields.")
  623. parser.add_argument(
  624. '-S', '--reverse-sort',
  625. action=AppendSort,
  626. help="Sort by this fields, but backwards.")
  627. parser.add_argument(
  628. '-Y', '--summary',
  629. action='store_true',
  630. help="Only show the total.")
  631. parser.add_argument(
  632. '-A', '--everything',
  633. action='store_true',
  634. help="Include builtin and libc specific symbols.")
  635. parser.add_argument(
  636. '--tree',
  637. action='store_true',
  638. help="Only show the function call tree.")
  639. parser.add_argument(
  640. '-L', '--depth',
  641. nargs='?',
  642. type=lambda x: int(x, 0),
  643. const=0,
  644. help="Depth of function calls to show. 0 show all calls but may not "
  645. "terminate!")
  646. parser.add_argument(
  647. '-e', '--error-on-recursion',
  648. action='store_true',
  649. help="Error if any functions are recursive.")
  650. parser.add_argument(
  651. '--build-dir',
  652. help="Specify the relative build directory. Used to map object files "
  653. "to the correct source files.")
  654. sys.exit(main(**{k: v
  655. for k, v in vars(parser.parse_intermixed_args()).items()
  656. if v is not None}))