comparison mercurial/statprof.py @ 30949:cb440e7af05d

statprof: allow rendering in the Chrome trace viewer format
author Bryan O'Sullivan <bryano@fb.com>
date Sun, 12 Feb 2017 22:20:20 -0800
parents be3a4fde38eb
children 2912b06905dc
comparison
equal deleted inserted replaced
30948:be3a4fde38eb 30949:cb440e7af05d
431 ByMethod = 1 431 ByMethod = 1
432 AboutMethod = 2 432 AboutMethod = 2
433 Hotpath = 3 433 Hotpath = 3
434 FlameGraph = 4 434 FlameGraph = 4
435 Json = 5 435 Json = 5
436 Chrome = 6
436 437
437 def display(fp=None, format=3, data=None, **kwargs): 438 def display(fp=None, format=3, data=None, **kwargs):
438 '''Print statistics, either to stdout or the given file object.''' 439 '''Print statistics, either to stdout or the given file object.'''
439 data = data or state 440 data = data or state
440 441
455 display_hotpath(data, fp, **kwargs) 456 display_hotpath(data, fp, **kwargs)
456 elif format == DisplayFormats.FlameGraph: 457 elif format == DisplayFormats.FlameGraph:
457 write_to_flame(data, fp, **kwargs) 458 write_to_flame(data, fp, **kwargs)
458 elif format == DisplayFormats.Json: 459 elif format == DisplayFormats.Json:
459 write_to_json(data, fp) 460 write_to_json(data, fp)
461 elif format == DisplayFormats.Chrome:
462 write_to_chrome(data, fp, **kwargs)
460 else: 463 else:
461 raise Exception("Invalid display format") 464 raise Exception("Invalid display format")
462 465
463 if format != DisplayFormats.Json: 466 if format not in (DisplayFormats.Json, DisplayFormats.Chrome):
464 print('---', file=fp) 467 print('---', file=fp)
465 print('Sample count: %d' % len(data.samples), file=fp) 468 print('Sample count: %d' % len(data.samples), file=fp)
466 print('Total time: %f seconds' % data.accumulated_time, file=fp) 469 print('Total time: %f seconds' % data.accumulated_time, file=fp)
467 470
468 def display_by_line(data, fp): 471 def display_by_line(data, fp):
740 stack.append((frame.path, frame.lineno, frame.function)) 743 stack.append((frame.path, frame.lineno, frame.function))
741 744
742 samples.append((sample.time, stack)) 745 samples.append((sample.time, stack))
743 746
744 print(json.dumps(samples), file=fp) 747 print(json.dumps(samples), file=fp)
748
749 def write_to_chrome(data, fp, minthreshold=0.005, maxthreshold=0.999):
750 samples = []
751 laststack = collections.deque()
752 lastseen = collections.deque()
753
754 # The Chrome tracing format allows us to use a compact stack
755 # representation to save space. It's fiddly but worth it.
756 # We maintain a bijection between stack and ID.
757 stack2id = {}
758 id2stack = [] # will eventually be rendered
759
760 def stackid(stack):
761 if not stack:
762 return
763 if stack in stack2id:
764 return stack2id[stack]
765 parent = stackid(stack[1:])
766 myid = len(stack2id)
767 stack2id[stack] = myid
768 id2stack.append(dict(category=stack[0][0], name='%s %s' % stack[0]))
769 if parent is not None:
770 id2stack[-1].update(parent=parent)
771 return myid
772
773 def endswith(a, b):
774 return list(a)[-len(b):] == list(b)
775
776 # The sampling profiler can sample multiple times without
777 # advancing the clock, potentially causing the Chrome trace viewer
778 # to render single-pixel columns that we cannot zoom in on. We
779 # work around this by pretending that zero-duration samples are a
780 # millisecond in length.
781
782 clamp = 0.001
783
784 # We provide knobs that by default attempt to filter out stack
785 # frames that are too noisy:
786 #
787 # * A few take almost all execution time. These are usually boring
788 # setup functions, giving a stack that is deep but uninformative.
789 #
790 # * Numerous samples take almost no time, but introduce lots of
791 # noisy, oft-deep "spines" into a rendered profile.
792
793 blacklist = set()
794 totaltime = data.samples[-1].time - data.samples[0].time
795 minthreshold = totaltime * minthreshold
796 maxthreshold = max(totaltime * maxthreshold, clamp)
797
798 def poplast():
799 oldsid = stackid(tuple(laststack))
800 oldcat, oldfunc = laststack.popleft()
801 oldtime, oldidx = lastseen.popleft()
802 duration = sample.time - oldtime
803 if minthreshold <= duration <= maxthreshold:
804 # ensure no zero-duration events
805 sampletime = max(oldtime + clamp, sample.time)
806 samples.append(dict(ph='E', name=oldfunc, cat=oldcat, sf=oldsid,
807 ts=sampletime*1e6, pid=0))
808 else:
809 blacklist.add(oldidx)
810
811 # Much fiddling to synthesize correctly(ish) nested begin/end
812 # events given only stack snapshots.
813
814 for sample in data.samples:
815 tos = sample.stack[0]
816 name = tos.function
817 path = simplifypath(tos.path)
818 category = '%s:%d' % (path, tos.lineno)
819 stack = tuple((('%s:%d' % (simplifypath(frame.path), frame.lineno),
820 frame.function) for frame in sample.stack))
821 qstack = collections.deque(stack)
822 if laststack == qstack:
823 continue
824 while laststack and qstack and laststack[-1] == qstack[-1]:
825 laststack.pop()
826 qstack.pop()
827 while laststack:
828 poplast()
829 for f in reversed(qstack):
830 lastseen.appendleft((sample.time, len(samples)))
831 laststack.appendleft(f)
832 path, name = f
833 sid = stackid(tuple(laststack))
834 samples.append(dict(ph='B', name=name, cat=path, ts=sample.time*1e6,
835 sf=sid, pid=0))
836 laststack = collections.deque(stack)
837 while laststack:
838 poplast()
839 events = [s[1] for s in enumerate(samples) if s[0] not in blacklist]
840 frames = collections.OrderedDict((str(k), v)
841 for (k,v) in enumerate(id2stack))
842 json.dump(dict(traceEvents=events, stackFrames=frames), fp, indent=1)
843 fp.write('\n')
745 844
746 def printusage(): 845 def printusage():
747 print(""" 846 print("""
748 The statprof command line allows you to inspect the last profile's results in 847 The statprof command line allows you to inspect the last profile's results in
749 the following forms: 848 the following forms: