##// END OF EJS Templates
statprof: update the name as the i increases (issue6003)...
Pulkit Goyal -
r40417:4613f927 default
parent child Browse files
Show More
@@ -1,966 +1,967 b''
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 threading
115 import threading
116 import time
116 import time
117
117
118 from . import (
118 from . import (
119 encoding,
119 encoding,
120 pycompat,
120 pycompat,
121 )
121 )
122
122
123 defaultdict = collections.defaultdict
123 defaultdict = collections.defaultdict
124 contextmanager = contextlib.contextmanager
124 contextmanager = contextlib.contextmanager
125
125
126 __all__ = ['start', 'stop', 'reset', 'display', 'profile']
126 __all__ = ['start', 'stop', 'reset', 'display', 'profile']
127
127
128 skips = {
128 skips = {
129 r"util.py:check",
129 r"util.py:check",
130 r"extensions.py:closure",
130 r"extensions.py:closure",
131 r"color.py:colorcmd",
131 r"color.py:colorcmd",
132 r"dispatch.py:checkargs",
132 r"dispatch.py:checkargs",
133 r"dispatch.py:<lambda>",
133 r"dispatch.py:<lambda>",
134 r"dispatch.py:_runcatch",
134 r"dispatch.py:_runcatch",
135 r"dispatch.py:_dispatch",
135 r"dispatch.py:_dispatch",
136 r"dispatch.py:_runcommand",
136 r"dispatch.py:_runcommand",
137 r"pager.py:pagecmd",
137 r"pager.py:pagecmd",
138 r"dispatch.py:run",
138 r"dispatch.py:run",
139 r"dispatch.py:dispatch",
139 r"dispatch.py:dispatch",
140 r"dispatch.py:runcommand",
140 r"dispatch.py:runcommand",
141 r"hg.py:<module>",
141 r"hg.py:<module>",
142 r"evolve.py:warnobserrors",
142 r"evolve.py:warnobserrors",
143 }
143 }
144
144
145 ###########################################################################
145 ###########################################################################
146 ## Utils
146 ## Utils
147
147
148 def clock():
148 def clock():
149 times = os.times()
149 times = os.times()
150 return (times[0] + times[1], times[4])
150 return (times[0] + times[1], times[4])
151
151
152
152
153 ###########################################################################
153 ###########################################################################
154 ## Collection data structures
154 ## Collection data structures
155
155
156 class ProfileState(object):
156 class ProfileState(object):
157 def __init__(self, frequency=None):
157 def __init__(self, frequency=None):
158 self.reset(frequency)
158 self.reset(frequency)
159 self.track = 'cpu'
159 self.track = 'cpu'
160
160
161 def reset(self, frequency=None):
161 def reset(self, frequency=None):
162 # total so far
162 # total so far
163 self.accumulated_time = (0.0, 0.0)
163 self.accumulated_time = (0.0, 0.0)
164 # start_time when timer is active
164 # start_time when timer is active
165 self.last_start_time = None
165 self.last_start_time = None
166 # a float
166 # a float
167 if frequency:
167 if frequency:
168 self.sample_interval = 1.0 / frequency
168 self.sample_interval = 1.0 / frequency
169 elif not hasattr(self, 'sample_interval'):
169 elif not hasattr(self, 'sample_interval'):
170 # default to 1000 Hz
170 # default to 1000 Hz
171 self.sample_interval = 1.0 / 1000.0
171 self.sample_interval = 1.0 / 1000.0
172 else:
172 else:
173 # leave the frequency as it was
173 # leave the frequency as it was
174 pass
174 pass
175 self.remaining_prof_time = None
175 self.remaining_prof_time = None
176 # for user start/stop nesting
176 # for user start/stop nesting
177 self.profile_level = 0
177 self.profile_level = 0
178
178
179 self.samples = []
179 self.samples = []
180
180
181 def accumulate_time(self, stop_time):
181 def accumulate_time(self, stop_time):
182 increment = (
182 increment = (
183 stop_time[0] - self.last_start_time[0],
183 stop_time[0] - self.last_start_time[0],
184 stop_time[1] - self.last_start_time[1],
184 stop_time[1] - self.last_start_time[1],
185 )
185 )
186 self.accumulated_time = (
186 self.accumulated_time = (
187 self.accumulated_time[0] + increment[0],
187 self.accumulated_time[0] + increment[0],
188 self.accumulated_time[1] + increment[1],
188 self.accumulated_time[1] + increment[1],
189 )
189 )
190
190
191 def seconds_per_sample(self):
191 def seconds_per_sample(self):
192 return self.accumulated_time[self.timeidx] / len(self.samples)
192 return self.accumulated_time[self.timeidx] / len(self.samples)
193
193
194 @property
194 @property
195 def timeidx(self):
195 def timeidx(self):
196 if self.track == 'real':
196 if self.track == 'real':
197 return 1
197 return 1
198 return 0
198 return 0
199
199
200 state = ProfileState()
200 state = ProfileState()
201
201
202
202
203 class CodeSite(object):
203 class CodeSite(object):
204 cache = {}
204 cache = {}
205
205
206 __slots__ = (u'path', u'lineno', u'function', u'source')
206 __slots__ = (u'path', u'lineno', u'function', u'source')
207
207
208 def __init__(self, path, lineno, function):
208 def __init__(self, path, lineno, function):
209 self.path = path
209 self.path = path
210 self.lineno = lineno
210 self.lineno = lineno
211 self.function = function
211 self.function = function
212 self.source = None
212 self.source = None
213
213
214 def __eq__(self, other):
214 def __eq__(self, other):
215 try:
215 try:
216 return (self.lineno == other.lineno and
216 return (self.lineno == other.lineno and
217 self.path == other.path)
217 self.path == other.path)
218 except:
218 except:
219 return False
219 return False
220
220
221 def __hash__(self):
221 def __hash__(self):
222 return hash((self.lineno, self.path))
222 return hash((self.lineno, self.path))
223
223
224 @classmethod
224 @classmethod
225 def get(cls, path, lineno, function):
225 def get(cls, path, lineno, function):
226 k = (path, lineno)
226 k = (path, lineno)
227 try:
227 try:
228 return cls.cache[k]
228 return cls.cache[k]
229 except KeyError:
229 except KeyError:
230 v = cls(path, lineno, function)
230 v = cls(path, lineno, function)
231 cls.cache[k] = v
231 cls.cache[k] = v
232 return v
232 return v
233
233
234 def getsource(self, length):
234 def getsource(self, length):
235 if self.source is None:
235 if self.source is None:
236 lineno = self.lineno - 1
236 lineno = self.lineno - 1
237 fp = None
237 fp = None
238 try:
238 try:
239 fp = open(self.path)
239 fp = open(self.path)
240 for i, line in enumerate(fp):
240 for i, line in enumerate(fp):
241 if i == lineno:
241 if i == lineno:
242 self.source = line.strip()
242 self.source = line.strip()
243 break
243 break
244 except:
244 except:
245 pass
245 pass
246 finally:
246 finally:
247 if fp:
247 if fp:
248 fp.close()
248 fp.close()
249 if self.source is None:
249 if self.source is None:
250 self.source = ''
250 self.source = ''
251
251
252 source = self.source
252 source = self.source
253 if len(source) > length:
253 if len(source) > length:
254 source = source[:(length - 3)] + "..."
254 source = source[:(length - 3)] + "..."
255 return source
255 return source
256
256
257 def filename(self):
257 def filename(self):
258 return os.path.basename(self.path)
258 return os.path.basename(self.path)
259
259
260 class Sample(object):
260 class Sample(object):
261 __slots__ = (u'stack', u'time')
261 __slots__ = (u'stack', u'time')
262
262
263 def __init__(self, stack, time):
263 def __init__(self, stack, time):
264 self.stack = stack
264 self.stack = stack
265 self.time = time
265 self.time = time
266
266
267 @classmethod
267 @classmethod
268 def from_frame(cls, frame, time):
268 def from_frame(cls, frame, time):
269 stack = []
269 stack = []
270
270
271 while frame:
271 while frame:
272 stack.append(CodeSite.get(frame.f_code.co_filename, frame.f_lineno,
272 stack.append(CodeSite.get(frame.f_code.co_filename, frame.f_lineno,
273 frame.f_code.co_name))
273 frame.f_code.co_name))
274 frame = frame.f_back
274 frame = frame.f_back
275
275
276 return Sample(stack, time)
276 return Sample(stack, time)
277
277
278 ###########################################################################
278 ###########################################################################
279 ## SIGPROF handler
279 ## SIGPROF handler
280
280
281 def profile_signal_handler(signum, frame):
281 def profile_signal_handler(signum, frame):
282 if state.profile_level > 0:
282 if state.profile_level > 0:
283 now = clock()
283 now = clock()
284 state.accumulate_time(now)
284 state.accumulate_time(now)
285
285
286 timestamp = state.accumulated_time[state.timeidx]
286 timestamp = state.accumulated_time[state.timeidx]
287 state.samples.append(Sample.from_frame(frame, timestamp))
287 state.samples.append(Sample.from_frame(frame, timestamp))
288
288
289 signal.setitimer(signal.ITIMER_PROF,
289 signal.setitimer(signal.ITIMER_PROF,
290 state.sample_interval, 0.0)
290 state.sample_interval, 0.0)
291 state.last_start_time = now
291 state.last_start_time = now
292
292
293 stopthread = threading.Event()
293 stopthread = threading.Event()
294 def samplerthread(tid):
294 def samplerthread(tid):
295 while not stopthread.is_set():
295 while not stopthread.is_set():
296 now = clock()
296 now = clock()
297 state.accumulate_time(now)
297 state.accumulate_time(now)
298
298
299 frame = sys._current_frames()[tid]
299 frame = sys._current_frames()[tid]
300
300
301 timestamp = state.accumulated_time[state.timeidx]
301 timestamp = state.accumulated_time[state.timeidx]
302 state.samples.append(Sample.from_frame(frame, timestamp))
302 state.samples.append(Sample.from_frame(frame, timestamp))
303
303
304 state.last_start_time = now
304 state.last_start_time = now
305 time.sleep(state.sample_interval)
305 time.sleep(state.sample_interval)
306
306
307 stopthread.clear()
307 stopthread.clear()
308
308
309 ###########################################################################
309 ###########################################################################
310 ## Profiling API
310 ## Profiling API
311
311
312 def is_active():
312 def is_active():
313 return state.profile_level > 0
313 return state.profile_level > 0
314
314
315 lastmechanism = None
315 lastmechanism = None
316 def start(mechanism='thread', track='cpu'):
316 def start(mechanism='thread', track='cpu'):
317 '''Install the profiling signal handler, and start profiling.'''
317 '''Install the profiling signal handler, and start profiling.'''
318 state.track = track # note: nesting different mode won't work
318 state.track = track # note: nesting different mode won't work
319 state.profile_level += 1
319 state.profile_level += 1
320 if state.profile_level == 1:
320 if state.profile_level == 1:
321 state.last_start_time = clock()
321 state.last_start_time = clock()
322 rpt = state.remaining_prof_time
322 rpt = state.remaining_prof_time
323 state.remaining_prof_time = None
323 state.remaining_prof_time = None
324
324
325 global lastmechanism
325 global lastmechanism
326 lastmechanism = mechanism
326 lastmechanism = mechanism
327
327
328 if mechanism == 'signal':
328 if mechanism == 'signal':
329 signal.signal(signal.SIGPROF, profile_signal_handler)
329 signal.signal(signal.SIGPROF, profile_signal_handler)
330 signal.setitimer(signal.ITIMER_PROF,
330 signal.setitimer(signal.ITIMER_PROF,
331 rpt or state.sample_interval, 0.0)
331 rpt or state.sample_interval, 0.0)
332 elif mechanism == 'thread':
332 elif mechanism == 'thread':
333 frame = inspect.currentframe()
333 frame = inspect.currentframe()
334 tid = [k for k, f in sys._current_frames().items() if f == frame][0]
334 tid = [k for k, f in sys._current_frames().items() if f == frame][0]
335 state.thread = threading.Thread(target=samplerthread,
335 state.thread = threading.Thread(target=samplerthread,
336 args=(tid,), name="samplerthread")
336 args=(tid,), name="samplerthread")
337 state.thread.start()
337 state.thread.start()
338
338
339 def stop():
339 def stop():
340 '''Stop profiling, and uninstall the profiling signal handler.'''
340 '''Stop profiling, and uninstall the profiling signal handler.'''
341 state.profile_level -= 1
341 state.profile_level -= 1
342 if state.profile_level == 0:
342 if state.profile_level == 0:
343 if lastmechanism == 'signal':
343 if lastmechanism == 'signal':
344 rpt = signal.setitimer(signal.ITIMER_PROF, 0.0, 0.0)
344 rpt = signal.setitimer(signal.ITIMER_PROF, 0.0, 0.0)
345 signal.signal(signal.SIGPROF, signal.SIG_IGN)
345 signal.signal(signal.SIGPROF, signal.SIG_IGN)
346 state.remaining_prof_time = rpt[0]
346 state.remaining_prof_time = rpt[0]
347 elif lastmechanism == 'thread':
347 elif lastmechanism == 'thread':
348 stopthread.set()
348 stopthread.set()
349 state.thread.join()
349 state.thread.join()
350
350
351 state.accumulate_time(clock())
351 state.accumulate_time(clock())
352 state.last_start_time = None
352 state.last_start_time = None
353 statprofpath = encoding.environ.get('STATPROF_DEST')
353 statprofpath = encoding.environ.get('STATPROF_DEST')
354 if statprofpath:
354 if statprofpath:
355 save_data(statprofpath)
355 save_data(statprofpath)
356
356
357 return state
357 return state
358
358
359 def save_data(path):
359 def save_data(path):
360 with open(path, 'w+') as file:
360 with open(path, 'w+') as file:
361 file.write("%f %f\n" % state.accumulated_time)
361 file.write("%f %f\n" % state.accumulated_time)
362 for sample in state.samples:
362 for sample in state.samples:
363 time = sample.time
363 time = sample.time
364 stack = sample.stack
364 stack = sample.stack
365 sites = ['\1'.join([s.path, b'%d' % s.lineno, s.function])
365 sites = ['\1'.join([s.path, b'%d' % s.lineno, s.function])
366 for s in stack]
366 for s in stack]
367 file.write("%d\0%s\n" % (time, '\0'.join(sites)))
367 file.write("%d\0%s\n" % (time, '\0'.join(sites)))
368
368
369 def load_data(path):
369 def load_data(path):
370 lines = open(path, 'r').read().splitlines()
370 lines = open(path, 'r').read().splitlines()
371
371
372 state.accumulated_time = [float(value) for value in lines[0].split()]
372 state.accumulated_time = [float(value) for value in lines[0].split()]
373 state.samples = []
373 state.samples = []
374 for line in lines[1:]:
374 for line in lines[1:]:
375 parts = line.split('\0')
375 parts = line.split('\0')
376 time = float(parts[0])
376 time = float(parts[0])
377 rawsites = parts[1:]
377 rawsites = parts[1:]
378 sites = []
378 sites = []
379 for rawsite in rawsites:
379 for rawsite in rawsites:
380 siteparts = rawsite.split('\1')
380 siteparts = rawsite.split('\1')
381 sites.append(CodeSite.get(siteparts[0], int(siteparts[1]),
381 sites.append(CodeSite.get(siteparts[0], int(siteparts[1]),
382 siteparts[2]))
382 siteparts[2]))
383
383
384 state.samples.append(Sample(sites, time))
384 state.samples.append(Sample(sites, time))
385
385
386
386
387
387
388 def reset(frequency=None):
388 def reset(frequency=None):
389 '''Clear out the state of the profiler. Do not call while the
389 '''Clear out the state of the profiler. Do not call while the
390 profiler is running.
390 profiler is running.
391
391
392 The optional frequency argument specifies the number of samples to
392 The optional frequency argument specifies the number of samples to
393 collect per second.'''
393 collect per second.'''
394 assert state.profile_level == 0, "Can't reset() while statprof is running"
394 assert state.profile_level == 0, "Can't reset() while statprof is running"
395 CodeSite.cache.clear()
395 CodeSite.cache.clear()
396 state.reset(frequency)
396 state.reset(frequency)
397
397
398
398
399 @contextmanager
399 @contextmanager
400 def profile():
400 def profile():
401 start()
401 start()
402 try:
402 try:
403 yield
403 yield
404 finally:
404 finally:
405 stop()
405 stop()
406 display()
406 display()
407
407
408
408
409 ###########################################################################
409 ###########################################################################
410 ## Reporting API
410 ## Reporting API
411
411
412 class SiteStats(object):
412 class SiteStats(object):
413 def __init__(self, site):
413 def __init__(self, site):
414 self.site = site
414 self.site = site
415 self.selfcount = 0
415 self.selfcount = 0
416 self.totalcount = 0
416 self.totalcount = 0
417
417
418 def addself(self):
418 def addself(self):
419 self.selfcount += 1
419 self.selfcount += 1
420
420
421 def addtotal(self):
421 def addtotal(self):
422 self.totalcount += 1
422 self.totalcount += 1
423
423
424 def selfpercent(self):
424 def selfpercent(self):
425 return self.selfcount / len(state.samples) * 100
425 return self.selfcount / len(state.samples) * 100
426
426
427 def totalpercent(self):
427 def totalpercent(self):
428 return self.totalcount / len(state.samples) * 100
428 return self.totalcount / len(state.samples) * 100
429
429
430 def selfseconds(self):
430 def selfseconds(self):
431 return self.selfcount * state.seconds_per_sample()
431 return self.selfcount * state.seconds_per_sample()
432
432
433 def totalseconds(self):
433 def totalseconds(self):
434 return self.totalcount * state.seconds_per_sample()
434 return self.totalcount * state.seconds_per_sample()
435
435
436 @classmethod
436 @classmethod
437 def buildstats(cls, samples):
437 def buildstats(cls, samples):
438 stats = {}
438 stats = {}
439
439
440 for sample in samples:
440 for sample in samples:
441 for i, site in enumerate(sample.stack):
441 for i, site in enumerate(sample.stack):
442 sitestat = stats.get(site)
442 sitestat = stats.get(site)
443 if not sitestat:
443 if not sitestat:
444 sitestat = SiteStats(site)
444 sitestat = SiteStats(site)
445 stats[site] = sitestat
445 stats[site] = sitestat
446
446
447 sitestat.addtotal()
447 sitestat.addtotal()
448
448
449 if i == 0:
449 if i == 0:
450 sitestat.addself()
450 sitestat.addself()
451
451
452 return [s for s in stats.itervalues()]
452 return [s for s in stats.itervalues()]
453
453
454 class DisplayFormats:
454 class DisplayFormats:
455 ByLine = 0
455 ByLine = 0
456 ByMethod = 1
456 ByMethod = 1
457 AboutMethod = 2
457 AboutMethod = 2
458 Hotpath = 3
458 Hotpath = 3
459 FlameGraph = 4
459 FlameGraph = 4
460 Json = 5
460 Json = 5
461 Chrome = 6
461 Chrome = 6
462
462
463 def display(fp=None, format=3, data=None, **kwargs):
463 def display(fp=None, format=3, data=None, **kwargs):
464 '''Print statistics, either to stdout or the given file object.'''
464 '''Print statistics, either to stdout or the given file object.'''
465 if data is None:
465 if data is None:
466 data = state
466 data = state
467
467
468 if fp is None:
468 if fp is None:
469 import sys
469 import sys
470 fp = sys.stdout
470 fp = sys.stdout
471 if len(data.samples) == 0:
471 if len(data.samples) == 0:
472 fp.write(b'No samples recorded.\n')
472 fp.write(b'No samples recorded.\n')
473 return
473 return
474
474
475 if format == DisplayFormats.ByLine:
475 if format == DisplayFormats.ByLine:
476 display_by_line(data, fp)
476 display_by_line(data, fp)
477 elif format == DisplayFormats.ByMethod:
477 elif format == DisplayFormats.ByMethod:
478 display_by_method(data, fp)
478 display_by_method(data, fp)
479 elif format == DisplayFormats.AboutMethod:
479 elif format == DisplayFormats.AboutMethod:
480 display_about_method(data, fp, **kwargs)
480 display_about_method(data, fp, **kwargs)
481 elif format == DisplayFormats.Hotpath:
481 elif format == DisplayFormats.Hotpath:
482 display_hotpath(data, fp, **kwargs)
482 display_hotpath(data, fp, **kwargs)
483 elif format == DisplayFormats.FlameGraph:
483 elif format == DisplayFormats.FlameGraph:
484 write_to_flame(data, fp, **kwargs)
484 write_to_flame(data, fp, **kwargs)
485 elif format == DisplayFormats.Json:
485 elif format == DisplayFormats.Json:
486 write_to_json(data, fp)
486 write_to_json(data, fp)
487 elif format == DisplayFormats.Chrome:
487 elif format == DisplayFormats.Chrome:
488 write_to_chrome(data, fp, **kwargs)
488 write_to_chrome(data, fp, **kwargs)
489 else:
489 else:
490 raise Exception("Invalid display format")
490 raise Exception("Invalid display format")
491
491
492 if format not in (DisplayFormats.Json, DisplayFormats.Chrome):
492 if format not in (DisplayFormats.Json, DisplayFormats.Chrome):
493 fp.write(b'---\n')
493 fp.write(b'---\n')
494 fp.write(b'Sample count: %d\n' % len(data.samples))
494 fp.write(b'Sample count: %d\n' % len(data.samples))
495 fp.write(b'Total time: %f seconds (%f wall)\n' % data.accumulated_time)
495 fp.write(b'Total time: %f seconds (%f wall)\n' % data.accumulated_time)
496
496
497 def display_by_line(data, fp):
497 def display_by_line(data, fp):
498 '''Print the profiler data with each sample line represented
498 '''Print the profiler data with each sample line represented
499 as one row in a table. Sorted by self-time per line.'''
499 as one row in a table. Sorted by self-time per line.'''
500 stats = SiteStats.buildstats(data.samples)
500 stats = SiteStats.buildstats(data.samples)
501 stats.sort(reverse=True, key=lambda x: x.selfseconds())
501 stats.sort(reverse=True, key=lambda x: x.selfseconds())
502
502
503 fp.write(b'%5.5s %10.10s %7.7s %-8.8s\n' % (
503 fp.write(b'%5.5s %10.10s %7.7s %-8.8s\n' % (
504 b'% ', b'cumulative', b'self', b''))
504 b'% ', b'cumulative', b'self', b''))
505 fp.write(b'%5.5s %9.9s %8.8s %-8.8s\n' % (
505 fp.write(b'%5.5s %9.9s %8.8s %-8.8s\n' % (
506 b"time", b"seconds", b"seconds", b"name"))
506 b"time", b"seconds", b"seconds", b"name"))
507
507
508 for stat in stats:
508 for stat in stats:
509 site = stat.site
509 site = stat.site
510 sitelabel = '%s:%d:%s' % (pycompat.fsencode(site.filename()),
510 sitelabel = '%s:%d:%s' % (pycompat.fsencode(site.filename()),
511 site.lineno,
511 site.lineno,
512 pycompat.sysbytes(site.function))
512 pycompat.sysbytes(site.function))
513 fp.write(b'%6.2f %9.2f %9.2f %s\n' % (
513 fp.write(b'%6.2f %9.2f %9.2f %s\n' % (
514 stat.selfpercent(), stat.totalseconds(),
514 stat.selfpercent(), stat.totalseconds(),
515 stat.selfseconds(), sitelabel))
515 stat.selfseconds(), sitelabel))
516
516
517 def display_by_method(data, fp):
517 def display_by_method(data, fp):
518 '''Print the profiler data with each sample function represented
518 '''Print the profiler data with each sample function represented
519 as one row in a table. Important lines within that function are
519 as one row in a table. Important lines within that function are
520 output as nested rows. Sorted by self-time per line.'''
520 output as nested rows. Sorted by self-time per line.'''
521 fp.write(b'%5.5s %10.10s %7.7s %-8.8s\n' %
521 fp.write(b'%5.5s %10.10s %7.7s %-8.8s\n' %
522 ('% ', 'cumulative', 'self', ''))
522 ('% ', 'cumulative', 'self', ''))
523 fp.write(b'%5.5s %9.9s %8.8s %-8.8s\n' %
523 fp.write(b'%5.5s %9.9s %8.8s %-8.8s\n' %
524 ("time", "seconds", "seconds", "name"))
524 ("time", "seconds", "seconds", "name"))
525
525
526 stats = SiteStats.buildstats(data.samples)
526 stats = SiteStats.buildstats(data.samples)
527
527
528 grouped = defaultdict(list)
528 grouped = defaultdict(list)
529 for stat in stats:
529 for stat in stats:
530 grouped[stat.site.filename() + r":" + stat.site.function].append(stat)
530 grouped[stat.site.filename() + r":" + stat.site.function].append(stat)
531
531
532 # compute sums for each function
532 # compute sums for each function
533 functiondata = []
533 functiondata = []
534 for fname, sitestats in grouped.iteritems():
534 for fname, sitestats in grouped.iteritems():
535 total_cum_sec = 0
535 total_cum_sec = 0
536 total_self_sec = 0
536 total_self_sec = 0
537 total_percent = 0
537 total_percent = 0
538 for stat in sitestats:
538 for stat in sitestats:
539 total_cum_sec += stat.totalseconds()
539 total_cum_sec += stat.totalseconds()
540 total_self_sec += stat.selfseconds()
540 total_self_sec += stat.selfseconds()
541 total_percent += stat.selfpercent()
541 total_percent += stat.selfpercent()
542
542
543 functiondata.append((fname,
543 functiondata.append((fname,
544 total_cum_sec,
544 total_cum_sec,
545 total_self_sec,
545 total_self_sec,
546 total_percent,
546 total_percent,
547 sitestats))
547 sitestats))
548
548
549 # sort by total self sec
549 # sort by total self sec
550 functiondata.sort(reverse=True, key=lambda x: x[2])
550 functiondata.sort(reverse=True, key=lambda x: x[2])
551
551
552 for function in functiondata:
552 for function in functiondata:
553 if function[3] < 0.05:
553 if function[3] < 0.05:
554 continue
554 continue
555 fp.write(b'%6.2f %9.2f %9.2f %s\n' % (
555 fp.write(b'%6.2f %9.2f %9.2f %s\n' % (
556 function[3], # total percent
556 function[3], # total percent
557 function[1], # total cum sec
557 function[1], # total cum sec
558 function[2], # total self sec
558 function[2], # total self sec
559 pycompat.sysbytes(function[0]))) # file:function
559 pycompat.sysbytes(function[0]))) # file:function
560
560
561 function[4].sort(reverse=True, key=lambda i: i.selfseconds())
561 function[4].sort(reverse=True, key=lambda i: i.selfseconds())
562 for stat in function[4]:
562 for stat in function[4]:
563 # only show line numbers for significant locations (>1% time spent)
563 # only show line numbers for significant locations (>1% time spent)
564 if stat.selfpercent() > 1:
564 if stat.selfpercent() > 1:
565 source = stat.site.getsource(25)
565 source = stat.site.getsource(25)
566 if sys.version_info.major >= 3 and not isinstance(source, bytes):
566 if sys.version_info.major >= 3 and not isinstance(source, bytes):
567 source = pycompat.bytestr(source)
567 source = pycompat.bytestr(source)
568
568
569 stattuple = (stat.selfpercent(), stat.selfseconds(),
569 stattuple = (stat.selfpercent(), stat.selfseconds(),
570 stat.site.lineno, source)
570 stat.site.lineno, source)
571
571
572 fp.write(b'%33.0f%% %6.2f line %d: %s\n' % stattuple)
572 fp.write(b'%33.0f%% %6.2f line %d: %s\n' % stattuple)
573
573
574 def display_about_method(data, fp, function=None, **kwargs):
574 def display_about_method(data, fp, function=None, **kwargs):
575 if function is None:
575 if function is None:
576 raise Exception("Invalid function")
576 raise Exception("Invalid function")
577
577
578 filename = None
578 filename = None
579 if ':' in function:
579 if ':' in function:
580 filename, function = function.split(':')
580 filename, function = function.split(':')
581
581
582 relevant_samples = 0
582 relevant_samples = 0
583 parents = {}
583 parents = {}
584 children = {}
584 children = {}
585
585
586 for sample in data.samples:
586 for sample in data.samples:
587 for i, site in enumerate(sample.stack):
587 for i, site in enumerate(sample.stack):
588 if site.function == function and (not filename
588 if site.function == function and (not filename
589 or site.filename() == filename):
589 or site.filename() == filename):
590 relevant_samples += 1
590 relevant_samples += 1
591 if i != len(sample.stack) - 1:
591 if i != len(sample.stack) - 1:
592 parent = sample.stack[i + 1]
592 parent = sample.stack[i + 1]
593 if parent in parents:
593 if parent in parents:
594 parents[parent] = parents[parent] + 1
594 parents[parent] = parents[parent] + 1
595 else:
595 else:
596 parents[parent] = 1
596 parents[parent] = 1
597
597
598 if site in children:
598 if site in children:
599 children[site] = children[site] + 1
599 children[site] = children[site] + 1
600 else:
600 else:
601 children[site] = 1
601 children[site] = 1
602
602
603 parents = [(parent, count) for parent, count in parents.iteritems()]
603 parents = [(parent, count) for parent, count in parents.iteritems()]
604 parents.sort(reverse=True, key=lambda x: x[1])
604 parents.sort(reverse=True, key=lambda x: x[1])
605 for parent, count in parents:
605 for parent, count in parents:
606 fp.write(b'%6.2f%% %s:%s line %s: %s\n' %
606 fp.write(b'%6.2f%% %s:%s line %s: %s\n' %
607 (count / relevant_samples * 100,
607 (count / relevant_samples * 100,
608 pycompat.fsencode(parent.filename()),
608 pycompat.fsencode(parent.filename()),
609 pycompat.sysbytes(parent.function),
609 pycompat.sysbytes(parent.function),
610 parent.lineno,
610 parent.lineno,
611 pycompat.sysbytes(parent.getsource(50))))
611 pycompat.sysbytes(parent.getsource(50))))
612
612
613 stats = SiteStats.buildstats(data.samples)
613 stats = SiteStats.buildstats(data.samples)
614 stats = [s for s in stats
614 stats = [s for s in stats
615 if s.site.function == function and
615 if s.site.function == function and
616 (not filename or s.site.filename() == filename)]
616 (not filename or s.site.filename() == filename)]
617
617
618 total_cum_sec = 0
618 total_cum_sec = 0
619 total_self_sec = 0
619 total_self_sec = 0
620 total_self_percent = 0
620 total_self_percent = 0
621 total_cum_percent = 0
621 total_cum_percent = 0
622 for stat in stats:
622 for stat in stats:
623 total_cum_sec += stat.totalseconds()
623 total_cum_sec += stat.totalseconds()
624 total_self_sec += stat.selfseconds()
624 total_self_sec += stat.selfseconds()
625 total_self_percent += stat.selfpercent()
625 total_self_percent += stat.selfpercent()
626 total_cum_percent += stat.totalpercent()
626 total_cum_percent += stat.totalpercent()
627
627
628 fp.write(
628 fp.write(
629 b'\n %s:%s Total: %0.2fs (%0.2f%%) Self: %0.2fs (%0.2f%%)\n\n'
629 b'\n %s:%s Total: %0.2fs (%0.2f%%) Self: %0.2fs (%0.2f%%)\n\n'
630 % (
630 % (
631 pycompat.sysbytes(filename or '___'),
631 pycompat.sysbytes(filename or '___'),
632 pycompat.sysbytes(function),
632 pycompat.sysbytes(function),
633 total_cum_sec,
633 total_cum_sec,
634 total_cum_percent,
634 total_cum_percent,
635 total_self_sec,
635 total_self_sec,
636 total_self_percent
636 total_self_percent
637 ))
637 ))
638
638
639 children = [(child, count) for child, count in children.iteritems()]
639 children = [(child, count) for child, count in children.iteritems()]
640 children.sort(reverse=True, key=lambda x: x[1])
640 children.sort(reverse=True, key=lambda x: x[1])
641 for child, count in children:
641 for child, count in children:
642 fp.write(b' %6.2f%% line %s: %s\n' %
642 fp.write(b' %6.2f%% line %s: %s\n' %
643 (count / relevant_samples * 100, child.lineno,
643 (count / relevant_samples * 100, child.lineno,
644 pycompat.sysbytes(child.getsource(50))))
644 pycompat.sysbytes(child.getsource(50))))
645
645
646 def display_hotpath(data, fp, limit=0.05, **kwargs):
646 def display_hotpath(data, fp, limit=0.05, **kwargs):
647 class HotNode(object):
647 class HotNode(object):
648 def __init__(self, site):
648 def __init__(self, site):
649 self.site = site
649 self.site = site
650 self.count = 0
650 self.count = 0
651 self.children = {}
651 self.children = {}
652
652
653 def add(self, stack, time):
653 def add(self, stack, time):
654 self.count += time
654 self.count += time
655 site = stack[0]
655 site = stack[0]
656 child = self.children.get(site)
656 child = self.children.get(site)
657 if not child:
657 if not child:
658 child = HotNode(site)
658 child = HotNode(site)
659 self.children[site] = child
659 self.children[site] = child
660
660
661 if len(stack) > 1:
661 if len(stack) > 1:
662 i = 1
662 i = 1
663 # Skip boiler plate parts of the stack
663 # Skip boiler plate parts of the stack
664 name = r'%s:%s' % (stack[i].filename(), stack[i].function)
664 name = r'%s:%s' % (stack[i].filename(), stack[i].function)
665 while i < len(stack) and name in skips:
665 while i < len(stack) and name in skips:
666 i += 1
666 i += 1
667 name = r'%s:%s' % (stack[i].filename(), stack[i].function)
667 if i < len(stack):
668 if i < len(stack):
668 child.add(stack[i:], time)
669 child.add(stack[i:], time)
669
670
670 root = HotNode(None)
671 root = HotNode(None)
671 lasttime = data.samples[0].time
672 lasttime = data.samples[0].time
672 for sample in data.samples:
673 for sample in data.samples:
673 root.add(sample.stack[::-1], sample.time - lasttime)
674 root.add(sample.stack[::-1], sample.time - lasttime)
674 lasttime = sample.time
675 lasttime = sample.time
675
676
676 def _write(node, depth, multiple_siblings):
677 def _write(node, depth, multiple_siblings):
677 site = node.site
678 site = node.site
678 visiblechildren = [c for c in node.children.itervalues()
679 visiblechildren = [c for c in node.children.itervalues()
679 if c.count >= (limit * root.count)]
680 if c.count >= (limit * root.count)]
680 if site:
681 if site:
681 indent = depth * 2 - 1
682 indent = depth * 2 - 1
682 filename = ''
683 filename = ''
683 function = ''
684 function = ''
684 if len(node.children) > 0:
685 if len(node.children) > 0:
685 childsite = list(node.children.itervalues())[0].site
686 childsite = list(node.children.itervalues())[0].site
686 filename = (childsite.filename() + ':').ljust(15)
687 filename = (childsite.filename() + ':').ljust(15)
687 function = childsite.function
688 function = childsite.function
688
689
689 # lots of string formatting
690 # lots of string formatting
690 listpattern = ''.ljust(indent) +\
691 listpattern = ''.ljust(indent) +\
691 ('\\' if multiple_siblings else '|') +\
692 ('\\' if multiple_siblings else '|') +\
692 ' %4.1f%% %s %s'
693 ' %4.1f%% %s %s'
693 liststring = listpattern % (node.count / root.count * 100,
694 liststring = listpattern % (node.count / root.count * 100,
694 filename, function)
695 filename, function)
695 codepattern = '%' + str(55 - len(liststring)) + 's %s: %s'
696 codepattern = '%' + str(55 - len(liststring)) + 's %s: %s'
696 codestring = codepattern % ('line', site.lineno, site.getsource(30))
697 codestring = codepattern % ('line', site.lineno, site.getsource(30))
697
698
698 finalstring = liststring + codestring
699 finalstring = liststring + codestring
699 childrensamples = sum([c.count for c in node.children.itervalues()])
700 childrensamples = sum([c.count for c in node.children.itervalues()])
700 # Make frames that performed more than 10% of the operation red
701 # Make frames that performed more than 10% of the operation red
701 if node.count - childrensamples > (0.1 * root.count):
702 if node.count - childrensamples > (0.1 * root.count):
702 finalstring = '\033[91m' + finalstring + '\033[0m'
703 finalstring = '\033[91m' + finalstring + '\033[0m'
703 # Make frames that didn't actually perform work dark grey
704 # Make frames that didn't actually perform work dark grey
704 elif node.count - childrensamples == 0:
705 elif node.count - childrensamples == 0:
705 finalstring = '\033[90m' + finalstring + '\033[0m'
706 finalstring = '\033[90m' + finalstring + '\033[0m'
706 fp.write(finalstring + b'\n')
707 fp.write(finalstring + b'\n')
707
708
708 newdepth = depth
709 newdepth = depth
709 if len(visiblechildren) > 1 or multiple_siblings:
710 if len(visiblechildren) > 1 or multiple_siblings:
710 newdepth += 1
711 newdepth += 1
711
712
712 visiblechildren.sort(reverse=True, key=lambda x: x.count)
713 visiblechildren.sort(reverse=True, key=lambda x: x.count)
713 for child in visiblechildren:
714 for child in visiblechildren:
714 _write(child, newdepth, len(visiblechildren) > 1)
715 _write(child, newdepth, len(visiblechildren) > 1)
715
716
716 if root.count > 0:
717 if root.count > 0:
717 _write(root, 0, False)
718 _write(root, 0, False)
718
719
719 def write_to_flame(data, fp, scriptpath=None, outputfile=None, **kwargs):
720 def write_to_flame(data, fp, scriptpath=None, outputfile=None, **kwargs):
720 if scriptpath is None:
721 if scriptpath is None:
721 scriptpath = encoding.environ['HOME'] + '/flamegraph.pl'
722 scriptpath = encoding.environ['HOME'] + '/flamegraph.pl'
722 if not os.path.exists(scriptpath):
723 if not os.path.exists(scriptpath):
723 fp.write(b'error: missing %s\n' % scriptpath)
724 fp.write(b'error: missing %s\n' % scriptpath)
724 fp.write(b'get it here: https://github.com/brendangregg/FlameGraph\n')
725 fp.write(b'get it here: https://github.com/brendangregg/FlameGraph\n')
725 return
726 return
726
727
727 fd, path = pycompat.mkstemp()
728 fd, path = pycompat.mkstemp()
728
729
729 file = open(path, "w+")
730 file = open(path, "w+")
730
731
731 lines = {}
732 lines = {}
732 for sample in data.samples:
733 for sample in data.samples:
733 sites = [s.function for s in sample.stack]
734 sites = [s.function for s in sample.stack]
734 sites.reverse()
735 sites.reverse()
735 line = ';'.join(sites)
736 line = ';'.join(sites)
736 if line in lines:
737 if line in lines:
737 lines[line] = lines[line] + 1
738 lines[line] = lines[line] + 1
738 else:
739 else:
739 lines[line] = 1
740 lines[line] = 1
740
741
741 for line, count in lines.iteritems():
742 for line, count in lines.iteritems():
742 file.write("%s %d\n" % (line, count))
743 file.write("%s %d\n" % (line, count))
743
744
744 file.close()
745 file.close()
745
746
746 if outputfile is None:
747 if outputfile is None:
747 outputfile = '~/flamegraph.svg'
748 outputfile = '~/flamegraph.svg'
748
749
749 os.system("perl ~/flamegraph.pl %s > %s" % (path, outputfile))
750 os.system("perl ~/flamegraph.pl %s > %s" % (path, outputfile))
750 fp.write(b'Written to %s\n' % outputfile)
751 fp.write(b'Written to %s\n' % outputfile)
751
752
752 _pathcache = {}
753 _pathcache = {}
753 def simplifypath(path):
754 def simplifypath(path):
754 '''Attempt to make the path to a Python module easier to read by
755 '''Attempt to make the path to a Python module easier to read by
755 removing whatever part of the Python search path it was found
756 removing whatever part of the Python search path it was found
756 on.'''
757 on.'''
757
758
758 if path in _pathcache:
759 if path in _pathcache:
759 return _pathcache[path]
760 return _pathcache[path]
760 hgpath = pycompat.fsencode(encoding.__file__).rsplit(os.sep, 2)[0]
761 hgpath = pycompat.fsencode(encoding.__file__).rsplit(os.sep, 2)[0]
761 for p in [hgpath] + sys.path:
762 for p in [hgpath] + sys.path:
762 prefix = p + os.sep
763 prefix = p + os.sep
763 if path.startswith(prefix):
764 if path.startswith(prefix):
764 path = path[len(prefix):]
765 path = path[len(prefix):]
765 break
766 break
766 _pathcache[path] = path
767 _pathcache[path] = path
767 return path
768 return path
768
769
769 def write_to_json(data, fp):
770 def write_to_json(data, fp):
770 samples = []
771 samples = []
771
772
772 for sample in data.samples:
773 for sample in data.samples:
773 stack = []
774 stack = []
774
775
775 for frame in sample.stack:
776 for frame in sample.stack:
776 stack.append((frame.path, frame.lineno, frame.function))
777 stack.append((frame.path, frame.lineno, frame.function))
777
778
778 samples.append((sample.time, stack))
779 samples.append((sample.time, stack))
779
780
780 data = json.dumps(samples)
781 data = json.dumps(samples)
781 if not isinstance(data, bytes):
782 if not isinstance(data, bytes):
782 data = data.encode('utf-8')
783 data = data.encode('utf-8')
783
784
784 fp.write(data)
785 fp.write(data)
785
786
786 def write_to_chrome(data, fp, minthreshold=0.005, maxthreshold=0.999):
787 def write_to_chrome(data, fp, minthreshold=0.005, maxthreshold=0.999):
787 samples = []
788 samples = []
788 laststack = collections.deque()
789 laststack = collections.deque()
789 lastseen = collections.deque()
790 lastseen = collections.deque()
790
791
791 # The Chrome tracing format allows us to use a compact stack
792 # The Chrome tracing format allows us to use a compact stack
792 # representation to save space. It's fiddly but worth it.
793 # representation to save space. It's fiddly but worth it.
793 # We maintain a bijection between stack and ID.
794 # We maintain a bijection between stack and ID.
794 stack2id = {}
795 stack2id = {}
795 id2stack = [] # will eventually be rendered
796 id2stack = [] # will eventually be rendered
796
797
797 def stackid(stack):
798 def stackid(stack):
798 if not stack:
799 if not stack:
799 return
800 return
800 if stack in stack2id:
801 if stack in stack2id:
801 return stack2id[stack]
802 return stack2id[stack]
802 parent = stackid(stack[1:])
803 parent = stackid(stack[1:])
803 myid = len(stack2id)
804 myid = len(stack2id)
804 stack2id[stack] = myid
805 stack2id[stack] = myid
805 id2stack.append(dict(category=stack[0][0], name='%s %s' % stack[0]))
806 id2stack.append(dict(category=stack[0][0], name='%s %s' % stack[0]))
806 if parent is not None:
807 if parent is not None:
807 id2stack[-1].update(parent=parent)
808 id2stack[-1].update(parent=parent)
808 return myid
809 return myid
809
810
810 def endswith(a, b):
811 def endswith(a, b):
811 return list(a)[-len(b):] == list(b)
812 return list(a)[-len(b):] == list(b)
812
813
813 # The sampling profiler can sample multiple times without
814 # The sampling profiler can sample multiple times without
814 # advancing the clock, potentially causing the Chrome trace viewer
815 # advancing the clock, potentially causing the Chrome trace viewer
815 # to render single-pixel columns that we cannot zoom in on. We
816 # to render single-pixel columns that we cannot zoom in on. We
816 # work around this by pretending that zero-duration samples are a
817 # work around this by pretending that zero-duration samples are a
817 # millisecond in length.
818 # millisecond in length.
818
819
819 clamp = 0.001
820 clamp = 0.001
820
821
821 # We provide knobs that by default attempt to filter out stack
822 # We provide knobs that by default attempt to filter out stack
822 # frames that are too noisy:
823 # frames that are too noisy:
823 #
824 #
824 # * A few take almost all execution time. These are usually boring
825 # * A few take almost all execution time. These are usually boring
825 # setup functions, giving a stack that is deep but uninformative.
826 # setup functions, giving a stack that is deep but uninformative.
826 #
827 #
827 # * Numerous samples take almost no time, but introduce lots of
828 # * Numerous samples take almost no time, but introduce lots of
828 # noisy, oft-deep "spines" into a rendered profile.
829 # noisy, oft-deep "spines" into a rendered profile.
829
830
830 blacklist = set()
831 blacklist = set()
831 totaltime = data.samples[-1].time - data.samples[0].time
832 totaltime = data.samples[-1].time - data.samples[0].time
832 minthreshold = totaltime * minthreshold
833 minthreshold = totaltime * minthreshold
833 maxthreshold = max(totaltime * maxthreshold, clamp)
834 maxthreshold = max(totaltime * maxthreshold, clamp)
834
835
835 def poplast():
836 def poplast():
836 oldsid = stackid(tuple(laststack))
837 oldsid = stackid(tuple(laststack))
837 oldcat, oldfunc = laststack.popleft()
838 oldcat, oldfunc = laststack.popleft()
838 oldtime, oldidx = lastseen.popleft()
839 oldtime, oldidx = lastseen.popleft()
839 duration = sample.time - oldtime
840 duration = sample.time - oldtime
840 if minthreshold <= duration <= maxthreshold:
841 if minthreshold <= duration <= maxthreshold:
841 # ensure no zero-duration events
842 # ensure no zero-duration events
842 sampletime = max(oldtime + clamp, sample.time)
843 sampletime = max(oldtime + clamp, sample.time)
843 samples.append(dict(ph='E', name=oldfunc, cat=oldcat, sf=oldsid,
844 samples.append(dict(ph='E', name=oldfunc, cat=oldcat, sf=oldsid,
844 ts=sampletime*1e6, pid=0))
845 ts=sampletime*1e6, pid=0))
845 else:
846 else:
846 blacklist.add(oldidx)
847 blacklist.add(oldidx)
847
848
848 # Much fiddling to synthesize correctly(ish) nested begin/end
849 # Much fiddling to synthesize correctly(ish) nested begin/end
849 # events given only stack snapshots.
850 # events given only stack snapshots.
850
851
851 for sample in data.samples:
852 for sample in data.samples:
852 tos = sample.stack[0]
853 tos = sample.stack[0]
853 name = tos.function
854 name = tos.function
854 path = simplifypath(tos.path)
855 path = simplifypath(tos.path)
855 stack = tuple((('%s:%d' % (simplifypath(frame.path), frame.lineno),
856 stack = tuple((('%s:%d' % (simplifypath(frame.path), frame.lineno),
856 frame.function) for frame in sample.stack))
857 frame.function) for frame in sample.stack))
857 qstack = collections.deque(stack)
858 qstack = collections.deque(stack)
858 if laststack == qstack:
859 if laststack == qstack:
859 continue
860 continue
860 while laststack and qstack and laststack[-1] == qstack[-1]:
861 while laststack and qstack and laststack[-1] == qstack[-1]:
861 laststack.pop()
862 laststack.pop()
862 qstack.pop()
863 qstack.pop()
863 while laststack:
864 while laststack:
864 poplast()
865 poplast()
865 for f in reversed(qstack):
866 for f in reversed(qstack):
866 lastseen.appendleft((sample.time, len(samples)))
867 lastseen.appendleft((sample.time, len(samples)))
867 laststack.appendleft(f)
868 laststack.appendleft(f)
868 path, name = f
869 path, name = f
869 sid = stackid(tuple(laststack))
870 sid = stackid(tuple(laststack))
870 samples.append(dict(ph='B', name=name, cat=path, ts=sample.time*1e6,
871 samples.append(dict(ph='B', name=name, cat=path, ts=sample.time*1e6,
871 sf=sid, pid=0))
872 sf=sid, pid=0))
872 laststack = collections.deque(stack)
873 laststack = collections.deque(stack)
873 while laststack:
874 while laststack:
874 poplast()
875 poplast()
875 events = [s[1] for s in enumerate(samples) if s[0] not in blacklist]
876 events = [s[1] for s in enumerate(samples) if s[0] not in blacklist]
876 frames = collections.OrderedDict((str(k), v)
877 frames = collections.OrderedDict((str(k), v)
877 for (k,v) in enumerate(id2stack))
878 for (k,v) in enumerate(id2stack))
878 json.dump(dict(traceEvents=events, stackFrames=frames), fp, indent=1)
879 json.dump(dict(traceEvents=events, stackFrames=frames), fp, indent=1)
879 fp.write('\n')
880 fp.write('\n')
880
881
881 def printusage():
882 def printusage():
882 print(r"""
883 print(r"""
883 The statprof command line allows you to inspect the last profile's results in
884 The statprof command line allows you to inspect the last profile's results in
884 the following forms:
885 the following forms:
885
886
886 usage:
887 usage:
887 hotpath [-l --limit percent]
888 hotpath [-l --limit percent]
888 Shows a graph of calls with the percent of time each takes.
889 Shows a graph of calls with the percent of time each takes.
889 Red calls take over 10%% of the total time themselves.
890 Red calls take over 10%% of the total time themselves.
890 lines
891 lines
891 Shows the actual sampled lines.
892 Shows the actual sampled lines.
892 functions
893 functions
893 Shows the samples grouped by function.
894 Shows the samples grouped by function.
894 function [filename:]functionname
895 function [filename:]functionname
895 Shows the callers and callees of a particular function.
896 Shows the callers and callees of a particular function.
896 flame [-s --script-path] [-o --output-file path]
897 flame [-s --script-path] [-o --output-file path]
897 Writes out a flamegraph to output-file (defaults to ~/flamegraph.svg)
898 Writes out a flamegraph to output-file (defaults to ~/flamegraph.svg)
898 Requires that ~/flamegraph.pl exist.
899 Requires that ~/flamegraph.pl exist.
899 (Specify alternate script path with --script-path.)""")
900 (Specify alternate script path with --script-path.)""")
900
901
901 def main(argv=None):
902 def main(argv=None):
902 if argv is None:
903 if argv is None:
903 argv = sys.argv
904 argv = sys.argv
904
905
905 if len(argv) == 1:
906 if len(argv) == 1:
906 printusage()
907 printusage()
907 return 0
908 return 0
908
909
909 displayargs = {}
910 displayargs = {}
910
911
911 optstart = 2
912 optstart = 2
912 displayargs['function'] = None
913 displayargs['function'] = None
913 if argv[1] == r'hotpath':
914 if argv[1] == r'hotpath':
914 displayargs['format'] = DisplayFormats.Hotpath
915 displayargs['format'] = DisplayFormats.Hotpath
915 elif argv[1] == r'lines':
916 elif argv[1] == r'lines':
916 displayargs['format'] = DisplayFormats.ByLine
917 displayargs['format'] = DisplayFormats.ByLine
917 elif argv[1] == r'functions':
918 elif argv[1] == r'functions':
918 displayargs['format'] = DisplayFormats.ByMethod
919 displayargs['format'] = DisplayFormats.ByMethod
919 elif argv[1] == r'function':
920 elif argv[1] == r'function':
920 displayargs['format'] = DisplayFormats.AboutMethod
921 displayargs['format'] = DisplayFormats.AboutMethod
921 displayargs['function'] = argv[2]
922 displayargs['function'] = argv[2]
922 optstart = 3
923 optstart = 3
923 elif argv[1] == r'flame':
924 elif argv[1] == r'flame':
924 displayargs['format'] = DisplayFormats.FlameGraph
925 displayargs['format'] = DisplayFormats.FlameGraph
925 else:
926 else:
926 printusage()
927 printusage()
927 return 0
928 return 0
928
929
929 # process options
930 # process options
930 try:
931 try:
931 opts, args = pycompat.getoptb(sys.argv[optstart:], "hl:f:o:p:",
932 opts, args = pycompat.getoptb(sys.argv[optstart:], "hl:f:o:p:",
932 ["help", "limit=", "file=", "output-file=", "script-path="])
933 ["help", "limit=", "file=", "output-file=", "script-path="])
933 except getopt.error as msg:
934 except getopt.error as msg:
934 print(msg)
935 print(msg)
935 printusage()
936 printusage()
936 return 2
937 return 2
937
938
938 displayargs['limit'] = 0.05
939 displayargs['limit'] = 0.05
939 path = None
940 path = None
940 for o, value in opts:
941 for o, value in opts:
941 if o in (r"-l", r"--limit"):
942 if o in (r"-l", r"--limit"):
942 displayargs['limit'] = float(value)
943 displayargs['limit'] = float(value)
943 elif o in (r"-f", r"--file"):
944 elif o in (r"-f", r"--file"):
944 path = value
945 path = value
945 elif o in (r"-o", r"--output-file"):
946 elif o in (r"-o", r"--output-file"):
946 displayargs['outputfile'] = value
947 displayargs['outputfile'] = value
947 elif o in (r"-p", r"--script-path"):
948 elif o in (r"-p", r"--script-path"):
948 displayargs['scriptpath'] = value
949 displayargs['scriptpath'] = value
949 elif o in (r"-h", r"help"):
950 elif o in (r"-h", r"help"):
950 printusage()
951 printusage()
951 return 0
952 return 0
952 else:
953 else:
953 assert False, "unhandled option %s" % o
954 assert False, "unhandled option %s" % o
954
955
955 if not path:
956 if not path:
956 print(r'must specify --file to load')
957 print(r'must specify --file to load')
957 return 1
958 return 1
958
959
959 load_data(path=path)
960 load_data(path=path)
960
961
961 display(**pycompat.strkwargs(displayargs))
962 display(**pycompat.strkwargs(displayargs))
962
963
963 return 0
964 return 0
964
965
965 if __name__ == r"__main__":
966 if __name__ == r"__main__":
966 sys.exit(main())
967 sys.exit(main())
@@ -1,165 +1,167 b''
1 test --time
1 test --time
2
2
3 $ hg --time help -q help 2>&1 | grep time > /dev/null
3 $ hg --time help -q help 2>&1 | grep time > /dev/null
4 $ hg init a
4 $ hg init a
5 $ cd a
5 $ cd a
6
6
7 Function to check that statprof ran
7 Function to check that statprof ran
8 $ statprofran () {
8 $ statprofran () {
9 > egrep 'Sample count:|No samples recorded' > /dev/null
9 > egrep 'Sample count:|No samples recorded' > /dev/null
10 > }
10 > }
11
11
12 test --profile
12 test --profile
13
13
14 $ hg st --profile 2>&1 | statprofran
14 $ hg st --profile 2>&1 | statprofran
15
15
16 Abreviated version
16 Abreviated version
17
17
18 $ hg st --prof 2>&1 | statprofran
18 $ hg st --prof 2>&1 | statprofran
19
19
20 In alias
20 In alias
21
21
22 $ hg --config "alias.profst=status --profile" profst 2>&1 | statprofran
22 $ hg --config "alias.profst=status --profile" profst 2>&1 | statprofran
23
23
24 #if lsprof
24 #if lsprof
25
25
26 $ prof='hg --config profiling.type=ls --profile'
26 $ prof='hg --config profiling.type=ls --profile'
27
27
28 $ $prof st 2>../out
28 $ $prof st 2>../out
29 $ grep CallCount ../out > /dev/null || cat ../out
29 $ grep CallCount ../out > /dev/null || cat ../out
30
30
31 $ $prof --config profiling.output=../out st
31 $ $prof --config profiling.output=../out st
32 $ grep CallCount ../out > /dev/null || cat ../out
32 $ grep CallCount ../out > /dev/null || cat ../out
33
33
34 $ $prof --config profiling.output=blackbox --config extensions.blackbox= st
34 $ $prof --config profiling.output=blackbox --config extensions.blackbox= st
35 $ grep CallCount .hg/blackbox.log > /dev/null || cat .hg/blackbox.log
35 $ grep CallCount .hg/blackbox.log > /dev/null || cat .hg/blackbox.log
36
36
37 $ $prof --config profiling.format=text st 2>../out
37 $ $prof --config profiling.format=text st 2>../out
38 $ grep CallCount ../out > /dev/null || cat ../out
38 $ grep CallCount ../out > /dev/null || cat ../out
39
39
40 $ echo "[profiling]" >> $HGRCPATH
40 $ echo "[profiling]" >> $HGRCPATH
41 $ echo "format=kcachegrind" >> $HGRCPATH
41 $ echo "format=kcachegrind" >> $HGRCPATH
42
42
43 $ $prof st 2>../out
43 $ $prof st 2>../out
44 $ grep 'events: Ticks' ../out > /dev/null || cat ../out
44 $ grep 'events: Ticks' ../out > /dev/null || cat ../out
45
45
46 $ $prof --config profiling.output=../out st
46 $ $prof --config profiling.output=../out st
47 $ grep 'events: Ticks' ../out > /dev/null || cat ../out
47 $ grep 'events: Ticks' ../out > /dev/null || cat ../out
48
48
49 #endif
49 #endif
50
50
51 #if lsprof serve
51 #if lsprof serve
52
52
53 Profiling of HTTP requests works
53 Profiling of HTTP requests works
54
54
55 $ $prof --config profiling.format=text --config profiling.output=../profile.log serve -d -p $HGPORT --pid-file ../hg.pid -A ../access.log
55 $ $prof --config profiling.format=text --config profiling.output=../profile.log serve -d -p $HGPORT --pid-file ../hg.pid -A ../access.log
56 $ cat ../hg.pid >> $DAEMON_PIDS
56 $ cat ../hg.pid >> $DAEMON_PIDS
57 $ hg -q clone -U http://localhost:$HGPORT ../clone
57 $ hg -q clone -U http://localhost:$HGPORT ../clone
58
58
59 A single profile is logged because file logging doesn't append
59 A single profile is logged because file logging doesn't append
60 $ grep CallCount ../profile.log | wc -l
60 $ grep CallCount ../profile.log | wc -l
61 \s*1 (re)
61 \s*1 (re)
62
62
63 #endif
63 #endif
64
64
65 Install an extension that can sleep and guarantee a profiler has time to run
65 Install an extension that can sleep and guarantee a profiler has time to run
66
66
67 $ cat >> sleepext.py << EOF
67 $ cat >> sleepext.py << EOF
68 > import time
68 > import time
69 > from mercurial import registrar
69 > from mercurial import registrar
70 > cmdtable = {}
70 > cmdtable = {}
71 > command = registrar.command(cmdtable)
71 > command = registrar.command(cmdtable)
72 > @command(b'sleep', [], b'hg sleep')
72 > @command(b'sleep', [], b'hg sleep')
73 > def sleep(ui, *args, **kwargs):
73 > def sleep(ui, *args, **kwargs):
74 > time.sleep(0.1)
74 > time.sleep(0.1)
75 > EOF
75 > EOF
76
76
77 $ cat >> $HGRCPATH << EOF
77 $ cat >> $HGRCPATH << EOF
78 > [extensions]
78 > [extensions]
79 > sleep = `pwd`/sleepext.py
79 > sleep = `pwd`/sleepext.py
80 > EOF
80 > EOF
81
81
82 statistical profiler works
82 statistical profiler works
83
83
84 $ hg --profile sleep 2>../out
84 $ hg --profile sleep 2>../out
85 $ cat ../out | statprofran
85 $ cat ../out | statprofran
86
86
87 Various statprof formatters work
87 Various statprof formatters work
88
88
89 $ hg --profile --config profiling.statformat=byline sleep 2>../out
89 $ hg --profile --config profiling.statformat=byline sleep 2>../out
90 $ head -n 3 ../out
90 $ head -n 3 ../out
91 % cumulative self
91 % cumulative self
92 time seconds seconds name
93 * sleepext.py:*:sleep (glob)
92 $ cat ../out | statprofran
94 $ cat ../out | statprofran
93
95
94 $ hg --profile --config profiling.statformat=bymethod sleep 2>../out
96 $ hg --profile --config profiling.statformat=bymethod sleep 2>../out
95 $ head -n 1 ../out
97 $ head -n 1 ../out
96 % cumulative self
98 % cumulative self
97 $ cat ../out | statprofran
99 $ cat ../out | statprofran
98
100
99 $ hg --profile --config profiling.statformat=hotpath sleep 2>../out
101 $ hg --profile --config profiling.statformat=hotpath sleep 2>../out
100 $ cat ../out | statprofran
102 $ cat ../out | statprofran
101
103
102 $ hg --profile --config profiling.statformat=json sleep 2>../out
104 $ hg --profile --config profiling.statformat=json sleep 2>../out
103 $ cat ../out
105 $ cat ../out
104 \[\[-?\d+.* (re)
106 \[\[-?\d+.* (re)
105
107
106 statprof can be used as a standalone module
108 statprof can be used as a standalone module
107
109
108 $ "$PYTHON" -m mercurial.statprof hotpath
110 $ "$PYTHON" -m mercurial.statprof hotpath
109 must specify --file to load
111 must specify --file to load
110 [1]
112 [1]
111
113
112 $ cd ..
114 $ cd ..
113
115
114 #if no-chg
116 #if no-chg
115 profiler extension could be loaded before other extensions
117 profiler extension could be loaded before other extensions
116
118
117 $ cat > fooprof.py <<EOF
119 $ cat > fooprof.py <<EOF
118 > from __future__ import absolute_import
120 > from __future__ import absolute_import
119 > import contextlib
121 > import contextlib
120 > import sys
122 > import sys
121 > @contextlib.contextmanager
123 > @contextlib.contextmanager
122 > def profile(ui, fp):
124 > def profile(ui, fp):
123 > print('fooprof: start profile')
125 > print('fooprof: start profile')
124 > sys.stdout.flush()
126 > sys.stdout.flush()
125 > yield
127 > yield
126 > print('fooprof: end profile')
128 > print('fooprof: end profile')
127 > sys.stdout.flush()
129 > sys.stdout.flush()
128 > def extsetup(ui):
130 > def extsetup(ui):
129 > ui.write(b'fooprof: loaded\n')
131 > ui.write(b'fooprof: loaded\n')
130 > EOF
132 > EOF
131
133
132 $ cat > otherextension.py <<EOF
134 $ cat > otherextension.py <<EOF
133 > from __future__ import absolute_import
135 > from __future__ import absolute_import
134 > def extsetup(ui):
136 > def extsetup(ui):
135 > ui.write(b'otherextension: loaded\n')
137 > ui.write(b'otherextension: loaded\n')
136 > EOF
138 > EOF
137
139
138 $ hg init b
140 $ hg init b
139 $ cd b
141 $ cd b
140 $ cat >> .hg/hgrc <<EOF
142 $ cat >> .hg/hgrc <<EOF
141 > [extensions]
143 > [extensions]
142 > other = $TESTTMP/otherextension.py
144 > other = $TESTTMP/otherextension.py
143 > fooprof = $TESTTMP/fooprof.py
145 > fooprof = $TESTTMP/fooprof.py
144 > EOF
146 > EOF
145
147
146 $ hg root
148 $ hg root
147 otherextension: loaded
149 otherextension: loaded
148 fooprof: loaded
150 fooprof: loaded
149 $TESTTMP/b
151 $TESTTMP/b
150 $ HGPROF=fooprof hg root --profile
152 $ HGPROF=fooprof hg root --profile
151 fooprof: loaded
153 fooprof: loaded
152 fooprof: start profile
154 fooprof: start profile
153 otherextension: loaded
155 otherextension: loaded
154 $TESTTMP/b
156 $TESTTMP/b
155 fooprof: end profile
157 fooprof: end profile
156
158
157 $ HGPROF=other hg root --profile 2>&1 | head -n 2
159 $ HGPROF=other hg root --profile 2>&1 | head -n 2
158 otherextension: loaded
160 otherextension: loaded
159 unrecognized profiler 'other' - ignored
161 unrecognized profiler 'other' - ignored
160
162
161 $ HGPROF=unknown hg root --profile 2>&1 | head -n 1
163 $ HGPROF=unknown hg root --profile 2>&1 | head -n 1
162 unrecognized profiler 'unknown' - ignored
164 unrecognized profiler 'unknown' - ignored
163
165
164 $ cd ..
166 $ cd ..
165 #endif
167 #endif
General Comments 0
You need to be logged in to leave comments. Login now