##// END OF EJS Templates
py3: make a bytes version of getopt.getopt()...
Pulkit Goyal -
r30578:c6ce11f2 default
parent child Browse files
Show More
@@ -1,157 +1,159 b''
1 1 # fancyopts.py - better command line parsing
2 2 #
3 3 # Copyright 2005-2009 Matt Mackall <mpm@selenic.com> and others
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 from __future__ import absolute_import
9 9
10 import getopt
11
12 10 from .i18n import _
13 from . import error
11 from . import (
12 error,
13 pycompat,
14 )
14 15
15 16 # Set of flags to not apply boolean negation logic on
16 17 nevernegate = set([
17 18 # avoid --no-noninteractive
18 19 'noninteractive',
19 20 # These two flags are special because they cause hg to do one
20 21 # thing and then exit, and so aren't suitable for use in things
21 22 # like aliases anyway.
22 23 'help',
23 24 'version',
24 25 ])
25 26
26 27 def gnugetopt(args, options, longoptions):
27 28 """Parse options mostly like getopt.gnu_getopt.
28 29
29 30 This is different from getopt.gnu_getopt in that an argument of - will
30 31 become an argument of - instead of vanishing completely.
31 32 """
32 33 extraargs = []
33 34 if '--' in args:
34 35 stopindex = args.index('--')
35 36 extraargs = args[stopindex + 1:]
36 37 args = args[:stopindex]
37 opts, parseargs = getopt.getopt(args, options, longoptions)
38 opts, parseargs = pycompat.getoptb(args, options, longoptions)
38 39 args = []
39 40 while parseargs:
40 41 arg = parseargs.pop(0)
41 42 if arg and arg[0] == '-' and len(arg) > 1:
42 43 parseargs.insert(0, arg)
43 topts, newparseargs = getopt.getopt(parseargs, options, longoptions)
44 topts, newparseargs = pycompat.getoptb(parseargs,\
45 options, longoptions)
44 46 opts = opts + topts
45 47 parseargs = newparseargs
46 48 else:
47 49 args.append(arg)
48 50 args.extend(extraargs)
49 51 return opts, args
50 52
51 53
52 54 def fancyopts(args, options, state, gnu=False):
53 55 """
54 56 read args, parse options, and store options in state
55 57
56 58 each option is a tuple of:
57 59
58 60 short option or ''
59 61 long option
60 62 default value
61 63 description
62 64 option value label(optional)
63 65
64 66 option types include:
65 67
66 68 boolean or none - option sets variable in state to true
67 69 string - parameter string is stored in state
68 70 list - parameter string is added to a list
69 71 integer - parameter strings is stored as int
70 72 function - call function with parameter
71 73
72 74 non-option args are returned
73 75 """
74 76 namelist = []
75 77 shortlist = ''
76 78 argmap = {}
77 79 defmap = {}
78 80 negations = {}
79 81 alllong = set(o[1] for o in options)
80 82
81 83 for option in options:
82 84 if len(option) == 5:
83 85 short, name, default, comment, dummy = option
84 86 else:
85 87 short, name, default, comment = option
86 88 # convert opts to getopt format
87 89 oname = name
88 90 name = name.replace('-', '_')
89 91
90 92 argmap['-' + short] = argmap['--' + oname] = name
91 93 defmap[name] = default
92 94
93 95 # copy defaults to state
94 96 if isinstance(default, list):
95 97 state[name] = default[:]
96 98 elif callable(default):
97 99 state[name] = None
98 100 else:
99 101 state[name] = default
100 102
101 103 # does it take a parameter?
102 104 if not (default is None or default is True or default is False):
103 105 if short:
104 106 short += ':'
105 107 if oname:
106 108 oname += '='
107 109 elif oname not in nevernegate:
108 110 if oname.startswith('no-'):
109 111 insert = oname[3:]
110 112 else:
111 113 insert = 'no-' + oname
112 114 # backout (as a practical example) has both --commit and
113 115 # --no-commit options, so we don't want to allow the
114 116 # negations of those flags.
115 117 if insert not in alllong:
116 118 assert ('--' + oname) not in negations
117 119 negations['--' + insert] = '--' + oname
118 120 namelist.append(insert)
119 121 if short:
120 122 shortlist += short
121 123 if name:
122 124 namelist.append(oname)
123 125
124 126 # parse arguments
125 127 if gnu:
126 128 parse = gnugetopt
127 129 else:
128 parse = getopt.getopt
130 parse = pycompat.getoptb
129 131 opts, args = parse(args, shortlist, namelist)
130 132
131 133 # transfer result to state
132 134 for opt, val in opts:
133 135 boolval = True
134 136 negation = negations.get(opt, False)
135 137 if negation:
136 138 opt = negation
137 139 boolval = False
138 140 name = argmap[opt]
139 141 obj = defmap[name]
140 142 t = type(obj)
141 143 if callable(obj):
142 144 state[name] = defmap[name](val)
143 145 elif t is type(1):
144 146 try:
145 147 state[name] = int(val)
146 148 except ValueError:
147 149 raise error.Abort(_('invalid value %r for option %s, '
148 150 'expected int') % (val, opt))
149 151 elif t is type(''):
150 152 state[name] = val
151 153 elif t is type([]):
152 154 state[name].append(val)
153 155 elif t is type(None) or t is type(False):
154 156 state[name] = boolval
155 157
156 158 # return unparsed args
157 159 return args
@@ -1,235 +1,252 b''
1 1 # pycompat.py - portability shim for python 3
2 2 #
3 3 # This software may be used and distributed according to the terms of the
4 4 # GNU General Public License version 2 or any later version.
5 5
6 6 """Mercurial portability shim for python 3.
7 7
8 8 This contains aliases to hide python version-specific details from the core.
9 9 """
10 10
11 11 from __future__ import absolute_import
12 12
13 import getopt
13 14 import os
14 15 import sys
15 16
16 17 ispy3 = (sys.version_info[0] >= 3)
17 18
18 19 if not ispy3:
19 20 import cPickle as pickle
20 21 import cStringIO as io
21 22 import httplib
22 23 import Queue as _queue
23 24 import SocketServer as socketserver
24 25 import urlparse
25 26 urlunquote = urlparse.unquote
26 27 import xmlrpclib
27 28 else:
28 29 import http.client as httplib
29 30 import io
30 31 import pickle
31 32 import queue as _queue
32 33 import socketserver
33 34 import urllib.parse as urlparse
34 35 urlunquote = urlparse.unquote_to_bytes
35 36 import xmlrpc.client as xmlrpclib
36 37
37 38 if ispy3:
38 39 import builtins
39 40 import functools
40 41 fsencode = os.fsencode
41 42 fsdecode = os.fsdecode
42 43 # A bytes version of os.name.
43 44 osname = os.name.encode('ascii')
44 45 ospathsep = os.pathsep.encode('ascii')
45 46 ossep = os.sep.encode('ascii')
46 47 # os.getcwd() on Python 3 returns string, but it has os.getcwdb() which
47 48 # returns bytes.
48 49 getcwd = os.getcwdb
49 50
50 51 # TODO: .buffer might not exist if std streams were replaced; we'll need
51 52 # a silly wrapper to make a bytes stream backed by a unicode one.
52 53 stdin = sys.stdin.buffer
53 54 stdout = sys.stdout.buffer
54 55 stderr = sys.stderr.buffer
55 56
56 57 # Since Python 3 converts argv to wchar_t type by Py_DecodeLocale() on Unix,
57 58 # we can use os.fsencode() to get back bytes argv.
58 59 #
59 60 # https://hg.python.org/cpython/file/v3.5.1/Programs/python.c#l55
60 61 #
61 62 # TODO: On Windows, the native argv is wchar_t, so we'll need a different
62 63 # workaround to simulate the Python 2 (i.e. ANSI Win32 API) behavior.
63 64 sysargv = list(map(os.fsencode, sys.argv))
64 65
65 66 def sysstr(s):
66 67 """Return a keyword str to be passed to Python functions such as
67 68 getattr() and str.encode()
68 69
69 70 This never raises UnicodeDecodeError. Non-ascii characters are
70 71 considered invalid and mapped to arbitrary but unique code points
71 72 such that 'sysstr(a) != sysstr(b)' for all 'a != b'.
72 73 """
73 74 if isinstance(s, builtins.str):
74 75 return s
75 76 return s.decode(u'latin-1')
76 77
77 78 def _wrapattrfunc(f):
78 79 @functools.wraps(f)
79 80 def w(object, name, *args):
80 81 return f(object, sysstr(name), *args)
81 82 return w
82 83
83 84 # these wrappers are automagically imported by hgloader
84 85 delattr = _wrapattrfunc(builtins.delattr)
85 86 getattr = _wrapattrfunc(builtins.getattr)
86 87 hasattr = _wrapattrfunc(builtins.hasattr)
87 88 setattr = _wrapattrfunc(builtins.setattr)
88 89 xrange = builtins.range
89 90
91 # getopt.getopt() on Python 3 deals with unicodes internally so we cannot
92 # pass bytes there. Passing unicodes will result in unicodes as return
93 # values which we need to convert again to bytes.
94 def getoptb(args, shortlist, namelist):
95 args = [a.decode('latin-1') for a in args]
96 shortlist = shortlist.decode('latin-1')
97 namelist = [a.decode('latin-1') for a in namelist]
98 opts, args = getopt.getopt(args, shortlist, namelist)
99 opts = [(a[0].encode('latin-1'), a[1].encode('latin-1'))
100 for a in opts]
101 args = [a.encode('latin-1') for a in args]
102 return opts, args
103
90 104 else:
91 105 def sysstr(s):
92 106 return s
93 107
94 108 # Partial backport from os.py in Python 3, which only accepts bytes.
95 109 # In Python 2, our paths should only ever be bytes, a unicode path
96 110 # indicates a bug.
97 111 def fsencode(filename):
98 112 if isinstance(filename, str):
99 113 return filename
100 114 else:
101 115 raise TypeError(
102 116 "expect str, not %s" % type(filename).__name__)
103 117
104 118 # In Python 2, fsdecode() has a very chance to receive bytes. So it's
105 119 # better not to touch Python 2 part as it's already working fine.
106 120 def fsdecode(filename):
107 121 return filename
108 122
123 def getoptb(args, shortlist, namelist):
124 return getopt.getopt(args, shortlist, namelist)
125
109 126 osname = os.name
110 127 ospathsep = os.pathsep
111 128 ossep = os.sep
112 129 stdin = sys.stdin
113 130 stdout = sys.stdout
114 131 stderr = sys.stderr
115 132 sysargv = sys.argv
116 133 getcwd = os.getcwd
117 134
118 135 stringio = io.StringIO
119 136 empty = _queue.Empty
120 137 queue = _queue.Queue
121 138
122 139 class _pycompatstub(object):
123 140 def __init__(self):
124 141 self._aliases = {}
125 142
126 143 def _registeraliases(self, origin, items):
127 144 """Add items that will be populated at the first access"""
128 145 items = map(sysstr, items)
129 146 self._aliases.update(
130 147 (item.replace(sysstr('_'), sysstr('')).lower(), (origin, item))
131 148 for item in items)
132 149
133 150 def __getattr__(self, name):
134 151 try:
135 152 origin, item = self._aliases[name]
136 153 except KeyError:
137 154 raise AttributeError(name)
138 155 self.__dict__[name] = obj = getattr(origin, item)
139 156 return obj
140 157
141 158 httpserver = _pycompatstub()
142 159 urlreq = _pycompatstub()
143 160 urlerr = _pycompatstub()
144 161 if not ispy3:
145 162 import BaseHTTPServer
146 163 import CGIHTTPServer
147 164 import SimpleHTTPServer
148 165 import urllib2
149 166 import urllib
150 167 urlreq._registeraliases(urllib, (
151 168 "addclosehook",
152 169 "addinfourl",
153 170 "ftpwrapper",
154 171 "pathname2url",
155 172 "quote",
156 173 "splitattr",
157 174 "splitpasswd",
158 175 "splitport",
159 176 "splituser",
160 177 "unquote",
161 178 "url2pathname",
162 179 "urlencode",
163 180 ))
164 181 urlreq._registeraliases(urllib2, (
165 182 "AbstractHTTPHandler",
166 183 "BaseHandler",
167 184 "build_opener",
168 185 "FileHandler",
169 186 "FTPHandler",
170 187 "HTTPBasicAuthHandler",
171 188 "HTTPDigestAuthHandler",
172 189 "HTTPHandler",
173 190 "HTTPPasswordMgrWithDefaultRealm",
174 191 "HTTPSHandler",
175 192 "install_opener",
176 193 "ProxyHandler",
177 194 "Request",
178 195 "urlopen",
179 196 ))
180 197 urlerr._registeraliases(urllib2, (
181 198 "HTTPError",
182 199 "URLError",
183 200 ))
184 201 httpserver._registeraliases(BaseHTTPServer, (
185 202 "HTTPServer",
186 203 "BaseHTTPRequestHandler",
187 204 ))
188 205 httpserver._registeraliases(SimpleHTTPServer, (
189 206 "SimpleHTTPRequestHandler",
190 207 ))
191 208 httpserver._registeraliases(CGIHTTPServer, (
192 209 "CGIHTTPRequestHandler",
193 210 ))
194 211
195 212 else:
196 213 import urllib.request
197 214 urlreq._registeraliases(urllib.request, (
198 215 "AbstractHTTPHandler",
199 216 "addclosehook",
200 217 "addinfourl",
201 218 "BaseHandler",
202 219 "build_opener",
203 220 "FileHandler",
204 221 "FTPHandler",
205 222 "ftpwrapper",
206 223 "HTTPHandler",
207 224 "HTTPSHandler",
208 225 "install_opener",
209 226 "pathname2url",
210 227 "HTTPBasicAuthHandler",
211 228 "HTTPDigestAuthHandler",
212 229 "HTTPPasswordMgrWithDefaultRealm",
213 230 "ProxyHandler",
214 231 "quote",
215 232 "Request",
216 233 "splitattr",
217 234 "splitpasswd",
218 235 "splitport",
219 236 "splituser",
220 237 "unquote",
221 238 "url2pathname",
222 239 "urlopen",
223 240 ))
224 241 import urllib.error
225 242 urlerr._registeraliases(urllib.error, (
226 243 "HTTPError",
227 244 "URLError",
228 245 ))
229 246 import http.server
230 247 httpserver._registeraliases(http.server, (
231 248 "HTTPServer",
232 249 "BaseHTTPRequestHandler",
233 250 "SimpleHTTPRequestHandler",
234 251 "CGIHTTPRequestHandler",
235 252 ))
@@ -1,805 +1,809 b''
1 1 #!/usr/bin/env python
2 2 ## statprof.py
3 3 ## Copyright (C) 2012 Bryan O'Sullivan <bos@serpentine.com>
4 4 ## Copyright (C) 2011 Alex Fraser <alex at phatcore dot com>
5 5 ## Copyright (C) 2004,2005 Andy Wingo <wingo at pobox dot com>
6 6 ## Copyright (C) 2001 Rob Browning <rlb at defaultvalue dot org>
7 7
8 8 ## This library is free software; you can redistribute it and/or
9 9 ## modify it under the terms of the GNU Lesser General Public
10 10 ## License as published by the Free Software Foundation; either
11 11 ## version 2.1 of the License, or (at your option) any later version.
12 12 ##
13 13 ## This library is distributed in the hope that it will be useful,
14 14 ## but WITHOUT ANY WARRANTY; without even the implied warranty of
15 15 ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
16 16 ## Lesser General Public License for more details.
17 17 ##
18 18 ## You should have received a copy of the GNU Lesser General Public
19 19 ## License along with this program; if not, contact:
20 20 ##
21 21 ## Free Software Foundation Voice: +1-617-542-5942
22 22 ## 59 Temple Place - Suite 330 Fax: +1-617-542-2652
23 23 ## Boston, MA 02111-1307, USA gnu@gnu.org
24 24
25 25 """
26 26 statprof is intended to be a fairly simple statistical profiler for
27 27 python. It was ported directly from a statistical profiler for guile,
28 28 also named statprof, available from guile-lib [0].
29 29
30 30 [0] http://wingolog.org/software/guile-lib/statprof/
31 31
32 32 To start profiling, call statprof.start():
33 33 >>> start()
34 34
35 35 Then run whatever it is that you want to profile, for example:
36 36 >>> import test.pystone; test.pystone.pystones()
37 37
38 38 Then stop the profiling and print out the results:
39 39 >>> stop()
40 40 >>> display()
41 41 % cumulative self
42 42 time seconds seconds name
43 43 26.72 1.40 0.37 pystone.py:79:Proc0
44 44 13.79 0.56 0.19 pystone.py:133:Proc1
45 45 13.79 0.19 0.19 pystone.py:208:Proc8
46 46 10.34 0.16 0.14 pystone.py:229:Func2
47 47 6.90 0.10 0.10 pystone.py:45:__init__
48 48 4.31 0.16 0.06 pystone.py:53:copy
49 49 ...
50 50
51 51 All of the numerical data is statistically approximate. In the
52 52 following column descriptions, and in all of statprof, "time" refers
53 53 to execution time (both user and system), not wall clock time.
54 54
55 55 % time
56 56 The percent of the time spent inside the procedure itself (not
57 57 counting children).
58 58
59 59 cumulative seconds
60 60 The total number of seconds spent in the procedure, including
61 61 children.
62 62
63 63 self seconds
64 64 The total number of seconds spent in the procedure itself (not
65 65 counting children).
66 66
67 67 name
68 68 The name of the procedure.
69 69
70 70 By default statprof keeps the data collected from previous runs. If you
71 71 want to clear the collected data, call reset():
72 72 >>> reset()
73 73
74 74 reset() can also be used to change the sampling frequency from the
75 75 default of 1000 Hz. For example, to tell statprof to sample 50 times a
76 76 second:
77 77 >>> reset(50)
78 78
79 79 This means that statprof will sample the call stack after every 1/50 of
80 80 a second of user + system time spent running on behalf of the python
81 81 process. When your process is idle (for example, blocking in a read(),
82 82 as is the case at the listener), the clock does not advance. For this
83 83 reason statprof is not currently not suitable for profiling io-bound
84 84 operations.
85 85
86 86 The profiler uses the hash of the code object itself to identify the
87 87 procedures, so it won't confuse different procedures with the same name.
88 88 They will show up as two different rows in the output.
89 89
90 90 Right now the profiler is quite simplistic. I cannot provide
91 91 call-graphs or other higher level information. What you see in the
92 92 table is pretty much all there is. Patches are welcome :-)
93 93
94 94
95 95 Threading
96 96 ---------
97 97
98 98 Because signals only get delivered to the main thread in Python,
99 99 statprof only profiles the main thread. However because the time
100 100 reporting function uses per-process timers, the results can be
101 101 significantly off if other threads' work patterns are not similar to the
102 102 main thread's work patterns.
103 103 """
104 104 # no-check-code
105 105 from __future__ import absolute_import, division, print_function
106 106
107 107 import collections
108 108 import contextlib
109 109 import getopt
110 110 import inspect
111 111 import json
112 112 import os
113 113 import signal
114 114 import sys
115 115 import tempfile
116 116 import threading
117 117 import time
118 118
119 from . import (
120 pycompat,
121 )
122
119 123 defaultdict = collections.defaultdict
120 124 contextmanager = contextlib.contextmanager
121 125
122 126 __all__ = ['start', 'stop', 'reset', 'display', 'profile']
123 127
124 128 skips = set(["util.py:check", "extensions.py:closure",
125 129 "color.py:colorcmd", "dispatch.py:checkargs",
126 130 "dispatch.py:<lambda>", "dispatch.py:_runcatch",
127 131 "dispatch.py:_dispatch", "dispatch.py:_runcommand",
128 132 "pager.py:pagecmd", "dispatch.py:run",
129 133 "dispatch.py:dispatch", "dispatch.py:runcommand",
130 134 "hg.py:<module>", "evolve.py:warnobserrors",
131 135 ])
132 136
133 137 ###########################################################################
134 138 ## Utils
135 139
136 140 def clock():
137 141 times = os.times()
138 142 return times[0] + times[1]
139 143
140 144
141 145 ###########################################################################
142 146 ## Collection data structures
143 147
144 148 class ProfileState(object):
145 149 def __init__(self, frequency=None):
146 150 self.reset(frequency)
147 151
148 152 def reset(self, frequency=None):
149 153 # total so far
150 154 self.accumulated_time = 0.0
151 155 # start_time when timer is active
152 156 self.last_start_time = None
153 157 # a float
154 158 if frequency:
155 159 self.sample_interval = 1.0 / frequency
156 160 elif not hasattr(self, 'sample_interval'):
157 161 # default to 1000 Hz
158 162 self.sample_interval = 1.0 / 1000.0
159 163 else:
160 164 # leave the frequency as it was
161 165 pass
162 166 self.remaining_prof_time = None
163 167 # for user start/stop nesting
164 168 self.profile_level = 0
165 169
166 170 self.samples = []
167 171
168 172 def accumulate_time(self, stop_time):
169 173 self.accumulated_time += stop_time - self.last_start_time
170 174
171 175 def seconds_per_sample(self):
172 176 return self.accumulated_time / len(self.samples)
173 177
174 178 state = ProfileState()
175 179
176 180
177 181 class CodeSite(object):
178 182 cache = {}
179 183
180 184 __slots__ = (u'path', u'lineno', u'function', u'source')
181 185
182 186 def __init__(self, path, lineno, function):
183 187 self.path = path
184 188 self.lineno = lineno
185 189 self.function = function
186 190 self.source = None
187 191
188 192 def __eq__(self, other):
189 193 try:
190 194 return (self.lineno == other.lineno and
191 195 self.path == other.path)
192 196 except:
193 197 return False
194 198
195 199 def __hash__(self):
196 200 return hash((self.lineno, self.path))
197 201
198 202 @classmethod
199 203 def get(cls, path, lineno, function):
200 204 k = (path, lineno)
201 205 try:
202 206 return cls.cache[k]
203 207 except KeyError:
204 208 v = cls(path, lineno, function)
205 209 cls.cache[k] = v
206 210 return v
207 211
208 212 def getsource(self, length):
209 213 if self.source is None:
210 214 lineno = self.lineno - 1
211 215 fp = None
212 216 try:
213 217 fp = open(self.path)
214 218 for i, line in enumerate(fp):
215 219 if i == lineno:
216 220 self.source = line.strip()
217 221 break
218 222 except:
219 223 pass
220 224 finally:
221 225 if fp:
222 226 fp.close()
223 227 if self.source is None:
224 228 self.source = ''
225 229
226 230 source = self.source
227 231 if len(source) > length:
228 232 source = source[:(length - 3)] + "..."
229 233 return source
230 234
231 235 def filename(self):
232 236 return os.path.basename(self.path)
233 237
234 238 class Sample(object):
235 239 __slots__ = (u'stack', u'time')
236 240
237 241 def __init__(self, stack, time):
238 242 self.stack = stack
239 243 self.time = time
240 244
241 245 @classmethod
242 246 def from_frame(cls, frame, time):
243 247 stack = []
244 248
245 249 while frame:
246 250 stack.append(CodeSite.get(frame.f_code.co_filename, frame.f_lineno,
247 251 frame.f_code.co_name))
248 252 frame = frame.f_back
249 253
250 254 return Sample(stack, time)
251 255
252 256 ###########################################################################
253 257 ## SIGPROF handler
254 258
255 259 def profile_signal_handler(signum, frame):
256 260 if state.profile_level > 0:
257 261 now = clock()
258 262 state.accumulate_time(now)
259 263
260 264 state.samples.append(Sample.from_frame(frame, state.accumulated_time))
261 265
262 266 signal.setitimer(signal.ITIMER_PROF,
263 267 state.sample_interval, 0.0)
264 268 state.last_start_time = now
265 269
266 270 stopthread = threading.Event()
267 271 def samplerthread(tid):
268 272 while not stopthread.is_set():
269 273 now = clock()
270 274 state.accumulate_time(now)
271 275
272 276 frame = sys._current_frames()[tid]
273 277 state.samples.append(Sample.from_frame(frame, state.accumulated_time))
274 278
275 279 state.last_start_time = now
276 280 time.sleep(state.sample_interval)
277 281
278 282 stopthread.clear()
279 283
280 284 ###########################################################################
281 285 ## Profiling API
282 286
283 287 def is_active():
284 288 return state.profile_level > 0
285 289
286 290 lastmechanism = None
287 291 def start(mechanism='thread'):
288 292 '''Install the profiling signal handler, and start profiling.'''
289 293 state.profile_level += 1
290 294 if state.profile_level == 1:
291 295 state.last_start_time = clock()
292 296 rpt = state.remaining_prof_time
293 297 state.remaining_prof_time = None
294 298
295 299 global lastmechanism
296 300 lastmechanism = mechanism
297 301
298 302 if mechanism == 'signal':
299 303 signal.signal(signal.SIGPROF, profile_signal_handler)
300 304 signal.setitimer(signal.ITIMER_PROF,
301 305 rpt or state.sample_interval, 0.0)
302 306 elif mechanism == 'thread':
303 307 frame = inspect.currentframe()
304 308 tid = [k for k, f in sys._current_frames().items() if f == frame][0]
305 309 state.thread = threading.Thread(target=samplerthread,
306 310 args=(tid,), name="samplerthread")
307 311 state.thread.start()
308 312
309 313 def stop():
310 314 '''Stop profiling, and uninstall the profiling signal handler.'''
311 315 state.profile_level -= 1
312 316 if state.profile_level == 0:
313 317 if lastmechanism == 'signal':
314 318 rpt = signal.setitimer(signal.ITIMER_PROF, 0.0, 0.0)
315 319 signal.signal(signal.SIGPROF, signal.SIG_IGN)
316 320 state.remaining_prof_time = rpt[0]
317 321 elif lastmechanism == 'thread':
318 322 stopthread.set()
319 323 state.thread.join()
320 324
321 325 state.accumulate_time(clock())
322 326 state.last_start_time = None
323 327 statprofpath = os.environ.get('STATPROF_DEST')
324 328 if statprofpath:
325 329 save_data(statprofpath)
326 330
327 331 return state
328 332
329 333 def save_data(path):
330 334 with open(path, 'w+') as file:
331 335 file.write(str(state.accumulated_time) + '\n')
332 336 for sample in state.samples:
333 337 time = str(sample.time)
334 338 stack = sample.stack
335 339 sites = ['\1'.join([s.path, str(s.lineno), s.function])
336 340 for s in stack]
337 341 file.write(time + '\0' + '\0'.join(sites) + '\n')
338 342
339 343 def load_data(path):
340 344 lines = open(path, 'r').read().splitlines()
341 345
342 346 state.accumulated_time = float(lines[0])
343 347 state.samples = []
344 348 for line in lines[1:]:
345 349 parts = line.split('\0')
346 350 time = float(parts[0])
347 351 rawsites = parts[1:]
348 352 sites = []
349 353 for rawsite in rawsites:
350 354 siteparts = rawsite.split('\1')
351 355 sites.append(CodeSite.get(siteparts[0], int(siteparts[1]),
352 356 siteparts[2]))
353 357
354 358 state.samples.append(Sample(sites, time))
355 359
356 360
357 361
358 362 def reset(frequency=None):
359 363 '''Clear out the state of the profiler. Do not call while the
360 364 profiler is running.
361 365
362 366 The optional frequency argument specifies the number of samples to
363 367 collect per second.'''
364 368 assert state.profile_level == 0, "Can't reset() while statprof is running"
365 369 CodeSite.cache.clear()
366 370 state.reset(frequency)
367 371
368 372
369 373 @contextmanager
370 374 def profile():
371 375 start()
372 376 try:
373 377 yield
374 378 finally:
375 379 stop()
376 380 display()
377 381
378 382
379 383 ###########################################################################
380 384 ## Reporting API
381 385
382 386 class SiteStats(object):
383 387 def __init__(self, site):
384 388 self.site = site
385 389 self.selfcount = 0
386 390 self.totalcount = 0
387 391
388 392 def addself(self):
389 393 self.selfcount += 1
390 394
391 395 def addtotal(self):
392 396 self.totalcount += 1
393 397
394 398 def selfpercent(self):
395 399 return self.selfcount / len(state.samples) * 100
396 400
397 401 def totalpercent(self):
398 402 return self.totalcount / len(state.samples) * 100
399 403
400 404 def selfseconds(self):
401 405 return self.selfcount * state.seconds_per_sample()
402 406
403 407 def totalseconds(self):
404 408 return self.totalcount * state.seconds_per_sample()
405 409
406 410 @classmethod
407 411 def buildstats(cls, samples):
408 412 stats = {}
409 413
410 414 for sample in samples:
411 415 for i, site in enumerate(sample.stack):
412 416 sitestat = stats.get(site)
413 417 if not sitestat:
414 418 sitestat = SiteStats(site)
415 419 stats[site] = sitestat
416 420
417 421 sitestat.addtotal()
418 422
419 423 if i == 0:
420 424 sitestat.addself()
421 425
422 426 return [s for s in stats.itervalues()]
423 427
424 428 class DisplayFormats:
425 429 ByLine = 0
426 430 ByMethod = 1
427 431 AboutMethod = 2
428 432 Hotpath = 3
429 433 FlameGraph = 4
430 434 Json = 5
431 435
432 436 def display(fp=None, format=3, data=None, **kwargs):
433 437 '''Print statistics, either to stdout or the given file object.'''
434 438 data = data or state
435 439
436 440 if fp is None:
437 441 import sys
438 442 fp = sys.stdout
439 443 if len(data.samples) == 0:
440 444 print('No samples recorded.', file=fp)
441 445 return
442 446
443 447 if format == DisplayFormats.ByLine:
444 448 display_by_line(data, fp)
445 449 elif format == DisplayFormats.ByMethod:
446 450 display_by_method(data, fp)
447 451 elif format == DisplayFormats.AboutMethod:
448 452 display_about_method(data, fp, **kwargs)
449 453 elif format == DisplayFormats.Hotpath:
450 454 display_hotpath(data, fp, **kwargs)
451 455 elif format == DisplayFormats.FlameGraph:
452 456 write_to_flame(data, fp, **kwargs)
453 457 elif format == DisplayFormats.Json:
454 458 write_to_json(data, fp)
455 459 else:
456 460 raise Exception("Invalid display format")
457 461
458 462 if format != DisplayFormats.Json:
459 463 print('---', file=fp)
460 464 print('Sample count: %d' % len(data.samples), file=fp)
461 465 print('Total time: %f seconds' % data.accumulated_time, file=fp)
462 466
463 467 def display_by_line(data, fp):
464 468 '''Print the profiler data with each sample line represented
465 469 as one row in a table. Sorted by self-time per line.'''
466 470 stats = SiteStats.buildstats(data.samples)
467 471 stats.sort(reverse=True, key=lambda x: x.selfseconds())
468 472
469 473 print('%5.5s %10.10s %7.7s %-8.8s' %
470 474 ('% ', 'cumulative', 'self', ''), file=fp)
471 475 print('%5.5s %9.9s %8.8s %-8.8s' %
472 476 ("time", "seconds", "seconds", "name"), file=fp)
473 477
474 478 for stat in stats:
475 479 site = stat.site
476 480 sitelabel = '%s:%d:%s' % (site.filename(), site.lineno, site.function)
477 481 print('%6.2f %9.2f %9.2f %s' % (stat.selfpercent(),
478 482 stat.totalseconds(),
479 483 stat.selfseconds(),
480 484 sitelabel),
481 485 file=fp)
482 486
483 487 def display_by_method(data, fp):
484 488 '''Print the profiler data with each sample function represented
485 489 as one row in a table. Important lines within that function are
486 490 output as nested rows. Sorted by self-time per line.'''
487 491 print('%5.5s %10.10s %7.7s %-8.8s' %
488 492 ('% ', 'cumulative', 'self', ''), file=fp)
489 493 print('%5.5s %9.9s %8.8s %-8.8s' %
490 494 ("time", "seconds", "seconds", "name"), file=fp)
491 495
492 496 stats = SiteStats.buildstats(data.samples)
493 497
494 498 grouped = defaultdict(list)
495 499 for stat in stats:
496 500 grouped[stat.site.filename() + ":" + stat.site.function].append(stat)
497 501
498 502 # compute sums for each function
499 503 functiondata = []
500 504 for fname, sitestats in grouped.iteritems():
501 505 total_cum_sec = 0
502 506 total_self_sec = 0
503 507 total_percent = 0
504 508 for stat in sitestats:
505 509 total_cum_sec += stat.totalseconds()
506 510 total_self_sec += stat.selfseconds()
507 511 total_percent += stat.selfpercent()
508 512
509 513 functiondata.append((fname,
510 514 total_cum_sec,
511 515 total_self_sec,
512 516 total_percent,
513 517 sitestats))
514 518
515 519 # sort by total self sec
516 520 functiondata.sort(reverse=True, key=lambda x: x[2])
517 521
518 522 for function in functiondata:
519 523 if function[3] < 0.05:
520 524 continue
521 525 print('%6.2f %9.2f %9.2f %s' % (function[3], # total percent
522 526 function[1], # total cum sec
523 527 function[2], # total self sec
524 528 function[0]), # file:function
525 529 file=fp)
526 530 function[4].sort(reverse=True, key=lambda i: i.selfseconds())
527 531 for stat in function[4]:
528 532 # only show line numbers for significant locations (>1% time spent)
529 533 if stat.selfpercent() > 1:
530 534 source = stat.site.getsource(25)
531 535 stattuple = (stat.selfpercent(), stat.selfseconds(),
532 536 stat.site.lineno, source)
533 537
534 538 print('%33.0f%% %6.2f line %s: %s' % (stattuple), file=fp)
535 539
536 540 def display_about_method(data, fp, function=None, **kwargs):
537 541 if function is None:
538 542 raise Exception("Invalid function")
539 543
540 544 filename = None
541 545 if ':' in function:
542 546 filename, function = function.split(':')
543 547
544 548 relevant_samples = 0
545 549 parents = {}
546 550 children = {}
547 551
548 552 for sample in data.samples:
549 553 for i, site in enumerate(sample.stack):
550 554 if site.function == function and (not filename
551 555 or site.filename() == filename):
552 556 relevant_samples += 1
553 557 if i != len(sample.stack) - 1:
554 558 parent = sample.stack[i + 1]
555 559 if parent in parents:
556 560 parents[parent] = parents[parent] + 1
557 561 else:
558 562 parents[parent] = 1
559 563
560 564 if site in children:
561 565 children[site] = children[site] + 1
562 566 else:
563 567 children[site] = 1
564 568
565 569 parents = [(parent, count) for parent, count in parents.iteritems()]
566 570 parents.sort(reverse=True, key=lambda x: x[1])
567 571 for parent, count in parents:
568 572 print('%6.2f%% %s:%s line %s: %s' %
569 573 (count / relevant_samples * 100, parent.filename(),
570 574 parent.function, parent.lineno, parent.getsource(50)), file=fp)
571 575
572 576 stats = SiteStats.buildstats(data.samples)
573 577 stats = [s for s in stats
574 578 if s.site.function == function and
575 579 (not filename or s.site.filename() == filename)]
576 580
577 581 total_cum_sec = 0
578 582 total_self_sec = 0
579 583 total_self_percent = 0
580 584 total_cum_percent = 0
581 585 for stat in stats:
582 586 total_cum_sec += stat.totalseconds()
583 587 total_self_sec += stat.selfseconds()
584 588 total_self_percent += stat.selfpercent()
585 589 total_cum_percent += stat.totalpercent()
586 590
587 591 print(
588 592 '\n %s:%s Total: %0.2fs (%0.2f%%) Self: %0.2fs (%0.2f%%)\n' %
589 593 (
590 594 filename or '___',
591 595 function,
592 596 total_cum_sec,
593 597 total_cum_percent,
594 598 total_self_sec,
595 599 total_self_percent
596 600 ), file=fp)
597 601
598 602 children = [(child, count) for child, count in children.iteritems()]
599 603 children.sort(reverse=True, key=lambda x: x[1])
600 604 for child, count in children:
601 605 print(' %6.2f%% line %s: %s' %
602 606 (count / relevant_samples * 100, child.lineno,
603 607 child.getsource(50)), file=fp)
604 608
605 609 def display_hotpath(data, fp, limit=0.05, **kwargs):
606 610 class HotNode(object):
607 611 def __init__(self, site):
608 612 self.site = site
609 613 self.count = 0
610 614 self.children = {}
611 615
612 616 def add(self, stack, time):
613 617 self.count += time
614 618 site = stack[0]
615 619 child = self.children.get(site)
616 620 if not child:
617 621 child = HotNode(site)
618 622 self.children[site] = child
619 623
620 624 if len(stack) > 1:
621 625 i = 1
622 626 # Skip boiler plate parts of the stack
623 627 while i < len(stack) and '%s:%s' % (stack[i].filename(), stack[i].function) in skips:
624 628 i += 1
625 629 if i < len(stack):
626 630 child.add(stack[i:], time)
627 631
628 632 root = HotNode(None)
629 633 lasttime = data.samples[0].time
630 634 for sample in data.samples:
631 635 root.add(sample.stack[::-1], sample.time - lasttime)
632 636 lasttime = sample.time
633 637
634 638 def _write(node, depth, multiple_siblings):
635 639 site = node.site
636 640 visiblechildren = [c for c in node.children.itervalues()
637 641 if c.count >= (limit * root.count)]
638 642 if site:
639 643 indent = depth * 2 - 1
640 644 filename = ''
641 645 function = ''
642 646 if len(node.children) > 0:
643 647 childsite = list(node.children.itervalues())[0].site
644 648 filename = (childsite.filename() + ':').ljust(15)
645 649 function = childsite.function
646 650
647 651 # lots of string formatting
648 652 listpattern = ''.ljust(indent) +\
649 653 ('\\' if multiple_siblings else '|') +\
650 654 ' %4.1f%% %s %s'
651 655 liststring = listpattern % (node.count / root.count * 100,
652 656 filename, function)
653 657 codepattern = '%' + str(55 - len(liststring)) + 's %s: %s'
654 658 codestring = codepattern % ('line', site.lineno, site.getsource(30))
655 659
656 660 finalstring = liststring + codestring
657 661 childrensamples = sum([c.count for c in node.children.itervalues()])
658 662 # Make frames that performed more than 10% of the operation red
659 663 if node.count - childrensamples > (0.1 * root.count):
660 664 finalstring = '\033[91m' + finalstring + '\033[0m'
661 665 # Make frames that didn't actually perform work dark grey
662 666 elif node.count - childrensamples == 0:
663 667 finalstring = '\033[90m' + finalstring + '\033[0m'
664 668 print(finalstring, file=fp)
665 669
666 670 newdepth = depth
667 671 if len(visiblechildren) > 1 or multiple_siblings:
668 672 newdepth += 1
669 673
670 674 visiblechildren.sort(reverse=True, key=lambda x: x.count)
671 675 for child in visiblechildren:
672 676 _write(child, newdepth, len(visiblechildren) > 1)
673 677
674 678 if root.count > 0:
675 679 _write(root, 0, False)
676 680
677 681 def write_to_flame(data, fp, scriptpath=None, outputfile=None, **kwargs):
678 682 if scriptpath is None:
679 683 scriptpath = os.environ['HOME'] + '/flamegraph.pl'
680 684 if not os.path.exists(scriptpath):
681 685 print("error: missing %s" % scriptpath, file=fp)
682 686 print("get it here: https://github.com/brendangregg/FlameGraph",
683 687 file=fp)
684 688 return
685 689
686 690 fd, path = tempfile.mkstemp()
687 691
688 692 file = open(path, "w+")
689 693
690 694 lines = {}
691 695 for sample in data.samples:
692 696 sites = [s.function for s in sample.stack]
693 697 sites.reverse()
694 698 line = ';'.join(sites)
695 699 if line in lines:
696 700 lines[line] = lines[line] + 1
697 701 else:
698 702 lines[line] = 1
699 703
700 704 for line, count in lines.iteritems():
701 705 file.write("%s %s\n" % (line, count))
702 706
703 707 file.close()
704 708
705 709 if outputfile is None:
706 710 outputfile = '~/flamegraph.svg'
707 711
708 712 os.system("perl ~/flamegraph.pl %s > %s" % (path, outputfile))
709 713 print("Written to %s" % outputfile, file=fp)
710 714
711 715 def write_to_json(data, fp):
712 716 samples = []
713 717
714 718 for sample in data.samples:
715 719 stack = []
716 720
717 721 for frame in sample.stack:
718 722 stack.append((frame.path, frame.lineno, frame.function))
719 723
720 724 samples.append((sample.time, stack))
721 725
722 726 print(json.dumps(samples), file=fp)
723 727
724 728 def printusage():
725 729 print("""
726 730 The statprof command line allows you to inspect the last profile's results in
727 731 the following forms:
728 732
729 733 usage:
730 734 hotpath [-l --limit percent]
731 735 Shows a graph of calls with the percent of time each takes.
732 736 Red calls take over 10%% of the total time themselves.
733 737 lines
734 738 Shows the actual sampled lines.
735 739 functions
736 740 Shows the samples grouped by function.
737 741 function [filename:]functionname
738 742 Shows the callers and callees of a particular function.
739 743 flame [-s --script-path] [-o --output-file path]
740 744 Writes out a flamegraph to output-file (defaults to ~/flamegraph.svg)
741 745 Requires that ~/flamegraph.pl exist.
742 746 (Specify alternate script path with --script-path.)""")
743 747
744 748 def main(argv=None):
745 749 if argv is None:
746 750 argv = sys.argv
747 751
748 752 if len(argv) == 1:
749 753 printusage()
750 754 return 0
751 755
752 756 displayargs = {}
753 757
754 758 optstart = 2
755 759 displayargs['function'] = None
756 760 if argv[1] == 'hotpath':
757 761 displayargs['format'] = DisplayFormats.Hotpath
758 762 elif argv[1] == 'lines':
759 763 displayargs['format'] = DisplayFormats.ByLine
760 764 elif argv[1] == 'functions':
761 765 displayargs['format'] = DisplayFormats.ByMethod
762 766 elif argv[1] == 'function':
763 767 displayargs['format'] = DisplayFormats.AboutMethod
764 768 displayargs['function'] = argv[2]
765 769 optstart = 3
766 770 elif argv[1] == 'flame':
767 771 displayargs['format'] = DisplayFormats.FlameGraph
768 772 else:
769 773 printusage()
770 774 return 0
771 775
772 776 # process options
773 777 try:
774 opts, args = getopt.getopt(sys.argv[optstart:], "hl:f:o:p:",
778 opts, args = pycompat.getoptb(sys.argv[optstart:], "hl:f:o:p:",
775 779 ["help", "limit=", "file=", "output-file=", "script-path="])
776 780 except getopt.error as msg:
777 781 print(msg)
778 782 printusage()
779 783 return 2
780 784
781 785 displayargs['limit'] = 0.05
782 786 path = None
783 787 for o, value in opts:
784 788 if o in ("-l", "--limit"):
785 789 displayargs['limit'] = float(value)
786 790 elif o in ("-f", "--file"):
787 791 path = value
788 792 elif o in ("-o", "--output-file"):
789 793 displayargs['outputfile'] = value
790 794 elif o in ("-p", "--script-path"):
791 795 displayargs['scriptpath'] = value
792 796 elif o in ("-h", "help"):
793 797 printusage()
794 798 return 0
795 799 else:
796 800 assert False, "unhandled option %s" % o
797 801
798 802 load_data(path=path)
799 803
800 804 display(**displayargs)
801 805
802 806 return 0
803 807
804 808 if __name__ == "__main__":
805 809 sys.exit(main())
General Comments 0
You need to be logged in to leave comments. Login now