home *** CD-ROM | disk | FTP | other *** search
/ OS/2 Shareware BBS: 10 Tools / 10-Tools.zip / pyos2bin.zip / Lib / pstats.py < prev    next >
Text File  |  1997-10-22  |  16KB  |  533 lines

  1. #
  2. # Class for printing reports on profiled python code. rev 1.0  4/1/94
  3. #
  4. # Based on prior profile module by Sjoerd Mullender...
  5. #   which was hacked somewhat by: Guido van Rossum
  6. #
  7. # see jprofile.doc and jprofile.py for more info.
  8.  
  9. # Copyright 1994, by InfoSeek Corporation, all rights reserved.
  10. # Written by James Roskind
  11. # Permission to use, copy, modify, and distribute this Python software
  12. # and its associated documentation for any purpose (subject to the
  13. # restriction in the following sentence) without fee is hereby granted,
  14. # provided that the above copyright notice appears in all copies, and
  15. # that both that copyright notice and this permission notice appear in
  16. # supporting documentation, and that the name of InfoSeek not be used in
  17. # advertising or publicity pertaining to distribution of the software
  18. # without specific, written prior permission.  This permission is
  19. # explicitly restricted to the copying and modification of the software
  20. # to remain in Python, compiled Python, or other languages (such as C)
  21. # wherein the modified or derived code is exclusively imported into a
  22. # Python module.
  23. # INFOSEEK CORPORATION DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS
  24. # SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
  25. # FITNESS. IN NO EVENT SHALL INFOSEEK CORPORATION BE LIABLE FOR ANY
  26. # SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER
  27. # RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF
  28. # CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
  29. # CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
  30.  
  31.  
  32. import os
  33. import time
  34. import string
  35. import marshal
  36. import re
  37.  
  38. #**************************************************************************
  39. # Class Stats documentation
  40. #**************************************************************************
  41. # This class is used for creating reports from data generated by the
  42. # Profile class.  It is a "friend" of that class, and imports data either
  43. # by direct access to members of Profile class, or by reading in a dictionary
  44. # that was emitted (via marshal) from the Profile class.
  45. #
  46. # The big change from the previous Profiler (in terms of raw functionality)
  47. # is that an "add()" method has been provided to combine Stats from
  48. # several distinct profile runs.  Both the constructor and the add()
  49. # method now take arbitrarilly many file names as arguments.
  50. #
  51. # All the print methods now take an argument that indicats how many lines
  52. # to print.  If the arg is a floating point number between 0 and 1.0, then
  53. # it is taken as a decimal percentage of the availabel lines to be printed
  54. # (e.g., .1 means print 10% of all available lines).  If it is an integer,
  55. # it is taken to mean the number of lines of data that you wish to have
  56. # printed.
  57. #
  58. # The sort_stats() method now processes some additionaly options (i.e., in
  59. # addition to the old -1, 0, 1, or 2).  It takes an arbitrary number of quoted
  60. # strings to select the sort order.  For example sort_stats('time', 'name')
  61. # sorts on the major key of "internal function time", and on the minor
  62. # key of 'the name of the function'.  Look at the two tables in sort_stats()
  63. # and get_sort_arg_defs(self) for more examples.
  64. #
  65. # All methods now return "self",  so you can string together commands like:
  66. #    Stats('foo', 'goo').strip_dirs().sort_stats('calls').\
  67. #                               print_stats(5).print_callers(5)
  68. #**************************************************************************
  69. import fpformat
  70.  
  71. class Stats:
  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.         apply(self.add, args).ignore()
  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.  
  104.     def load_stats(self, arg):
  105.         if not arg:  self.stats = {}
  106.         elif type(arg) == type(""):
  107.             f = open(arg, 'rb')
  108.             self.stats = marshal.load(f)
  109.             f.close()
  110.             try:
  111.                 file_stats = os.stat(arg)
  112.                 arg = time.ctime(file_stats[8]) + "    " + arg
  113.             except:  # in case this is not unix
  114.                 pass
  115.             self.files = [ arg ]
  116.         elif hasattr(arg, 'create_stats'):
  117.             arg.create_stats()
  118.             self.stats = arg.stats
  119.             arg.stats = {}
  120.         if not self.stats:
  121.             raise TypeError,  "Cannot create or construct a " \
  122.                   + `self.__class__` \
  123.                   + " object from '" + `arg` + "'"
  124.         return
  125.  
  126.     def get_top_level_stats(self):
  127.         for func in self.stats.keys():
  128.             cc, nc, tt, ct, callers = self.stats[func]
  129.             self.total_calls = self.total_calls + nc
  130.             self.prim_calls  = self.prim_calls  + cc
  131.             self.total_tt    = self.total_tt    + tt
  132.             if callers.has_key(("jprofile", 0, "profiler")):
  133.                 self.top_level[func] = None
  134.             if len(func_std_string(func)) > self.max_name_len:
  135.                 self.max_name_len = len(func_std_string(func))
  136.                     
  137.     def add(self, *arg_list):
  138.         if not arg_list: return self
  139.         if len(arg_list) > 1: apply(self.add, arg_list[1:])
  140.         other = arg_list[0]
  141.         if type(self) != type(other) or \
  142.               self.__class__ != other.__class__:
  143.             other = Stats(other)
  144.         self.files = self.files + other.files
  145.         self.total_calls = self.total_calls + other.total_calls
  146.         self.prim_calls = self.prim_calls + other.prim_calls
  147.         self.total_tt = self.total_tt + other.total_tt
  148.         for func in other.top_level.keys():
  149.             self.top_level[func] = None
  150.  
  151.         if self.max_name_len < other.max_name_len:
  152.             self.max_name_len = other.max_name_len
  153.  
  154.         self.fcn_list = None
  155.  
  156.         for func in other.stats.keys():
  157.             if self.stats.has_key(func):
  158.                 old_func_stat = self.stats[func]
  159.             else:
  160.                 old_func_stat = (0, 0, 0, 0, {},)
  161.             self.stats[func] = add_func_stats(old_func_stat, \
  162.                   other.stats[func])
  163.         return self
  164.             
  165.  
  166.  
  167.     # list the tuple indicies and directions for sorting,
  168.     # along with some printable description
  169.     sort_arg_dict_default = {\
  170.           "calls"     : (((1,-1),              ), "call count"),\
  171.           "cumulative": (((3,-1),              ), "cumulative time"),\
  172.           "file"      : (((4, 1),              ), "file name"),\
  173.           "line"      : (((5, 1),              ), "line number"),\
  174.           "module"    : (((4, 1),              ), "file name"),\
  175.           "name"      : (((6, 1),              ), "function name"),\
  176.           "nfl"       : (((6, 1),(4, 1),(5, 1),), "name/file/line"), \
  177.           "pcalls"    : (((0,-1),              ), "call count"),\
  178.           "stdname"   : (((7, 1),              ), "standard name"),\
  179.           "time"      : (((2,-1),              ), "internal time"),\
  180.           }
  181.  
  182.     # Expand all abbreviations that are unique
  183.     def get_sort_arg_defs(self):
  184.         if not self.sort_arg_dict:
  185.             self.sort_arg_dict = dict = {}
  186.             std_list = dict.keys()
  187.             bad_list = {}
  188.             for word in self.sort_arg_dict_default.keys():
  189.                 fragment = word
  190.                 while fragment:
  191.                     if not fragment:
  192.                         break
  193.                     if dict.has_key(fragment):
  194.                         bad_list[fragment] = 0
  195.                         break
  196.                     dict[fragment] = self. \
  197.                           sort_arg_dict_default[word]
  198.                     fragment = fragment[:-1]
  199.             for word in bad_list.keys():
  200.                 del dict[word]
  201.         return self.sort_arg_dict
  202.             
  203.  
  204.     def sort_stats(self, *field):
  205.         if not field:
  206.             self.fcn_list = 0
  207.             return self
  208.         if len(field) == 1 and type(field[0]) == type(1):
  209.             # Be compatible with old profiler
  210.             field = [ {-1: "stdname", \
  211.                   0:"calls", \
  212.                   1:"time", \
  213.                   2: "cumulative" }  [ field[0] ] ]
  214.  
  215.         sort_arg_defs = self.get_sort_arg_defs()
  216.         sort_tuple = ()
  217.         self.sort_type = ""
  218.         connector = ""
  219.         for word in field:
  220.             sort_tuple = sort_tuple + sort_arg_defs[word][0]
  221.             self.sort_type = self.sort_type + connector + \
  222.                   sort_arg_defs[word][1]
  223.             connector = ", "
  224.                     
  225.         stats_list = []
  226.         for func in self.stats.keys():
  227.             cc, nc, tt, ct, callers = self.stats[func]
  228.             stats_list.append((cc, nc, tt, ct) + func_split(func) \
  229.                            + (func_std_string(func), func,)  )
  230.  
  231.         stats_list.sort(TupleComp(sort_tuple).compare)
  232.  
  233.         self.fcn_list = fcn_list = []
  234.         for tuple in stats_list:
  235.             fcn_list.append(tuple[-1])
  236.         return self
  237.  
  238.  
  239.     def reverse_order(self):
  240.         if self.fcn_list: self.fcn_list.reverse()
  241.         return self
  242.  
  243.     def strip_dirs(self):
  244.         oldstats = self.stats
  245.         self.stats = newstats = {}
  246.         max_name_len = 0
  247.         for func in oldstats.keys():
  248.             cc, nc, tt, ct, callers = oldstats[func]
  249.             newfunc = func_strip_path(func)
  250.             if len(func_std_string(newfunc)) > max_name_len:
  251.                 max_name_len = len(func_std_string(newfunc))
  252.             newcallers = {}
  253.             for func2 in callers.keys():
  254.                 newcallers[func_strip_path(func2)] = \
  255.                       callers[func2]
  256.  
  257.             if newstats.has_key(newfunc):
  258.                 newstats[newfunc] = add_func_stats( \
  259.                       newstats[newfunc],\
  260.                       (cc, nc, tt, ct, newcallers))
  261.             else:
  262.                 newstats[newfunc] = (cc, nc, tt, ct, newcallers)
  263.         old_top = self.top_level
  264.         self.top_level = new_top = {}
  265.         for func in old_top.keys():
  266.             new_top[func_strip_path(func)] = None
  267.  
  268.         self.max_name_len = max_name_len
  269.  
  270.         self.fcn_list = None
  271.         self.all_callees = None
  272.         return self
  273.  
  274.  
  275.  
  276.     def calc_callees(self):
  277.         if self.all_callees: return
  278.         self.all_callees = all_callees = {}
  279.         for func in self.stats.keys():
  280.             if not all_callees.has_key(func):
  281.                 all_callees[func] = {}
  282.             cc, nc, tt, ct, callers = self.stats[func]
  283.             for func2 in callers.keys():
  284.                 if not all_callees.has_key(func2):
  285.                     all_callees[func2] = {}
  286.                 all_callees[func2][func]  = callers[func2]
  287.         return
  288.  
  289.         #******************************************************************
  290.     # The following functions support actual printing of reports
  291.         #******************************************************************
  292.  
  293.     # Optional "amount" is either a line count, or a percentage of lines.
  294.  
  295.     def eval_print_amount(self, sel, list, msg):
  296.         new_list = list
  297.         if type(sel) == type(""):
  298.             new_list = []
  299.             for func in list:
  300.                 if re.search(sel, func_std_string(func)):
  301.                     new_list.append(func)
  302.         else:
  303.             count = len(list)
  304.             if type(sel) == type(1.0) and 0.0 <= sel < 1.0:
  305.                 count = int (count * sel + .5)
  306.                 new_list = list[:count]
  307.             elif type(sel) == type(1) and 0 <= sel < count:
  308.                 count = sel
  309.                 new_list = list[:count]
  310.         if len(list) != len(new_list):
  311.             msg = msg + "   List reduced from " + `len(list)` \
  312.                   + " to " + `len(new_list)` + \
  313.                   " due to restriction <" + `sel` + ">\n"
  314.             
  315.         return new_list, msg
  316.  
  317.  
  318.  
  319.     def get_print_list(self, sel_list):
  320.         width = self.max_name_len
  321.         if self.fcn_list:
  322.             list = self.fcn_list[:]
  323.             msg = "   Ordered by: " + self.sort_type + '\n'
  324.         else:
  325.             list = self.stats.keys()
  326.             msg = "   Random listing order was used\n"
  327.  
  328.         for selection in sel_list:
  329.             list,msg = self.eval_print_amount(selection, list, msg)
  330.  
  331.         count = len(list)
  332.  
  333.         if not list:
  334.             return 0, list
  335.         print msg
  336.         if count < len(self.stats):
  337.             width = 0
  338.             for func in list:
  339.                 if  len(func_std_string(func)) > width:
  340.                     width = len(func_std_string(func))
  341.         return width+2, list
  342.         
  343.     def print_stats(self, *amount):
  344.         for filename in self.files:
  345.             print filename
  346.         if self.files: print
  347.         indent = "        "
  348.         for func in self.top_level.keys():
  349.             print indent, func_get_function_name(func)
  350.         
  351.         print  indent, self.total_calls, "function calls",
  352.         if self.total_calls != self.prim_calls:
  353.             print "(" + `self.prim_calls`, "primitive calls)", 
  354.         print "in", fpformat.fix(self.total_tt, 3), "CPU seconds"
  355.         print
  356.         width, list = self.get_print_list(amount)
  357.         if list:
  358.             self.print_title()
  359.             for func in list:
  360.                 self.print_line(func)
  361.             print 
  362.             print
  363.         return self
  364.  
  365.             
  366.     def print_callees(self, *amount):
  367.         width, list = self.get_print_list(amount)
  368.         if list:
  369.             self.calc_callees()
  370.  
  371.             self.print_call_heading(width, "called...")
  372.             for func in list:
  373.                 if self.all_callees.has_key(func):
  374.                     self.print_call_line(width, \
  375.                           func, self.all_callees[func])
  376.                 else:
  377.                     self.print_call_line(width, func, {})
  378.             print
  379.             print
  380.         return self
  381.  
  382.     def print_callers(self, *amount):
  383.         width, list = self.get_print_list(amount)
  384.         if list:
  385.             self.print_call_heading(width, "was called by...")
  386.             for func in list:
  387.                 cc, nc, tt, ct, callers = self.stats[func]
  388.                 self.print_call_line(width, func, callers)
  389.             print
  390.             print
  391.         return self
  392.  
  393.     def print_call_heading(self, name_size, column_title):
  394.         print string.ljust("Function ", name_size) + column_title
  395.  
  396.  
  397.     def print_call_line(self, name_size, source, call_dict):
  398.         print string.ljust(func_std_string(source), name_size),
  399.         if not call_dict:
  400.             print "--"
  401.             return
  402.         clist = call_dict.keys()
  403.         clist.sort()
  404.         name_size = name_size + 1
  405.         indent = ""
  406.         for func in clist:
  407.             name = func_std_string(func)
  408.             print indent*name_size + name + '(' \
  409.                   + `call_dict[func]`+')', \
  410.                   f8(self.stats[func][3])
  411.             indent = " "
  412.  
  413.  
  414.  
  415.     def print_title(self):
  416.         print string.rjust('ncalls', 9),
  417.         print string.rjust('tottime', 8),
  418.         print string.rjust('percall', 8),
  419.         print string.rjust('cumtime', 8),
  420.         print string.rjust('percall', 8),
  421.         print 'filename:lineno(function)'
  422.  
  423.  
  424.     def print_line(self, func):  # hack : should print percentages
  425.         cc, nc, tt, ct, callers = self.stats[func]
  426.         c = `nc`
  427.         if nc != cc:
  428.             c = c + '/' + `cc`
  429.         print string.rjust(c, 9),
  430.         print f8(tt),
  431.         if nc == 0:
  432.             print ' '*8,
  433.         else:
  434.             print f8(tt/nc),
  435.         print f8(ct),
  436.         if cc == 0:
  437.             print ' '*8,
  438.         else:
  439.             print f8(ct/cc),
  440.         print func_std_string(func)
  441.  
  442.  
  443.     def ignore(self):
  444.         pass # has no return value, so use at end of line :-)
  445.  
  446.  
  447. #**************************************************************************
  448. # class TupleComp Documentation
  449. #**************************************************************************
  450. # This class provides a generic function for comparing any two tuples.
  451. # Each instance records a list of tuple-indicies (from most significant
  452. # to least significant), and sort direction (ascending or decending) for
  453. # each tuple-index.  The compare functions can then be used as the function
  454. # argument to the system sort() function when a list of tuples need to be
  455. # sorted in the instances order.
  456. #**************************************************************************
  457. class TupleComp:
  458.     def __init__(self, comp_select_list):
  459.         self.comp_select_list = comp_select_list
  460.  
  461.     def compare (self, left, right):
  462.         for index, direction in self.comp_select_list:
  463.             l = left[index]
  464.             r = right[index]
  465.             if l < r:
  466.                 return -direction
  467.             if l > r:
  468.                 return direction
  469.         return 0
  470.  
  471.         
  472.  
  473. #**************************************************************************
  474.  
  475. def func_strip_path(func_name):
  476.     file, line, name = func_name
  477.      return os.path.basename(file), line, name
  478.  
  479. def func_get_function_name(func):
  480.     return func[2]
  481.  
  482. def func_std_string(func_name): # match what old profile produced
  483.     file, line, name = func_name
  484.     return file + ":" + `line` + "(" + name + ")"
  485.  
  486. def func_split(func_name):
  487.     return func_name
  488.  
  489. #**************************************************************************
  490. # The following functions combine statists for pairs functions.
  491. # The bulk of the processing involves correctly handling "call" lists,
  492. # such as callers and callees. 
  493. #**************************************************************************
  494.  
  495.     # Add together all the stats for two profile entries
  496. def add_func_stats(target, source):                
  497.     cc, nc, tt, ct, callers = source
  498.     t_cc, t_nc, t_tt, t_ct, t_callers = target
  499.     return (cc+t_cc, nc+t_nc, tt+t_tt, ct+t_ct, \
  500.           add_callers(t_callers, callers))
  501.  
  502.  
  503.     # Combine two caller lists in a single list.
  504. def add_callers(target, source):
  505.     new_callers = {}
  506.     for func in target.keys():
  507.         new_callers[func] = target[func]
  508.     for func in source.keys():
  509.         if new_callers.has_key(func):
  510.             new_callers[func] = source[func] + new_callers[func]
  511.         else:
  512.             new_callers[func] = source[func]
  513.     return new_callers
  514.  
  515.      # Sum the caller statistics to get total number of calls recieved
  516. def count_calls(callers):
  517.     nc = 0
  518.     for func in callers.keys():
  519.         nc = nc + callers[func]
  520.     return nc
  521.  
  522. #**************************************************************************
  523. # The following functions support printing of reports
  524. #**************************************************************************
  525.  
  526. def f8(x):
  527.     return string.rjust(fpformat.fix(x, 3), 8)
  528.  
  529.  
  530.