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