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 not fragment:
227                        break
228                    if fragment in dict:
229                        bad_list[fragment] = 0
230                        break
231                    dict[fragment] = tup
232                    fragment = fragment[:-1]
233            for word in bad_list:
234                del dict[word]
235        return self.sort_arg_dict
236
237    def sort_stats(self, *field):
238        if not field:
239            self.fcn_list = 0
240            return self
241        if len(field) == 1 and isinstance(field[0], int):
242            # Be compatible with old profiler
243            field = [ {-1: "stdname",
244                       0:  "calls",
245                       1:  "time",
246                       2:  "cumulative"}[field[0]] ]
247        elif len(field) >= 2:
248            for arg in field[1:]:
249                if type(arg) != type(field[0]):
250                    raise TypeError("Can't have mixed argument type")
251
252        sort_arg_defs = self.get_sort_arg_defs()
253
254        sort_tuple = ()
255        self.sort_type = ""
256        connector = ""
257        for word in field:
258            if isinstance(word, SortKey):
259                word = word.value
260            sort_tuple = sort_tuple + sort_arg_defs[word][0]
261            self.sort_type += connector + sort_arg_defs[word][1]
262            connector = ", "
263
264        stats_list = []
265        for func, (cc, nc, tt, ct, callers) in self.stats.items():
266            stats_list.append((cc, nc, tt, ct) + func +
267                              (func_std_string(func), func))
268
269        stats_list.sort(key=cmp_to_key(TupleComp(sort_tuple).compare))
270
271        self.fcn_list = fcn_list = []
272        for tuple in stats_list:
273            fcn_list.append(tuple[-1])
274        return self
275
276    def reverse_order(self):
277        if self.fcn_list:
278            self.fcn_list.reverse()
279        return self
280
281    def strip_dirs(self):
282        oldstats = self.stats
283        self.stats = newstats = {}
284        max_name_len = 0
285        for func, (cc, nc, tt, ct, callers) in oldstats.items():
286            newfunc = func_strip_path(func)
287            if len(func_std_string(newfunc)) > max_name_len:
288                max_name_len = len(func_std_string(newfunc))
289            newcallers = {}
290            for func2, caller in callers.items():
291                newcallers[func_strip_path(func2)] = caller
292
293            if newfunc in newstats:
294                newstats[newfunc] = add_func_stats(
295                                        newstats[newfunc],
296                                        (cc, nc, tt, ct, newcallers))
297            else:
298                newstats[newfunc] = (cc, nc, tt, ct, newcallers)
299        old_top = self.top_level
300        self.top_level = new_top = set()
301        for func in old_top:
302            new_top.add(func_strip_path(func))
303
304        self.max_name_len = max_name_len
305
306        self.fcn_list = None
307        self.all_callees = None
308        return self
309
310    def calc_callees(self):
311        if self.all_callees:
312            return
313        self.all_callees = all_callees = {}
314        for func, (cc, nc, tt, ct, callers) in self.stats.items():
315            if not func in all_callees:
316                all_callees[func] = {}
317            for func2, caller in callers.items():
318                if not func2 in all_callees:
319                    all_callees[func2] = {}
320                all_callees[func2][func]  = caller
321        return
322
323    #******************************************************************
324    # The following functions support actual printing of reports
325    #******************************************************************
326
327    # Optional "amount" is either a line count, or a percentage of lines.
328
329    def eval_print_amount(self, sel, list, msg):
330        new_list = list
331        if isinstance(sel, str):
332            try:
333                rex = re.compile(sel)
334            except re.error:
335                msg += "   <Invalid regular expression %r>\n" % sel
336                return new_list, msg
337            new_list = []
338            for func in list:
339                if rex.search(func_std_string(func)):
340                    new_list.append(func)
341        else:
342            count = len(list)
343            if isinstance(sel, float) and 0.0 <= sel < 1.0:
344                count = int(count * sel + .5)
345                new_list = list[:count]
346            elif isinstance(sel, int) and 0 <= sel < count:
347                count = sel
348                new_list = list[:count]
349        if len(list) != len(new_list):
350            msg += "   List reduced from %r to %r due to restriction <%r>\n" % (
351                len(list), len(new_list), sel)
352
353        return new_list, msg
354
355    def get_stats_profile(self):
356        """This method returns an instance of StatsProfile, which contains a mapping
357        of function names to instances of FunctionProfile. Each FunctionProfile
358        instance holds information related to the function's profile such as how
359        long the function took to run, how many times it was called, etc...
360        """
361        func_list = self.fcn_list[:] if self.fcn_list else list(self.stats.keys())
362        if not func_list:
363            return StatsProfile(0, {})
364
365        total_tt = float(f8(self.total_tt))
366        func_profiles = {}
367        stats_profile = StatsProfile(total_tt, func_profiles)
368
369        for func in func_list:
370            cc, nc, tt, ct, callers = self.stats[func]
371            file_name, line_number, func_name = func
372            ncalls = str(nc) if nc == cc else (str(nc) + '/' + str(cc))
373            tottime = float(f8(tt))
374            percall_tottime = -1 if nc == 0 else float(f8(tt/nc))
375            cumtime = float(f8(ct))
376            percall_cumtime = -1 if cc == 0 else float(f8(ct/cc))
377            func_profile = FunctionProfile(
378                ncalls,
379                tottime, # time spent in this function alone
380                percall_tottime,
381                cumtime, # time spent in the function plus all functions that this function called,
382                percall_cumtime,
383                file_name,
384                line_number
385            )
386            func_profiles[func_name] = func_profile
387
388        return stats_profile
389
390    def get_print_list(self, sel_list):
391        width = self.max_name_len
392        if self.fcn_list:
393            stat_list = self.fcn_list[:]
394            msg = "   Ordered by: " + self.sort_type + '\n'
395        else:
396            stat_list = list(self.stats.keys())
397            msg = "   Random listing order was used\n"
398
399        for selection in sel_list:
400            stat_list, msg = self.eval_print_amount(selection, stat_list, msg)
401
402        count = len(stat_list)
403
404        if not stat_list:
405            return 0, stat_list
406        print(msg, file=self.stream)
407        if count < len(self.stats):
408            width = 0
409            for func in stat_list:
410                if  len(func_std_string(func)) > width:
411                    width = len(func_std_string(func))
412        return width+2, stat_list
413
414    def print_stats(self, *amount):
415        for filename in self.files:
416            print(filename, file=self.stream)
417        if self.files:
418            print(file=self.stream)
419        indent = ' ' * 8
420        for func in self.top_level:
421            print(indent, func_get_function_name(func), file=self.stream)
422
423        print(indent, self.total_calls, "function calls", end=' ', file=self.stream)
424        if self.total_calls != self.prim_calls:
425            print("(%d primitive calls)" % self.prim_calls, end=' ', file=self.stream)
426        print("in %.3f seconds" % self.total_tt, file=self.stream)
427        print(file=self.stream)
428        width, list = self.get_print_list(amount)
429        if list:
430            self.print_title()
431            for func in list:
432                self.print_line(func)
433            print(file=self.stream)
434            print(file=self.stream)
435        return self
436
437    def print_callees(self, *amount):
438        width, list = self.get_print_list(amount)
439        if list:
440            self.calc_callees()
441
442            self.print_call_heading(width, "called...")
443            for func in list:
444                if func in self.all_callees:
445                    self.print_call_line(width, func, self.all_callees[func])
446                else:
447                    self.print_call_line(width, func, {})
448            print(file=self.stream)
449            print(file=self.stream)
450        return self
451
452    def print_callers(self, *amount):
453        width, list = self.get_print_list(amount)
454        if list:
455            self.print_call_heading(width, "was called by...")
456            for func in list:
457                cc, nc, tt, ct, callers = self.stats[func]
458                self.print_call_line(width, func, callers, "<-")
459            print(file=self.stream)
460            print(file=self.stream)
461        return self
462
463    def print_call_heading(self, name_size, column_title):
464        print("Function ".ljust(name_size) + column_title, file=self.stream)
465        # print sub-header only if we have new-style callers
466        subheader = False
467        for cc, nc, tt, ct, callers in self.stats.values():
468            if callers:
469                value = next(iter(callers.values()))
470                subheader = isinstance(value, tuple)
471                break
472        if subheader:
473            print(" "*name_size + "    ncalls  tottime  cumtime", file=self.stream)
474
475    def print_call_line(self, name_size, source, call_dict, arrow="->"):
476        print(func_std_string(source).ljust(name_size) + arrow, end=' ', file=self.stream)
477        if not call_dict:
478            print(file=self.stream)
479            return
480        clist = sorted(call_dict.keys())
481        indent = ""
482        for func in clist:
483            name = func_std_string(func)
484            value = call_dict[func]
485            if isinstance(value, tuple):
486                nc, cc, tt, ct = value
487                if nc != cc:
488                    substats = '%d/%d' % (nc, cc)
489                else:
490                    substats = '%d' % (nc,)
491                substats = '%s %s %s  %s' % (substats.rjust(7+2*len(indent)),
492                                             f8(tt), f8(ct), name)
493                left_width = name_size + 1
494            else:
495                substats = '%s(%r) %s' % (name, value, f8(self.stats[func][3]))
496                left_width = name_size + 3
497            print(indent*left_width + substats, file=self.stream)
498            indent = " "
499
500    def print_title(self):
501        print('   ncalls  tottime  percall  cumtime  percall', end=' ', file=self.stream)
502        print('filename:lineno(function)', file=self.stream)
503
504    def print_line(self, func):  # hack: should print percentages
505        cc, nc, tt, ct, callers = self.stats[func]
506        c = str(nc)
507        if nc != cc:
508            c = c + '/' + str(cc)
509        print(c.rjust(9), end=' ', file=self.stream)
510        print(f8(tt), end=' ', file=self.stream)
511        if nc == 0:
512            print(' '*8, end=' ', file=self.stream)
513        else:
514            print(f8(tt/nc), end=' ', file=self.stream)
515        print(f8(ct), end=' ', file=self.stream)
516        if cc == 0:
517            print(' '*8, end=' ', file=self.stream)
518        else:
519            print(f8(ct/cc), end=' ', file=self.stream)
520        print(func_std_string(func), file=self.stream)
521
522class TupleComp:
523    """This class provides a generic function for comparing any two tuples.
524    Each instance records a list of tuple-indices (from most significant
525    to least significant), and sort direction (ascending or descending) for
526    each tuple-index.  The compare functions can then be used as the function
527    argument to the system sort() function when a list of tuples need to be
528    sorted in the instances order."""
529
530    def __init__(self, comp_select_list):
531        self.comp_select_list = comp_select_list
532
533    def compare (self, left, right):
534        for index, direction in self.comp_select_list:
535            l = left[index]
536            r = right[index]
537            if l < r:
538                return -direction
539            if l > r:
540                return direction
541        return 0
542
543
544#**************************************************************************
545# func_name is a triple (file:string, line:int, name:string)
546
547def func_strip_path(func_name):
548    filename, line, name = func_name
549    return os.path.basename(filename), line, name
550
551def func_get_function_name(func):
552    return func[2]
553
554def func_std_string(func_name): # match what old profile produced
555    if func_name[:2] == ('~', 0):
556        # special case for built-in functions
557        name = func_name[2]
558        if name.startswith('<') and name.endswith('>'):
559            return '{%s}' % name[1:-1]
560        else:
561            return name
562    else:
563        return "%s:%d(%s)" % func_name
564
565#**************************************************************************
566# The following functions combine statistics for pairs functions.
567# The bulk of the processing involves correctly handling "call" lists,
568# such as callers and callees.
569#**************************************************************************
570
571def add_func_stats(target, source):
572    """Add together all the stats for two profile entries."""
573    cc, nc, tt, ct, callers = source
574    t_cc, t_nc, t_tt, t_ct, t_callers = target
575    return (cc+t_cc, nc+t_nc, tt+t_tt, ct+t_ct,
576              add_callers(t_callers, callers))
577
578def add_callers(target, source):
579    """Combine two caller lists in a single list."""
580    new_callers = {}
581    for func, caller in target.items():
582        new_callers[func] = caller
583    for func, caller in source.items():
584        if func in new_callers:
585            if isinstance(caller, tuple):
586                # format used by cProfile
587                new_callers[func] = tuple(i + j for i, j in zip(caller, new_callers[func]))
588            else:
589                # format used by profile
590                new_callers[func] += caller
591        else:
592            new_callers[func] = caller
593    return new_callers
594
595def count_calls(callers):
596    """Sum the caller statistics to get total number of calls received."""
597    nc = 0
598    for calls in callers.values():
599        nc += calls
600    return nc
601
602#**************************************************************************
603# The following functions support printing of reports
604#**************************************************************************
605
606def f8(x):
607    return "%8.3f" % x
608
609#**************************************************************************
610# Statistics browser added by ESR, April 2001
611#**************************************************************************
612
613if __name__ == '__main__':
614    import cmd
615    try:
616        import readline
617    except ImportError:
618        pass
619
620    class ProfileBrowser(cmd.Cmd):
621        def __init__(self, profile=None):
622            cmd.Cmd.__init__(self)
623            self.prompt = "% "
624            self.stats = None
625            self.stream = sys.stdout
626            if profile is not None:
627                self.do_read(profile)
628
629        def generic(self, fn, line):
630            args = line.split()
631            processed = []
632            for term in args:
633                try:
634                    processed.append(int(term))
635                    continue
636                except ValueError:
637                    pass
638                try:
639                    frac = float(term)
640                    if frac > 1 or frac < 0:
641                        print("Fraction argument must be in [0, 1]", file=self.stream)
642                        continue
643                    processed.append(frac)
644                    continue
645                except ValueError:
646                    pass
647                processed.append(term)
648            if self.stats:
649                getattr(self.stats, fn)(*processed)
650            else:
651                print("No statistics object is loaded.", file=self.stream)
652            return 0
653        def generic_help(self):
654            print("Arguments may be:", file=self.stream)
655            print("* An integer maximum number of entries to print.", file=self.stream)
656            print("* A decimal fractional number between 0 and 1, controlling", file=self.stream)
657            print("  what fraction of selected entries to print.", file=self.stream)
658            print("* A regular expression; only entries with function names", file=self.stream)
659            print("  that match it are printed.", file=self.stream)
660
661        def do_add(self, line):
662            if self.stats:
663                try:
664                    self.stats.add(line)
665                except OSError as e:
666                    print("Failed to load statistics for %s: %s" % (line, e), file=self.stream)
667            else:
668                print("No statistics object is loaded.", file=self.stream)
669            return 0
670        def help_add(self):
671            print("Add profile info from given file to current statistics object.", file=self.stream)
672
673        def do_callees(self, line):
674            return self.generic('print_callees', line)
675        def help_callees(self):
676            print("Print callees statistics from the current stat object.", file=self.stream)
677            self.generic_help()
678
679        def do_callers(self, line):
680            return self.generic('print_callers', line)
681        def help_callers(self):
682            print("Print callers statistics from the current stat object.", file=self.stream)
683            self.generic_help()
684
685        def do_EOF(self, line):
686            print("", file=self.stream)
687            return 1
688        def help_EOF(self):
689            print("Leave the profile browser.", file=self.stream)
690
691        def do_quit(self, line):
692            return 1
693        def help_quit(self):
694            print("Leave the profile browser.", file=self.stream)
695
696        def do_read(self, line):
697            if line:
698                try:
699                    self.stats = Stats(line)
700                except OSError as err:
701                    print(err.args[1], file=self.stream)
702                    return
703                except Exception as err:
704                    print(err.__class__.__name__ + ':', err, file=self.stream)
705                    return
706                self.prompt = line + "% "
707            elif len(self.prompt) > 2:
708                line = self.prompt[:-2]
709                self.do_read(line)
710            else:
711                print("No statistics object is current -- cannot reload.", file=self.stream)
712            return 0
713        def help_read(self):
714            print("Read in profile data from a specified file.", file=self.stream)
715            print("Without argument, reload the current file.", file=self.stream)
716
717        def do_reverse(self, line):
718            if self.stats:
719                self.stats.reverse_order()
720            else:
721                print("No statistics object is loaded.", file=self.stream)
722            return 0
723        def help_reverse(self):
724            print("Reverse the sort order of the profiling report.", file=self.stream)
725
726        def do_sort(self, line):
727            if not self.stats:
728                print("No statistics object is loaded.", file=self.stream)
729                return
730            abbrevs = self.stats.get_sort_arg_defs()
731            if line and all((x in abbrevs) for x in line.split()):
732                self.stats.sort_stats(*line.split())
733            else:
734                print("Valid sort keys (unique prefixes are accepted):", file=self.stream)
735                for (key, value) in Stats.sort_arg_dict_default.items():
736                    print("%s -- %s" % (key, value[1]), file=self.stream)
737            return 0
738        def help_sort(self):
739            print("Sort profile data according to specified keys.", file=self.stream)
740            print("(Typing `sort' without arguments lists valid keys.)", file=self.stream)
741        def complete_sort(self, text, *args):
742            return [a for a in Stats.sort_arg_dict_default if a.startswith(text)]
743
744        def do_stats(self, line):
745            return self.generic('print_stats', line)
746        def help_stats(self):
747            print("Print statistics from the current stat object.", file=self.stream)
748            self.generic_help()
749
750        def do_strip(self, line):
751            if self.stats:
752                self.stats.strip_dirs()
753            else:
754                print("No statistics object is loaded.", file=self.stream)
755        def help_strip(self):
756            print("Strip leading path information from filenames in the report.", file=self.stream)
757
758        def help_help(self):
759            print("Show help for a given command.", file=self.stream)
760
761        def postcmd(self, stop, line):
762            if stop:
763                return stop
764            return None
765
766    if len(sys.argv) > 1:
767        initprofile = sys.argv[1]
768    else:
769        initprofile = None
770    try:
771        browser = ProfileBrowser(initprofile)
772        for profile in sys.argv[2:]:
773            browser.do_add(profile)
774        print("Welcome to the profile statistics browser.", file=browser.stream)
775        browser.cmdloop()
776        print("Goodbye.", file=browser.stream)
777    except KeyboardInterrupt:
778        pass
779
780# That's all, folks.
781