statprof: allow rendering in the Chrome trace viewer format
authorBryan O'Sullivan <bryano@fb.com>
Sun, 12 Feb 2017 22:20:20 -0800
changeset 30929 cb440e7af05d
parent 30928 be3a4fde38eb
child 30930 517bc1cd7033
statprof: allow rendering in the Chrome trace viewer format
mercurial/statprof.py
--- a/mercurial/statprof.py	Sun Feb 12 22:16:58 2017 -0800
+++ b/mercurial/statprof.py	Sun Feb 12 22:20:20 2017 -0800
@@ -433,6 +433,7 @@
     Hotpath = 3
     FlameGraph = 4
     Json = 5
+    Chrome = 6
 
 def display(fp=None, format=3, data=None, **kwargs):
     '''Print statistics, either to stdout or the given file object.'''
@@ -457,10 +458,12 @@
         write_to_flame(data, fp, **kwargs)
     elif format == DisplayFormats.Json:
         write_to_json(data, fp)
+    elif format == DisplayFormats.Chrome:
+        write_to_chrome(data, fp, **kwargs)
     else:
         raise Exception("Invalid display format")
 
-    if format != DisplayFormats.Json:
+    if format not in (DisplayFormats.Json, DisplayFormats.Chrome):
         print('---', file=fp)
         print('Sample count: %d' % len(data.samples), file=fp)
         print('Total time: %f seconds' % data.accumulated_time, file=fp)
@@ -743,6 +746,102 @@
 
     print(json.dumps(samples), file=fp)
 
+def write_to_chrome(data, fp, minthreshold=0.005, maxthreshold=0.999):
+    samples = []
+    laststack = collections.deque()
+    lastseen = collections.deque()
+
+    # The Chrome tracing format allows us to use a compact stack
+    # representation to save space. It's fiddly but worth it.
+    # We maintain a bijection between stack and ID.
+    stack2id = {}
+    id2stack = [] # will eventually be rendered
+
+    def stackid(stack):
+        if not stack:
+            return
+        if stack in stack2id:
+            return stack2id[stack]
+        parent = stackid(stack[1:])
+        myid = len(stack2id)
+        stack2id[stack] = myid
+        id2stack.append(dict(category=stack[0][0], name='%s %s' % stack[0]))
+        if parent is not None:
+            id2stack[-1].update(parent=parent)
+        return myid
+
+    def endswith(a, b):
+        return list(a)[-len(b):] == list(b)
+
+    # The sampling profiler can sample multiple times without
+    # advancing the clock, potentially causing the Chrome trace viewer
+    # to render single-pixel columns that we cannot zoom in on.  We
+    # work around this by pretending that zero-duration samples are a
+    # millisecond in length.
+
+    clamp = 0.001
+
+    # We provide knobs that by default attempt to filter out stack
+    # frames that are too noisy:
+    #
+    # * A few take almost all execution time. These are usually boring
+    #   setup functions, giving a stack that is deep but uninformative.
+    #
+    # * Numerous samples take almost no time, but introduce lots of
+    #   noisy, oft-deep "spines" into a rendered profile.
+
+    blacklist = set()
+    totaltime = data.samples[-1].time - data.samples[0].time
+    minthreshold = totaltime * minthreshold
+    maxthreshold = max(totaltime * maxthreshold, clamp)
+
+    def poplast():
+        oldsid = stackid(tuple(laststack))
+        oldcat, oldfunc = laststack.popleft()
+        oldtime, oldidx = lastseen.popleft()
+        duration = sample.time - oldtime
+        if minthreshold <= duration <= maxthreshold:
+            # ensure no zero-duration events
+            sampletime = max(oldtime + clamp, sample.time)
+            samples.append(dict(ph='E', name=oldfunc, cat=oldcat, sf=oldsid,
+                                ts=sampletime*1e6, pid=0))
+        else:
+            blacklist.add(oldidx)
+
+    # Much fiddling to synthesize correctly(ish) nested begin/end
+    # events given only stack snapshots.
+
+    for sample in data.samples:
+        tos = sample.stack[0]
+        name = tos.function
+        path = simplifypath(tos.path)
+        category = '%s:%d' % (path, tos.lineno)
+        stack = tuple((('%s:%d' % (simplifypath(frame.path), frame.lineno),
+                        frame.function) for frame in sample.stack))
+        qstack = collections.deque(stack)
+        if laststack == qstack:
+            continue
+        while laststack and qstack and laststack[-1] == qstack[-1]:
+            laststack.pop()
+            qstack.pop()
+        while laststack:
+            poplast()
+        for f in reversed(qstack):
+            lastseen.appendleft((sample.time, len(samples)))
+            laststack.appendleft(f)
+            path, name = f
+            sid = stackid(tuple(laststack))
+            samples.append(dict(ph='B', name=name, cat=path, ts=sample.time*1e6,
+                                sf=sid, pid=0))
+        laststack = collections.deque(stack)
+    while laststack:
+        poplast()
+    events = [s[1] for s in enumerate(samples) if s[0] not in blacklist]
+    frames = collections.OrderedDict((str(k), v)
+                                     for (k,v) in enumerate(id2stack))
+    json.dump(dict(traceEvents=events, stackFrames=frames), fp, indent=1)
+    fp.write('\n')
+
 def printusage():
     print("""
 The statprof command line allows you to inspect the last profile's results in