# HG changeset patch # User Bryan O'Sullivan # Date 1486966820 28800 # Node ID cb440e7af05d7cfa1b7e55940872fc3e1be7bd41 # Parent be3a4fde38eb48f4f4c2bca27fc1523123b611b4 statprof: allow rendering in the Chrome trace viewer format diff -r be3a4fde38eb -r cb440e7af05d 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