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.
40 from functools
import cmp_to_key
45 """This class is used for creating reports from data generated by the
46 Profile class. It is a "friend" of that class, and imports data either
47 by direct access to members of Profile class, or by reading in a dictionary
48 that was emitted (via marshal) from the Profile class.
50 The big change from the previous Profiler (in terms of raw functionality)
51 is that an "add()" method has been provided to combine Stats from
52 several distinct profile runs. Both the constructor and the add()
53 method now take arbitrarily many file names as arguments.
55 All the print methods now take an argument that indicates how many lines
56 to print. If the arg is a floating point number between 0 and 1.0, then
57 it is taken as a decimal percentage of the available lines to be printed
58 (e.g., .1 means print 10% of all available lines). If it is an integer,
59 it is taken to mean the number of lines of data that you wish to have
62 The sort_stats() method now processes some additional options (i.e., in
63 addition to the old -1, 0, 1, or 2). It takes an arbitrary number of
64 quoted strings to select the sort order. For example sort_stats('time',
65 'name') sorts on the major key of 'internal function time', and on the
66 minor key of 'the name of the function'. Look at the two tables in
67 sort_stats() and get_sort_arg_defs(self) for more examples.
69 All methods return self, so you can string together commands like:
70 Stats('foo', 'goo').strip_dirs().sort_stats('calls').\
71 print_stats(5).print_callers(5)
74 def __init__(self
, *args
, **kwds
):
75 # I can't figure out how to explictly specify a stream keyword arg
77 # def __init__(self, *args, stream=sys.stdout): ...
78 # so I use **kwds and sqauwk if something unexpected is passed in.
79 self
.stream
= sys
.stdout
81 self
.stream
= kwds
["stream"]
86 extras
= ", ".join(["%s=%s" % (k
, kwds
[k
]) for k
in keys
])
87 raise ValueError, "unrecognized keyword args: %s" % extras
97 self
.all_callees
= None # calc only if needed
103 self
.max_name_len
= 0
106 self
.sort_arg_dict
= {}
110 self
.get_top_level_stats()
114 print >> self
.stream
, "Invalid timing data",
115 if self
.files
: print >> self
.stream
, self
.files
[-1],
118 def load_stats(self
, arg
):
119 if not arg
: self
.stats
= {}
120 elif isinstance(arg
, basestring
):
122 self
.stats
= marshal
.load(f
)
125 file_stats
= os
.stat(arg
)
126 arg
= time
.ctime(file_stats
.st_mtime
) + " " + arg
127 except: # in case this is not unix
130 elif hasattr(arg
, 'create_stats'):
132 self
.stats
= arg
.stats
135 raise TypeError, "Cannot create or construct a %r object from '%r''" % (
139 def get_top_level_stats(self
):
140 for func
, (cc
, nc
, tt
, ct
, callers
) in self
.stats
.items():
141 self
.total_calls
+= nc
142 self
.prim_calls
+= cc
144 if ("jprofile", 0, "profiler") in callers
:
145 self
.top_level
[func
] = None
146 if len(func_std_string(func
)) > self
.max_name_len
:
147 self
.max_name_len
= len(func_std_string(func
))
149 def add(self
, *arg_list
):
150 if not arg_list
: return self
151 if len(arg_list
) > 1: self
.add(*arg_list
[1:])
153 if type(self
) != type(other
) or self
.__class
__ != other
.__class
__:
155 self
.files
+= other
.files
156 self
.total_calls
+= other
.total_calls
157 self
.prim_calls
+= other
.prim_calls
158 self
.total_tt
+= other
.total_tt
159 for func
in other
.top_level
:
160 self
.top_level
[func
] = None
162 if self
.max_name_len
< other
.max_name_len
:
163 self
.max_name_len
= other
.max_name_len
167 for func
, stat
in other
.stats
.iteritems():
168 if func
in self
.stats
:
169 old_func_stat
= self
.stats
[func
]
171 old_func_stat
= (0, 0, 0, 0, {},)
172 self
.stats
[func
] = add_func_stats(old_func_stat
, stat
)
175 def dump_stats(self
, filename
):
176 """Write the profile data to a file we know how to load back."""
177 f
= file(filename
, 'wb')
179 marshal
.dump(self
.stats
, f
)
183 # list the tuple indices and directions for sorting,
184 # along with some printable description
185 sort_arg_dict_default
= {
186 "calls" : (((1,-1), ), "call count"),
187 "cumulative": (((3,-1), ), "cumulative time"),
188 "file" : (((4, 1), ), "file name"),
189 "line" : (((5, 1), ), "line number"),
190 "module" : (((4, 1), ), "file name"),
191 "name" : (((6, 1), ), "function name"),
192 "nfl" : (((6, 1),(4, 1),(5, 1),), "name/file/line"),
193 "pcalls" : (((0,-1), ), "call count"),
194 "stdname" : (((7, 1), ), "standard name"),
195 "time" : (((2,-1), ), "internal time"),
198 def get_sort_arg_defs(self
):
199 """Expand all abbreviations that are unique."""
200 if not self
.sort_arg_dict
:
201 self
.sort_arg_dict
= dict = {}
203 for word
, tup
in self
.sort_arg_dict_default
.iteritems():
209 bad_list
[fragment
] = 0
212 fragment
= fragment
[:-1]
213 for word
in bad_list
:
215 return self
.sort_arg_dict
217 def sort_stats(self
, *field
):
221 if len(field
) == 1 and type(field
[0]) == type(1):
222 # Be compatible with old profiler
223 field
= [ {-1: "stdname",
226 2: "cumulative" } [ field
[0] ] ]
228 sort_arg_defs
= self
.get_sort_arg_defs()
233 sort_tuple
= sort_tuple
+ sort_arg_defs
[word
][0]
234 self
.sort_type
+= connector
+ sort_arg_defs
[word
][1]
238 for func
, (cc
, nc
, tt
, ct
, callers
) in self
.stats
.iteritems():
239 stats_list
.append((cc
, nc
, tt
, ct
) + func
+
240 (func_std_string(func
), func
))
242 stats_list
.sort(key
=cmp_to_key(TupleComp(sort_tuple
).compare
))
244 self
.fcn_list
= fcn_list
= []
245 for tuple in stats_list
:
246 fcn_list
.append(tuple[-1])
249 def reverse_order(self
):
251 self
.fcn_list
.reverse()
254 def strip_dirs(self
):
255 oldstats
= self
.stats
256 self
.stats
= newstats
= {}
258 for func
, (cc
, nc
, tt
, ct
, callers
) in oldstats
.iteritems():
259 newfunc
= func_strip_path(func
)
260 if len(func_std_string(newfunc
)) > max_name_len
:
261 max_name_len
= len(func_std_string(newfunc
))
263 for func2
, caller
in callers
.iteritems():
264 newcallers
[func_strip_path(func2
)] = caller
266 if newfunc
in newstats
:
267 newstats
[newfunc
] = add_func_stats(
269 (cc
, nc
, tt
, ct
, newcallers
))
271 newstats
[newfunc
] = (cc
, nc
, tt
, ct
, newcallers
)
272 old_top
= self
.top_level
273 self
.top_level
= new_top
= {}
275 new_top
[func_strip_path(func
)] = None
277 self
.max_name_len
= max_name_len
280 self
.all_callees
= None
283 def calc_callees(self
):
284 if self
.all_callees
: return
285 self
.all_callees
= all_callees
= {}
286 for func
, (cc
, nc
, tt
, ct
, callers
) in self
.stats
.iteritems():
287 if not func
in all_callees
:
288 all_callees
[func
] = {}
289 for func2
, caller
in callers
.iteritems():
290 if not func2
in all_callees
:
291 all_callees
[func2
] = {}
292 all_callees
[func2
][func
] = caller
295 #******************************************************************
296 # The following functions support actual printing of reports
297 #******************************************************************
299 # Optional "amount" is either a line count, or a percentage of lines.
301 def eval_print_amount(self
, sel
, list, msg
):
303 if type(sel
) == type(""):
306 if re
.search(sel
, func_std_string(func
)):
307 new_list
.append(func
)
310 if type(sel
) == type(1.0) and 0.0 <= sel
< 1.0:
311 count
= int(count
* sel
+ .5)
312 new_list
= list[:count
]
313 elif type(sel
) == type(1) and 0 <= sel
< count
:
315 new_list
= list[:count
]
316 if len(list) != len(new_list
):
317 msg
= msg
+ " List reduced from %r to %r due to restriction <%r>\n" % (
318 len(list), len(new_list
), sel
)
322 def get_print_list(self
, sel_list
):
323 width
= self
.max_name_len
325 list = self
.fcn_list
[:]
326 msg
= " Ordered by: " + self
.sort_type
+ '\n'
328 list = self
.stats
.keys()
329 msg
= " Random listing order was used\n"
331 for selection
in sel_list
:
332 list, msg
= self
.eval_print_amount(selection
, list, msg
)
338 print >> self
.stream
, msg
339 if count
< len(self
.stats
):
342 if len(func_std_string(func
)) > width
:
343 width
= len(func_std_string(func
))
346 def print_stats(self
, *amount
):
347 for filename
in self
.files
:
348 print >> self
.stream
, filename
349 if self
.files
: print >> self
.stream
351 for func
in self
.top_level
:
352 print >> self
.stream
, indent
, func_get_function_name(func
)
354 print >> self
.stream
, indent
, self
.total_calls
, "function calls",
355 if self
.total_calls
!= self
.prim_calls
:
356 print >> self
.stream
, "(%d primitive calls)" % self
.prim_calls
,
357 print >> self
.stream
, "in %.3f CPU seconds" % self
.total_tt
359 width
, list = self
.get_print_list(amount
)
363 self
.print_line(func
)
368 def print_callees(self
, *amount
):
369 width
, list = self
.get_print_list(amount
)
373 self
.print_call_heading(width
, "called...")
375 if func
in self
.all_callees
:
376 self
.print_call_line(width
, func
, self
.all_callees
[func
])
378 self
.print_call_line(width
, func
, {})
383 def print_callers(self
, *amount
):
384 width
, list = self
.get_print_list(amount
)
386 self
.print_call_heading(width
, "was called by...")
388 cc
, nc
, tt
, ct
, callers
= self
.stats
[func
]
389 self
.print_call_line(width
, func
, callers
, "<-")
394 def print_call_heading(self
, name_size
, column_title
):
395 print >> self
.stream
, "Function ".ljust(name_size
) + column_title
396 # print sub-header only if we have new-style callers
398 for cc
, nc
, tt
, ct
, callers
in self
.stats
.itervalues():
400 value
= callers
.itervalues().next()
401 subheader
= isinstance(value
, tuple)
404 print >> self
.stream
, " "*name_size
+ " ncalls tottime cumtime"
406 def print_call_line(self
, name_size
, source
, call_dict
, arrow
="->"):
407 print >> self
.stream
, func_std_string(source
).ljust(name_size
) + arrow
,
411 clist
= call_dict
.keys()
415 name
= func_std_string(func
)
416 value
= call_dict
[func
]
417 if isinstance(value
, tuple):
418 nc
, cc
, tt
, ct
= value
420 substats
= '%d/%d' % (nc
, cc
)
422 substats
= '%d' % (nc
,)
423 substats
= '%s %s %s %s' % (substats
.rjust(7+2*len(indent
)),
424 f8(tt
), f8(ct
), name
)
425 left_width
= name_size
+ 1
427 substats
= '%s(%r) %s' % (name
, value
, f8(self
.stats
[func
][3]))
428 left_width
= name_size
+ 3
429 print >> self
.stream
, indent
*left_width
+ substats
432 def print_title(self
):
433 print >> self
.stream
, ' ncalls tottime percall cumtime percall',
434 print >> self
.stream
, 'filename:lineno(function)'
436 def print_line(self
, func
): # hack : should print percentages
437 cc
, nc
, tt
, ct
, callers
= self
.stats
[func
]
440 c
= c
+ '/' + str(cc
)
441 print >> self
.stream
, c
.rjust(9),
442 print >> self
.stream
, f8(tt
),
444 print >> self
.stream
, ' '*8,
446 print >> self
.stream
, f8(float(tt
)/nc
),
447 print >> self
.stream
, f8(ct
),
449 print >> self
.stream
, ' '*8,
451 print >> self
.stream
, f8(float(ct
)/cc
),
452 print >> self
.stream
, func_std_string(func
)
455 """This class provides a generic function for comparing any two tuples.
456 Each instance records a list of tuple-indices (from most significant
457 to least significant), and sort direction (ascending or decending) for
458 each tuple-index. The compare functions can then be used as the function
459 argument to the system sort() function when a list of tuples need to be
460 sorted in the instances order."""
462 def __init__(self
, comp_select_list
):
463 self
.comp_select_list
= comp_select_list
465 def compare (self
, left
, right
):
466 for index
, direction
in self
.comp_select_list
:
475 #**************************************************************************
476 # func_name is a triple (file:string, line:int, name:string)
478 def func_strip_path(func_name
):
479 filename
, line
, name
= func_name
480 return os
.path
.basename(filename
), line
, name
482 def func_get_function_name(func
):
485 def func_std_string(func_name
): # match what old profile produced
486 if func_name
[:2] == ('~', 0):
487 # special case for built-in functions
489 if name
.startswith('<') and name
.endswith('>'):
490 return '{%s}' % name
[1:-1]
494 return "%s:%d(%s)" % func_name
496 #**************************************************************************
497 # The following functions combine statists for pairs functions.
498 # The bulk of the processing involves correctly handling "call" lists,
499 # such as callers and callees.
500 #**************************************************************************
502 def add_func_stats(target
, source
):
503 """Add together all the stats for two profile entries."""
504 cc
, nc
, tt
, ct
, callers
= source
505 t_cc
, t_nc
, t_tt
, t_ct
, t_callers
= target
506 return (cc
+t_cc
, nc
+t_nc
, tt
+t_tt
, ct
+t_ct
,
507 add_callers(t_callers
, callers
))
509 def add_callers(target
, source
):
510 """Combine two caller lists in a single list."""
512 for func
, caller
in target
.iteritems():
513 new_callers
[func
] = caller
514 for func
, caller
in source
.iteritems():
515 if func
in new_callers
:
516 new_callers
[func
] = tuple([i
[0] + i
[1] for i
in
517 zip(caller
, new_callers
[func
])])
519 new_callers
[func
] = caller
522 def count_calls(callers
):
523 """Sum the caller statistics to get total number of calls received."""
525 for calls
in callers
.itervalues():
529 #**************************************************************************
530 # The following functions support printing of reports
531 #**************************************************************************
536 #**************************************************************************
537 # Statistics browser added by ESR, April 2001
538 #**************************************************************************
540 if __name__
== '__main__':
547 class ProfileBrowser(cmd
.Cmd
):
548 def __init__(self
, profile
=None):
549 cmd
.Cmd
.__init
__(self
)
551 if profile
is not None:
552 self
.stats
= Stats(profile
)
553 self
.stream
= self
.stats
.stream
556 self
.stream
= sys
.stdout
558 def generic(self
, fn
, line
):
563 processed
.append(int(term
))
569 if frac
> 1 or frac
< 0:
570 print >> self
.stream
, "Fraction argument must be in [0, 1]"
572 processed
.append(frac
)
576 processed
.append(term
)
578 getattr(self
.stats
, fn
)(*processed
)
580 print >> self
.stream
, "No statistics object is loaded."
582 def generic_help(self
):
583 print >> self
.stream
, "Arguments may be:"
584 print >> self
.stream
, "* An integer maximum number of entries to print."
585 print >> self
.stream
, "* A decimal fractional number between 0 and 1, controlling"
586 print >> self
.stream
, " what fraction of selected entries to print."
587 print >> self
.stream
, "* A regular expression; only entries with function names"
588 print >> self
.stream
, " that match it are printed."
590 def do_add(self
, line
):
594 print >> self
.stream
, "Add profile info from given file to current statistics object."
596 def do_callees(self
, line
):
597 return self
.generic('print_callees', line
)
598 def help_callees(self
):
599 print >> self
.stream
, "Print callees statistics from the current stat object."
602 def do_callers(self
, line
):
603 return self
.generic('print_callers', line
)
604 def help_callers(self
):
605 print >> self
.stream
, "Print callers statistics from the current stat object."
608 def do_EOF(self
, line
):
609 print >> self
.stream
, ""
612 print >> self
.stream
, "Leave the profile brower."
614 def do_quit(self
, line
):
617 print >> self
.stream
, "Leave the profile brower."
619 def do_read(self
, line
):
622 self
.stats
= Stats(line
)
623 except IOError, args
:
624 print >> self
.stream
, args
[1]
626 self
.prompt
= line
+ "% "
627 elif len(self
.prompt
) > 2:
628 line
= self
.prompt
[-2:]
630 print >> self
.stream
, "No statistics object is current -- cannot reload."
633 print >> self
.stream
, "Read in profile data from a specified file."
635 def do_reverse(self
, line
):
636 self
.stats
.reverse_order()
638 def help_reverse(self
):
639 print >> self
.stream
, "Reverse the sort order of the profiling report."
641 def do_sort(self
, line
):
642 abbrevs
= self
.stats
.get_sort_arg_defs()
643 if line
and all((x
in abbrevs
) for x
in line
.split()):
644 self
.stats
.sort_stats(*line
.split())
646 print >> self
.stream
, "Valid sort keys (unique prefixes are accepted):"
647 for (key
, value
) in Stats
.sort_arg_dict_default
.iteritems():
648 print >> self
.stream
, "%s -- %s" % (key
, value
[1])
651 print >> self
.stream
, "Sort profile data according to specified keys."
652 print >> self
.stream
, "(Typing `sort' without arguments lists valid keys.)"
653 def complete_sort(self
, text
, *args
):
654 return [a
for a
in Stats
.sort_arg_dict_default
if a
.startswith(text
)]
656 def do_stats(self
, line
):
657 return self
.generic('print_stats', line
)
658 def help_stats(self
):
659 print >> self
.stream
, "Print statistics from the current stat object."
662 def do_strip(self
, line
):
663 self
.stats
.strip_dirs()
665 def help_strip(self
):
666 print >> self
.stream
, "Strip leading path information from filenames in the report."
668 def postcmd(self
, stop
, line
):
674 if len(sys
.argv
) > 1:
675 initprofile
= sys
.argv
[1]
679 browser
= ProfileBrowser(initprofile
)
680 print >> browser
.stream
, "Welcome to the profile statistics browser."
682 print >> browser
.stream
, "Goodbye."
683 except KeyboardInterrupt: