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
[8]) + " " + 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 " \
122 + " object from '" + `arg`
+ "'"
125 def get_top_level_stats(self
):
126 for func
, (cc
, nc
, tt
, ct
, callers
) in self
.stats
.items():
127 self
.total_calls
+= nc
128 self
.prim_calls
+= cc
130 if callers
.has_key(("jprofile", 0, "profiler")):
131 self
.top_level
[func
] = None
132 if len(func_std_string(func
)) > self
.max_name_len
:
133 self
.max_name_len
= len(func_std_string(func
))
135 def add(self
, *arg_list
):
136 if not arg_list
: return self
137 if len(arg_list
) > 1: apply(self
.add
, arg_list
[1:])
139 if type(self
) != type(other
) or self
.__class
__ != other
.__class
__:
141 self
.files
+= other
.files
142 self
.total_calls
+= other
.total_calls
143 self
.prim_calls
+= other
.prim_calls
144 self
.total_tt
+= other
.total_tt
145 for func
in other
.top_level
.keys():
146 self
.top_level
[func
] = None
148 if self
.max_name_len
< other
.max_name_len
:
149 self
.max_name_len
= other
.max_name_len
153 for func
in other
.stats
.keys():
154 if self
.stats
.has_key(func
):
155 old_func_stat
= self
.stats
[func
]
157 old_func_stat
= (0, 0, 0, 0, {},)
158 self
.stats
[func
] = add_func_stats(old_func_stat
, other
.stats
[func
])
161 # list the tuple indices and directions for sorting,
162 # along with some printable description
163 sort_arg_dict_default
= {
164 "calls" : (((1,-1), ), "call count"),
165 "cumulative": (((3,-1), ), "cumulative time"),
166 "file" : (((4, 1), ), "file name"),
167 "line" : (((5, 1), ), "line number"),
168 "module" : (((4, 1), ), "file name"),
169 "name" : (((6, 1), ), "function name"),
170 "nfl" : (((6, 1),(4, 1),(5, 1),), "name/file/line"),
171 "pcalls" : (((0,-1), ), "call count"),
172 "stdname" : (((7, 1), ), "standard name"),
173 "time" : (((2,-1), ), "internal time"),
176 def get_sort_arg_defs(self
):
177 """Expand all abbreviations that are unique."""
178 if not self
.sort_arg_dict
:
179 self
.sort_arg_dict
= dict = {}
181 for word
in self
.sort_arg_dict_default
.keys():
186 if dict.has_key(fragment
):
187 bad_list
[fragment
] = 0
189 dict[fragment
] = self
.sort_arg_dict_default
[word
]
190 fragment
= fragment
[:-1]
191 for word
in bad_list
.keys():
193 return self
.sort_arg_dict
195 def sort_stats(self
, *field
):
199 if len(field
) == 1 and type(field
[0]) == type(1):
200 # Be compatible with old profiler
201 field
= [ {-1: "stdname",
204 2: "cumulative" } [ field
[0] ] ]
206 sort_arg_defs
= self
.get_sort_arg_defs()
211 sort_tuple
= sort_tuple
+ sort_arg_defs
[word
][0]
212 self
.sort_type
+= connector
+ sort_arg_defs
[word
][1]
216 for func
in self
.stats
.keys():
217 cc
, nc
, tt
, ct
, callers
= self
.stats
[func
]
218 stats_list
.append((cc
, nc
, tt
, ct
) + func
+
219 (func_std_string(func
), func
))
221 stats_list
.sort(TupleComp(sort_tuple
).compare
)
223 self
.fcn_list
= fcn_list
= []
224 for tuple in stats_list
:
225 fcn_list
.append(tuple[-1])
228 def reverse_order(self
):
230 self
.fcn_list
.reverse()
233 def strip_dirs(self
):
234 oldstats
= self
.stats
235 self
.stats
= newstats
= {}
237 for func
in oldstats
.keys():
238 cc
, nc
, tt
, ct
, callers
= oldstats
[func
]
239 newfunc
= func_strip_path(func
)
240 if len(func_std_string(newfunc
)) > max_name_len
:
241 max_name_len
= len(func_std_string(newfunc
))
243 for func2
in callers
.keys():
244 newcallers
[func_strip_path(func2
)] = callers
[func2
]
246 if newstats
.has_key(newfunc
):
247 newstats
[newfunc
] = add_func_stats(
249 (cc
, nc
, tt
, ct
, newcallers
))
251 newstats
[newfunc
] = (cc
, nc
, tt
, ct
, newcallers
)
252 old_top
= self
.top_level
253 self
.top_level
= new_top
= {}
254 for func
in old_top
.keys():
255 new_top
[func_strip_path(func
)] = None
257 self
.max_name_len
= max_name_len
260 self
.all_callees
= None
263 def calc_callees(self
):
264 if self
.all_callees
: return
265 self
.all_callees
= all_callees
= {}
266 for func
in self
.stats
.keys():
267 if not all_callees
.has_key(func
):
268 all_callees
[func
] = {}
269 cc
, nc
, tt
, ct
, callers
= self
.stats
[func
]
270 for func2
in callers
.keys():
271 if not all_callees
.has_key(func2
):
272 all_callees
[func2
] = {}
273 all_callees
[func2
][func
] = callers
[func2
]
276 #******************************************************************
277 # The following functions support actual printing of reports
278 #******************************************************************
280 # Optional "amount" is either a line count, or a percentage of lines.
282 def eval_print_amount(self
, sel
, list, msg
):
284 if type(sel
) == type(""):
287 if re
.search(sel
, func_std_string(func
)):
288 new_list
.append(func
)
291 if type(sel
) == type(1.0) and 0.0 <= sel
< 1.0:
292 count
= int(count
* sel
+ .5)
293 new_list
= list[:count
]
294 elif type(sel
) == type(1) and 0 <= sel
< count
:
296 new_list
= list[:count
]
297 if len(list) != len(new_list
):
298 msg
= msg
+ " List reduced from " + `
len(list)` \
299 + " to " + `
len(new_list
)`
+ \
300 " due to restriction <" + `sel`
+ ">\n"
304 def get_print_list(self
, sel_list
):
305 width
= self
.max_name_len
307 list = self
.fcn_list
[:]
308 msg
= " Ordered by: " + self
.sort_type
+ '\n'
310 list = self
.stats
.keys()
311 msg
= " Random listing order was used\n"
313 for selection
in sel_list
:
314 list, msg
= self
.eval_print_amount(selection
, list, msg
)
321 if count
< len(self
.stats
):
324 if len(func_std_string(func
)) > width
:
325 width
= len(func_std_string(func
))
328 def print_stats(self
, *amount
):
329 for filename
in self
.files
:
333 for func
in self
.top_level
.keys():
334 print indent
, func_get_function_name(func
)
336 print indent
, self
.total_calls
, "function calls",
337 if self
.total_calls
!= self
.prim_calls
:
338 print "(%d primitive calls)" % self
.prim_calls
,
339 print "in %.3f CPU seconds" % self
.total_tt
341 width
, list = self
.get_print_list(amount
)
345 self
.print_line(func
)
350 def print_callees(self
, *amount
):
351 width
, list = self
.get_print_list(amount
)
355 self
.print_call_heading(width
, "called...")
357 if self
.all_callees
.has_key(func
):
358 self
.print_call_line(width
, func
, self
.all_callees
[func
])
360 self
.print_call_line(width
, func
, {})
365 def print_callers(self
, *amount
):
366 width
, list = self
.get_print_list(amount
)
368 self
.print_call_heading(width
, "was called by...")
370 cc
, nc
, tt
, ct
, callers
= self
.stats
[func
]
371 self
.print_call_line(width
, func
, callers
)
376 def print_call_heading(self
, name_size
, column_title
):
377 print "Function ".ljust(name_size
) + column_title
379 def print_call_line(self
, name_size
, source
, call_dict
):
380 print func_std_string(source
).ljust(name_size
),
384 clist
= call_dict
.keys()
386 name_size
= name_size
+ 1
389 name
= func_std_string(func
)
390 print indent
*name_size
+ name
+ '(' \
391 + `call_dict
[func
]`
+')', \
392 f8(self
.stats
[func
][3])
395 def print_title(self
):
396 print ' ncalls tottime percall cumtime percall', \
397 'filename:lineno(function)'
399 def print_line(self
, func
): # hack : should print percentages
400 cc
, nc
, tt
, ct
, callers
= self
.stats
[func
]
403 c
= c
+ '/' + str(cc
)
415 print func_std_string(func
)
418 # Deprecated since 1.5.1 -- see the docs.
419 pass # has no return value, so use at end of line :-)
422 """This class provides a generic function for comparing any two tuples.
423 Each instance records a list of tuple-indices (from most significant
424 to least significant), and sort direction (ascending or decending) for
425 each tuple-index. The compare functions can then be used as the function
426 argument to the system sort() function when a list of tuples need to be
427 sorted in the instances order."""
429 def __init__(self
, comp_select_list
):
430 self
.comp_select_list
= comp_select_list
432 def compare (self
, left
, right
):
433 for index
, direction
in self
.comp_select_list
:
442 #**************************************************************************
443 # func_name is a triple (file:string, line:int, name:string)
445 def func_strip_path(func_name
):
446 file, line
, name
= func_name
447 return os
.path
.basename(file), line
, name
449 def func_get_function_name(func
):
452 def func_std_string(func_name
): # match what old profile produced
453 return "%s:%d(%s)" % func_name
455 #**************************************************************************
456 # The following functions combine statists for pairs functions.
457 # The bulk of the processing involves correctly handling "call" lists,
458 # such as callers and callees.
459 #**************************************************************************
461 def add_func_stats(target
, source
):
462 """Add together all the stats for two profile entries."""
463 cc
, nc
, tt
, ct
, callers
= source
464 t_cc
, t_nc
, t_tt
, t_ct
, t_callers
= target
465 return (cc
+t_cc
, nc
+t_nc
, tt
+t_tt
, ct
+t_ct
,
466 add_callers(t_callers
, callers
))
468 def add_callers(target
, source
):
469 """Combine two caller lists in a single list."""
471 for func
in target
.keys():
472 new_callers
[func
] = target
[func
]
473 for func
in source
.keys():
474 if new_callers
.has_key(func
):
475 new_callers
[func
] = source
[func
] + new_callers
[func
]
477 new_callers
[func
] = source
[func
]
480 def count_calls(callers
):
481 """Sum the caller statistics to get total number of calls received."""
483 for func
in callers
.keys():
487 #**************************************************************************
488 # The following functions support printing of reports
489 #**************************************************************************
494 #**************************************************************************
495 # Statistics browser added by ESR, April 2001
496 #**************************************************************************
498 if __name__
== '__main__':
505 class ProfileBrowser(cmd
.Cmd
):
506 def __init__(self
, profile
=None):
507 cmd
.Cmd
.__init
__(self
)
510 self
.stats
= Stats(profile
)
514 def generic(self
, fn
, line
):
519 processed
.append(int(term
))
525 if frac
> 1 or frac
< 0:
526 print "Fraction argument mus be in [0, 1]"
528 processed
.append(frac
)
532 processed
.append(term
)
534 apply(getattr(self
.stats
, fn
), processed
)
536 print "No statistics object is loaded."
538 def generic_help(self
):
539 print "Arguments may be:"
540 print "* An integer maximum number of entries to print."
541 print "* A decimal fractional number between 0 and 1, controlling"
542 print " what fraction of selected entries to print."
543 print "* A regular expression; only entries with function names"
544 print " that match it are printed."
546 def do_add(self
, line
):
550 print "Add profile info from given file to current statistics object."
552 def do_callees(self
, line
):
553 return self
.generic('print_callees', line
)
554 def help_callees(self
):
555 print "Print callees statistics from the current stat object."
558 def do_callers(self
, line
):
559 return self
.generic('print_callers', line
)
560 def help_callers(self
):
561 print "Print callers statistics from the current stat object."
564 def do_EOF(self
, line
):
568 print "Leave the profile brower."
570 def do_quit(self
, line
):
573 print "Leave the profile brower."
575 def do_read(self
, line
):
578 self
.stats
= Stats(line
)
579 except IOError, args
:
582 self
.prompt
= line
+ "% "
583 elif len(self
.prompt
) > 2:
584 line
= self
.prompt
[-2:]
586 print "No statistics object is current -- cannot reload."
589 print "Read in profile data from a specified file."
591 def do_reverse(self
, line
):
592 self
.stats
.reverse_order()
594 def help_reverse(self
):
595 print "Reverse the sort order of the profiling report."
597 def do_sort(self
, line
):
598 abbrevs
= self
.stats
.get_sort_arg_defs().keys()
599 if line
and not filter(lambda x
,a
=abbrevs
: x
not in a
,line
.split()):
600 apply(self
.stats
.sort_stats
, line
.split())
602 print "Valid sort keys (unique prefixes are accepted):"
603 for (key
, value
) in Stats
.sort_arg_dict_default
.items():
604 print "%s -- %s" % (key
, value
[1])
607 print "Sort profile data according to specified keys."
608 print "(Typing `sort' without arguments lists valid keys.)"
609 def complete_sort(self
, text
, *args
):
610 return [a
for a
in Stats
.sort_arg_dict_default
.keys() if a
.startswith(text
)]
612 def do_stats(self
, line
):
613 return self
.generic('print_stats', line
)
614 def help_stats(self
):
615 print "Print statistics from the current stat object."
618 def do_strip(self
, line
):
619 self
.stats
.strip_dirs()
621 def help_strip(self
):
622 print "Strip leading path information from filenames in the report."
624 def postcmd(self
, stop
, line
):
630 print "Welcome to the profile statistics browser."
631 if len(sys
.argv
) > 1:
632 initprofile
= sys
.argv
[1]
636 ProfileBrowser(initprofile
).cmdloop()
638 except KeyboardInterrupt: