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