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
23 import sys
24 import os
25 import time
26 import marshal
27 import re
28
29 from enum import StrEnum, _simple_enum
30 from functools import cmp_to_key
31 from dataclasses import dataclass
32 from typing import Dict
33
34 __all__ = ["Stats", "SortKey", "FunctionProfile", "StatsProfile"]
35
36 @_simple_enum(StrEnum)
37 class ESC[4;38;5;81mSortKey:
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)
59 class ESC[4;38;5;81mFunctionProfile:
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)
69 class ESC[4;38;5;81mStatsProfile:
70 '''Class for keeping track of an item in inventory.'''
71 total_tt: float
72 func_profiles: Dict[str, FunctionProfile]
73
74 class ESC[4;38;5;81mStats:
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.error:
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
520 class ESC[4;38;5;81mTupleComp:
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
545 def func_strip_path(func_name):
546 filename, line, name = func_name
547 return os.path.basename(filename), line, name
548
549 def func_get_function_name(func):
550 return func[2]
551
552 def 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
569 def 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
576 def 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
593 def 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
604 def f8(x):
605 return "%8.3f" % x
606
607 #**************************************************************************
608 # Statistics browser added by ESR, April 2001
609 #**************************************************************************
610
611 if __name__ == '__main__':
612 import cmd
613 try:
614 import readline
615 except ImportError:
616 pass
617
618 class ESC[4;38;5;81mProfileBrowser(ESC[4;38;5;149mcmdESC[4;38;5;149m.ESC[4;38;5;149mCmd):
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.