Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes14kdownloads
pstats.py778 linesDownload Raw Back to Lib
1"""Class for printing reports on profiled python code."""2 3# Written by James Roskind4# Based on prior profile module by Sjoerd Mullender...5#   which was hacked somewhat by: Guido van Rossum6 7# Copyright Disney Enterprises, Inc.  All Rights Reserved.8# Licensed to PSF under a Contributor Agreement9#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 at13#14# http://www.apache.org/licenses/LICENSE-2.015#16# Unless required by applicable law or agreed to in writing, software17# 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 language20# governing permissions and limitations under the License.21 22 23import sys24import os25import time26import marshal27import re28 29from enum import StrEnum, _simple_enum30from functools import cmp_to_key31from dataclasses import dataclass32 33__all__ = ["Stats", "SortKey", "FunctionProfile", "StatsProfile"]34 35@_simple_enum(StrEnum)36class SortKey: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_ = value51        for other_value in values[1:]:52            cls._value2member_map_[other_value] = obj53        obj._all_values = values54        return obj55 56 57@dataclass(unsafe_hash=True)58class FunctionProfile:59    ncalls: str60    tottime: float61    percall_tottime: float62    cumtime: float63    percall_cumtime: float64    file_name: str65    line_number: int66 67@dataclass(unsafe_hash=True)68class StatsProfile:69    '''Class for keeping track of an item in inventory.'''70    total_tt: float71    func_profiles: dict[str, FunctionProfile]72 73class Stats:74    """This class is used for creating reports from data generated by the75    Profile class.  It is a "friend" of that class, and imports data either76    by direct access to members of Profile class, or by reading in a dictionary77    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 from81    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 lines85    to print.  If the arg is a floating-point number between 0 and 1.0, then86    it is taken as a decimal percentage of the available lines to be printed87    (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 have89    printed.90 91    The sort_stats() method now processes some additional options (i.e., in92    addition to the old -1, 0, 1, or 2 that are respectively interpreted as93    'stdname', 'calls', 'time', and 'cumulative').  It takes either an94    arbitrary number of quoted strings or SortKey enum to select the sort95    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 on99    the minor key of 'the name of the function'.  Look at the two tables in100    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.stdout109        if not len(args):110            arg = None111        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 needed119        self.files = []120        self.fcn_list = None121        self.total_tt = 0122        self.total_calls = 0123        self.prim_calls = 0124        self.max_name_len = 0125        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            raise135 136    def load_stats(self, arg):137        if arg is None:138            self.stats = {}139            return140        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) + "    " + arg146            except:  # in case this is not unix147                pass148            self.files = [arg]149        elif hasattr(arg, 'create_stats'):150            arg.create_stats()151            self.stats = arg.stats152            arg.stats = {}153        if not self.stats:154            raise TypeError("Cannot create or construct a %r object from %r"155                            % (self.__class__, arg))156        return157 158    def get_top_level_stats(self):159        for func, (cc, nc, tt, ct, callers) in self.stats.items():160            self.total_calls += nc161            self.prim_calls  += cc162            self.total_tt    += tt163            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 self171        for item in reversed(arg_list):172            if type(self) != type(item):173                item = Stats(item)174            self.files += item.files175            self.total_calls += item.total_calls176            self.prim_calls += item.prim_calls177            self.total_tt += item.total_tt178            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_len183 184            self.fcn_list = None185 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 self193 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 description201    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 = word224                while fragment:225                    if fragment in dict:226                        bad_list[fragment] = 0227                        break228                    dict[fragment] = tup229                    fragment = fragment[:-1]230            for word in bad_list:231                del dict[word]232        return self.sort_arg_dict233 234    def sort_stats(self, *field):235        if not field:236            self.fcn_list = 0237            return self238        if len(field) == 1 and isinstance(field[0], int):239            # Be compatible with old profiler240            field = [ {-1: "stdname",241                       0:  "calls",242                       1:  "time",243                       2:  "cumulative"}[field[0]] ]244        elif len(field) >= 2:245            for arg in field[1:]:246                if type(arg) != type(field[0]):247                    raise TypeError("Can't have mixed argument type")248 249        sort_arg_defs = self.get_sort_arg_defs()250 251        sort_tuple = ()252        self.sort_type = ""253        connector = ""254        for word in field:255            if isinstance(word, SortKey):256                word = word.value257            sort_tuple = sort_tuple + sort_arg_defs[word][0]258            self.sort_type += connector + sort_arg_defs[word][1]259            connector = ", "260 261        stats_list = []262        for func, (cc, nc, tt, ct, callers) in self.stats.items():263            stats_list.append((cc, nc, tt, ct) + func +264                              (func_std_string(func), func))265 266        stats_list.sort(key=cmp_to_key(TupleComp(sort_tuple).compare))267 268        self.fcn_list = fcn_list = []269        for tuple in stats_list:270            fcn_list.append(tuple[-1])271        return self272 273    def reverse_order(self):274        if self.fcn_list:275            self.fcn_list.reverse()276        return self277 278    def strip_dirs(self):279        oldstats = self.stats280        self.stats = newstats = {}281        max_name_len = 0282        for func, (cc, nc, tt, ct, callers) in oldstats.items():283            newfunc = func_strip_path(func)284            if len(func_std_string(newfunc)) > max_name_len:285                max_name_len = len(func_std_string(newfunc))286            newcallers = {}287            for func2, caller in callers.items():288                newcallers[func_strip_path(func2)] = caller289 290            if newfunc in newstats:291                newstats[newfunc] = add_func_stats(292                                        newstats[newfunc],293                                        (cc, nc, tt, ct, newcallers))294            else:295                newstats[newfunc] = (cc, nc, tt, ct, newcallers)296        old_top = self.top_level297        self.top_level = new_top = set()298        for func in old_top:299            new_top.add(func_strip_path(func))300 301        self.max_name_len = max_name_len302 303        self.fcn_list = None304        self.all_callees = None305        return self306 307    def calc_callees(self):308        if self.all_callees:309            return310        self.all_callees = all_callees = {}311        for func, (cc, nc, tt, ct, callers) in self.stats.items():312            if not func in all_callees:313                all_callees[func] = {}314            for func2, caller in callers.items():315                if not func2 in all_callees:316                    all_callees[func2] = {}317                all_callees[func2][func]  = caller318        return319 320    #******************************************************************321    # The following functions support actual printing of reports322    #******************************************************************323 324    # Optional "amount" is either a line count, or a percentage of lines.325 326    def eval_print_amount(self, sel, list, msg):327        new_list = list328        if isinstance(sel, str):329            try:330                rex = re.compile(sel)331            except re.PatternError:332                msg += "   <Invalid regular expression %r>\n" % sel333                return new_list, msg334            new_list = []335            for func in list:336                if rex.search(func_std_string(func)):337                    new_list.append(func)338        else:339            count = len(list)340            if isinstance(sel, float) and 0.0 <= sel < 1.0:341                count = int(count * sel + .5)342                new_list = list[:count]343            elif isinstance(sel, int) and 0 <= sel < count:344                count = sel345                new_list = list[:count]346        if len(list) != len(new_list):347            msg += "   List reduced from %r to %r due to restriction <%r>\n" % (348                len(list), len(new_list), sel)349 350        return new_list, msg351 352    def get_stats_profile(self):353        """This method returns an instance of StatsProfile, which contains a mapping354        of function names to instances of FunctionProfile. Each FunctionProfile355        instance holds information related to the function's profile such as how356        long the function took to run, how many times it was called, etc...357        """358        func_list = self.fcn_list[:] if self.fcn_list else list(self.stats.keys())359        if not func_list:360            return StatsProfile(0, {})361 362        total_tt = float(f8(self.total_tt))363        func_profiles = {}364        stats_profile = StatsProfile(total_tt, func_profiles)365 366        for func in func_list:367            cc, nc, tt, ct, callers = self.stats[func]368            file_name, line_number, func_name = func369            ncalls = str(nc) if nc == cc else (str(nc) + '/' + str(cc))370            tottime = float(f8(tt))371            percall_tottime = -1 if nc == 0 else float(f8(tt/nc))372            cumtime = float(f8(ct))373            percall_cumtime = -1 if cc == 0 else float(f8(ct/cc))374            func_profile = FunctionProfile(375                ncalls,376                tottime, # time spent in this function alone377                percall_tottime,378                cumtime, # time spent in the function plus all functions that this function called,379                percall_cumtime,380                file_name,381                line_number382            )383            func_profiles[func_name] = func_profile384 385        return stats_profile386 387    def get_print_list(self, sel_list):388        width = self.max_name_len389        if self.fcn_list:390            stat_list = self.fcn_list[:]391            msg = "   Ordered by: " + self.sort_type + '\n'392        else:393            stat_list = list(self.stats.keys())394            msg = "   Random listing order was used\n"395 396        for selection in sel_list:397            stat_list, msg = self.eval_print_amount(selection, stat_list, msg)398 399        count = len(stat_list)400 401        if not stat_list:402            return 0, stat_list403        print(msg, file=self.stream)404        if count < len(self.stats):405            width = 0406            for func in stat_list:407                if  len(func_std_string(func)) > width:408                    width = len(func_std_string(func))409        return width+2, stat_list410 411    def print_stats(self, *amount):412        for filename in self.files:413            print(filename, file=self.stream)414        if self.files:415            print(file=self.stream)416        indent = ' ' * 8417        for func in self.top_level:418            print(indent, func_get_function_name(func), file=self.stream)419 420        print(indent, self.total_calls, "function calls", end=' ', file=self.stream)421        if self.total_calls != self.prim_calls:422            print("(%d primitive calls)" % self.prim_calls, end=' ', file=self.stream)423        print("in %.3f seconds" % self.total_tt, file=self.stream)424        print(file=self.stream)425        width, list = self.get_print_list(amount)426        if list:427            self.print_title()428            for func in list:429                self.print_line(func)430            print(file=self.stream)431            print(file=self.stream)432        return self433 434    def print_callees(self, *amount):435        width, list = self.get_print_list(amount)436        if list:437            self.calc_callees()438 439            self.print_call_heading(width, "called...")440            for func in list:441                if func in self.all_callees:442                    self.print_call_line(width, func, self.all_callees[func])443                else:444                    self.print_call_line(width, func, {})445            print(file=self.stream)446            print(file=self.stream)447        return self448 449    def print_callers(self, *amount):450        width, list = self.get_print_list(amount)451        if list:452            self.print_call_heading(width, "was called by...")453            for func in list:454                cc, nc, tt, ct, callers = self.stats[func]455                self.print_call_line(width, func, callers, "<-")456            print(file=self.stream)457            print(file=self.stream)458        return self459 460    def print_call_heading(self, name_size, column_title):461        print("Function ".ljust(name_size) + column_title, file=self.stream)462        # print sub-header only if we have new-style callers463        subheader = False464        for cc, nc, tt, ct, callers in self.stats.values():465            if callers:466                value = next(iter(callers.values()))467                subheader = isinstance(value, tuple)468                break469        if subheader:470            print(" "*name_size + "    ncalls  tottime  cumtime", file=self.stream)471 472    def print_call_line(self, name_size, source, call_dict, arrow="->"):473        print(func_std_string(source).ljust(name_size) + arrow, end=' ', file=self.stream)474        if not call_dict:475            print(file=self.stream)476            return477        clist = sorted(call_dict.keys())478        indent = ""479        for func in clist:480            name = func_std_string(func)481            value = call_dict[func]482            if isinstance(value, tuple):483                nc, cc, tt, ct = value484                if nc != cc:485                    substats = '%d/%d' % (nc, cc)486                else:487                    substats = '%d' % (nc,)488                substats = '%s %s %s  %s' % (substats.rjust(7+2*len(indent)),489                                             f8(tt), f8(ct), name)490                left_width = name_size + 1491            else:492                substats = '%s(%r) %s' % (name, value, f8(self.stats[func][3]))493                left_width = name_size + 3494            print(indent*left_width + substats, file=self.stream)495            indent = " "496 497    def print_title(self):498        print('   ncalls  tottime  percall  cumtime  percall', end=' ', file=self.stream)499        print('filename:lineno(function)', file=self.stream)500 501    def print_line(self, func):  # hack: should print percentages502        cc, nc, tt, ct, callers = self.stats[func]503        c = str(nc)504        if nc != cc:505            c = c + '/' + str(cc)506        print(c.rjust(9), end=' ', file=self.stream)507        print(f8(tt), end=' ', file=self.stream)508        if nc == 0:509            print(' '*8, end=' ', file=self.stream)510        else:511            print(f8(tt/nc), end=' ', file=self.stream)512        print(f8(ct), end=' ', file=self.stream)513        if cc == 0:514            print(' '*8, end=' ', file=self.stream)515        else:516            print(f8(ct/cc), end=' ', file=self.stream)517        print(func_std_string(func), file=self.stream)518 519class TupleComp:520    """This class provides a generic function for comparing any two tuples.521    Each instance records a list of tuple-indices (from most significant522    to least significant), and sort direction (ascending or descending) for523    each tuple-index.  The compare functions can then be used as the function524    argument to the system sort() function when a list of tuples need to be525    sorted in the instances order."""526 527    def __init__(self, comp_select_list):528        self.comp_select_list = comp_select_list529 530    def compare (self, left, right):531        for index, direction in self.comp_select_list:532            l = left[index]533            r = right[index]534            if l < r:535                return -direction536            if l > r:537                return direction538        return 0539 540 541#**************************************************************************542# func_name is a triple (file:string, line:int, name:string)543 544def func_strip_path(func_name):545    filename, line, name = func_name546    return os.path.basename(filename), line, name547 548def func_get_function_name(func):549    return func[2]550 551def func_std_string(func_name): # match what old profile produced552    if func_name[:2] == ('~', 0):553        # special case for built-in functions554        name = func_name[2]555        if name.startswith('<') and name.endswith('>'):556            return '{%s}' % name[1:-1]557        else:558            return name559    else:560        return "%s:%d(%s)" % func_name561 562#**************************************************************************563# The following functions combine statistics for pairs functions.564# The bulk of the processing involves correctly handling "call" lists,565# such as callers and callees.566#**************************************************************************567 568def add_func_stats(target, source):569    """Add together all the stats for two profile entries."""570    cc, nc, tt, ct, callers = source571    t_cc, t_nc, t_tt, t_ct, t_callers = target572    return (cc+t_cc, nc+t_nc, tt+t_tt, ct+t_ct,573              add_callers(t_callers, callers))574 575def add_callers(target, source):576    """Combine two caller lists in a single list."""577    new_callers = {}578    for func, caller in target.items():579        new_callers[func] = caller580    for func, caller in source.items():581        if func in new_callers:582            if isinstance(caller, tuple):583                # format used by cProfile584                new_callers[func] = tuple(i + j for i, j in zip(caller, new_callers[func]))585            else:586                # format used by profile587                new_callers[func] += caller588        else:589            new_callers[func] = caller590    return new_callers591 592def count_calls(callers):593    """Sum the caller statistics to get total number of calls received."""594    nc = 0595    for calls in callers.values():596        nc += calls597    return nc598 599#**************************************************************************600# The following functions support printing of reports601#**************************************************************************602 603def f8(x):604    return "%8.3f" % x605 606#**************************************************************************607# Statistics browser added by ESR, April 2001608#**************************************************************************609 610if __name__ == '__main__':611    import cmd612    try:613        import readline  # noqa: F401614    except ImportError:615        pass616 617    class ProfileBrowser(cmd.Cmd):618        def __init__(self, profile=None):619            cmd.Cmd.__init__(self)620            self.prompt = "% "621            self.stats = None622            self.stream = sys.stdout623            if profile is not None:624                self.do_read(profile)625 626        def generic(self, fn, line):627            args = line.split()628            processed = []629            for term in args:630                try:631                    processed.append(int(term))632                    continue633                except ValueError:634                    pass635                try:636                    frac = float(term)637                    if frac > 1 or frac < 0:638                        print("Fraction argument must be in [0, 1]", file=self.stream)639                        continue640                    processed.append(frac)641                    continue642                except ValueError:643                    pass644                processed.append(term)645            if self.stats:646                getattr(self.stats, fn)(*processed)647            else:648                print("No statistics object is loaded.", file=self.stream)649            return 0650        def generic_help(self):651            print("Arguments may be:", file=self.stream)652            print("* An integer maximum number of entries to print.", file=self.stream)653            print("* A decimal fractional number between 0 and 1, controlling", file=self.stream)654            print("  what fraction of selected entries to print.", file=self.stream)655            print("* A regular expression; only entries with function names", file=self.stream)656            print("  that match it are printed.", file=self.stream)657 658        def do_add(self, line):659            if self.stats:660                try:661                    self.stats.add(line)662                except OSError as e:663                    print("Failed to load statistics for %s: %s" % (line, e), file=self.stream)664            else:665                print("No statistics object is loaded.", file=self.stream)666            return 0667        def help_add(self):668            print("Add profile info from given file to current statistics object.", file=self.stream)669 670        def do_callees(self, line):671            return self.generic('print_callees', line)672        def help_callees(self):673            print("Print callees statistics from the current stat object.", file=self.stream)674            self.generic_help()675 676        def do_callers(self, line):677            return self.generic('print_callers', line)678        def help_callers(self):679            print("Print callers statistics from the current stat object.", file=self.stream)680            self.generic_help()681 682        def do_EOF(self, line):683            print("", file=self.stream)684            return 1685        def help_EOF(self):686            print("Leave the profile browser.", file=self.stream)687 688        def do_quit(self, line):689            return 1690        def help_quit(self):691            print("Leave the profile browser.", file=self.stream)692 693        def do_read(self, line):694            if line:695                try:696                    self.stats = Stats(line)697                except OSError as err:698                    print(err.args[1], file=self.stream)699                    return700                except Exception as err:701                    print(err.__class__.__name__ + ':', err, file=self.stream)702                    return703                self.prompt = line + "% "704            elif len(self.prompt) > 2:705                line = self.prompt[:-2]706                self.do_read(line)707            else:708                print("No statistics object is current -- cannot reload.", file=self.stream)709            return 0710        def help_read(self):711            print("Read in profile data from a specified file.", file=self.stream)712            print("Without argument, reload the current file.", file=self.stream)713 714        def do_reverse(self, line):715            if self.stats:716                self.stats.reverse_order()717            else:718                print("No statistics object is loaded.", file=self.stream)719            return 0720        def help_reverse(self):721            print("Reverse the sort order of the profiling report.", file=self.stream)722 723        def do_sort(self, line):724            if not self.stats:725                print("No statistics object is loaded.", file=self.stream)726                return727            abbrevs = self.stats.get_sort_arg_defs()728            if line and all((x in abbrevs) for x in line.split()):729                self.stats.sort_stats(*line.split())730            else:731                print("Valid sort keys (unique prefixes are accepted):", file=self.stream)732                for (key, value) in Stats.sort_arg_dict_default.items():733                    print("%s -- %s" % (key, value[1]), file=self.stream)734            return 0735        def help_sort(self):736            print("Sort profile data according to specified keys.", file=self.stream)737            print("(Typing `sort' without arguments lists valid keys.)", file=self.stream)738        def complete_sort(self, text, *args):739            return [a for a in Stats.sort_arg_dict_default if a.startswith(text)]740 741        def do_stats(self, line):742            return self.generic('print_stats', line)743        def help_stats(self):744            print("Print statistics from the current stat object.", file=self.stream)745            self.generic_help()746 747        def do_strip(self, line):748            if self.stats:749                self.stats.strip_dirs()750            else:751                print("No statistics object is loaded.", file=self.stream)752        def help_strip(self):753            print("Strip leading path information from filenames in the report.", file=self.stream)754 755        def help_help(self):756            print("Show help for a given command.", file=self.stream)757 758        def postcmd(self, stop, line):759            if stop:760                return stop761            return None762 763    if len(sys.argv) > 1:764        initprofile = sys.argv[1]765    else:766        initprofile = None767    try:768        browser = ProfileBrowser(initprofile)769        for profile in sys.argv[2:]:770            browser.do_add(profile)771        print("Welcome to the profile statistics browser.", file=browser.stream)772        browser.cmdloop()773        print("Goodbye.", file=browser.stream)774    except KeyboardInterrupt:775        pass776 777# That's all, folks.778 
codekingpro/portable-devtools · Team Ai