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