##// END OF EJS Templates
branching: merge with stable
marmoute -
r52735:3785814b merge default
parent child Browse files
Show More
@@ -1,340 +1,357
1 1 # profiling.py - profiling functions
2 2 #
3 3 # Copyright 2016 Gregory Szorc <gregory.szorc@gmail.com>
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
9 9 import contextlib
10 10 import os
11 11 import signal
12 12 import subprocess
13 import sys
13 14
14 15 from .i18n import _
15 16 from .pycompat import (
16 17 open,
17 18 )
18 19 from . import (
19 20 encoding,
20 21 error,
21 22 extensions,
22 23 pycompat,
23 24 util,
24 25 )
25 26
26 27
27 28 def _loadprofiler(ui, profiler):
28 29 """load profiler extension. return profile method, or None on failure"""
29 30 extname = profiler
30 31 extensions.loadall(ui, whitelist=[extname])
31 32 try:
32 33 mod = extensions.find(extname)
33 34 except KeyError:
34 35 return None
35 36 else:
36 37 return getattr(mod, 'profile', None)
37 38
38 39
39 40 @contextlib.contextmanager
40 41 def lsprofile(ui, fp):
41 42 format = ui.config(b'profiling', b'format')
42 43 field = ui.config(b'profiling', b'sort')
43 44 limit = ui.configint(b'profiling', b'limit')
44 45 climit = ui.configint(b'profiling', b'nested')
45 46
46 47 if format not in [b'text', b'kcachegrind']:
47 48 ui.warn(_(b"unrecognized profiling format '%s' - Ignored\n") % format)
48 49 format = b'text'
49 50
50 51 try:
51 52 from . import lsprof
52 53 except ImportError:
53 54 raise error.Abort(
54 55 _(
55 56 b'lsprof not available - install from '
56 57 b'http://codespeak.net/svn/user/arigo/hack/misc/lsprof/'
57 58 )
58 59 )
59 60 p = lsprof.Profiler()
60 p.enable(subcalls=True)
61 try:
62 p.enable(subcalls=True)
63 except ValueError as exc:
64 if str(exc) != "Another profiling tool is already active":
65 raise
66 if not hasattr(sys, "monitoring"):
67 raise
68 # python >=3.12 prevent more than one profiler to run at the same
69 # time, tries to improve the report to help the user understand
70 # what is going on.
71 other_tool_name = sys.monitoring.get_tool(sys.monitoring.PROFILER_ID)
72 if other_tool_name == "cProfile":
73 msg = 'cannot recursively call `lsprof`'
74 raise error.Abort(msg) from None
75 else:
76 m = 'failed to start "lsprofile"; another profiler already running: %s'
77 raise error.Abort(_(m) % other_tool_name) from None
61 78 try:
62 79 yield
63 80 finally:
64 81 p.disable()
65 82
66 83 if format == b'kcachegrind':
67 84 from . import lsprofcalltree
68 85
69 86 calltree = lsprofcalltree.KCacheGrind(p)
70 87 calltree.output(fp)
71 88 else:
72 89 # format == 'text'
73 90 stats = lsprof.Stats(p.getstats())
74 91 stats.sort(pycompat.sysstr(field))
75 92 stats.pprint(limit=limit, file=fp, climit=climit)
76 93 fp.flush()
77 94
78 95
79 96 @contextlib.contextmanager
80 97 def flameprofile(ui, fp):
81 98 try:
82 99 from flamegraph import flamegraph # pytype: disable=import-error
83 100 except ImportError:
84 101 raise error.Abort(
85 102 _(
86 103 b'flamegraph not available - install from '
87 104 b'https://github.com/evanhempel/python-flamegraph'
88 105 )
89 106 )
90 107 # developer config: profiling.freq
91 108 freq = ui.configint(b'profiling', b'freq')
92 109 filter_ = None
93 110 collapse_recursion = True
94 111 thread = flamegraph.ProfileThread(
95 112 fp, 1.0 / freq, filter_, collapse_recursion
96 113 )
97 114 start_time = util.timer()
98 115 try:
99 116 thread.start()
100 117 yield
101 118 finally:
102 119 thread.stop()
103 120 thread.join()
104 121 m = b'Collected %d stack frames (%d unique) in %2.2f seconds.'
105 122 m %= (
106 123 (
107 124 util.timer() - start_time,
108 125 thread.num_frames(),
109 126 thread.num_frames(unique=True),
110 127 ),
111 128 )
112 129 print(m, flush=True)
113 130
114 131
115 132 @contextlib.contextmanager
116 133 def statprofile(ui, fp):
117 134 from . import statprof
118 135
119 136 freq = ui.configint(b'profiling', b'freq')
120 137 if freq > 0:
121 138 # Cannot reset when profiler is already active. So silently no-op.
122 139 if statprof.state.profile_level == 0:
123 140 statprof.reset(freq)
124 141 else:
125 142 ui.warn(_(b"invalid sampling frequency '%s' - ignoring\n") % freq)
126 143
127 144 track = ui.config(
128 145 b'profiling', b'time-track', pycompat.iswindows and b'cpu' or b'real'
129 146 )
130 147 statprof.start(mechanism=b'thread', track=track)
131 148
132 149 try:
133 150 yield
134 151 finally:
135 152 data = statprof.stop()
136 153
137 154 profformat = ui.config(b'profiling', b'statformat')
138 155
139 156 formats = {
140 157 b'byline': statprof.DisplayFormats.ByLine,
141 158 b'bymethod': statprof.DisplayFormats.ByMethod,
142 159 b'hotpath': statprof.DisplayFormats.Hotpath,
143 160 b'json': statprof.DisplayFormats.Json,
144 161 b'chrome': statprof.DisplayFormats.Chrome,
145 162 }
146 163
147 164 if profformat in formats:
148 165 displayformat = formats[profformat]
149 166 else:
150 167 ui.warn(_(b'unknown profiler output format: %s\n') % profformat)
151 168 displayformat = statprof.DisplayFormats.Hotpath
152 169
153 170 kwargs = {}
154 171
155 172 def fraction(s):
156 173 if isinstance(s, (float, int)):
157 174 return float(s)
158 175 if s.endswith(b'%'):
159 176 v = float(s[:-1]) / 100
160 177 else:
161 178 v = float(s)
162 179 if 0 <= v <= 1:
163 180 return v
164 181 raise ValueError(s)
165 182
166 183 if profformat == b'chrome':
167 184 showmin = ui.configwith(fraction, b'profiling', b'showmin', 0.005)
168 185 showmax = ui.configwith(fraction, b'profiling', b'showmax')
169 186 kwargs.update(minthreshold=showmin, maxthreshold=showmax)
170 187 elif profformat == b'hotpath':
171 188 # inconsistent config: profiling.showmin
172 189 limit = ui.configwith(fraction, b'profiling', b'showmin', 0.05)
173 190 kwargs['limit'] = limit
174 191 showtime = ui.configbool(b'profiling', b'showtime')
175 192 kwargs['showtime'] = showtime
176 193
177 194 statprof.display(fp, data=data, format=displayformat, **kwargs)
178 195 fp.flush()
179 196
180 197
181 198 @contextlib.contextmanager
182 199 def pyspy_profile(ui, fp):
183 200 exe = ui.config(b'profiling', b'py-spy.exe')
184 201
185 202 freq = ui.configint(b'profiling', b'py-spy.freq')
186 203
187 204 format = ui.config(b'profiling', b'py-spy.format')
188 205
189 206 fd = fp.fileno()
190 207
191 208 output_path = "/dev/fd/%d" % (fd)
192 209
193 210 my_pid = os.getpid()
194 211
195 212 cmd = [
196 213 exe,
197 214 "record",
198 215 "--pid",
199 216 str(my_pid),
200 217 "--native",
201 218 "--rate",
202 219 str(freq),
203 220 "--output",
204 221 output_path,
205 222 ]
206 223
207 224 if format:
208 225 cmd.extend(["--format", format])
209 226
210 227 proc = subprocess.Popen(
211 228 cmd,
212 229 pass_fds={fd},
213 230 stdout=subprocess.PIPE,
214 231 )
215 232
216 233 _ = proc.stdout.readline()
217 234
218 235 try:
219 236 yield
220 237 finally:
221 238 os.kill(proc.pid, signal.SIGINT)
222 239 proc.communicate()
223 240
224 241
225 242 class profile:
226 243 """Start profiling.
227 244
228 245 Profiling is active when the context manager is active. When the context
229 246 manager exits, profiling results will be written to the configured output.
230 247 """
231 248
232 249 def __init__(self, ui, enabled=True):
233 250 self._ui = ui
234 251 self._output = None
235 252 self._fp = None
236 253 self._fpdoclose = True
237 254 self._flushfp = None
238 255 self._profiler = None
239 256 self._enabled = enabled
240 257 self._entered = False
241 258 self._started = False
242 259
243 260 def __enter__(self):
244 261 self._entered = True
245 262 if self._enabled:
246 263 self.start()
247 264 return self
248 265
249 266 def start(self):
250 267 """Start profiling.
251 268
252 269 The profiling will stop at the context exit.
253 270
254 271 If the profiler was already started, this has no effect."""
255 272 if not self._entered:
256 273 raise error.ProgrammingError(b'use a context manager to start')
257 274 if self._started:
258 275 return
259 276 self._started = True
260 277 profiler = encoding.environ.get(b'HGPROF')
261 278 proffn = None
262 279 if profiler is None:
263 280 profiler = self._ui.config(b'profiling', b'type')
264 281 if profiler not in (b'ls', b'stat', b'flame', b'py-spy'):
265 282 # try load profiler from extension with the same name
266 283 proffn = _loadprofiler(self._ui, profiler)
267 284 if proffn is None:
268 285 self._ui.warn(
269 286 _(b"unrecognized profiler '%s' - ignored\n") % profiler
270 287 )
271 288 profiler = b'stat'
272 289
273 290 self._output = self._ui.config(b'profiling', b'output')
274 291
275 292 try:
276 293 if self._output == b'blackbox':
277 294 self._fp = util.stringio()
278 295 elif self._output:
279 296 path = util.expandpath(self._output)
280 297 self._fp = open(path, b'wb')
281 298 elif pycompat.iswindows:
282 299 # parse escape sequence by win32print()
283 300 class uifp:
284 301 def __init__(self, ui):
285 302 self._ui = ui
286 303
287 304 def write(self, data):
288 305 self._ui.write_err(data)
289 306
290 307 def flush(self):
291 308 self._ui.flush()
292 309
293 310 self._fpdoclose = False
294 311 self._fp = uifp(self._ui)
295 312 else:
296 313 self._fpdoclose = False
297 314 self._fp = self._ui.ferr
298 315 # Ensure we've flushed fout before writing to ferr.
299 316 self._flushfp = self._ui.fout
300 317
301 318 if proffn is not None:
302 319 pass
303 320 elif profiler == b'ls':
304 321 proffn = lsprofile
305 322 elif profiler == b'flame':
306 323 proffn = flameprofile
307 324 elif profiler == b'py-spy':
308 325 proffn = pyspy_profile
309 326 else:
310 327 proffn = statprofile
311 328
312 329 self._profiler = proffn(self._ui, self._fp)
313 330 self._profiler.__enter__()
314 331 except: # re-raises
315 332 self._closefp()
316 333 raise
317 334
318 335 def __exit__(self, exception_type, exception_value, traceback):
319 336 propagate = None
320 337 if self._profiler is not None:
321 338 self._uiflush()
322 339 propagate = self._profiler.__exit__(
323 340 exception_type, exception_value, traceback
324 341 )
325 342 if self._output == b'blackbox':
326 343 val = b'Profile:\n%s' % self._fp.getvalue()
327 344 # ui.log treats the input as a format string,
328 345 # so we need to escape any % signs.
329 346 val = val.replace(b'%', b'%%')
330 347 self._ui.log(b'profile', val)
331 348 self._closefp()
332 349 return propagate
333 350
334 351 def _closefp(self):
335 352 if self._fpdoclose and self._fp is not None:
336 353 self._fp.close()
337 354
338 355 def _uiflush(self):
339 356 if self._flushfp:
340 357 self._flushfp.flush()
@@ -1,180 +1,194
1 1 test --time
2 2
3 3 $ hg --time help -q help 2>&1 | grep time > /dev/null
4 4 $ hg init a
5 5 $ cd a
6 6
7 7 Function to check that statprof ran
8 8 $ statprofran () {
9 9 > grep -E 'Sample count:|No samples recorded' > /dev/null
10 10 > }
11 11
12 12 test --profile
13 13
14 14 $ hg st --profile 2>&1 | statprofran
15 15
16 16 Abreviated version
17 17
18 18 $ hg st --prof 2>&1 | statprofran
19 19
20 20 In alias
21 21
22 22 $ hg --config "alias.profst=status --profile" profst 2>&1 | statprofran
23 23
24 24 #if lsprof
25 25
26 26 $ prof () {
27 27 > hg --config profiling.type=ls --profile $@
28 28 > }
29 29
30 30 $ prof st 2>../out
31 31 $ grep CallCount ../out > /dev/null || cat ../out
32 32
33 33 $ prof --config profiling.output=../out st
34 34 $ grep CallCount ../out > /dev/null || cat ../out
35 35
36 36 $ prof --config profiling.output=blackbox --config extensions.blackbox= st
37 37 $ grep CallCount .hg/blackbox.log > /dev/null || cat .hg/blackbox.log
38 38
39 39 $ prof --config profiling.format=text st 2>../out
40 40 $ grep CallCount ../out > /dev/null || cat ../out
41 41
42 42 $ echo "[profiling]" >> $HGRCPATH
43 43 $ echo "format=kcachegrind" >> $HGRCPATH
44 44
45 45 $ prof st 2>../out
46 46 $ grep 'events: Ticks' ../out > /dev/null || cat ../out
47 47
48 48 $ prof --config profiling.output=../out st
49 49 $ grep 'events: Ticks' ../out > /dev/null || cat ../out
50 50
51 51 #endif
52 52
53 #if lsprof serve
53 #if serve
54 54
55 55 Profiling of HTTP requests works
56 56
57 $ prof --config profiling.format=text --config profiling.output=../profile.log serve -d -p $HGPORT --pid-file ../hg.pid -A ../access.log
57 $ stats_prof () {
58 > hg --config profiling.type=stat --profile $@
59 > }
60
61 $ stats_prof \
62 > --config profiling.format=text \
63 > --config profiling.output=../profile.log \
64 > serve -d \
65 > -p $HGPORT \
66 > --pid-file ../hg.pid \
67 > -A ../access.log \
68 > --errorlog ../error.log
58 69 $ cat ../hg.pid >> $DAEMON_PIDS
59 70 $ hg -q clone -U http://localhost:$HGPORT ../clone
71 $ cat ../error.log
60 72
61 73 A single profile is logged because file logging doesn't append
62 $ grep CallCount ../profile.log | wc -l
74 $ grep 'Sample count:' ../profile.log | wc -l
75 \s*1 (re)
76 $ grep 'Total time:' ../profile.log | wc -l
63 77 \s*1 (re)
64 78
65 79 #endif
66 80
67 81 Install an extension that can sleep and guarantee a profiler has time to run
68 82
69 83 $ cat >> sleepext_with_a_long_filename.py << EOF
70 84 > import time
71 85 > from mercurial import registrar
72 86 > cmdtable = {}
73 87 > command = registrar.command(cmdtable)
74 88 > @command(b'sleep', [], b'hg sleep')
75 89 > def sleep_for_at_least_one_stat_cycle(ui, *args, **kwargs):
76 90 > t = time.time() # don't use time.sleep because we need CPU time
77 91 > while time.time() - t < 0.5:
78 92 > pass
79 93 > EOF
80 94
81 95 $ cat >> $HGRCPATH << EOF
82 96 > [extensions]
83 97 > sleep = `pwd`/sleepext_with_a_long_filename.py
84 98 > EOF
85 99
86 100 statistical profiler works
87 101
88 102 $ hg --profile sleep 2>../out
89 103 $ cat ../out | statprofran
90 104
91 105 Various statprof formatters work
92 106
93 107 $ hg --profile --config profiling.statformat=byline sleep 2>../out || cat ../out
94 108 $ grep -v _path_stat ../out | head -n 3
95 109 % cumulative self
96 110 time seconds seconds name
97 111 * sleepext_with_a_long_filename.py:*:sleep_for_at_least_one_stat_cycle (glob)
98 112 $ cat ../out | statprofran
99 113
100 114 $ hg --profile --config profiling.statformat=bymethod sleep 2>../out || cat ../out
101 115 $ head -n 1 ../out
102 116 % cumulative self
103 117 $ cat ../out | statprofran
104 118
105 119 Windows real time tracking is broken, only use CPU
106 120
107 121 #if no-windows
108 122 $ hg --profile --config profiling.time-track=real --config profiling.statformat=hotpath sleep 2>../out || cat ../out
109 123 $ cat ../out | statprofran
110 124 $ grep sleepext_with_a_long_filename.py ../out | head -n 1
111 125 .* [0-9.]+% [0-9.]+s sleepext_with_a_long_filename.py:\s*sleep_for_at_least_one_stat_cycle, line \d+:\s+(while|pass).* (re)
112 126 #endif
113 127
114 128 $ hg --profile --config profiling.time-track=cpu --config profiling.statformat=hotpath sleep 2>../out || cat ../out
115 129 $ cat ../out | statprofran
116 130 $ grep sleepext_with_a_long_filename.py ../out | head -n 1
117 131 .* [0-9.]+% [0-9.]+s sleepext_with_a_long_filename.py:\s*sleep_for_at_least_one_stat_cycle, line \d+:\s+(while|pass).* (re)
118 132
119 133 $ hg --profile --config profiling.statformat=json sleep 2>../out || cat ../out
120 134 $ cat ../out
121 135 \[\[-?\d+.* (re)
122 136
123 137 statprof can be used as a standalone module
124 138
125 139 $ "$PYTHON" -m mercurial.statprof hotpath
126 140 must specify --file to load
127 141 [1]
128 142
129 143 $ cd ..
130 144
131 145 #if no-chg
132 146 profiler extension could be loaded before other extensions
133 147
134 148 $ cat > fooprof.py <<EOF
135 149 > import contextlib
136 150 > import sys
137 151 > @contextlib.contextmanager
138 152 > def profile(ui, fp):
139 153 > print('fooprof: start profile')
140 154 > sys.stdout.flush()
141 155 > yield
142 156 > print('fooprof: end profile')
143 157 > sys.stdout.flush()
144 158 > def extsetup(ui):
145 159 > ui.write(b'fooprof: loaded\n')
146 160 > EOF
147 161
148 162 $ cat > otherextension.py <<EOF
149 163 > def extsetup(ui):
150 164 > ui.write(b'otherextension: loaded\n')
151 165 > EOF
152 166
153 167 $ hg init b
154 168 $ cd b
155 169 $ cat >> .hg/hgrc <<EOF
156 170 > [extensions]
157 171 > other = $TESTTMP/otherextension.py
158 172 > fooprof = $TESTTMP/fooprof.py
159 173 > EOF
160 174
161 175 $ hg root
162 176 otherextension: loaded
163 177 fooprof: loaded
164 178 $TESTTMP/b
165 179 $ HGPROF=fooprof hg root --profile
166 180 fooprof: loaded
167 181 fooprof: start profile
168 182 otherextension: loaded
169 183 $TESTTMP/b
170 184 fooprof: end profile
171 185
172 186 $ HGPROF=other hg root --profile 2>&1 | head -n 2
173 187 otherextension: loaded
174 188 unrecognized profiler 'other' - ignored
175 189
176 190 $ HGPROF=unknown hg root --profile 2>&1 | head -n 1
177 191 unrecognized profiler 'unknown' - ignored
178 192
179 193 $ cd ..
180 194 #endif
General Comments 0
You need to be logged in to leave comments. Login now