• 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 StrEnum, _simple_enum
30from functools import cmp_to_key
31from dataclasses import dataclass
32from typing import Dict
33
34__all__ = ["Stats", "SortKey", "FunctionProfile", "StatsProfile"]
35
36@_simple_enum(StrEnum)
37class SortKey:
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)
59class FunctionProfile:
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)
69class StatsProfile:
70    '''Class for keeping track of an item in inventory.'''
71    total_tt: float
72    func_profiles: Dict[str, FunctionProfile]
73
74class Stats:
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.PatternError:
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
520class TupleComp:
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
545def func_strip_path(func_name):
546    filename, line, name = func_name
547    return os.path.basename(filename), line, name
548
549def func_get_function_name(func):
550    return func[2]
551
552def 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
569def 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
576def 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
593def 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
604def f8(x):
605    return "%8.3f" % x
606
607#**************************************************************************
608# Statistics browser added by ESR, April 2001
609#**************************************************************************
610
611if __name__ == '__main__':
612    import cmd
613    try:
614        import readline
615    except ImportError:
616        pass
617
618    class ProfileBrowser(cmd.Cmd):
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.
779