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