statprof: vendor statprof.py
authorGregory Szorc <gregory.szorc@gmail.com>
Tue, 01 Nov 2016 18:54:03 -0700
changeset 30253 b032a7b676c6
parent 30252 bb5869668189
child 30254 ad4c0236168d
statprof: vendor statprof.py Vendored from https://bitbucket.org/facebook/hg-experimental changeset 73f9db47ae5a1a9fa29a98dfe92d557ad51234c3 without modification. This introduces a number of code style violations. The file already has the magic words to skip test-check-code.t. I'll make additional changes to clean up the test-check-py3-compat.t warnings and to change some behavior in the code that isn't suitable for general use. test-check-commit.t also complains about numerous things. But there's nothing we can do if we're importing as-is.
mercurial/statprof.py
tests/test-check-code.t
tests/test-check-py3-compat.t
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mercurial/statprof.py	Tue Nov 01 18:54:03 2016 -0700
@@ -0,0 +1,797 @@
+#!/usr/bin/env python
+## statprof.py
+## Copyright (C) 2012 Bryan O'Sullivan <bos@serpentine.com>
+## Copyright (C) 2011 Alex Fraser <alex at phatcore dot com>
+## Copyright (C) 2004,2005 Andy Wingo <wingo at pobox dot com>
+## Copyright (C) 2001 Rob Browning <rlb at defaultvalue dot org>
+
+## This library is free software; you can redistribute it and/or
+## modify it under the terms of the GNU Lesser General Public
+## License as published by the Free Software Foundation; either
+## version 2.1 of the License, or (at your option) any later version.
+##
+## This library is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+## Lesser General Public License for more details.
+##
+## You should have received a copy of the GNU Lesser General Public
+## License along with this program; if not, contact:
+##
+## Free Software Foundation           Voice:  +1-617-542-5942
+## 59 Temple Place - Suite 330        Fax:    +1-617-542-2652
+## Boston, MA  02111-1307,  USA       gnu@gnu.org
+
+"""
+statprof is intended to be a fairly simple statistical profiler for
+python. It was ported directly from a statistical profiler for guile,
+also named statprof, available from guile-lib [0].
+
+[0] http://wingolog.org/software/guile-lib/statprof/
+
+To start profiling, call statprof.start():
+>>> start()
+
+Then run whatever it is that you want to profile, for example:
+>>> import test.pystone; test.pystone.pystones()
+
+Then stop the profiling and print out the results:
+>>> stop()
+>>> display()
+  %   cumulative      self
+ time    seconds   seconds  name
+ 26.72      1.40      0.37  pystone.py:79:Proc0
+ 13.79      0.56      0.19  pystone.py:133:Proc1
+ 13.79      0.19      0.19  pystone.py:208:Proc8
+ 10.34      0.16      0.14  pystone.py:229:Func2
+  6.90      0.10      0.10  pystone.py:45:__init__
+  4.31      0.16      0.06  pystone.py:53:copy
+    ...
+
+All of the numerical data is statistically approximate. In the
+following column descriptions, and in all of statprof, "time" refers
+to execution time (both user and system), not wall clock time.
+
+% time
+    The percent of the time spent inside the procedure itself (not
+    counting children).
+
+cumulative seconds
+    The total number of seconds spent in the procedure, including
+    children.
+
+self seconds
+    The total number of seconds spent in the procedure itself (not
+    counting children).
+
+name
+    The name of the procedure.
+
+By default statprof keeps the data collected from previous runs. If you
+want to clear the collected data, call reset():
+>>> reset()
+
+reset() can also be used to change the sampling frequency from the
+default of 1000 Hz. For example, to tell statprof to sample 50 times a
+second:
+>>> reset(50)
+
+This means that statprof will sample the call stack after every 1/50 of
+a second of user + system time spent running on behalf of the python
+process. When your process is idle (for example, blocking in a read(),
+as is the case at the listener), the clock does not advance. For this
+reason statprof is not currently not suitable for profiling io-bound
+operations.
+
+The profiler uses the hash of the code object itself to identify the
+procedures, so it won't confuse different procedures with the same name.
+They will show up as two different rows in the output.
+
+Right now the profiler is quite simplistic.  I cannot provide
+call-graphs or other higher level information.  What you see in the
+table is pretty much all there is. Patches are welcome :-)
+
+
+Threading
+---------
+
+Because signals only get delivered to the main thread in Python,
+statprof only profiles the main thread. However because the time
+reporting function uses per-process timers, the results can be
+significantly off if other threads' work patterns are not similar to the
+main thread's work patterns.
+"""
+# no-check-code
+from __future__ import division
+
+import inspect, json, os, signal, tempfile, sys, getopt, threading, traceback
+import time
+from collections import defaultdict
+from contextlib import contextmanager
+from subprocess import call
+
+__all__ = ['start', 'stop', 'reset', 'display', 'profile']
+
+skips = set(["util.py:check", "extensions.py:closure",
+             "color.py:colorcmd", "dispatch.py:checkargs",
+             "dispatch.py:<lambda>", "dispatch.py:_runcatch",
+             "dispatch.py:_dispatch", "dispatch.py:_runcommand",
+             "pager.py:pagecmd", "dispatch.py:run",
+             "dispatch.py:dispatch", "dispatch.py:runcommand",
+             "hg.py:<module>", "evolve.py:warnobserrors",
+         ])
+
+###########################################################################
+## Utils
+
+def clock():
+    times = os.times()
+    return times[0] + times[1]
+
+
+###########################################################################
+## Collection data structures
+
+class ProfileState(object):
+    def __init__(self, frequency=None):
+        self.reset(frequency)
+
+    def reset(self, frequency=None):
+        # total so far
+        self.accumulated_time = 0.0
+        # start_time when timer is active
+        self.last_start_time = None
+        # a float
+        if frequency:
+            self.sample_interval = 1.0 / frequency
+        elif not hasattr(self, 'sample_interval'):
+            # default to 1000 Hz
+            self.sample_interval = 1.0 / 1000.0
+        else:
+            # leave the frequency as it was
+            pass
+        self.remaining_prof_time = None
+        # for user start/stop nesting
+        self.profile_level = 0
+
+        self.samples = []
+
+    def accumulate_time(self, stop_time):
+        self.accumulated_time += stop_time - self.last_start_time
+
+    def seconds_per_sample(self):
+        return self.accumulated_time / len(self.samples)
+
+state = ProfileState()
+
+
+class CodeSite(object):
+    cache = {}
+
+    __slots__ = ('path', 'lineno', 'function', 'source')
+
+    def __init__(self, path, lineno, function):
+        self.path = path
+        self.lineno = lineno
+        self.function = function
+        self.source = None
+
+    def __eq__(self, other):
+        try:
+            return (self.lineno == other.lineno and
+                    self.path == other.path)
+        except:
+            return False
+
+    def __hash__(self):
+        return hash((self.lineno, self.path))
+
+    @classmethod
+    def get(cls, path, lineno, function):
+        k = (path, lineno)
+        try:
+            return cls.cache[k]
+        except KeyError:
+            v = cls(path, lineno, function)
+            cls.cache[k] = v
+            return v
+
+    def getsource(self, length):
+        if self.source is None:
+            lineno = self.lineno - 1
+            fp = None
+            try:
+                fp = open(self.path)
+                for i, line in enumerate(fp):
+                    if i == lineno:
+                        self.source = line.strip()
+                        break
+            except:
+                pass
+            finally:
+                if fp:
+                    fp.close()
+            if self.source is None:
+                self.source = ''
+
+        source = self.source
+        if len(source) > length:
+            source = source[:(length - 3)] + "..."
+        return source
+
+    def filename(self):
+        return os.path.basename(self.path)
+
+class Sample(object):
+    __slots__ = ('stack', 'time')
+
+    def __init__(self, stack, time):
+        self.stack = stack
+        self.time = time
+
+    @classmethod
+    def from_frame(cls, frame, time):
+        stack = []
+
+        while frame:
+            stack.append(CodeSite.get(frame.f_code.co_filename, frame.f_lineno,
+                                      frame.f_code.co_name))
+            frame = frame.f_back
+
+        return Sample(stack, time)
+
+###########################################################################
+## SIGPROF handler
+
+def profile_signal_handler(signum, frame):
+    if state.profile_level > 0:
+        now = clock()
+        state.accumulate_time(now)
+
+        state.samples.append(Sample.from_frame(frame, state.accumulated_time))
+
+        signal.setitimer(signal.ITIMER_PROF,
+            state.sample_interval, 0.0)
+        state.last_start_time = now
+
+stopthread = threading.Event()
+def samplerthread(tid):
+    while not stopthread.is_set():
+        now = clock()
+        state.accumulate_time(now)
+
+        frame = sys._current_frames()[tid]
+        state.samples.append(Sample.from_frame(frame, state.accumulated_time))
+
+        state.last_start_time = now
+        time.sleep(state.sample_interval)
+
+    stopthread.clear()
+
+###########################################################################
+## Profiling API
+
+def is_active():
+    return state.profile_level > 0
+
+lastmechanism = None
+def start(mechanism='thread'):
+    '''Install the profiling signal handler, and start profiling.'''
+    state.profile_level += 1
+    if state.profile_level == 1:
+        state.last_start_time = clock()
+        rpt = state.remaining_prof_time
+        state.remaining_prof_time = None
+
+        global lastmechanism
+        lastmechanism = mechanism
+
+        if mechanism == 'signal':
+            signal.signal(signal.SIGPROF, profile_signal_handler)
+            signal.setitimer(signal.ITIMER_PROF,
+                rpt or state.sample_interval, 0.0)
+        elif mechanism == 'thread':
+            frame = inspect.currentframe()
+            tid = [k for k, f in sys._current_frames().items() if f == frame][0]
+            state.thread = threading.Thread(target=samplerthread,
+                                 args=(tid,), name="samplerthread")
+            state.thread.start()
+
+def stop():
+    '''Stop profiling, and uninstall the profiling signal handler.'''
+    state.profile_level -= 1
+    if state.profile_level == 0:
+        if lastmechanism == 'signal':
+            rpt = signal.setitimer(signal.ITIMER_PROF, 0.0, 0.0)
+            signal.signal(signal.SIGPROF, signal.SIG_IGN)
+            state.remaining_prof_time = rpt[0]
+        elif lastmechanism == 'thread':
+            stopthread.set()
+            state.thread.join()
+
+        state.accumulate_time(clock())
+        state.last_start_time = None
+        statprofpath = os.environ.get('STATPROF_DEST')
+        save_data(statprofpath)
+
+def save_data(path=None):
+    try:
+        path = path or (os.environ['HOME'] + '/statprof.data')
+        file = open(path, "w+")
+
+        file.write(str(state.accumulated_time) + '\n')
+        for sample in state.samples:
+            time = str(sample.time)
+            stack = sample.stack
+            sites = ['\1'.join([s.path, str(s.lineno), s.function])
+                     for s in stack]
+            file.write(time + '\0' + '\0'.join(sites) + '\n')
+
+        file.close()
+    except (IOError, OSError) as ex:
+        # The home directory probably didn't exist, or wasn't writable. Oh well.
+        pass
+
+def load_data(path=None):
+    path = path or (os.environ['HOME'] + '/statprof.data')
+    lines = open(path, 'r').read().splitlines()
+
+    state.accumulated_time = float(lines[0])
+    state.samples = []
+    for line in lines[1:]:
+        parts = line.split('\0')
+        time = float(parts[0])
+        rawsites = parts[1:]
+        sites = []
+        for rawsite in rawsites:
+            siteparts = rawsite.split('\1')
+            sites.append(CodeSite.get(siteparts[0], int(siteparts[1]),
+                        siteparts[2]))
+
+        state.samples.append(Sample(sites, time))
+
+
+
+def reset(frequency=None):
+    '''Clear out the state of the profiler.  Do not call while the
+    profiler is running.
+
+    The optional frequency argument specifies the number of samples to
+    collect per second.'''
+    assert state.profile_level == 0, "Can't reset() while statprof is running"
+    CodeSite.cache.clear()
+    state.reset(frequency)
+
+
+@contextmanager
+def profile():
+    start()
+    try:
+        yield
+    finally:
+        stop()
+        display()
+
+
+###########################################################################
+## Reporting API
+
+class SiteStats(object):
+    def __init__(self, site):
+        self.site = site
+        self.selfcount = 0
+        self.totalcount = 0
+
+    def addself(self):
+        self.selfcount += 1
+
+    def addtotal(self):
+        self.totalcount += 1
+
+    def selfpercent(self):
+        return self.selfcount / len(state.samples) * 100
+
+    def totalpercent(self):
+        return self.totalcount / len(state.samples) * 100
+
+    def selfseconds(self):
+        return self.selfcount * state.seconds_per_sample()
+
+    def totalseconds(self):
+        return self.totalcount * state.seconds_per_sample()
+
+    @classmethod
+    def buildstats(cls, samples):
+        stats = {}
+
+        for sample in samples:
+            for i, site in enumerate(sample.stack):
+                sitestat = stats.get(site)
+                if not sitestat:
+                    sitestat = SiteStats(site)
+                    stats[site] = sitestat
+
+                sitestat.addtotal()
+
+                if i == 0:
+                    sitestat.addself()
+
+        return [s for s in stats.itervalues()]
+
+class DisplayFormats:
+    ByLine = 0
+    ByMethod = 1
+    AboutMethod = 2
+    Hotpath = 3
+    FlameGraph = 4
+    Json = 5
+
+def display(fp=None, format=3, **kwargs):
+    '''Print statistics, either to stdout or the given file object.'''
+
+    if fp is None:
+        import sys
+        fp = sys.stdout
+    if len(state.samples) == 0:
+        print >> fp, ('No samples recorded.')
+        return
+
+    if format == DisplayFormats.ByLine:
+        display_by_line(fp)
+    elif format == DisplayFormats.ByMethod:
+        display_by_method(fp)
+    elif format == DisplayFormats.AboutMethod:
+        display_about_method(fp, **kwargs)
+    elif format == DisplayFormats.Hotpath:
+        display_hotpath(fp, **kwargs)
+    elif format == DisplayFormats.FlameGraph:
+        write_to_flame(fp, **kwargs)
+    elif format == DisplayFormats.Json:
+        write_to_json(fp)
+    else:
+        raise Exception("Invalid display format")
+
+    if format != DisplayFormats.Json:
+        print >> fp, ('---')
+        print >> fp, ('Sample count: %d' % len(state.samples))
+        print >> fp, ('Total time: %f seconds' % state.accumulated_time)
+
+def display_by_line(fp):
+    '''Print the profiler data with each sample line represented
+    as one row in a table.  Sorted by self-time per line.'''
+    stats = SiteStats.buildstats(state.samples)
+    stats.sort(reverse=True, key=lambda x: x.selfseconds())
+
+    print >> fp, ('%5.5s %10.10s   %7.7s  %-8.8s' %
+                  ('%  ', 'cumulative', 'self', ''))
+    print >> fp, ('%5.5s  %9.9s  %8.8s  %-8.8s' %
+                  ("time", "seconds", "seconds", "name"))
+
+    for stat in stats:
+        site = stat.site
+        sitelabel = '%s:%d:%s' % (site.filename(), site.lineno, site.function)
+        print >> fp, ('%6.2f %9.2f %9.2f  %s' % (stat.selfpercent(),
+                                                 stat.totalseconds(),
+                                                 stat.selfseconds(),
+                                                 sitelabel))
+
+def display_by_method(fp):
+    '''Print the profiler data with each sample function represented
+    as one row in a table.  Important lines within that function are
+    output as nested rows.  Sorted by self-time per line.'''
+    print >> fp, ('%5.5s %10.10s   %7.7s  %-8.8s' %
+                  ('%  ', 'cumulative', 'self', ''))
+    print >> fp, ('%5.5s  %9.9s  %8.8s  %-8.8s' %
+                  ("time", "seconds", "seconds", "name"))
+
+    stats = SiteStats.buildstats(state.samples)
+
+    grouped = defaultdict(list)
+    for stat in stats:
+        grouped[stat.site.filename() + ":" + stat.site.function].append(stat)
+
+    # compute sums for each function
+    functiondata = []
+    for fname, sitestats in grouped.iteritems():
+        total_cum_sec = 0
+        total_self_sec = 0
+        total_percent = 0
+        for stat in sitestats:
+            total_cum_sec += stat.totalseconds()
+            total_self_sec += stat.selfseconds()
+            total_percent += stat.selfpercent()
+
+        functiondata.append((fname,
+                             total_cum_sec,
+                             total_self_sec,
+                             total_percent,
+                             sitestats))
+
+    # sort by total self sec
+    functiondata.sort(reverse=True, key=lambda x: x[2])
+
+    for function in functiondata:
+        if function[3] < 0.05:
+            continue
+        print >> fp, ('%6.2f %9.2f %9.2f  %s' % (function[3], # total percent
+                                                 function[1], # total cum sec
+                                                 function[2], # total self sec
+                                                 function[0])) # file:function
+        function[4].sort(reverse=True, key=lambda i: i.selfseconds())
+        for stat in function[4]:
+            # only show line numbers for significant locations (>1% time spent)
+            if stat.selfpercent() > 1:
+                source = stat.site.getsource(25)
+                stattuple = (stat.selfpercent(), stat.selfseconds(),
+                             stat.site.lineno, source)
+
+                print >> fp, ('%33.0f%% %6.2f   line %s: %s' % (stattuple))
+
+def display_about_method(fp, function=None, **kwargs):
+    if function is None:
+        raise Exception("Invalid function")
+
+    filename = None
+    if ':' in function:
+        filename, function = function.split(':')
+
+    relevant_samples = 0
+    parents = {}
+    children = {}
+
+    for sample in state.samples:
+        for i, site in enumerate(sample.stack):
+            if site.function == function and (not filename
+                or site.filename() == filename):
+                relevant_samples += 1
+                if i != len(sample.stack) - 1:
+                    parent = sample.stack[i + 1]
+                    if parent in parents:
+                        parents[parent] = parents[parent] + 1
+                    else:
+                        parents[parent] = 1
+
+                if site in children:
+                    children[site] = children[site] + 1
+                else:
+                    children[site] = 1
+
+    parents = [(parent, count) for parent, count in parents.iteritems()]
+    parents.sort(reverse=True, key=lambda x: x[1])
+    for parent, count in parents:
+        print >> fp, ('%6.2f%%   %s:%s   line %s: %s' %
+            (count / relevant_samples * 100, parent.filename(),
+            parent.function, parent.lineno, parent.getsource(50)))
+
+    stats = SiteStats.buildstats(state.samples)
+    stats = [s for s in stats
+               if s.site.function == function and
+               (not filename or s.site.filename() == filename)]
+
+    total_cum_sec = 0
+    total_self_sec = 0
+    total_self_percent = 0
+    total_cum_percent = 0
+    for stat in stats:
+        total_cum_sec += stat.totalseconds()
+        total_self_sec += stat.selfseconds()
+        total_self_percent += stat.selfpercent()
+        total_cum_percent += stat.totalpercent()
+
+    print >> fp, (
+        '\n    %s:%s    Total: %0.2fs (%0.2f%%)    Self: %0.2fs (%0.2f%%)\n' %
+        (
+        filename or '___',
+        function,
+        total_cum_sec,
+        total_cum_percent,
+        total_self_sec,
+        total_self_percent
+        ))
+
+    children = [(child, count) for child, count in children.iteritems()]
+    children.sort(reverse=True, key=lambda x: x[1])
+    for child, count in children:
+        print >> fp, ('        %6.2f%%   line %s: %s' %
+            (count / relevant_samples * 100, child.lineno, child.getsource(50)))
+
+def display_hotpath(fp, limit=0.05, **kwargs):
+    class HotNode(object):
+        def __init__(self, site):
+            self.site = site
+            self.count = 0
+            self.children = {}
+
+        def add(self, stack, time):
+            self.count += time
+            site = stack[0]
+            child = self.children.get(site)
+            if not child:
+                child = HotNode(site)
+                self.children[site] = child
+
+            if len(stack) > 1:
+                i = 1
+                # Skip boiler plate parts of the stack
+                while i < len(stack) and '%s:%s' % (stack[i].filename(), stack[i].function) in skips:
+                    i += 1
+                if i < len(stack):
+                    child.add(stack[i:], time)
+
+    root = HotNode(None)
+    lasttime = state.samples[0].time
+    for sample in state.samples:
+        root.add(sample.stack[::-1], sample.time - lasttime)
+        lasttime = sample.time
+
+    def _write(node, depth, multiple_siblings):
+        site = node.site
+        visiblechildren = [c for c in node.children.itervalues()
+                             if c.count >= (limit * root.count)]
+        if site:
+            indent = depth * 2 - 1
+            filename = ''
+            function = ''
+            if len(node.children) > 0:
+                childsite = list(node.children.itervalues())[0].site
+                filename = (childsite.filename() + ':').ljust(15)
+                function = childsite.function
+
+            # lots of string formatting
+            listpattern = ''.ljust(indent) +\
+                          ('\\' if multiple_siblings else '|') +\
+                          ' %4.1f%%  %s %s'
+            liststring = listpattern % (node.count / root.count * 100,
+                                        filename, function)
+            codepattern = '%' + str(55 - len(liststring)) + 's %s:  %s'
+            codestring = codepattern % ('line', site.lineno, site.getsource(30))
+
+            finalstring = liststring + codestring
+            childrensamples = sum([c.count for c in node.children.itervalues()])
+            # Make frames that performed more than 10% of the operation red
+            if node.count - childrensamples > (0.1 * root.count):
+                finalstring = '\033[91m' + finalstring + '\033[0m'
+            # Make frames that didn't actually perform work dark grey
+            elif node.count - childrensamples == 0:
+                finalstring = '\033[90m' + finalstring + '\033[0m'
+            print >> fp, finalstring
+
+        newdepth = depth
+        if len(visiblechildren) > 1 or multiple_siblings:
+            newdepth += 1
+
+        visiblechildren.sort(reverse=True, key=lambda x: x.count)
+        for child in visiblechildren:
+            _write(child, newdepth, len(visiblechildren) > 1)
+
+    if root.count > 0:
+        _write(root, 0, False)
+
+def write_to_flame(fp, scriptpath=None, outputfile=None, **kwargs):
+    if scriptpath is None:
+        scriptpath = os.environ['HOME'] + '/flamegraph.pl'
+    if not os.path.exists(scriptpath):
+        print >> fp, "error: missing %s" % scriptpath
+        print >> fp, "get it here: https://github.com/brendangregg/FlameGraph"
+        return
+
+    fd, path = tempfile.mkstemp()
+
+    file = open(path, "w+")
+
+    lines = {}
+    for sample in state.samples:
+        sites = [s.function for s in sample.stack]
+        sites.reverse()
+        line = ';'.join(sites)
+        if line in lines:
+            lines[line] = lines[line] + 1
+        else:
+            lines[line] = 1
+
+    for line, count in lines.iteritems():
+        file.write("%s %s\n" % (line, count))
+
+    file.close()
+
+    if outputfile is None:
+        outputfile = '~/flamegraph.svg'
+
+    os.system("perl ~/flamegraph.pl %s > %s" % (path, outputfile))
+    print "Written to %s" % outputfile
+
+def write_to_json(fp):
+    samples = []
+
+    for sample in state.samples:
+        stack = []
+
+        for frame in sample.stack:
+            stack.append((frame.path, frame.lineno, frame.function))
+
+        samples.append((sample.time, stack))
+
+    print >> fp, json.dumps(samples)
+
+def printusage():
+    print """
+The statprof command line allows you to inspect the last profile's results in
+the following forms:
+
+usage:
+    hotpath [-l --limit percent]
+        Shows a graph of calls with the percent of time each takes.
+        Red calls take over 10%% of the total time themselves.
+    lines
+        Shows the actual sampled lines.
+    functions
+        Shows the samples grouped by function.
+    function [filename:]functionname
+        Shows the callers and callees of a particular function.
+    flame [-s --script-path] [-o --output-file path]
+        Writes out a flamegraph to output-file (defaults to ~/flamegraph.svg)
+        Requires that ~/flamegraph.pl exist.
+        (Specify alternate script path with --script-path.)"""
+
+def main(argv=None):
+    if argv is None:
+        argv = sys.argv
+
+    if len(argv) == 1:
+        printusage()
+        return 0
+
+    displayargs = {}
+
+    optstart = 2
+    displayargs['function'] = None
+    if argv[1] == 'hotpath':
+        displayargs['format'] = DisplayFormats.Hotpath
+    elif argv[1] == 'lines':
+        displayargs['format'] = DisplayFormats.ByLine
+    elif argv[1] == 'functions':
+        displayargs['format'] = DisplayFormats.ByMethod
+    elif argv[1] == 'function':
+        displayargs['format'] = DisplayFormats.AboutMethod
+        displayargs['function'] = argv[2]
+        optstart = 3
+    elif argv[1] == 'flame':
+        displayargs['format'] = DisplayFormats.FlameGraph
+    else:
+        printusage()
+        return 0
+
+    # process options
+    try:
+        opts, args = getopt.getopt(sys.argv[optstart:], "hl:f:o:p:",
+                                   ["help", "limit=", "file=", "output-file=", "script-path="])
+    except getopt.error as msg:
+        print msg
+        printusage()
+        return 2
+
+    displayargs['limit'] = 0.05
+    path = None
+    for o, value in opts:
+        if o in ("-l", "--limit"):
+            displayargs['limit'] = float(value)
+        elif o in ("-f", "--file"):
+            path = value
+        elif o in ("-o", "--output-file"):
+            displayargs['outputfile'] = value
+        elif o in ("-p", "--script-path"):
+            displayargs['scriptpath'] = value
+        elif o in ("-h", "help"):
+            printusage()
+            return 0
+        else:
+            assert False, "unhandled option %s" % o
+
+    load_data(path=path)
+
+    display(**displayargs)
+
+    return 0
+
+if __name__ == "__main__":
+    sys.exit(main())
--- a/tests/test-check-code.t	Tue Nov 01 15:40:21 2016 -0400
+++ b/tests/test-check-code.t	Tue Nov 01 18:54:03 2016 -0700
@@ -17,3 +17,4 @@
   Skipping i18n/polib.py it has no-che?k-code (glob)
   Skipping mercurial/httpclient/__init__.py it has no-che?k-code (glob)
   Skipping mercurial/httpclient/_readers.py it has no-che?k-code (glob)
+  Skipping mercurial/statprof.py it has no-che?k-code (glob)
--- a/tests/test-check-py3-compat.t	Tue Nov 01 15:40:21 2016 -0400
+++ b/tests/test-check-py3-compat.t	Tue Nov 01 18:54:03 2016 -0700
@@ -9,6 +9,8 @@
   hgext/fsmonitor/pywatchman/capabilities.py not using absolute_import
   hgext/fsmonitor/pywatchman/pybser.py not using absolute_import
   i18n/check-translation.py not using absolute_import
+  mercurial/statprof.py not using absolute_import
+  mercurial/statprof.py requires print_function
   setup.py not using absolute_import
   tests/test-demandimport.py not using absolute_import
 
@@ -22,6 +24,7 @@
   hgext/fsmonitor/watchmanclient.py: error importing: <ImportError> No module named 'pybser' (error at __init__.py:*)
   hgext/mq.py: error importing: <TypeError> __import__() argument 1 must be str, not bytes (error at extensions.py:*)
   mercurial/scmwindows.py: error importing: <ImportError> No module named 'winreg' (error at scmwindows.py:*)
+  mercurial/statprof.py: invalid syntax: Missing parentheses in call to 'print' (<unknown>, line *)
   mercurial/win32.py: error importing: <ImportError> No module named 'msvcrt' (error at win32.py:*)
   mercurial/windows.py: error importing: <ImportError> No module named 'msvcrt' (error at windows.py:*)