Module pstats
[hide private]
[frames] | no frames]

Source Code for Module pstats

  1  """Class for printing reports on profiled python code.""" 
  2   
  3  # Class for printing reports on profiled python code. rev 1.0  4/1/94 
  4  # 
  5  # Based on prior profile module by Sjoerd Mullender... 
  6  #   which was hacked somewhat by: Guido van Rossum 
  7  # 
  8  # see profile.doc and profile.py for more info. 
  9   
 10  # Copyright 1994, by InfoSeek Corporation, all rights reserved. 
 11  # Written by James Roskind 
 12  # 
 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 
 24  # Python module. 
 25  # 
 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. 
 33   
 34   
 35  import os 
 36  import time 
 37  import marshal 
 38  import re 
 39   
 40  __all__ = ["Stats"] 
 41   
42 -class Stats:
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. 47 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. 52 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 58 printed. 59 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. 66 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) 70 """ 71
72 - def __init__(self, *args):
73 if not len(args): 74 arg = None 75 else: 76 arg = args[0] 77 args = args[1:] 78 self.init(arg) 79 self.add(*args)
80
81 - def init(self, arg):
82 self.all_callees = None # calc only if needed 83 self.files = [] 84 self.fcn_list = None 85 self.total_tt = 0 86 self.total_calls = 0 87 self.prim_calls = 0 88 self.max_name_len = 0 89 self.top_level = {} 90 self.stats = {} 91 self.sort_arg_dict = {} 92 self.load_stats(arg) 93 trouble = 1 94 try: 95 self.get_top_level_stats() 96 trouble = 0 97 finally: 98 if trouble: 99 print "Invalid timing data", 100 if self.files: print self.files[-1], 101 print
102
103 - def load_stats(self, arg):
104 if not arg: self.stats = {} 105 elif type(arg) == type(""): 106 f = open(arg, 'rb') 107 self.stats = marshal.load(f) 108 f.close() 109 try: 110 file_stats = os.stat(arg) 111 arg = time.ctime(file_stats.st_mtime) + " " + arg 112 except: # in case this is not unix 113 pass 114 self.files = [ arg ] 115 elif hasattr(arg, 'create_stats'): 116 arg.create_stats() 117 self.stats = arg.stats 118 arg.stats = {} 119 if not self.stats: 120 raise TypeError, "Cannot create or construct a %r object from '%r''" % ( 121 self.__class__, arg) 122 return
123
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 128 self.total_tt += tt 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))
133
134 - def add(self, *arg_list):
135 if not arg_list: return self 136 if len(arg_list) > 1: self.add(*arg_list[1:]) 137 other = arg_list[0] 138 if type(self) != type(other) or self.__class__ != other.__class__: 139 other = Stats(other) 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 146 147 if self.max_name_len < other.max_name_len: 148 self.max_name_len = other.max_name_len 149 150 self.fcn_list = None 151 152 for func, stat in other.stats.iteritems(): 153 if func in self.stats: 154 old_func_stat = self.stats[func] 155 else: 156 old_func_stat = (0, 0, 0, 0, {},) 157 self.stats[func] = add_func_stats(old_func_stat, stat) 158 return self
159
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') 163 try: 164 marshal.dump(self.stats, f) 165 finally: 166 f.close()
167 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"), 181 } 182
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 = {} 187 bad_list = {} 188 for word, tup in self.sort_arg_dict_default.iteritems(): 189 fragment = word 190 while fragment: 191 if not fragment: 192 break 193 if fragment in dict: 194 bad_list[fragment] = 0 195 break 196 dict[fragment] = tup 197 fragment = fragment[:-1] 198 for word in bad_list: 199 del dict[word] 200 return self.sort_arg_dict
201
202 - def sort_stats(self, *field):
203 if not field: 204 self.fcn_list = 0 205 return self 206 if len(field) == 1 and type(field[0]) == type(1): 207 # Be compatible with old profiler 208 field = [ {-1: "stdname", 209 0:"calls", 210 1:"time", 211 2: "cumulative" } [ field[0] ] ] 212 213 sort_arg_defs = self.get_sort_arg_defs() 214 sort_tuple = () 215 self.sort_type = "" 216 connector = "" 217 for word in field: 218 sort_tuple = sort_tuple + sort_arg_defs[word][0] 219 self.sort_type += connector + sort_arg_defs[word][1] 220 connector = ", " 221 222 stats_list = [] 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)) 226 227 stats_list.sort(TupleComp(sort_tuple).compare) 228 229 self.fcn_list = fcn_list = [] 230 for tuple in stats_list: 231 fcn_list.append(tuple[-1]) 232 return self
233
234 - def reverse_order(self):
235 if self.fcn_list: 236 self.fcn_list.reverse() 237 return self
238
239 - def strip_dirs(self):
240 oldstats = self.stats 241 self.stats = newstats = {} 242 max_name_len = 0 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)) 247 newcallers = {} 248 for func2, caller in callers.iteritems(): 249 newcallers[func_strip_path(func2)] = caller 250 251 if newfunc in newstats: 252 newstats[newfunc] = add_func_stats( 253 newstats[newfunc], 254 (cc, nc, tt, ct, newcallers)) 255 else: 256 newstats[newfunc] = (cc, nc, tt, ct, newcallers) 257 old_top = self.top_level 258 self.top_level = new_top = {} 259 for func in old_top: 260 new_top[func_strip_path(func)] = None 261 262 self.max_name_len = max_name_len 263 264 self.fcn_list = None 265 self.all_callees = None 266 return self
267
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 278 return
279 280 #****************************************************************** 281 # The following functions support actual printing of reports 282 #****************************************************************** 283 284 # Optional "amount" is either a line count, or a percentage of lines. 285
286 - def eval_print_amount(self, sel, list, msg):
287 new_list = list 288 if type(sel) == type(""): 289 new_list = [] 290 for func in list: 291 if re.search(sel, func_std_string(func)): 292 new_list.append(func) 293 else: 294 count = len(list) 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: 299 count = sel 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) 304 305 return new_list, msg
306
307 - def get_print_list(self, sel_list):
308 width = self.max_name_len 309 if self.fcn_list: 310 list = self.fcn_list[:] 311 msg = " Ordered by: " + self.sort_type + '\n' 312 else: 313 list = self.stats.keys() 314 msg = " Random listing order was used\n" 315 316 for selection in sel_list: 317 list, msg = self.eval_print_amount(selection, list, msg) 318 319 count = len(list) 320 321 if not list: 322 return 0, list 323 print msg 324 if count < len(self.stats): 325 width = 0 326 for func in list: 327 if len(func_std_string(func)) > width: 328 width = len(func_std_string(func)) 329 return width+2, list
330
331 - def print_stats(self, *amount):
332 for filename in self.files: 333 print filename 334 if self.files: print 335 indent = ' ' * 8 336 for func in self.top_level: 337 print indent, func_get_function_name(func) 338 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 343 print 344 width, list = self.get_print_list(amount) 345 if list: 346 self.print_title() 347 for func in list: 348 self.print_line(func) 349 print 350 print 351 return self
352
353 - def print_callees(self, *amount):
354 width, list = self.get_print_list(amount) 355 if list: 356 self.calc_callees() 357 358 self.print_call_heading(width, "called...") 359 for func in list: 360 if func in self.all_callees: 361 self.print_call_line(width, func, self.all_callees[func]) 362 else: 363 self.print_call_line(width, func, {}) 364 print 365 print 366 return self
367
368 - def print_callers(self, *amount):
369 width, list = self.get_print_list(amount) 370 if list: 371 self.print_call_heading(width, "was called by...") 372 for func in list: 373 cc, nc, tt, ct, callers = self.stats[func] 374 self.print_call_line(width, func, callers) 375 print 376 print 377 return self
378
379 - def print_call_heading(self, name_size, column_title):
380 print "Function ".ljust(name_size) + column_title
381
382 - def print_call_line(self, name_size, source, call_dict):
383 print func_std_string(source).ljust(name_size), 384 if not call_dict: 385 print "--" 386 return 387 clist = call_dict.keys() 388 clist.sort() 389 name_size = name_size + 1 390 indent = "" 391 for func in clist: 392 name = func_std_string(func) 393 print indent*name_size + name + '(%r)' % (call_dict[func],), \ 394 f8(self.stats[func][3]) 395 indent = " "
396
397 - def print_title(self):
398 print ' ncalls tottime percall cumtime percall', \ 399 'filename:lineno(function)'
400
401 - def print_line(self, func): # hack : should print percentages
402 cc, nc, tt, ct, callers = self.stats[func] 403 c = str(nc) 404 if nc != cc: 405 c = c + '/' + str(cc) 406 print c.rjust(9), 407 print f8(tt), 408 if nc == 0: 409 print ' '*8, 410 else: 411 print f8(tt/nc), 412 print f8(ct), 413 if cc == 0: 414 print ' '*8, 415 else: 416 print f8(ct/cc), 417 print func_std_string(func)
418
419 - def ignore(self):
420 # Deprecated since 1.5.1 -- see the docs. 421 pass # has no return value, so use at end of line :-)
422
423 -class TupleComp:
424 """This class provides a generic function for comparing any two tuples. 425 Each instance records a list of tuple-indices (from most significant 426 to least significant), and sort direction (ascending or decending) for 427 each tuple-index. The compare functions can then be used as the function 428 argument to the system sort() function when a list of tuples need to be 429 sorted in the instances order.""" 430
431 - def __init__(self, comp_select_list):
432 self.comp_select_list = comp_select_list
433
434 - def compare (self, left, right):
435 for index, direction in self.comp_select_list: 436 l = left[index] 437 r = right[index] 438 if l < r: 439 return -direction 440 if l > r: 441 return direction 442 return 0
443 444 #************************************************************************** 445 # func_name is a triple (file:string, line:int, name:string) 446
447 -def func_strip_path(func_name):
448 filename, line, name = func_name 449 return os.path.basename(filename), line, name
450
451 -def func_get_function_name(func):
452 return func[2]
453
454 -def func_std_string(func_name): # match what old profile produced
455 return "%s:%d(%s)" % func_name 456 457 #************************************************************************** 458 # The following functions combine statists for pairs functions. 459 # The bulk of the processing involves correctly handling "call" lists, 460 # such as callers and callees. 461 #************************************************************************** 462
463 -def add_func_stats(target, source):
464 """Add together all the stats for two profile entries.""" 465 cc, nc, tt, ct, callers = source 466 t_cc, t_nc, t_tt, t_ct, t_callers = target 467 return (cc+t_cc, nc+t_nc, tt+t_tt, ct+t_ct, 468 add_callers(t_callers, callers))
469
470 -def add_callers(target, source):
471 """Combine two caller lists in a single list.""" 472 new_callers = {} 473 for func, caller in target.iteritems(): 474 new_callers[func] = caller 475 for func, caller in source.iteritems(): 476 if func in new_callers: 477 new_callers[func] = caller + new_callers[func] 478 else: 479 new_callers[func] = caller 480 return new_callers
481
482 -def count_calls(callers):
483 """Sum the caller statistics to get total number of calls received.""" 484 nc = 0 485 for calls in callers.itervalues(): 486 nc += calls 487 return nc
488 489 #************************************************************************** 490 # The following functions support printing of reports 491 #************************************************************************** 492
493 -def f8(x):
494 return "%8.3f" % x
495 496 #************************************************************************** 497 # Statistics browser added by ESR, April 2001 498 #************************************************************************** 499 500 if __name__ == '__main__': 501 import cmd 502 try: 503 import readline 504 except ImportError: 505 pass 506 507 class ProfileBrowser(cmd.Cmd): 508 def __init__(self, profile=None): 509 cmd.Cmd.__init__(self) 510 self.prompt = "% " 511 if profile is not None: 512 self.stats = Stats(profile) 513 else: 514 self.stats = None 515 516 def generic(self, fn, line): 517 args = line.split() 518 processed = [] 519 for term in args: 520 try: 521 processed.append(int(term)) 522 continue 523 except ValueError: 524 pass 525 try: 526 frac = float(term) 527 if frac > 1 or frac < 0: 528 print "Fraction argument mus be in [0, 1]" 529 continue 530 processed.append(frac) 531 continue 532 except ValueError: 533 pass 534 processed.append(term) 535 if self.stats: 536 getattr(self.stats, fn)(*processed) 537 else: 538 print "No statistics object is loaded." 539 return 0 540 def generic_help(self): 541 print "Arguments may be:" 542 print "* An integer maximum number of entries to print." 543 print "* A decimal fractional number between 0 and 1, controlling" 544 print " what fraction of selected entries to print." 545 print "* A regular expression; only entries with function names" 546 print " that match it are printed." 547 548 def do_add(self, line): 549 self.stats.add(line) 550 return 0 551 def help_add(self): 552 print "Add profile info from given file to current statistics object." 553 554 def do_callees(self, line): 555 return self.generic('print_callees', line) 556 def help_callees(self): 557 print "Print callees statistics from the current stat object." 558 self.generic_help() 559 560 def do_callers(self, line): 561 return self.generic('print_callers', line) 562 def help_callers(self): 563 print "Print callers statistics from the current stat object." 564 self.generic_help() 565 566 def do_EOF(self, line): 567 print "" 568 return 1 569 def help_EOF(self): 570 print "Leave the profile brower." 571 572 def do_quit(self, line): 573 return 1 574 def help_quit(self): 575 print "Leave the profile brower." 576 577 def do_read(self, line): 578 if line: 579 try: 580 self.stats = Stats(line) 581 except IOError, args: 582 print args[1] 583 return 584 self.prompt = line + "% " 585 elif len(self.prompt) > 2: 586 line = self.prompt[-2:] 587 else: 588 print "No statistics object is current -- cannot reload." 589 return 0 590 def help_read(self): 591 print "Read in profile data from a specified file." 592 593 def do_reverse(self, line): 594 self.stats.reverse_order() 595 return 0 596 def help_reverse(self): 597 print "Reverse the sort order of the profiling report." 598 599 def do_sort(self, line): 600 abbrevs = self.stats.get_sort_arg_defs() 601 if line and not filter(lambda x,a=abbrevs: x not in a,line.split()): 602 self.stats.sort_stats(*line.split()) 603 else: 604 print "Valid sort keys (unique prefixes are accepted):" 605 for (key, value) in Stats.sort_arg_dict_default.iteritems(): 606 print "%s -- %s" % (key, value[1]) 607 return 0 608 def help_sort(self): 609 print "Sort profile data according to specified keys." 610 print "(Typing `sort' without arguments lists valid keys.)" 611 def complete_sort(self, text, *args): 612 return [a for a in Stats.sort_arg_dict_default if a.startswith(text)] 613 614 def do_stats(self, line): 615 return self.generic('print_stats', line) 616 def help_stats(self): 617 print "Print statistics from the current stat object." 618 self.generic_help() 619 620 def do_strip(self, line): 621 self.stats.strip_dirs() 622 return 0 623 def help_strip(self): 624 print "Strip leading path information from filenames in the report." 625 626 def postcmd(self, stop, line): 627 if stop: 628 return stop 629 return None 630 631 import sys 632 print "Welcome to the profile statistics browser." 633 if len(sys.argv) > 1: 634 initprofile = sys.argv[1] 635 else: 636 initprofile = None 637 try: 638 ProfileBrowser(initprofile).cmdloop() 639 print "Goodbye." 640 except KeyboardInterrupt: 641 pass 642 643 # That's all, folks. 644