profiling.py
282 lines
| 8.4 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 _ | ||||
|
r43359 | from .pycompat import ( | ||
getattr, | ||||
open, | ||||
) | ||||
|
r29781 | from . import ( | ||
|
r30820 | encoding, | ||
|
r29781 | error, | ||
|
r32417 | extensions, | ||
|
r36701 | pycompat, | ||
|
r29781 | util, | ||
) | ||||
|
r43346 | |||
|
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) | ||||
|
r43346 | |||
|
r29783 | @contextlib.contextmanager | ||
def lsprofile(ui, fp): | ||||
|
r43347 | format = ui.config(b'profiling', b'format') | ||
field = ui.config(b'profiling', b'sort') | ||||
limit = ui.configint(b'profiling', b'limit') | ||||
climit = ui.configint(b'profiling', b'nested') | ||||
|
r29781 | |||
|
r43347 | if format not in [b'text', b'kcachegrind']: | ||
|
r43387 | ui.warn(_(b"unrecognized profiling format '%s' - Ignored\n") % format) | ||
|
r43347 | format = b'text' | ||
|
r29781 | |||
try: | ||||
from . import lsprof | ||||
except ImportError: | ||||
|
r43346 | raise error.Abort( | ||
_( | ||||
|
r43347 | b'lsprof not available - install from ' | ||
b'http://codespeak.net/svn/user/arigo/hack/misc/lsprof/' | ||||
|
r43346 | ) | ||
) | ||||
|
r29781 | p = lsprof.Profiler() | ||
p.enable(subcalls=True) | ||||
try: | ||||
|
r29783 | yield | ||
|
r29781 | finally: | ||
p.disable() | ||||
|
r43347 | if format == b'kcachegrind': | ||
|
r29781 | from . import lsprofcalltree | ||
|
r43346 | |||
|
r29781 | calltree = lsprofcalltree.KCacheGrind(p) | ||
calltree.output(fp) | ||||
else: | ||||
# format == 'text' | ||||
stats = lsprof.Stats(p.getstats()) | ||||
|
r40228 | stats.sort(pycompat.sysstr(field)) | ||
|
r29781 | stats.pprint(limit=limit, file=fp, climit=climit) | ||
|
r43346 | |||
|
r29783 | @contextlib.contextmanager | ||
def flameprofile(ui, fp): | ||||
|
r29781 | try: | ||
|
r44130 | from flamegraph import flamegraph # pytype: disable=import-error | ||
|
r29781 | except ImportError: | ||
|
r43346 | raise error.Abort( | ||
_( | ||||
|
r43347 | b'flamegraph not available - install from ' | ||
b'https://github.com/evanhempel/python-flamegraph' | ||||
|
r43346 | ) | ||
) | ||||
|
r29781 | # developer config: profiling.freq | ||
|
r43347 | freq = ui.configint(b'profiling', b'freq') | ||
|
r29781 | filter_ = None | ||
collapse_recursion = True | ||||
|
r43346 | 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() | ||||
|
r43346 | print( | ||
|
r43347 | b'Collected %d stack frames (%d unique) in %2.2f seconds.' | ||
|
r43346 | % ( | ||
util.timer() - start_time, | ||||
thread.num_frames(), | ||||
thread.num_frames(unique=True), | ||||
) | ||||
) | ||||
|
r29781 | |||
|
r29783 | @contextlib.contextmanager | ||
def statprofile(ui, fp): | ||||
|
r30316 | from . import statprof | ||
|
r29781 | |||
|
r43347 | freq = ui.configint(b'profiling', b'freq') | ||
|
r29781 | 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: | ||
|
r43347 | ui.warn(_(b"invalid sampling frequency '%s' - ignoring\n") % freq) | ||
|
r29781 | |||
|
r43346 | track = ui.config( | ||
|
r43347 | b'profiling', b'time-track', pycompat.iswindows and b'cpu' or b'real' | ||
|
r43346 | ) | ||
|
r43347 | statprof.start(mechanism=b'thread', track=track) | ||
|
r30316 | |||
|
r29781 | try: | ||
|
r29783 | yield | ||
|
r29781 | finally: | ||
|
r30316 | data = statprof.stop() | ||
|
r43347 | profformat = ui.config(b'profiling', b'statformat') | ||
|
r30316 | |||
formats = { | ||||
|
r43347 | b'byline': statprof.DisplayFormats.ByLine, | ||
b'bymethod': statprof.DisplayFormats.ByMethod, | ||||
b'hotpath': statprof.DisplayFormats.Hotpath, | ||||
b'json': statprof.DisplayFormats.Json, | ||||
b'chrome': statprof.DisplayFormats.Chrome, | ||||
|
r30316 | } | ||
if profformat in formats: | ||||
displayformat = formats[profformat] | ||||
else: | ||||
|
r43347 | ui.warn(_(b'unknown profiler output format: %s\n') % profformat) | ||
|
r30316 | displayformat = statprof.DisplayFormats.Hotpath | ||
|
r30930 | kwargs = {} | ||
def fraction(s): | ||||
r32978 | if isinstance(s, (float, int)): | |||
return float(s) | ||||
|
r43347 | if s.endswith(b'%'): | ||
|
r30930 | v = float(s[:-1]) / 100 | ||
else: | ||||
v = float(s) | ||||
if 0 <= v <= 1: | ||||
return v | ||||
raise ValueError(s) | ||||
|
r43347 | if profformat == b'chrome': | ||
showmin = ui.configwith(fraction, b'profiling', b'showmin', 0.005) | ||||
showmax = ui.configwith(fraction, b'profiling', b'showmax') | ||||
|
r30930 | kwargs.update(minthreshold=showmin, maxthreshold=showmax) | ||
|
r43347 | elif profformat == b'hotpath': | ||
|
r33192 | # inconsistent config: profiling.showmin | ||
|
r43347 | limit = ui.configwith(fraction, b'profiling', b'showmin', 0.05) | ||
|
r43906 | kwargs['limit'] = limit | ||
|
r43347 | showtime = ui.configbool(b'profiling', b'showtime') | ||
|
r43906 | kwargs['showtime'] = showtime | ||
|
r30930 | |||
statprof.display(fp, data=data, format=displayformat, **kwargs) | ||||
|
r29781 | |||
|
r43346 | |||
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. | ||||
""" | ||||
|
r43346 | |||
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: | ||||
|
r44131 | raise error.ProgrammingError(b'use a context manager to start') | ||
r32784 | if self._started: | |||
return | ||||
self._started = True | ||||
|
r43347 | profiler = encoding.environ.get(b'HGPROF') | ||
r32783 | proffn = None | |||
if profiler is None: | ||||
|
r43347 | profiler = self._ui.config(b'profiling', b'type') | ||
if profiler not in (b'ls', b'stat', b'flame'): | ||||
r32783 | # try load profiler from extension with the same name | |||
proffn = _loadprofiler(self._ui, profiler) | ||||
if proffn is None: | ||||
|
r43346 | self._ui.warn( | ||
|
r43347 | _(b"unrecognized profiler '%s' - ignored\n") % profiler | ||
|
r43346 | ) | ||
|
r43347 | profiler = b'stat' | ||
|
r29781 | |||
|
r43347 | self._output = self._ui.config(b'profiling', b'output') | ||
|
r29781 | |||
r32808 | try: | |||
|
r43347 | if self._output == b'blackbox': | ||
r32807 | self._fp = util.stringio() | |||
elif self._output: | ||||
path = self._ui.expandpath(self._output) | ||||
|
r43347 | self._fp = open(path, b'wb') | ||
|
r36701 | elif pycompat.iswindows: | ||
# parse escape sequence by win32print() | ||||
class uifp(object): | ||||
def __init__(self, ui): | ||||
self._ui = ui | ||||
|
r43346 | |||
|
r36701 | def write(self, data): | ||
self._ui.write_err(data) | ||||
|
r43346 | |||
|
r36701 | def flush(self): | ||
self._ui.flush() | ||||
|
r43346 | |||
|
r36701 | self._fpdoclose = False | ||
self._fp = uifp(self._ui) | ||||
r32807 | else: | |||
self._fpdoclose = False | ||||
self._fp = self._ui.ferr | ||||
r32783 | ||||
r32807 | if proffn is not None: | |||
pass | ||||
|
r43347 | elif profiler == b'ls': | ||
r32807 | proffn = lsprofile | |||
|
r43347 | elif profiler == b'flame': | ||
r32807 | proffn = flameprofile | |||
else: | ||||
proffn = statprofile | ||||
|
r29783 | |||
r32807 | self._profiler = proffn(self._ui, self._fp) | |||
self._profiler.__enter__() | ||||
|
r43346 | except: # re-raises | ||
r32808 | self._closefp() | |||
raise | ||||
|
r29783 | |||
r32783 | def __exit__(self, exception_type, exception_value, traceback): | |||
r32810 | propagate = None | |||
r32809 | if self._profiler is not None: | |||
|
r43346 | propagate = self._profiler.__exit__( | ||
exception_type, exception_value, traceback | ||||
) | ||||
|
r43347 | if self._output == b'blackbox': | ||
val = b'Profile:\n%s' % self._fp.getvalue() | ||||
r32809 | # ui.log treats the input as a format string, | |||
# so we need to escape any % signs. | ||||
|
r43347 | val = val.replace(b'%', b'%%') | ||
self._ui.log(b'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() | ||||