Benjamin Peterson | 90f5ba5 | 2010-03-11 22:53:45 +0000 | [diff] [blame] | 1 | #! /usr/bin/env python3 |
Armin Rigo | a871ef2 | 2006-02-08 12:53:56 +0000 | [diff] [blame] | 2 | |
| 3 | """Python interface for the 'lsprof' profiler. |
| 4 | Compatible with the 'profile' module. |
| 5 | """ |
| 6 | |
Georg Brandl | b6b1324 | 2009-09-04 17:15:16 +0000 | [diff] [blame] | 7 | __all__ = ["run", "runctx", "Profile"] |
Armin Rigo | a871ef2 | 2006-02-08 12:53:56 +0000 | [diff] [blame] | 8 | |
| 9 | import _lsprof |
Giampaolo Rodola' | fca677a | 2013-02-25 11:36:40 +0100 | [diff] [blame] | 10 | import profile as _pyprofile |
Armin Rigo | a871ef2 | 2006-02-08 12:53:56 +0000 | [diff] [blame] | 11 | |
| 12 | # ____________________________________________________________ |
| 13 | # Simple interface |
| 14 | |
| 15 | def run(statement, filename=None, sort=-1): |
Giampaolo Rodola' | fca677a | 2013-02-25 11:36:40 +0100 | [diff] [blame] | 16 | return _pyprofile._Utils(Profile).run(statement, filename, sort) |
Armin Rigo | a871ef2 | 2006-02-08 12:53:56 +0000 | [diff] [blame] | 17 | |
Georg Brandl | 8e43fbf | 2010-08-02 12:20:23 +0000 | [diff] [blame] | 18 | def runctx(statement, globals, locals, filename=None, sort=-1): |
Giampaolo Rodola' | fca677a | 2013-02-25 11:36:40 +0100 | [diff] [blame] | 19 | return _pyprofile._Utils(Profile).runctx(statement, globals, locals, |
| 20 | filename, sort) |
Armin Rigo | a871ef2 | 2006-02-08 12:53:56 +0000 | [diff] [blame] | 21 | |
Giampaolo Rodola' | fca677a | 2013-02-25 11:36:40 +0100 | [diff] [blame] | 22 | run.__doc__ = _pyprofile.run.__doc__ |
| 23 | runctx.__doc__ = _pyprofile.runctx.__doc__ |
Armin Rigo | a871ef2 | 2006-02-08 12:53:56 +0000 | [diff] [blame] | 24 | |
Armin Rigo | a871ef2 | 2006-02-08 12:53:56 +0000 | [diff] [blame] | 25 | # ____________________________________________________________ |
| 26 | |
| 27 | class Profile(_lsprof.Profiler): |
INADA Naoki | 2ebd381 | 2018-08-03 18:09:57 +0900 | [diff] [blame] | 28 | """Profile(timer=None, timeunit=None, subcalls=True, builtins=True) |
Armin Rigo | a871ef2 | 2006-02-08 12:53:56 +0000 | [diff] [blame] | 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. |
INADA Naoki | 2ebd381 | 2018-08-03 18:09:57 +0900 | [diff] [blame] | 32 | For custom timer functions returning integers, timeunit can |
Armin Rigo | a871ef2 | 2006-02-08 12:53:56 +0000 | [diff] [blame] | 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 |
Giampaolo Rodola' | 2f50aaf | 2013-02-12 02:04:27 +0100 | [diff] [blame] | 46 | with open(file, 'wb') as f: |
| 47 | self.create_stats() |
| 48 | marshal.dump(self.stats, f) |
Armin Rigo | a871ef2 | 2006-02-08 12:53:56 +0000 | [diff] [blame] | 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: |
Georg Brandl | 7cae87c | 2006-09-06 06:51:57 +0000 | [diff] [blame] | 100 | exec(cmd, globals, locals) |
Armin Rigo | a871ef2 | 2006-02-08 12:53:56 +0000 | [diff] [blame] | 101 | finally: |
| 102 | self.disable() |
| 103 | return self |
| 104 | |
| 105 | # This method is more useful to profile a single function call. |
Serhiy Storchaka | 142566c | 2019-06-05 18:22:31 +0300 | [diff] [blame] | 106 | def runcall(self, func, /, *args, **kw): |
Armin Rigo | a871ef2 | 2006-02-08 12:53:56 +0000 | [diff] [blame] | 107 | self.enable() |
| 108 | try: |
| 109 | return func(*args, **kw) |
| 110 | finally: |
| 111 | self.disable() |
| 112 | |
Scott Sanderson | 2e01b75 | 2018-06-01 16:36:23 -0400 | [diff] [blame] | 113 | def __enter__(self): |
| 114 | self.enable() |
| 115 | return self |
| 116 | |
| 117 | def __exit__(self, *exc_info): |
| 118 | self.disable() |
| 119 | |
Armin Rigo | a871ef2 | 2006-02-08 12:53:56 +0000 | [diff] [blame] | 120 | # ____________________________________________________________ |
| 121 | |
| 122 | def label(code): |
Guido van Rossum | 3172c5d | 2007-10-16 18:12:55 +0000 | [diff] [blame] | 123 | if isinstance(code, str): |
Armin Rigo | a871ef2 | 2006-02-08 12:53:56 +0000 | [diff] [blame] | 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 | |
| 130 | def main(): |
Sanyam Khurana | 7973e27 | 2017-11-08 16:20:56 +0530 | [diff] [blame] | 131 | import os |
| 132 | import sys |
| 133 | import runpy |
Stéphane Wirtel | fcd5e84 | 2018-10-17 12:03:40 +0200 | [diff] [blame] | 134 | import pstats |
Armin Rigo | a871ef2 | 2006-02-08 12:53:56 +0000 | [diff] [blame] | 135 | from optparse import OptionParser |
Sanyam Khurana | 7973e27 | 2017-11-08 16:20:56 +0530 | [diff] [blame] | 136 | usage = "cProfile.py [-o output_file_path] [-s sort] [-m module | scriptfile] [arg] ..." |
Armin Rigo | a871ef2 | 2006-02-08 12:53:56 +0000 | [diff] [blame] | 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", |
Georg Brandl | 8e43fbf | 2010-08-02 12:20:23 +0000 | [diff] [blame] | 142 | help="Sort order when printing to stdout, based on pstats.Stats class", |
Stéphane Wirtel | fcd5e84 | 2018-10-17 12:03:40 +0200 | [diff] [blame] | 143 | default=-1, |
| 144 | choices=sorted(pstats.Stats.sort_arg_dict_default)) |
Sanyam Khurana | 7973e27 | 2017-11-08 16:20:56 +0530 | [diff] [blame] | 145 | parser.add_option('-m', dest="module", action="store_true", |
| 146 | help="Profile a library module", default=False) |
Armin Rigo | a871ef2 | 2006-02-08 12:53:56 +0000 | [diff] [blame] | 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 | |
Anthony Sottile | 3c0ac18 | 2020-10-18 13:48:31 -0700 | [diff] [blame] | 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 | |
Georg Brandl | 8e43fbf | 2010-08-02 12:20:23 +0000 | [diff] [blame] | 160 | if len(args) > 0: |
Sanyam Khurana | 7973e27 | 2017-11-08 16:20:56 +0530 | [diff] [blame] | 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 | } |
Georg Brandl | 8e43fbf | 2010-08-02 12:20:23 +0000 | [diff] [blame] | 178 | runctx(code, globs, None, options.outfile, options.sort) |
Armin Rigo | a871ef2 | 2006-02-08 12:53:56 +0000 | [diff] [blame] | 179 | else: |
| 180 | parser.print_usage() |
| 181 | return parser |
| 182 | |
| 183 | # When invoked as main program, invoke the profiler on a script |
| 184 | if __name__ == '__main__': |
| 185 | main() |