calls.py 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170
  1. #!/usr/bin/env python3
  2. #
  3. # Script to show the callgraph in a human readable manner. Basically just a
  4. # wrapper aroung GCC's -fcallgraph-info flag.
  5. #
  6. import os
  7. import glob
  8. import itertools as it
  9. import re
  10. import csv
  11. import collections as co
  12. CI_PATHS = ['*.ci']
  13. def collect(paths, **args):
  14. # parse the vcg format
  15. k_pattern = re.compile('([a-z]+)\s*:', re.DOTALL)
  16. v_pattern = re.compile('(?:"(.*?)"|([a-z]+))', re.DOTALL)
  17. def parse_vcg(rest):
  18. def parse_vcg(rest):
  19. node = []
  20. while True:
  21. rest = rest.lstrip()
  22. m = k_pattern.match(rest)
  23. if not m:
  24. return (node, rest)
  25. k, rest = m.group(1), rest[m.end(0):]
  26. rest = rest.lstrip()
  27. if rest.startswith('{'):
  28. v, rest = parse_vcg(rest[1:])
  29. assert rest[0] == '}', "unexpected %r" % rest[0:1]
  30. rest = rest[1:]
  31. node.append((k, v))
  32. else:
  33. m = v_pattern.match(rest)
  34. assert m, "unexpected %r" % rest[0:1]
  35. v, rest = m.group(1) or m.group(2), rest[m.end(0):]
  36. node.append((k, v))
  37. node, rest = parse_vcg(rest)
  38. assert rest == '', "unexpected %r" % rest[0:1]
  39. return node
  40. # collect into functions
  41. results = co.defaultdict(lambda: (None, None, set()))
  42. f_pattern = re.compile(r'([^\\]*)\\n([^:]*)')
  43. for path in paths:
  44. with open(path) as f:
  45. vcg = parse_vcg(f.read())
  46. for k, graph in vcg:
  47. if k != 'graph':
  48. continue
  49. for k, info in graph:
  50. if k == 'node':
  51. info = dict(info)
  52. m = f_pattern.match(info['label'])
  53. if m:
  54. function, file = m.groups()
  55. _, _, targets = results[info['title']]
  56. results[info['title']] = (file, function, targets)
  57. elif k == 'edge':
  58. info = dict(info)
  59. _, _, targets = results[info['sourcename']]
  60. targets.add(info['targetname'])
  61. else:
  62. continue
  63. if not args.get('everything'):
  64. for source, (s_file, s_function, _) in list(results.items()):
  65. # discard internal functions
  66. if s_file.startswith('<') or s_file.startswith('/usr/include'):
  67. del results[source]
  68. # flatten into a list
  69. flat_results = []
  70. for _, (s_file, s_function, targets) in results.items():
  71. for target in targets:
  72. if target not in results:
  73. continue
  74. t_file, t_function, _ = results[target]
  75. flat_results.append((s_file, s_function, t_file, t_function))
  76. return flat_results
  77. def main(**args):
  78. # find sizes
  79. if not args.get('use', None):
  80. # find .ci files
  81. paths = []
  82. for path in args['ci_paths']:
  83. if os.path.isdir(path):
  84. path = path + '/*.ci'
  85. for path in glob.glob(path):
  86. paths.append(path)
  87. if not paths:
  88. print('no .ci files found in %r?' % args['ci_paths'])
  89. sys.exit(-1)
  90. results = collect(paths, **args)
  91. else:
  92. with open(args['use']) as f:
  93. r = csv.DictReader(f)
  94. results = [
  95. ( result['file'],
  96. result['function'],
  97. result['callee_file'],
  98. result['callee_function'])
  99. for result in r]
  100. # write results to CSV
  101. if args.get('output'):
  102. with open(args['output'], 'w') as f:
  103. w = csv.writer(f)
  104. w.writerow(['file', 'function', 'callee_file', 'callee_function'])
  105. for file, func, c_file, c_func in sorted(results):
  106. w.writerow((file, func, c_file, c_func))
  107. # print results
  108. def dedup_entries(results, by='function'):
  109. entries = co.defaultdict(lambda: set())
  110. for file, func, c_file, c_func in results:
  111. entry = (file if by == 'file' else func)
  112. entries[entry].add(c_file if by == 'file' else c_func)
  113. return entries
  114. def print_entries(by='function'):
  115. entries = dedup_entries(results, by=by)
  116. for name, callees in sorted(entries.items()):
  117. print(name)
  118. for i, c_name in enumerate(sorted(callees)):
  119. print(" -> %s" % c_name)
  120. if args.get('quiet'):
  121. pass
  122. elif args.get('files'):
  123. print_entries(by='file')
  124. else:
  125. print_entries(by='function')
  126. if __name__ == "__main__":
  127. import argparse
  128. import sys
  129. parser = argparse.ArgumentParser(
  130. description="Find code size at the function level.")
  131. parser.add_argument('ci_paths', nargs='*', default=CI_PATHS,
  132. help="Description of where to find *.ci files. May be a directory \
  133. or a list of paths. Defaults to %r." % CI_PATHS)
  134. parser.add_argument('-v', '--verbose', action='store_true',
  135. help="Output commands that run behind the scenes.")
  136. parser.add_argument('-o', '--output',
  137. help="Specify CSV file to store results.")
  138. parser.add_argument('-u', '--use',
  139. help="Don't parse callgraph files, instead use this CSV file.")
  140. parser.add_argument('-A', '--everything', action='store_true',
  141. help="Include builtin and libc specific symbols.")
  142. parser.add_argument('--files', action='store_true',
  143. help="Show file-level calls.")
  144. parser.add_argument('-q', '--quiet', action='store_true',
  145. help="Don't show anything, useful with -o.")
  146. parser.add_argument('--build-dir',
  147. help="Specify the relative build directory. Used to map object files \
  148. to the correct source files.")
  149. sys.exit(main(**vars(parser.parse_args())))