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