1 """Class for printing reports on profiled python code."""
3 # Class for printing reports on profiled python code. rev 1.0 4/1/94
5 # Based on prior profile module by Sjoerd Mullender...
6 # which was hacked somewhat by: Guido van Rossum
8 # see profile.doc and profile.py for more info.
10 # Copyright 1994, by InfoSeek Corporation, all rights reserved.
11 # Written by James Roskind
13 # Permission to use, copy, modify, and distribute this Python software
14 # and its associated documentation for any purpose (subject to the
15 # restriction in the following sentence) without fee is hereby granted,
16 # provided that the above copyright notice appears in all copies, and
17 # that both that copyright notice and this permission notice appear in
18 # supporting documentation, and that the name of InfoSeek not be used in
19 # advertising or publicity pertaining to distribution of the software
20 # without specific, written prior permission. This permission is
21 # explicitly restricted to the copying and modification of the software
22 # to remain in Python, compiled Python, or other languages (such as C)
23 # wherein the modified or derived code is exclusively imported into a
26 # INFOSEEK CORPORATION DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS
27 # SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
28 # FITNESS. IN NO EVENT SHALL INFOSEEK CORPORATION BE LIABLE FOR ANY
29 # SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER
30 # RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF
31 # CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
32 # CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
43 """This class is used for creating reports from data generated by the
44 Profile class. It is a "friend" of that class, and imports data either
45 by direct access to members of Profile class, or by reading in a dictionary
46 that was emitted (via marshal) from the Profile class.
48 The big change from the previous Profiler (in terms of raw functionality)
49 is that an "add()" method has been provided to combine Stats from
50 several distinct profile runs. Both the constructor and the add()
51 method now take arbitrarily many file names as arguments.
53 All the print methods now take an argument that indicates how many lines
54 to print. If the arg is a floating point number between 0 and 1.0, then
55 it is taken as a decimal percentage of the available lines to be printed
56 (e.g., .1 means print 10% of all available lines). If it is an integer,
57 it is taken to mean the number of lines of data that you wish to have
60 The sort_stats() method now processes some additional options (i.e., in
61 addition to the old -1, 0, 1, or 2). It takes an arbitrary number of quoted
62 strings to select the sort order. For example sort_stats('time', 'name')
63 sorts on the major key of "internal function time", and on the minor
64 key of 'the name of the function'. Look at the two tables in sort_stats()
65 and get_sort_arg_defs(self) for more examples.
67 All methods now return "self", so you can string together commands like:
68 Stats('foo', 'goo').strip_dirs().sort_stats('calls').\
69 print_stats(5).print_callers(5)
72 def __init__(self
, *args
):
82 self
.all_callees
= None # calc only if needed
91 self
.sort_arg_dict
= {}
95 self
.get_top_level_stats()
99 print "Invalid timing data",
100 if self
.files
: print self
.files
[-1],
103 def load_stats(self
, arg
):
104 if not arg
: self
.stats
= {}
105 elif type(arg
) == type(""):
107 self
.stats
= marshal
.load(f
)
110 file_stats
= os
.stat(arg
)
111 arg
= time
.ctime(file_stats
.st_mtime
) + " " + arg
112 except: # in case this is not unix
115 elif hasattr(arg
, 'create_stats'):
117 self
.stats
= arg
.stats
120 raise TypeError, "Cannot create or construct a %r object from '%r''" % (
124 def get_top_level_stats(self
):
125 for func
, (cc
, nc
, tt
, ct
, callers
) in self
.stats
.items():
126 self
.total_calls
+= nc
127 self
.prim_calls
+= cc
129 if callers
.has_key(("jprofile", 0, "profiler")):
130 self
.top_level
[func
] = None
131 if len(func_std_string(func
)) > self
.max_name_len
:
132 self
.max_name_len
= len(func_std_string(func
))
134 def add(self
, *arg_list
):
135 if not arg_list
: return self
136 if len(arg_list
) > 1: self
.add(*arg_list
[1:])
138 if type(self
) != type(other
) or self
.__class
__ != other
.__class
__:
140 self
.files
+= other
.files
141 self
.total_calls
+= other
.total_calls
142 self
.prim_calls
+= other
.prim_calls
143 self
.total_tt
+= other
.total_tt
144 for func
in other
.top_level
:
145 self
.top_level
[func
] = None
147 if self
.max_name_len
< other
.max_name_len
:
148 self
.max_name_len
= other
.max_name_len
152 for func
, stat
in other
.stats
.iteritems():
153 if func
in self
.stats
:
154 old_func_stat
= self
.stats
[func
]
156 old_func_stat
= (0, 0, 0, 0, {},)
157 self
.stats
[func
] = add_func_stats(old_func_stat
, stat
)
160 def dump_stats(self
, filename
):
161 """Write the profile data to a file we know how to load back."""
162 f
= file(filename
, 'wb')
164 marshal
.dump(self
.stats
, f
)
168 # list the tuple indices and directions for sorting,
169 # along with some printable description
170 sort_arg_dict_default
= {
171 "calls" : (((1,-1), ), "call count"),
172 "cumulative": (((3,-1), ), "cumulative time"),
173 "file" : (((4, 1), ), "file name"),
174 "line" : (((5, 1), ), "line number"),
175 "module" : (((4, 1), ), "file name"),
176 "name" : (((6, 1), ), "function name"),
177 "nfl" : (((6, 1),(4, 1),(5, 1),), "name/file/line"),
178 "pcalls" : (((0,-1), ), "call count"),
179 "stdname" : (((7, 1), ), "standard name"),
180 "time" : (((2,-1), ), "internal time"),
183 def get_sort_arg_defs(self
):
184 """Expand all abbreviations that are unique."""
185 if not self
.sort_arg_dict
:
186 self
.sort_arg_dict
= dict = {}
188 for word
, tup
in self
.sort_arg_dict_default
.iteritems():
194 bad_list
[fragment
] = 0
197 fragment
= fragment
[:-1]
198 for word
in bad_list
:
200 return self
.sort_arg_dict
202 def sort_stats(self
, *field
):
206 if len(field
) == 1 and type(field
[0]) == type(1):
207 # Be compatible with old profiler
208 field
= [ {-1: "stdname",
211 2: "cumulative" } [ field
[0] ] ]
213 sort_arg_defs
= self
.get_sort_arg_defs()
218 sort_tuple
= sort_tuple
+ sort_arg_defs
[word
][0]
219 self
.sort_type
+= connector
+ sort_arg_defs
[word
][1]
223 for func
, (cc
, nc
, tt
, ct
, callers
) in self
.stats
.iteritems():
224 stats_list
.append((cc
, nc
, tt
, ct
) + func
+
225 (func_std_string(func
), func
))
227 stats_list
.sort(TupleComp(sort_tuple
).compare
)
229 self
.fcn_list
= fcn_list
= []
230 for tuple in stats_list
:
231 fcn_list
.append(tuple[-1])
234 def reverse_order(self
):
236 self
.fcn_list
.reverse()
239 def strip_dirs(self
):
240 oldstats
= self
.stats
241 self
.stats
= newstats
= {}
243 for func
, (cc
, nc
, tt
, ct
, callers
) in oldstats
.iteritems():
244 newfunc
= func_strip_path(func
)
245 if len(func_std_string(newfunc
)) > max_name_len
:
246 max_name_len
= len(func_std_string(newfunc
))
248 for func2
, caller
in callers
.iteritems():
249 newcallers
[func_strip_path(func2
)] = caller
251 if newfunc
in newstats
:
252 newstats
[newfunc
] = add_func_stats(
254 (cc
, nc
, tt
, ct
, newcallers
))
256 newstats
[newfunc
] = (cc
, nc
, tt
, ct
, newcallers
)
257 old_top
= self
.top_level
258 self
.top_level
= new_top
= {}
260 new_top
[func_strip_path(func
)] = None
262 self
.max_name_len
= max_name_len
265 self
.all_callees
= None
268 def calc_callees(self
):
269 if self
.all_callees
: return
270 self
.all_callees
= all_callees
= {}
271 for func
, (cc
, nc
, tt
, ct
, callers
) in self
.stats
.iteritems():
272 if not func
in all_callees
:
273 all_callees
[func
] = {}
274 for func2
, caller
in callers
.iteritems():
275 if not func2
in all_callees
:
276 all_callees
[func2
] = {}
277 all_callees
[func2
][func
] = caller
280 #******************************************************************
281 # The following functions support actual printing of reports
282 #******************************************************************
284 # Optional "amount" is either a line count, or a percentage of lines.
286 def eval_print_amount(self
, sel
, list, msg
):
288 if type(sel
) == type(""):
291 if re
.search(sel
, func_std_string(func
)):
292 new_list
.append(func
)
295 if type(sel
) == type(1.0) and 0.0 <= sel
< 1.0:
296 count
= int(count
* sel
+ .5)
297 new_list
= list[:count
]
298 elif type(sel
) == type(1) and 0 <= sel
< count
:
300 new_list
= list[:count
]
301 if len(list) != len(new_list
):
302 msg
= msg
+ " List reduced from %r to %r due to restriction <%r>\n" % (
303 len(list), len(new_list
), sel
)
307 def get_print_list(self
, sel_list
):
308 width
= self
.max_name_len
310 list = self
.fcn_list
[:]
311 msg
= " Ordered by: " + self
.sort_type
+ '\n'
313 list = self
.stats
.keys()
314 msg
= " Random listing order was used\n"
316 for selection
in sel_list
:
317 list, msg
= self
.eval_print_amount(selection
, list, msg
)
324 if count
< len(self
.stats
):
327 if len(func_std_string(func
)) > width
:
328 width
= len(func_std_string(func
))
331 def print_stats(self
, *amount
):
332 for filename
in self
.files
:
336 for func
in self
.top_level
:
337 print indent
, func_get_function_name(func
)
339 print indent
, self
.total_calls
, "function calls",
340 if self
.total_calls
!= self
.prim_calls
:
341 print "(%d primitive calls)" % self
.prim_calls
,
342 print "in %.3f CPU seconds" % self
.total_tt
344 width
, list = self
.get_print_list(amount
)
348 self
.print_line(func
)
353 def print_callees(self
, *amount
):
354 width
, list = self
.get_print_list(amount
)
358 self
.print_call_heading(width
, "called...")
360 if func
in self
.all_callees
:
361 self
.print_call_line(width
, func
, self
.all_callees
[func
])
363 self
.print_call_line(width
, func
, {})
368 def print_callers(self
, *amount
):
369 width
, list = self
.get_print_list(amount
)
371 self
.print_call_heading(width
, "was called by...")
373 cc
, nc
, tt
, ct
, callers
= self
.stats
[func
]
374 self
.print_call_line(width
, func
, callers
, "<-")
379 def print_call_heading(self
, name_size
, column_title
):
380 print "Function ".ljust(name_size
) + column_title
381 # print sub-header only if we have new-style callers
383 for cc
, nc
, tt
, ct
, callers
in self
.stats
.itervalues():
385 value
= callers
.itervalues().next()
386 subheader
= isinstance(value
, tuple)
389 print " "*name_size
+ " ncalls tottime cumtime"
391 def print_call_line(self
, name_size
, source
, call_dict
, arrow
="->"):
392 print func_std_string(source
).ljust(name_size
) + arrow
,
396 clist
= call_dict
.keys()
400 name
= func_std_string(func
)
401 value
= call_dict
[func
]
402 if isinstance(value
, tuple):
403 nc
, cc
, tt
, ct
= value
405 substats
= '%d/%d' % (nc
, cc
)
407 substats
= '%d' % (nc
,)
408 substats
= '%s %s %s %s' % (substats
.rjust(7+2*len(indent
)),
409 f8(tt
), f8(ct
), name
)
410 left_width
= name_size
+ 1
412 substats
= '%s(%r) %s' % (name
, value
, f8(self
.stats
[func
][3]))
413 left_width
= name_size
+ 3
414 print indent
*left_width
+ substats
417 def print_title(self
):
418 print ' ncalls tottime percall cumtime percall', \
419 'filename:lineno(function)'
421 def print_line(self
, func
): # hack : should print percentages
422 cc
, nc
, tt
, ct
, callers
= self
.stats
[func
]
425 c
= c
+ '/' + str(cc
)
437 print func_std_string(func
)
440 """This class provides a generic function for comparing any two tuples.
441 Each instance records a list of tuple-indices (from most significant
442 to least significant), and sort direction (ascending or decending) for
443 each tuple-index. The compare functions can then be used as the function
444 argument to the system sort() function when a list of tuples need to be
445 sorted in the instances order."""
447 def __init__(self
, comp_select_list
):
448 self
.comp_select_list
= comp_select_list
450 def compare (self
, left
, right
):
451 for index
, direction
in self
.comp_select_list
:
460 #**************************************************************************
461 # func_name is a triple (file:string, line:int, name:string)
463 def func_strip_path(func_name
):
464 filename
, line
, name
= func_name
465 return os
.path
.basename(filename
), line
, name
467 def func_get_function_name(func
):
470 def func_std_string(func_name
): # match what old profile produced
471 if func_name
[:2] == ('~', 0):
472 # special case for built-in functions
474 if name
.startswith('<') and name
.endswith('>'):
475 return '{%s}' % name
[1:-1]
479 return "%s:%d(%s)" % func_name
481 #**************************************************************************
482 # The following functions combine statists for pairs functions.
483 # The bulk of the processing involves correctly handling "call" lists,
484 # such as callers and callees.
485 #**************************************************************************
487 def add_func_stats(target
, source
):
488 """Add together all the stats for two profile entries."""
489 cc
, nc
, tt
, ct
, callers
= source
490 t_cc
, t_nc
, t_tt
, t_ct
, t_callers
= target
491 return (cc
+t_cc
, nc
+t_nc
, tt
+t_tt
, ct
+t_ct
,
492 add_callers(t_callers
, callers
))
494 def add_callers(target
, source
):
495 """Combine two caller lists in a single list."""
497 for func
, caller
in target
.iteritems():
498 new_callers
[func
] = caller
499 for func
, caller
in source
.iteritems():
500 if func
in new_callers
:
501 new_callers
[func
] = caller
+ new_callers
[func
]
503 new_callers
[func
] = caller
506 def count_calls(callers
):
507 """Sum the caller statistics to get total number of calls received."""
509 for calls
in callers
.itervalues():
513 #**************************************************************************
514 # The following functions support printing of reports
515 #**************************************************************************
520 #**************************************************************************
521 # Statistics browser added by ESR, April 2001
522 #**************************************************************************
524 if __name__
== '__main__':
531 class ProfileBrowser(cmd
.Cmd
):
532 def __init__(self
, profile
=None):
533 cmd
.Cmd
.__init
__(self
)
535 if profile
is not None:
536 self
.stats
= Stats(profile
)
540 def generic(self
, fn
, line
):
545 processed
.append(int(term
))
551 if frac
> 1 or frac
< 0:
552 print "Fraction argument mus be in [0, 1]"
554 processed
.append(frac
)
558 processed
.append(term
)
560 getattr(self
.stats
, fn
)(*processed
)
562 print "No statistics object is loaded."
564 def generic_help(self
):
565 print "Arguments may be:"
566 print "* An integer maximum number of entries to print."
567 print "* A decimal fractional number between 0 and 1, controlling"
568 print " what fraction of selected entries to print."
569 print "* A regular expression; only entries with function names"
570 print " that match it are printed."
572 def do_add(self
, line
):
576 print "Add profile info from given file to current statistics object."
578 def do_callees(self
, line
):
579 return self
.generic('print_callees', line
)
580 def help_callees(self
):
581 print "Print callees statistics from the current stat object."
584 def do_callers(self
, line
):
585 return self
.generic('print_callers', line
)
586 def help_callers(self
):
587 print "Print callers statistics from the current stat object."
590 def do_EOF(self
, line
):
594 print "Leave the profile brower."
596 def do_quit(self
, line
):
599 print "Leave the profile brower."
601 def do_read(self
, line
):
604 self
.stats
= Stats(line
)
605 except IOError, args
:
608 self
.prompt
= line
+ "% "
609 elif len(self
.prompt
) > 2:
610 line
= self
.prompt
[-2:]
612 print "No statistics object is current -- cannot reload."
615 print "Read in profile data from a specified file."
617 def do_reverse(self
, line
):
618 self
.stats
.reverse_order()
620 def help_reverse(self
):
621 print "Reverse the sort order of the profiling report."
623 def do_sort(self
, line
):
624 abbrevs
= self
.stats
.get_sort_arg_defs()
625 if line
and not filter(lambda x
,a
=abbrevs
: x
not in a
,line
.split()):
626 self
.stats
.sort_stats(*line
.split())
628 print "Valid sort keys (unique prefixes are accepted):"
629 for (key
, value
) in Stats
.sort_arg_dict_default
.iteritems():
630 print "%s -- %s" % (key
, value
[1])
633 print "Sort profile data according to specified keys."
634 print "(Typing `sort' without arguments lists valid keys.)"
635 def complete_sort(self
, text
, *args
):
636 return [a
for a
in Stats
.sort_arg_dict_default
if a
.startswith(text
)]
638 def do_stats(self
, line
):
639 return self
.generic('print_stats', line
)
640 def help_stats(self
):
641 print "Print statistics from the current stat object."
644 def do_strip(self
, line
):
645 self
.stats
.strip_dirs()
647 def help_strip(self
):
648 print "Strip leading path information from filenames in the report."
650 def postcmd(self
, stop
, line
):
656 print "Welcome to the profile statistics browser."
657 if len(sys
.argv
) > 1:
658 initprofile
= sys
.argv
[1]
662 ProfileBrowser(initprofile
).cmdloop()
664 except KeyboardInterrupt: