Commit 25ff8d4a authored by Jason Madden's avatar Jason Madden

Introduce GEVENT_TRACK_GREENLET_TREE to disable greenlet tree features

As a performance optimization for applications where spawning
greenlets is critical. Plus some other optimizations to speed up
spawning in the general case.

CPython 3.6 with 1.2.2 vs these changes with tracking disabled:

| Benchmark              | 36_122_bench_spawn | 36config_bench_spawn_tree_off |
+------------------------+--------------------+-------------------------------+
| eventlet spawn         | 12.6 us            | 12.2 us: 1.04x faster (-4%)   |
| eventlet sleep         | 5.22 us            | 4.97 us: 1.05x faster (-5%)   |
| gevent spawn           | 4.27 us            | 5.06 us: 1.19x slower (+19%)  |
| gevent sleep           | 2.63 us            | 1.25 us: 2.11x faster (-53%)  |
| geventpool spawn       | 9.00 us            | 8.31 us: 1.08x faster (-8%)   |
| geventpool sleep       | 4.82 us            | 2.83 us: 1.70x faster (-41%)  |
| geventraw spawn        | 2.51 us            | 2.81 us: 1.12x slower (+12%)  |
| geventraw sleep        | 649 ns             | 679 ns: 1.05x slower (+5%)    |
| geventpool join        | 3.47 us            | 1.42 us: 2.44x faster (-59%)  |
| geventpool spawn kwarg | 11.0 us            | 8.95 us: 1.23x faster (-19%)  |
| geventraw spawn kwarg  | 3.87 us            | 4.20 us: 1.08x slower (+8%)   |

The differences compared to master are hard to quantify because the
standard deviation ends up being more than 10% of the mean in many
cases---and about a 10% improvement is what we typically see, so it
goes back and forth.
parent 373a5271
......@@ -14,6 +14,14 @@
monkey-patch the underlying class, ``threading._Event``. Some code
may be type-checking for that. See :issue:`1136`.
- Introduce the configuration variable
`gevent.config.track_greenlet_tree` (aka
``GEVENT_TRACK_GREENLET_TREE``) to allow disabling the greenlet tree
features for applications where greenlet spawning is performance
critical. This restores spawning performance to 1.2 levels.
- Add additional optimizations for spawning greenlets, making it
faster than 1.3a2.
1.3a2 (2018-03-06)
==================
......
......@@ -109,6 +109,8 @@ def validate_bool(value):
def validate_anything(value):
return value
convert_str_value_as_is = validate_anything
class Setting(object):
name = None
value = None
......@@ -396,6 +398,27 @@ class TraceMalloc(Setting):
tracking information for FFI objects.
"""
class TrackGreenletTree(Setting):
name = 'track_greenlet_tree'
environment_key = 'GEVENT_TRACK_GREENLET_TREE'
default = True
validate = staticmethod(validate_bool)
# Don't do string-to-list conversion.
_convert = staticmethod(convert_str_value_as_is)
desc = """\
Should `Greenlet` objects track their spawning tree?
Setting this to a false value will make spawning `Greenlet`
objects and using `spawn_raw` faster, but the
``spawning_greenlet``, ``spawn_tree_locals`` and ``spawning_stack``
will not be captured.
.. versionadded:: 1.3b1
"""
# The ares settings are all interpreted by
# gevent/resolver/ares.pyx, so we don't do
# any validation here.
......@@ -410,7 +433,7 @@ class AresSettingMixin(object):
validate = staticmethod(validate_anything)
_convert = staticmethod(validate_anything)
_convert = staticmethod(convert_str_value_as_is)
class AresFlags(AresSettingMixin, Setting):
name = 'ares_flags'
......
......@@ -75,6 +75,9 @@ cdef inline list _extract_stack(int limit)
@cython.locals(previous=_Frame, frame=tuple, f=_Frame)
cdef _Frame _Frame_from_list(list frames)
@cython.final
cdef inline get_hub()
cdef class Greenlet(greenlet):
cdef readonly object value
......@@ -128,7 +131,8 @@ cdef class Greenlet(greenlet):
# doing a module global lookup. This is especially important
# for spawning greenlets.
cdef _greenlet__init__
cdef get_hub
cdef _threadlocal
cdef get_hub_class
cdef wref
cdef Timeout
......@@ -139,6 +143,7 @@ cdef wait
cdef iwait
cdef reraise
cdef InvalidSwitchError
cpdef GEVENT_CONFIG
@cython.final
......
......@@ -16,10 +16,13 @@ from gevent._tblib import load_traceback
from gevent.hub import GreenletExit
from gevent.hub import InvalidSwitchError
from gevent.hub import Waiter
from gevent.hub import get_hub
from gevent.hub import _threadlocal
from gevent.hub import get_hub_class
from gevent.hub import iwait
from gevent.hub import wait
from gevent.timeout import Timeout
from gevent._config import config as GEVENT_CONFIG
from gevent._util import Lazy
from gevent._util import readproperty
......@@ -39,6 +42,16 @@ __all__ = [
locals()['getcurrent'] = __import__('greenlet').getcurrent
locals()['greenlet_init'] = lambda: None
def get_hub():
# This is identical to gevent.hub._get_hub_noargs so that it
# can be inlined for greenlet spawning by cython.
hub = _threadlocal.hub
if hub is None:
hubtype = get_hub_class()
hub = _threadlocal.hub = hubtype()
return hub
if _PYPY:
import _continuation # pylint:disable=import-error
_continulet = _continuation.continulet
......@@ -204,6 +217,12 @@ class Greenlet(greenlet):
spawning greenlets, specify a larger value for improved debugging.
.. versionadded:: 1.3a2
.. versionchanged:: 1.3b1
The ``GEVENT_TRACK_GREENLET_TREE`` configuration value may be set to
a false value to disable ``spawn_tree_locals``, ``spawning_greenlet``,
and ``spawning_stack``. The first two will be None in that case, and the
latter will be empty.
"""
# greenlet.greenlet(run=None, parent=None)
# Calling it with both positional arguments instead of a keyword
......@@ -233,7 +252,6 @@ class Greenlet(greenlet):
_greenlet__init__(self, None, get_hub())
#super(Greenlet, self).__init__(None, get_hub())
if run is not None:
self._run = run
......@@ -266,6 +284,7 @@ class Greenlet(greenlet):
# Failed with exception: (t, v, dump_traceback(tb)))
self._exc_info = None
if GEVENT_CONFIG.track_greenlet_tree:
spawner = getcurrent() # pylint:disable=undefined-variable
self.spawning_greenlet = wref(spawner)
try:
......@@ -279,6 +298,12 @@ class Greenlet(greenlet):
self._spawning_stack_frames = _extract_stack(self.spawning_stack_limit)
self._spawning_stack_frames.extend(getattr(spawner, '_spawning_stack_frames', []))
else:
# None is the default for all of these in Cython, but we
# need to declare them for pure-Python mode.
self.spawning_greenlet = None
self.spawn_tree_locals = None
self._spawning_stack_frames = None
@Lazy
def spawning_stack(self):
......@@ -286,7 +311,7 @@ class Greenlet(greenlet):
# code. It's tempting to discard _spawning_stack_frames
# after this, but child greenlets may still be created
# that need it.
return _Frame_from_list(self._spawning_stack_frames)
return _Frame_from_list(self._spawning_stack_frames or [])
def _get_minimal_ident(self):
reg = self.parent.ident_registry
......
......@@ -27,7 +27,7 @@ __all__ = [
'Waiter',
]
from gevent._config import config
from gevent._config import config as GEVENT_CONFIG
from gevent._compat import string_types
from gevent._compat import xrange
from gevent._util import _NONE
......@@ -63,8 +63,6 @@ get_ident = thread.get_ident
MAIN_THREAD = get_ident()
class LoopExit(Exception):
"""
Exception thrown when the hub finishes running.
......@@ -83,7 +81,6 @@ class LoopExit(Exception):
affinity from a different thread
"""
pass
class BlockingSwitchOutError(AssertionError):
......@@ -101,6 +98,24 @@ class ConcurrentObjectUseError(AssertionError):
# first one
pass
class TrackedRawGreenlet(RawGreenlet):
def __init__(self, function, parent):
RawGreenlet.__init__(self, function, parent)
# See greenlet.py's Greenlet class. We capture the cheap
# parts to maintain the tree structure, but we do not capture
# the stack because that's too expensive for 'spawn_raw'.
current = getcurrent()
self.spawning_greenlet = wref(current)
# See Greenlet for how trees are maintained.
try:
self.spawn_tree_locals = current.spawn_tree_locals
except AttributeError:
self.spawn_tree_locals = {}
if current.parent:
current.spawn_tree_locals = self.spawn_tree_locals
def spawn_raw(function, *args, **kwargs):
"""
......@@ -129,36 +144,31 @@ def spawn_raw(function, *args, **kwargs):
Populate the ``spawning_greenlet`` and ``spawn_tree_locals``
attributes of the returned greenlet.
.. versionchanged:: 1.3b1
*Only* populate ``spawning_greenlet`` and ``spawn_tree_locals``
if ``GEVENT_TRACK_GREENLET_TREE`` is enabled (the default). If not enabled,
those attributes will not be set.
"""
if not callable(function):
raise TypeError("function must be callable")
# The hub is always the parent.
hub = get_hub()
hub = _get_hub_noargs()
factory = TrackedRawGreenlet if GEVENT_CONFIG.track_greenlet_tree else RawGreenlet
# The callback class object that we use to run this doesn't
# accept kwargs (and those objects are heavily used, as well as being
# implemented twice in core.ppyx and corecffi.py) so do it with a partial
if kwargs:
function = _functools_partial(function, *args, **kwargs)
g = RawGreenlet(function, hub)
g = factory(function, hub)
hub.loop.run_callback(g.switch)
else:
g = RawGreenlet(function, hub)
g = factory(function, hub)
hub.loop.run_callback(g.switch, *args)
# See greenlet.py's Greenlet class. We capture the cheap
# parts to maintain the tree structure, but we do not capture
# the stack because that's too expensive.
current = getcurrent()
g.spawning_greenlet = wref(current)
# See Greenlet for how trees are maintained.
try:
g.spawn_tree_locals = current.spawn_tree_locals
except AttributeError:
g.spawn_tree_locals = {}
if current.parent:
current.spawn_tree_locals = g.spawn_tree_locals
return g
......@@ -187,7 +197,7 @@ def sleep(seconds=0, ref=True):
.. seealso:: :func:`idle`
"""
hub = get_hub()
hub = _get_hub_noargs()
loop = hub.loop
if seconds <= 0:
waiter = Waiter()
......@@ -209,7 +219,7 @@ def idle(priority=0):
.. seealso:: :func:`sleep`
"""
hub = get_hub()
hub = _get_hub_noargs()
watcher = hub.loop.idle()
if priority:
watcher.priority = priority
......@@ -243,7 +253,7 @@ def kill(greenlet, exception=GreenletExit):
# after it's been killed
greenlet.kill(exception=exception, block=False)
else:
get_hub().loop.run_callback(greenlet.throw, exception)
_get_hub_noargs().loop.run_callback(greenlet.throw, exception)
class signal(object):
......@@ -274,7 +284,7 @@ class signal(object):
if not callable(handler):
raise TypeError("signal handler must be callable.")
self.hub = get_hub()
self.hub = _get_hub_noargs()
self.watcher = self.hub.loop.signal(signalnum, ref=False)
self.watcher.start(self._start)
self.handler = handler
......@@ -393,6 +403,12 @@ def get_hub(*args, **kwargs):
If a hub does not exist in the current thread, a new one is
created of the type returned by :func:`get_hub_class`.
.. deprecated:: 1.3b1
The ``*args`` and ``**kwargs`` arguments are deprecated. They were
only used when the hub was created, and so were non-deterministic---to be
sure they were used, *all* callers had to pass them, or they were order-dependent.
Use ``set_hub`` instead.
"""
hub = _threadlocal.hub
if hub is None:
......@@ -400,6 +416,15 @@ def get_hub(*args, **kwargs):
hub = _threadlocal.hub = hubtype(*args, **kwargs)
return hub
def _get_hub_noargs():
# Just like get_hub, but cheaper to call because it
# takes no arguments or kwargs. See also a copy in
# gevent/greenlet.py
hub = _threadlocal.hub
if hub is None:
hubtype = get_hub_class()
hub = _threadlocal.hub = hubtype()
return hub
def _get_hub():
"""Return the hub for the current thread.
......@@ -428,7 +453,7 @@ def _config(default, envvar):
hub_ident_registry = IdentRegistry()
class Hub(RawGreenlet):
class Hub(TrackedRawGreenlet):
"""
A greenlet that runs the event loop.
......@@ -457,7 +482,7 @@ class Hub(RawGreenlet):
def __init__(self, loop=None, default=None):
RawGreenlet.__init__(self)
TrackedRawGreenlet.__init__(self, None, None)
if hasattr(loop, 'run'):
if default is not None:
raise TypeError("Unexpected argument: default")
......@@ -476,7 +501,7 @@ class Hub(RawGreenlet):
self.loop = self.loop_class(flags=loop, default=default) # pylint:disable=not-callable
self._resolver = None
self._threadpool = None
self.format_context = config.format_context
self.format_context = GEVENT_CONFIG.format_context
self.minimal_ident = hub_ident_registry.get_ident(self)
@Lazy
......@@ -485,11 +510,11 @@ class Hub(RawGreenlet):
@property
def loop_class(self):
return config.loop
return GEVENT_CONFIG.loop
@property
def backend(self):
return config.libev_backend
return GEVENT_CONFIG.libev_backend
def __repr__(self):
if self.loop is None:
......@@ -741,7 +766,7 @@ class Hub(RawGreenlet):
@property
def resolver_class(self):
return config.resolver
return GEVENT_CONFIG.resolver
def _get_resolver(self):
if self._resolver is None:
......@@ -759,7 +784,7 @@ class Hub(RawGreenlet):
@property
def threadpool_class(self):
return config.threadpool
return GEVENT_CONFIG.threadpool
def _get_threadpool(self):
if self._threadpool is None:
......@@ -820,7 +845,7 @@ class Waiter(object):
__slots__ = ['hub', 'greenlet', 'value', '_exception']
def __init__(self, hub=None):
self.hub = get_hub() if hub is None else hub
self.hub = _get_hub_noargs() if hub is None else hub
self.greenlet = None
self.value = None
self._exception = _NONE
......@@ -963,7 +988,7 @@ def iwait(objects, timeout=None, count=None):
"""
# QQQ would be nice to support iterable here that can be generated slowly (why?)
if objects is None:
yield get_hub().join(timeout=timeout)
yield _get_hub_noargs().join(timeout=timeout)
return
count = len(objects) if count is None else min(count, len(objects))
......@@ -971,7 +996,7 @@ def iwait(objects, timeout=None, count=None):
switch = waiter.switch
if timeout is not None:
timer = get_hub().loop.timer(timeout, priority=-1)
timer = _get_hub_noargs().loop.timer(timeout, priority=-1)
timer.start(switch, _NONE)
try:
......@@ -1031,7 +1056,7 @@ def wait(objects=None, timeout=None, count=None):
.. seealso:: :func:`iwait`
"""
if objects is None:
return get_hub().join(timeout=timeout)
return _get_hub_noargs().join(timeout=timeout)
return list(iwait(objects, timeout, count))
......
......@@ -283,6 +283,10 @@ class GreenletTree(object):
tb = ''.join(traceback.format_stack(frame))
tree.child_multidata(tb)
@staticmethod
def __spawning_parent(greenlet):
return (getattr(greenlet, 'spawning_greenlet', None) or _noop)()
def __render_locals(self, tree):
gr_locals = all_local_dicts_for_greenlet(self.greenlet)
if gr_locals:
......@@ -316,7 +320,7 @@ class GreenletTree(object):
if spawning_stack and tree.details and tree.details['stacks']:
self.__render_tb(tree, 'Spawned at:', spawning_stack)
spawning_parent = getattr(self.greenlet, 'spawning_greenlet', _noop)()
spawning_parent = self.__spawning_parent(self.greenlet)
tree_locals = getattr(self.greenlet, 'spawn_tree_locals', None)
if tree_locals and tree_locals is not getattr(spawning_parent, 'spawn_tree_locals', None):
tree.child_data('Spawn Tree Locals')
......@@ -377,7 +381,7 @@ class GreenletTree(object):
if not isinstance(ob, RawGreenlet):
continue
spawn_parent = getattr(ob, 'spawning_greenlet', _noop)()
spawn_parent = cls.__spawning_parent(ob)
if spawn_parent is None:
root = cls._root_greenlet(ob)
......
......@@ -59,15 +59,22 @@ class TestFormat(greentest.TestCase):
@greentest.skipOnPyPy("See TestFormat")
class TestTree(greentest.TestCase):
@greentest.ignores_leakcheck
def test_tree(self):
def setUp(self):
super(TestTree, self).setUp()
self.track_greenlet_tree = gevent.config.track_greenlet_tree
gevent.config.track_greenlet_tree = True
def tearDown(self):
gevent.config.track_greenlet_tree = self.track_greenlet_tree
super(TestTree, self).tearDown()
def _build_tree(self):
# pylint:disable=too-many-locals
# Python 2.7 on Travis seems to show unexpected greenlet objects
# so perhaps we need a GC?
gc.collect()
for _ in range(3):
gc.collect()
import re
glets = []
l = MyLocal(42)
assert l
......@@ -96,6 +103,8 @@ class TestTree(greentest.TestCase):
return s(t2)
s3 = s(t3)
if s3.spawn_tree_locals is not None:
# Can only do this if we're tracking spawn trees
s3.spawn_tree_locals['stl'] = 'STL'
s3.join()
......@@ -104,10 +113,17 @@ class TestTree(greentest.TestCase):
s4.join()
tree = s4.value
return tree, str(tree), tree.format(details={'stacks': False})
@greentest.ignores_leakcheck
def test_tree(self):
import re
tree, str_tree, tree_format = self._build_tree()
self.assertTrue(tree.root)
self.assertNotIn('Parent', str(tree)) # Simple output
value = tree.format(details={'stacks': False})
self.assertNotIn('Parent', str_tree) # Simple output
value = tree_format
hexobj = re.compile('0x[0123456789abcdef]+L?', re.I)
value = hexobj.sub('X', value)
value = value.replace('epoll', 'select')
......@@ -145,5 +161,11 @@ class TestTree(greentest.TestCase):
""".strip()
self.assertEqual(value, expected)
@greentest.ignores_leakcheck
def test_tree_no_track(self):
gevent.config.track_greenlet_tree = False
self._build_tree()
if __name__ == '__main__':
greentest.main()
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment