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