##// END OF EJS Templates
profile: introduce a knob to control if the context is actually profiling...
marmoute -
r32785:37ec8f24 default
parent child Browse files
Show More
@@ -1,235 +1,234 b''
1 # profiling.py - profiling functions
1 # profiling.py - profiling functions
2 #
2 #
3 # Copyright 2016 Gregory Szorc <gregory.szorc@gmail.com>
3 # Copyright 2016 Gregory Szorc <gregory.szorc@gmail.com>
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, print_function
8 from __future__ import absolute_import, print_function
9
9
10 import contextlib
10 import contextlib
11
11
12 from .i18n import _
12 from .i18n import _
13 from . import (
13 from . import (
14 encoding,
14 encoding,
15 error,
15 error,
16 extensions,
16 extensions,
17 util,
17 util,
18 )
18 )
19
19
20 def _loadprofiler(ui, profiler):
20 def _loadprofiler(ui, profiler):
21 """load profiler extension. return profile method, or None on failure"""
21 """load profiler extension. return profile method, or None on failure"""
22 extname = profiler
22 extname = profiler
23 extensions.loadall(ui, whitelist=[extname])
23 extensions.loadall(ui, whitelist=[extname])
24 try:
24 try:
25 mod = extensions.find(extname)
25 mod = extensions.find(extname)
26 except KeyError:
26 except KeyError:
27 return None
27 return None
28 else:
28 else:
29 return getattr(mod, 'profile', None)
29 return getattr(mod, 'profile', None)
30
30
31 @contextlib.contextmanager
31 @contextlib.contextmanager
32 def lsprofile(ui, fp):
32 def lsprofile(ui, fp):
33 format = ui.config('profiling', 'format', default='text')
33 format = ui.config('profiling', 'format', default='text')
34 field = ui.config('profiling', 'sort', default='inlinetime')
34 field = ui.config('profiling', 'sort', default='inlinetime')
35 limit = ui.configint('profiling', 'limit', default=30)
35 limit = ui.configint('profiling', 'limit', default=30)
36 climit = ui.configint('profiling', 'nested', default=0)
36 climit = ui.configint('profiling', 'nested', default=0)
37
37
38 if format not in ['text', 'kcachegrind']:
38 if format not in ['text', 'kcachegrind']:
39 ui.warn(_("unrecognized profiling format '%s'"
39 ui.warn(_("unrecognized profiling format '%s'"
40 " - Ignored\n") % format)
40 " - Ignored\n") % format)
41 format = 'text'
41 format = 'text'
42
42
43 try:
43 try:
44 from . import lsprof
44 from . import lsprof
45 except ImportError:
45 except ImportError:
46 raise error.Abort(_(
46 raise error.Abort(_(
47 'lsprof not available - install from '
47 'lsprof not available - install from '
48 'http://codespeak.net/svn/user/arigo/hack/misc/lsprof/'))
48 'http://codespeak.net/svn/user/arigo/hack/misc/lsprof/'))
49 p = lsprof.Profiler()
49 p = lsprof.Profiler()
50 p.enable(subcalls=True)
50 p.enable(subcalls=True)
51 try:
51 try:
52 yield
52 yield
53 finally:
53 finally:
54 p.disable()
54 p.disable()
55
55
56 if format == 'kcachegrind':
56 if format == 'kcachegrind':
57 from . import lsprofcalltree
57 from . import lsprofcalltree
58 calltree = lsprofcalltree.KCacheGrind(p)
58 calltree = lsprofcalltree.KCacheGrind(p)
59 calltree.output(fp)
59 calltree.output(fp)
60 else:
60 else:
61 # format == 'text'
61 # format == 'text'
62 stats = lsprof.Stats(p.getstats())
62 stats = lsprof.Stats(p.getstats())
63 stats.sort(field)
63 stats.sort(field)
64 stats.pprint(limit=limit, file=fp, climit=climit)
64 stats.pprint(limit=limit, file=fp, climit=climit)
65
65
66 @contextlib.contextmanager
66 @contextlib.contextmanager
67 def flameprofile(ui, fp):
67 def flameprofile(ui, fp):
68 try:
68 try:
69 from flamegraph import flamegraph
69 from flamegraph import flamegraph
70 except ImportError:
70 except ImportError:
71 raise error.Abort(_(
71 raise error.Abort(_(
72 'flamegraph not available - install from '
72 'flamegraph not available - install from '
73 'https://github.com/evanhempel/python-flamegraph'))
73 'https://github.com/evanhempel/python-flamegraph'))
74 # developer config: profiling.freq
74 # developer config: profiling.freq
75 freq = ui.configint('profiling', 'freq', default=1000)
75 freq = ui.configint('profiling', 'freq', default=1000)
76 filter_ = None
76 filter_ = None
77 collapse_recursion = True
77 collapse_recursion = True
78 thread = flamegraph.ProfileThread(fp, 1.0 / freq,
78 thread = flamegraph.ProfileThread(fp, 1.0 / freq,
79 filter_, collapse_recursion)
79 filter_, collapse_recursion)
80 start_time = util.timer()
80 start_time = util.timer()
81 try:
81 try:
82 thread.start()
82 thread.start()
83 yield
83 yield
84 finally:
84 finally:
85 thread.stop()
85 thread.stop()
86 thread.join()
86 thread.join()
87 print('Collected %d stack frames (%d unique) in %2.2f seconds.' % (
87 print('Collected %d stack frames (%d unique) in %2.2f seconds.' % (
88 util.timer() - start_time, thread.num_frames(),
88 util.timer() - start_time, thread.num_frames(),
89 thread.num_frames(unique=True)))
89 thread.num_frames(unique=True)))
90
90
91 @contextlib.contextmanager
91 @contextlib.contextmanager
92 def statprofile(ui, fp):
92 def statprofile(ui, fp):
93 from . import statprof
93 from . import statprof
94
94
95 freq = ui.configint('profiling', 'freq', default=1000)
95 freq = ui.configint('profiling', 'freq', default=1000)
96 if freq > 0:
96 if freq > 0:
97 # Cannot reset when profiler is already active. So silently no-op.
97 # Cannot reset when profiler is already active. So silently no-op.
98 if statprof.state.profile_level == 0:
98 if statprof.state.profile_level == 0:
99 statprof.reset(freq)
99 statprof.reset(freq)
100 else:
100 else:
101 ui.warn(_("invalid sampling frequency '%s' - ignoring\n") % freq)
101 ui.warn(_("invalid sampling frequency '%s' - ignoring\n") % freq)
102
102
103 statprof.start(mechanism='thread')
103 statprof.start(mechanism='thread')
104
104
105 try:
105 try:
106 yield
106 yield
107 finally:
107 finally:
108 data = statprof.stop()
108 data = statprof.stop()
109
109
110 profformat = ui.config('profiling', 'statformat', 'hotpath')
110 profformat = ui.config('profiling', 'statformat', 'hotpath')
111
111
112 formats = {
112 formats = {
113 'byline': statprof.DisplayFormats.ByLine,
113 'byline': statprof.DisplayFormats.ByLine,
114 'bymethod': statprof.DisplayFormats.ByMethod,
114 'bymethod': statprof.DisplayFormats.ByMethod,
115 'hotpath': statprof.DisplayFormats.Hotpath,
115 'hotpath': statprof.DisplayFormats.Hotpath,
116 'json': statprof.DisplayFormats.Json,
116 'json': statprof.DisplayFormats.Json,
117 'chrome': statprof.DisplayFormats.Chrome,
117 'chrome': statprof.DisplayFormats.Chrome,
118 }
118 }
119
119
120 if profformat in formats:
120 if profformat in formats:
121 displayformat = formats[profformat]
121 displayformat = formats[profformat]
122 else:
122 else:
123 ui.warn(_('unknown profiler output format: %s\n') % profformat)
123 ui.warn(_('unknown profiler output format: %s\n') % profformat)
124 displayformat = statprof.DisplayFormats.Hotpath
124 displayformat = statprof.DisplayFormats.Hotpath
125
125
126 kwargs = {}
126 kwargs = {}
127
127
128 def fraction(s):
128 def fraction(s):
129 if s.endswith('%'):
129 if s.endswith('%'):
130 v = float(s[:-1]) / 100
130 v = float(s[:-1]) / 100
131 else:
131 else:
132 v = float(s)
132 v = float(s)
133 if 0 <= v <= 1:
133 if 0 <= v <= 1:
134 return v
134 return v
135 raise ValueError(s)
135 raise ValueError(s)
136
136
137 if profformat == 'chrome':
137 if profformat == 'chrome':
138 showmin = ui.configwith(fraction, 'profiling', 'showmin', 0.005)
138 showmin = ui.configwith(fraction, 'profiling', 'showmin', 0.005)
139 showmax = ui.configwith(fraction, 'profiling', 'showmax', 0.999)
139 showmax = ui.configwith(fraction, 'profiling', 'showmax', 0.999)
140 kwargs.update(minthreshold=showmin, maxthreshold=showmax)
140 kwargs.update(minthreshold=showmin, maxthreshold=showmax)
141
141
142 statprof.display(fp, data=data, format=displayformat, **kwargs)
142 statprof.display(fp, data=data, format=displayformat, **kwargs)
143
143
144 class profile(object):
144 class profile(object):
145 """Start profiling.
145 """Start profiling.
146
146
147 Profiling is active when the context manager is active. When the context
147 Profiling is active when the context manager is active. When the context
148 manager exits, profiling results will be written to the configured output.
148 manager exits, profiling results will be written to the configured output.
149 """
149 """
150 def __init__(self, ui):
150 def __init__(self, ui, enabled=True):
151 self._ui = ui
151 self._ui = ui
152 self._output = None
152 self._output = None
153 self._fp = None
153 self._fp = None
154 self._profiler = None
154 self._profiler = None
155 self._enabled = enabled
155 self._entered = False
156 self._entered = False
156 self._started = False
157 self._started = False
157
158
158 def __enter__(self):
159 def __enter__(self):
159 self._entered = True
160 self._entered = True
160 self.start()
161 if self._enabled:
162 self.start()
161
163
162 def start(self):
164 def start(self):
163 """Start profiling.
165 """Start profiling.
164
166
165 The profiling will stop at the context exit.
167 The profiling will stop at the context exit.
166
168
167 If the profiler was already started, this has no effect."""
169 If the profiler was already started, this has no effect."""
168 if not self._entered:
170 if not self._entered:
169 raise error.ProgrammingError()
171 raise error.ProgrammingError()
170 if self._started:
172 if self._started:
171 return
173 return
172 self._started = True
174 self._started = True
173 profiler = encoding.environ.get('HGPROF')
175 profiler = encoding.environ.get('HGPROF')
174 proffn = None
176 proffn = None
175 if profiler is None:
177 if profiler is None:
176 profiler = self._ui.config('profiling', 'type', default='stat')
178 profiler = self._ui.config('profiling', 'type', default='stat')
177 if profiler not in ('ls', 'stat', 'flame'):
179 if profiler not in ('ls', 'stat', 'flame'):
178 # try load profiler from extension with the same name
180 # try load profiler from extension with the same name
179 proffn = _loadprofiler(self._ui, profiler)
181 proffn = _loadprofiler(self._ui, profiler)
180 if proffn is None:
182 if proffn is None:
181 self._ui.warn(_("unrecognized profiler '%s' - ignored\n")
183 self._ui.warn(_("unrecognized profiler '%s' - ignored\n")
182 % profiler)
184 % profiler)
183 profiler = 'stat'
185 profiler = 'stat'
184
186
185 self._output = self._ui.config('profiling', 'output')
187 self._output = self._ui.config('profiling', 'output')
186
188
187 if self._output == 'blackbox':
189 if self._output == 'blackbox':
188 self._fp = util.stringio()
190 self._fp = util.stringio()
189 elif self._output:
191 elif self._output:
190 path = self._ui.expandpath(self._output)
192 path = self._ui.expandpath(self._output)
191 self._fp = open(path, 'wb')
193 self._fp = open(path, 'wb')
192 else:
194 else:
193 self._fp = self._ui.ferr
195 self._fp = self._ui.ferr
194
196
195 if proffn is not None:
197 if proffn is not None:
196 pass
198 pass
197 elif profiler == 'ls':
199 elif profiler == 'ls':
198 proffn = lsprofile
200 proffn = lsprofile
199 elif profiler == 'flame':
201 elif profiler == 'flame':
200 proffn = flameprofile
202 proffn = flameprofile
201 else:
203 else:
202 proffn = statprofile
204 proffn = statprofile
203
205
204 self._profiler = proffn(self._ui, self._fp)
206 self._profiler = proffn(self._ui, self._fp)
205 self._profiler.__enter__()
207 self._profiler.__enter__()
206
208
207 def __exit__(self, exception_type, exception_value, traceback):
209 def __exit__(self, exception_type, exception_value, traceback):
208 if self._profiler is None:
210 if self._profiler is None:
209 return
211 return
210 self._profiler.__exit__(exception_type, exception_value, traceback)
212 self._profiler.__exit__(exception_type, exception_value, traceback)
211 if self._output:
213 if self._output:
212 if self._output == 'blackbox':
214 if self._output == 'blackbox':
213 val = 'Profile:\n%s' % self._fp.getvalue()
215 val = 'Profile:\n%s' % self._fp.getvalue()
214 # ui.log treats the input as a format string,
216 # ui.log treats the input as a format string,
215 # so we need to escape any % signs.
217 # so we need to escape any % signs.
216 val = val.replace('%', '%%')
218 val = val.replace('%', '%%')
217 self._ui.log('profile', val)
219 self._ui.log('profile', val)
218 self._fp.close()
220 self._fp.close()
219
221
220 @contextlib.contextmanager
222 @contextlib.contextmanager
221 def maybeprofile(ui):
223 def maybeprofile(ui):
222 """Profile if enabled, else do nothing.
224 """Profile if enabled, else do nothing.
223
225
224 This context manager can be used to optionally profile if profiling
226 This context manager can be used to optionally profile if profiling
225 is enabled. Otherwise, it does nothing.
227 is enabled. Otherwise, it does nothing.
226
228
227 The purpose of this context manager is to make calling code simpler:
229 The purpose of this context manager is to make calling code simpler:
228 just use a single code path for calling into code you may want to profile
230 just use a single code path for calling into code you may want to profile
229 and this function determines whether to start profiling.
231 and this function determines whether to start profiling.
230 """
232 """
231 if ui.configbool('profiling', 'enabled'):
233 with profile(ui, enabled=ui.configbool('profiling', 'enabled')):
232 with profile(ui):
233 yield
234 else:
235 yield
234 yield
General Comments 0
You need to be logged in to leave comments. Login now