comparison mercurial/statprof.py @ 43076:2372284d9457

formatting: blacken the codebase This is using my patch to black (https://github.com/psf/black/pull/826) so we don't un-wrap collection literals. Done with: hg files 'set:**.py - mercurial/thirdparty/** - "contrib/python-zstandard/**"' | xargs black -S # skip-blame mass-reformatting only # no-check-commit reformats foo_bar functions Differential Revision: https://phab.mercurial-scm.org/D6971
author Augie Fackler <augie@google.com>
date Sun, 06 Oct 2019 09:45:02 -0400
parents 9a3be115fb78
children 687b865b95ad
comparison
equal deleted inserted replaced
43075:57875cf423c9 43076:2372284d9457
142 } 142 }
143 143
144 ########################################################################### 144 ###########################################################################
145 ## Utils 145 ## Utils
146 146
147
147 def clock(): 148 def clock():
148 times = os.times() 149 times = os.times()
149 return (times[0] + times[1], times[4]) 150 return (times[0] + times[1], times[4])
150 151
151 152
152 ########################################################################### 153 ###########################################################################
153 ## Collection data structures 154 ## Collection data structures
155
154 156
155 class ProfileState(object): 157 class ProfileState(object):
156 def __init__(self, frequency=None): 158 def __init__(self, frequency=None):
157 self.reset(frequency) 159 self.reset(frequency)
158 self.track = 'cpu' 160 self.track = 'cpu'
194 def timeidx(self): 196 def timeidx(self):
195 if self.track == 'real': 197 if self.track == 'real':
196 return 1 198 return 1
197 return 0 199 return 0
198 200
201
199 state = ProfileState() 202 state = ProfileState()
200 203
201 204
202 class CodeSite(object): 205 class CodeSite(object):
203 cache = {} 206 cache = {}
212 self.function = function 215 self.function = function
213 self.source = None 216 self.source = None
214 217
215 def __eq__(self, other): 218 def __eq__(self, other):
216 try: 219 try:
217 return (self.lineno == other.lineno and 220 return self.lineno == other.lineno and self.path == other.path
218 self.path == other.path)
219 except: 221 except:
220 return False 222 return False
221 223
222 def __hash__(self): 224 def __hash__(self):
223 return hash((self.lineno, self.path)) 225 return hash((self.lineno, self.path))
246 if self.source is None: 248 if self.source is None:
247 self.source = '' 249 self.source = ''
248 250
249 source = self.source 251 source = self.source
250 if len(source) > length: 252 if len(source) > length:
251 source = source[:(length - 3)] + "..." 253 source = source[: (length - 3)] + "..."
252 return source 254 return source
253 255
254 def filename(self): 256 def filename(self):
255 return os.path.basename(self.path) 257 return os.path.basename(self.path)
256 258
257 def skipname(self): 259 def skipname(self):
258 return r'%s:%s' % (self.filename(), self.function) 260 return r'%s:%s' % (self.filename(), self.function)
261
259 262
260 class Sample(object): 263 class Sample(object):
261 __slots__ = (r'stack', r'time') 264 __slots__ = (r'stack', r'time')
262 265
263 def __init__(self, stack, time): 266 def __init__(self, stack, time):
267 @classmethod 270 @classmethod
268 def from_frame(cls, frame, time): 271 def from_frame(cls, frame, time):
269 stack = [] 272 stack = []
270 273
271 while frame: 274 while frame:
272 stack.append(CodeSite.get( 275 stack.append(
273 pycompat.sysbytes(frame.f_code.co_filename), 276 CodeSite.get(
274 frame.f_lineno, 277 pycompat.sysbytes(frame.f_code.co_filename),
275 pycompat.sysbytes(frame.f_code.co_name))) 278 frame.f_lineno,
279 pycompat.sysbytes(frame.f_code.co_name),
280 )
281 )
276 frame = frame.f_back 282 frame = frame.f_back
277 283
278 return Sample(stack, time) 284 return Sample(stack, time)
285
279 286
280 ########################################################################### 287 ###########################################################################
281 ## SIGPROF handler 288 ## SIGPROF handler
289
282 290
283 def profile_signal_handler(signum, frame): 291 def profile_signal_handler(signum, frame):
284 if state.profile_level > 0: 292 if state.profile_level > 0:
285 now = clock() 293 now = clock()
286 state.accumulate_time(now) 294 state.accumulate_time(now)
287 295
288 timestamp = state.accumulated_time[state.timeidx] 296 timestamp = state.accumulated_time[state.timeidx]
289 state.samples.append(Sample.from_frame(frame, timestamp)) 297 state.samples.append(Sample.from_frame(frame, timestamp))
290 298
291 signal.setitimer(signal.ITIMER_PROF, 299 signal.setitimer(signal.ITIMER_PROF, state.sample_interval, 0.0)
292 state.sample_interval, 0.0)
293 state.last_start_time = now 300 state.last_start_time = now
294 301
302
295 stopthread = threading.Event() 303 stopthread = threading.Event()
304
305
296 def samplerthread(tid): 306 def samplerthread(tid):
297 while not stopthread.is_set(): 307 while not stopthread.is_set():
298 now = clock() 308 now = clock()
299 state.accumulate_time(now) 309 state.accumulate_time(now)
300 310
306 state.last_start_time = now 316 state.last_start_time = now
307 time.sleep(state.sample_interval) 317 time.sleep(state.sample_interval)
308 318
309 stopthread.clear() 319 stopthread.clear()
310 320
321
311 ########################################################################### 322 ###########################################################################
312 ## Profiling API 323 ## Profiling API
313 324
325
314 def is_active(): 326 def is_active():
315 return state.profile_level > 0 327 return state.profile_level > 0
316 328
329
317 lastmechanism = None 330 lastmechanism = None
331
332
318 def start(mechanism='thread', track='cpu'): 333 def start(mechanism='thread', track='cpu'):
319 '''Install the profiling signal handler, and start profiling.''' 334 '''Install the profiling signal handler, and start profiling.'''
320 state.track = track # note: nesting different mode won't work 335 state.track = track # note: nesting different mode won't work
321 state.profile_level += 1 336 state.profile_level += 1
322 if state.profile_level == 1: 337 if state.profile_level == 1:
323 state.last_start_time = clock() 338 state.last_start_time = clock()
324 rpt = state.remaining_prof_time 339 rpt = state.remaining_prof_time
325 state.remaining_prof_time = None 340 state.remaining_prof_time = None
327 global lastmechanism 342 global lastmechanism
328 lastmechanism = mechanism 343 lastmechanism = mechanism
329 344
330 if mechanism == 'signal': 345 if mechanism == 'signal':
331 signal.signal(signal.SIGPROF, profile_signal_handler) 346 signal.signal(signal.SIGPROF, profile_signal_handler)
332 signal.setitimer(signal.ITIMER_PROF, 347 signal.setitimer(
333 rpt or state.sample_interval, 0.0) 348 signal.ITIMER_PROF, rpt or state.sample_interval, 0.0
349 )
334 elif mechanism == 'thread': 350 elif mechanism == 'thread':
335 frame = inspect.currentframe() 351 frame = inspect.currentframe()
336 tid = [k for k, f in sys._current_frames().items() if f == frame][0] 352 tid = [k for k, f in sys._current_frames().items() if f == frame][0]
337 state.thread = threading.Thread(target=samplerthread, 353 state.thread = threading.Thread(
338 args=(tid,), name="samplerthread") 354 target=samplerthread, args=(tid,), name="samplerthread"
355 )
339 state.thread.start() 356 state.thread.start()
357
340 358
341 def stop(): 359 def stop():
342 '''Stop profiling, and uninstall the profiling signal handler.''' 360 '''Stop profiling, and uninstall the profiling signal handler.'''
343 state.profile_level -= 1 361 state.profile_level -= 1
344 if state.profile_level == 0: 362 if state.profile_level == 0:
356 if statprofpath: 374 if statprofpath:
357 save_data(statprofpath) 375 save_data(statprofpath)
358 376
359 return state 377 return state
360 378
379
361 def save_data(path): 380 def save_data(path):
362 with open(path, 'w+') as file: 381 with open(path, 'w+') as file:
363 file.write("%f %f\n" % state.accumulated_time) 382 file.write("%f %f\n" % state.accumulated_time)
364 for sample in state.samples: 383 for sample in state.samples:
365 time = sample.time 384 time = sample.time
366 stack = sample.stack 385 stack = sample.stack
367 sites = ['\1'.join([s.path, b'%d' % s.lineno, s.function]) 386 sites = [
368 for s in stack] 387 '\1'.join([s.path, b'%d' % s.lineno, s.function]) for s in stack
388 ]
369 file.write("%d\0%s\n" % (time, '\0'.join(sites))) 389 file.write("%d\0%s\n" % (time, '\0'.join(sites)))
390
370 391
371 def load_data(path): 392 def load_data(path):
372 lines = open(path, 'rb').read().splitlines() 393 lines = open(path, 'rb').read().splitlines()
373 394
374 state.accumulated_time = [float(value) for value in lines[0].split()] 395 state.accumulated_time = [float(value) for value in lines[0].split()]
378 time = float(parts[0]) 399 time = float(parts[0])
379 rawsites = parts[1:] 400 rawsites = parts[1:]
380 sites = [] 401 sites = []
381 for rawsite in rawsites: 402 for rawsite in rawsites:
382 siteparts = rawsite.split('\1') 403 siteparts = rawsite.split('\1')
383 sites.append(CodeSite.get(siteparts[0], int(siteparts[1]), 404 sites.append(
384 siteparts[2])) 405 CodeSite.get(siteparts[0], int(siteparts[1]), siteparts[2])
406 )
385 407
386 state.samples.append(Sample(sites, time)) 408 state.samples.append(Sample(sites, time))
387
388 409
389 410
390 def reset(frequency=None): 411 def reset(frequency=None):
391 '''Clear out the state of the profiler. Do not call while the 412 '''Clear out the state of the profiler. Do not call while the
392 profiler is running. 413 profiler is running.
409 430
410 431
411 ########################################################################### 432 ###########################################################################
412 ## Reporting API 433 ## Reporting API
413 434
435
414 class SiteStats(object): 436 class SiteStats(object):
415 def __init__(self, site): 437 def __init__(self, site):
416 self.site = site 438 self.site = site
417 self.selfcount = 0 439 self.selfcount = 0
418 self.totalcount = 0 440 self.totalcount = 0
450 472
451 if i == 0: 473 if i == 0:
452 sitestat.addself() 474 sitestat.addself()
453 475
454 return [s for s in stats.itervalues()] 476 return [s for s in stats.itervalues()]
477
455 478
456 class DisplayFormats: 479 class DisplayFormats:
457 ByLine = 0 480 ByLine = 0
458 ByMethod = 1 481 ByMethod = 1
459 AboutMethod = 2 482 AboutMethod = 2
460 Hotpath = 3 483 Hotpath = 3
461 FlameGraph = 4 484 FlameGraph = 4
462 Json = 5 485 Json = 5
463 Chrome = 6 486 Chrome = 6
464 487
488
465 def display(fp=None, format=3, data=None, **kwargs): 489 def display(fp=None, format=3, data=None, **kwargs):
466 '''Print statistics, either to stdout or the given file object.''' 490 '''Print statistics, either to stdout or the given file object.'''
467 if data is None: 491 if data is None:
468 data = state 492 data = state
469 493
470 if fp is None: 494 if fp is None:
471 import sys 495 import sys
496
472 fp = sys.stdout 497 fp = sys.stdout
473 if len(data.samples) == 0: 498 if len(data.samples) == 0:
474 fp.write(b'No samples recorded.\n') 499 fp.write(b'No samples recorded.\n')
475 return 500 return
476 501
494 if format not in (DisplayFormats.Json, DisplayFormats.Chrome): 519 if format not in (DisplayFormats.Json, DisplayFormats.Chrome):
495 fp.write(b'---\n') 520 fp.write(b'---\n')
496 fp.write(b'Sample count: %d\n' % len(data.samples)) 521 fp.write(b'Sample count: %d\n' % len(data.samples))
497 fp.write(b'Total time: %f seconds (%f wall)\n' % data.accumulated_time) 522 fp.write(b'Total time: %f seconds (%f wall)\n' % data.accumulated_time)
498 523
524
499 def display_by_line(data, fp): 525 def display_by_line(data, fp):
500 '''Print the profiler data with each sample line represented 526 '''Print the profiler data with each sample line represented
501 as one row in a table. Sorted by self-time per line.''' 527 as one row in a table. Sorted by self-time per line.'''
502 stats = SiteStats.buildstats(data.samples) 528 stats = SiteStats.buildstats(data.samples)
503 stats.sort(reverse=True, key=lambda x: x.selfseconds()) 529 stats.sort(reverse=True, key=lambda x: x.selfseconds())
504 530
505 fp.write(b'%5.5s %10.10s %7.7s %-8.8s\n' % ( 531 fp.write(
506 b'% ', b'cumulative', b'self', b'')) 532 b'%5.5s %10.10s %7.7s %-8.8s\n'
507 fp.write(b'%5.5s %9.9s %8.8s %-8.8s\n' % ( 533 % (b'% ', b'cumulative', b'self', b'')
508 b"time", b"seconds", b"seconds", b"name")) 534 )
535 fp.write(
536 b'%5.5s %9.9s %8.8s %-8.8s\n'
537 % (b"time", b"seconds", b"seconds", b"name")
538 )
509 539
510 for stat in stats: 540 for stat in stats:
511 site = stat.site 541 site = stat.site
512 sitelabel = '%s:%d:%s' % (site.filename(), 542 sitelabel = '%s:%d:%s' % (site.filename(), site.lineno, site.function)
513 site.lineno, 543 fp.write(
514 site.function) 544 b'%6.2f %9.2f %9.2f %s\n'
515 fp.write(b'%6.2f %9.2f %9.2f %s\n' % ( 545 % (
516 stat.selfpercent(), stat.totalseconds(), 546 stat.selfpercent(),
517 stat.selfseconds(), sitelabel)) 547 stat.totalseconds(),
548 stat.selfseconds(),
549 sitelabel,
550 )
551 )
552
518 553
519 def display_by_method(data, fp): 554 def display_by_method(data, fp):
520 '''Print the profiler data with each sample function represented 555 '''Print the profiler data with each sample function represented
521 as one row in a table. Important lines within that function are 556 as one row in a table. Important lines within that function are
522 output as nested rows. Sorted by self-time per line.''' 557 output as nested rows. Sorted by self-time per line.'''
523 fp.write(b'%5.5s %10.10s %7.7s %-8.8s\n' % 558 fp.write(
524 ('% ', 'cumulative', 'self', '')) 559 b'%5.5s %10.10s %7.7s %-8.8s\n' % ('% ', 'cumulative', 'self', '')
525 fp.write(b'%5.5s %9.9s %8.8s %-8.8s\n' % 560 )
526 ("time", "seconds", "seconds", "name")) 561 fp.write(
562 b'%5.5s %9.9s %8.8s %-8.8s\n'
563 % ("time", "seconds", "seconds", "name")
564 )
527 565
528 stats = SiteStats.buildstats(data.samples) 566 stats = SiteStats.buildstats(data.samples)
529 567
530 grouped = defaultdict(list) 568 grouped = defaultdict(list)
531 for stat in stats: 569 for stat in stats:
540 for stat in sitestats: 578 for stat in sitestats:
541 total_cum_sec += stat.totalseconds() 579 total_cum_sec += stat.totalseconds()
542 total_self_sec += stat.selfseconds() 580 total_self_sec += stat.selfseconds()
543 total_percent += stat.selfpercent() 581 total_percent += stat.selfpercent()
544 582
545 functiondata.append((fname, 583 functiondata.append(
546 total_cum_sec, 584 (fname, total_cum_sec, total_self_sec, total_percent, sitestats)
547 total_self_sec, 585 )
548 total_percent,
549 sitestats))
550 586
551 # sort by total self sec 587 # sort by total self sec
552 functiondata.sort(reverse=True, key=lambda x: x[2]) 588 functiondata.sort(reverse=True, key=lambda x: x[2])
553 589
554 for function in functiondata: 590 for function in functiondata:
555 if function[3] < 0.05: 591 if function[3] < 0.05:
556 continue 592 continue
557 fp.write(b'%6.2f %9.2f %9.2f %s\n' % ( 593 fp.write(
558 function[3], # total percent 594 b'%6.2f %9.2f %9.2f %s\n'
559 function[1], # total cum sec 595 % (
560 function[2], # total self sec 596 function[3], # total percent
561 function[0])) # file:function 597 function[1], # total cum sec
598 function[2], # total self sec
599 function[0],
600 )
601 ) # file:function
562 602
563 function[4].sort(reverse=True, key=lambda i: i.selfseconds()) 603 function[4].sort(reverse=True, key=lambda i: i.selfseconds())
564 for stat in function[4]: 604 for stat in function[4]:
565 # only show line numbers for significant locations (>1% time spent) 605 # only show line numbers for significant locations (>1% time spent)
566 if stat.selfpercent() > 1: 606 if stat.selfpercent() > 1:
567 source = stat.site.getsource(25) 607 source = stat.site.getsource(25)
568 if sys.version_info.major >= 3 and not isinstance(source, bytes): 608 if sys.version_info.major >= 3 and not isinstance(
609 source, bytes
610 ):
569 source = pycompat.bytestr(source) 611 source = pycompat.bytestr(source)
570 612
571 stattuple = (stat.selfpercent(), stat.selfseconds(), 613 stattuple = (
572 stat.site.lineno, source) 614 stat.selfpercent(),
615 stat.selfseconds(),
616 stat.site.lineno,
617 source,
618 )
573 619
574 fp.write(b'%33.0f%% %6.2f line %d: %s\n' % stattuple) 620 fp.write(b'%33.0f%% %6.2f line %d: %s\n' % stattuple)
621
575 622
576 def display_about_method(data, fp, function=None, **kwargs): 623 def display_about_method(data, fp, function=None, **kwargs):
577 if function is None: 624 if function is None:
578 raise Exception("Invalid function") 625 raise Exception("Invalid function")
579 626
585 parents = {} 632 parents = {}
586 children = {} 633 children = {}
587 634
588 for sample in data.samples: 635 for sample in data.samples:
589 for i, site in enumerate(sample.stack): 636 for i, site in enumerate(sample.stack):
590 if site.function == function and (not filename 637 if site.function == function and (
591 or site.filename() == filename): 638 not filename or site.filename() == filename
639 ):
592 relevant_samples += 1 640 relevant_samples += 1
593 if i != len(sample.stack) - 1: 641 if i != len(sample.stack) - 1:
594 parent = sample.stack[i + 1] 642 parent = sample.stack[i + 1]
595 if parent in parents: 643 if parent in parents:
596 parents[parent] = parents[parent] + 1 644 parents[parent] = parents[parent] + 1
603 children[site] = 1 651 children[site] = 1
604 652
605 parents = [(parent, count) for parent, count in parents.iteritems()] 653 parents = [(parent, count) for parent, count in parents.iteritems()]
606 parents.sort(reverse=True, key=lambda x: x[1]) 654 parents.sort(reverse=True, key=lambda x: x[1])
607 for parent, count in parents: 655 for parent, count in parents:
608 fp.write(b'%6.2f%% %s:%s line %s: %s\n' % 656 fp.write(
609 (count / relevant_samples * 100, 657 b'%6.2f%% %s:%s line %s: %s\n'
610 pycompat.fsencode(parent.filename()), 658 % (
611 pycompat.sysbytes(parent.function), 659 count / relevant_samples * 100,
612 parent.lineno, 660 pycompat.fsencode(parent.filename()),
613 pycompat.sysbytes(parent.getsource(50)))) 661 pycompat.sysbytes(parent.function),
662 parent.lineno,
663 pycompat.sysbytes(parent.getsource(50)),
664 )
665 )
614 666
615 stats = SiteStats.buildstats(data.samples) 667 stats = SiteStats.buildstats(data.samples)
616 stats = [s for s in stats 668 stats = [
617 if s.site.function == function and 669 s
618 (not filename or s.site.filename() == filename)] 670 for s in stats
671 if s.site.function == function
672 and (not filename or s.site.filename() == filename)
673 ]
619 674
620 total_cum_sec = 0 675 total_cum_sec = 0
621 total_self_sec = 0 676 total_self_sec = 0
622 total_self_percent = 0 677 total_self_percent = 0
623 total_cum_percent = 0 678 total_cum_percent = 0
628 total_cum_percent += stat.totalpercent() 683 total_cum_percent += stat.totalpercent()
629 684
630 fp.write( 685 fp.write(
631 b'\n %s:%s Total: %0.2fs (%0.2f%%) Self: %0.2fs (%0.2f%%)\n\n' 686 b'\n %s:%s Total: %0.2fs (%0.2f%%) Self: %0.2fs (%0.2f%%)\n\n'
632 % ( 687 % (
633 pycompat.sysbytes(filename or '___'), 688 pycompat.sysbytes(filename or '___'),
634 pycompat.sysbytes(function), 689 pycompat.sysbytes(function),
635 total_cum_sec, 690 total_cum_sec,
636 total_cum_percent, 691 total_cum_percent,
637 total_self_sec, 692 total_self_sec,
638 total_self_percent 693 total_self_percent,
639 )) 694 )
695 )
640 696
641 children = [(child, count) for child, count in children.iteritems()] 697 children = [(child, count) for child, count in children.iteritems()]
642 children.sort(reverse=True, key=lambda x: x[1]) 698 children.sort(reverse=True, key=lambda x: x[1])
643 for child, count in children: 699 for child, count in children:
644 fp.write(b' %6.2f%% line %s: %s\n' % 700 fp.write(
645 (count / relevant_samples * 100, child.lineno, 701 b' %6.2f%% line %s: %s\n'
646 pycompat.sysbytes(child.getsource(50)))) 702 % (
703 count / relevant_samples * 100,
704 child.lineno,
705 pycompat.sysbytes(child.getsource(50)),
706 )
707 )
708
647 709
648 def display_hotpath(data, fp, limit=0.05, **kwargs): 710 def display_hotpath(data, fp, limit=0.05, **kwargs):
649 class HotNode(object): 711 class HotNode(object):
650 def __init__(self, site): 712 def __init__(self, site):
651 self.site = site 713 self.site = site
675 lasttime = sample.time 737 lasttime = sample.time
676 showtime = kwargs.get(r'showtime', True) 738 showtime = kwargs.get(r'showtime', True)
677 739
678 def _write(node, depth, multiple_siblings): 740 def _write(node, depth, multiple_siblings):
679 site = node.site 741 site = node.site
680 visiblechildren = [c for c in node.children.itervalues() 742 visiblechildren = [
681 if c.count >= (limit * root.count)] 743 c
744 for c in node.children.itervalues()
745 if c.count >= (limit * root.count)
746 ]
682 if site: 747 if site:
683 indent = depth * 2 - 1 748 indent = depth * 2 - 1
684 filename = '' 749 filename = ''
685 function = '' 750 function = ''
686 if len(node.children) > 0: 751 if len(node.children) > 0:
687 childsite = list(node.children.itervalues())[0].site 752 childsite = list(node.children.itervalues())[0].site
688 filename = (childsite.filename() + ':').ljust(15) 753 filename = (childsite.filename() + ':').ljust(15)
689 function = childsite.function 754 function = childsite.function
690 755
691 # lots of string formatting 756 # lots of string formatting
692 listpattern = ''.ljust(indent) +\ 757 listpattern = (
693 ('\\' if multiple_siblings else '|') +\ 758 ''.ljust(indent)
694 ' %4.1f%%' +\ 759 + ('\\' if multiple_siblings else '|')
695 (' %5.2fs' % node.count if showtime else '') +\ 760 + ' %4.1f%%'
696 ' %s %s' 761 + (' %5.2fs' % node.count if showtime else '')
697 liststring = listpattern % (node.count / root.count * 100, 762 + ' %s %s'
698 filename, function) 763 )
764 liststring = listpattern % (
765 node.count / root.count * 100,
766 filename,
767 function,
768 )
699 codepattern = '%' + ('%d' % (55 - len(liststring))) + 's %d: %s' 769 codepattern = '%' + ('%d' % (55 - len(liststring))) + 's %d: %s'
700 codestring = codepattern % ('line', site.lineno, site.getsource(30)) 770 codestring = codepattern % ('line', site.lineno, site.getsource(30))
701 771
702 finalstring = liststring + codestring 772 finalstring = liststring + codestring
703 childrensamples = sum([c.count for c in node.children.itervalues()]) 773 childrensamples = sum([c.count for c in node.children.itervalues()])
718 _write(child, newdepth, len(visiblechildren) > 1) 788 _write(child, newdepth, len(visiblechildren) > 1)
719 789
720 if root.count > 0: 790 if root.count > 0:
721 _write(root, 0, False) 791 _write(root, 0, False)
722 792
793
723 def write_to_flame(data, fp, scriptpath=None, outputfile=None, **kwargs): 794 def write_to_flame(data, fp, scriptpath=None, outputfile=None, **kwargs):
724 if scriptpath is None: 795 if scriptpath is None:
725 scriptpath = encoding.environ['HOME'] + '/flamegraph.pl' 796 scriptpath = encoding.environ['HOME'] + '/flamegraph.pl'
726 if not os.path.exists(scriptpath): 797 if not os.path.exists(scriptpath):
727 fp.write(b'error: missing %s\n' % scriptpath) 798 fp.write(b'error: missing %s\n' % scriptpath)
748 outputfile = '~/flamegraph.svg' 819 outputfile = '~/flamegraph.svg'
749 820
750 os.system("perl ~/flamegraph.pl %s > %s" % (path, outputfile)) 821 os.system("perl ~/flamegraph.pl %s > %s" % (path, outputfile))
751 fp.write(b'Written to %s\n' % outputfile) 822 fp.write(b'Written to %s\n' % outputfile)
752 823
824
753 _pathcache = {} 825 _pathcache = {}
826
827
754 def simplifypath(path): 828 def simplifypath(path):
755 '''Attempt to make the path to a Python module easier to read by 829 '''Attempt to make the path to a Python module easier to read by
756 removing whatever part of the Python search path it was found 830 removing whatever part of the Python search path it was found
757 on.''' 831 on.'''
758 832
760 return _pathcache[path] 834 return _pathcache[path]
761 hgpath = encoding.__file__.rsplit(os.sep, 2)[0] 835 hgpath = encoding.__file__.rsplit(os.sep, 2)[0]
762 for p in [hgpath] + sys.path: 836 for p in [hgpath] + sys.path:
763 prefix = p + os.sep 837 prefix = p + os.sep
764 if path.startswith(prefix): 838 if path.startswith(prefix):
765 path = path[len(prefix):] 839 path = path[len(prefix) :]
766 break 840 break
767 _pathcache[path] = path 841 _pathcache[path] = path
768 return path 842 return path
769 843
844
770 def write_to_json(data, fp): 845 def write_to_json(data, fp):
771 samples = [] 846 samples = []
772 847
773 for sample in data.samples: 848 for sample in data.samples:
774 stack = [] 849 stack = []
775 850
776 for frame in sample.stack: 851 for frame in sample.stack:
777 stack.append( 852 stack.append(
778 (pycompat.sysstr(frame.path), 853 (
779 frame.lineno, 854 pycompat.sysstr(frame.path),
780 pycompat.sysstr(frame.function))) 855 frame.lineno,
856 pycompat.sysstr(frame.function),
857 )
858 )
781 859
782 samples.append((sample.time, stack)) 860 samples.append((sample.time, stack))
783 861
784 data = json.dumps(samples) 862 data = json.dumps(samples)
785 if not isinstance(data, bytes): 863 if not isinstance(data, bytes):
786 data = data.encode('utf-8') 864 data = data.encode('utf-8')
787 865
788 fp.write(data) 866 fp.write(data)
867
789 868
790 def write_to_chrome(data, fp, minthreshold=0.005, maxthreshold=0.999): 869 def write_to_chrome(data, fp, minthreshold=0.005, maxthreshold=0.999):
791 samples = [] 870 samples = []
792 laststack = collections.deque() 871 laststack = collections.deque()
793 lastseen = collections.deque() 872 lastseen = collections.deque()
794 873
795 # The Chrome tracing format allows us to use a compact stack 874 # The Chrome tracing format allows us to use a compact stack
796 # representation to save space. It's fiddly but worth it. 875 # representation to save space. It's fiddly but worth it.
797 # We maintain a bijection between stack and ID. 876 # We maintain a bijection between stack and ID.
798 stack2id = {} 877 stack2id = {}
799 id2stack = [] # will eventually be rendered 878 id2stack = [] # will eventually be rendered
800 879
801 def stackid(stack): 880 def stackid(stack):
802 if not stack: 881 if not stack:
803 return 882 return
804 if stack in stack2id: 883 if stack in stack2id:
839 oldtime, oldidx = lastseen.popleft() 918 oldtime, oldidx = lastseen.popleft()
840 duration = sample.time - oldtime 919 duration = sample.time - oldtime
841 if minthreshold <= duration <= maxthreshold: 920 if minthreshold <= duration <= maxthreshold:
842 # ensure no zero-duration events 921 # ensure no zero-duration events
843 sampletime = max(oldtime + clamp, sample.time) 922 sampletime = max(oldtime + clamp, sample.time)
844 samples.append(dict(ph=r'E', name=oldfunc, cat=oldcat, sf=oldsid, 923 samples.append(
845 ts=sampletime*1e6, pid=0)) 924 dict(
925 ph=r'E',
926 name=oldfunc,
927 cat=oldcat,
928 sf=oldsid,
929 ts=sampletime * 1e6,
930 pid=0,
931 )
932 )
846 else: 933 else:
847 blacklist.add(oldidx) 934 blacklist.add(oldidx)
848 935
849 # Much fiddling to synthesize correctly(ish) nested begin/end 936 # Much fiddling to synthesize correctly(ish) nested begin/end
850 # events given only stack snapshots. 937 # events given only stack snapshots.
851 938
852 for sample in data.samples: 939 for sample in data.samples:
853 stack = tuple(((r'%s:%d' % (simplifypath(pycompat.sysstr(frame.path)), 940 stack = tuple(
854 frame.lineno), 941 (
855 pycompat.sysstr(frame.function)) 942 (
856 for frame in sample.stack)) 943 r'%s:%d'
944 % (simplifypath(pycompat.sysstr(frame.path)), frame.lineno),
945 pycompat.sysstr(frame.function),
946 )
947 for frame in sample.stack
948 )
949 )
857 qstack = collections.deque(stack) 950 qstack = collections.deque(stack)
858 if laststack == qstack: 951 if laststack == qstack:
859 continue 952 continue
860 while laststack and qstack and laststack[-1] == qstack[-1]: 953 while laststack and qstack and laststack[-1] == qstack[-1]:
861 laststack.pop() 954 laststack.pop()
865 for f in reversed(qstack): 958 for f in reversed(qstack):
866 lastseen.appendleft((sample.time, len(samples))) 959 lastseen.appendleft((sample.time, len(samples)))
867 laststack.appendleft(f) 960 laststack.appendleft(f)
868 path, name = f 961 path, name = f
869 sid = stackid(tuple(laststack)) 962 sid = stackid(tuple(laststack))
870 samples.append(dict(ph=r'B', name=name, cat=path, 963 samples.append(
871 ts=sample.time*1e6, sf=sid, pid=0)) 964 dict(
965 ph=r'B',
966 name=name,
967 cat=path,
968 ts=sample.time * 1e6,
969 sf=sid,
970 pid=0,
971 )
972 )
872 laststack = collections.deque(stack) 973 laststack = collections.deque(stack)
873 while laststack: 974 while laststack:
874 poplast() 975 poplast()
875 events = [sample for idx, sample in enumerate(samples) 976 events = [
876 if idx not in blacklist] 977 sample for idx, sample in enumerate(samples) if idx not in blacklist
877 frames = collections.OrderedDict((str(k), v) 978 ]
878 for (k,v) in enumerate(id2stack)) 979 frames = collections.OrderedDict(
980 (str(k), v) for (k, v) in enumerate(id2stack)
981 )
879 data = json.dumps(dict(traceEvents=events, stackFrames=frames), indent=1) 982 data = json.dumps(dict(traceEvents=events, stackFrames=frames), indent=1)
880 if not isinstance(data, bytes): 983 if not isinstance(data, bytes):
881 data = data.encode('utf-8') 984 data = data.encode('utf-8')
882 fp.write(data) 985 fp.write(data)
883 fp.write('\n') 986 fp.write('\n')
884 987
988
885 def printusage(): 989 def printusage():
886 print(r""" 990 print(
991 r"""
887 The statprof command line allows you to inspect the last profile's results in 992 The statprof command line allows you to inspect the last profile's results in
888 the following forms: 993 the following forms:
889 994
890 usage: 995 usage:
891 hotpath [-l --limit percent] 996 hotpath [-l --limit percent]
898 function [filename:]functionname 1003 function [filename:]functionname
899 Shows the callers and callees of a particular function. 1004 Shows the callers and callees of a particular function.
900 flame [-s --script-path] [-o --output-file path] 1005 flame [-s --script-path] [-o --output-file path]
901 Writes out a flamegraph to output-file (defaults to ~/flamegraph.svg) 1006 Writes out a flamegraph to output-file (defaults to ~/flamegraph.svg)
902 Requires that ~/flamegraph.pl exist. 1007 Requires that ~/flamegraph.pl exist.
903 (Specify alternate script path with --script-path.)""") 1008 (Specify alternate script path with --script-path.)"""
1009 )
1010
904 1011
905 def main(argv=None): 1012 def main(argv=None):
906 if argv is None: 1013 if argv is None:
907 argv = sys.argv 1014 argv = sys.argv
908 1015
930 printusage() 1037 printusage()
931 return 0 1038 return 0
932 1039
933 # process options 1040 # process options
934 try: 1041 try:
935 opts, args = pycompat.getoptb(sys.argv[optstart:], "hl:f:o:p:", 1042 opts, args = pycompat.getoptb(
936 ["help", "limit=", "file=", "output-file=", "script-path="]) 1043 sys.argv[optstart:],
1044 "hl:f:o:p:",
1045 ["help", "limit=", "file=", "output-file=", "script-path="],
1046 )
937 except getopt.error as msg: 1047 except getopt.error as msg:
938 print(msg) 1048 print(msg)
939 printusage() 1049 printusage()
940 return 2 1050 return 2
941 1051
964 1074
965 display(**pycompat.strkwargs(displayargs)) 1075 display(**pycompat.strkwargs(displayargs))
966 1076
967 return 0 1077 return 0
968 1078
1079
969 if __name__ == r"__main__": 1080 if __name__ == r"__main__":
970 sys.exit(main()) 1081 sys.exit(main())