Patchwork [3,of,7,V2] profiling: make profiling functions context managers (API)

login
register
mail settings
Submitter Gregory Szorc
Date Aug. 15, 2016, 1:37 a.m.
Message ID <3338d4536f490fca0558.1471225060@ubuntu-vm-main>
Download mbox | patch
Permalink /patch/16297/
State Accepted
Headers show

Comments

Gregory Szorc - Aug. 15, 2016, 1:37 a.m.
# HG changeset patch
# User Gregory Szorc <gregory.szorc@gmail.com>
# Date 1471224322 25200
#      Sun Aug 14 18:25:22 2016 -0700
# Node ID 3338d4536f490fca055881b4e3884b28e4e25d22
# Parent  fd888ffaab6720688e4ad3f0358be09509effb6f
profiling: make profiling functions context managers (API)

This makes profiling more flexible since we can now call multiple
functions when a profiler is active. But the real reason for this
is to enable a future consumer to profile a function that returns
a generator. We can't do this from the profiling function itself
because functions can either be generators or have return values:
they can't be both. So therefore it isn't possible to have a generic
profiling function that can both consume and re-emit a generator
and return a value.

Patch

diff --git a/mercurial/dispatch.py b/mercurial/dispatch.py
--- a/mercurial/dispatch.py
+++ b/mercurial/dispatch.py
@@ -904,17 +904,18 @@  def _runcommand(ui, options, cmd, cmdfun
     """
     def checkargs():
         try:
             return cmdfunc()
         except error.SignatureError:
             raise error.CommandError(cmd, _("invalid arguments"))
 
     if ui.configbool('profiling', 'enabled'):
-        return profiling.profile(ui, checkargs)
+        with profiling.profile(ui):
+            return 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
--- a/mercurial/profiling.py
+++ b/mercurial/profiling.py
@@ -2,27 +2,29 @@ 
 #
 # 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 contextlib
 import os
 import sys
 import time
 
 from .i18n import _
 from . import (
     error,
     util,
 )
 
-def lsprofile(ui, func, fp):
+@contextlib.contextmanager
+def lsprofile(ui, 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)
@@ -32,76 +34,83 @@  def lsprofile(ui, func, fp):
         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()
+        yield
     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):
+@contextlib.contextmanager
+def flameprofile(ui, 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()
+        yield
     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):
+@contextlib.contextmanager
+def statprofile(ui, 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()
+        yield
     finally:
         statprof.stop()
         statprof.display(fp)
 
-def profile(ui, fn):
-    """Profile a function call."""
+@contextlib.contextmanager
+def profile(ui):
+    """Start profiling.
+
+    Profiling is active when the context manager is active. When the context
+    manager exits, profiling results will be written to the configured output.
+    """
     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')
@@ -111,21 +120,25 @@  def profile(ui, fn):
     elif output:
         path = ui.expandpath(output)
         fp = open(path, 'wb')
     else:
         fp = sys.stderr
 
     try:
         if profiler == 'ls':
-            return lsprofile(ui, fn, fp)
+            proffn = lsprofile
         elif profiler == 'flame':
-            return flameprofile(ui, fn, fp)
+            proffn = flameprofile
         else:
-            return statprofile(ui, fn, fp)
+            proffn = statprofile
+
+        with proffn(ui, fp):
+            yield
+
     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)