]> git.lizzy.rs Git - plan9front.git/blob - sys/lib/python/pstats.py
dist/mkfile: run binds in subshell
[plan9front.git] / sys / lib / python / pstats.py
1 """Class for printing reports on profiled python code."""
2
3 # Class for printing reports on profiled python code. rev 1.0  4/1/94
4 #
5 # Based on prior profile module by Sjoerd Mullender...
6 #   which was hacked somewhat by: Guido van Rossum
7 #
8 # see profile.doc and profile.py for more info.
9
10 # Copyright 1994, by InfoSeek Corporation, all rights reserved.
11 # Written by James Roskind
12 #
13 # Permission to use, copy, modify, and distribute this Python software
14 # and its associated documentation for any purpose (subject to the
15 # restriction in the following sentence) without fee is hereby granted,
16 # provided that the above copyright notice appears in all copies, and
17 # that both that copyright notice and this permission notice appear in
18 # supporting documentation, and that the name of InfoSeek not be used in
19 # advertising or publicity pertaining to distribution of the software
20 # without specific, written prior permission.  This permission is
21 # explicitly restricted to the copying and modification of the software
22 # to remain in Python, compiled Python, or other languages (such as C)
23 # wherein the modified or derived code is exclusively imported into a
24 # Python module.
25 #
26 # INFOSEEK CORPORATION DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS
27 # SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
28 # FITNESS. IN NO EVENT SHALL INFOSEEK CORPORATION BE LIABLE FOR ANY
29 # SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER
30 # RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF
31 # CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
32 # CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
33
34
35 import sys
36 import os
37 import time
38 import marshal
39 import re
40
41 __all__ = ["Stats"]
42
43 class Stats:
44     """This class is used for creating reports from data generated by the
45     Profile class.  It is a "friend" of that class, and imports data either
46     by direct access to members of Profile class, or by reading in a dictionary
47     that was emitted (via marshal) from the Profile class.
48
49     The big change from the previous Profiler (in terms of raw functionality)
50     is that an "add()" method has been provided to combine Stats from
51     several distinct profile runs.  Both the constructor and the add()
52     method now take arbitrarily many file names as arguments.
53
54     All the print methods now take an argument that indicates how many lines
55     to print.  If the arg is a floating point number between 0 and 1.0, then
56     it is taken as a decimal percentage of the available lines to be printed
57     (e.g., .1 means print 10% of all available lines).  If it is an integer,
58     it is taken to mean the number of lines of data that you wish to have
59     printed.
60
61     The sort_stats() method now processes some additional options (i.e., in
62     addition to the old -1, 0, 1, or 2).  It takes an arbitrary number of
63     quoted strings to select the sort order.  For example sort_stats('time',
64     'name') sorts on the major key of 'internal function time', and on the
65     minor key of 'the name of the function'.  Look at the two tables in
66     sort_stats() and get_sort_arg_defs(self) for more examples.
67
68     All methods return self,  so you can string together commands like:
69         Stats('foo', 'goo').strip_dirs().sort_stats('calls').\
70                             print_stats(5).print_callers(5)
71     """
72
73     def __init__(self, *args, **kwds):
74         # I can't figure out how to explictly specify a stream keyword arg
75         # with *args:
76         #   def __init__(self, *args, stream=sys.stdout): ...
77         # so I use **kwds and sqauwk if something unexpected is passed in.
78         self.stream = sys.stdout
79         if "stream" in kwds:
80             self.stream = kwds["stream"]
81             del kwds["stream"]
82         if kwds:
83             keys = kwds.keys()
84             keys.sort()
85             extras = ", ".join(["%s=%s" % (k, kwds[k]) for k in keys])
86             raise ValueError, "unrecognized keyword args: %s" % extras
87         if not len(args):
88             arg = None
89         else:
90             arg = args[0]
91             args = args[1:]
92         self.init(arg)
93         self.add(*args)
94
95     def init(self, arg):
96         self.all_callees = None  # calc only if needed
97         self.files = []
98         self.fcn_list = None
99         self.total_tt = 0
100         self.total_calls = 0
101         self.prim_calls = 0
102         self.max_name_len = 0
103         self.top_level = {}
104         self.stats = {}
105         self.sort_arg_dict = {}
106         self.load_stats(arg)
107         trouble = 1
108         try:
109             self.get_top_level_stats()
110             trouble = 0
111         finally:
112             if trouble:
113                 print >> self.stream, "Invalid timing data",
114                 if self.files: print >> self.stream, self.files[-1],
115                 print >> self.stream
116
117     def load_stats(self, arg):
118         if not arg:  self.stats = {}
119         elif isinstance(arg, basestring):
120             f = open(arg, 'rb')
121             self.stats = marshal.load(f)
122             f.close()
123             try:
124                 file_stats = os.stat(arg)
125                 arg = time.ctime(file_stats.st_mtime) + "    " + arg
126             except:  # in case this is not unix
127                 pass
128             self.files = [ arg ]
129         elif hasattr(arg, 'create_stats'):
130             arg.create_stats()
131             self.stats = arg.stats
132             arg.stats = {}
133         if not self.stats:
134             raise TypeError,  "Cannot create or construct a %r object from '%r''" % (
135                               self.__class__, arg)
136         return
137
138     def get_top_level_stats(self):
139         for func, (cc, nc, tt, ct, callers) in self.stats.items():
140             self.total_calls += nc
141             self.prim_calls  += cc
142             self.total_tt    += tt
143             if callers.has_key(("jprofile", 0, "profiler")):
144                 self.top_level[func] = None
145             if len(func_std_string(func)) > self.max_name_len:
146                 self.max_name_len = len(func_std_string(func))
147
148     def add(self, *arg_list):
149         if not arg_list: return self
150         if len(arg_list) > 1: self.add(*arg_list[1:])
151         other = arg_list[0]
152         if type(self) != type(other) or self.__class__ != other.__class__:
153             other = Stats(other)
154         self.files += other.files
155         self.total_calls += other.total_calls
156         self.prim_calls += other.prim_calls
157         self.total_tt += other.total_tt
158         for func in other.top_level:
159             self.top_level[func] = None
160
161         if self.max_name_len < other.max_name_len:
162             self.max_name_len = other.max_name_len
163
164         self.fcn_list = None
165
166         for func, stat in other.stats.iteritems():
167             if func in self.stats:
168                 old_func_stat = self.stats[func]
169             else:
170                 old_func_stat = (0, 0, 0, 0, {},)
171             self.stats[func] = add_func_stats(old_func_stat, stat)
172         return self
173
174     def dump_stats(self, filename):
175         """Write the profile data to a file we know how to load back."""
176         f = file(filename, 'wb')
177         try:
178             marshal.dump(self.stats, f)
179         finally:
180             f.close()
181
182     # list the tuple indices and directions for sorting,
183     # along with some printable description
184     sort_arg_dict_default = {
185               "calls"     : (((1,-1),              ), "call count"),
186               "cumulative": (((3,-1),              ), "cumulative time"),
187               "file"      : (((4, 1),              ), "file name"),
188               "line"      : (((5, 1),              ), "line number"),
189               "module"    : (((4, 1),              ), "file name"),
190               "name"      : (((6, 1),              ), "function name"),
191               "nfl"       : (((6, 1),(4, 1),(5, 1),), "name/file/line"),
192               "pcalls"    : (((0,-1),              ), "call count"),
193               "stdname"   : (((7, 1),              ), "standard name"),
194               "time"      : (((2,-1),              ), "internal time"),
195               }
196
197     def get_sort_arg_defs(self):
198         """Expand all abbreviations that are unique."""
199         if not self.sort_arg_dict:
200             self.sort_arg_dict = dict = {}
201             bad_list = {}
202             for word, tup in self.sort_arg_dict_default.iteritems():
203                 fragment = word
204                 while fragment:
205                     if not fragment:
206                         break
207                     if fragment in dict:
208                         bad_list[fragment] = 0
209                         break
210                     dict[fragment] = tup
211                     fragment = fragment[:-1]
212             for word in bad_list:
213                 del dict[word]
214         return self.sort_arg_dict
215
216     def sort_stats(self, *field):
217         if not field:
218             self.fcn_list = 0
219             return self
220         if len(field) == 1 and type(field[0]) == type(1):
221             # Be compatible with old profiler
222             field = [ {-1: "stdname",
223                       0:"calls",
224                       1:"time",
225                       2: "cumulative" }  [ field[0] ] ]
226
227         sort_arg_defs = self.get_sort_arg_defs()
228         sort_tuple = ()
229         self.sort_type = ""
230         connector = ""
231         for word in field:
232             sort_tuple = sort_tuple + sort_arg_defs[word][0]
233             self.sort_type += connector + sort_arg_defs[word][1]
234             connector = ", "
235
236         stats_list = []
237         for func, (cc, nc, tt, ct, callers) in self.stats.iteritems():
238             stats_list.append((cc, nc, tt, ct) + func +
239                               (func_std_string(func), func))
240
241         stats_list.sort(TupleComp(sort_tuple).compare)
242
243         self.fcn_list = fcn_list = []
244         for tuple in stats_list:
245             fcn_list.append(tuple[-1])
246         return self
247
248     def reverse_order(self):
249         if self.fcn_list:
250             self.fcn_list.reverse()
251         return self
252
253     def strip_dirs(self):
254         oldstats = self.stats
255         self.stats = newstats = {}
256         max_name_len = 0
257         for func, (cc, nc, tt, ct, callers) in oldstats.iteritems():
258             newfunc = func_strip_path(func)
259             if len(func_std_string(newfunc)) > max_name_len:
260                 max_name_len = len(func_std_string(newfunc))
261             newcallers = {}
262             for func2, caller in callers.iteritems():
263                 newcallers[func_strip_path(func2)] = caller
264
265             if newfunc in newstats:
266                 newstats[newfunc] = add_func_stats(
267                                         newstats[newfunc],
268                                         (cc, nc, tt, ct, newcallers))
269             else:
270                 newstats[newfunc] = (cc, nc, tt, ct, newcallers)
271         old_top = self.top_level
272         self.top_level = new_top = {}
273         for func in old_top:
274             new_top[func_strip_path(func)] = None
275
276         self.max_name_len = max_name_len
277
278         self.fcn_list = None
279         self.all_callees = None
280         return self
281
282     def calc_callees(self):
283         if self.all_callees: return
284         self.all_callees = all_callees = {}
285         for func, (cc, nc, tt, ct, callers) in self.stats.iteritems():
286             if not func in all_callees:
287                 all_callees[func] = {}
288             for func2, caller in callers.iteritems():
289                 if not func2 in all_callees:
290                     all_callees[func2] = {}
291                 all_callees[func2][func]  = caller
292         return
293
294     #******************************************************************
295     # The following functions support actual printing of reports
296     #******************************************************************
297
298     # Optional "amount" is either a line count, or a percentage of lines.
299
300     def eval_print_amount(self, sel, list, msg):
301         new_list = list
302         if type(sel) == type(""):
303             new_list = []
304             for func in list:
305                 if re.search(sel, func_std_string(func)):
306                     new_list.append(func)
307         else:
308             count = len(list)
309             if type(sel) == type(1.0) and 0.0 <= sel < 1.0:
310                 count = int(count * sel + .5)
311                 new_list = list[:count]
312             elif type(sel) == type(1) and 0 <= sel < count:
313                 count = sel
314                 new_list = list[:count]
315         if len(list) != len(new_list):
316             msg = msg + "   List reduced from %r to %r due to restriction <%r>\n" % (
317                          len(list), len(new_list), sel)
318
319         return new_list, msg
320
321     def get_print_list(self, sel_list):
322         width = self.max_name_len
323         if self.fcn_list:
324             list = self.fcn_list[:]
325             msg = "   Ordered by: " + self.sort_type + '\n'
326         else:
327             list = self.stats.keys()
328             msg = "   Random listing order was used\n"
329
330         for selection in sel_list:
331             list, msg = self.eval_print_amount(selection, list, msg)
332
333         count = len(list)
334
335         if not list:
336             return 0, list
337         print >> self.stream, msg
338         if count < len(self.stats):
339             width = 0
340             for func in list:
341                 if  len(func_std_string(func)) > width:
342                     width = len(func_std_string(func))
343         return width+2, list
344
345     def print_stats(self, *amount):
346         for filename in self.files:
347             print >> self.stream, filename
348         if self.files: print >> self.stream
349         indent = ' ' * 8
350         for func in self.top_level:
351             print >> self.stream, indent, func_get_function_name(func)
352
353         print >> self.stream, indent, self.total_calls, "function calls",
354         if self.total_calls != self.prim_calls:
355             print >> self.stream, "(%d primitive calls)" % self.prim_calls,
356         print >> self.stream, "in %.3f CPU seconds" % self.total_tt
357         print >> self.stream
358         width, list = self.get_print_list(amount)
359         if list:
360             self.print_title()
361             for func in list:
362                 self.print_line(func)
363             print >> self.stream
364             print >> self.stream
365         return self
366
367     def print_callees(self, *amount):
368         width, list = self.get_print_list(amount)
369         if list:
370             self.calc_callees()
371
372             self.print_call_heading(width, "called...")
373             for func in list:
374                 if func in self.all_callees:
375                     self.print_call_line(width, func, self.all_callees[func])
376                 else:
377                     self.print_call_line(width, func, {})
378             print >> self.stream
379             print >> self.stream
380         return self
381
382     def print_callers(self, *amount):
383         width, list = self.get_print_list(amount)
384         if list:
385             self.print_call_heading(width, "was called by...")
386             for func in list:
387                 cc, nc, tt, ct, callers = self.stats[func]
388                 self.print_call_line(width, func, callers, "<-")
389             print >> self.stream
390             print >> self.stream
391         return self
392
393     def print_call_heading(self, name_size, column_title):
394         print >> self.stream, "Function ".ljust(name_size) + column_title
395         # print sub-header only if we have new-style callers
396         subheader = False
397         for cc, nc, tt, ct, callers in self.stats.itervalues():
398             if callers:
399                 value = callers.itervalues().next()
400                 subheader = isinstance(value, tuple)
401                 break
402         if subheader:
403             print >> self.stream, " "*name_size + "    ncalls  tottime  cumtime"
404
405     def print_call_line(self, name_size, source, call_dict, arrow="->"):
406         print >> self.stream, func_std_string(source).ljust(name_size) + arrow,
407         if not call_dict:
408             print >> self.stream
409             return
410         clist = call_dict.keys()
411         clist.sort()
412         indent = ""
413         for func in clist:
414             name = func_std_string(func)
415             value = call_dict[func]
416             if isinstance(value, tuple):
417                 nc, cc, tt, ct = value
418                 if nc != cc:
419                     substats = '%d/%d' % (nc, cc)
420                 else:
421                     substats = '%d' % (nc,)
422                 substats = '%s %s %s  %s' % (substats.rjust(7+2*len(indent)),
423                                              f8(tt), f8(ct), name)
424                 left_width = name_size + 1
425             else:
426                 substats = '%s(%r) %s' % (name, value, f8(self.stats[func][3]))
427                 left_width = name_size + 3
428             print >> self.stream, indent*left_width + substats
429             indent = " "
430
431     def print_title(self):
432         print >> self.stream, '   ncalls  tottime  percall  cumtime  percall',
433         print >> self.stream, 'filename:lineno(function)'
434
435     def print_line(self, func):  # hack : should print percentages
436         cc, nc, tt, ct, callers = self.stats[func]
437         c = str(nc)
438         if nc != cc:
439             c = c + '/' + str(cc)
440         print >> self.stream, c.rjust(9),
441         print >> self.stream, f8(tt),
442         if nc == 0:
443             print >> self.stream, ' '*8,
444         else:
445             print >> self.stream, f8(tt/nc),
446         print >> self.stream, f8(ct),
447         if cc == 0:
448             print >> self.stream, ' '*8,
449         else:
450             print >> self.stream, f8(ct/cc),
451         print >> self.stream, func_std_string(func)
452
453 class TupleComp:
454     """This class provides a generic function for comparing any two tuples.
455     Each instance records a list of tuple-indices (from most significant
456     to least significant), and sort direction (ascending or decending) for
457     each tuple-index.  The compare functions can then be used as the function
458     argument to the system sort() function when a list of tuples need to be
459     sorted in the instances order."""
460
461     def __init__(self, comp_select_list):
462         self.comp_select_list = comp_select_list
463
464     def compare (self, left, right):
465         for index, direction in self.comp_select_list:
466             l = left[index]
467             r = right[index]
468             if l < r:
469                 return -direction
470             if l > r:
471                 return direction
472         return 0
473
474 #**************************************************************************
475 # func_name is a triple (file:string, line:int, name:string)
476
477 def func_strip_path(func_name):
478     filename, line, name = func_name
479     return os.path.basename(filename), line, name
480
481 def func_get_function_name(func):
482     return func[2]
483
484 def func_std_string(func_name): # match what old profile produced
485     if func_name[:2] == ('~', 0):
486         # special case for built-in functions
487         name = func_name[2]
488         if name.startswith('<') and name.endswith('>'):
489             return '{%s}' % name[1:-1]
490         else:
491             return name
492     else:
493         return "%s:%d(%s)" % func_name
494
495 #**************************************************************************
496 # The following functions combine statists for pairs functions.
497 # The bulk of the processing involves correctly handling "call" lists,
498 # such as callers and callees.
499 #**************************************************************************
500
501 def add_func_stats(target, source):
502     """Add together all the stats for two profile entries."""
503     cc, nc, tt, ct, callers = source
504     t_cc, t_nc, t_tt, t_ct, t_callers = target
505     return (cc+t_cc, nc+t_nc, tt+t_tt, ct+t_ct,
506               add_callers(t_callers, callers))
507
508 def add_callers(target, source):
509     """Combine two caller lists in a single list."""
510     new_callers = {}
511     for func, caller in target.iteritems():
512         new_callers[func] = caller
513     for func, caller in source.iteritems():
514         if func in new_callers:
515             new_callers[func] = caller + new_callers[func]
516         else:
517             new_callers[func] = caller
518     return new_callers
519
520 def count_calls(callers):
521     """Sum the caller statistics to get total number of calls received."""
522     nc = 0
523     for calls in callers.itervalues():
524         nc += calls
525     return nc
526
527 #**************************************************************************
528 # The following functions support printing of reports
529 #**************************************************************************
530
531 def f8(x):
532     return "%8.3f" % x
533
534 #**************************************************************************
535 # Statistics browser added by ESR, April 2001
536 #**************************************************************************
537
538 if __name__ == '__main__':
539     import cmd
540     try:
541         import readline
542     except ImportError:
543         pass
544
545     class ProfileBrowser(cmd.Cmd):
546         def __init__(self, profile=None):
547             cmd.Cmd.__init__(self)
548             self.prompt = "% "
549             if profile is not None:
550                 self.stats = Stats(profile)
551                 self.stream = self.stats.stream
552             else:
553                 self.stats = None
554                 self.stream = sys.stdout
555
556         def generic(self, fn, line):
557             args = line.split()
558             processed = []
559             for term in args:
560                 try:
561                     processed.append(int(term))
562                     continue
563                 except ValueError:
564                     pass
565                 try:
566                     frac = float(term)
567                     if frac > 1 or frac < 0:
568                         print >> self.stream, "Fraction argument must be in [0, 1]"
569                         continue
570                     processed.append(frac)
571                     continue
572                 except ValueError:
573                     pass
574                 processed.append(term)
575             if self.stats:
576                 getattr(self.stats, fn)(*processed)
577             else:
578                 print >> self.stream, "No statistics object is loaded."
579             return 0
580         def generic_help(self):
581             print >> self.stream, "Arguments may be:"
582             print >> self.stream, "* An integer maximum number of entries to print."
583             print >> self.stream, "* A decimal fractional number between 0 and 1, controlling"
584             print >> self.stream, "  what fraction of selected entries to print."
585             print >> self.stream, "* A regular expression; only entries with function names"
586             print >> self.stream, "  that match it are printed."
587
588         def do_add(self, line):
589             self.stats.add(line)
590             return 0
591         def help_add(self):
592             print >> self.stream, "Add profile info from given file to current statistics object."
593
594         def do_callees(self, line):
595             return self.generic('print_callees', line)
596         def help_callees(self):
597             print >> self.stream, "Print callees statistics from the current stat object."
598             self.generic_help()
599
600         def do_callers(self, line):
601             return self.generic('print_callers', line)
602         def help_callers(self):
603             print >> self.stream, "Print callers statistics from the current stat object."
604             self.generic_help()
605
606         def do_EOF(self, line):
607             print >> self.stream, ""
608             return 1
609         def help_EOF(self):
610             print >> self.stream, "Leave the profile brower."
611
612         def do_quit(self, line):
613             return 1
614         def help_quit(self):
615             print >> self.stream, "Leave the profile brower."
616
617         def do_read(self, line):
618             if line:
619                 try:
620                     self.stats = Stats(line)
621                 except IOError, args:
622                     print >> self.stream, args[1]
623                     return
624                 self.prompt = line + "% "
625             elif len(self.prompt) > 2:
626                 line = self.prompt[-2:]
627             else:
628                 print >> self.stream, "No statistics object is current -- cannot reload."
629             return 0
630         def help_read(self):
631             print >> self.stream, "Read in profile data from a specified file."
632
633         def do_reverse(self, line):
634             self.stats.reverse_order()
635             return 0
636         def help_reverse(self):
637             print >> self.stream, "Reverse the sort order of the profiling report."
638
639         def do_sort(self, line):
640             abbrevs = self.stats.get_sort_arg_defs()
641             if line and not filter(lambda x,a=abbrevs: x not in a,line.split()):
642                 self.stats.sort_stats(*line.split())
643             else:
644                 print >> self.stream, "Valid sort keys (unique prefixes are accepted):"
645                 for (key, value) in Stats.sort_arg_dict_default.iteritems():
646                     print >> self.stream, "%s -- %s" % (key, value[1])
647             return 0
648         def help_sort(self):
649             print >> self.stream, "Sort profile data according to specified keys."
650             print >> self.stream, "(Typing `sort' without arguments lists valid keys.)"
651         def complete_sort(self, text, *args):
652             return [a for a in Stats.sort_arg_dict_default if a.startswith(text)]
653
654         def do_stats(self, line):
655             return self.generic('print_stats', line)
656         def help_stats(self):
657             print >> self.stream, "Print statistics from the current stat object."
658             self.generic_help()
659
660         def do_strip(self, line):
661             self.stats.strip_dirs()
662             return 0
663         def help_strip(self):
664             print >> self.stream, "Strip leading path information from filenames in the report."
665
666         def postcmd(self, stop, line):
667             if stop:
668                 return stop
669             return None
670
671     import sys
672     if len(sys.argv) > 1:
673         initprofile = sys.argv[1]
674     else:
675         initprofile = None
676     try:
677         browser = ProfileBrowser(initprofile)
678         print >> browser.stream, "Welcome to the profile statistics browser."
679         browser.cmdloop()
680         print >> browser.stream, "Goodbye."
681     except KeyboardInterrupt:
682         pass
683
684 # That's all, folks.