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