profiling.py
361 lines
| 10.5 KiB
| text/x-python
|
PythonLexer
/ mercurial / profiling.py
Gregory Szorc
|
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. | ||||
Matt Harbison
|
r52756 | from __future__ import annotations | ||
Gregory Szorc
|
r29781 | |||
Gregory Szorc
|
r29783 | import contextlib | ||
Arseniy Alekseyev
|
r52654 | import os | ||
import signal | ||||
import subprocess | ||||
r52733 | import sys | |||
Gregory Szorc
|
r29781 | |||
from .i18n import _ | ||||
from . import ( | ||||
Pulkit Goyal
|
r30820 | encoding, | ||
Gregory Szorc
|
r29781 | error, | ||
Jun Wu
|
r32417 | extensions, | ||
Matt Harbison
|
r36701 | pycompat, | ||
Gregory Szorc
|
r29781 | util, | ||
) | ||||
Augie Fackler
|
r43346 | |||
Jun Wu
|
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) | ||||
Augie Fackler
|
r43346 | |||
Gregory Szorc
|
r29783 | @contextlib.contextmanager | ||
def lsprofile(ui, fp): | ||||
Augie Fackler
|
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') | ||||
Gregory Szorc
|
r29781 | |||
Augie Fackler
|
r43347 | if format not in [b'text', b'kcachegrind']: | ||
Martin von Zweigbergk
|
r43387 | ui.warn(_(b"unrecognized profiling format '%s' - Ignored\n") % format) | ||
Augie Fackler
|
r43347 | format = b'text' | ||
Gregory Szorc
|
r29781 | |||
try: | ||||
from . import lsprof | ||||
except ImportError: | ||||
Augie Fackler
|
r43346 | raise error.Abort( | ||
_( | ||||
Augie Fackler
|
r43347 | b'lsprof not available - install from ' | ||
b'http://codespeak.net/svn/user/arigo/hack/misc/lsprof/' | ||||
Augie Fackler
|
r43346 | ) | ||
) | ||||
Gregory Szorc
|
r29781 | p = lsprof.Profiler() | ||
r52733 | try: | |||
p.enable(subcalls=True) | ||||
except ValueError as exc: | ||||
if str(exc) != "Another profiling tool is already active": | ||||
raise | ||||
if not hasattr(sys, "monitoring"): | ||||
raise | ||||
# python >=3.12 prevent more than one profiler to run at the same | ||||
# time, tries to improve the report to help the user understand | ||||
# what is going on. | ||||
other_tool_name = sys.monitoring.get_tool(sys.monitoring.PROFILER_ID) | ||||
if other_tool_name == "cProfile": | ||||
Matt Harbison
|
r52790 | msg = b'cannot recursively call `lsprof`' | ||
r52733 | raise error.Abort(msg) from None | |||
else: | ||||
Matt Harbison
|
r52790 | tool = b'<unknown>' | ||
if other_tool_name: | ||||
tool = encoding.strtolocal(other_tool_name) | ||||
m = b'failed to start "lsprofile"; another profiler already running: %s' | ||||
raise error.Abort(_(m) % tool) from None | ||||
Gregory Szorc
|
r29781 | try: | ||
Gregory Szorc
|
r29783 | yield | ||
Gregory Szorc
|
r29781 | finally: | ||
p.disable() | ||||
Augie Fackler
|
r43347 | if format == b'kcachegrind': | ||
Gregory Szorc
|
r29781 | from . import lsprofcalltree | ||
Augie Fackler
|
r43346 | |||
Gregory Szorc
|
r29781 | calltree = lsprofcalltree.KCacheGrind(p) | ||
calltree.output(fp) | ||||
else: | ||||
# format == 'text' | ||||
stats = lsprof.Stats(p.getstats()) | ||||
Gregory Szorc
|
r40228 | stats.sort(pycompat.sysstr(field)) | ||
Gregory Szorc
|
r29781 | stats.pprint(limit=limit, file=fp, climit=climit) | ||
r52481 | fp.flush() | |||
Gregory Szorc
|
r29781 | |||
Augie Fackler
|
r43346 | |||
Gregory Szorc
|
r29783 | @contextlib.contextmanager | ||
def flameprofile(ui, fp): | ||||
Gregory Szorc
|
r29781 | try: | ||
Matt Harbison
|
r44130 | from flamegraph import flamegraph # pytype: disable=import-error | ||
Gregory Szorc
|
r29781 | except ImportError: | ||
Augie Fackler
|
r43346 | raise error.Abort( | ||
_( | ||||
Augie Fackler
|
r43347 | b'flamegraph not available - install from ' | ||
b'https://github.com/evanhempel/python-flamegraph' | ||||
Augie Fackler
|
r43346 | ) | ||
) | ||||
Gregory Szorc
|
r29781 | # developer config: profiling.freq | ||
Augie Fackler
|
r43347 | freq = ui.configint(b'profiling', b'freq') | ||
Gregory Szorc
|
r29781 | filter_ = None | ||
collapse_recursion = True | ||||
Augie Fackler
|
r43346 | thread = flamegraph.ProfileThread( | ||
fp, 1.0 / freq, filter_, collapse_recursion | ||||
) | ||||
Simon Farnsworth
|
r30975 | start_time = util.timer() | ||
Gregory Szorc
|
r29781 | try: | ||
thread.start() | ||||
Gregory Szorc
|
r29783 | yield | ||
Gregory Szorc
|
r29781 | finally: | ||
thread.stop() | ||||
thread.join() | ||||
r52481 | m = b'Collected %d stack frames (%d unique) in %2.2f seconds.' | |||
m %= ( | ||||
( | ||||
Augie Fackler
|
r43346 | util.timer() - start_time, | ||
thread.num_frames(), | ||||
thread.num_frames(unique=True), | ||||
r52481 | ), | |||
Augie Fackler
|
r43346 | ) | ||
r52481 | print(m, flush=True) | |||
Augie Fackler
|
r43346 | |||
Gregory Szorc
|
r29781 | |||
Gregory Szorc
|
r29783 | @contextlib.contextmanager | ||
def statprofile(ui, fp): | ||||
Gregory Szorc
|
r30316 | from . import statprof | ||
Gregory Szorc
|
r29781 | |||
Augie Fackler
|
r43347 | freq = ui.configint(b'profiling', b'freq') | ||
Gregory Szorc
|
r29781 | if freq > 0: | ||
Gregory Szorc
|
r29785 | # Cannot reset when profiler is already active. So silently no-op. | ||
if statprof.state.profile_level == 0: | ||||
statprof.reset(freq) | ||||
Gregory Szorc
|
r29781 | else: | ||
Augie Fackler
|
r43347 | ui.warn(_(b"invalid sampling frequency '%s' - ignoring\n") % freq) | ||
Gregory Szorc
|
r29781 | |||
Augie Fackler
|
r43346 | track = ui.config( | ||
Augie Fackler
|
r43347 | b'profiling', b'time-track', pycompat.iswindows and b'cpu' or b'real' | ||
Augie Fackler
|
r43346 | ) | ||
Augie Fackler
|
r43347 | statprof.start(mechanism=b'thread', track=track) | ||
Gregory Szorc
|
r30316 | |||
Gregory Szorc
|
r29781 | try: | ||
Gregory Szorc
|
r29783 | yield | ||
Gregory Szorc
|
r29781 | finally: | ||
Gregory Szorc
|
r30316 | data = statprof.stop() | ||
Augie Fackler
|
r43347 | profformat = ui.config(b'profiling', b'statformat') | ||
Gregory Szorc
|
r30316 | |||
formats = { | ||||
Augie Fackler
|
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, | ||||
Gregory Szorc
|
r30316 | } | ||
if profformat in formats: | ||||
displayformat = formats[profformat] | ||||
else: | ||||
Augie Fackler
|
r43347 | ui.warn(_(b'unknown profiler output format: %s\n') % profformat) | ||
Gregory Szorc
|
r30316 | displayformat = statprof.DisplayFormats.Hotpath | ||
Bryan O'Sullivan
|
r30930 | kwargs = {} | ||
def fraction(s): | ||||
r32978 | if isinstance(s, (float, int)): | |||
return float(s) | ||||
Augie Fackler
|
r43347 | if s.endswith(b'%'): | ||
Bryan O'Sullivan
|
r30930 | v = float(s[:-1]) / 100 | ||
else: | ||||
v = float(s) | ||||
if 0 <= v <= 1: | ||||
return v | ||||
raise ValueError(s) | ||||
Augie Fackler
|
r43347 | if profformat == b'chrome': | ||
showmin = ui.configwith(fraction, b'profiling', b'showmin', 0.005) | ||||
showmax = ui.configwith(fraction, b'profiling', b'showmax') | ||||
Bryan O'Sullivan
|
r30930 | kwargs.update(minthreshold=showmin, maxthreshold=showmax) | ||
Augie Fackler
|
r43347 | elif profformat == b'hotpath': | ||
Gregory Szorc
|
r33192 | # inconsistent config: profiling.showmin | ||
Augie Fackler
|
r43347 | limit = ui.configwith(fraction, b'profiling', b'showmin', 0.05) | ||
Augie Fackler
|
r43906 | kwargs['limit'] = limit | ||
Augie Fackler
|
r43347 | showtime = ui.configbool(b'profiling', b'showtime') | ||
Augie Fackler
|
r43906 | kwargs['showtime'] = showtime | ||
Bryan O'Sullivan
|
r30930 | |||
statprof.display(fp, data=data, format=displayformat, **kwargs) | ||||
r52481 | fp.flush() | |||
Gregory Szorc
|
r29781 | |||
Augie Fackler
|
r43346 | |||
Arseniy Alekseyev
|
r52654 | @contextlib.contextmanager | ||
def pyspy_profile(ui, fp): | ||||
exe = ui.config(b'profiling', b'py-spy.exe') | ||||
freq = ui.configint(b'profiling', b'py-spy.freq') | ||||
format = ui.config(b'profiling', b'py-spy.format') | ||||
fd = fp.fileno() | ||||
output_path = "/dev/fd/%d" % (fd) | ||||
my_pid = os.getpid() | ||||
cmd = [ | ||||
exe, | ||||
"record", | ||||
"--pid", | ||||
str(my_pid), | ||||
"--native", | ||||
"--rate", | ||||
str(freq), | ||||
"--output", | ||||
output_path, | ||||
] | ||||
if format: | ||||
cmd.extend(["--format", format]) | ||||
proc = subprocess.Popen( | ||||
cmd, | ||||
pass_fds={fd}, | ||||
stdout=subprocess.PIPE, | ||||
) | ||||
_ = proc.stdout.readline() | ||||
try: | ||||
yield | ||||
finally: | ||||
os.kill(proc.pid, signal.SIGINT) | ||||
proc.communicate() | ||||
Gregory Szorc
|
r49801 | class profile: | ||
Gregory Szorc
|
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. | ||||
""" | ||||
Augie Fackler
|
r43346 | |||
r32785 | def __init__(self, ui, enabled=True): | |||
r32783 | self._ui = ui | |||
self._output = None | ||||
self._fp = None | ||||
r32805 | self._fpdoclose = True | |||
Kyle Lippincott
|
r44653 | self._flushfp = None | ||
r32783 | self._profiler = None | |||
r32785 | self._enabled = enabled | |||
r32784 | self._entered = False | |||
self._started = False | ||||
Gregory Szorc
|
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: | ||||
Matt Harbison
|
r44131 | raise error.ProgrammingError(b'use a context manager to start') | ||
r32784 | if self._started: | |||
return | ||||
self._started = True | ||||
Augie Fackler
|
r43347 | profiler = encoding.environ.get(b'HGPROF') | ||
r32783 | proffn = None | |||
if profiler is None: | ||||
Augie Fackler
|
r43347 | profiler = self._ui.config(b'profiling', b'type') | ||
Arseniy Alekseyev
|
r52654 | if profiler not in (b'ls', b'stat', b'flame', b'py-spy'): | ||
r32783 | # try load profiler from extension with the same name | |||
proffn = _loadprofiler(self._ui, profiler) | ||||
if proffn is None: | ||||
Augie Fackler
|
r43346 | self._ui.warn( | ||
Augie Fackler
|
r43347 | _(b"unrecognized profiler '%s' - ignored\n") % profiler | ||
Augie Fackler
|
r43346 | ) | ||
Augie Fackler
|
r43347 | profiler = b'stat' | ||
Gregory Szorc
|
r29781 | |||
Augie Fackler
|
r43347 | self._output = self._ui.config(b'profiling', b'output') | ||
Gregory Szorc
|
r29781 | |||
r32808 | try: | |||
Augie Fackler
|
r43347 | if self._output == b'blackbox': | ||
r32807 | self._fp = util.stringio() | |||
elif self._output: | ||||
r47720 | path = util.expandpath(self._output) | |||
Matt Harbison
|
r53260 | self._fp = open(path, 'wb') | ||
Matt Harbison
|
r36701 | elif pycompat.iswindows: | ||
# parse escape sequence by win32print() | ||||
Gregory Szorc
|
r49801 | class uifp: | ||
Matt Harbison
|
r36701 | def __init__(self, ui): | ||
self._ui = ui | ||||
Augie Fackler
|
r43346 | |||
Matt Harbison
|
r36701 | def write(self, data): | ||
self._ui.write_err(data) | ||||
Augie Fackler
|
r43346 | |||
Matt Harbison
|
r36701 | def flush(self): | ||
self._ui.flush() | ||||
Augie Fackler
|
r43346 | |||
Matt Harbison
|
r36701 | self._fpdoclose = False | ||
self._fp = uifp(self._ui) | ||||
r32807 | else: | |||
self._fpdoclose = False | ||||
self._fp = self._ui.ferr | ||||
Kyle Lippincott
|
r44653 | # Ensure we've flushed fout before writing to ferr. | ||
self._flushfp = self._ui.fout | ||||
r32783 | ||||
r32807 | if proffn is not None: | |||
pass | ||||
Augie Fackler
|
r43347 | elif profiler == b'ls': | ||
r32807 | proffn = lsprofile | |||
Augie Fackler
|
r43347 | elif profiler == b'flame': | ||
r32807 | proffn = flameprofile | |||
Arseniy Alekseyev
|
r52654 | elif profiler == b'py-spy': | ||
proffn = pyspy_profile | ||||
r32807 | else: | |||
proffn = statprofile | ||||
Gregory Szorc
|
r29783 | |||
r32807 | self._profiler = proffn(self._ui, self._fp) | |||
self._profiler.__enter__() | ||||
Augie Fackler
|
r43346 | except: # re-raises | ||
r32808 | self._closefp() | |||
raise | ||||
Gregory Szorc
|
r29783 | |||
r32783 | def __exit__(self, exception_type, exception_value, traceback): | |||
r32810 | propagate = None | |||
r32809 | if self._profiler is not None: | |||
Kyle Lippincott
|
r44653 | self._uiflush() | ||
Augie Fackler
|
r43346 | propagate = self._profiler.__exit__( | ||
exception_type, exception_value, traceback | ||||
) | ||||
Augie Fackler
|
r43347 | if self._output == b'blackbox': | ||
Matt Harbison
|
r53297 | fp = self._fp | ||
# Help pytype: blackbox output uses io.BytesIO instead of a file | ||||
assert isinstance(fp, util.stringio) | ||||
val = b'Profile:\n%s' % fp.getvalue() | ||||
r32809 | # ui.log treats the input as a format string, | |||
# so we need to escape any % signs. | ||||
Augie Fackler
|
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() | ||||
Kyle Lippincott
|
r44653 | |||
def _uiflush(self): | ||||
if self._flushfp: | ||||
self._flushfp.flush() | ||||