Patchwork [1,of,6] profiling: move profiling code from dispatch.py (API)

login
register
mail settings
Submitter Gregory Szorc
Date Aug. 15, 2016, 12:03 a.m.
Message ID <40cbc513713837acd6a9.1471219433@ubuntu-vm-main>
Download mbox | patch
Permalink /patch/16289/
State Superseded
Headers show

Comments

Gregory Szorc - Aug. 15, 2016, 12:03 a.m.
# HG changeset patch
# User Gregory Szorc <gregory.szorc@gmail.com>
# Date 1471217444 25200
#      Sun Aug 14 16:30:44 2016 -0700
# Node ID 40cbc513713837acd6a9f593bfd48759eabcce33
# Parent  279cd80059d41bbdb91ea9073278cbbe7f1b43d5
profiling: move profiling code from dispatch.py (API)

Currently, profiling code lives in dispatch.py, which is a low-level
module centered around command dispatch. Furthermore, dispatch.py
imports a lot of other modules, meaning that importing dispatch.py
to get at profiling functionality would often result in a module import
cycle.

Profiling is a generic activity. It shouldn't be limited to command
dispatch. This patch moves profiling code from dispatch.py to the
new profiling.py. The low-level "run a profiler against a function"
functions have been moved verbatim. The code for determining how to
invoke the profiler has been extracted to its own function.

I decided to create a new module rather than stick this code
elsewhere (such as util.py) because util.py is already quite large.
And, I foresee this file growing larger once Facebook's profiling
enhancements get added to it.
Gregory Szorc - Aug. 15, 2016, 12:45 a.m.
On Sun, Aug 14, 2016 at 5:03 PM, Gregory Szorc <gregory.szorc@gmail.com>
wrote:

> # HG changeset patch
> # User Gregory Szorc <gregory.szorc@gmail.com>
> # Date 1471217444 25200
> #      Sun Aug 14 16:30:44 2016 -0700
> # Node ID 40cbc513713837acd6a9f593bfd48759eabcce33
> # Parent  279cd80059d41bbdb91ea9073278cbbe7f1b43d5
> profiling: move profiling code from dispatch.py (API)
>

I found a few minor issues with this series. Please don't ninja queue. I'll
send a v2, hopefully tonight.

Patch

diff --git a/mercurial/dispatch.py b/mercurial/dispatch.py
--- a/mercurial/dispatch.py
+++ b/mercurial/dispatch.py
@@ -29,16 +29,17 @@  from . import (
     demandimport,
     encoding,
     error,
     extensions,
     fancyopts,
     fileset,
     hg,
     hook,
+    profiling,
     revset,
     templatefilters,
     templatekw,
     templater,
     ui as uimod,
     util,
 )
 
@@ -887,140 +888,29 @@  def _dispatch(req):
     d = lambda: util.checksignature(func)(ui, *args, **cmdoptions)
     try:
         return runcommand(lui, repo, cmd, fullargs, ui, options, d,
                           cmdpats, cmdoptions)
     finally:
         if repo and repo != req.repo:
             repo.close()
 
-def lsprofile(ui, func, fp):
-    format = ui.config('profiling', 'format', default='text')
-    field = ui.config('profiling', 'sort', default='inlinetime')
-    limit = ui.configint('profiling', 'limit', default=30)
-    climit = ui.configint('profiling', 'nested', default=0)
-
-    if format not in ['text', 'kcachegrind']:
-        ui.warn(_("unrecognized profiling format '%s'"
-                    " - Ignored\n") % format)
-        format = 'text'
-
-    try:
-        from . import lsprof
-    except ImportError:
-        raise error.Abort(_(
-            'lsprof not available - install from '
-            'http://codespeak.net/svn/user/arigo/hack/misc/lsprof/'))
-    p = lsprof.Profiler()
-    p.enable(subcalls=True)
-    try:
-        return func()
-    finally:
-        p.disable()
-
-        if format == 'kcachegrind':
-            from . import lsprofcalltree
-            calltree = lsprofcalltree.KCacheGrind(p)
-            calltree.output(fp)
-        else:
-            # format == 'text'
-            stats = lsprof.Stats(p.getstats())
-            stats.sort(field)
-            stats.pprint(limit=limit, file=fp, climit=climit)
-
-def flameprofile(ui, func, fp):
-    try:
-        from flamegraph import flamegraph
-    except ImportError:
-        raise error.Abort(_(
-            'flamegraph not available - install from '
-            'https://github.com/evanhempel/python-flamegraph'))
-    # developer config: profiling.freq
-    freq = ui.configint('profiling', 'freq', default=1000)
-    filter_ = None
-    collapse_recursion = True
-    thread = flamegraph.ProfileThread(fp, 1.0 / freq,
-                                      filter_, collapse_recursion)
-    start_time = time.clock()
-    try:
-        thread.start()
-        func()
-    finally:
-        thread.stop()
-        thread.join()
-        print('Collected %d stack frames (%d unique) in %2.2f seconds.' % (
-            time.clock() - start_time, thread.num_frames(),
-            thread.num_frames(unique=True)))
-
-
-def statprofile(ui, func, fp):
-    try:
-        import statprof
-    except ImportError:
-        raise error.Abort(_(
-            'statprof not available - install using "easy_install statprof"'))
-
-    freq = ui.configint('profiling', 'freq', default=1000)
-    if freq > 0:
-        statprof.reset(freq)
-    else:
-        ui.warn(_("invalid sampling frequency '%s' - ignoring\n") % freq)
-
-    statprof.start()
-    try:
-        return func()
-    finally:
-        statprof.stop()
-        statprof.display(fp)
-
 def _runcommand(ui, options, cmd, cmdfunc):
     """Enables the profiler if applicable.
 
     ``profiling.enabled`` - boolean config that enables or disables profiling
     """
     def checkargs():
         try:
             return cmdfunc()
         except error.SignatureError:
             raise error.CommandError(cmd, _("invalid arguments"))
 
     if options['profile'] or ui.configbool('profiling', 'enabled'):
-        profiler = os.getenv('HGPROF')
-        if profiler is None:
-            profiler = ui.config('profiling', 'type', default='ls')
-        if profiler not in ('ls', 'stat', 'flame'):
-            ui.warn(_("unrecognized profiler '%s' - ignored\n") % profiler)
-            profiler = 'ls'
-
-        output = ui.config('profiling', 'output')
-
-        if output == 'blackbox':
-            fp = util.stringio()
-        elif output:
-            path = ui.expandpath(output)
-            fp = open(path, 'wb')
-        else:
-            fp = sys.stderr
-
-        try:
-            if profiler == 'ls':
-                return lsprofile(ui, checkargs, fp)
-            elif profiler == 'flame':
-                return flameprofile(ui, checkargs, fp)
-            else:
-                return statprofile(ui, checkargs, fp)
-        finally:
-            if output:
-                if output == 'blackbox':
-                    val = "Profile:\n%s" % fp.getvalue()
-                    # ui.log treats the input as a format string,
-                    # so we need to escape any % signs.
-                    val = val.replace('%', '%%')
-                    ui.log('profile', val)
-                fp.close()
+        return profiling.profile(ui, checkargs)
     else:
         return checkargs()
 
 def _exceptionwarning(ui):
     """Produce a warning message for the current active exception"""
 
     # For compatibility checking, we discard the portion of the hg
     # version after the + on the assumption that if a "normal
diff --git a/mercurial/profiling.py b/mercurial/profiling.py
new file mode 100644
--- /dev/null
+++ b/mercurial/profiling.py
@@ -0,0 +1,132 @@ 
+# __init__.py - Startup and module loading logic for Mercurial.
+#
+# Copyright 2016 Gregory Szorc <gregory.szorc@gmail.com>
+#
+# This software may be used and distributed according to the terms of the
+# GNU General Public License version 2 or any later version.
+
+from __future__ import absolute_import, print_function
+
+import os
+import sys
+import time
+
+from .i18n import _
+from . import (
+    error,
+    util,
+)
+
+def lsprofile(ui, func, fp):
+    format = ui.config('profiling', 'format', default='text')
+    field = ui.config('profiling', 'sort', default='inlinetime')
+    limit = ui.configint('profiling', 'limit', default=30)
+    climit = ui.configint('profiling', 'nested', default=0)
+
+    if format not in ['text', 'kcachegrind']:
+        ui.warn(_("unrecognized profiling format '%s'"
+                    " - Ignored\n") % format)
+        format = 'text'
+
+    try:
+        from . import lsprof
+    except ImportError:
+        raise error.Abort(_(
+            'lsprof not available - install from '
+            'http://codespeak.net/svn/user/arigo/hack/misc/lsprof/'))
+    p = lsprof.Profiler()
+    p.enable(subcalls=True)
+    try:
+        return func()
+    finally:
+        p.disable()
+
+        if format == 'kcachegrind':
+            from . import lsprofcalltree
+            calltree = lsprofcalltree.KCacheGrind(p)
+            calltree.output(fp)
+        else:
+            # format == 'text'
+            stats = lsprof.Stats(p.getstats())
+            stats.sort(field)
+            stats.pprint(limit=limit, file=fp, climit=climit)
+
+def flameprofile(ui, func, fp):
+    try:
+        from flamegraph import flamegraph
+    except ImportError:
+        raise error.Abort(_(
+            'flamegraph not available - install from '
+            'https://github.com/evanhempel/python-flamegraph'))
+    # developer config: profiling.freq
+    freq = ui.configint('profiling', 'freq', default=1000)
+    filter_ = None
+    collapse_recursion = True
+    thread = flamegraph.ProfileThread(fp, 1.0 / freq,
+                                      filter_, collapse_recursion)
+    start_time = time.clock()
+    try:
+        thread.start()
+        func()
+    finally:
+        thread.stop()
+        thread.join()
+        print('Collected %d stack frames (%d unique) in %2.2f seconds.' % (
+            time.clock() - start_time, thread.num_frames(),
+            thread.num_frames(unique=True)))
+
+def statprofile(ui, func, fp):
+    try:
+        import statprof
+    except ImportError:
+        raise error.Abort(_(
+            'statprof not available - install using "easy_install statprof"'))
+
+    freq = ui.configint('profiling', 'freq', default=1000)
+    if freq > 0:
+        statprof.reset(freq)
+    else:
+        ui.warn(_("invalid sampling frequency '%s' - ignoring\n") % freq)
+
+    statprof.start()
+    try:
+        return func()
+    finally:
+        statprof.stop()
+        statprof.display(fp)
+
+def profile(ui, fn):
+    """Profile a function call."""
+    profiler = os.getenv('HGPROF')
+    if profiler is None:
+        profiler = ui.config('profiling', 'type', default='ls')
+    if profiler not in ('ls', 'stat', 'flame'):
+        ui.warn(_("unrecognized profiler '%s' - ignored\n") % profiler)
+        profiler = 'ls'
+
+    output = ui.config('profiling', 'output')
+
+    if output == 'blackbox':
+        fp = util.stringio()
+    elif output:
+        path = ui.expandpath(output)
+        fp = open(path, 'wb')
+    else:
+        fp = sys.stderr
+
+    try:
+        if profiler == 'ls':
+            return lsprofile(ui, fn, fp)
+        elif profiler == 'flame':
+            return flameprofile(ui, fn, fp)
+        else:
+            return statprofile(ui, fn, fp)
+    finally:
+        if output:
+            if output == 'blackbox':
+                val = 'Profile:\n%s' % fp.getvalue()
+                # ui.log treats the input as a format string,
+                # so we need to escape any % signs.
+                val = val.replace('%', '%%')
+                ui.log('profile', val)
+            fp.close()