4 U{http://pycallgraph.slowchop.com/}
6 Copyright Gerald Kaszuba 2007
8 This program is free software; you can redistribute it and/or modify
9 it under the terms of the GNU General Public License as published by
10 the Free Software Foundation; either version 2 of the License, or
11 (at your option) any later version.
13 This program is distributed in the hope that it will be useful,
14 but WITHOUT ANY WARRANTY; without even the implied warranty of
15 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 GNU General Public License for more details.
18 You should have received a copy of the GNU General Public License
19 along with this program; if not, write to the Free Software
20 Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
24 __author__
= 'Gerald Kaszuba'
32 from distutils
import sysconfig
34 # Initialise module variables.
35 # TODO Move these into settings
40 def colourize_node(calls
, total_time
):
41 value
= float(total_time
* 2 + calls
) / 3
42 return '%f %f %f' % (value
/ 2 + .5, value
, 0.9)
45 def colourize_edge(calls
, total_time
):
46 value
= float(total_time
* 2 + calls
) / 3
47 return '%f %f %f' % (value
/ 2 + .5, value
, 0.7)
52 global graph_attributes
57 'label': r
'%(func)s\ncalls: %(hits)i\ntotal time: %(total_time)f',
60 'node_colour': colourize_node
,
61 'edge_colour': colourize_edge
,
62 'dont_exclude_anything': False,
63 'include_stdlib': True,
66 # TODO: Move this into settings
69 'fontname': 'Verdana',
71 'fontcolor': '0 0 0.5',
72 'label': r
'Generated by Python Call Graph v%s\n' \
73 r
'http://pycallgraph.slowchop.com' % __version__
,
76 'fontname': 'Verdana',
86 """Resets all collected statistics. This is run automatically by
87 start_trace(reset=True) and when the module is loaded.
95 global call_stack_timer
100 call_stack
= ['__main__']
102 # counters for each function
106 # accumative time per function
110 # keeps track of the start time of each call on the stack
111 call_stack_timer
= []
114 class PyCallGraphException(Exception):
115 """Exception used for pycallgraph"""
119 class GlobbingFilter(object):
120 """Filter module names using a set of globs.
122 Objects are matched against the exclude list first, then the include list.
123 Anything that passes through without matching either, is excluded.
126 def __init__(self
, include
=None, exclude
=None, max_depth
=None,
128 if include
is None and exclude
is None:
131 elif include
is None:
133 elif exclude
is None:
135 self
.include
= include
136 self
.exclude
= exclude
137 self
.max_depth
= max_depth
or 9999
138 self
.min_depth
= min_depth
or 0
140 def __call__(self
, stack
, module_name
=None, class_name
=None,
141 func_name
=None, full_name
=None):
142 from fnmatch
import fnmatch
143 if len(stack
) > self
.max_depth
:
145 if len(stack
) < self
.min_depth
:
147 for pattern
in self
.exclude
:
148 if fnmatch(full_name
, pattern
):
150 for pattern
in self
.include
:
151 if fnmatch(full_name
, pattern
):
156 def is_module_stdlib(file_name
):
157 """Returns True if the file_name is in the lib directory."""
158 # TODO: Move these calls away from this function so it doesn't have to run
160 lib_path
= sysconfig
.get_python_lib()
161 path
= os
.path
.split(lib_path
)
162 if path
[1] == 'site-packages':
164 return file_name
.lower().startswith(lib_path
.lower())
167 def start_trace(reset
=True, filter_func
=None, time_filter_func
=None):
168 """Begins a trace. Setting reset to True will reset all previously recorded
169 trace data. filter_func needs to point to a callable function that accepts
170 the parameters (call_stack, module_name, class_name, func_name, full_name).
171 Every call will be passed into this function and it is up to the function
172 to decide if it should be included or not. Returning False means the call
173 will be filtered out and not included in the call graph.
181 trace_filter
= filter_func
183 trace_filter
= GlobbingFilter(exclude
=['pycallgraph.*'])
186 time_filter
= time_filter_func
188 time_filter
= GlobbingFilter()
194 """Stops the currently running trace, if any."""
198 def tracer(frame
, event
, arg
):
199 """This is an internal function that is called every time a call is made
200 during a trace. It keeps track of relationships between calls.
202 global func_count_max
214 # Stores all the parts of a human readable name of the current call.
217 # Work out the module name
218 module
= inspect
.getmodule(code
)
220 module_name
= module
.__name
__
221 module_path
= module
.__file
__
222 if not settings
['include_stdlib'] \
223 and is_module_stdlib(module_path
):
225 if module_name
== '__main__':
230 full_name_list
.append(module_name
)
232 # Work out the class name.
234 class_name
= frame
.f_locals
['self'].__class
__.__name
__
235 full_name_list
.append(class_name
)
236 except (KeyError, AttributeError):
239 # Work out the current function or method
240 func_name
= code
.co_name
242 func_name
= '__main__'
243 full_name_list
.append(func_name
)
245 # Create a readable representation of the current call
246 full_name
= '.'.join(full_name_list
)
248 # Load the trace filter, if any. 'keep' determines if we should ignore
250 if keep
and trace_filter
:
251 keep
= trace_filter(call_stack
, module_name
, class_name
,
252 func_name
, full_name
)
254 # Store the call information
258 if fr
not in call_dict
:
260 if full_name
not in call_dict
[fr
]:
261 call_dict
[fr
][full_name
] = 0
262 call_dict
[fr
][full_name
] += 1
264 if full_name
not in func_count
:
265 func_count
[full_name
] = 0
266 func_count
[full_name
] += 1
267 if func_count
[full_name
] > func_count_max
:
268 func_count_max
= func_count
[full_name
]
270 call_stack
.append(full_name
)
271 call_stack_timer
.append(time
.time())
274 call_stack
.append('')
275 call_stack_timer
.append(None)
277 if event
== 'return':
279 full_name
= call_stack
.pop(-1)
280 t
= call_stack_timer
.pop(-1)
281 if t
and time_filter(stack
=call_stack
, full_name
=full_name
):
282 if full_name
not in func_time
:
283 func_time
[full_name
] = 0
284 call_time
= (time
.time() - t
)
285 func_time
[full_name
] += call_time
286 if func_time
[full_name
] > func_time_max
:
287 func_time_max
= func_time
[full_name
]
292 def get_dot(stop
=True):
293 """Returns a string containing a DOT file. Setting stop to True will cause
298 def frac_calculation(func
, count
):
299 global func_count_max
302 calls_frac
= float(count
) / func_count_max
304 total_time
= func_time
[func
]
308 total_time_frac
= float(total_time
) / func_time_max
311 return calls_frac
, total_time_frac
, total_time
315 ret
= ['digraph G {', ]
316 for comp
, comp_attr
in graph_attributes
.items():
317 ret
.append('%s [' % comp
)
318 for attr
, val
in comp_attr
.items():
319 ret
.append('%(attr)s = "%(val)s",' % locals())
321 for func
, hits
in func_count
.items():
322 calls_frac
, total_time_frac
, total_time
= frac_calculation(func
, hits
)
323 col
= settings
['node_colour'](calls_frac
, total_time_frac
)
324 attribs
= ['%s="%s"' % a
for a
in settings
['node_attributes'].items()]
325 node_str
= '"%s" [%s];' % (func
, ','.join(attribs
))
326 ret
.append(node_str
% locals())
327 for fr_key
, fr_val
in call_dict
.items():
330 for to_key
, to_val
in fr_val
.items():
331 calls_frac
, total_time_frac
, totla_time
= \
332 frac_calculation(to_key
, to_val
)
333 col
= settings
['edge_colour'](calls_frac
, total_time_frac
)
334 edge
= '[ color = "%s" ]' % col
335 ret
.append('"%s"->"%s" %s' % (fr_key
, to_key
, edge
))
341 def save_dot(filename
):
342 """Generates a DOT file and writes it into filename."""
343 open(filename
, 'w').write(get_dot())
346 def make_graph(filename
, format
=None, tool
=None, stop
=None):
347 """This has been changed to make_dot_graph."""
348 raise PyCallGraphException( \
349 'make_graph is depricated. Please use make_dot_graph')
352 def make_dot_graph(filename
, format
='png', tool
='dot', stop
=True):
353 """Creates a graph using a Graphviz tool that supports the dot language. It
354 will output into a file specified by filename with the format specified.
355 Setting stop to True will stop the current trace.
360 # create a temporary file to be used for the dot data
361 fd
, tempname
= tempfile
.mkstemp()
362 f
= os
.fdopen(fd
, 'w')
367 regex_user_expand
= re
.compile('\A~')
368 if regex_user_expand
.match(filename
):
369 filename
= os
.path
.expanduser(filename
)
371 filename
= os
.path
.expandvars(filename
) # expand, just in case
373 cmd
= '%(tool)s -T%(format)s -o%(filename)s %(tempname)s' % locals()
377 raise PyCallGraphException( \
378 'The command "%(cmd)s" failed with error ' \
379 'code %(ret)i.' % locals())
384 def simple_memoize(callable_object
):
385 """Simple memoization for functions without keyword arguments.
387 This is useful for mapping code objects to module in this context.
388 inspect.getmodule() requires a number of system calls, which may slow down
389 the tracing considerably. Caching the mapping from code objects (there is
390 *one* code object for each function, regardless of how many simultaneous
391 activations records there are).
393 In this context we can ignore keyword arguments, but a generic memoizer
394 ought to take care of that as well.
399 if rest
not in cache
:
400 cache
[rest
] = callable_object(*rest
)
407 graph_attributes
= {}
410 inspect
.getmodule
= simple_memoize(inspect
.getmodule
)