More updates towards supporting function documentation:
[scons.git] / bin / caller-tree.py
1 #!/usr/bin/env python
2 #
3 # Quick script to process the *summary* output from SCons.Debug.caller()
4 # and print indented calling trees with call counts.
5 #
6 # The way to use this is to add something like the following to a function
7 # for which you want information about who calls it and how many times:
8 #
9 #       from SCons.Debug import caller
10 #       caller(0, 1, 2, 3, 4, 5)
11 #
12 # Each integer represents how many stack frames back SCons will go
13 # and capture the calling information, so in the above example it will
14 # capture the calls six levels up the stack in a central dictionary.
15 #
16 # At the end of any run where SCons.Debug.caller() is used, SCons will
17 # print a summary of the calls and counts that looks like the following:
18 #
19 #       Callers of Node/__init__.py:629(calc_signature):
20 #                1 Node/__init__.py:683(calc_signature)
21 #       Callers of Node/__init__.py:676(gen_binfo):
22 #                6 Node/FS.py:2035(current)
23 #                1 Node/__init__.py:722(get_bsig)
24 #
25 # If you cut-and-paste that summary output and feed it to this script
26 # on standard input, it will figure out how these entries hook up and
27 # print a calling tree for each one looking something like:
28 #
29 #   Node/__init__.py:676(gen_binfo)
30 #     Node/FS.py:2035(current)                                           6
31 #       Taskmaster.py:253(make_ready_current)                           18
32 #         Script/Main.py:201(make_ready)                                18
33 #
34 # Note that you should *not* look at the call-count numbers in the right
35 # hand column as the actual number of times each line *was called by*
36 # the function on the next line.  Rather, it's the *total* number
37 # of times each function was found in the call chain for any of the
38 # calls to SCons.Debug.caller().  If you're looking at more than one
39 # function at the same time, for example, their counts will intermix.
40 # So use this to get a *general* idea of who's calling what, not for
41 # fine-grained performance tuning.
42
43 import sys
44
45 class Entry:
46     def __init__(self, file_line_func):
47         self.file_line_func = file_line_func
48         self.called_by = []
49         self.calls = []
50
51 AllCalls = {}
52
53 def get_call(flf):
54     try:
55         e = AllCalls[flf]
56     except KeyError:
57         e = AllCalls[flf] = Entry(flf)
58     return e
59
60 prefix = 'Callers of '
61
62 c = None
63 for line in sys.stdin.readlines():
64     if line[0] == '#':
65         pass
66     elif line[:len(prefix)] == prefix:
67         c = get_call(line[len(prefix):-2])
68     else:
69         num_calls, flf = line.strip().split()
70         e = get_call(flf)
71         c.called_by.append((e, num_calls))
72         e.calls.append(c)
73
74 stack = []
75
76 def print_entry(e, level, calls):
77     print '%-72s%6s' % ((' '*2*level) + e.file_line_func, calls)
78     if e in stack:
79         print (' '*2*(level+1))+'RECURSION'
80         print
81     elif e.called_by:
82         stack.append(e)
83         for c in e.called_by:
84             print_entry(c[0], level+1, c[1])
85         stack.pop()
86     else:
87         print
88
89 for e in [ e for e in AllCalls.values() if not e.calls ]:
90     print_entry(e, 0, '')
91
92 # Local Variables:
93 # tab-width:4
94 # indent-tabs-mode:nil
95 # End:
96 # vim: set expandtab tabstop=4 shiftwidth=4: