Commit 9c8e01d8 authored by Jason Madden's avatar Jason Madden

Compile greenlet with Cython to make up for most of the lost speed.

We're now only ~2x slower, instead of 10x.
parent 15fc1ecc
......@@ -9,6 +9,7 @@ gevent.*.[ch]
src/gevent/__pycache__
src/gevent/_semaphore.c
src/gevent/local.c
src/gevent/greenlet.c
src/gevent/libev/corecext.c
src/gevent/libev/corecext.h
src/gevent/libev/_corecffi.c
......
......@@ -90,6 +90,9 @@
a "spawn tree local" mapping. Based on a proposal from PayPal and
comments by Mahmoud Hashemi and Kurt Rose. See :issue:`755`.
- The :mod:`gevent.greenlet` module is now compiled with Cython to
offset any performance loss due to :issue:`755`.
1.3a1 (2018-01-27)
==================
......
......@@ -223,7 +223,6 @@ latex_documents = [
# prevent some stuff from showing up in docs
import socket
import gevent.socket
del gevent.Greenlet.throw
for item in gevent.socket.__all__[:]:
if getattr(gevent.socket, item) is getattr(socket, item, None):
gevent.socket.__all__.remove(item)
......@@ -36,25 +36,7 @@ generated.
.. automethod:: Greenlet.__init__
.. attribute:: Greenlet.value
Holds the value returned by the function if the greenlet has
finished successfully. Until then, or if it finished in error, ``None``.
.. tip:: Recall that a greenlet killed with the default
:class:`GreenletExit` is considered to have finished
successfully, and the ``GreenletExit`` exception will be
its value.
.. autoattribute:: Greenlet.exception
.. autoattribute:: Greenlet.spawn_tree_locals
:annotation: = {}
.. autoattribute:: Greenlet.spawning_greenlet
:annotation: = weakref.ref()
.. autoattribute:: Greenlet.spawning_stack
:annotation: = <Frame>
.. autoattribute:: Greenlet.spawning_stack_limit
.. automethod:: Greenlet.ready
.. automethod:: Greenlet.successful
.. automethod:: Greenlet.start
......
......@@ -4,6 +4,7 @@ from __future__ import print_function
import sys
import os
import os.path
import sysconfig
# setuptools is *required* on Windows
# (https://bugs.python.org/issue23246) and for PyPy. No reason not to
......@@ -59,11 +60,28 @@ LOCAL = Extension(name="gevent.local",
depends=['src/gevent/local.pxd'])
LOCAL = cythonize1(LOCAL)
# The sysconfig dir is not enough if we're in a virtualenv
# See https://github.com/pypa/pip/issues/4610
include_dirs = [sysconfig.get_path("include")]
venv_include_dir = os.path.join(sys.prefix, 'include', 'site',
'python' + sysconfig.get_python_version())
venv_include_dir = os.path.abspath(venv_include_dir)
if os.path.exists(venv_include_dir):
include_dirs.append(venv_include_dir)
GREENLET = Extension(name="gevent.greenlet",
sources=["src/gevent/greenlet.py"],
depends=['src/gevent/greenlet.pxd'],
include_dirs=include_dirs)
GREENLET = cythonize1(GREENLET)
EXT_MODULES = [
CORE,
ARES,
SEMAPHORE,
LOCAL,
GREENLET,
]
LIBEV_CFFI_MODULE = 'src/gevent/libev/_corecffi_build.py:ffi'
......@@ -91,6 +109,7 @@ if PYPY:
setup_requires = []
EXT_MODULES.remove(CORE)
EXT_MODULES.remove(LOCAL)
EXT_MODULES.remove(GREENLET)
EXT_MODULES.remove(SEMAPHORE)
# By building the semaphore with Cython under PyPy, we get
# atomic operations (specifically, exiting/releasing), at the
......
# cython: auto_pickle=False
cimport cython
cdef extern from "greenlet/greenlet.h":
ctypedef class greenlet.greenlet [object PyGreenlet]:
pass
cdef class SpawnedLink:
cdef public object callback
@cython.final
cdef class SuccessSpawnedLink(SpawnedLink):
pass
@cython.final
cdef class FailureSpawnedLink(SpawnedLink):
pass
@cython.final
@cython.internal
cdef class _Frame:
cdef readonly object f_code
cdef readonly int f_lineno
cdef public _Frame f_back
cdef class Greenlet(greenlet):
cdef readonly object value
cdef readonly args
cdef readonly object spawning_greenlet
cdef public dict spawn_tree_locals
cdef readonly _Frame spawning_stack
# A test case reads these, otherwise they would
# be private
cdef readonly tuple _exc_info
cdef readonly list _links
cdef object _notifier
cdef object _start_event
cdef dict _kwargs
cdef bint __started_but_aborted(self)
cdef bint __start_cancelled_by_kill(self)
cdef bint __start_pending(self)
cdef bint __never_started_or_killed(self)
cdef bint __start_completed(self)
cdef __cancel_start(self)
cdef _report_result(self, object result)
cdef _report_error(self, tuple exc_info)
@cython.final
@cython.internal
cdef class _dummy_event:
cdef readonly bint pending
cdef readonly bint active
cpdef stop(self)
cpdef start(self, cb)
cpdef close(self)
cdef _dummy_event _cancelled_start_event
cdef _dummy_event _start_completed_event
@cython.locals(diehards=list)
cdef _killall3(list greenlets, object exception, object waiter)
cdef _killall(list greenlets, object exception)
@cython.locals(done=list)
cpdef joinall(greenlets, timeout=*, raise_error=*, count=*)
# Copyright (c) 2009-2012 Denis Bilenko. See LICENSE for details.
# cython: auto_pickle=False
from __future__ import absolute_import
from collections import deque
import sys
from weakref import ref as wref
......@@ -12,7 +12,6 @@ from greenlet import getcurrent
from gevent._compat import PY3
from gevent._compat import PYPY
from gevent._compat import reraise
from gevent._util import Lazy
from gevent._tblib import dump_traceback
from gevent._tblib import load_traceback
from gevent.hub import GreenletExit
......@@ -23,8 +22,6 @@ from gevent.hub import iwait
from gevent.hub import wait
from gevent.timeout import Timeout
__all__ = [
'Greenlet',
'joinall',
......@@ -38,7 +35,8 @@ if PYPY:
class SpawnedLink(object):
"""A wrapper around link that calls it in another greenlet.
"""
A wrapper around link that calls it in another greenlet.
Can be called only from main loop.
"""
......@@ -93,7 +91,6 @@ class FailureSpawnedLink(SpawnedLink):
if not source.successful():
return SpawnedLink.__call__(self, source)
class _Frame(object):
__slots__ = ('f_code', 'f_lineno', 'f_back')
......@@ -111,55 +108,11 @@ class Greenlet(greenlet):
"""
# pylint:disable=too-many-public-methods,too-many-instance-attributes
#: A dictionary that is shared between all the greenlets
#: in a "spawn tree", that is, a spawning greenlet and all
#: its descendent greenlets. All children of the main (root)
#: greenlet start their own spawn trees. Assign a new dictionary
#: to this attribute on an instance of this class to create a new
#: spawn tree (as far as locals are concerned).
#:
#: .. versionadded:: 1.3a2
spawn_tree_locals = None
#: A weak-reference to the greenlet that was current when this object
#: was created. Note that the :attr:`parent` attribute is always the
#: hub.
#:
#: .. versionadded:: 1.3a2
spawning_greenlet = None
#: A lightweight frame-like object capturing the stack when
#: this greenlet was created as well as the stack when the spawning
#: greenlet was created (if applicable). This can be passed to
#: :func:`traceback.print_stack`.
#:
#: .. versionadded:: 1.3a2
spawning_stack = None
#: A class attribute specifying how many levels of the spawning
#: stack will be kept. Specify a smaller number for higher performance,
#: specify a larger value for improved debugging.
#:
#: .. versionadded:: 1.3a2
spawning_stack_limit = 10
value = None
_exc_info = ()
_notifier = None
#: An event, such as a timer or a callback that fires. It is established in
#: start() and start_later() as those two objects, respectively.
#: Once this becomes non-None, the Greenlet cannot be started again. Conversely,
#: kill() and throw() check for non-None to determine if this object has ever been
#: scheduled for starting. A placeholder _dummy_event is assigned by them to prevent
#: the greenlet from being started in the future, if necessary.
_start_event = None
args = ()
_kwargs = None
def __init__(self, run=None, *args, **kwargs): # pylint:disable=keyword-arg-before-vararg
"""
Greenlet constructor.
Greenlet(run=None, *args, **kwargs) -> Greenlet
:param args: The arguments passed to the ``run`` function.
:param kwargs: The keyword arguments passed to the ``run`` function.
......@@ -170,6 +123,53 @@ class Greenlet(greenlet):
The ``run`` argument to the constructor is now verified to be a callable
object. Previously, passing a non-callable object would fail after the greenlet
was spawned.
.. attribute:: value
Holds the value returned by the function if the greenlet has
finished successfully. Until then, or if it finished in error, ``None``.
.. tip:: Recall that a greenlet killed with the default
:class:`GreenletExit` is considered to have finished
successfully, and the ``GreenletExit`` exception will be
its value.
.. attribute:: spawn_tree_locals
A dictionary that is shared between all the greenlets
in a "spawn tree", that is, a spawning greenlet and all
its descendent greenlets. All children of the main (root)
greenlet start their own spawn trees. Assign a new dictionary
to this attribute on an instance of this class to create a new
spawn tree (as far as locals are concerned).
.. versionadded:: 1.3a2
.. attribute:: spawning_greenlet
A weak-reference to the greenlet that was current when this object
was created. Note that the :attr:`parent` attribute is always the
hub.
.. versionadded:: 1.3a2
.. attribute:: spawning_stack
A lightweight frame-like object capturing the stack when
this greenlet was created as well as the stack when the spawning
greenlet was created (if applicable). This can be passed to
:func:`traceback.print_stack`.
.. versionadded:: 1.3a2
.. attribute:: spawning_stack_limit
A class attribute specifying how many levels of the spawning
stack will be kept. Specify a smaller number for higher performance,
specify a larger value for improved debugging.
.. versionadded:: 1.3a2
"""
# greenlet.greenlet(run=None, parent=None)
# Calling it with both positional arguments instead of a keyword
......@@ -192,8 +192,14 @@ class Greenlet(greenlet):
# 2.7.14 : Mean +- std dev: 14.8 us +- 0.5 us -> 10.2x
# PyPy2 5.10.0: Mean +- std dev: 3.24 us +- 0.17 us -> 1.5x
# Compiling with Cython gets us to these numbers:
# 3.6.4 : Mean +- std dev: 4.36 us +- 0.23 us
# 2.7.12 : Mean +- std dev: 3.81 us +- 0.15 us
# PyPy2 5.10.0 : Mean +- std dev: 3.63 us +- 0.22 us
greenlet.__init__(self, None, get_hub())
#greenlet.__init__(self, None, get_hub())
super(Greenlet, self).__init__(None, get_hub())
if run is not None:
self._run = run
......@@ -204,10 +210,21 @@ class Greenlet(greenlet):
if not callable(self._run):
raise TypeError("The run argument or self._run must be callable")
if args:
self.args = args
if kwargs:
self._kwargs = kwargs
#: An event, such as a timer or a callback that fires. It is established in
#: start() and start_later() as those two objects, respectively.
#: Once this becomes non-None, the Greenlet cannot be started again. Conversely,
#: kill() and throw() check for non-None to determine if this object has ever been
#: scheduled for starting. A placeholder _dummy_event is assigned by them to prevent
#: the greenlet from being started in the future, if necessary.
self._start_event = None
self.args = args
self._kwargs = kwargs
self.value = None
self._exc_info = ()
self._notifier = None
self._links = []
spawner = getcurrent()
self.spawning_greenlet = wref(spawner)
......@@ -242,13 +259,6 @@ class Greenlet(greenlet):
def kwargs(self):
return self._kwargs or {}
@Lazy
def _links(self):
return deque()
def _has_links(self):
return '_links' in self.__dict__ and self._links
def _raise_exception(self):
reraise(*self.exc_info)
......@@ -257,9 +267,14 @@ class Greenlet(greenlet):
# needed by killall
return self.parent.loop
def __bool__(self):
return self._start_event is not None and self._exc_info is Greenlet._exc_info
__nonzero__ = __bool__
def __nonzero__(self):
return self._start_event is not None and self._exc_info == ()
try:
__bool__ = __nonzero__ # Python 3
except NameError: # pragma: no cover
# When we're compiled with Cython, the __nonzero__ function
# goes directly into the slot and can't be accessed by name.
pass
### Lifecycle
......@@ -269,38 +284,33 @@ class Greenlet(greenlet):
def dead(self):
if self._greenlet__main:
return False
if self.__start_cancelled_by_kill or self.__started_but_aborted:
if self.__start_cancelled_by_kill() or self.__started_but_aborted():
return True
return self._greenlet__started and not _continulet.is_pending(self)
else:
@property
def dead(self):
return self.__start_cancelled_by_kill or self.__started_but_aborted or greenlet.dead.__get__(self)
return self.__start_cancelled_by_kill() or self.__started_but_aborted() or greenlet.dead.__get__(self)
@property
def __never_started_or_killed(self):
return self._start_event is None
@property
def __start_pending(self):
return (self._start_event is not None
and (self._start_event.pending or getattr(self._start_event, 'active', False)))
@property
def __start_cancelled_by_kill(self):
return self._start_event is _cancelled_start_event
@property
def __start_completed(self):
return self._start_event is _start_completed_event
@property
def __started_but_aborted(self):
return (not self.__never_started_or_killed # we have been started or killed
and not self.__start_cancelled_by_kill # we weren't killed, so we must have been started
and not self.__start_completed # the start never completed
and not self.__start_pending) # and we're not pending, so we must have been aborted
return (not self.__never_started_or_killed() # we have been started or killed
and not self.__start_cancelled_by_kill() # we weren't killed, so we must have been started
and not self.__start_completed() # the start never completed
and not self.__start_pending()) # and we're not pending, so we must have been aborted
def __cancel_start(self):
if self._start_event is None:
......@@ -317,7 +327,7 @@ class Greenlet(greenlet):
def __handle_death_before_start(self, *args):
# args is (t, v, tb) or simply t or v
if self._exc_info is Greenlet._exc_info and self.dead:
if self._exc_info == () and self.dead:
# the greenlet was never switched to before and it will never be, _report_error was not called
# the result was not set and the links weren't notified. let's do it here.
# checking that self.dead is true is essential, because throw() does not necessarily kill the greenlet
......@@ -397,8 +407,9 @@ class Greenlet(greenlet):
@property
def exception(self):
"""Holds the exception instance raised by the function if the greenlet has finished with an error.
Otherwise ``None``.
"""
Holds the exception instance raised by the function if the
greenlet has finished with an error. Otherwise ``None``.
"""
return self._exc_info[1] if self._exc_info else None
......@@ -444,7 +455,12 @@ class Greenlet(greenlet):
self._start_event = self.parent.loop.run_callback(self.switch)
def start_later(self, seconds):
"""Schedule the greenlet to run in the future loop iteration *seconds* later"""
"""
start_later(seconds) -> None
Schedule the greenlet to run in the future loop iteration
*seconds* later
"""
if self._start_event is None:
self._start_event = self.parent.loop.timer(seconds)
self._start_event.start(self.switch)
......@@ -539,11 +555,17 @@ class Greenlet(greenlet):
# thus it should not raise when the greenlet is already killed (= not started)
def get(self, block=True, timeout=None):
"""Return the result the greenlet has returned or re-raise the exception it has raised.
"""
get(block=True, timeout=None) -> object
Return the result the greenlet has returned or re-raise the
exception it has raised.
If block is ``False``, raise :class:`gevent.Timeout` if the greenlet is still alive.
If block is ``True``, unschedule the current greenlet until the result is available
or the timeout expires. In the latter case, :class:`gevent.Timeout` is raised.
If block is ``False``, raise :class:`gevent.Timeout` if the
greenlet is still alive. If block is ``True``, unschedule the
current greenlet until the result is available or the timeout
expires. In the latter case, :class:`gevent.Timeout` is
raised.
"""
if self.ready():
if self.successful():
......@@ -577,8 +599,11 @@ class Greenlet(greenlet):
self._raise_exception()
def join(self, timeout=None):
"""Wait until the greenlet finishes or *timeout* expires.
Return ``None`` regardless.
"""
join(timeout=None) -> None
Wait until the greenlet finishes or *timeout* expires. Return
``None`` regardless.
"""
if self.ready():
return
......@@ -604,7 +629,7 @@ class Greenlet(greenlet):
def _report_result(self, result):
self._exc_info = (None, None, None)
self.value = result
if self._has_links() and not self._notifier:
if self._links and not self._notifier:
self._notifier = self.parent.loop.run_callback(self._notify_links)
def _report_error(self, exc_info):
......@@ -614,7 +639,7 @@ class Greenlet(greenlet):
self._exc_info = exc_info[0], exc_info[1], dump_traceback(exc_info[2])
if self._has_links() and not self._notifier:
if self._links and not self._notifier:
self._notifier = self.parent.loop.run_callback(self._notify_links)
try:
......@@ -635,8 +660,8 @@ class Greenlet(greenlet):
self._report_result(result)
finally:
self.__dict__.pop('_run', None)
self.__dict__.pop('args', None)
self.__dict__.pop('kwargs', None)
self.args = None
self._kwargs = None
def _run(self):
"""Subclasses may override this method to take any number of arguments and keyword arguments.
......@@ -692,13 +717,23 @@ class Greenlet(greenlet):
self.link(callback, SpawnedLink=SpawnedLink)
def link_exception(self, callback, SpawnedLink=FailureSpawnedLink):
"""Like :meth:`link` but *callback* is only notified when the greenlet dies because of an unhandled exception."""
"""
Like :meth:`link` but *callback* is only notified when the
greenlet dies because of an unhandled exception.
"""
# pylint:disable=redefined-outer-name
self.link(callback, SpawnedLink=SpawnedLink)
def _notify_links(self):
while self._links:
link = self._links.popleft() # pylint:disable=no-member
# Early links are allowed to remove later links
# before we get to them, and they're also allowed to
# add new links, so we have to be careful about iterating.
# We don't expect this list to be very large, so the time spent
# manipulating it should be small. a deque is probably not justified.
# Cython has optimizations to transform this into a memmove anyway.
link = self._links.pop(0)
try:
link(self)
except: # pylint:disable=bare-except
......@@ -706,8 +741,10 @@ class Greenlet(greenlet):
class _dummy_event(object):
pending = False
active = False
__slots__ = ('pending', 'active')
def __init__(self):
self.pending = self.active = False
def stop(self):
pass
......@@ -715,11 +752,11 @@ class _dummy_event(object):
def start(self, cb): # pylint:disable=unused-argument
raise AssertionError("Cannot start the dummy event")
close = stop
def close(self):
pass
_cancelled_start_event = _dummy_event()
_start_completed_event = _dummy_event()
del _dummy_event
def _kill(glet, exception, waiter):
......
......@@ -374,7 +374,8 @@ class TestStuff(greentest.TestCase):
link(results.listener2)
link(results.listener3)
sleep(DELAY * 10)
assert results.results == [5], results.results
self.assertEqual([5], results.results)
def test_multiple_listeners_error_unlink_Greenlet_link(self):
p = gevent.spawn(lambda: 5)
......@@ -515,14 +516,14 @@ class TestBasic(greentest.TestCase):
assert g.exception is None
gevent.sleep(0.001)
assert g
assert not g.dead
assert g.started
assert not g.ready()
assert not g.successful()
assert g.value is None
assert g.exception is None
assert not link_test
self.assertTrue(g)
self.assertFalse(g.dead, g)
self.assertTrue(g.started, g)
self.assertFalse(g.ready(), g)
self.assertFalse(g.successful(), g)
self.assertIsNone(g.value, g)
self.assertIsNone(g.exception, g)
self.assertFalse(link_test)
gevent.sleep(0.02)
assert not g
......
......@@ -11,7 +11,7 @@ import greentest
import gevent
from gevent import util
@greentest.skipOnPyPy("5.10.x is *very* slow formatting stacks")
class TestFormat(greentest.TestCase):
def test_basic(self):
......
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