changeset 29783:5d44197c208b

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.
author Gregory Szorc <gregory.szorc@gmail.com>
date Sun, 14 Aug 2016 18:25:22 -0700
parents 97bfc2e5fba5
children e3501546f7e4
files mercurial/dispatch.py mercurial/profiling.py
diffstat 2 files changed, 26 insertions(+), 12 deletions(-) [+]
line wrap: on
line diff
--- a/mercurial/dispatch.py	Sun Aug 14 16:35:58 2016 -0700
+++ b/mercurial/dispatch.py	Sun Aug 14 18:25:22 2016 -0700
@@ -909,7 +909,8 @@
             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()
 
--- a/mercurial/profiling.py	Sun Aug 14 16:35:58 2016 -0700
+++ b/mercurial/profiling.py	Sun Aug 14 18:25:22 2016 -0700
@@ -7,6 +7,7 @@
 
 from __future__ import absolute_import, print_function
 
+import contextlib
 import os
 import sys
 import time
@@ -17,7 +18,8 @@
     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)
@@ -37,7 +39,7 @@
     p = lsprof.Profiler()
     p.enable(subcalls=True)
     try:
-        return func()
+        yield
     finally:
         p.disable()
 
@@ -51,7 +53,8 @@
             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:
@@ -67,7 +70,7 @@
     start_time = time.clock()
     try:
         thread.start()
-        func()
+        yield
     finally:
         thread.stop()
         thread.join()
@@ -75,7 +78,8 @@
             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:
@@ -90,13 +94,18 @@
 
     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')
@@ -116,11 +125,15 @@
 
     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':