Mercurial > hg
changeset 29781:2654a0aac80d
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.
author | Gregory Szorc <gregory.szorc@gmail.com> |
---|---|
date | Sun, 14 Aug 2016 16:30:44 -0700 |
parents | 531e85eec23c |
children | 97bfc2e5fba5 |
files | mercurial/dispatch.py mercurial/profiling.py |
diffstat | 2 files changed, 134 insertions(+), 112 deletions(-) [+] |
line wrap: on
line diff
--- 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()