blob: 0f93ae02c95074dfd6bddff780474d11c28a654c [file] [log] [blame]
Olivier Deprezf4ef2d02021-04-20 13:36:24 +02001"""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 Enum
30from functools import cmp_to_key
31from dataclasses import dataclass
32from typing import Dict
33
34__all__ = ["Stats", "SortKey", "FunctionProfile", "StatsProfile"]
35
36class SortKey(str, Enum):
37 CALLS = 'calls', 'ncalls'
38 CUMULATIVE = 'cumulative', 'cumtime'
39 FILENAME = 'filename', 'module'
40 LINE = 'line'
41 NAME = 'name'
42 NFL = 'nfl'
43 PCALLS = 'pcalls'
44 STDNAME = 'stdname'
45 TIME = 'time', 'tottime'
46
47 def __new__(cls, *values):
48 value = values[0]
49 obj = str.__new__(cls, value)
50 obj._value_ = value
51 for other_value in values[1:]:
52 cls._value2member_map_[other_value] = obj
53 obj._all_values = values
54 return obj
55
56
57@dataclass(unsafe_hash=True)
58class FunctionProfile:
59 ncalls: int
60 tottime: float
61 percall_tottime: float
62 cumtime: float
63 percall_cumtime: float
64 file_name: str
65 line_number: int
66
67@dataclass(unsafe_hash=True)
68class StatsProfile:
69 '''Class for keeping track of an item in inventory.'''
70 total_tt: float
71 func_profiles: Dict[str, FunctionProfile]
72
73class Stats:
74 """This class is used for creating reports from data generated by the
75 Profile class. It is a "friend" of that class, and imports data either
76 by direct access to members of Profile class, or by reading in a dictionary
77 that was emitted (via marshal) from the Profile class.
78
79 The big change from the previous Profiler (in terms of raw functionality)
80 is that an "add()" method has been provided to combine Stats from
81 several distinct profile runs. Both the constructor and the add()
82 method now take arbitrarily many file names as arguments.
83
84 All the print methods now take an argument that indicates how many lines
85 to print. If the arg is a floating point number between 0 and 1.0, then
86 it is taken as a decimal percentage of the available lines to be printed
87 (e.g., .1 means print 10% of all available lines). If it is an integer,
88 it is taken to mean the number of lines of data that you wish to have
89 printed.
90
91 The sort_stats() method now processes some additional options (i.e., in
92 addition to the old -1, 0, 1, or 2 that are respectively interpreted as
93 'stdname', 'calls', 'time', and 'cumulative'). It takes either an
94 arbitrary number of quoted strings or SortKey enum to select the sort
95 order.
96
97 For example sort_stats('time', 'name') or sort_stats(SortKey.TIME,
98 SortKey.NAME) sorts on the major key of 'internal function time', and on
99 the minor key of 'the name of the function'. Look at the two tables in
100 sort_stats() and get_sort_arg_defs(self) for more examples.
101
102 All methods return self, so you can string together commands like:
103 Stats('foo', 'goo').strip_dirs().sort_stats('calls').\
104 print_stats(5).print_callers(5)
105 """
106
107 def __init__(self, *args, stream=None):
108 self.stream = stream or sys.stdout
109 if not len(args):
110 arg = None
111 else:
112 arg = args[0]
113 args = args[1:]
114 self.init(arg)
115 self.add(*args)
116
117 def init(self, arg):
118 self.all_callees = None # calc only if needed
119 self.files = []
120 self.fcn_list = None
121 self.total_tt = 0
122 self.total_calls = 0
123 self.prim_calls = 0
124 self.max_name_len = 0
125 self.top_level = set()
126 self.stats = {}
127 self.sort_arg_dict = {}
128 self.load_stats(arg)
129 try:
130 self.get_top_level_stats()
131 except Exception:
132 print("Invalid timing data %s" %
133 (self.files[-1] if self.files else ''), file=self.stream)
134 raise
135
136 def load_stats(self, arg):
137 if arg is None:
138 self.stats = {}
139 return
140 elif isinstance(arg, str):
141 with open(arg, 'rb') as f:
142 self.stats = marshal.load(f)
143 try:
144 file_stats = os.stat(arg)
145 arg = time.ctime(file_stats.st_mtime) + " " + arg
146 except: # in case this is not unix
147 pass
148 self.files = [arg]
149 elif hasattr(arg, 'create_stats'):
150 arg.create_stats()
151 self.stats = arg.stats
152 arg.stats = {}
153 if not self.stats:
154 raise TypeError("Cannot create or construct a %r object from %r"
155 % (self.__class__, arg))
156 return
157
158 def get_top_level_stats(self):
159 for func, (cc, nc, tt, ct, callers) in self.stats.items():
160 self.total_calls += nc
161 self.prim_calls += cc
162 self.total_tt += tt
163 if ("jprofile", 0, "profiler") in callers:
164 self.top_level.add(func)
165 if len(func_std_string(func)) > self.max_name_len:
166 self.max_name_len = len(func_std_string(func))
167
168 def add(self, *arg_list):
169 if not arg_list:
170 return self
171 for item in reversed(arg_list):
172 if type(self) != type(item):
173 item = Stats(item)
174 self.files += item.files
175 self.total_calls += item.total_calls
176 self.prim_calls += item.prim_calls
177 self.total_tt += item.total_tt
178 for func in item.top_level:
179 self.top_level.add(func)
180
181 if self.max_name_len < item.max_name_len:
182 self.max_name_len = item.max_name_len
183
184 self.fcn_list = None
185
186 for func, stat in item.stats.items():
187 if func in self.stats:
188 old_func_stat = self.stats[func]
189 else:
190 old_func_stat = (0, 0, 0, 0, {},)
191 self.stats[func] = add_func_stats(old_func_stat, stat)
192 return self
193
194 def dump_stats(self, filename):
195 """Write the profile data to a file we know how to load back."""
196 with open(filename, 'wb') as f:
197 marshal.dump(self.stats, f)
198
199 # list the tuple indices and directions for sorting,
200 # along with some printable description
201 sort_arg_dict_default = {
202 "calls" : (((1,-1), ), "call count"),
203 "ncalls" : (((1,-1), ), "call count"),
204 "cumtime" : (((3,-1), ), "cumulative time"),
205 "cumulative": (((3,-1), ), "cumulative time"),
206 "filename" : (((4, 1), ), "file name"),
207 "line" : (((5, 1), ), "line number"),
208 "module" : (((4, 1), ), "file name"),
209 "name" : (((6, 1), ), "function name"),
210 "nfl" : (((6, 1),(4, 1),(5, 1),), "name/file/line"),
211 "pcalls" : (((0,-1), ), "primitive call count"),
212 "stdname" : (((7, 1), ), "standard name"),
213 "time" : (((2,-1), ), "internal time"),
214 "tottime" : (((2,-1), ), "internal time"),
215 }
216
217 def get_sort_arg_defs(self):
218 """Expand all abbreviations that are unique."""
219 if not self.sort_arg_dict:
220 self.sort_arg_dict = dict = {}
221 bad_list = {}
222 for word, tup in self.sort_arg_dict_default.items():
223 fragment = word
224 while fragment:
225 if not fragment:
226 break
227 if fragment in dict:
228 bad_list[fragment] = 0
229 break
230 dict[fragment] = tup
231 fragment = fragment[:-1]
232 for word in bad_list:
233 del dict[word]
234 return self.sort_arg_dict
235
236 def sort_stats(self, *field):
237 if not field:
238 self.fcn_list = 0
239 return self
240 if len(field) == 1 and isinstance(field[0], int):
241 # Be compatible with old profiler
242 field = [ {-1: "stdname",
243 0: "calls",
244 1: "time",
245 2: "cumulative"}[field[0]] ]
246 elif len(field) >= 2:
247 for arg in field[1:]:
248 if type(arg) != type(field[0]):
249 raise TypeError("Can't have mixed argument type")
250
251 sort_arg_defs = self.get_sort_arg_defs()
252
253 sort_tuple = ()
254 self.sort_type = ""
255 connector = ""
256 for word in field:
257 if isinstance(word, SortKey):
258 word = word.value
259 sort_tuple = sort_tuple + sort_arg_defs[word][0]
260 self.sort_type += connector + sort_arg_defs[word][1]
261 connector = ", "
262
263 stats_list = []
264 for func, (cc, nc, tt, ct, callers) in self.stats.items():
265 stats_list.append((cc, nc, tt, ct) + func +
266 (func_std_string(func), func))
267
268 stats_list.sort(key=cmp_to_key(TupleComp(sort_tuple).compare))
269
270 self.fcn_list = fcn_list = []
271 for tuple in stats_list:
272 fcn_list.append(tuple[-1])
273 return self
274
275 def reverse_order(self):
276 if self.fcn_list:
277 self.fcn_list.reverse()
278 return self
279
280 def strip_dirs(self):
281 oldstats = self.stats
282 self.stats = newstats = {}
283 max_name_len = 0
284 for func, (cc, nc, tt, ct, callers) in oldstats.items():
285 newfunc = func_strip_path(func)
286 if len(func_std_string(newfunc)) > max_name_len:
287 max_name_len = len(func_std_string(newfunc))
288 newcallers = {}
289 for func2, caller in callers.items():
290 newcallers[func_strip_path(func2)] = caller
291
292 if newfunc in newstats:
293 newstats[newfunc] = add_func_stats(
294 newstats[newfunc],
295 (cc, nc, tt, ct, newcallers))
296 else:
297 newstats[newfunc] = (cc, nc, tt, ct, newcallers)
298 old_top = self.top_level
299 self.top_level = new_top = set()
300 for func in old_top:
301 new_top.add(func_strip_path(func))
302
303 self.max_name_len = max_name_len
304
305 self.fcn_list = None
306 self.all_callees = None
307 return self
308
309 def calc_callees(self):
310 if self.all_callees:
311 return
312 self.all_callees = all_callees = {}
313 for func, (cc, nc, tt, ct, callers) in self.stats.items():
314 if not func in all_callees:
315 all_callees[func] = {}
316 for func2, caller in callers.items():
317 if not func2 in all_callees:
318 all_callees[func2] = {}
319 all_callees[func2][func] = caller
320 return
321
322 #******************************************************************
323 # The following functions support actual printing of reports
324 #******************************************************************
325
326 # Optional "amount" is either a line count, or a percentage of lines.
327
328 def eval_print_amount(self, sel, list, msg):
329 new_list = list
330 if isinstance(sel, str):
331 try:
332 rex = re.compile(sel)
333 except re.error:
334 msg += " <Invalid regular expression %r>\n" % sel
335 return new_list, msg
336 new_list = []
337 for func in list:
338 if rex.search(func_std_string(func)):
339 new_list.append(func)
340 else:
341 count = len(list)
342 if isinstance(sel, float) and 0.0 <= sel < 1.0:
343 count = int(count * sel + .5)
344 new_list = list[:count]
345 elif isinstance(sel, int) and 0 <= sel < count:
346 count = sel
347 new_list = list[:count]
348 if len(list) != len(new_list):
349 msg += " List reduced from %r to %r due to restriction <%r>\n" % (
350 len(list), len(new_list), sel)
351
352 return new_list, msg
353
354 def get_stats_profile(self):
355 """This method returns an instance of StatsProfile, which contains a mapping
356 of function names to instances of FunctionProfile. Each FunctionProfile
357 instance holds information related to the function's profile such as how
358 long the function took to run, how many times it was called, etc...
359 """
360 func_list = self.fcn_list[:] if self.fcn_list else list(self.stats.keys())
361 if not func_list:
362 return StatsProfile(0, {})
363
364 total_tt = float(f8(self.total_tt))
365 func_profiles = {}
366 stats_profile = StatsProfile(total_tt, func_profiles)
367
368 for func in func_list:
369 cc, nc, tt, ct, callers = self.stats[func]
370 file_name, line_number, func_name = func
371 ncalls = str(nc) if nc == cc else (str(nc) + '/' + str(cc))
372 tottime = float(f8(tt))
373 percall_tottime = -1 if nc == 0 else float(f8(tt/nc))
374 cumtime = float(f8(ct))
375 percall_cumtime = -1 if cc == 0 else float(f8(ct/cc))
376 func_profile = FunctionProfile(
377 ncalls,
378 tottime, # time spent in this function alone
379 percall_tottime,
380 cumtime, # time spent in the function plus all functions that this function called,
381 percall_cumtime,
382 file_name,
383 line_number
384 )
385 func_profiles[func_name] = func_profile
386
387 return stats_profile
388
389 def get_print_list(self, sel_list):
390 width = self.max_name_len
391 if self.fcn_list:
392 stat_list = self.fcn_list[:]
393 msg = " Ordered by: " + self.sort_type + '\n'
394 else:
395 stat_list = list(self.stats.keys())
396 msg = " Random listing order was used\n"
397
398 for selection in sel_list:
399 stat_list, msg = self.eval_print_amount(selection, stat_list, msg)
400
401 count = len(stat_list)
402
403 if not stat_list:
404 return 0, stat_list
405 print(msg, file=self.stream)
406 if count < len(self.stats):
407 width = 0
408 for func in stat_list:
409 if len(func_std_string(func)) > width:
410 width = len(func_std_string(func))
411 return width+2, stat_list
412
413 def print_stats(self, *amount):
414 for filename in self.files:
415 print(filename, file=self.stream)
416 if self.files:
417 print(file=self.stream)
418 indent = ' ' * 8
419 for func in self.top_level:
420 print(indent, func_get_function_name(func), file=self.stream)
421
422 print(indent, self.total_calls, "function calls", end=' ', file=self.stream)
423 if self.total_calls != self.prim_calls:
424 print("(%d primitive calls)" % self.prim_calls, end=' ', file=self.stream)
425 print("in %.3f seconds" % self.total_tt, file=self.stream)
426 print(file=self.stream)
427 width, list = self.get_print_list(amount)
428 if list:
429 self.print_title()
430 for func in list:
431 self.print_line(func)
432 print(file=self.stream)
433 print(file=self.stream)
434 return self
435
436 def print_callees(self, *amount):
437 width, list = self.get_print_list(amount)
438 if list:
439 self.calc_callees()
440
441 self.print_call_heading(width, "called...")
442 for func in list:
443 if func in self.all_callees:
444 self.print_call_line(width, func, self.all_callees[func])
445 else:
446 self.print_call_line(width, func, {})
447 print(file=self.stream)
448 print(file=self.stream)
449 return self
450
451 def print_callers(self, *amount):
452 width, list = self.get_print_list(amount)
453 if list:
454 self.print_call_heading(width, "was called by...")
455 for func in list:
456 cc, nc, tt, ct, callers = self.stats[func]
457 self.print_call_line(width, func, callers, "<-")
458 print(file=self.stream)
459 print(file=self.stream)
460 return self
461
462 def print_call_heading(self, name_size, column_title):
463 print("Function ".ljust(name_size) + column_title, file=self.stream)
464 # print sub-header only if we have new-style callers
465 subheader = False
466 for cc, nc, tt, ct, callers in self.stats.values():
467 if callers:
468 value = next(iter(callers.values()))
469 subheader = isinstance(value, tuple)
470 break
471 if subheader:
472 print(" "*name_size + " ncalls tottime cumtime", file=self.stream)
473
474 def print_call_line(self, name_size, source, call_dict, arrow="->"):
475 print(func_std_string(source).ljust(name_size) + arrow, end=' ', file=self.stream)
476 if not call_dict:
477 print(file=self.stream)
478 return
479 clist = sorted(call_dict.keys())
480 indent = ""
481 for func in clist:
482 name = func_std_string(func)
483 value = call_dict[func]
484 if isinstance(value, tuple):
485 nc, cc, tt, ct = value
486 if nc != cc:
487 substats = '%d/%d' % (nc, cc)
488 else:
489 substats = '%d' % (nc,)
490 substats = '%s %s %s %s' % (substats.rjust(7+2*len(indent)),
491 f8(tt), f8(ct), name)
492 left_width = name_size + 1
493 else:
494 substats = '%s(%r) %s' % (name, value, f8(self.stats[func][3]))
495 left_width = name_size + 3
496 print(indent*left_width + substats, file=self.stream)
497 indent = " "
498
499 def print_title(self):
500 print(' ncalls tottime percall cumtime percall', end=' ', file=self.stream)
501 print('filename:lineno(function)', file=self.stream)
502
503 def print_line(self, func): # hack: should print percentages
504 cc, nc, tt, ct, callers = self.stats[func]
505 c = str(nc)
506 if nc != cc:
507 c = c + '/' + str(cc)
508 print(c.rjust(9), end=' ', file=self.stream)
509 print(f8(tt), end=' ', file=self.stream)
510 if nc == 0:
511 print(' '*8, end=' ', file=self.stream)
512 else:
513 print(f8(tt/nc), end=' ', file=self.stream)
514 print(f8(ct), end=' ', file=self.stream)
515 if cc == 0:
516 print(' '*8, end=' ', file=self.stream)
517 else:
518 print(f8(ct/cc), end=' ', file=self.stream)
519 print(func_std_string(func), file=self.stream)
520
521class TupleComp:
522 """This class provides a generic function for comparing any two tuples.
523 Each instance records a list of tuple-indices (from most significant
524 to least significant), and sort direction (ascending or decending) for
525 each tuple-index. The compare functions can then be used as the function
526 argument to the system sort() function when a list of tuples need to be
527 sorted in the instances order."""
528
529 def __init__(self, comp_select_list):
530 self.comp_select_list = comp_select_list
531
532 def compare (self, left, right):
533 for index, direction in self.comp_select_list:
534 l = left[index]
535 r = right[index]
536 if l < r:
537 return -direction
538 if l > r:
539 return direction
540 return 0
541
542
543#**************************************************************************
544# func_name is a triple (file:string, line:int, name:string)
545
546def func_strip_path(func_name):
547 filename, line, name = func_name
548 return os.path.basename(filename), line, name
549
550def func_get_function_name(func):
551 return func[2]
552
553def func_std_string(func_name): # match what old profile produced
554 if func_name[:2] == ('~', 0):
555 # special case for built-in functions
556 name = func_name[2]
557 if name.startswith('<') and name.endswith('>'):
558 return '{%s}' % name[1:-1]
559 else:
560 return name
561 else:
562 return "%s:%d(%s)" % func_name
563
564#**************************************************************************
565# The following functions combine statistics for pairs functions.
566# The bulk of the processing involves correctly handling "call" lists,
567# such as callers and callees.
568#**************************************************************************
569
570def add_func_stats(target, source):
571 """Add together all the stats for two profile entries."""
572 cc, nc, tt, ct, callers = source
573 t_cc, t_nc, t_tt, t_ct, t_callers = target
574 return (cc+t_cc, nc+t_nc, tt+t_tt, ct+t_ct,
575 add_callers(t_callers, callers))
576
577def add_callers(target, source):
578 """Combine two caller lists in a single list."""
579 new_callers = {}
580 for func, caller in target.items():
581 new_callers[func] = caller
582 for func, caller in source.items():
583 if func in new_callers:
584 if isinstance(caller, tuple):
585 # format used by cProfile
586 new_callers[func] = tuple(i + j for i, j in zip(caller, new_callers[func]))
587 else:
588 # format used by profile
589 new_callers[func] += caller
590 else:
591 new_callers[func] = caller
592 return new_callers
593
594def count_calls(callers):
595 """Sum the caller statistics to get total number of calls received."""
596 nc = 0
597 for calls in callers.values():
598 nc += calls
599 return nc
600
601#**************************************************************************
602# The following functions support printing of reports
603#**************************************************************************
604
605def f8(x):
606 return "%8.3f" % x
607
608#**************************************************************************
609# Statistics browser added by ESR, April 2001
610#**************************************************************************
611
612if __name__ == '__main__':
613 import cmd
614 try:
615 import readline
616 except ImportError:
617 pass
618
619 class ProfileBrowser(cmd.Cmd):
620 def __init__(self, profile=None):
621 cmd.Cmd.__init__(self)
622 self.prompt = "% "
623 self.stats = None
624 self.stream = sys.stdout
625 if profile is not None:
626 self.do_read(profile)
627
628 def generic(self, fn, line):
629 args = line.split()
630 processed = []
631 for term in args:
632 try:
633 processed.append(int(term))
634 continue
635 except ValueError:
636 pass
637 try:
638 frac = float(term)
639 if frac > 1 or frac < 0:
640 print("Fraction argument must be in [0, 1]", file=self.stream)
641 continue
642 processed.append(frac)
643 continue
644 except ValueError:
645 pass
646 processed.append(term)
647 if self.stats:
648 getattr(self.stats, fn)(*processed)
649 else:
650 print("No statistics object is loaded.", file=self.stream)
651 return 0
652 def generic_help(self):
653 print("Arguments may be:", file=self.stream)
654 print("* An integer maximum number of entries to print.", file=self.stream)
655 print("* A decimal fractional number between 0 and 1, controlling", file=self.stream)
656 print(" what fraction of selected entries to print.", file=self.stream)
657 print("* A regular expression; only entries with function names", file=self.stream)
658 print(" that match it are printed.", file=self.stream)
659
660 def do_add(self, line):
661 if self.stats:
662 try:
663 self.stats.add(line)
664 except OSError as e:
665 print("Failed to load statistics for %s: %s" % (line, e), file=self.stream)
666 else:
667 print("No statistics object is loaded.", file=self.stream)
668 return 0
669 def help_add(self):
670 print("Add profile info from given file to current statistics object.", file=self.stream)
671
672 def do_callees(self, line):
673 return self.generic('print_callees', line)
674 def help_callees(self):
675 print("Print callees statistics from the current stat object.", file=self.stream)
676 self.generic_help()
677
678 def do_callers(self, line):
679 return self.generic('print_callers', line)
680 def help_callers(self):
681 print("Print callers statistics from the current stat object.", file=self.stream)
682 self.generic_help()
683
684 def do_EOF(self, line):
685 print("", file=self.stream)
686 return 1
687 def help_EOF(self):
688 print("Leave the profile browser.", file=self.stream)
689
690 def do_quit(self, line):
691 return 1
692 def help_quit(self):
693 print("Leave the profile browser.", file=self.stream)
694
695 def do_read(self, line):
696 if line:
697 try:
698 self.stats = Stats(line)
699 except OSError as err:
700 print(err.args[1], file=self.stream)
701 return
702 except Exception as err:
703 print(err.__class__.__name__ + ':', err, file=self.stream)
704 return
705 self.prompt = line + "% "
706 elif len(self.prompt) > 2:
707 line = self.prompt[:-2]
708 self.do_read(line)
709 else:
710 print("No statistics object is current -- cannot reload.", file=self.stream)
711 return 0
712 def help_read(self):
713 print("Read in profile data from a specified file.", file=self.stream)
714 print("Without argument, reload the current file.", file=self.stream)
715
716 def do_reverse(self, line):
717 if self.stats:
718 self.stats.reverse_order()
719 else:
720 print("No statistics object is loaded.", file=self.stream)
721 return 0
722 def help_reverse(self):
723 print("Reverse the sort order of the profiling report.", file=self.stream)
724
725 def do_sort(self, line):
726 if not self.stats:
727 print("No statistics object is loaded.", file=self.stream)
728 return
729 abbrevs = self.stats.get_sort_arg_defs()
730 if line and all((x in abbrevs) for x in line.split()):
731 self.stats.sort_stats(*line.split())
732 else:
733 print("Valid sort keys (unique prefixes are accepted):", file=self.stream)
734 for (key, value) in Stats.sort_arg_dict_default.items():
735 print("%s -- %s" % (key, value[1]), file=self.stream)
736 return 0
737 def help_sort(self):
738 print("Sort profile data according to specified keys.", file=self.stream)
739 print("(Typing `sort' without arguments lists valid keys.)", file=self.stream)
740 def complete_sort(self, text, *args):
741 return [a for a in Stats.sort_arg_dict_default if a.startswith(text)]
742
743 def do_stats(self, line):
744 return self.generic('print_stats', line)
745 def help_stats(self):
746 print("Print statistics from the current stat object.", file=self.stream)
747 self.generic_help()
748
749 def do_strip(self, line):
750 if self.stats:
751 self.stats.strip_dirs()
752 else:
753 print("No statistics object is loaded.", file=self.stream)
754 def help_strip(self):
755 print("Strip leading path information from filenames in the report.", file=self.stream)
756
757 def help_help(self):
758 print("Show help for a given command.", file=self.stream)
759
760 def postcmd(self, stop, line):
761 if stop:
762 return stop
763 return None
764
765 if len(sys.argv) > 1:
766 initprofile = sys.argv[1]
767 else:
768 initprofile = None
769 try:
770 browser = ProfileBrowser(initprofile)
771 for profile in sys.argv[2:]:
772 browser.do_add(profile)
773 print("Welcome to the profile statistics browser.", file=browser.stream)
774 browser.cmdloop()
775 print("Goodbye.", file=browser.stream)
776 except KeyboardInterrupt:
777 pass
778
779# That's all, folks.