pstats.py 25.5 KB
Newer Older
1 2
"""Class for printing reports on profiled python code."""

3
# Written by James Roskind
4 5 6
# Based on prior profile module by Sjoerd Mullender...
#   which was hacked somewhat by: Guido van Rossum

7 8
# Copyright Disney Enterprises, Inc.  All Rights Reserved.
# Licensed to PSF under a Contributor Agreement
Benjamin Peterson's avatar
Benjamin Peterson committed
9
#
10 11 12
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
Benjamin Peterson's avatar
Benjamin Peterson committed
13
#
14
# http://www.apache.org/licenses/LICENSE-2.0
Benjamin Peterson's avatar
Benjamin Peterson committed
15
#
16 17 18 19 20
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
# either express or implied.  See the License for the specific language
# governing permissions and limitations under the License.
21 22


23
import sys
24 25 26
import os
import time
import marshal
27
import re
28
from functools import cmp_to_key
29

30 31
__all__ = ["Stats"]

32
class Stats:
Tim Peters's avatar
Tim Peters committed
33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50
    """This class is used for creating reports from data generated by the
    Profile class.  It is a "friend" of that class, and imports data either
    by direct access to members of Profile class, or by reading in a dictionary
    that was emitted (via marshal) from the Profile class.

    The big change from the previous Profiler (in terms of raw functionality)
    is that an "add()" method has been provided to combine Stats from
    several distinct profile runs.  Both the constructor and the add()
    method now take arbitrarily many file names as arguments.

    All the print methods now take an argument that indicates how many lines
    to print.  If the arg is a floating point number between 0 and 1.0, then
    it is taken as a decimal percentage of the available lines to be printed
    (e.g., .1 means print 10% of all available lines).  If it is an integer,
    it is taken to mean the number of lines of data that you wish to have
    printed.

    The sort_stats() method now processes some additional options (i.e., in
51 52 53 54 55
    addition to the old -1, 0, 1, or 2).  It takes an arbitrary number of
    quoted strings to select the sort order.  For example sort_stats('time',
    'name') sorts on the major key of 'internal function time', and on the
    minor key of 'the name of the function'.  Look at the two tables in
    sort_stats() and get_sort_arg_defs(self) for more examples.
Tim Peters's avatar
Tim Peters committed
56

57
    All methods return self, so you can string together commands like:
Tim Peters's avatar
Tim Peters committed
58 59 60 61
        Stats('foo', 'goo').strip_dirs().sort_stats('calls').\
                            print_stats(5).print_callers(5)
    """

62 63
    def __init__(self, *args, stream=None):
        self.stream = stream or sys.stdout
Tim Peters's avatar
Tim Peters committed
64 65 66 67 68 69
        if not len(args):
            arg = None
        else:
            arg = args[0]
            args = args[1:]
        self.init(arg)
70
        self.add(*args)
Tim Peters's avatar
Tim Peters committed
71 72 73 74 75 76 77 78 79

    def init(self, arg):
        self.all_callees = None  # calc only if needed
        self.files = []
        self.fcn_list = None
        self.total_tt = 0
        self.total_calls = 0
        self.prim_calls = 0
        self.max_name_len = 0
80
        self.top_level = set()
Tim Peters's avatar
Tim Peters committed
81 82 83 84 85
        self.stats = {}
        self.sort_arg_dict = {}
        self.load_stats(arg)
        try:
            self.get_top_level_stats()
86 87 88 89
        except Exception:
            print("Invalid timing data %s" %
                  (self.files[-1] if self.files else ''), file=self.stream)
            raise
Tim Peters's avatar
Tim Peters committed
90 91

    def load_stats(self, arg):
92 93 94
        if arg is None:
            self.stats = {}
            return
95
        elif isinstance(arg, str):
Tim Peters's avatar
Tim Peters committed
96 97 98 99 100
            f = open(arg, 'rb')
            self.stats = marshal.load(f)
            f.close()
            try:
                file_stats = os.stat(arg)
101
                arg = time.ctime(file_stats.st_mtime) + "    " + arg
Tim Peters's avatar
Tim Peters committed
102 103
            except:  # in case this is not unix
                pass
104
            self.files = [arg]
Tim Peters's avatar
Tim Peters committed
105 106 107 108 109
        elif hasattr(arg, 'create_stats'):
            arg.create_stats()
            self.stats = arg.stats
            arg.stats = {}
        if not self.stats:
110
            raise TypeError("Cannot create or construct a %r object from %r"
111
                            % (self.__class__, arg))
Tim Peters's avatar
Tim Peters committed
112 113 114
        return

    def get_top_level_stats(self):
115 116 117 118
        for func, (cc, nc, tt, ct, callers) in self.stats.items():
            self.total_calls += nc
            self.prim_calls  += cc
            self.total_tt    += tt
119
            if ("jprofile", 0, "profiler") in callers:
120
                self.top_level.add(func)
Tim Peters's avatar
Tim Peters committed
121 122 123 124
            if len(func_std_string(func)) > self.max_name_len:
                self.max_name_len = len(func_std_string(func))

    def add(self, *arg_list):
125 126 127 128 129 130 131 132 133 134
        if not arg_list:
            return self
        for item in reversed(arg_list):
            if type(self) != type(item):
                item = Stats(item)
            self.files += item.files
            self.total_calls += item.total_calls
            self.prim_calls += item.prim_calls
            self.total_tt += item.total_tt
            for func in item.top_level:
135
                self.top_level.add(func)
Tim Peters's avatar
Tim Peters committed
136

137 138 139 140
            if self.max_name_len < item.max_name_len:
                self.max_name_len = item.max_name_len

            self.fcn_list = None
Tim Peters's avatar
Tim Peters committed
141

142 143 144 145 146 147
            for func, stat in item.stats.items():
                if func in self.stats:
                    old_func_stat = self.stats[func]
                else:
                    old_func_stat = (0, 0, 0, 0, {},)
                self.stats[func] = add_func_stats(old_func_stat, stat)
Tim Peters's avatar
Tim Peters committed
148 149
        return self

150 151
    def dump_stats(self, filename):
        """Write the profile data to a file we know how to load back."""
152
        f = open(filename, 'wb')
153 154 155 156 157
        try:
            marshal.dump(self.stats, f)
        finally:
            f.close()

Tim Peters's avatar
Tim Peters committed
158 159
    # list the tuple indices and directions for sorting,
    # along with some printable description
160 161 162 163 164 165 166 167 168 169 170
    sort_arg_dict_default = {
              "calls"     : (((1,-1),              ), "call count"),
              "cumulative": (((3,-1),              ), "cumulative time"),
              "file"      : (((4, 1),              ), "file name"),
              "line"      : (((5, 1),              ), "line number"),
              "module"    : (((4, 1),              ), "file name"),
              "name"      : (((6, 1),              ), "function name"),
              "nfl"       : (((6, 1),(4, 1),(5, 1),), "name/file/line"),
              "pcalls"    : (((0,-1),              ), "call count"),
              "stdname"   : (((7, 1),              ), "standard name"),
              "time"      : (((2,-1),              ), "internal time"),
Tim Peters's avatar
Tim Peters committed
171 172 173 174 175 176 177
              }

    def get_sort_arg_defs(self):
        """Expand all abbreviations that are unique."""
        if not self.sort_arg_dict:
            self.sort_arg_dict = dict = {}
            bad_list = {}
178
            for word, tup in self.sort_arg_dict_default.items():
Tim Peters's avatar
Tim Peters committed
179 180 181 182
                fragment = word
                while fragment:
                    if not fragment:
                        break
183
                    if fragment in dict:
Tim Peters's avatar
Tim Peters committed
184 185
                        bad_list[fragment] = 0
                        break
186
                    dict[fragment] = tup
Tim Peters's avatar
Tim Peters committed
187
                    fragment = fragment[:-1]
188
            for word in bad_list:
Tim Peters's avatar
Tim Peters committed
189 190 191 192 193 194 195
                del dict[word]
        return self.sort_arg_dict

    def sort_stats(self, *field):
        if not field:
            self.fcn_list = 0
            return self
196
        if len(field) == 1 and isinstance(field[0], int):
Tim Peters's avatar
Tim Peters committed
197
            # Be compatible with old profiler
198
            field = [ {-1: "stdname",
199 200 201
                       0:  "calls",
                       1:  "time",
                       2:  "cumulative"}[field[0]] ]
Tim Peters's avatar
Tim Peters committed
202 203 204 205 206 207 208

        sort_arg_defs = self.get_sort_arg_defs()
        sort_tuple = ()
        self.sort_type = ""
        connector = ""
        for word in field:
            sort_tuple = sort_tuple + sort_arg_defs[word][0]
209
            self.sort_type += connector + sort_arg_defs[word][1]
Tim Peters's avatar
Tim Peters committed
210 211 212
            connector = ", "

        stats_list = []
213
        for func, (cc, nc, tt, ct, callers) in self.stats.items():
214 215
            stats_list.append((cc, nc, tt, ct) + func +
                              (func_std_string(func), func))
Tim Peters's avatar
Tim Peters committed
216

217
        stats_list.sort(key=cmp_to_key(TupleComp(sort_tuple).compare))
Tim Peters's avatar
Tim Peters committed
218 219 220 221 222 223 224

        self.fcn_list = fcn_list = []
        for tuple in stats_list:
            fcn_list.append(tuple[-1])
        return self

    def reverse_order(self):
225 226
        if self.fcn_list:
            self.fcn_list.reverse()
Tim Peters's avatar
Tim Peters committed
227 228 229 230 231 232
        return self

    def strip_dirs(self):
        oldstats = self.stats
        self.stats = newstats = {}
        max_name_len = 0
233
        for func, (cc, nc, tt, ct, callers) in oldstats.items():
Tim Peters's avatar
Tim Peters committed
234 235 236 237
            newfunc = func_strip_path(func)
            if len(func_std_string(newfunc)) > max_name_len:
                max_name_len = len(func_std_string(newfunc))
            newcallers = {}
238
            for func2, caller in callers.items():
239
                newcallers[func_strip_path(func2)] = caller
Tim Peters's avatar
Tim Peters committed
240

241
            if newfunc in newstats:
242 243 244
                newstats[newfunc] = add_func_stats(
                                        newstats[newfunc],
                                        (cc, nc, tt, ct, newcallers))
Tim Peters's avatar
Tim Peters committed
245 246 247
            else:
                newstats[newfunc] = (cc, nc, tt, ct, newcallers)
        old_top = self.top_level
248
        self.top_level = new_top = set()
249
        for func in old_top:
250
            new_top.add(func_strip_path(func))
Tim Peters's avatar
Tim Peters committed
251 252 253 254 255 256 257 258

        self.max_name_len = max_name_len

        self.fcn_list = None
        self.all_callees = None
        return self

    def calc_callees(self):
259 260
        if self.all_callees:
            return
Tim Peters's avatar
Tim Peters committed
261
        self.all_callees = all_callees = {}
262
        for func, (cc, nc, tt, ct, callers) in self.stats.items():
263
            if not func in all_callees:
Tim Peters's avatar
Tim Peters committed
264
                all_callees[func] = {}
265
            for func2, caller in callers.items():
266
                if not func2 in all_callees:
Tim Peters's avatar
Tim Peters committed
267
                    all_callees[func2] = {}
268
                all_callees[func2][func]  = caller
Tim Peters's avatar
Tim Peters committed
269 270 271 272 273 274 275 276 277 278
        return

    #******************************************************************
    # The following functions support actual printing of reports
    #******************************************************************

    # Optional "amount" is either a line count, or a percentage of lines.

    def eval_print_amount(self, sel, list, msg):
        new_list = list
279 280 281 282 283 284
        if isinstance(sel, str):
            try:
                rex = re.compile(sel)
            except re.error:
                msg += "   <Invalid regular expression %r>\n" % sel
                return new_list, msg
Tim Peters's avatar
Tim Peters committed
285 286
            new_list = []
            for func in list:
287
                if rex.search(func_std_string(func)):
Tim Peters's avatar
Tim Peters committed
288 289 290
                    new_list.append(func)
        else:
            count = len(list)
291
            if isinstance(sel, float) and 0.0 <= sel < 1.0:
292
                count = int(count * sel + .5)
Tim Peters's avatar
Tim Peters committed
293
                new_list = list[:count]
294
            elif isinstance(sel, int) and 0 <= sel < count:
Tim Peters's avatar
Tim Peters committed
295 296 297
                count = sel
                new_list = list[:count]
        if len(list) != len(new_list):
298 299
            msg += "   List reduced from %r to %r due to restriction <%r>\n" % (
                len(list), len(new_list), sel)
Tim Peters's avatar
Tim Peters committed
300 301 302 303 304 305

        return new_list, msg

    def get_print_list(self, sel_list):
        width = self.max_name_len
        if self.fcn_list:
306
            stat_list = self.fcn_list[:]
Tim Peters's avatar
Tim Peters committed
307 308
            msg = "   Ordered by: " + self.sort_type + '\n'
        else:
309
            stat_list = list(self.stats.keys())
Tim Peters's avatar
Tim Peters committed
310 311 312
            msg = "   Random listing order was used\n"

        for selection in sel_list:
313
            stat_list, msg = self.eval_print_amount(selection, stat_list, msg)
Tim Peters's avatar
Tim Peters committed
314

315
        count = len(stat_list)
Tim Peters's avatar
Tim Peters committed
316

317 318
        if not stat_list:
            return 0, stat_list
319
        print(msg, file=self.stream)
Tim Peters's avatar
Tim Peters committed
320 321
        if count < len(self.stats):
            width = 0
322
            for func in stat_list:
Tim Peters's avatar
Tim Peters committed
323 324
                if  len(func_std_string(func)) > width:
                    width = len(func_std_string(func))
325
        return width+2, stat_list
Tim Peters's avatar
Tim Peters committed
326 327 328

    def print_stats(self, *amount):
        for filename in self.files:
329
            print(filename, file=self.stream)
330 331
        if self.files:
            print(file=self.stream)
332
        indent = ' ' * 8
333
        for func in self.top_level:
334
            print(indent, func_get_function_name(func), file=self.stream)
Tim Peters's avatar
Tim Peters committed
335

336
        print(indent, self.total_calls, "function calls", end=' ', file=self.stream)
Tim Peters's avatar
Tim Peters committed
337
        if self.total_calls != self.prim_calls:
338
            print("(%d primitive calls)" % self.prim_calls, end=' ', file=self.stream)
339
        print("in %.3f seconds" % self.total_tt, file=self.stream)
340
        print(file=self.stream)
Tim Peters's avatar
Tim Peters committed
341 342 343 344 345
        width, list = self.get_print_list(amount)
        if list:
            self.print_title()
            for func in list:
                self.print_line(func)
346 347
            print(file=self.stream)
            print(file=self.stream)
Tim Peters's avatar
Tim Peters committed
348 349 350 351 352 353 354 355 356
        return self

    def print_callees(self, *amount):
        width, list = self.get_print_list(amount)
        if list:
            self.calc_callees()

            self.print_call_heading(width, "called...")
            for func in list:
357
                if func in self.all_callees:
358
                    self.print_call_line(width, func, self.all_callees[func])
Tim Peters's avatar
Tim Peters committed
359 360
                else:
                    self.print_call_line(width, func, {})
361 362
            print(file=self.stream)
            print(file=self.stream)
Tim Peters's avatar
Tim Peters committed
363 364 365 366 367 368 369 370
        return self

    def print_callers(self, *amount):
        width, list = self.get_print_list(amount)
        if list:
            self.print_call_heading(width, "was called by...")
            for func in list:
                cc, nc, tt, ct, callers = self.stats[func]
Armin Rigo's avatar
Armin Rigo committed
371
                self.print_call_line(width, func, callers, "<-")
372 373
            print(file=self.stream)
            print(file=self.stream)
Tim Peters's avatar
Tim Peters committed
374 375 376
        return self

    def print_call_heading(self, name_size, column_title):
377
        print("Function ".ljust(name_size) + column_title, file=self.stream)
Armin Rigo's avatar
Armin Rigo committed
378 379
        # print sub-header only if we have new-style callers
        subheader = False
380
        for cc, nc, tt, ct, callers in self.stats.values():
Armin Rigo's avatar
Armin Rigo committed
381
            if callers:
382
                value = next(iter(callers.values()))
Armin Rigo's avatar
Armin Rigo committed
383 384 385
                subheader = isinstance(value, tuple)
                break
        if subheader:
386
            print(" "*name_size + "    ncalls  tottime  cumtime", file=self.stream)
Armin Rigo's avatar
Armin Rigo committed
387 388

    def print_call_line(self, name_size, source, call_dict, arrow="->"):
389
        print(func_std_string(source).ljust(name_size) + arrow, end=' ', file=self.stream)
Tim Peters's avatar
Tim Peters committed
390
        if not call_dict:
391
            print(file=self.stream)
Tim Peters's avatar
Tim Peters committed
392
            return
393
        clist = sorted(call_dict.keys())
Tim Peters's avatar
Tim Peters committed
394 395 396
        indent = ""
        for func in clist:
            name = func_std_string(func)
Armin Rigo's avatar
Armin Rigo committed
397 398 399 400 401 402 403 404 405 406 407 408 409
            value = call_dict[func]
            if isinstance(value, tuple):
                nc, cc, tt, ct = value
                if nc != cc:
                    substats = '%d/%d' % (nc, cc)
                else:
                    substats = '%d' % (nc,)
                substats = '%s %s %s  %s' % (substats.rjust(7+2*len(indent)),
                                             f8(tt), f8(ct), name)
                left_width = name_size + 1
            else:
                substats = '%s(%r) %s' % (name, value, f8(self.stats[func][3]))
                left_width = name_size + 3
410
            print(indent*left_width + substats, file=self.stream)
Tim Peters's avatar
Tim Peters committed
411 412 413
            indent = " "

    def print_title(self):
414 415
        print('   ncalls  tottime  percall  cumtime  percall', end=' ', file=self.stream)
        print('filename:lineno(function)', file=self.stream)
Tim Peters's avatar
Tim Peters committed
416

417
    def print_line(self, func):  # hack: should print percentages
Tim Peters's avatar
Tim Peters committed
418
        cc, nc, tt, ct, callers = self.stats[func]
419
        c = str(nc)
Tim Peters's avatar
Tim Peters committed
420
        if nc != cc:
421
            c = c + '/' + str(cc)
422 423
        print(c.rjust(9), end=' ', file=self.stream)
        print(f8(tt), end=' ', file=self.stream)
Tim Peters's avatar
Tim Peters committed
424
        if nc == 0:
425
            print(' '*8, end=' ', file=self.stream)
Tim Peters's avatar
Tim Peters committed
426
        else:
427 428
            print(f8(tt/nc), end=' ', file=self.stream)
        print(f8(ct), end=' ', file=self.stream)
Tim Peters's avatar
Tim Peters committed
429
        if cc == 0:
430
            print(' '*8, end=' ', file=self.stream)
Tim Peters's avatar
Tim Peters committed
431
        else:
432 433
            print(f8(ct/cc), end=' ', file=self.stream)
        print(func_std_string(func), file=self.stream)
Tim Peters's avatar
Tim Peters committed
434

435
class TupleComp:
Tim Peters's avatar
Tim Peters committed
436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455
    """This class provides a generic function for comparing any two tuples.
    Each instance records a list of tuple-indices (from most significant
    to least significant), and sort direction (ascending or decending) for
    each tuple-index.  The compare functions can then be used as the function
    argument to the system sort() function when a list of tuples need to be
    sorted in the instances order."""

    def __init__(self, comp_select_list):
        self.comp_select_list = comp_select_list

    def compare (self, left, right):
        for index, direction in self.comp_select_list:
            l = left[index]
            r = right[index]
            if l < r:
                return -direction
            if l > r:
                return direction
        return 0

456

457
#**************************************************************************
458
# func_name is a triple (file:string, line:int, name:string)
459 460

def func_strip_path(func_name):
461 462
    filename, line, name = func_name
    return os.path.basename(filename), line, name
463 464

def func_get_function_name(func):
Tim Peters's avatar
Tim Peters committed
465
    return func[2]
466 467

def func_std_string(func_name): # match what old profile produced
Armin Rigo's avatar
Armin Rigo committed
468 469 470 471 472 473 474 475 476
    if func_name[:2] == ('~', 0):
        # special case for built-in functions
        name = func_name[2]
        if name.startswith('<') and name.endswith('>'):
            return '{%s}' % name[1:-1]
        else:
            return name
    else:
        return "%s:%d(%s)" % func_name
477 478 479 480

#**************************************************************************
# The following functions combine statists for pairs functions.
# The bulk of the processing involves correctly handling "call" lists,
Tim Peters's avatar
Tim Peters committed
481
# such as callers and callees.
482 483
#**************************************************************************

484
def add_func_stats(target, source):
Tim Peters's avatar
Tim Peters committed
485 486 487
    """Add together all the stats for two profile entries."""
    cc, nc, tt, ct, callers = source
    t_cc, t_nc, t_tt, t_ct, t_callers = target
488
    return (cc+t_cc, nc+t_nc, tt+t_tt, ct+t_ct,
Tim Peters's avatar
Tim Peters committed
489
              add_callers(t_callers, callers))
490 491

def add_callers(target, source):
Tim Peters's avatar
Tim Peters committed
492 493
    """Combine two caller lists in a single list."""
    new_callers = {}
494
    for func, caller in target.items():
495
        new_callers[func] = caller
496
    for func, caller in source.items():
497
        if func in new_callers:
498 499 500 501 502 503 504
            if isinstance(caller, tuple):
                # format used by cProfile
                new_callers[func] = tuple([i[0] + i[1] for i in
                                           zip(caller, new_callers[func])])
            else:
                # format used by profile
                new_callers[func] += caller
Tim Peters's avatar
Tim Peters committed
505
        else:
506
            new_callers[func] = caller
Tim Peters's avatar
Tim Peters committed
507
    return new_callers
508 509

def count_calls(callers):
Tim Peters's avatar
Tim Peters committed
510 511
    """Sum the caller statistics to get total number of calls received."""
    nc = 0
512
    for calls in callers.values():
513
        nc += calls
Tim Peters's avatar
Tim Peters committed
514
    return nc
515 516 517 518 519 520

#**************************************************************************
# The following functions support printing of reports
#**************************************************************************

def f8(x):
521
    return "%8.3f" % x
522 523 524 525 526 527 528

#**************************************************************************
# Statistics browser added by ESR, April 2001
#**************************************************************************

if __name__ == '__main__':
    import cmd
529 530
    try:
        import readline
531
    except ImportError:
532
        pass
533 534 535

    class ProfileBrowser(cmd.Cmd):
        def __init__(self, profile=None):
536
            cmd.Cmd.__init__(self)
537
            self.prompt = "% "
538 539
            self.stats = None
            self.stream = sys.stdout
540
            if profile is not None:
541
                self.do_read(profile)
542 543 544 545 546 547 548 549 550 551 552 553 554

        def generic(self, fn, line):
            args = line.split()
            processed = []
            for term in args:
                try:
                    processed.append(int(term))
                    continue
                except ValueError:
                    pass
                try:
                    frac = float(term)
                    if frac > 1 or frac < 0:
555
                        print("Fraction argument must be in [0, 1]", file=self.stream)
556 557 558 559 560 561 562
                        continue
                    processed.append(frac)
                    continue
                except ValueError:
                    pass
                processed.append(term)
            if self.stats:
563
                getattr(self.stats, fn)(*processed)
564
            else:
565
                print("No statistics object is loaded.", file=self.stream)
566
            return 0
567
        def generic_help(self):
568 569 570 571 572 573
            print("Arguments may be:", file=self.stream)
            print("* An integer maximum number of entries to print.", file=self.stream)
            print("* A decimal fractional number between 0 and 1, controlling", file=self.stream)
            print("  what fraction of selected entries to print.", file=self.stream)
            print("* A regular expression; only entries with function names", file=self.stream)
            print("  that match it are printed.", file=self.stream)
574 575

        def do_add(self, line):
576 577 578 579
            if self.stats:
                self.stats.add(line)
            else:
                print("No statistics object is loaded.", file=self.stream)
580 581
            return 0
        def help_add(self):
582
            print("Add profile info from given file to current statistics object.", file=self.stream)
583 584

        def do_callees(self, line):
585
            return self.generic('print_callees', line)
586
        def help_callees(self):
587
            print("Print callees statistics from the current stat object.", file=self.stream)
588
            self.generic_help()
589 590

        def do_callers(self, line):
591
            return self.generic('print_callers', line)
592
        def help_callers(self):
593
            print("Print callers statistics from the current stat object.", file=self.stream)
594
            self.generic_help()
595 596

        def do_EOF(self, line):
597
            print("", file=self.stream)
598 599
            return 1
        def help_EOF(self):
600
            print("Leave the profile brower.", file=self.stream)
601 602 603 604

        def do_quit(self, line):
            return 1
        def help_quit(self):
605
            print("Leave the profile brower.", file=self.stream)
606 607 608 609 610

        def do_read(self, line):
            if line:
                try:
                    self.stats = Stats(line)
611 612
                except IOError as err:
                    print(err.args[1], file=self.stream)
613
                    return
614 615 616
                except Exception as err:
                    print(err.__class__.__name__ + ':', err, file=self.stream)
                    return
617
                self.prompt = line + "% "
618
            elif len(self.prompt) > 2:
619 620
                line = self.prompt[:-2]
                self.do_read(line)
621
            else:
622
                print("No statistics object is current -- cannot reload.", file=self.stream)
623 624
            return 0
        def help_read(self):
625
            print("Read in profile data from a specified file.", file=self.stream)
626
            print("Without argument, reload the current file.", file=self.stream)
627 628

        def do_reverse(self, line):
629 630 631 632
            if self.stats:
                self.stats.reverse_order()
            else:
                print("No statistics object is loaded.", file=self.stream)
633 634
            return 0
        def help_reverse(self):
635
            print("Reverse the sort order of the profiling report.", file=self.stream)
636 637

        def do_sort(self, line):
638 639 640
            if not self.stats:
                print("No statistics object is loaded.", file=self.stream)
                return
641
            abbrevs = self.stats.get_sort_arg_defs()
642
            if line and all((x in abbrevs) for x in line.split()):
643
                self.stats.sort_stats(*line.split())
644
            else:
645
                print("Valid sort keys (unique prefixes are accepted):", file=self.stream)
646
                for (key, value) in Stats.sort_arg_dict_default.items():
647
                    print("%s -- %s" % (key, value[1]), file=self.stream)
648 649
            return 0
        def help_sort(self):
650 651
            print("Sort profile data according to specified keys.", file=self.stream)
            print("(Typing `sort' without arguments lists valid keys.)", file=self.stream)
652
        def complete_sort(self, text, *args):
653
            return [a for a in Stats.sort_arg_dict_default if a.startswith(text)]
654 655 656 657

        def do_stats(self, line):
            return self.generic('print_stats', line)
        def help_stats(self):
658
            print("Print statistics from the current stat object.", file=self.stream)
659
            self.generic_help()
660 661

        def do_strip(self, line):
662 663 664 665
            if self.stats:
                self.stats.strip_dirs()
            else:
                print("No statistics object is loaded.", file=self.stream)
666
        def help_strip(self):
667
            print("Strip leading path information from filenames in the report.", file=self.stream)
668

669 670 671
        def help_help(self):
            print("Show help for a given command.", file=self.stream)

672 673 674 675 676 677 678 679 680 681
        def postcmd(self, stop, line):
            if stop:
                return stop
            return None

    if len(sys.argv) > 1:
        initprofile = sys.argv[1]
    else:
        initprofile = None
    try:
682
        browser = ProfileBrowser(initprofile)
683 684
        for profile in sys.argv[2:]:
            browser.do_add(profile)
685
        print("Welcome to the profile statistics browser.", file=browser.stream)
686
        browser.cmdloop()
687
        print("Goodbye.", file=browser.stream)
688 689 690 691
    except KeyboardInterrupt:
        pass

# That's all, folks.