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