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.
--- a/mercurial/dispatch.py Mon Aug 15 12:26:02 2016 -0400
+++ b/mercurial/dispatch.py Sun Aug 14 16:30:44 2016 -0700
@@ -34,6 +34,7 @@
fileset,
hg,
hook,
+ profiling,
revset,
templatefilters,
templatekw,
@@ -892,85 +893,6 @@
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.
@@ -983,39 +905,7 @@
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()
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/mercurial/profiling.py Sun Aug 14 16:30:44 2016 -0700
@@ -0,0 +1,132 @@
+# profiling.py - profiling functions
+#
+# 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()