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