(root)/
Python-3.12.0/
Lib/
pstats.py
       1  """Class for printing reports on profiled python code."""
       2  
       3  # Written by James Roskind
       4  # Based on prior profile module by Sjoerd Mullender...
       5  #   which was hacked somewhat by: Guido van Rossum
       6  
       7  # Copyright Disney Enterprises, Inc.  All Rights Reserved.
       8  # Licensed to PSF under a Contributor Agreement
       9  #
      10  # Licensed under the Apache License, Version 2.0 (the "License");
      11  # you may not use this file except in compliance with the License.
      12  # You may obtain a copy of the License at
      13  #
      14  # http://www.apache.org/licenses/LICENSE-2.0
      15  #
      16  # Unless required by applicable law or agreed to in writing, software
      17  # distributed under the License is distributed on an "AS IS" BASIS,
      18  # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
      19  # either express or implied.  See the License for the specific language
      20  # governing permissions and limitations under the License.
      21  
      22  
      23  import sys
      24  import os
      25  import time
      26  import marshal
      27  import re
      28  
      29  from enum import StrEnum, _simple_enum
      30  from functools import cmp_to_key
      31  from dataclasses import dataclass
      32  from typing import Dict
      33  
      34  __all__ = ["Stats", "SortKey", "FunctionProfile", "StatsProfile"]
      35  
      36  @_simple_enum(StrEnum)
      37  class ESC[4;38;5;81mSortKey:
      38      CALLS = 'calls', 'ncalls'
      39      CUMULATIVE = 'cumulative', 'cumtime'
      40      FILENAME = 'filename', 'module'
      41      LINE = 'line'
      42      NAME = 'name'
      43      NFL = 'nfl'
      44      PCALLS = 'pcalls'
      45      STDNAME = 'stdname'
      46      TIME = 'time', 'tottime'
      47  
      48      def __new__(cls, *values):
      49          value = values[0]
      50          obj = str.__new__(cls, value)
      51          obj._value_ = value
      52          for other_value in values[1:]:
      53              cls._value2member_map_[other_value] = obj
      54          obj._all_values = values
      55          return obj
      56  
      57  
      58  @dataclass(unsafe_hash=True)
      59  class ESC[4;38;5;81mFunctionProfile:
      60      ncalls: str
      61      tottime: float
      62      percall_tottime: float
      63      cumtime: float
      64      percall_cumtime: float
      65      file_name: str
      66      line_number: int
      67  
      68  @dataclass(unsafe_hash=True)
      69  class ESC[4;38;5;81mStatsProfile:
      70      '''Class for keeping track of an item in inventory.'''
      71      total_tt: float
      72      func_profiles: Dict[str, FunctionProfile]
      73  
      74  class ESC[4;38;5;81mStats:
      75      """This class is used for creating reports from data generated by the
      76      Profile class.  It is a "friend" of that class, and imports data either
      77      by direct access to members of Profile class, or by reading in a dictionary
      78      that was emitted (via marshal) from the Profile class.
      79  
      80      The big change from the previous Profiler (in terms of raw functionality)
      81      is that an "add()" method has been provided to combine Stats from
      82      several distinct profile runs.  Both the constructor and the add()
      83      method now take arbitrarily many file names as arguments.
      84  
      85      All the print methods now take an argument that indicates how many lines
      86      to print.  If the arg is a floating point number between 0 and 1.0, then
      87      it is taken as a decimal percentage of the available lines to be printed
      88      (e.g., .1 means print 10% of all available lines).  If it is an integer,
      89      it is taken to mean the number of lines of data that you wish to have
      90      printed.
      91  
      92      The sort_stats() method now processes some additional options (i.e., in
      93      addition to the old -1, 0, 1, or 2 that are respectively interpreted as
      94      'stdname', 'calls', 'time', and 'cumulative').  It takes either an
      95      arbitrary number of quoted strings or SortKey enum to select the sort
      96      order.
      97  
      98      For example sort_stats('time', 'name') or sort_stats(SortKey.TIME,
      99      SortKey.NAME) sorts on the major key of 'internal function time', and on
     100      the minor key of 'the name of the function'.  Look at the two tables in
     101      sort_stats() and get_sort_arg_defs(self) for more examples.
     102  
     103      All methods return self, so you can string together commands like:
     104          Stats('foo', 'goo').strip_dirs().sort_stats('calls').\
     105                              print_stats(5).print_callers(5)
     106      """
     107  
     108      def __init__(self, *args, stream=None):
     109          self.stream = stream or sys.stdout
     110          if not len(args):
     111              arg = None
     112          else:
     113              arg = args[0]
     114              args = args[1:]
     115          self.init(arg)
     116          self.add(*args)
     117  
     118      def init(self, arg):
     119          self.all_callees = None  # calc only if needed
     120          self.files = []
     121          self.fcn_list = None
     122          self.total_tt = 0
     123          self.total_calls = 0
     124          self.prim_calls = 0
     125          self.max_name_len = 0
     126          self.top_level = set()
     127          self.stats = {}
     128          self.sort_arg_dict = {}
     129          self.load_stats(arg)
     130          try:
     131              self.get_top_level_stats()
     132          except Exception:
     133              print("Invalid timing data %s" %
     134                    (self.files[-1] if self.files else ''), file=self.stream)
     135              raise
     136  
     137      def load_stats(self, arg):
     138          if arg is None:
     139              self.stats = {}
     140              return
     141          elif isinstance(arg, str):
     142              with open(arg, 'rb') as f:
     143                  self.stats = marshal.load(f)
     144              try:
     145                  file_stats = os.stat(arg)
     146                  arg = time.ctime(file_stats.st_mtime) + "    " + arg
     147              except:  # in case this is not unix
     148                  pass
     149              self.files = [arg]
     150          elif hasattr(arg, 'create_stats'):
     151              arg.create_stats()
     152              self.stats = arg.stats
     153              arg.stats = {}
     154          if not self.stats:
     155              raise TypeError("Cannot create or construct a %r object from %r"
     156                              % (self.__class__, arg))
     157          return
     158  
     159      def get_top_level_stats(self):
     160          for func, (cc, nc, tt, ct, callers) in self.stats.items():
     161              self.total_calls += nc
     162              self.prim_calls  += cc
     163              self.total_tt    += tt
     164              if ("jprofile", 0, "profiler") in callers:
     165                  self.top_level.add(func)
     166              if len(func_std_string(func)) > self.max_name_len:
     167                  self.max_name_len = len(func_std_string(func))
     168  
     169      def add(self, *arg_list):
     170          if not arg_list:
     171              return self
     172          for item in reversed(arg_list):
     173              if type(self) != type(item):
     174                  item = Stats(item)
     175              self.files += item.files
     176              self.total_calls += item.total_calls
     177              self.prim_calls += item.prim_calls
     178              self.total_tt += item.total_tt
     179              for func in item.top_level:
     180                  self.top_level.add(func)
     181  
     182              if self.max_name_len < item.max_name_len:
     183                  self.max_name_len = item.max_name_len
     184  
     185              self.fcn_list = None
     186  
     187              for func, stat in item.stats.items():
     188                  if func in self.stats:
     189                      old_func_stat = self.stats[func]
     190                  else:
     191                      old_func_stat = (0, 0, 0, 0, {},)
     192                  self.stats[func] = add_func_stats(old_func_stat, stat)
     193          return self
     194  
     195      def dump_stats(self, filename):
     196          """Write the profile data to a file we know how to load back."""
     197          with open(filename, 'wb') as f:
     198              marshal.dump(self.stats, f)
     199  
     200      # list the tuple indices and directions for sorting,
     201      # along with some printable description
     202      sort_arg_dict_default = {
     203                "calls"     : (((1,-1),              ), "call count"),
     204                "ncalls"    : (((1,-1),              ), "call count"),
     205                "cumtime"   : (((3,-1),              ), "cumulative time"),
     206                "cumulative": (((3,-1),              ), "cumulative time"),
     207                "filename"  : (((4, 1),              ), "file name"),
     208                "line"      : (((5, 1),              ), "line number"),
     209                "module"    : (((4, 1),              ), "file name"),
     210                "name"      : (((6, 1),              ), "function name"),
     211                "nfl"       : (((6, 1),(4, 1),(5, 1),), "name/file/line"),
     212                "pcalls"    : (((0,-1),              ), "primitive call count"),
     213                "stdname"   : (((7, 1),              ), "standard name"),
     214                "time"      : (((2,-1),              ), "internal time"),
     215                "tottime"   : (((2,-1),              ), "internal time"),
     216                }
     217  
     218      def get_sort_arg_defs(self):
     219          """Expand all abbreviations that are unique."""
     220          if not self.sort_arg_dict:
     221              self.sort_arg_dict = dict = {}
     222              bad_list = {}
     223              for word, tup in self.sort_arg_dict_default.items():
     224                  fragment = word
     225                  while fragment:
     226                      if fragment in dict:
     227                          bad_list[fragment] = 0
     228                          break
     229                      dict[fragment] = tup
     230                      fragment = fragment[:-1]
     231              for word in bad_list:
     232                  del dict[word]
     233          return self.sort_arg_dict
     234  
     235      def sort_stats(self, *field):
     236          if not field:
     237              self.fcn_list = 0
     238              return self
     239          if len(field) == 1 and isinstance(field[0], int):
     240              # Be compatible with old profiler
     241              field = [ {-1: "stdname",
     242                         0:  "calls",
     243                         1:  "time",
     244                         2:  "cumulative"}[field[0]] ]
     245          elif len(field) >= 2:
     246              for arg in field[1:]:
     247                  if type(arg) != type(field[0]):
     248                      raise TypeError("Can't have mixed argument type")
     249  
     250          sort_arg_defs = self.get_sort_arg_defs()
     251  
     252          sort_tuple = ()
     253          self.sort_type = ""
     254          connector = ""
     255          for word in field:
     256              if isinstance(word, SortKey):
     257                  word = word.value
     258              sort_tuple = sort_tuple + sort_arg_defs[word][0]
     259              self.sort_type += connector + sort_arg_defs[word][1]
     260              connector = ", "
     261  
     262          stats_list = []
     263          for func, (cc, nc, tt, ct, callers) in self.stats.items():
     264              stats_list.append((cc, nc, tt, ct) + func +
     265                                (func_std_string(func), func))
     266  
     267          stats_list.sort(key=cmp_to_key(TupleComp(sort_tuple).compare))
     268  
     269          self.fcn_list = fcn_list = []
     270          for tuple in stats_list:
     271              fcn_list.append(tuple[-1])
     272          return self
     273  
     274      def reverse_order(self):
     275          if self.fcn_list:
     276              self.fcn_list.reverse()
     277          return self
     278  
     279      def strip_dirs(self):
     280          oldstats = self.stats
     281          self.stats = newstats = {}
     282          max_name_len = 0
     283          for func, (cc, nc, tt, ct, callers) in oldstats.items():
     284              newfunc = func_strip_path(func)
     285              if len(func_std_string(newfunc)) > max_name_len:
     286                  max_name_len = len(func_std_string(newfunc))
     287              newcallers = {}
     288              for func2, caller in callers.items():
     289                  newcallers[func_strip_path(func2)] = caller
     290  
     291              if newfunc in newstats:
     292                  newstats[newfunc] = add_func_stats(
     293                                          newstats[newfunc],
     294                                          (cc, nc, tt, ct, newcallers))
     295              else:
     296                  newstats[newfunc] = (cc, nc, tt, ct, newcallers)
     297          old_top = self.top_level
     298          self.top_level = new_top = set()
     299          for func in old_top:
     300              new_top.add(func_strip_path(func))
     301  
     302          self.max_name_len = max_name_len
     303  
     304          self.fcn_list = None
     305          self.all_callees = None
     306          return self
     307  
     308      def calc_callees(self):
     309          if self.all_callees:
     310              return
     311          self.all_callees = all_callees = {}
     312          for func, (cc, nc, tt, ct, callers) in self.stats.items():
     313              if not func in all_callees:
     314                  all_callees[func] = {}
     315              for func2, caller in callers.items():
     316                  if not func2 in all_callees:
     317                      all_callees[func2] = {}
     318                  all_callees[func2][func]  = caller
     319          return
     320  
     321      #******************************************************************
     322      # The following functions support actual printing of reports
     323      #******************************************************************
     324  
     325      # Optional "amount" is either a line count, or a percentage of lines.
     326  
     327      def eval_print_amount(self, sel, list, msg):
     328          new_list = list
     329          if isinstance(sel, str):
     330              try:
     331                  rex = re.compile(sel)
     332              except re.error:
     333                  msg += "   <Invalid regular expression %r>\n" % sel
     334                  return new_list, msg
     335              new_list = []
     336              for func in list:
     337                  if rex.search(func_std_string(func)):
     338                      new_list.append(func)
     339          else:
     340              count = len(list)
     341              if isinstance(sel, float) and 0.0 <= sel < 1.0:
     342                  count = int(count * sel + .5)
     343                  new_list = list[:count]
     344              elif isinstance(sel, int) and 0 <= sel < count:
     345                  count = sel
     346                  new_list = list[:count]
     347          if len(list) != len(new_list):
     348              msg += "   List reduced from %r to %r due to restriction <%r>\n" % (
     349                  len(list), len(new_list), sel)
     350  
     351          return new_list, msg
     352  
     353      def get_stats_profile(self):
     354          """This method returns an instance of StatsProfile, which contains a mapping
     355          of function names to instances of FunctionProfile. Each FunctionProfile
     356          instance holds information related to the function's profile such as how
     357          long the function took to run, how many times it was called, etc...
     358          """
     359          func_list = self.fcn_list[:] if self.fcn_list else list(self.stats.keys())
     360          if not func_list:
     361              return StatsProfile(0, {})
     362  
     363          total_tt = float(f8(self.total_tt))
     364          func_profiles = {}
     365          stats_profile = StatsProfile(total_tt, func_profiles)
     366  
     367          for func in func_list:
     368              cc, nc, tt, ct, callers = self.stats[func]
     369              file_name, line_number, func_name = func
     370              ncalls = str(nc) if nc == cc else (str(nc) + '/' + str(cc))
     371              tottime = float(f8(tt))
     372              percall_tottime = -1 if nc == 0 else float(f8(tt/nc))
     373              cumtime = float(f8(ct))
     374              percall_cumtime = -1 if cc == 0 else float(f8(ct/cc))
     375              func_profile = FunctionProfile(
     376                  ncalls,
     377                  tottime, # time spent in this function alone
     378                  percall_tottime,
     379                  cumtime, # time spent in the function plus all functions that this function called,
     380                  percall_cumtime,
     381                  file_name,
     382                  line_number
     383              )
     384              func_profiles[func_name] = func_profile
     385  
     386          return stats_profile
     387  
     388      def get_print_list(self, sel_list):
     389          width = self.max_name_len
     390          if self.fcn_list:
     391              stat_list = self.fcn_list[:]
     392              msg = "   Ordered by: " + self.sort_type + '\n'
     393          else:
     394              stat_list = list(self.stats.keys())
     395              msg = "   Random listing order was used\n"
     396  
     397          for selection in sel_list:
     398              stat_list, msg = self.eval_print_amount(selection, stat_list, msg)
     399  
     400          count = len(stat_list)
     401  
     402          if not stat_list:
     403              return 0, stat_list
     404          print(msg, file=self.stream)
     405          if count < len(self.stats):
     406              width = 0
     407              for func in stat_list:
     408                  if  len(func_std_string(func)) > width:
     409                      width = len(func_std_string(func))
     410          return width+2, stat_list
     411  
     412      def print_stats(self, *amount):
     413          for filename in self.files:
     414              print(filename, file=self.stream)
     415          if self.files:
     416              print(file=self.stream)
     417          indent = ' ' * 8
     418          for func in self.top_level:
     419              print(indent, func_get_function_name(func), file=self.stream)
     420  
     421          print(indent, self.total_calls, "function calls", end=' ', file=self.stream)
     422          if self.total_calls != self.prim_calls:
     423              print("(%d primitive calls)" % self.prim_calls, end=' ', file=self.stream)
     424          print("in %.3f seconds" % self.total_tt, file=self.stream)
     425          print(file=self.stream)
     426          width, list = self.get_print_list(amount)
     427          if list:
     428              self.print_title()
     429              for func in list:
     430                  self.print_line(func)
     431              print(file=self.stream)
     432              print(file=self.stream)
     433          return self
     434  
     435      def print_callees(self, *amount):
     436          width, list = self.get_print_list(amount)
     437          if list:
     438              self.calc_callees()
     439  
     440              self.print_call_heading(width, "called...")
     441              for func in list:
     442                  if func in self.all_callees:
     443                      self.print_call_line(width, func, self.all_callees[func])
     444                  else:
     445                      self.print_call_line(width, func, {})
     446              print(file=self.stream)
     447              print(file=self.stream)
     448          return self
     449  
     450      def print_callers(self, *amount):
     451          width, list = self.get_print_list(amount)
     452          if list:
     453              self.print_call_heading(width, "was called by...")
     454              for func in list:
     455                  cc, nc, tt, ct, callers = self.stats[func]
     456                  self.print_call_line(width, func, callers, "<-")
     457              print(file=self.stream)
     458              print(file=self.stream)
     459          return self
     460  
     461      def print_call_heading(self, name_size, column_title):
     462          print("Function ".ljust(name_size) + column_title, file=self.stream)
     463          # print sub-header only if we have new-style callers
     464          subheader = False
     465          for cc, nc, tt, ct, callers in self.stats.values():
     466              if callers:
     467                  value = next(iter(callers.values()))
     468                  subheader = isinstance(value, tuple)
     469                  break
     470          if subheader:
     471              print(" "*name_size + "    ncalls  tottime  cumtime", file=self.stream)
     472  
     473      def print_call_line(self, name_size, source, call_dict, arrow="->"):
     474          print(func_std_string(source).ljust(name_size) + arrow, end=' ', file=self.stream)
     475          if not call_dict:
     476              print(file=self.stream)
     477              return
     478          clist = sorted(call_dict.keys())
     479          indent = ""
     480          for func in clist:
     481              name = func_std_string(func)
     482              value = call_dict[func]
     483              if isinstance(value, tuple):
     484                  nc, cc, tt, ct = value
     485                  if nc != cc:
     486                      substats = '%d/%d' % (nc, cc)
     487                  else:
     488                      substats = '%d' % (nc,)
     489                  substats = '%s %s %s  %s' % (substats.rjust(7+2*len(indent)),
     490                                               f8(tt), f8(ct), name)
     491                  left_width = name_size + 1
     492              else:
     493                  substats = '%s(%r) %s' % (name, value, f8(self.stats[func][3]))
     494                  left_width = name_size + 3
     495              print(indent*left_width + substats, file=self.stream)
     496              indent = " "
     497  
     498      def print_title(self):
     499          print('   ncalls  tottime  percall  cumtime  percall', end=' ', file=self.stream)
     500          print('filename:lineno(function)', file=self.stream)
     501  
     502      def print_line(self, func):  # hack: should print percentages
     503          cc, nc, tt, ct, callers = self.stats[func]
     504          c = str(nc)
     505          if nc != cc:
     506              c = c + '/' + str(cc)
     507          print(c.rjust(9), end=' ', file=self.stream)
     508          print(f8(tt), end=' ', file=self.stream)
     509          if nc == 0:
     510              print(' '*8, end=' ', file=self.stream)
     511          else:
     512              print(f8(tt/nc), end=' ', file=self.stream)
     513          print(f8(ct), end=' ', file=self.stream)
     514          if cc == 0:
     515              print(' '*8, end=' ', file=self.stream)
     516          else:
     517              print(f8(ct/cc), end=' ', file=self.stream)
     518          print(func_std_string(func), file=self.stream)
     519  
     520  class ESC[4;38;5;81mTupleComp:
     521      """This class provides a generic function for comparing any two tuples.
     522      Each instance records a list of tuple-indices (from most significant
     523      to least significant), and sort direction (ascending or descending) for
     524      each tuple-index.  The compare functions can then be used as the function
     525      argument to the system sort() function when a list of tuples need to be
     526      sorted in the instances order."""
     527  
     528      def __init__(self, comp_select_list):
     529          self.comp_select_list = comp_select_list
     530  
     531      def compare (self, left, right):
     532          for index, direction in self.comp_select_list:
     533              l = left[index]
     534              r = right[index]
     535              if l < r:
     536                  return -direction
     537              if l > r:
     538                  return direction
     539          return 0
     540  
     541  
     542  #**************************************************************************
     543  # func_name is a triple (file:string, line:int, name:string)
     544  
     545  def func_strip_path(func_name):
     546      filename, line, name = func_name
     547      return os.path.basename(filename), line, name
     548  
     549  def func_get_function_name(func):
     550      return func[2]
     551  
     552  def func_std_string(func_name): # match what old profile produced
     553      if func_name[:2] == ('~', 0):
     554          # special case for built-in functions
     555          name = func_name[2]
     556          if name.startswith('<') and name.endswith('>'):
     557              return '{%s}' % name[1:-1]
     558          else:
     559              return name
     560      else:
     561          return "%s:%d(%s)" % func_name
     562  
     563  #**************************************************************************
     564  # The following functions combine statistics for pairs functions.
     565  # The bulk of the processing involves correctly handling "call" lists,
     566  # such as callers and callees.
     567  #**************************************************************************
     568  
     569  def add_func_stats(target, source):
     570      """Add together all the stats for two profile entries."""
     571      cc, nc, tt, ct, callers = source
     572      t_cc, t_nc, t_tt, t_ct, t_callers = target
     573      return (cc+t_cc, nc+t_nc, tt+t_tt, ct+t_ct,
     574                add_callers(t_callers, callers))
     575  
     576  def add_callers(target, source):
     577      """Combine two caller lists in a single list."""
     578      new_callers = {}
     579      for func, caller in target.items():
     580          new_callers[func] = caller
     581      for func, caller in source.items():
     582          if func in new_callers:
     583              if isinstance(caller, tuple):
     584                  # format used by cProfile
     585                  new_callers[func] = tuple(i + j for i, j in zip(caller, new_callers[func]))
     586              else:
     587                  # format used by profile
     588                  new_callers[func] += caller
     589          else:
     590              new_callers[func] = caller
     591      return new_callers
     592  
     593  def count_calls(callers):
     594      """Sum the caller statistics to get total number of calls received."""
     595      nc = 0
     596      for calls in callers.values():
     597          nc += calls
     598      return nc
     599  
     600  #**************************************************************************
     601  # The following functions support printing of reports
     602  #**************************************************************************
     603  
     604  def f8(x):
     605      return "%8.3f" % x
     606  
     607  #**************************************************************************
     608  # Statistics browser added by ESR, April 2001
     609  #**************************************************************************
     610  
     611  if __name__ == '__main__':
     612      import cmd
     613      try:
     614          import readline
     615      except ImportError:
     616          pass
     617  
     618      class ESC[4;38;5;81mProfileBrowser(ESC[4;38;5;149mcmdESC[4;38;5;149m.ESC[4;38;5;149mCmd):
     619          def __init__(self, profile=None):
     620              cmd.Cmd.__init__(self)
     621              self.prompt = "% "
     622              self.stats = None
     623              self.stream = sys.stdout
     624              if profile is not None:
     625                  self.do_read(profile)
     626  
     627          def generic(self, fn, line):
     628              args = line.split()
     629              processed = []
     630              for term in args:
     631                  try:
     632                      processed.append(int(term))
     633                      continue
     634                  except ValueError:
     635                      pass
     636                  try:
     637                      frac = float(term)
     638                      if frac > 1 or frac < 0:
     639                          print("Fraction argument must be in [0, 1]", file=self.stream)
     640                          continue
     641                      processed.append(frac)
     642                      continue
     643                  except ValueError:
     644                      pass
     645                  processed.append(term)
     646              if self.stats:
     647                  getattr(self.stats, fn)(*processed)
     648              else:
     649                  print("No statistics object is loaded.", file=self.stream)
     650              return 0
     651          def generic_help(self):
     652              print("Arguments may be:", file=self.stream)
     653              print("* An integer maximum number of entries to print.", file=self.stream)
     654              print("* A decimal fractional number between 0 and 1, controlling", file=self.stream)
     655              print("  what fraction of selected entries to print.", file=self.stream)
     656              print("* A regular expression; only entries with function names", file=self.stream)
     657              print("  that match it are printed.", file=self.stream)
     658  
     659          def do_add(self, line):
     660              if self.stats:
     661                  try:
     662                      self.stats.add(line)
     663                  except OSError as e:
     664                      print("Failed to load statistics for %s: %s" % (line, e), file=self.stream)
     665              else:
     666                  print("No statistics object is loaded.", file=self.stream)
     667              return 0
     668          def help_add(self):
     669              print("Add profile info from given file to current statistics object.", file=self.stream)
     670  
     671          def do_callees(self, line):
     672              return self.generic('print_callees', line)
     673          def help_callees(self):
     674              print("Print callees statistics from the current stat object.", file=self.stream)
     675              self.generic_help()
     676  
     677          def do_callers(self, line):
     678              return self.generic('print_callers', line)
     679          def help_callers(self):
     680              print("Print callers statistics from the current stat object.", file=self.stream)
     681              self.generic_help()
     682  
     683          def do_EOF(self, line):
     684              print("", file=self.stream)
     685              return 1
     686          def help_EOF(self):
     687              print("Leave the profile browser.", file=self.stream)
     688  
     689          def do_quit(self, line):
     690              return 1
     691          def help_quit(self):
     692              print("Leave the profile browser.", file=self.stream)
     693  
     694          def do_read(self, line):
     695              if line:
     696                  try:
     697                      self.stats = Stats(line)
     698                  except OSError as err:
     699                      print(err.args[1], file=self.stream)
     700                      return
     701                  except Exception as err:
     702                      print(err.__class__.__name__ + ':', err, file=self.stream)
     703                      return
     704                  self.prompt = line + "% "
     705              elif len(self.prompt) > 2:
     706                  line = self.prompt[:-2]
     707                  self.do_read(line)
     708              else:
     709                  print("No statistics object is current -- cannot reload.", file=self.stream)
     710              return 0
     711          def help_read(self):
     712              print("Read in profile data from a specified file.", file=self.stream)
     713              print("Without argument, reload the current file.", file=self.stream)
     714  
     715          def do_reverse(self, line):
     716              if self.stats:
     717                  self.stats.reverse_order()
     718              else:
     719                  print("No statistics object is loaded.", file=self.stream)
     720              return 0
     721          def help_reverse(self):
     722              print("Reverse the sort order of the profiling report.", file=self.stream)
     723  
     724          def do_sort(self, line):
     725              if not self.stats:
     726                  print("No statistics object is loaded.", file=self.stream)
     727                  return
     728              abbrevs = self.stats.get_sort_arg_defs()
     729              if line and all((x in abbrevs) for x in line.split()):
     730                  self.stats.sort_stats(*line.split())
     731              else:
     732                  print("Valid sort keys (unique prefixes are accepted):", file=self.stream)
     733                  for (key, value) in Stats.sort_arg_dict_default.items():
     734                      print("%s -- %s" % (key, value[1]), file=self.stream)
     735              return 0
     736          def help_sort(self):
     737              print("Sort profile data according to specified keys.", file=self.stream)
     738              print("(Typing `sort' without arguments lists valid keys.)", file=self.stream)
     739          def complete_sort(self, text, *args):
     740              return [a for a in Stats.sort_arg_dict_default if a.startswith(text)]
     741  
     742          def do_stats(self, line):
     743              return self.generic('print_stats', line)
     744          def help_stats(self):
     745              print("Print statistics from the current stat object.", file=self.stream)
     746              self.generic_help()
     747  
     748          def do_strip(self, line):
     749              if self.stats:
     750                  self.stats.strip_dirs()
     751              else:
     752                  print("No statistics object is loaded.", file=self.stream)
     753          def help_strip(self):
     754              print("Strip leading path information from filenames in the report.", file=self.stream)
     755  
     756          def help_help(self):
     757              print("Show help for a given command.", file=self.stream)
     758  
     759          def postcmd(self, stop, line):
     760              if stop:
     761                  return stop
     762              return None
     763  
     764      if len(sys.argv) > 1:
     765          initprofile = sys.argv[1]
     766      else:
     767          initprofile = None
     768      try:
     769          browser = ProfileBrowser(initprofile)
     770          for profile in sys.argv[2:]:
     771              browser.do_add(profile)
     772          print("Welcome to the profile statistics browser.", file=browser.stream)
     773          browser.cmdloop()
     774          print("Goodbye.", file=browser.stream)
     775      except KeyboardInterrupt:
     776          pass
     777  
     778  # That's all, folks.