profiling.py
238 lines
| 7.5 KiB
| text/x-python
|
PythonLexer
/ mercurial / profiling.py
|
r29781 | # 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 | ||||
|
r29783 | import contextlib | ||
|
r29781 | |||
from .i18n import _ | ||||
from . import ( | ||||
|
r30820 | encoding, | ||
|
r29781 | error, | ||
|
r32417 | extensions, | ||
|
r29781 | util, | ||
) | ||||
|
r32417 | def _loadprofiler(ui, profiler): | ||
"""load profiler extension. return profile method, or None on failure""" | ||||
extname = profiler | ||||
extensions.loadall(ui, whitelist=[extname]) | ||||
try: | ||||
mod = extensions.find(extname) | ||||
except KeyError: | ||||
return None | ||||
else: | ||||
return getattr(mod, 'profile', None) | ||||
|
r29783 | @contextlib.contextmanager | ||
def lsprofile(ui, fp): | ||||
|
r29781 | 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: | ||||
|
r29783 | yield | ||
|
r29781 | 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) | ||||
|
r29783 | @contextlib.contextmanager | ||
def flameprofile(ui, fp): | ||||
|
r29781 | 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) | ||||
|
r30975 | start_time = util.timer() | ||
|
r29781 | try: | ||
thread.start() | ||||
|
r29783 | yield | ||
|
r29781 | finally: | ||
thread.stop() | ||||
thread.join() | ||||
print('Collected %d stack frames (%d unique) in %2.2f seconds.' % ( | ||||
|
r30975 | util.timer() - start_time, thread.num_frames(), | ||
|
r29781 | thread.num_frames(unique=True))) | ||
|
r29783 | @contextlib.contextmanager | ||
def statprofile(ui, fp): | ||||
|
r30316 | from . import statprof | ||
|
r29781 | |||
freq = ui.configint('profiling', 'freq', default=1000) | ||||
if freq > 0: | ||||
|
r29785 | # Cannot reset when profiler is already active. So silently no-op. | ||
if statprof.state.profile_level == 0: | ||||
statprof.reset(freq) | ||||
|
r29781 | else: | ||
ui.warn(_("invalid sampling frequency '%s' - ignoring\n") % freq) | ||||
|
r30316 | statprof.start(mechanism='thread') | ||
|
r29781 | try: | ||
|
r29783 | yield | ||
|
r29781 | finally: | ||
|
r30316 | data = statprof.stop() | ||
profformat = ui.config('profiling', 'statformat', 'hotpath') | ||||
formats = { | ||||
'byline': statprof.DisplayFormats.ByLine, | ||||
'bymethod': statprof.DisplayFormats.ByMethod, | ||||
'hotpath': statprof.DisplayFormats.Hotpath, | ||||
'json': statprof.DisplayFormats.Json, | ||||
|
r30930 | 'chrome': statprof.DisplayFormats.Chrome, | ||
|
r30316 | } | ||
if profformat in formats: | ||||
displayformat = formats[profformat] | ||||
else: | ||||
ui.warn(_('unknown profiler output format: %s\n') % profformat) | ||||
displayformat = statprof.DisplayFormats.Hotpath | ||||
|
r30930 | kwargs = {} | ||
def fraction(s): | ||||
r32978 | if isinstance(s, (float, int)): | |||
return float(s) | ||||
|
r30930 | if s.endswith('%'): | ||
v = float(s[:-1]) / 100 | ||||
else: | ||||
v = float(s) | ||||
if 0 <= v <= 1: | ||||
return v | ||||
raise ValueError(s) | ||||
if profformat == 'chrome': | ||||
showmin = ui.configwith(fraction, 'profiling', 'showmin', 0.005) | ||||
showmax = ui.configwith(fraction, 'profiling', 'showmax', 0.999) | ||||
kwargs.update(minthreshold=showmin, maxthreshold=showmax) | ||||
|
r32851 | elif profformat == 'hotpath': | ||
|
r33192 | # inconsistent config: profiling.showmin | ||
|
r32851 | limit = ui.configwith(fraction, 'profiling', 'showmin', 0.05) | ||
kwargs['limit'] = limit | ||||
|
r30930 | |||
statprof.display(fp, data=data, format=displayformat, **kwargs) | ||||
|
r29781 | |||
r32783 | class profile(object): | |||
|
r29783 | """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. | ||||
""" | ||||
r32785 | def __init__(self, ui, enabled=True): | |||
r32783 | self._ui = ui | |||
self._output = None | ||||
self._fp = None | ||||
r32805 | self._fpdoclose = True | |||
r32783 | self._profiler = None | |||
r32785 | self._enabled = enabled | |||
r32784 | self._entered = False | |||
self._started = False | ||||
|
r29781 | |||
r32783 | def __enter__(self): | |||
r32784 | self._entered = True | |||
r32785 | if self._enabled: | |||
self.start() | ||||
r32786 | return self | |||
r32784 | ||||
def start(self): | ||||
"""Start profiling. | ||||
The profiling will stop at the context exit. | ||||
If the profiler was already started, this has no effect.""" | ||||
if not self._entered: | ||||
raise error.ProgrammingError() | ||||
if self._started: | ||||
return | ||||
self._started = True | ||||
r32783 | profiler = encoding.environ.get('HGPROF') | |||
proffn = None | ||||
if profiler is None: | ||||
profiler = self._ui.config('profiling', 'type', default='stat') | ||||
if profiler not in ('ls', 'stat', 'flame'): | ||||
# try load profiler from extension with the same name | ||||
proffn = _loadprofiler(self._ui, profiler) | ||||
if proffn is None: | ||||
self._ui.warn(_("unrecognized profiler '%s' - ignored\n") | ||||
% profiler) | ||||
profiler = 'stat' | ||||
|
r29781 | |||
r32783 | self._output = self._ui.config('profiling', 'output') | |||
|
r29781 | |||
r32808 | try: | |||
r32807 | if self._output == 'blackbox': | |||
self._fp = util.stringio() | ||||
elif self._output: | ||||
path = self._ui.expandpath(self._output) | ||||
self._fp = open(path, 'wb') | ||||
else: | ||||
self._fpdoclose = False | ||||
self._fp = self._ui.ferr | ||||
r32783 | ||||
r32807 | if proffn is not None: | |||
pass | ||||
elif profiler == 'ls': | ||||
proffn = lsprofile | ||||
elif profiler == 'flame': | ||||
proffn = flameprofile | ||||
else: | ||||
proffn = statprofile | ||||
|
r29783 | |||
r32807 | self._profiler = proffn(self._ui, self._fp) | |||
self._profiler.__enter__() | ||||
r32808 | except: # re-raises | |||
self._closefp() | ||||
raise | ||||
|
r29783 | |||
r32783 | def __exit__(self, exception_type, exception_value, traceback): | |||
r32810 | propagate = None | |||
r32809 | if self._profiler is not None: | |||
r32810 | propagate = self._profiler.__exit__(exception_type, exception_value, | |||
traceback) | ||||
r32809 | if self._output == 'blackbox': | |||
val = 'Profile:\n%s' % self._fp.getvalue() | ||||
# ui.log treats the input as a format string, | ||||
# so we need to escape any % signs. | ||||
val = val.replace('%', '%%') | ||||
self._ui.log('profile', val) | ||||
r32806 | self._closefp() | |||
r32810 | return propagate | |||
r32804 | ||||
def _closefp(self): | ||||
r32805 | if self._fpdoclose and self._fp is not None: | |||
self._fp.close() | ||||