##// END OF EJS Templates
statprof: pass data structure to display functions...
Gregory Szorc -
r30258:eea89068 default
parent child Browse files
Show More
@@ -1,802 +1,803 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 def save_data(path):
327 def save_data(path):
328 with open(path, 'w+') as file:
328 with open(path, 'w+') as file:
329 file.write(str(state.accumulated_time) + '\n')
329 file.write(str(state.accumulated_time) + '\n')
330 for sample in state.samples:
330 for sample in state.samples:
331 time = str(sample.time)
331 time = str(sample.time)
332 stack = sample.stack
332 stack = sample.stack
333 sites = ['\1'.join([s.path, str(s.lineno), s.function])
333 sites = ['\1'.join([s.path, str(s.lineno), s.function])
334 for s in stack]
334 for s in stack]
335 file.write(time + '\0' + '\0'.join(sites) + '\n')
335 file.write(time + '\0' + '\0'.join(sites) + '\n')
336
336
337 def load_data(path):
337 def load_data(path):
338 lines = open(path, 'r').read().splitlines()
338 lines = open(path, 'r').read().splitlines()
339
339
340 state.accumulated_time = float(lines[0])
340 state.accumulated_time = float(lines[0])
341 state.samples = []
341 state.samples = []
342 for line in lines[1:]:
342 for line in lines[1:]:
343 parts = line.split('\0')
343 parts = line.split('\0')
344 time = float(parts[0])
344 time = float(parts[0])
345 rawsites = parts[1:]
345 rawsites = parts[1:]
346 sites = []
346 sites = []
347 for rawsite in rawsites:
347 for rawsite in rawsites:
348 siteparts = rawsite.split('\1')
348 siteparts = rawsite.split('\1')
349 sites.append(CodeSite.get(siteparts[0], int(siteparts[1]),
349 sites.append(CodeSite.get(siteparts[0], int(siteparts[1]),
350 siteparts[2]))
350 siteparts[2]))
351
351
352 state.samples.append(Sample(sites, time))
352 state.samples.append(Sample(sites, time))
353
353
354
354
355
355
356 def reset(frequency=None):
356 def reset(frequency=None):
357 '''Clear out the state of the profiler. Do not call while the
357 '''Clear out the state of the profiler. Do not call while the
358 profiler is running.
358 profiler is running.
359
359
360 The optional frequency argument specifies the number of samples to
360 The optional frequency argument specifies the number of samples to
361 collect per second.'''
361 collect per second.'''
362 assert state.profile_level == 0, "Can't reset() while statprof is running"
362 assert state.profile_level == 0, "Can't reset() while statprof is running"
363 CodeSite.cache.clear()
363 CodeSite.cache.clear()
364 state.reset(frequency)
364 state.reset(frequency)
365
365
366
366
367 @contextmanager
367 @contextmanager
368 def profile():
368 def profile():
369 start()
369 start()
370 try:
370 try:
371 yield
371 yield
372 finally:
372 finally:
373 stop()
373 stop()
374 display()
374 display()
375
375
376
376
377 ###########################################################################
377 ###########################################################################
378 ## Reporting API
378 ## Reporting API
379
379
380 class SiteStats(object):
380 class SiteStats(object):
381 def __init__(self, site):
381 def __init__(self, site):
382 self.site = site
382 self.site = site
383 self.selfcount = 0
383 self.selfcount = 0
384 self.totalcount = 0
384 self.totalcount = 0
385
385
386 def addself(self):
386 def addself(self):
387 self.selfcount += 1
387 self.selfcount += 1
388
388
389 def addtotal(self):
389 def addtotal(self):
390 self.totalcount += 1
390 self.totalcount += 1
391
391
392 def selfpercent(self):
392 def selfpercent(self):
393 return self.selfcount / len(state.samples) * 100
393 return self.selfcount / len(state.samples) * 100
394
394
395 def totalpercent(self):
395 def totalpercent(self):
396 return self.totalcount / len(state.samples) * 100
396 return self.totalcount / len(state.samples) * 100
397
397
398 def selfseconds(self):
398 def selfseconds(self):
399 return self.selfcount * state.seconds_per_sample()
399 return self.selfcount * state.seconds_per_sample()
400
400
401 def totalseconds(self):
401 def totalseconds(self):
402 return self.totalcount * state.seconds_per_sample()
402 return self.totalcount * state.seconds_per_sample()
403
403
404 @classmethod
404 @classmethod
405 def buildstats(cls, samples):
405 def buildstats(cls, samples):
406 stats = {}
406 stats = {}
407
407
408 for sample in samples:
408 for sample in samples:
409 for i, site in enumerate(sample.stack):
409 for i, site in enumerate(sample.stack):
410 sitestat = stats.get(site)
410 sitestat = stats.get(site)
411 if not sitestat:
411 if not sitestat:
412 sitestat = SiteStats(site)
412 sitestat = SiteStats(site)
413 stats[site] = sitestat
413 stats[site] = sitestat
414
414
415 sitestat.addtotal()
415 sitestat.addtotal()
416
416
417 if i == 0:
417 if i == 0:
418 sitestat.addself()
418 sitestat.addself()
419
419
420 return [s for s in stats.itervalues()]
420 return [s for s in stats.itervalues()]
421
421
422 class DisplayFormats:
422 class DisplayFormats:
423 ByLine = 0
423 ByLine = 0
424 ByMethod = 1
424 ByMethod = 1
425 AboutMethod = 2
425 AboutMethod = 2
426 Hotpath = 3
426 Hotpath = 3
427 FlameGraph = 4
427 FlameGraph = 4
428 Json = 5
428 Json = 5
429
429
430 def display(fp=None, format=3, **kwargs):
430 def display(fp=None, format=3, data=None, **kwargs):
431 '''Print statistics, either to stdout or the given file object.'''
431 '''Print statistics, either to stdout or the given file object.'''
432 data = data or state
432
433
433 if fp is None:
434 if fp is None:
434 import sys
435 import sys
435 fp = sys.stdout
436 fp = sys.stdout
436 if len(state.samples) == 0:
437 if len(data.samples) == 0:
437 print('No samples recorded.', file=fp)
438 print('No samples recorded.', file=fp)
438 return
439 return
439
440
440 if format == DisplayFormats.ByLine:
441 if format == DisplayFormats.ByLine:
441 display_by_line(fp)
442 display_by_line(data, fp)
442 elif format == DisplayFormats.ByMethod:
443 elif format == DisplayFormats.ByMethod:
443 display_by_method(fp)
444 display_by_method(data, fp)
444 elif format == DisplayFormats.AboutMethod:
445 elif format == DisplayFormats.AboutMethod:
445 display_about_method(fp, **kwargs)
446 display_about_method(data, fp, **kwargs)
446 elif format == DisplayFormats.Hotpath:
447 elif format == DisplayFormats.Hotpath:
447 display_hotpath(fp, **kwargs)
448 display_hotpath(data, fp, **kwargs)
448 elif format == DisplayFormats.FlameGraph:
449 elif format == DisplayFormats.FlameGraph:
449 write_to_flame(fp, **kwargs)
450 write_to_flame(data, fp, **kwargs)
450 elif format == DisplayFormats.Json:
451 elif format == DisplayFormats.Json:
451 write_to_json(fp)
452 write_to_json(data, fp)
452 else:
453 else:
453 raise Exception("Invalid display format")
454 raise Exception("Invalid display format")
454
455
455 if format != DisplayFormats.Json:
456 if format != DisplayFormats.Json:
456 print('---', file=fp)
457 print('---', file=fp)
457 print('Sample count: %d' % len(state.samples), file=fp)
458 print('Sample count: %d' % len(data.samples), file=fp)
458 print('Total time: %f seconds' % state.accumulated_time, file=fp)
459 print('Total time: %f seconds' % data.accumulated_time, file=fp)
459
460
460 def display_by_line(fp):
461 def display_by_line(data, fp):
461 '''Print the profiler data with each sample line represented
462 '''Print the profiler data with each sample line represented
462 as one row in a table. Sorted by self-time per line.'''
463 as one row in a table. Sorted by self-time per line.'''
463 stats = SiteStats.buildstats(state.samples)
464 stats = SiteStats.buildstats(data.samples)
464 stats.sort(reverse=True, key=lambda x: x.selfseconds())
465 stats.sort(reverse=True, key=lambda x: x.selfseconds())
465
466
466 print('%5.5s %10.10s %7.7s %-8.8s' %
467 print('%5.5s %10.10s %7.7s %-8.8s' %
467 ('% ', 'cumulative', 'self', ''), file=fp)
468 ('% ', 'cumulative', 'self', ''), file=fp)
468 print('%5.5s %9.9s %8.8s %-8.8s' %
469 print('%5.5s %9.9s %8.8s %-8.8s' %
469 ("time", "seconds", "seconds", "name"), file=fp)
470 ("time", "seconds", "seconds", "name"), file=fp)
470
471
471 for stat in stats:
472 for stat in stats:
472 site = stat.site
473 site = stat.site
473 sitelabel = '%s:%d:%s' % (site.filename(), site.lineno, site.function)
474 sitelabel = '%s:%d:%s' % (site.filename(), site.lineno, site.function)
474 print('%6.2f %9.2f %9.2f %s' % (stat.selfpercent(),
475 print('%6.2f %9.2f %9.2f %s' % (stat.selfpercent(),
475 stat.totalseconds(),
476 stat.totalseconds(),
476 stat.selfseconds(),
477 stat.selfseconds(),
477 sitelabel),
478 sitelabel),
478 file=fp)
479 file=fp)
479
480
480 def display_by_method(fp):
481 def display_by_method(data, fp):
481 '''Print the profiler data with each sample function represented
482 '''Print the profiler data with each sample function represented
482 as one row in a table. Important lines within that function are
483 as one row in a table. Important lines within that function are
483 output as nested rows. Sorted by self-time per line.'''
484 output as nested rows. Sorted by self-time per line.'''
484 print('%5.5s %10.10s %7.7s %-8.8s' %
485 print('%5.5s %10.10s %7.7s %-8.8s' %
485 ('% ', 'cumulative', 'self', ''), file=fp)
486 ('% ', 'cumulative', 'self', ''), file=fp)
486 print('%5.5s %9.9s %8.8s %-8.8s' %
487 print('%5.5s %9.9s %8.8s %-8.8s' %
487 ("time", "seconds", "seconds", "name"), file=fp)
488 ("time", "seconds", "seconds", "name"), file=fp)
488
489
489 stats = SiteStats.buildstats(state.samples)
490 stats = SiteStats.buildstats(data.samples)
490
491
491 grouped = defaultdict(list)
492 grouped = defaultdict(list)
492 for stat in stats:
493 for stat in stats:
493 grouped[stat.site.filename() + ":" + stat.site.function].append(stat)
494 grouped[stat.site.filename() + ":" + stat.site.function].append(stat)
494
495
495 # compute sums for each function
496 # compute sums for each function
496 functiondata = []
497 functiondata = []
497 for fname, sitestats in grouped.iteritems():
498 for fname, sitestats in grouped.iteritems():
498 total_cum_sec = 0
499 total_cum_sec = 0
499 total_self_sec = 0
500 total_self_sec = 0
500 total_percent = 0
501 total_percent = 0
501 for stat in sitestats:
502 for stat in sitestats:
502 total_cum_sec += stat.totalseconds()
503 total_cum_sec += stat.totalseconds()
503 total_self_sec += stat.selfseconds()
504 total_self_sec += stat.selfseconds()
504 total_percent += stat.selfpercent()
505 total_percent += stat.selfpercent()
505
506
506 functiondata.append((fname,
507 functiondata.append((fname,
507 total_cum_sec,
508 total_cum_sec,
508 total_self_sec,
509 total_self_sec,
509 total_percent,
510 total_percent,
510 sitestats))
511 sitestats))
511
512
512 # sort by total self sec
513 # sort by total self sec
513 functiondata.sort(reverse=True, key=lambda x: x[2])
514 functiondata.sort(reverse=True, key=lambda x: x[2])
514
515
515 for function in functiondata:
516 for function in functiondata:
516 if function[3] < 0.05:
517 if function[3] < 0.05:
517 continue
518 continue
518 print('%6.2f %9.2f %9.2f %s' % (function[3], # total percent
519 print('%6.2f %9.2f %9.2f %s' % (function[3], # total percent
519 function[1], # total cum sec
520 function[1], # total cum sec
520 function[2], # total self sec
521 function[2], # total self sec
521 function[0]), # file:function
522 function[0]), # file:function
522 file=fp)
523 file=fp)
523 function[4].sort(reverse=True, key=lambda i: i.selfseconds())
524 function[4].sort(reverse=True, key=lambda i: i.selfseconds())
524 for stat in function[4]:
525 for stat in function[4]:
525 # only show line numbers for significant locations (>1% time spent)
526 # only show line numbers for significant locations (>1% time spent)
526 if stat.selfpercent() > 1:
527 if stat.selfpercent() > 1:
527 source = stat.site.getsource(25)
528 source = stat.site.getsource(25)
528 stattuple = (stat.selfpercent(), stat.selfseconds(),
529 stattuple = (stat.selfpercent(), stat.selfseconds(),
529 stat.site.lineno, source)
530 stat.site.lineno, source)
530
531
531 print('%33.0f%% %6.2f line %s: %s' % (stattuple), file=fp)
532 print('%33.0f%% %6.2f line %s: %s' % (stattuple), file=fp)
532
533
533 def display_about_method(fp, function=None, **kwargs):
534 def display_about_method(data, fp, function=None, **kwargs):
534 if function is None:
535 if function is None:
535 raise Exception("Invalid function")
536 raise Exception("Invalid function")
536
537
537 filename = None
538 filename = None
538 if ':' in function:
539 if ':' in function:
539 filename, function = function.split(':')
540 filename, function = function.split(':')
540
541
541 relevant_samples = 0
542 relevant_samples = 0
542 parents = {}
543 parents = {}
543 children = {}
544 children = {}
544
545
545 for sample in state.samples:
546 for sample in data.samples:
546 for i, site in enumerate(sample.stack):
547 for i, site in enumerate(sample.stack):
547 if site.function == function and (not filename
548 if site.function == function and (not filename
548 or site.filename() == filename):
549 or site.filename() == filename):
549 relevant_samples += 1
550 relevant_samples += 1
550 if i != len(sample.stack) - 1:
551 if i != len(sample.stack) - 1:
551 parent = sample.stack[i + 1]
552 parent = sample.stack[i + 1]
552 if parent in parents:
553 if parent in parents:
553 parents[parent] = parents[parent] + 1
554 parents[parent] = parents[parent] + 1
554 else:
555 else:
555 parents[parent] = 1
556 parents[parent] = 1
556
557
557 if site in children:
558 if site in children:
558 children[site] = children[site] + 1
559 children[site] = children[site] + 1
559 else:
560 else:
560 children[site] = 1
561 children[site] = 1
561
562
562 parents = [(parent, count) for parent, count in parents.iteritems()]
563 parents = [(parent, count) for parent, count in parents.iteritems()]
563 parents.sort(reverse=True, key=lambda x: x[1])
564 parents.sort(reverse=True, key=lambda x: x[1])
564 for parent, count in parents:
565 for parent, count in parents:
565 print('%6.2f%% %s:%s line %s: %s' %
566 print('%6.2f%% %s:%s line %s: %s' %
566 (count / relevant_samples * 100, parent.filename(),
567 (count / relevant_samples * 100, parent.filename(),
567 parent.function, parent.lineno, parent.getsource(50)), file=fp)
568 parent.function, parent.lineno, parent.getsource(50)), file=fp)
568
569
569 stats = SiteStats.buildstats(state.samples)
570 stats = SiteStats.buildstats(data.samples)
570 stats = [s for s in stats
571 stats = [s for s in stats
571 if s.site.function == function and
572 if s.site.function == function and
572 (not filename or s.site.filename() == filename)]
573 (not filename or s.site.filename() == filename)]
573
574
574 total_cum_sec = 0
575 total_cum_sec = 0
575 total_self_sec = 0
576 total_self_sec = 0
576 total_self_percent = 0
577 total_self_percent = 0
577 total_cum_percent = 0
578 total_cum_percent = 0
578 for stat in stats:
579 for stat in stats:
579 total_cum_sec += stat.totalseconds()
580 total_cum_sec += stat.totalseconds()
580 total_self_sec += stat.selfseconds()
581 total_self_sec += stat.selfseconds()
581 total_self_percent += stat.selfpercent()
582 total_self_percent += stat.selfpercent()
582 total_cum_percent += stat.totalpercent()
583 total_cum_percent += stat.totalpercent()
583
584
584 print(
585 print(
585 '\n %s:%s Total: %0.2fs (%0.2f%%) Self: %0.2fs (%0.2f%%)\n' %
586 '\n %s:%s Total: %0.2fs (%0.2f%%) Self: %0.2fs (%0.2f%%)\n' %
586 (
587 (
587 filename or '___',
588 filename or '___',
588 function,
589 function,
589 total_cum_sec,
590 total_cum_sec,
590 total_cum_percent,
591 total_cum_percent,
591 total_self_sec,
592 total_self_sec,
592 total_self_percent
593 total_self_percent
593 ), file=fp)
594 ), file=fp)
594
595
595 children = [(child, count) for child, count in children.iteritems()]
596 children = [(child, count) for child, count in children.iteritems()]
596 children.sort(reverse=True, key=lambda x: x[1])
597 children.sort(reverse=True, key=lambda x: x[1])
597 for child, count in children:
598 for child, count in children:
598 print(' %6.2f%% line %s: %s' %
599 print(' %6.2f%% line %s: %s' %
599 (count / relevant_samples * 100, child.lineno,
600 (count / relevant_samples * 100, child.lineno,
600 child.getsource(50)), file=fp)
601 child.getsource(50)), file=fp)
601
602
602 def display_hotpath(fp, limit=0.05, **kwargs):
603 def display_hotpath(data, fp, limit=0.05, **kwargs):
603 class HotNode(object):
604 class HotNode(object):
604 def __init__(self, site):
605 def __init__(self, site):
605 self.site = site
606 self.site = site
606 self.count = 0
607 self.count = 0
607 self.children = {}
608 self.children = {}
608
609
609 def add(self, stack, time):
610 def add(self, stack, time):
610 self.count += time
611 self.count += time
611 site = stack[0]
612 site = stack[0]
612 child = self.children.get(site)
613 child = self.children.get(site)
613 if not child:
614 if not child:
614 child = HotNode(site)
615 child = HotNode(site)
615 self.children[site] = child
616 self.children[site] = child
616
617
617 if len(stack) > 1:
618 if len(stack) > 1:
618 i = 1
619 i = 1
619 # Skip boiler plate parts of the stack
620 # Skip boiler plate parts of the stack
620 while i < len(stack) and '%s:%s' % (stack[i].filename(), stack[i].function) in skips:
621 while i < len(stack) and '%s:%s' % (stack[i].filename(), stack[i].function) in skips:
621 i += 1
622 i += 1
622 if i < len(stack):
623 if i < len(stack):
623 child.add(stack[i:], time)
624 child.add(stack[i:], time)
624
625
625 root = HotNode(None)
626 root = HotNode(None)
626 lasttime = state.samples[0].time
627 lasttime = data.samples[0].time
627 for sample in state.samples:
628 for sample in data.samples:
628 root.add(sample.stack[::-1], sample.time - lasttime)
629 root.add(sample.stack[::-1], sample.time - lasttime)
629 lasttime = sample.time
630 lasttime = sample.time
630
631
631 def _write(node, depth, multiple_siblings):
632 def _write(node, depth, multiple_siblings):
632 site = node.site
633 site = node.site
633 visiblechildren = [c for c in node.children.itervalues()
634 visiblechildren = [c for c in node.children.itervalues()
634 if c.count >= (limit * root.count)]
635 if c.count >= (limit * root.count)]
635 if site:
636 if site:
636 indent = depth * 2 - 1
637 indent = depth * 2 - 1
637 filename = ''
638 filename = ''
638 function = ''
639 function = ''
639 if len(node.children) > 0:
640 if len(node.children) > 0:
640 childsite = list(node.children.itervalues())[0].site
641 childsite = list(node.children.itervalues())[0].site
641 filename = (childsite.filename() + ':').ljust(15)
642 filename = (childsite.filename() + ':').ljust(15)
642 function = childsite.function
643 function = childsite.function
643
644
644 # lots of string formatting
645 # lots of string formatting
645 listpattern = ''.ljust(indent) +\
646 listpattern = ''.ljust(indent) +\
646 ('\\' if multiple_siblings else '|') +\
647 ('\\' if multiple_siblings else '|') +\
647 ' %4.1f%% %s %s'
648 ' %4.1f%% %s %s'
648 liststring = listpattern % (node.count / root.count * 100,
649 liststring = listpattern % (node.count / root.count * 100,
649 filename, function)
650 filename, function)
650 codepattern = '%' + str(55 - len(liststring)) + 's %s: %s'
651 codepattern = '%' + str(55 - len(liststring)) + 's %s: %s'
651 codestring = codepattern % ('line', site.lineno, site.getsource(30))
652 codestring = codepattern % ('line', site.lineno, site.getsource(30))
652
653
653 finalstring = liststring + codestring
654 finalstring = liststring + codestring
654 childrensamples = sum([c.count for c in node.children.itervalues()])
655 childrensamples = sum([c.count for c in node.children.itervalues()])
655 # Make frames that performed more than 10% of the operation red
656 # Make frames that performed more than 10% of the operation red
656 if node.count - childrensamples > (0.1 * root.count):
657 if node.count - childrensamples > (0.1 * root.count):
657 finalstring = '\033[91m' + finalstring + '\033[0m'
658 finalstring = '\033[91m' + finalstring + '\033[0m'
658 # Make frames that didn't actually perform work dark grey
659 # Make frames that didn't actually perform work dark grey
659 elif node.count - childrensamples == 0:
660 elif node.count - childrensamples == 0:
660 finalstring = '\033[90m' + finalstring + '\033[0m'
661 finalstring = '\033[90m' + finalstring + '\033[0m'
661 print(finalstring, file=fp)
662 print(finalstring, file=fp)
662
663
663 newdepth = depth
664 newdepth = depth
664 if len(visiblechildren) > 1 or multiple_siblings:
665 if len(visiblechildren) > 1 or multiple_siblings:
665 newdepth += 1
666 newdepth += 1
666
667
667 visiblechildren.sort(reverse=True, key=lambda x: x.count)
668 visiblechildren.sort(reverse=True, key=lambda x: x.count)
668 for child in visiblechildren:
669 for child in visiblechildren:
669 _write(child, newdepth, len(visiblechildren) > 1)
670 _write(child, newdepth, len(visiblechildren) > 1)
670
671
671 if root.count > 0:
672 if root.count > 0:
672 _write(root, 0, False)
673 _write(root, 0, False)
673
674
674 def write_to_flame(fp, scriptpath=None, outputfile=None, **kwargs):
675 def write_to_flame(data, fp, scriptpath=None, outputfile=None, **kwargs):
675 if scriptpath is None:
676 if scriptpath is None:
676 scriptpath = os.environ['HOME'] + '/flamegraph.pl'
677 scriptpath = os.environ['HOME'] + '/flamegraph.pl'
677 if not os.path.exists(scriptpath):
678 if not os.path.exists(scriptpath):
678 print("error: missing %s" % scriptpath, file=fp)
679 print("error: missing %s" % scriptpath, file=fp)
679 print("get it here: https://github.com/brendangregg/FlameGraph",
680 print("get it here: https://github.com/brendangregg/FlameGraph",
680 file=fp)
681 file=fp)
681 return
682 return
682
683
683 fd, path = tempfile.mkstemp()
684 fd, path = tempfile.mkstemp()
684
685
685 file = open(path, "w+")
686 file = open(path, "w+")
686
687
687 lines = {}
688 lines = {}
688 for sample in state.samples:
689 for sample in data.samples:
689 sites = [s.function for s in sample.stack]
690 sites = [s.function for s in sample.stack]
690 sites.reverse()
691 sites.reverse()
691 line = ';'.join(sites)
692 line = ';'.join(sites)
692 if line in lines:
693 if line in lines:
693 lines[line] = lines[line] + 1
694 lines[line] = lines[line] + 1
694 else:
695 else:
695 lines[line] = 1
696 lines[line] = 1
696
697
697 for line, count in lines.iteritems():
698 for line, count in lines.iteritems():
698 file.write("%s %s\n" % (line, count))
699 file.write("%s %s\n" % (line, count))
699
700
700 file.close()
701 file.close()
701
702
702 if outputfile is None:
703 if outputfile is None:
703 outputfile = '~/flamegraph.svg'
704 outputfile = '~/flamegraph.svg'
704
705
705 os.system("perl ~/flamegraph.pl %s > %s" % (path, outputfile))
706 os.system("perl ~/flamegraph.pl %s > %s" % (path, outputfile))
706 print("Written to %s" % outputfile, file=fp)
707 print("Written to %s" % outputfile, file=fp)
707
708
708 def write_to_json(fp):
709 def write_to_json(data, fp):
709 samples = []
710 samples = []
710
711
711 for sample in state.samples:
712 for sample in data.samples:
712 stack = []
713 stack = []
713
714
714 for frame in sample.stack:
715 for frame in sample.stack:
715 stack.append((frame.path, frame.lineno, frame.function))
716 stack.append((frame.path, frame.lineno, frame.function))
716
717
717 samples.append((sample.time, stack))
718 samples.append((sample.time, stack))
718
719
719 print(json.dumps(samples), file=fp)
720 print(json.dumps(samples), file=fp)
720
721
721 def printusage():
722 def printusage():
722 print("""
723 print("""
723 The statprof command line allows you to inspect the last profile's results in
724 The statprof command line allows you to inspect the last profile's results in
724 the following forms:
725 the following forms:
725
726
726 usage:
727 usage:
727 hotpath [-l --limit percent]
728 hotpath [-l --limit percent]
728 Shows a graph of calls with the percent of time each takes.
729 Shows a graph of calls with the percent of time each takes.
729 Red calls take over 10%% of the total time themselves.
730 Red calls take over 10%% of the total time themselves.
730 lines
731 lines
731 Shows the actual sampled lines.
732 Shows the actual sampled lines.
732 functions
733 functions
733 Shows the samples grouped by function.
734 Shows the samples grouped by function.
734 function [filename:]functionname
735 function [filename:]functionname
735 Shows the callers and callees of a particular function.
736 Shows the callers and callees of a particular function.
736 flame [-s --script-path] [-o --output-file path]
737 flame [-s --script-path] [-o --output-file path]
737 Writes out a flamegraph to output-file (defaults to ~/flamegraph.svg)
738 Writes out a flamegraph to output-file (defaults to ~/flamegraph.svg)
738 Requires that ~/flamegraph.pl exist.
739 Requires that ~/flamegraph.pl exist.
739 (Specify alternate script path with --script-path.)""")
740 (Specify alternate script path with --script-path.)""")
740
741
741 def main(argv=None):
742 def main(argv=None):
742 if argv is None:
743 if argv is None:
743 argv = sys.argv
744 argv = sys.argv
744
745
745 if len(argv) == 1:
746 if len(argv) == 1:
746 printusage()
747 printusage()
747 return 0
748 return 0
748
749
749 displayargs = {}
750 displayargs = {}
750
751
751 optstart = 2
752 optstart = 2
752 displayargs['function'] = None
753 displayargs['function'] = None
753 if argv[1] == 'hotpath':
754 if argv[1] == 'hotpath':
754 displayargs['format'] = DisplayFormats.Hotpath
755 displayargs['format'] = DisplayFormats.Hotpath
755 elif argv[1] == 'lines':
756 elif argv[1] == 'lines':
756 displayargs['format'] = DisplayFormats.ByLine
757 displayargs['format'] = DisplayFormats.ByLine
757 elif argv[1] == 'functions':
758 elif argv[1] == 'functions':
758 displayargs['format'] = DisplayFormats.ByMethod
759 displayargs['format'] = DisplayFormats.ByMethod
759 elif argv[1] == 'function':
760 elif argv[1] == 'function':
760 displayargs['format'] = DisplayFormats.AboutMethod
761 displayargs['format'] = DisplayFormats.AboutMethod
761 displayargs['function'] = argv[2]
762 displayargs['function'] = argv[2]
762 optstart = 3
763 optstart = 3
763 elif argv[1] == 'flame':
764 elif argv[1] == 'flame':
764 displayargs['format'] = DisplayFormats.FlameGraph
765 displayargs['format'] = DisplayFormats.FlameGraph
765 else:
766 else:
766 printusage()
767 printusage()
767 return 0
768 return 0
768
769
769 # process options
770 # process options
770 try:
771 try:
771 opts, args = getopt.getopt(sys.argv[optstart:], "hl:f:o:p:",
772 opts, args = getopt.getopt(sys.argv[optstart:], "hl:f:o:p:",
772 ["help", "limit=", "file=", "output-file=", "script-path="])
773 ["help", "limit=", "file=", "output-file=", "script-path="])
773 except getopt.error as msg:
774 except getopt.error as msg:
774 print(msg)
775 print(msg)
775 printusage()
776 printusage()
776 return 2
777 return 2
777
778
778 displayargs['limit'] = 0.05
779 displayargs['limit'] = 0.05
779 path = None
780 path = None
780 for o, value in opts:
781 for o, value in opts:
781 if o in ("-l", "--limit"):
782 if o in ("-l", "--limit"):
782 displayargs['limit'] = float(value)
783 displayargs['limit'] = float(value)
783 elif o in ("-f", "--file"):
784 elif o in ("-f", "--file"):
784 path = value
785 path = value
785 elif o in ("-o", "--output-file"):
786 elif o in ("-o", "--output-file"):
786 displayargs['outputfile'] = value
787 displayargs['outputfile'] = value
787 elif o in ("-p", "--script-path"):
788 elif o in ("-p", "--script-path"):
788 displayargs['scriptpath'] = value
789 displayargs['scriptpath'] = value
789 elif o in ("-h", "help"):
790 elif o in ("-h", "help"):
790 printusage()
791 printusage()
791 return 0
792 return 0
792 else:
793 else:
793 assert False, "unhandled option %s" % o
794 assert False, "unhandled option %s" % o
794
795
795 load_data(path=path)
796 load_data(path=path)
796
797
797 display(**displayargs)
798 display(**displayargs)
798
799
799 return 0
800 return 0
800
801
801 if __name__ == "__main__":
802 if __name__ == "__main__":
802 sys.exit(main())
803 sys.exit(main())
General Comments 0
You need to be logged in to leave comments. Login now