mirror of
https://github.com/facebook/sapling.git
synced 2024-10-10 08:47:12 +03:00
d1e86f5ae3
The statprof sampling profiler runs with significantly less overhead. Its data is therefore more useful. Furthermore, its default output shows the hotpath by default, which I've found to be way more useful than the default profiler's function time table. There is one behavioral regression with this change worth noting: the statprof profiler currently doesn't profile individual hgweb requests like lsprof does. This is because the current implementation of statprof only profiles the thread that started profiling. The ability for lsprof to profile individual hgweb requests is relatively new and likely not widely used. Furthermore, I have plans to modify statprof to support profiling multiple threads. I expect that change to go through several iterations. I'm submitting this patch first so there is more time to test statprof. Perfect is the enemy of good.
177 lines
5.2 KiB
Python
177 lines
5.2 KiB
Python
# 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 contextlib
|
|
import os
|
|
import time
|
|
|
|
from .i18n import _
|
|
from . import (
|
|
error,
|
|
util,
|
|
)
|
|
|
|
@contextlib.contextmanager
|
|
def lsprofile(ui, 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:
|
|
yield
|
|
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)
|
|
|
|
@contextlib.contextmanager
|
|
def flameprofile(ui, 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()
|
|
yield
|
|
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)))
|
|
|
|
@contextlib.contextmanager
|
|
def statprofile(ui, fp):
|
|
from . import statprof
|
|
|
|
freq = ui.configint('profiling', 'freq', default=1000)
|
|
if freq > 0:
|
|
# Cannot reset when profiler is already active. So silently no-op.
|
|
if statprof.state.profile_level == 0:
|
|
statprof.reset(freq)
|
|
else:
|
|
ui.warn(_("invalid sampling frequency '%s' - ignoring\n") % freq)
|
|
|
|
statprof.start(mechanism='thread')
|
|
|
|
try:
|
|
yield
|
|
finally:
|
|
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,
|
|
}
|
|
|
|
if profformat in formats:
|
|
displayformat = formats[profformat]
|
|
else:
|
|
ui.warn(_('unknown profiler output format: %s\n') % profformat)
|
|
displayformat = statprof.DisplayFormats.Hotpath
|
|
|
|
statprof.display(fp, data=data, format=displayformat)
|
|
|
|
@contextlib.contextmanager
|
|
def profile(ui):
|
|
"""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.
|
|
"""
|
|
profiler = os.getenv('HGPROF')
|
|
if profiler is None:
|
|
profiler = ui.config('profiling', 'type', default='stat')
|
|
if profiler not in ('ls', 'stat', 'flame'):
|
|
ui.warn(_("unrecognized profiler '%s' - ignored\n") % profiler)
|
|
profiler = 'stat'
|
|
|
|
output = ui.config('profiling', 'output')
|
|
|
|
if output == 'blackbox':
|
|
fp = util.stringio()
|
|
elif output:
|
|
path = ui.expandpath(output)
|
|
fp = open(path, 'wb')
|
|
else:
|
|
fp = ui.ferr
|
|
|
|
try:
|
|
if profiler == 'ls':
|
|
proffn = lsprofile
|
|
elif profiler == 'flame':
|
|
proffn = flameprofile
|
|
else:
|
|
proffn = statprofile
|
|
|
|
with proffn(ui, fp):
|
|
yield
|
|
|
|
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()
|
|
|
|
@contextlib.contextmanager
|
|
def maybeprofile(ui):
|
|
"""Profile if enabled, else do nothing.
|
|
|
|
This context manager can be used to optionally profile if profiling
|
|
is enabled. Otherwise, it does nothing.
|
|
|
|
The purpose of this context manager is to make calling code simpler:
|
|
just use a single code path for calling into code you may want to profile
|
|
and this function determines whether to start profiling.
|
|
"""
|
|
if ui.configbool('profiling', 'enabled'):
|
|
with profile(ui):
|
|
yield
|
|
else:
|
|
yield
|