1#! /usr/bin/env python3 2 3"""Python interface for the 'lsprof' profiler. 4 Compatible with the 'profile' module. 5""" 6 7__all__ = ["run", "runctx", "Profile"] 8 9import _lsprof 10import profile as _pyprofile 11 12# ____________________________________________________________ 13# Simple interface 14 15def run(statement, filename=None, sort=-1): 16 return _pyprofile._Utils(Profile).run(statement, filename, sort) 17 18def runctx(statement, globals, locals, filename=None, sort=-1): 19 return _pyprofile._Utils(Profile).runctx(statement, globals, locals, 20 filename, sort) 21 22run.__doc__ = _pyprofile.run.__doc__ 23runctx.__doc__ = _pyprofile.runctx.__doc__ 24 25# ____________________________________________________________ 26 27class Profile(_lsprof.Profiler): 28 """Profile(timer=None, timeunit=None, subcalls=True, builtins=True) 29 30 Builds a profiler object using the specified timer function. 31 The default timer is a fast built-in one based on real time. 32 For custom timer functions returning integers, timeunit can 33 be a float specifying a scale (i.e. how long each integer unit 34 is, in seconds). 35 """ 36 37 # Most of the functionality is in the base class. 38 # This subclass only adds convenient and backward-compatible methods. 39 40 def print_stats(self, sort=-1): 41 import pstats 42 pstats.Stats(self).strip_dirs().sort_stats(sort).print_stats() 43 44 def dump_stats(self, file): 45 import marshal 46 with open(file, 'wb') as f: 47 self.create_stats() 48 marshal.dump(self.stats, f) 49 50 def create_stats(self): 51 self.disable() 52 self.snapshot_stats() 53 54 def snapshot_stats(self): 55 entries = self.getstats() 56 self.stats = {} 57 callersdicts = {} 58 # call information 59 for entry in entries: 60 func = label(entry.code) 61 nc = entry.callcount # ncalls column of pstats (before '/') 62 cc = nc - entry.reccallcount # ncalls column of pstats (after '/') 63 tt = entry.inlinetime # tottime column of pstats 64 ct = entry.totaltime # cumtime column of pstats 65 callers = {} 66 callersdicts[id(entry.code)] = callers 67 self.stats[func] = cc, nc, tt, ct, callers 68 # subcall information 69 for entry in entries: 70 if entry.calls: 71 func = label(entry.code) 72 for subentry in entry.calls: 73 try: 74 callers = callersdicts[id(subentry.code)] 75 except KeyError: 76 continue 77 nc = subentry.callcount 78 cc = nc - subentry.reccallcount 79 tt = subentry.inlinetime 80 ct = subentry.totaltime 81 if func in callers: 82 prev = callers[func] 83 nc += prev[0] 84 cc += prev[1] 85 tt += prev[2] 86 ct += prev[3] 87 callers[func] = nc, cc, tt, ct 88 89 # The following two methods can be called by clients to use 90 # a profiler to profile a statement, given as a string. 91 92 def run(self, cmd): 93 import __main__ 94 dict = __main__.__dict__ 95 return self.runctx(cmd, dict, dict) 96 97 def runctx(self, cmd, globals, locals): 98 self.enable() 99 try: 100 exec(cmd, globals, locals) 101 finally: 102 self.disable() 103 return self 104 105 # This method is more useful to profile a single function call. 106 def runcall(self, func, /, *args, **kw): 107 self.enable() 108 try: 109 return func(*args, **kw) 110 finally: 111 self.disable() 112 113 def __enter__(self): 114 self.enable() 115 return self 116 117 def __exit__(self, *exc_info): 118 self.disable() 119 120# ____________________________________________________________ 121 122def label(code): 123 if isinstance(code, str): 124 return ('~', 0, code) # built-in functions ('~' sorts at the end) 125 else: 126 return (code.co_filename, code.co_firstlineno, code.co_name) 127 128# ____________________________________________________________ 129 130def main(): 131 import os 132 import sys 133 import runpy 134 import pstats 135 from optparse import OptionParser 136 usage = "cProfile.py [-o output_file_path] [-s sort] [-m module | scriptfile] [arg] ..." 137 parser = OptionParser(usage=usage) 138 parser.allow_interspersed_args = False 139 parser.add_option('-o', '--outfile', dest="outfile", 140 help="Save stats to <outfile>", default=None) 141 parser.add_option('-s', '--sort', dest="sort", 142 help="Sort order when printing to stdout, based on pstats.Stats class", 143 default=-1, 144 choices=sorted(pstats.Stats.sort_arg_dict_default)) 145 parser.add_option('-m', dest="module", action="store_true", 146 help="Profile a library module", default=False) 147 148 if not sys.argv[1:]: 149 parser.print_usage() 150 sys.exit(2) 151 152 (options, args) = parser.parse_args() 153 sys.argv[:] = args 154 155 # The script that we're profiling may chdir, so capture the absolute path 156 # to the output file at startup. 157 if options.outfile is not None: 158 options.outfile = os.path.abspath(options.outfile) 159 160 if len(args) > 0: 161 if options.module: 162 code = "run_module(modname, run_name='__main__')" 163 globs = { 164 'run_module': runpy.run_module, 165 'modname': args[0] 166 } 167 else: 168 progname = args[0] 169 sys.path.insert(0, os.path.dirname(progname)) 170 with open(progname, 'rb') as fp: 171 code = compile(fp.read(), progname, 'exec') 172 globs = { 173 '__file__': progname, 174 '__name__': '__main__', 175 '__package__': None, 176 '__cached__': None, 177 } 178 try: 179 runctx(code, globs, None, options.outfile, options.sort) 180 except BrokenPipeError as exc: 181 # Prevent "Exception ignored" during interpreter shutdown. 182 sys.stdout = None 183 sys.exit(exc.errno) 184 else: 185 parser.print_usage() 186 return parser 187 188# When invoked as main program, invoke the profiler on a script 189if __name__ == '__main__': 190 main() 191