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