0001 """Class for printing reports on profiled python code.""" 0002 0003 # Class for printing reports on profiled python code. rev 1.0 4/1/94 0004 # 0005 # Based on prior profile module by Sjoerd Mullender... 0006 # which was hacked somewhat by: Guido van Rossum 0007 # 0008 # see profile.doc and profile.py for more info. 0009 0010 # Copyright 1994, by InfoSeek Corporation, all rights reserved. 0011 # Written by James Roskind 0012 # 0013 # Permission to use, copy, modify, and distribute this Python software 0014 # and its associated documentation for any purpose (subject to the 0015 # restriction in the following sentence) without fee is hereby granted, 0016 # provided that the above copyright notice appears in all copies, and 0017 # that both that copyright notice and this permission notice appear in 0018 # supporting documentation, and that the name of InfoSeek not be used in 0019 # advertising or publicity pertaining to distribution of the software 0020 # without specific, written prior permission. This permission is 0021 # explicitly restricted to the copying and modification of the software 0022 # to remain in Python, compiled Python, or other languages (such as C) 0023 # wherein the modified or derived code is exclusively imported into a 0024 # Python module. 0025 # 0026 # INFOSEEK CORPORATION DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS 0027 # SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND 0028 # FITNESS. IN NO EVENT SHALL INFOSEEK CORPORATION BE LIABLE FOR ANY 0029 # SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER 0030 # RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF 0031 # CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN 0032 # CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 0033 0034 0035 import os 0036 import time 0037 import marshal 0038 import re 0039 0040 __all__ = ["Stats"] 0041 0042 class Stats: 0043 """This class is used for creating reports from data generated by the 0044 Profile class. It is a "friend" of that class, and imports data either 0045 by direct access to members of Profile class, or by reading in a dictionary 0046 that was emitted (via marshal) from the Profile class. 0047 0048 The big change from the previous Profiler (in terms of raw functionality) 0049 is that an "add()" method has been provided to combine Stats from 0050 several distinct profile runs. Both the constructor and the add() 0051 method now take arbitrarily many file names as arguments. 0052 0053 All the print methods now take an argument that indicates how many lines 0054 to print. If the arg is a floating point number between 0 and 1.0, then 0055 it is taken as a decimal percentage of the available lines to be printed 0056 (e.g., .1 means print 10% of all available lines). If it is an integer, 0057 it is taken to mean the number of lines of data that you wish to have 0058 printed. 0059 0060 The sort_stats() method now processes some additional options (i.e., in 0061 addition to the old -1, 0, 1, or 2). It takes an arbitrary number of quoted 0062 strings to select the sort order. For example sort_stats('time', 'name') 0063 sorts on the major key of "internal function time", and on the minor 0064 key of 'the name of the function'. Look at the two tables in sort_stats() 0065 and get_sort_arg_defs(self) for more examples. 0066 0067 All methods now return "self", so you can string together commands like: 0068 Stats('foo', 'goo').strip_dirs().sort_stats('calls').\ 0069 print_stats(5).print_callers(5) 0070 """ 0071 0072 def __init__(self, *args): 0073 if not len(args): 0074 arg = None 0075 else: 0076 arg = args[0] 0077 args = args[1:] 0078 self.init(arg) 0079 self.add(*args) 0080 0081 def init(self, arg): 0082 self.all_callees = None # calc only if needed 0083 self.files = [] 0084 self.fcn_list = None 0085 self.total_tt = 0 0086 self.total_calls = 0 0087 self.prim_calls = 0 0088 self.max_name_len = 0 0089 self.top_level = {} 0090 self.stats = {} 0091 self.sort_arg_dict = {} 0092 self.load_stats(arg) 0093 trouble = 1 0094 try: 0095 self.get_top_level_stats() 0096 trouble = 0 0097 finally: 0098 if trouble: 0099 print "Invalid timing data", 0100 if self.files: print self.files[-1], 0101 print 0102 0103 def load_stats(self, arg): 0104 if not arg: self.stats = {} 0105 elif type(arg) == type(""): 0106 f = open(arg, 'rb') 0107 self.stats = marshal.load(f) 0108 f.close() 0109 try: 0110 file_stats = os.stat(arg) 0111 arg = time.ctime(file_stats.st_mtime) + " " + arg 0112 except: # in case this is not unix 0113 pass 0114 self.files = [ arg ] 0115 elif hasattr(arg, 'create_stats'): 0116 arg.create_stats() 0117 self.stats = arg.stats 0118 arg.stats = {} 0119 if not self.stats: 0120 raise TypeError, "Cannot create or construct a %r object from '%r''" % ( 0121 self.__class__, arg) 0122 return 0123 0124 def get_top_level_stats(self): 0125 for func, (cc, nc, tt, ct, callers) in self.stats.items(): 0126 self.total_calls += nc 0127 self.prim_calls += cc 0128 self.total_tt += tt 0129 if callers.has_key(("jprofile", 0, "profiler")): 0130 self.top_level[func] = None 0131 if len(func_std_string(func)) > self.max_name_len: 0132 self.max_name_len = len(func_std_string(func)) 0133 0134 def add(self, *arg_list): 0135 if not arg_list: return self 0136 if len(arg_list) > 1: self.add(*arg_list[1:]) 0137 other = arg_list[0] 0138 if type(self) != type(other) or self.__class__ != other.__class__: 0139 other = Stats(other) 0140 self.files += other.files 0141 self.total_calls += other.total_calls 0142 self.prim_calls += other.prim_calls 0143 self.total_tt += other.total_tt 0144 for func in other.top_level: 0145 self.top_level[func] = None 0146 0147 if self.max_name_len < other.max_name_len: 0148 self.max_name_len = other.max_name_len 0149 0150 self.fcn_list = None 0151 0152 for func, stat in other.stats.iteritems(): 0153 if func in self.stats: 0154 old_func_stat = self.stats[func] 0155 else: 0156 old_func_stat = (0, 0, 0, 0, {},) 0157 self.stats[func] = add_func_stats(old_func_stat, stat) 0158 return self 0159 0160 def dump_stats(self, filename): 0161 """Write the profile data to a file we know how to load back.""" 0162 f = file(filename, 'wb') 0163 try: 0164 marshal.dump(self.stats, f) 0165 finally: 0166 f.close() 0167 0168 # list the tuple indices and directions for sorting, 0169 # along with some printable description 0170 sort_arg_dict_default = { 0171 "calls" : (((1,-1), ), "call count"), 0172 "cumulative": (((3,-1), ), "cumulative time"), 0173 "file" : (((4, 1), ), "file name"), 0174 "line" : (((5, 1), ), "line number"), 0175 "module" : (((4, 1), ), "file name"), 0176 "name" : (((6, 1), ), "function name"), 0177 "nfl" : (((6, 1),(4, 1),(5, 1),), "name/file/line"), 0178 "pcalls" : (((0,-1), ), "call count"), 0179 "stdname" : (((7, 1), ), "standard name"), 0180 "time" : (((2,-1), ), "internal time"), 0181 } 0182 0183 def get_sort_arg_defs(self): 0184 """Expand all abbreviations that are unique.""" 0185 if not self.sort_arg_dict: 0186 self.sort_arg_dict = dict = {} 0187 bad_list = {} 0188 for word, tup in self.sort_arg_dict_default.iteritems(): 0189 fragment = word 0190 while fragment: 0191 if not fragment: 0192 break 0193 if fragment in dict: 0194 bad_list[fragment] = 0 0195 break 0196 dict[fragment] = tup 0197 fragment = fragment[:-1] 0198 for word in bad_list: 0199 del dict[word] 0200 return self.sort_arg_dict 0201 0202 def sort_stats(self, *field): 0203 if not field: 0204 self.fcn_list = 0 0205 return self 0206 if len(field) == 1 and type(field[0]) == type(1): 0207 # Be compatible with old profiler 0208 field = [ {-1: "stdname", 0209 0:"calls", 0210 1:"time", 0211 2: "cumulative" } [ field[0] ] ] 0212 0213 sort_arg_defs = self.get_sort_arg_defs() 0214 sort_tuple = () 0215 self.sort_type = "" 0216 connector = "" 0217 for word in field: 0218 sort_tuple = sort_tuple + sort_arg_defs[word][0] 0219 self.sort_type += connector + sort_arg_defs[word][1] 0220 connector = ", " 0221 0222 stats_list = [] 0223 for func, (cc, nc, tt, ct, callers) in self.stats.iteritems(): 0224 stats_list.append((cc, nc, tt, ct) + func + 0225 (func_std_string(func), func)) 0226 0227 stats_list.sort(TupleComp(sort_tuple).compare) 0228 0229 self.fcn_list = fcn_list = [] 0230 for tuple in stats_list: 0231 fcn_list.append(tuple[-1]) 0232 return self 0233 0234 def reverse_order(self): 0235 if self.fcn_list: 0236 self.fcn_list.reverse() 0237 return self 0238 0239 def strip_dirs(self): 0240 oldstats = self.stats 0241 self.stats = newstats = {} 0242 max_name_len = 0 0243 for func, (cc, nc, tt, ct, callers) in oldstats.iteritems(): 0244 newfunc = func_strip_path(func) 0245 if len(func_std_string(newfunc)) > max_name_len: 0246 max_name_len = len(func_std_string(newfunc)) 0247 newcallers = {} 0248 for func2, caller in callers.iteritems(): 0249 newcallers[func_strip_path(func2)] = caller 0250 0251 if newfunc in newstats: 0252 newstats[newfunc] = add_func_stats( 0253 newstats[newfunc], 0254 (cc, nc, tt, ct, newcallers)) 0255 else: 0256 newstats[newfunc] = (cc, nc, tt, ct, newcallers) 0257 old_top = self.top_level 0258 self.top_level = new_top = {} 0259 for func in old_top: 0260 new_top[func_strip_path(func)] = None 0261 0262 self.max_name_len = max_name_len 0263 0264 self.fcn_list = None 0265 self.all_callees = None 0266 return self 0267 0268 def calc_callees(self): 0269 if self.all_callees: return 0270 self.all_callees = all_callees = {} 0271 for func, (cc, nc, tt, ct, callers) in self.stats.iteritems(): 0272 if not func in all_callees: 0273 all_callees[func] = {} 0274 for func2, caller in callers.iteritems(): 0275 if not func2 in all_callees: 0276 all_callees[func2] = {} 0277 all_callees[func2][func] = caller 0278 return 0279 0280 #****************************************************************** 0281 # The following functions support actual printing of reports 0282 #****************************************************************** 0283 0284 # Optional "amount" is either a line count, or a percentage of lines. 0285 0286 def eval_print_amount(self, sel, list, msg): 0287 new_list = list 0288 if type(sel) == type(""): 0289 new_list = [] 0290 for func in list: 0291 if re.search(sel, func_std_string(func)): 0292 new_list.append(func) 0293 else: 0294 count = len(list) 0295 if type(sel) == type(1.0) and 0.0 <= sel < 1.0: 0296 count = int(count * sel + .5) 0297 new_list = list[:count] 0298 elif type(sel) == type(1) and 0 <= sel < count: 0299 count = sel 0300 new_list = list[:count] 0301 if len(list) != len(new_list): 0302 msg = msg + " List reduced from %r to %r due to restriction <%r>\n" % ( 0303 len(list), len(new_list), sel) 0304 0305 return new_list, msg 0306 0307 def get_print_list(self, sel_list): 0308 width = self.max_name_len 0309 if self.fcn_list: 0310 list = self.fcn_list[:] 0311 msg = " Ordered by: " + self.sort_type + '\n' 0312 else: 0313 list = self.stats.keys() 0314 msg = " Random listing order was used\n" 0315 0316 for selection in sel_list: 0317 list, msg = self.eval_print_amount(selection, list, msg) 0318 0319 count = len(list) 0320 0321 if not list: 0322 return 0, list 0323 print msg 0324 if count < len(self.stats): 0325 width = 0 0326 for func in list: 0327 if len(func_std_string(func)) > width: 0328 width = len(func_std_string(func)) 0329 return width+2, list 0330 0331 def print_stats(self, *amount): 0332 for filename in self.files: 0333 print filename 0334 if self.files: print 0335 indent = ' ' * 8 0336 for func in self.top_level: 0337 print indent, func_get_function_name(func) 0338 0339 print indent, self.total_calls, "function calls", 0340 if self.total_calls != self.prim_calls: 0341 print "(%d primitive calls)" % self.prim_calls, 0342 print "in %.3f CPU seconds" % self.total_tt 0343 print 0344 width, list = self.get_print_list(amount) 0345 if list: 0346 self.print_title() 0347 for func in list: 0348 self.print_line(func) 0349 print 0350 print 0351 return self 0352 0353 def print_callees(self, *amount): 0354 width, list = self.get_print_list(amount) 0355 if list: 0356 self.calc_callees() 0357 0358 self.print_call_heading(width, "called...") 0359 for func in list: 0360 if func in self.all_callees: 0361 self.print_call_line(width, func, self.all_callees[func]) 0362 else: 0363 self.print_call_line(width, func, {}) 0364 print 0365 print 0366 return self 0367 0368 def print_callers(self, *amount): 0369 width, list = self.get_print_list(amount) 0370 if list: 0371 self.print_call_heading(width, "was called by...") 0372 for func in list: 0373 cc, nc, tt, ct, callers = self.stats[func] 0374 self.print_call_line(width, func, callers) 0375 print 0376 print 0377 return self 0378 0379 def print_call_heading(self, name_size, column_title): 0380 print "Function ".ljust(name_size) + column_title 0381 0382 def print_call_line(self, name_size, source, call_dict): 0383 print func_std_string(source).ljust(name_size), 0384 if not call_dict: 0385 print "--" 0386 return 0387 clist = call_dict.keys() 0388 clist.sort() 0389 name_size = name_size + 1 0390 indent = "" 0391 for func in clist: 0392 name = func_std_string(func) 0393 print indent*name_size + name + '(%r)' % (call_dict[func],), \ 0394 f8(self.stats[func][3]) 0395 indent = " " 0396 0397 def print_title(self): 0398 print ' ncalls tottime percall cumtime percall', \ 0399 'filename:lineno(function)' 0400 0401 def print_line(self, func): # hack : should print percentages 0402 cc, nc, tt, ct, callers = self.stats[func] 0403 c = str(nc) 0404 if nc != cc: 0405 c = c + '/' + str(cc) 0406 print c.rjust(9), 0407 print f8(tt), 0408 if nc == 0: 0409 print ' '*8, 0410 else: 0411 print f8(tt/nc), 0412 print f8(ct), 0413 if cc == 0: 0414 print ' '*8, 0415 else: 0416 print f8(ct/cc), 0417 print func_std_string(func) 0418 0419 def ignore(self): 0420 # Deprecated since 1.5.1 -- see the docs. 0421 pass # has no return value, so use at end of line :-) 0422 0423 class TupleComp: 0424 """This class provides a generic function for comparing any two tuples. 0425 Each instance records a list of tuple-indices (from most significant 0426 to least significant), and sort direction (ascending or decending) for 0427 each tuple-index. The compare functions can then be used as the function 0428 argument to the system sort() function when a list of tuples need to be 0429 sorted in the instances order.""" 0430 0431 def __init__(self, comp_select_list): 0432 self.comp_select_list = comp_select_list 0433 0434 def compare (self, left, right): 0435 for index, direction in self.comp_select_list: 0436 l = left[index] 0437 r = right[index] 0438 if l < r: 0439 return -direction 0440 if l > r: 0441 return direction 0442 return 0 0443 0444 #************************************************************************** 0445 # func_name is a triple (file:string, line:int, name:string) 0446 0447 def func_strip_path(func_name): 0448 filename, line, name = func_name 0449 return os.path.basename(filename), line, name 0450 0451 def func_get_function_name(func): 0452 return func[2] 0453 0454 def func_std_string(func_name): # match what old profile produced 0455 return "%s:%d(%s)" % func_name 0456 0457 #************************************************************************** 0458 # The following functions combine statists for pairs functions. 0459 # The bulk of the processing involves correctly handling "call" lists, 0460 # such as callers and callees. 0461 #************************************************************************** 0462 0463 def add_func_stats(target, source): 0464 """Add together all the stats for two profile entries.""" 0465 cc, nc, tt, ct, callers = source 0466 t_cc, t_nc, t_tt, t_ct, t_callers = target 0467 return (cc+t_cc, nc+t_nc, tt+t_tt, ct+t_ct, 0468 add_callers(t_callers, callers)) 0469 0470 def add_callers(target, source): 0471 """Combine two caller lists in a single list.""" 0472 new_callers = {} 0473 for func, caller in target.iteritems(): 0474 new_callers[func] = caller 0475 for func, caller in source.iteritems(): 0476 if func in new_callers: 0477 new_callers[func] = caller + new_callers[func] 0478 else: 0479 new_callers[func] = caller 0480 return new_callers 0481 0482 def count_calls(callers): 0483 """Sum the caller statistics to get total number of calls received.""" 0484 nc = 0 0485 for calls in callers.itervalues(): 0486 nc += calls 0487 return nc 0488 0489 #************************************************************************** 0490 # The following functions support printing of reports 0491 #************************************************************************** 0492 0493 def f8(x): 0494 return "%8.3f" % x 0495 0496 #************************************************************************** 0497 # Statistics browser added by ESR, April 2001 0498 #************************************************************************** 0499 0500 if __name__ == '__main__': 0501 import cmd 0502 try: 0503 import readline 0504 except ImportError: 0505 pass 0506 0507 class ProfileBrowser(cmd.Cmd): 0508 def __init__(self, profile=None): 0509 cmd.Cmd.__init__(self) 0510 self.prompt = "% " 0511 if profile is not None: 0512 self.stats = Stats(profile) 0513 else: 0514 self.stats = None 0515 0516 def generic(self, fn, line): 0517 args = line.split() 0518 processed = [] 0519 for term in args: 0520 try: 0521 processed.append(int(term)) 0522 continue 0523 except ValueError: 0524 pass 0525 try: 0526 frac = float(term) 0527 if frac > 1 or frac < 0: 0528 print "Fraction argument mus be in [0, 1]" 0529 continue 0530 processed.append(frac) 0531 continue 0532 except ValueError: 0533 pass 0534 processed.append(term) 0535 if self.stats: 0536 getattr(self.stats, fn)(*processed) 0537 else: 0538 print "No statistics object is loaded." 0539 return 0 0540 def generic_help(self): 0541 print "Arguments may be:" 0542 print "* An integer maximum number of entries to print." 0543 print "* A decimal fractional number between 0 and 1, controlling" 0544 print " what fraction of selected entries to print." 0545 print "* A regular expression; only entries with function names" 0546 print " that match it are printed." 0547 0548 def do_add(self, line): 0549 self.stats.add(line) 0550 return 0 0551 def help_add(self): 0552 print "Add profile info from given file to current statistics object." 0553 0554 def do_callees(self, line): 0555 return self.generic('print_callees', line) 0556 def help_callees(self): 0557 print "Print callees statistics from the current stat object." 0558 self.generic_help() 0559 0560 def do_callers(self, line): 0561 return self.generic('print_callers', line) 0562 def help_callers(self): 0563 print "Print callers statistics from the current stat object." 0564 self.generic_help() 0565 0566 def do_EOF(self, line): 0567 print "" 0568 return 1 0569 def help_EOF(self): 0570 print "Leave the profile brower." 0571 0572 def do_quit(self, line): 0573 return 1 0574 def help_quit(self): 0575 print "Leave the profile brower." 0576 0577 def do_read(self, line): 0578 if line: 0579 try: 0580 self.stats = Stats(line) 0581 except IOError, args: 0582 print args[1] 0583 return 0584 self.prompt = line + "% " 0585 elif len(self.prompt) > 2: 0586 line = self.prompt[-2:] 0587 else: 0588 print "No statistics object is current -- cannot reload." 0589 return 0 0590 def help_read(self): 0591 print "Read in profile data from a specified file." 0592 0593 def do_reverse(self, line): 0594 self.stats.reverse_order() 0595 return 0 0596 def help_reverse(self): 0597 print "Reverse the sort order of the profiling report." 0598 0599 def do_sort(self, line): 0600 abbrevs = self.stats.get_sort_arg_defs() 0601 if line and not filter(lambda x,a=abbrevs: x not in a,line.split()): 0602 self.stats.sort_stats(*line.split()) 0603 else: 0604 print "Valid sort keys (unique prefixes are accepted):" 0605 for (key, value) in Stats.sort_arg_dict_default.iteritems(): 0606 print "%s -- %s" % (key, value[1]) 0607 return 0 0608 def help_sort(self): 0609 print "Sort profile data according to specified keys." 0610 print "(Typing `sort' without arguments lists valid keys.)" 0611 def complete_sort(self, text, *args): 0612 return [a for a in Stats.sort_arg_dict_default if a.startswith(text)] 0613 0614 def do_stats(self, line): 0615 return self.generic('print_stats', line) 0616 def help_stats(self): 0617 print "Print statistics from the current stat object." 0618 self.generic_help() 0619 0620 def do_strip(self, line): 0621 self.stats.strip_dirs() 0622 return 0 0623 def help_strip(self): 0624 print "Strip leading path information from filenames in the report." 0625 0626 def postcmd(self, stop, line): 0627 if stop: 0628 return stop 0629 return None 0630 0631 import sys 0632 print "Welcome to the profile statistics browser." 0633 if len(sys.argv) > 1: 0634 initprofile = sys.argv[1] 0635 else: 0636 initprofile = None 0637 try: 0638 ProfileBrowser(initprofile).cmdloop() 0639 print "Goodbye." 0640 except KeyboardInterrupt: 0641 pass 0642 0643 # That's all, folks. 0644
Generated by PyXR 0.9.4