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