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.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 35import sys 36import os 37import time 38import marshal 39import re 40from functools import cmp_to_key 41 42__all__ = ["Stats"] 43 44class Stats: 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. 49 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. 54 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 60 printed. 61 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. 68 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) 72 """ 73 74 def __init__(self, *args, **kwds): 75 # I can't figure out how to explictly specify a stream keyword arg 76 # with *args: 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 80 if "stream" in kwds: 81 self.stream = kwds["stream"] 82 del kwds["stream"] 83 if kwds: 84 keys = kwds.keys() 85 keys.sort() 86 extras = ", ".join(["%s=%s" % (k, kwds[k]) for k in keys]) 87 raise ValueError, "unrecognized keyword args: %s" % extras 88 if not len(args): 89 arg = None 90 else: 91 arg = args[0] 92 args = args[1:] 93 self.init(arg) 94 self.add(*args) 95 96 def init(self, arg): 97 self.all_callees = None # calc only if needed 98 self.files = [] 99 self.fcn_list = None 100 self.total_tt = 0 101 self.total_calls = 0 102 self.prim_calls = 0 103 self.max_name_len = 0 104 self.top_level = {} 105 self.stats = {} 106 self.sort_arg_dict = {} 107 self.load_stats(arg) 108 trouble = 1 109 try: 110 self.get_top_level_stats() 111 trouble = 0 112 finally: 113 if trouble: 114 print >> self.stream, "Invalid timing data", 115 if self.files: print >> self.stream, self.files[-1], 116 print >> self.stream 117 118 def load_stats(self, arg): 119 if not arg: self.stats = {} 120 elif isinstance(arg, basestring): 121 f = open(arg, 'rb') 122 self.stats = marshal.load(f) 123 f.close() 124 try: 125 file_stats = os.stat(arg) 126 arg = time.ctime(file_stats.st_mtime) + " " + arg 127 except: # in case this is not unix 128 pass 129 self.files = [ arg ] 130 elif hasattr(arg, 'create_stats'): 131 arg.create_stats() 132 self.stats = arg.stats 133 arg.stats = {} 134 if not self.stats: 135 raise TypeError, "Cannot create or construct a %r object from '%r''" % ( 136 self.__class__, arg) 137 return 138 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 143 self.total_tt += tt 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)) 148 149 def add(self, *arg_list): 150 if not arg_list: return self 151 if len(arg_list) > 1: self.add(*arg_list[1:]) 152 other = arg_list[0] 153 if type(self) != type(other) or self.__class__ != other.__class__: 154 other = Stats(other) 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 161 162 if self.max_name_len < other.max_name_len: 163 self.max_name_len = other.max_name_len 164 165 self.fcn_list = None 166 167 for func, stat in other.stats.iteritems(): 168 if func in self.stats: 169 old_func_stat = self.stats[func] 170 else: 171 old_func_stat = (0, 0, 0, 0, {},) 172 self.stats[func] = add_func_stats(old_func_stat, stat) 173 return self 174 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') 178 try: 179 marshal.dump(self.stats, f) 180 finally: 181 f.close() 182 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"), 196 } 197 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 = {} 202 bad_list = {} 203 for word, tup in self.sort_arg_dict_default.iteritems(): 204 fragment = word 205 while fragment: 206 if not fragment: 207 break 208 if fragment in dict: 209 bad_list[fragment] = 0 210 break 211 dict[fragment] = tup 212 fragment = fragment[:-1] 213 for word in bad_list: 214 del dict[word] 215 return self.sort_arg_dict 216 217 def sort_stats(self, *field): 218 if not field: 219 self.fcn_list = 0 220 return self 221 if len(field) == 1 and isinstance(field[0], (int, long)): 222 # Be compatible with old profiler 223 field = [ {-1: "stdname", 224 0: "calls", 225 1: "time", 226 2: "cumulative"}[field[0]] ] 227 228 sort_arg_defs = self.get_sort_arg_defs() 229 sort_tuple = () 230 self.sort_type = "" 231 connector = "" 232 for word in field: 233 sort_tuple = sort_tuple + sort_arg_defs[word][0] 234 self.sort_type += connector + sort_arg_defs[word][1] 235 connector = ", " 236 237 stats_list = [] 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)) 241 242 stats_list.sort(key=cmp_to_key(TupleComp(sort_tuple).compare)) 243 244 self.fcn_list = fcn_list = [] 245 for tuple in stats_list: 246 fcn_list.append(tuple[-1]) 247 return self 248 249 def reverse_order(self): 250 if self.fcn_list: 251 self.fcn_list.reverse() 252 return self 253 254 def strip_dirs(self): 255 oldstats = self.stats 256 self.stats = newstats = {} 257 max_name_len = 0 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)) 262 newcallers = {} 263 for func2, caller in callers.iteritems(): 264 newcallers[func_strip_path(func2)] = caller 265 266 if newfunc in newstats: 267 newstats[newfunc] = add_func_stats( 268 newstats[newfunc], 269 (cc, nc, tt, ct, newcallers)) 270 else: 271 newstats[newfunc] = (cc, nc, tt, ct, newcallers) 272 old_top = self.top_level 273 self.top_level = new_top = {} 274 for func in old_top: 275 new_top[func_strip_path(func)] = None 276 277 self.max_name_len = max_name_len 278 279 self.fcn_list = None 280 self.all_callees = None 281 return self 282 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 293 return 294 295 #****************************************************************** 296 # The following functions support actual printing of reports 297 #****************************************************************** 298 299 # Optional "amount" is either a line count, or a percentage of lines. 300 301 def eval_print_amount(self, sel, list, msg): 302 new_list = list 303 if isinstance(sel, basestring): 304 try: 305 rex = re.compile(sel) 306 except re.error: 307 msg += " <Invalid regular expression %r>\n" % sel 308 return new_list, msg 309 new_list = [] 310 for func in list: 311 if rex.search(func_std_string(func)): 312 new_list.append(func) 313 else: 314 count = len(list) 315 if isinstance(sel, float) and 0.0 <= sel < 1.0: 316 count = int(count * sel + .5) 317 new_list = list[:count] 318 elif isinstance(sel, (int, long)) and 0 <= sel < count: 319 count = sel 320 new_list = list[:count] 321 if len(list) != len(new_list): 322 msg += " List reduced from %r to %r due to restriction <%r>\n" % ( 323 len(list), len(new_list), sel) 324 325 return new_list, msg 326 327 def get_print_list(self, sel_list): 328 width = self.max_name_len 329 if self.fcn_list: 330 stat_list = self.fcn_list[:] 331 msg = " Ordered by: " + self.sort_type + '\n' 332 else: 333 stat_list = self.stats.keys() 334 msg = " Random listing order was used\n" 335 336 for selection in sel_list: 337 stat_list, msg = self.eval_print_amount(selection, stat_list, msg) 338 339 count = len(stat_list) 340 341 if not stat_list: 342 return 0, stat_list 343 print >> self.stream, msg 344 if count < len(self.stats): 345 width = 0 346 for func in stat_list: 347 if len(func_std_string(func)) > width: 348 width = len(func_std_string(func)) 349 return width+2, stat_list 350 351 def print_stats(self, *amount): 352 for filename in self.files: 353 print >> self.stream, filename 354 if self.files: print >> self.stream 355 indent = ' ' * 8 356 for func in self.top_level: 357 print >> self.stream, indent, func_get_function_name(func) 358 359 print >> self.stream, indent, self.total_calls, "function calls", 360 if self.total_calls != self.prim_calls: 361 print >> self.stream, "(%d primitive calls)" % self.prim_calls, 362 print >> self.stream, "in %.3f seconds" % self.total_tt 363 print >> self.stream 364 width, list = self.get_print_list(amount) 365 if list: 366 self.print_title() 367 for func in list: 368 self.print_line(func) 369 print >> self.stream 370 print >> self.stream 371 return self 372 373 def print_callees(self, *amount): 374 width, list = self.get_print_list(amount) 375 if list: 376 self.calc_callees() 377 378 self.print_call_heading(width, "called...") 379 for func in list: 380 if func in self.all_callees: 381 self.print_call_line(width, func, self.all_callees[func]) 382 else: 383 self.print_call_line(width, func, {}) 384 print >> self.stream 385 print >> self.stream 386 return self 387 388 def print_callers(self, *amount): 389 width, list = self.get_print_list(amount) 390 if list: 391 self.print_call_heading(width, "was called by...") 392 for func in list: 393 cc, nc, tt, ct, callers = self.stats[func] 394 self.print_call_line(width, func, callers, "<-") 395 print >> self.stream 396 print >> self.stream 397 return self 398 399 def print_call_heading(self, name_size, column_title): 400 print >> self.stream, "Function ".ljust(name_size) + column_title 401 # print sub-header only if we have new-style callers 402 subheader = False 403 for cc, nc, tt, ct, callers in self.stats.itervalues(): 404 if callers: 405 value = callers.itervalues().next() 406 subheader = isinstance(value, tuple) 407 break 408 if subheader: 409 print >> self.stream, " "*name_size + " ncalls tottime cumtime" 410 411 def print_call_line(self, name_size, source, call_dict, arrow="->"): 412 print >> self.stream, func_std_string(source).ljust(name_size) + arrow, 413 if not call_dict: 414 print >> self.stream 415 return 416 clist = call_dict.keys() 417 clist.sort() 418 indent = "" 419 for func in clist: 420 name = func_std_string(func) 421 value = call_dict[func] 422 if isinstance(value, tuple): 423 nc, cc, tt, ct = value 424 if nc != cc: 425 substats = '%d/%d' % (nc, cc) 426 else: 427 substats = '%d' % (nc,) 428 substats = '%s %s %s %s' % (substats.rjust(7+2*len(indent)), 429 f8(tt), f8(ct), name) 430 left_width = name_size + 1 431 else: 432 substats = '%s(%r) %s' % (name, value, f8(self.stats[func][3])) 433 left_width = name_size + 3 434 print >> self.stream, indent*left_width + substats 435 indent = " " 436 437 def print_title(self): 438 print >> self.stream, ' ncalls tottime percall cumtime percall', 439 print >> self.stream, 'filename:lineno(function)' 440 441 def print_line(self, func): # hack : should print percentages 442 cc, nc, tt, ct, callers = self.stats[func] 443 c = str(nc) 444 if nc != cc: 445 c = c + '/' + str(cc) 446 print >> self.stream, c.rjust(9), 447 print >> self.stream, f8(tt), 448 if nc == 0: 449 print >> self.stream, ' '*8, 450 else: 451 print >> self.stream, f8(float(tt)/nc), 452 print >> self.stream, f8(ct), 453 if cc == 0: 454 print >> self.stream, ' '*8, 455 else: 456 print >> self.stream, f8(float(ct)/cc), 457 print >> self.stream, func_std_string(func) 458 459class TupleComp: 460 """This class provides a generic function for comparing any two tuples. 461 Each instance records a list of tuple-indices (from most significant 462 to least significant), and sort direction (ascending or decending) for 463 each tuple-index. The compare functions can then be used as the function 464 argument to the system sort() function when a list of tuples need to be 465 sorted in the instances order.""" 466 467 def __init__(self, comp_select_list): 468 self.comp_select_list = comp_select_list 469 470 def compare (self, left, right): 471 for index, direction in self.comp_select_list: 472 l = left[index] 473 r = right[index] 474 if l < r: 475 return -direction 476 if l > r: 477 return direction 478 return 0 479 480#************************************************************************** 481# func_name is a triple (file:string, line:int, name:string) 482 483def func_strip_path(func_name): 484 filename, line, name = func_name 485 return os.path.basename(filename), line, name 486 487def func_get_function_name(func): 488 return func[2] 489 490def func_std_string(func_name): # match what old profile produced 491 if func_name[:2] == ('~', 0): 492 # special case for built-in functions 493 name = func_name[2] 494 if name.startswith('<') and name.endswith('>'): 495 return '{%s}' % name[1:-1] 496 else: 497 return name 498 else: 499 return "%s:%d(%s)" % func_name 500 501#************************************************************************** 502# The following functions combine statists for pairs functions. 503# The bulk of the processing involves correctly handling "call" lists, 504# such as callers and callees. 505#************************************************************************** 506 507def add_func_stats(target, source): 508 """Add together all the stats for two profile entries.""" 509 cc, nc, tt, ct, callers = source 510 t_cc, t_nc, t_tt, t_ct, t_callers = target 511 return (cc+t_cc, nc+t_nc, tt+t_tt, ct+t_ct, 512 add_callers(t_callers, callers)) 513 514def add_callers(target, source): 515 """Combine two caller lists in a single list.""" 516 new_callers = {} 517 for func, caller in target.iteritems(): 518 new_callers[func] = caller 519 for func, caller in source.iteritems(): 520 if func in new_callers: 521 if isinstance(caller, tuple): 522 # format used by cProfile 523 new_callers[func] = tuple([i[0] + i[1] for i in 524 zip(caller, new_callers[func])]) 525 else: 526 # format used by profile 527 new_callers[func] += caller 528 else: 529 new_callers[func] = caller 530 return new_callers 531 532def count_calls(callers): 533 """Sum the caller statistics to get total number of calls received.""" 534 nc = 0 535 for calls in callers.itervalues(): 536 nc += calls 537 return nc 538 539#************************************************************************** 540# The following functions support printing of reports 541#************************************************************************** 542 543def f8(x): 544 return "%8.3f" % x 545 546#************************************************************************** 547# Statistics browser added by ESR, April 2001 548#************************************************************************** 549 550if __name__ == '__main__': 551 import cmd 552 try: 553 import readline 554 except ImportError: 555 pass 556 557 class ProfileBrowser(cmd.Cmd): 558 def __init__(self, profile=None): 559 cmd.Cmd.__init__(self) 560 self.prompt = "% " 561 self.stats = None 562 self.stream = sys.stdout 563 if profile is not None: 564 self.do_read(profile) 565 566 def generic(self, fn, line): 567 args = line.split() 568 processed = [] 569 for term in args: 570 try: 571 processed.append(int(term)) 572 continue 573 except ValueError: 574 pass 575 try: 576 frac = float(term) 577 if frac > 1 or frac < 0: 578 print >> self.stream, "Fraction argument must be in [0, 1]" 579 continue 580 processed.append(frac) 581 continue 582 except ValueError: 583 pass 584 processed.append(term) 585 if self.stats: 586 getattr(self.stats, fn)(*processed) 587 else: 588 print >> self.stream, "No statistics object is loaded." 589 return 0 590 def generic_help(self): 591 print >> self.stream, "Arguments may be:" 592 print >> self.stream, "* An integer maximum number of entries to print." 593 print >> self.stream, "* A decimal fractional number between 0 and 1, controlling" 594 print >> self.stream, " what fraction of selected entries to print." 595 print >> self.stream, "* A regular expression; only entries with function names" 596 print >> self.stream, " that match it are printed." 597 598 def do_add(self, line): 599 if self.stats: 600 self.stats.add(line) 601 else: 602 print >> self.stream, "No statistics object is loaded." 603 return 0 604 def help_add(self): 605 print >> self.stream, "Add profile info from given file to current statistics object." 606 607 def do_callees(self, line): 608 return self.generic('print_callees', line) 609 def help_callees(self): 610 print >> self.stream, "Print callees statistics from the current stat object." 611 self.generic_help() 612 613 def do_callers(self, line): 614 return self.generic('print_callers', line) 615 def help_callers(self): 616 print >> self.stream, "Print callers statistics from the current stat object." 617 self.generic_help() 618 619 def do_EOF(self, line): 620 print >> self.stream, "" 621 return 1 622 def help_EOF(self): 623 print >> self.stream, "Leave the profile brower." 624 625 def do_quit(self, line): 626 return 1 627 def help_quit(self): 628 print >> self.stream, "Leave the profile brower." 629 630 def do_read(self, line): 631 if line: 632 try: 633 self.stats = Stats(line) 634 except IOError, args: 635 print >> self.stream, args[1] 636 return 637 except Exception as err: 638 print >> self.stream, err.__class__.__name__ + ':', err 639 return 640 self.prompt = line + "% " 641 elif len(self.prompt) > 2: 642 line = self.prompt[:-2] 643 self.do_read(line) 644 else: 645 print >> self.stream, "No statistics object is current -- cannot reload." 646 return 0 647 def help_read(self): 648 print >> self.stream, "Read in profile data from a specified file." 649 print >> self.stream, "Without argument, reload the current file." 650 651 def do_reverse(self, line): 652 if self.stats: 653 self.stats.reverse_order() 654 else: 655 print >> self.stream, "No statistics object is loaded." 656 return 0 657 def help_reverse(self): 658 print >> self.stream, "Reverse the sort order of the profiling report." 659 660 def do_sort(self, line): 661 if not self.stats: 662 print >> self.stream, "No statistics object is loaded." 663 return 664 abbrevs = self.stats.get_sort_arg_defs() 665 if line and all((x in abbrevs) for x in line.split()): 666 self.stats.sort_stats(*line.split()) 667 else: 668 print >> self.stream, "Valid sort keys (unique prefixes are accepted):" 669 for (key, value) in Stats.sort_arg_dict_default.iteritems(): 670 print >> self.stream, "%s -- %s" % (key, value[1]) 671 return 0 672 def help_sort(self): 673 print >> self.stream, "Sort profile data according to specified keys." 674 print >> self.stream, "(Typing `sort' without arguments lists valid keys.)" 675 def complete_sort(self, text, *args): 676 return [a for a in Stats.sort_arg_dict_default if a.startswith(text)] 677 678 def do_stats(self, line): 679 return self.generic('print_stats', line) 680 def help_stats(self): 681 print >> self.stream, "Print statistics from the current stat object." 682 self.generic_help() 683 684 def do_strip(self, line): 685 if self.stats: 686 self.stats.strip_dirs() 687 else: 688 print >> self.stream, "No statistics object is loaded." 689 def help_strip(self): 690 print >> self.stream, "Strip leading path information from filenames in the report." 691 692 def help_help(self): 693 print >> self.stream, "Show help for a given command." 694 695 def postcmd(self, stop, line): 696 if stop: 697 return stop 698 return None 699 700 import sys 701 if len(sys.argv) > 1: 702 initprofile = sys.argv[1] 703 else: 704 initprofile = None 705 try: 706 browser = ProfileBrowser(initprofile) 707 print >> browser.stream, "Welcome to the profile statistics browser." 708 browser.cmdloop() 709 print >> browser.stream, "Goodbye." 710 except KeyboardInterrupt: 711 pass 712 713# That's all, folks. 714