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