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.
44 """This class is used for creating reports from data generated by the
45 Profile class. It is a "friend" of that class, and imports data either
46 by direct access to members of Profile class, or by reading in a dictionary
47 that was emitted (via marshal) from the Profile class.
49 The big change from the previous Profiler (in terms of raw functionality)
50 is that an "add()" method has been provided to combine Stats from
51 several distinct profile runs. Both the constructor and the add()
52 method now take arbitrarily many file names as arguments.
54 All the print methods now take an argument that indicates how many lines
55 to print. If the arg is a floating point number between 0 and 1.0, then
56 it is taken as a decimal percentage of the available lines to be printed
57 (e.g., .1 means print 10% of all available lines). If it is an integer,
58 it is taken to mean the number of lines of data that you wish to have
61 The sort_stats() method now processes some additional options (i.e., in
62 addition to the old -1, 0, 1, or 2). It takes an arbitrary number of
63 quoted strings to select the sort order. For example sort_stats('time',
64 'name') sorts on the major key of 'internal function time', and on the
65 minor key of 'the name of the function'. Look at the two tables in
66 sort_stats() and get_sort_arg_defs(self) for more examples.
68 All methods return self, so you can string together commands like:
69 Stats('foo', 'goo').strip_dirs().sort_stats('calls').\
70 print_stats(5).print_callers(5)
73 def __init__(self
, *args
, **kwds
):
74 # I can't figure out how to explictly specify a stream keyword arg
76 # def __init__(self, *args, stream=sys.stdout): ...
77 # so I use **kwds and sqauwk if something unexpected is passed in.
78 self
.stream
= sys
.stdout
80 self
.stream
= kwds
["stream"]
85 extras
= ", ".join(["%s=%s" % (k
, kwds
[k
]) for k
in keys
])
86 raise ValueError("unrecognized keyword args: %s" % extras
)
96 self
.all_callees
= None # calc only if needed
102 self
.max_name_len
= 0
105 self
.sort_arg_dict
= {}
109 self
.get_top_level_stats()
113 print("Invalid timing data", end
=' ', file=self
.stream
)
114 if self
.files
: print(self
.files
[-1], end
=' ', file=self
.stream
)
115 print(file=self
.stream
)
117 def load_stats(self
, arg
):
118 if not arg
: self
.stats
= {}
119 elif isinstance(arg
, str):
121 self
.stats
= marshal
.load(f
)
124 file_stats
= os
.stat(arg
)
125 arg
= time
.ctime(file_stats
.st_mtime
) + " " + arg
126 except: # in case this is not unix
129 elif hasattr(arg
, 'create_stats'):
131 self
.stats
= arg
.stats
134 raise TypeError("Cannot create or construct a %r object from '%r''"
135 % (self
.__class
__, arg
))
138 def get_top_level_stats(self
):
139 for func
, (cc
, nc
, tt
, ct
, callers
) in self
.stats
.items():
140 self
.total_calls
+= nc
141 self
.prim_calls
+= cc
143 if ("jprofile", 0, "profiler") in callers
:
144 self
.top_level
[func
] = None
145 if len(func_std_string(func
)) > self
.max_name_len
:
146 self
.max_name_len
= len(func_std_string(func
))
148 def add(self
, *arg_list
):
149 if not arg_list
: return self
150 if len(arg_list
) > 1: self
.add(*arg_list
[1:])
152 if type(self
) != type(other
) or self
.__class
__ != other
.__class
__:
154 self
.files
+= other
.files
155 self
.total_calls
+= other
.total_calls
156 self
.prim_calls
+= other
.prim_calls
157 self
.total_tt
+= other
.total_tt
158 for func
in other
.top_level
:
159 self
.top_level
[func
] = None
161 if self
.max_name_len
< other
.max_name_len
:
162 self
.max_name_len
= other
.max_name_len
166 for func
, stat
in other
.stats
.items():
167 if func
in self
.stats
:
168 old_func_stat
= self
.stats
[func
]
170 old_func_stat
= (0, 0, 0, 0, {},)
171 self
.stats
[func
] = add_func_stats(old_func_stat
, stat
)
174 def dump_stats(self
, filename
):
175 """Write the profile data to a file we know how to load back."""
176 f
= open(filename
, 'wb')
178 marshal
.dump(self
.stats
, f
)
182 # list the tuple indices and directions for sorting,
183 # along with some printable description
184 sort_arg_dict_default
= {
185 "calls" : (((1,-1), ), "call count"),
186 "cumulative": (((3,-1), ), "cumulative time"),
187 "file" : (((4, 1), ), "file name"),
188 "line" : (((5, 1), ), "line number"),
189 "module" : (((4, 1), ), "file name"),
190 "name" : (((6, 1), ), "function name"),
191 "nfl" : (((6, 1),(4, 1),(5, 1),), "name/file/line"),
192 "pcalls" : (((0,-1), ), "call count"),
193 "stdname" : (((7, 1), ), "standard name"),
194 "time" : (((2,-1), ), "internal time"),
197 def get_sort_arg_defs(self
):
198 """Expand all abbreviations that are unique."""
199 if not self
.sort_arg_dict
:
200 self
.sort_arg_dict
= dict = {}
202 for word
, tup
in self
.sort_arg_dict_default
.items():
208 bad_list
[fragment
] = 0
211 fragment
= fragment
[:-1]
212 for word
in bad_list
:
214 return self
.sort_arg_dict
216 def sort_stats(self
, *field
):
220 if len(field
) == 1 and type(field
[0]) == type(1):
221 # Be compatible with old profiler
222 field
= [ {-1: "stdname",
225 2: "cumulative" } [ field
[0] ] ]
227 sort_arg_defs
= self
.get_sort_arg_defs()
232 sort_tuple
= sort_tuple
+ sort_arg_defs
[word
][0]
233 self
.sort_type
+= connector
+ sort_arg_defs
[word
][1]
237 for func
, (cc
, nc
, tt
, ct
, callers
) in self
.stats
.items():
238 stats_list
.append((cc
, nc
, tt
, ct
) + func
+
239 (func_std_string(func
), func
))
241 stats_list
.sort(key
=CmpToKey(TupleComp(sort_tuple
).compare
))
243 self
.fcn_list
= fcn_list
= []
244 for tuple in stats_list
:
245 fcn_list
.append(tuple[-1])
248 def reverse_order(self
):
250 self
.fcn_list
.reverse()
253 def strip_dirs(self
):
254 oldstats
= self
.stats
255 self
.stats
= newstats
= {}
257 for func
, (cc
, nc
, tt
, ct
, callers
) in oldstats
.items():
258 newfunc
= func_strip_path(func
)
259 if len(func_std_string(newfunc
)) > max_name_len
:
260 max_name_len
= len(func_std_string(newfunc
))
262 for func2
, caller
in callers
.items():
263 newcallers
[func_strip_path(func2
)] = caller
265 if newfunc
in newstats
:
266 newstats
[newfunc
] = add_func_stats(
268 (cc
, nc
, tt
, ct
, newcallers
))
270 newstats
[newfunc
] = (cc
, nc
, tt
, ct
, newcallers
)
271 old_top
= self
.top_level
272 self
.top_level
= new_top
= {}
274 new_top
[func_strip_path(func
)] = None
276 self
.max_name_len
= max_name_len
279 self
.all_callees
= None
282 def calc_callees(self
):
283 if self
.all_callees
: return
284 self
.all_callees
= all_callees
= {}
285 for func
, (cc
, nc
, tt
, ct
, callers
) in self
.stats
.items():
286 if not func
in all_callees
:
287 all_callees
[func
] = {}
288 for func2
, caller
in callers
.items():
289 if not func2
in all_callees
:
290 all_callees
[func2
] = {}
291 all_callees
[func2
][func
] = caller
294 #******************************************************************
295 # The following functions support actual printing of reports
296 #******************************************************************
298 # Optional "amount" is either a line count, or a percentage of lines.
300 def eval_print_amount(self
, sel
, list, msg
):
302 if type(sel
) == type(""):
305 if re
.search(sel
, func_std_string(func
)):
306 new_list
.append(func
)
309 if type(sel
) == type(1.0) and 0.0 <= sel
< 1.0:
310 count
= int(count
* sel
+ .5)
311 new_list
= list[:count
]
312 elif type(sel
) == type(1) and 0 <= sel
< count
:
314 new_list
= list[:count
]
315 if len(list) != len(new_list
):
316 msg
= msg
+ " List reduced from %r to %r due to restriction <%r>\n" % (
317 len(list), len(new_list
), sel
)
321 def get_print_list(self
, sel_list
):
322 width
= self
.max_name_len
324 list = self
.fcn_list
[:]
325 msg
= " Ordered by: " + self
.sort_type
+ '\n'
327 list = self
.stats
.keys()
328 msg
= " Random listing order was used\n"
330 for selection
in sel_list
:
331 list, msg
= self
.eval_print_amount(selection
, list, msg
)
337 print(msg
, file=self
.stream
)
338 if count
< len(self
.stats
):
341 if len(func_std_string(func
)) > width
:
342 width
= len(func_std_string(func
))
345 def print_stats(self
, *amount
):
346 for filename
in self
.files
:
347 print(filename
, file=self
.stream
)
348 if self
.files
: print(file=self
.stream
)
350 for func
in self
.top_level
:
351 print(indent
, func_get_function_name(func
), file=self
.stream
)
353 print(indent
, self
.total_calls
, "function calls", end
=' ', file=self
.stream
)
354 if self
.total_calls
!= self
.prim_calls
:
355 print("(%d primitive calls)" % self
.prim_calls
, end
=' ', file=self
.stream
)
356 print("in %.3f CPU seconds" % self
.total_tt
, file=self
.stream
)
357 print(file=self
.stream
)
358 width
, list = self
.get_print_list(amount
)
362 self
.print_line(func
)
363 print(file=self
.stream
)
364 print(file=self
.stream
)
367 def print_callees(self
, *amount
):
368 width
, list = self
.get_print_list(amount
)
372 self
.print_call_heading(width
, "called...")
374 if func
in self
.all_callees
:
375 self
.print_call_line(width
, func
, self
.all_callees
[func
])
377 self
.print_call_line(width
, func
, {})
378 print(file=self
.stream
)
379 print(file=self
.stream
)
382 def print_callers(self
, *amount
):
383 width
, list = self
.get_print_list(amount
)
385 self
.print_call_heading(width
, "was called by...")
387 cc
, nc
, tt
, ct
, callers
= self
.stats
[func
]
388 self
.print_call_line(width
, func
, callers
, "<-")
389 print(file=self
.stream
)
390 print(file=self
.stream
)
393 def print_call_heading(self
, name_size
, column_title
):
394 print("Function ".ljust(name_size
) + column_title
, file=self
.stream
)
395 # print sub-header only if we have new-style callers
397 for cc
, nc
, tt
, ct
, callers
in self
.stats
.values():
399 value
= next(iter(callers
.values()))
400 subheader
= isinstance(value
, tuple)
403 print(" "*name_size
+ " ncalls tottime cumtime", file=self
.stream
)
405 def print_call_line(self
, name_size
, source
, call_dict
, arrow
="->"):
406 print(func_std_string(source
).ljust(name_size
) + arrow
, end
=' ', file=self
.stream
)
408 print(file=self
.stream
)
410 clist
= sorted(call_dict
.keys())
413 name
= func_std_string(func
)
414 value
= call_dict
[func
]
415 if isinstance(value
, tuple):
416 nc
, cc
, tt
, ct
= value
418 substats
= '%d/%d' % (nc
, cc
)
420 substats
= '%d' % (nc
,)
421 substats
= '%s %s %s %s' % (substats
.rjust(7+2*len(indent
)),
422 f8(tt
), f8(ct
), name
)
423 left_width
= name_size
+ 1
425 substats
= '%s(%r) %s' % (name
, value
, f8(self
.stats
[func
][3]))
426 left_width
= name_size
+ 3
427 print(indent
*left_width
+ substats
, file=self
.stream
)
430 def print_title(self
):
431 print(' ncalls tottime percall cumtime percall', end
=' ', file=self
.stream
)
432 print('filename:lineno(function)', file=self
.stream
)
434 def print_line(self
, func
): # hack : should print percentages
435 cc
, nc
, tt
, ct
, callers
= self
.stats
[func
]
438 c
= c
+ '/' + str(cc
)
439 print(c
.rjust(9), end
=' ', file=self
.stream
)
440 print(f8(tt
), end
=' ', file=self
.stream
)
442 print(' '*8, end
=' ', file=self
.stream
)
444 print(f8(tt
/nc
), end
=' ', file=self
.stream
)
445 print(f8(ct
), end
=' ', file=self
.stream
)
447 print(' '*8, end
=' ', file=self
.stream
)
449 print(f8(ct
/cc
), end
=' ', file=self
.stream
)
450 print(func_std_string(func
), file=self
.stream
)
453 """This class provides a generic function for comparing any two tuples.
454 Each instance records a list of tuple-indices (from most significant
455 to least significant), and sort direction (ascending or decending) for
456 each tuple-index. The compare functions can then be used as the function
457 argument to the system sort() function when a list of tuples need to be
458 sorted in the instances order."""
460 def __init__(self
, comp_select_list
):
461 self
.comp_select_list
= comp_select_list
463 def compare (self
, left
, right
):
464 for index
, direction
in self
.comp_select_list
:
474 'Convert a cmp= function into a key= function'
476 def __init__(self
, obj
):
478 def __lt__(self
, other
):
479 return mycmp(self
.obj
, other
.obj
) == -1
483 #**************************************************************************
484 # func_name is a triple (file:string, line:int, name:string)
486 def func_strip_path(func_name
):
487 filename
, line
, name
= func_name
488 return os
.path
.basename(filename
), line
, name
490 def func_get_function_name(func
):
493 def func_std_string(func_name
): # match what old profile produced
494 if func_name
[:2] == ('~', 0):
495 # special case for built-in functions
497 if name
.startswith('<') and name
.endswith('>'):
498 return '{%s}' % name
[1:-1]
502 return "%s:%d(%s)" % func_name
504 #**************************************************************************
505 # The following functions combine statists for pairs functions.
506 # The bulk of the processing involves correctly handling "call" lists,
507 # such as callers and callees.
508 #**************************************************************************
510 def add_func_stats(target
, source
):
511 """Add together all the stats for two profile entries."""
512 cc
, nc
, tt
, ct
, callers
= source
513 t_cc
, t_nc
, t_tt
, t_ct
, t_callers
= target
514 return (cc
+t_cc
, nc
+t_nc
, tt
+t_tt
, ct
+t_ct
,
515 add_callers(t_callers
, callers
))
517 def add_callers(target
, source
):
518 """Combine two caller lists in a single list."""
520 for func
, caller
in target
.items():
521 new_callers
[func
] = caller
522 for func
, caller
in source
.items():
523 if func
in new_callers
:
524 new_callers
[func
] = tuple([i
[0] + i
[1] for i
in
525 zip(caller
, new_callers
[func
])])
527 new_callers
[func
] = caller
530 def count_calls(callers
):
531 """Sum the caller statistics to get total number of calls received."""
533 for calls
in callers
.values():
537 #**************************************************************************
538 # The following functions support printing of reports
539 #**************************************************************************
544 #**************************************************************************
545 # Statistics browser added by ESR, April 2001
546 #**************************************************************************
548 if __name__
== '__main__':
555 class ProfileBrowser(cmd
.Cmd
):
556 def __init__(self
, profile
=None):
557 cmd
.Cmd
.__init
__(self
)
559 if profile
is not None:
560 self
.stats
= Stats(profile
)
561 self
.stream
= self
.stats
.stream
564 self
.stream
= sys
.stdout
566 def generic(self
, fn
, line
):
571 processed
.append(int(term
))
577 if frac
> 1 or frac
< 0:
578 print("Fraction argument must be in [0, 1]", file=self
.stream
)
580 processed
.append(frac
)
584 processed
.append(term
)
586 getattr(self
.stats
, fn
)(*processed
)
588 print("No statistics object is loaded.", file=self
.stream
)
590 def generic_help(self
):
591 print("Arguments may be:", file=self
.stream
)
592 print("* An integer maximum number of entries to print.", file=self
.stream
)
593 print("* A decimal fractional number between 0 and 1, controlling", file=self
.stream
)
594 print(" what fraction of selected entries to print.", file=self
.stream
)
595 print("* A regular expression; only entries with function names", file=self
.stream
)
596 print(" that match it are printed.", file=self
.stream
)
598 def do_add(self
, line
):
602 print("Add profile info from given file to current statistics object.", file=self
.stream
)
604 def do_callees(self
, line
):
605 return self
.generic('print_callees', line
)
606 def help_callees(self
):
607 print("Print callees statistics from the current stat object.", file=self
.stream
)
610 def do_callers(self
, line
):
611 return self
.generic('print_callers', line
)
612 def help_callers(self
):
613 print("Print callers statistics from the current stat object.", file=self
.stream
)
616 def do_EOF(self
, line
):
617 print("", file=self
.stream
)
620 print("Leave the profile brower.", file=self
.stream
)
622 def do_quit(self
, line
):
625 print("Leave the profile brower.", file=self
.stream
)
627 def do_read(self
, line
):
630 self
.stats
= Stats(line
)
631 except IOError as err
:
632 print(err
.args
[1], file=self
.stream
)
634 self
.prompt
= line
+ "% "
635 elif len(self
.prompt
) > 2:
636 line
= self
.prompt
[-2:]
638 print("No statistics object is current -- cannot reload.", file=self
.stream
)
641 print("Read in profile data from a specified file.", file=self
.stream
)
643 def do_reverse(self
, line
):
644 self
.stats
.reverse_order()
646 def help_reverse(self
):
647 print("Reverse the sort order of the profiling report.", file=self
.stream
)
649 def do_sort(self
, line
):
650 abbrevs
= self
.stats
.get_sort_arg_defs()
651 if line
and not filter(lambda x
,a
=abbrevs
: x
not in a
,line
.split()):
652 self
.stats
.sort_stats(*line
.split())
654 print("Valid sort keys (unique prefixes are accepted):", file=self
.stream
)
655 for (key
, value
) in Stats
.sort_arg_dict_default
.items():
656 print("%s -- %s" % (key
, value
[1]), file=self
.stream
)
659 print("Sort profile data according to specified keys.", file=self
.stream
)
660 print("(Typing `sort' without arguments lists valid keys.)", file=self
.stream
)
661 def complete_sort(self
, text
, *args
):
662 return [a
for a
in Stats
.sort_arg_dict_default
if a
.startswith(text
)]
664 def do_stats(self
, line
):
665 return self
.generic('print_stats', line
)
666 def help_stats(self
):
667 print("Print statistics from the current stat object.", file=self
.stream
)
670 def do_strip(self
, line
):
671 self
.stats
.strip_dirs()
673 def help_strip(self
):
674 print("Strip leading path information from filenames in the report.", file=self
.stream
)
676 def postcmd(self
, stop
, line
):
682 if len(sys
.argv
) > 1:
683 initprofile
= sys
.argv
[1]
687 browser
= ProfileBrowser(initprofile
)
688 print("Welcome to the profile statistics browser.", file=browser
.stream
)
690 print("Goodbye.", file=browser
.stream
)
691 except KeyboardInterrupt: