##// 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 1 #!/usr/bin/env python
2 2 ## statprof.py
3 3 ## Copyright (C) 2012 Bryan O'Sullivan <bos@serpentine.com>
4 4 ## Copyright (C) 2011 Alex Fraser <alex at phatcore dot com>
5 5 ## Copyright (C) 2004,2005 Andy Wingo <wingo at pobox dot com>
6 6 ## Copyright (C) 2001 Rob Browning <rlb at defaultvalue dot org>
7 7
8 8 ## This library is free software; you can redistribute it and/or
9 9 ## modify it under the terms of the GNU Lesser General Public
10 10 ## License as published by the Free Software Foundation; either
11 11 ## version 2.1 of the License, or (at your option) any later version.
12 12 ##
13 13 ## This library is distributed in the hope that it will be useful,
14 14 ## but WITHOUT ANY WARRANTY; without even the implied warranty of
15 15 ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
16 16 ## Lesser General Public License for more details.
17 17 ##
18 18 ## You should have received a copy of the GNU Lesser General Public
19 19 ## License along with this program; if not, contact:
20 20 ##
21 21 ## Free Software Foundation Voice: +1-617-542-5942
22 22 ## 59 Temple Place - Suite 330 Fax: +1-617-542-2652
23 23 ## Boston, MA 02111-1307, USA gnu@gnu.org
24 24
25 25 """
26 26 statprof is intended to be a fairly simple statistical profiler for
27 27 python. It was ported directly from a statistical profiler for guile,
28 28 also named statprof, available from guile-lib [0].
29 29
30 30 [0] http://wingolog.org/software/guile-lib/statprof/
31 31
32 32 To start profiling, call statprof.start():
33 33 >>> start()
34 34
35 35 Then run whatever it is that you want to profile, for example:
36 36 >>> import test.pystone; test.pystone.pystones()
37 37
38 38 Then stop the profiling and print out the results:
39 39 >>> stop()
40 40 >>> display()
41 41 % cumulative self
42 42 time seconds seconds name
43 43 26.72 1.40 0.37 pystone.py:79:Proc0
44 44 13.79 0.56 0.19 pystone.py:133:Proc1
45 45 13.79 0.19 0.19 pystone.py:208:Proc8
46 46 10.34 0.16 0.14 pystone.py:229:Func2
47 47 6.90 0.10 0.10 pystone.py:45:__init__
48 48 4.31 0.16 0.06 pystone.py:53:copy
49 49 ...
50 50
51 51 All of the numerical data is statistically approximate. In the
52 52 following column descriptions, and in all of statprof, "time" refers
53 53 to execution time (both user and system), not wall clock time.
54 54
55 55 % time
56 56 The percent of the time spent inside the procedure itself (not
57 57 counting children).
58 58
59 59 cumulative seconds
60 60 The total number of seconds spent in the procedure, including
61 61 children.
62 62
63 63 self seconds
64 64 The total number of seconds spent in the procedure itself (not
65 65 counting children).
66 66
67 67 name
68 68 The name of the procedure.
69 69
70 70 By default statprof keeps the data collected from previous runs. If you
71 71 want to clear the collected data, call reset():
72 72 >>> reset()
73 73
74 74 reset() can also be used to change the sampling frequency from the
75 75 default of 1000 Hz. For example, to tell statprof to sample 50 times a
76 76 second:
77 77 >>> reset(50)
78 78
79 79 This means that statprof will sample the call stack after every 1/50 of
80 80 a second of user + system time spent running on behalf of the python
81 81 process. When your process is idle (for example, blocking in a read(),
82 82 as is the case at the listener), the clock does not advance. For this
83 83 reason statprof is not currently not suitable for profiling io-bound
84 84 operations.
85 85
86 86 The profiler uses the hash of the code object itself to identify the
87 87 procedures, so it won't confuse different procedures with the same name.
88 88 They will show up as two different rows in the output.
89 89
90 90 Right now the profiler is quite simplistic. I cannot provide
91 91 call-graphs or other higher level information. What you see in the
92 92 table is pretty much all there is. Patches are welcome :-)
93 93
94 94
95 95 Threading
96 96 ---------
97 97
98 98 Because signals only get delivered to the main thread in Python,
99 99 statprof only profiles the main thread. However because the time
100 100 reporting function uses per-process timers, the results can be
101 101 significantly off if other threads' work patterns are not similar to the
102 102 main thread's work patterns.
103 103 """
104 104 # no-check-code
105 105 from __future__ import division
106 106
107 107 import inspect, json, os, signal, tempfile, sys, getopt, threading
108 108 import time
109 109 from collections import defaultdict
110 110 from contextlib import contextmanager
111 111
112 112 __all__ = ['start', 'stop', 'reset', 'display', 'profile']
113 113
114 114 skips = set(["util.py:check", "extensions.py:closure",
115 115 "color.py:colorcmd", "dispatch.py:checkargs",
116 116 "dispatch.py:<lambda>", "dispatch.py:_runcatch",
117 117 "dispatch.py:_dispatch", "dispatch.py:_runcommand",
118 118 "pager.py:pagecmd", "dispatch.py:run",
119 119 "dispatch.py:dispatch", "dispatch.py:runcommand",
120 120 "hg.py:<module>", "evolve.py:warnobserrors",
121 121 ])
122 122
123 123 ###########################################################################
124 124 ## Utils
125 125
126 126 def clock():
127 127 times = os.times()
128 128 return times[0] + times[1]
129 129
130 130
131 131 ###########################################################################
132 132 ## Collection data structures
133 133
134 134 class ProfileState(object):
135 135 def __init__(self, frequency=None):
136 136 self.reset(frequency)
137 137
138 138 def reset(self, frequency=None):
139 139 # total so far
140 140 self.accumulated_time = 0.0
141 141 # start_time when timer is active
142 142 self.last_start_time = None
143 143 # a float
144 144 if frequency:
145 145 self.sample_interval = 1.0 / frequency
146 146 elif not hasattr(self, 'sample_interval'):
147 147 # default to 1000 Hz
148 148 self.sample_interval = 1.0 / 1000.0
149 149 else:
150 150 # leave the frequency as it was
151 151 pass
152 152 self.remaining_prof_time = None
153 153 # for user start/stop nesting
154 154 self.profile_level = 0
155 155
156 156 self.samples = []
157 157
158 158 def accumulate_time(self, stop_time):
159 159 self.accumulated_time += stop_time - self.last_start_time
160 160
161 161 def seconds_per_sample(self):
162 162 return self.accumulated_time / len(self.samples)
163 163
164 164 state = ProfileState()
165 165
166 166
167 167 class CodeSite(object):
168 168 cache = {}
169 169
170 170 __slots__ = ('path', 'lineno', 'function', 'source')
171 171
172 172 def __init__(self, path, lineno, function):
173 173 self.path = path
174 174 self.lineno = lineno
175 175 self.function = function
176 176 self.source = None
177 177
178 178 def __eq__(self, other):
179 179 try:
180 180 return (self.lineno == other.lineno and
181 181 self.path == other.path)
182 182 except:
183 183 return False
184 184
185 185 def __hash__(self):
186 186 return hash((self.lineno, self.path))
187 187
188 188 @classmethod
189 189 def get(cls, path, lineno, function):
190 190 k = (path, lineno)
191 191 try:
192 192 return cls.cache[k]
193 193 except KeyError:
194 194 v = cls(path, lineno, function)
195 195 cls.cache[k] = v
196 196 return v
197 197
198 198 def getsource(self, length):
199 199 if self.source is None:
200 200 lineno = self.lineno - 1
201 201 fp = None
202 202 try:
203 203 fp = open(self.path)
204 204 for i, line in enumerate(fp):
205 205 if i == lineno:
206 206 self.source = line.strip()
207 207 break
208 208 except:
209 209 pass
210 210 finally:
211 211 if fp:
212 212 fp.close()
213 213 if self.source is None:
214 214 self.source = ''
215 215
216 216 source = self.source
217 217 if len(source) > length:
218 218 source = source[:(length - 3)] + "..."
219 219 return source
220 220
221 221 def filename(self):
222 222 return os.path.basename(self.path)
223 223
224 224 class Sample(object):
225 225 __slots__ = ('stack', 'time')
226 226
227 227 def __init__(self, stack, time):
228 228 self.stack = stack
229 229 self.time = time
230 230
231 231 @classmethod
232 232 def from_frame(cls, frame, time):
233 233 stack = []
234 234
235 235 while frame:
236 236 stack.append(CodeSite.get(frame.f_code.co_filename, frame.f_lineno,
237 237 frame.f_code.co_name))
238 238 frame = frame.f_back
239 239
240 240 return Sample(stack, time)
241 241
242 242 ###########################################################################
243 243 ## SIGPROF handler
244 244
245 245 def profile_signal_handler(signum, frame):
246 246 if state.profile_level > 0:
247 247 now = clock()
248 248 state.accumulate_time(now)
249 249
250 250 state.samples.append(Sample.from_frame(frame, state.accumulated_time))
251 251
252 252 signal.setitimer(signal.ITIMER_PROF,
253 253 state.sample_interval, 0.0)
254 254 state.last_start_time = now
255 255
256 256 stopthread = threading.Event()
257 257 def samplerthread(tid):
258 258 while not stopthread.is_set():
259 259 now = clock()
260 260 state.accumulate_time(now)
261 261
262 262 frame = sys._current_frames()[tid]
263 263 state.samples.append(Sample.from_frame(frame, state.accumulated_time))
264 264
265 265 state.last_start_time = now
266 266 time.sleep(state.sample_interval)
267 267
268 268 stopthread.clear()
269 269
270 270 ###########################################################################
271 271 ## Profiling API
272 272
273 273 def is_active():
274 274 return state.profile_level > 0
275 275
276 276 lastmechanism = None
277 277 def start(mechanism='thread'):
278 278 '''Install the profiling signal handler, and start profiling.'''
279 279 state.profile_level += 1
280 280 if state.profile_level == 1:
281 281 state.last_start_time = clock()
282 282 rpt = state.remaining_prof_time
283 283 state.remaining_prof_time = None
284 284
285 285 global lastmechanism
286 286 lastmechanism = mechanism
287 287
288 288 if mechanism == 'signal':
289 289 signal.signal(signal.SIGPROF, profile_signal_handler)
290 290 signal.setitimer(signal.ITIMER_PROF,
291 291 rpt or state.sample_interval, 0.0)
292 292 elif mechanism == 'thread':
293 293 frame = inspect.currentframe()
294 294 tid = [k for k, f in sys._current_frames().items() if f == frame][0]
295 295 state.thread = threading.Thread(target=samplerthread,
296 296 args=(tid,), name="samplerthread")
297 297 state.thread.start()
298 298
299 299 def stop():
300 300 '''Stop profiling, and uninstall the profiling signal handler.'''
301 301 state.profile_level -= 1
302 302 if state.profile_level == 0:
303 303 if lastmechanism == 'signal':
304 304 rpt = signal.setitimer(signal.ITIMER_PROF, 0.0, 0.0)
305 305 signal.signal(signal.SIGPROF, signal.SIG_IGN)
306 306 state.remaining_prof_time = rpt[0]
307 307 elif lastmechanism == 'thread':
308 308 stopthread.set()
309 309 state.thread.join()
310 310
311 311 state.accumulate_time(clock())
312 312 state.last_start_time = None
313 313 statprofpath = os.environ.get('STATPROF_DEST')
314 if statprofpath:
314 315 save_data(statprofpath)
315 316
316 def save_data(path=None):
317 try:
318 path = path or (os.environ['HOME'] + '/statprof.data')
319 file = open(path, "w+")
320
317 def save_data(path):
318 with open(path, 'w+') as file:
321 319 file.write(str(state.accumulated_time) + '\n')
322 320 for sample in state.samples:
323 321 time = str(sample.time)
324 322 stack = sample.stack
325 323 sites = ['\1'.join([s.path, str(s.lineno), s.function])
326 324 for s in stack]
327 325 file.write(time + '\0' + '\0'.join(sites) + '\n')
328 326
329 file.close()
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')
327 def load_data(path):
336 328 lines = open(path, 'r').read().splitlines()
337 329
338 330 state.accumulated_time = float(lines[0])
339 331 state.samples = []
340 332 for line in lines[1:]:
341 333 parts = line.split('\0')
342 334 time = float(parts[0])
343 335 rawsites = parts[1:]
344 336 sites = []
345 337 for rawsite in rawsites:
346 338 siteparts = rawsite.split('\1')
347 339 sites.append(CodeSite.get(siteparts[0], int(siteparts[1]),
348 340 siteparts[2]))
349 341
350 342 state.samples.append(Sample(sites, time))
351 343
352 344
353 345
354 346 def reset(frequency=None):
355 347 '''Clear out the state of the profiler. Do not call while the
356 348 profiler is running.
357 349
358 350 The optional frequency argument specifies the number of samples to
359 351 collect per second.'''
360 352 assert state.profile_level == 0, "Can't reset() while statprof is running"
361 353 CodeSite.cache.clear()
362 354 state.reset(frequency)
363 355
364 356
365 357 @contextmanager
366 358 def profile():
367 359 start()
368 360 try:
369 361 yield
370 362 finally:
371 363 stop()
372 364 display()
373 365
374 366
375 367 ###########################################################################
376 368 ## Reporting API
377 369
378 370 class SiteStats(object):
379 371 def __init__(self, site):
380 372 self.site = site
381 373 self.selfcount = 0
382 374 self.totalcount = 0
383 375
384 376 def addself(self):
385 377 self.selfcount += 1
386 378
387 379 def addtotal(self):
388 380 self.totalcount += 1
389 381
390 382 def selfpercent(self):
391 383 return self.selfcount / len(state.samples) * 100
392 384
393 385 def totalpercent(self):
394 386 return self.totalcount / len(state.samples) * 100
395 387
396 388 def selfseconds(self):
397 389 return self.selfcount * state.seconds_per_sample()
398 390
399 391 def totalseconds(self):
400 392 return self.totalcount * state.seconds_per_sample()
401 393
402 394 @classmethod
403 395 def buildstats(cls, samples):
404 396 stats = {}
405 397
406 398 for sample in samples:
407 399 for i, site in enumerate(sample.stack):
408 400 sitestat = stats.get(site)
409 401 if not sitestat:
410 402 sitestat = SiteStats(site)
411 403 stats[site] = sitestat
412 404
413 405 sitestat.addtotal()
414 406
415 407 if i == 0:
416 408 sitestat.addself()
417 409
418 410 return [s for s in stats.itervalues()]
419 411
420 412 class DisplayFormats:
421 413 ByLine = 0
422 414 ByMethod = 1
423 415 AboutMethod = 2
424 416 Hotpath = 3
425 417 FlameGraph = 4
426 418 Json = 5
427 419
428 420 def display(fp=None, format=3, **kwargs):
429 421 '''Print statistics, either to stdout or the given file object.'''
430 422
431 423 if fp is None:
432 424 import sys
433 425 fp = sys.stdout
434 426 if len(state.samples) == 0:
435 427 print >> fp, ('No samples recorded.')
436 428 return
437 429
438 430 if format == DisplayFormats.ByLine:
439 431 display_by_line(fp)
440 432 elif format == DisplayFormats.ByMethod:
441 433 display_by_method(fp)
442 434 elif format == DisplayFormats.AboutMethod:
443 435 display_about_method(fp, **kwargs)
444 436 elif format == DisplayFormats.Hotpath:
445 437 display_hotpath(fp, **kwargs)
446 438 elif format == DisplayFormats.FlameGraph:
447 439 write_to_flame(fp, **kwargs)
448 440 elif format == DisplayFormats.Json:
449 441 write_to_json(fp)
450 442 else:
451 443 raise Exception("Invalid display format")
452 444
453 445 if format != DisplayFormats.Json:
454 446 print >> fp, ('---')
455 447 print >> fp, ('Sample count: %d' % len(state.samples))
456 448 print >> fp, ('Total time: %f seconds' % state.accumulated_time)
457 449
458 450 def display_by_line(fp):
459 451 '''Print the profiler data with each sample line represented
460 452 as one row in a table. Sorted by self-time per line.'''
461 453 stats = SiteStats.buildstats(state.samples)
462 454 stats.sort(reverse=True, key=lambda x: x.selfseconds())
463 455
464 456 print >> fp, ('%5.5s %10.10s %7.7s %-8.8s' %
465 457 ('% ', 'cumulative', 'self', ''))
466 458 print >> fp, ('%5.5s %9.9s %8.8s %-8.8s' %
467 459 ("time", "seconds", "seconds", "name"))
468 460
469 461 for stat in stats:
470 462 site = stat.site
471 463 sitelabel = '%s:%d:%s' % (site.filename(), site.lineno, site.function)
472 464 print >> fp, ('%6.2f %9.2f %9.2f %s' % (stat.selfpercent(),
473 465 stat.totalseconds(),
474 466 stat.selfseconds(),
475 467 sitelabel))
476 468
477 469 def display_by_method(fp):
478 470 '''Print the profiler data with each sample function represented
479 471 as one row in a table. Important lines within that function are
480 472 output as nested rows. Sorted by self-time per line.'''
481 473 print >> fp, ('%5.5s %10.10s %7.7s %-8.8s' %
482 474 ('% ', 'cumulative', 'self', ''))
483 475 print >> fp, ('%5.5s %9.9s %8.8s %-8.8s' %
484 476 ("time", "seconds", "seconds", "name"))
485 477
486 478 stats = SiteStats.buildstats(state.samples)
487 479
488 480 grouped = defaultdict(list)
489 481 for stat in stats:
490 482 grouped[stat.site.filename() + ":" + stat.site.function].append(stat)
491 483
492 484 # compute sums for each function
493 485 functiondata = []
494 486 for fname, sitestats in grouped.iteritems():
495 487 total_cum_sec = 0
496 488 total_self_sec = 0
497 489 total_percent = 0
498 490 for stat in sitestats:
499 491 total_cum_sec += stat.totalseconds()
500 492 total_self_sec += stat.selfseconds()
501 493 total_percent += stat.selfpercent()
502 494
503 495 functiondata.append((fname,
504 496 total_cum_sec,
505 497 total_self_sec,
506 498 total_percent,
507 499 sitestats))
508 500
509 501 # sort by total self sec
510 502 functiondata.sort(reverse=True, key=lambda x: x[2])
511 503
512 504 for function in functiondata:
513 505 if function[3] < 0.05:
514 506 continue
515 507 print >> fp, ('%6.2f %9.2f %9.2f %s' % (function[3], # total percent
516 508 function[1], # total cum sec
517 509 function[2], # total self sec
518 510 function[0])) # file:function
519 511 function[4].sort(reverse=True, key=lambda i: i.selfseconds())
520 512 for stat in function[4]:
521 513 # only show line numbers for significant locations (>1% time spent)
522 514 if stat.selfpercent() > 1:
523 515 source = stat.site.getsource(25)
524 516 stattuple = (stat.selfpercent(), stat.selfseconds(),
525 517 stat.site.lineno, source)
526 518
527 519 print >> fp, ('%33.0f%% %6.2f line %s: %s' % (stattuple))
528 520
529 521 def display_about_method(fp, function=None, **kwargs):
530 522 if function is None:
531 523 raise Exception("Invalid function")
532 524
533 525 filename = None
534 526 if ':' in function:
535 527 filename, function = function.split(':')
536 528
537 529 relevant_samples = 0
538 530 parents = {}
539 531 children = {}
540 532
541 533 for sample in state.samples:
542 534 for i, site in enumerate(sample.stack):
543 535 if site.function == function and (not filename
544 536 or site.filename() == filename):
545 537 relevant_samples += 1
546 538 if i != len(sample.stack) - 1:
547 539 parent = sample.stack[i + 1]
548 540 if parent in parents:
549 541 parents[parent] = parents[parent] + 1
550 542 else:
551 543 parents[parent] = 1
552 544
553 545 if site in children:
554 546 children[site] = children[site] + 1
555 547 else:
556 548 children[site] = 1
557 549
558 550 parents = [(parent, count) for parent, count in parents.iteritems()]
559 551 parents.sort(reverse=True, key=lambda x: x[1])
560 552 for parent, count in parents:
561 553 print >> fp, ('%6.2f%% %s:%s line %s: %s' %
562 554 (count / relevant_samples * 100, parent.filename(),
563 555 parent.function, parent.lineno, parent.getsource(50)))
564 556
565 557 stats = SiteStats.buildstats(state.samples)
566 558 stats = [s for s in stats
567 559 if s.site.function == function and
568 560 (not filename or s.site.filename() == filename)]
569 561
570 562 total_cum_sec = 0
571 563 total_self_sec = 0
572 564 total_self_percent = 0
573 565 total_cum_percent = 0
574 566 for stat in stats:
575 567 total_cum_sec += stat.totalseconds()
576 568 total_self_sec += stat.selfseconds()
577 569 total_self_percent += stat.selfpercent()
578 570 total_cum_percent += stat.totalpercent()
579 571
580 572 print >> fp, (
581 573 '\n %s:%s Total: %0.2fs (%0.2f%%) Self: %0.2fs (%0.2f%%)\n' %
582 574 (
583 575 filename or '___',
584 576 function,
585 577 total_cum_sec,
586 578 total_cum_percent,
587 579 total_self_sec,
588 580 total_self_percent
589 581 ))
590 582
591 583 children = [(child, count) for child, count in children.iteritems()]
592 584 children.sort(reverse=True, key=lambda x: x[1])
593 585 for child, count in children:
594 586 print >> fp, (' %6.2f%% line %s: %s' %
595 587 (count / relevant_samples * 100, child.lineno, child.getsource(50)))
596 588
597 589 def display_hotpath(fp, limit=0.05, **kwargs):
598 590 class HotNode(object):
599 591 def __init__(self, site):
600 592 self.site = site
601 593 self.count = 0
602 594 self.children = {}
603 595
604 596 def add(self, stack, time):
605 597 self.count += time
606 598 site = stack[0]
607 599 child = self.children.get(site)
608 600 if not child:
609 601 child = HotNode(site)
610 602 self.children[site] = child
611 603
612 604 if len(stack) > 1:
613 605 i = 1
614 606 # Skip boiler plate parts of the stack
615 607 while i < len(stack) and '%s:%s' % (stack[i].filename(), stack[i].function) in skips:
616 608 i += 1
617 609 if i < len(stack):
618 610 child.add(stack[i:], time)
619 611
620 612 root = HotNode(None)
621 613 lasttime = state.samples[0].time
622 614 for sample in state.samples:
623 615 root.add(sample.stack[::-1], sample.time - lasttime)
624 616 lasttime = sample.time
625 617
626 618 def _write(node, depth, multiple_siblings):
627 619 site = node.site
628 620 visiblechildren = [c for c in node.children.itervalues()
629 621 if c.count >= (limit * root.count)]
630 622 if site:
631 623 indent = depth * 2 - 1
632 624 filename = ''
633 625 function = ''
634 626 if len(node.children) > 0:
635 627 childsite = list(node.children.itervalues())[0].site
636 628 filename = (childsite.filename() + ':').ljust(15)
637 629 function = childsite.function
638 630
639 631 # lots of string formatting
640 632 listpattern = ''.ljust(indent) +\
641 633 ('\\' if multiple_siblings else '|') +\
642 634 ' %4.1f%% %s %s'
643 635 liststring = listpattern % (node.count / root.count * 100,
644 636 filename, function)
645 637 codepattern = '%' + str(55 - len(liststring)) + 's %s: %s'
646 638 codestring = codepattern % ('line', site.lineno, site.getsource(30))
647 639
648 640 finalstring = liststring + codestring
649 641 childrensamples = sum([c.count for c in node.children.itervalues()])
650 642 # Make frames that performed more than 10% of the operation red
651 643 if node.count - childrensamples > (0.1 * root.count):
652 644 finalstring = '\033[91m' + finalstring + '\033[0m'
653 645 # Make frames that didn't actually perform work dark grey
654 646 elif node.count - childrensamples == 0:
655 647 finalstring = '\033[90m' + finalstring + '\033[0m'
656 648 print >> fp, finalstring
657 649
658 650 newdepth = depth
659 651 if len(visiblechildren) > 1 or multiple_siblings:
660 652 newdepth += 1
661 653
662 654 visiblechildren.sort(reverse=True, key=lambda x: x.count)
663 655 for child in visiblechildren:
664 656 _write(child, newdepth, len(visiblechildren) > 1)
665 657
666 658 if root.count > 0:
667 659 _write(root, 0, False)
668 660
669 661 def write_to_flame(fp, scriptpath=None, outputfile=None, **kwargs):
670 662 if scriptpath is None:
671 663 scriptpath = os.environ['HOME'] + '/flamegraph.pl'
672 664 if not os.path.exists(scriptpath):
673 665 print >> fp, "error: missing %s" % scriptpath
674 666 print >> fp, "get it here: https://github.com/brendangregg/FlameGraph"
675 667 return
676 668
677 669 fd, path = tempfile.mkstemp()
678 670
679 671 file = open(path, "w+")
680 672
681 673 lines = {}
682 674 for sample in state.samples:
683 675 sites = [s.function for s in sample.stack]
684 676 sites.reverse()
685 677 line = ';'.join(sites)
686 678 if line in lines:
687 679 lines[line] = lines[line] + 1
688 680 else:
689 681 lines[line] = 1
690 682
691 683 for line, count in lines.iteritems():
692 684 file.write("%s %s\n" % (line, count))
693 685
694 686 file.close()
695 687
696 688 if outputfile is None:
697 689 outputfile = '~/flamegraph.svg'
698 690
699 691 os.system("perl ~/flamegraph.pl %s > %s" % (path, outputfile))
700 692 print "Written to %s" % outputfile
701 693
702 694 def write_to_json(fp):
703 695 samples = []
704 696
705 697 for sample in state.samples:
706 698 stack = []
707 699
708 700 for frame in sample.stack:
709 701 stack.append((frame.path, frame.lineno, frame.function))
710 702
711 703 samples.append((sample.time, stack))
712 704
713 705 print >> fp, json.dumps(samples)
714 706
715 707 def printusage():
716 708 print """
717 709 The statprof command line allows you to inspect the last profile's results in
718 710 the following forms:
719 711
720 712 usage:
721 713 hotpath [-l --limit percent]
722 714 Shows a graph of calls with the percent of time each takes.
723 715 Red calls take over 10%% of the total time themselves.
724 716 lines
725 717 Shows the actual sampled lines.
726 718 functions
727 719 Shows the samples grouped by function.
728 720 function [filename:]functionname
729 721 Shows the callers and callees of a particular function.
730 722 flame [-s --script-path] [-o --output-file path]
731 723 Writes out a flamegraph to output-file (defaults to ~/flamegraph.svg)
732 724 Requires that ~/flamegraph.pl exist.
733 725 (Specify alternate script path with --script-path.)"""
734 726
735 727 def main(argv=None):
736 728 if argv is None:
737 729 argv = sys.argv
738 730
739 731 if len(argv) == 1:
740 732 printusage()
741 733 return 0
742 734
743 735 displayargs = {}
744 736
745 737 optstart = 2
746 738 displayargs['function'] = None
747 739 if argv[1] == 'hotpath':
748 740 displayargs['format'] = DisplayFormats.Hotpath
749 741 elif argv[1] == 'lines':
750 742 displayargs['format'] = DisplayFormats.ByLine
751 743 elif argv[1] == 'functions':
752 744 displayargs['format'] = DisplayFormats.ByMethod
753 745 elif argv[1] == 'function':
754 746 displayargs['format'] = DisplayFormats.AboutMethod
755 747 displayargs['function'] = argv[2]
756 748 optstart = 3
757 749 elif argv[1] == 'flame':
758 750 displayargs['format'] = DisplayFormats.FlameGraph
759 751 else:
760 752 printusage()
761 753 return 0
762 754
763 755 # process options
764 756 try:
765 757 opts, args = getopt.getopt(sys.argv[optstart:], "hl:f:o:p:",
766 758 ["help", "limit=", "file=", "output-file=", "script-path="])
767 759 except getopt.error as msg:
768 760 print msg
769 761 printusage()
770 762 return 2
771 763
772 764 displayargs['limit'] = 0.05
773 765 path = None
774 766 for o, value in opts:
775 767 if o in ("-l", "--limit"):
776 768 displayargs['limit'] = float(value)
777 769 elif o in ("-f", "--file"):
778 770 path = value
779 771 elif o in ("-o", "--output-file"):
780 772 displayargs['outputfile'] = value
781 773 elif o in ("-p", "--script-path"):
782 774 displayargs['scriptpath'] = value
783 775 elif o in ("-h", "help"):
784 776 printusage()
785 777 return 0
786 778 else:
787 779 assert False, "unhandled option %s" % o
788 780
789 781 load_data(path=path)
790 782
791 783 display(**displayargs)
792 784
793 785 return 0
794 786
795 787 if __name__ == "__main__":
796 788 sys.exit(main())
General Comments 0
You need to be logged in to leave comments. Login now