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