Commit 9b4a7363 authored by Jason Madden's avatar Jason Madden Committed by GitHub

Merge pull request #1379 from gevent/issue1363

Performance and correctness improvements for Greenlets.
parents 45d00068 422a7126
...@@ -51,6 +51,19 @@ ...@@ -51,6 +51,19 @@
- Win: Make ``examples/process.py`` do something useful. See - Win: Make ``examples/process.py`` do something useful. See
:pr:`1378` by Robert Iannucci. :pr:`1378` by Robert Iannucci.
- Fix certain operations on a Greenlet in an invalid state (with an
invalid parent) to raise a `TypeError` sooner rather than an
`AttributeError` later. This is also slightly faster on CPython with
Cython. Inspired by :issue:`1363` as reported by Carson Ip. This
means that some extreme corner cases that might have passed by
replacing a Greenlet's parent with something that's not a gevent hub
now no longer will.
- Fix: The ``spawning_stack`` for Greenlets on CPython should now have
correct line numbers in more cases.
- Spawning greenlets can be up to 10% faster.
1.4.0 (2019-01-04) 1.4.0 (2019-01-04)
================== ==================
......
...@@ -39,7 +39,7 @@ cdef inline void greenlet_init(): ...@@ -39,7 +39,7 @@ cdef inline void greenlet_init():
cdef class WaitOperationsGreenlet(SwitchOutGreenletWithLoop): cdef class WaitOperationsGreenlet(SwitchOutGreenletWithLoop):
# The Hub will extend this class.
cpdef wait(self, watcher) cpdef wait(self, watcher)
cpdef cancel_wait(self, watcher, error, close_watcher=*) cpdef cancel_wait(self, watcher, error, close_watcher=*)
cpdef _cancel_wait(self, watcher, error, close_watcher) cpdef _cancel_wait(self, watcher, error, close_watcher)
......
...@@ -21,7 +21,7 @@ _version_info = namedtuple('version_info', ...@@ -21,7 +21,7 @@ _version_info = namedtuple('version_info',
#: .. deprecated:: 1.2 #: .. deprecated:: 1.2
#: Use ``pkg_resources.parse_version(__version__)`` (or the equivalent #: Use ``pkg_resources.parse_version(__version__)`` (or the equivalent
#: ``packaging.version.Version(__version__)``). #: ``packaging.version.Version(__version__)``).
version_info = _version_info(1, 4, 0, 'dev', 0) version_info = _version_info(1, 5, 0, 'dev', 0)
#: The human-readable PEP 440 version identifier. #: The human-readable PEP 440 version identifier.
#: Use ``pkg_resources.parse_version(__version__)`` or #: Use ``pkg_resources.parse_version(__version__)`` or
......
...@@ -4,6 +4,7 @@ cimport cython ...@@ -4,6 +4,7 @@ cimport cython
from gevent.__ident cimport IdentRegistry from gevent.__ident cimport IdentRegistry
from gevent.__hub_local cimport get_hub_noargs as get_hub from gevent.__hub_local cimport get_hub_noargs as get_hub
from gevent.__waiter cimport Waiter from gevent.__waiter cimport Waiter
from gevent.__greenlet_primitives cimport SwitchOutGreenletWithLoop
cdef bint _PYPY cdef bint _PYPY
cdef sys_getframe cdef sys_getframe
...@@ -15,7 +16,12 @@ cdef InvalidSwitchError ...@@ -15,7 +16,12 @@ cdef InvalidSwitchError
cdef extern from "greenlet/greenlet.h": cdef extern from "greenlet/greenlet.h":
ctypedef class greenlet.greenlet [object PyGreenlet]: ctypedef class greenlet.greenlet [object PyGreenlet]:
pass # Defining this as a void* means we can't access it as a python attribute
# in the Python code; but we can't define it as a greenlet because that doesn't
# properly handle the case that it can be NULL. So instead we inline a getparent
# function that does the same thing as the green_getparent accessor but without
# going through the overhead of generic attribute lookup.
cdef void* parent
# These are actually macros and so much be included # These are actually macros and so much be included
# (defined) in each .pxd, as are the two functions # (defined) in each .pxd, as are the two functions
...@@ -27,6 +33,17 @@ cdef extern from "greenlet/greenlet.h": ...@@ -27,6 +33,17 @@ cdef extern from "greenlet/greenlet.h":
cdef inline greenlet getcurrent(): cdef inline greenlet getcurrent():
return PyGreenlet_GetCurrent() return PyGreenlet_GetCurrent()
cdef inline object get_generic_parent(greenlet s):
# We don't use any typed functions on the return of this,
# so save the type check by making it just an object.
if s.parent != NULL:
return <object>s.parent
cdef inline SwitchOutGreenletWithLoop get_my_hub(greenlet s):
# Must not be called with s = None
if s.parent != NULL:
return <object>s.parent
cdef bint _greenlet_imported cdef bint _greenlet_imported
cdef inline void greenlet_init(): cdef inline void greenlet_init():
...@@ -44,12 +61,25 @@ cdef extern from "frameobject.h": ...@@ -44,12 +61,25 @@ cdef extern from "frameobject.h":
ctypedef class types.FrameType [object PyFrameObject]: ctypedef class types.FrameType [object PyFrameObject]:
cdef CodeType f_code cdef CodeType f_code
cdef int f_lineno # Accessing the f_lineno directly doesn't work. There is an accessor
# We can't declare this in the object, because it's # function, PyFrame_GetLineNumber that is needed to turn the raw line number
# into the executing line number.
# cdef int f_lineno
# We can't declare this in the object as an object, because it's
# allowed to be NULL, and Cython can't handle that. # allowed to be NULL, and Cython can't handle that.
# We have to go through the python machinery to get a # We have to go through the python machinery to get a
# proper None instead. # proper None instead, or use an inline function.
# cdef FrameType f_back cdef void* f_back
int PyFrame_GetLineNumber(FrameType frame)
@cython.nonecheck(False)
cdef inline FrameType get_f_back(FrameType frame):
if frame.f_back != NULL:
return <FrameType>frame.f_back
cdef inline int get_f_lineno(FrameType frame):
return PyFrame_GetLineNumber(frame)
cdef void _init() cdef void _init()
...@@ -75,25 +105,20 @@ cdef class _Frame: ...@@ -75,25 +105,20 @@ cdef class _Frame:
@cython.final @cython.final
@cython.locals(frames=list,frame=FrameType) @cython.locals(frame=FrameType,
cdef inline list _extract_stack(int limit) newest_Frame=_Frame,
newer_Frame=_Frame,
@cython.final older_Frame=_Frame)
@cython.locals(previous=_Frame, frame=tuple, f=_Frame) cdef inline _Frame _extract_stack(int limit)
cdef _Frame _Frame_from_list(list frames)
cdef class Greenlet(greenlet): cdef class Greenlet(greenlet):
cdef readonly object value cdef readonly object value
cdef readonly tuple args cdef readonly tuple args
cdef readonly dict kwargs cdef readonly dict kwargs
cdef readonly object spawning_greenlet cdef readonly object spawning_greenlet
cdef readonly _Frame spawning_stack
cdef public dict spawn_tree_locals cdef public dict spawn_tree_locals
# This is accessed with getattr() dynamically so it
# must be visible to Python
cdef readonly list _spawning_stack_frames
cdef list _links cdef list _links
cdef tuple _exc_info cdef tuple _exc_info
cdef object _notifier cdef object _notifier
...@@ -108,10 +133,11 @@ cdef class Greenlet(greenlet): ...@@ -108,10 +133,11 @@ cdef class Greenlet(greenlet):
cpdef rawlink(self, object callback) cpdef rawlink(self, object callback)
cpdef str _formatinfo(self) cpdef str _formatinfo(self)
# This is a helper function for a @property getter;
# defining locals() for a @property doesn't seem to work.
@cython.locals(reg=IdentRegistry) @cython.locals(reg=IdentRegistry)
cdef _get_minimal_ident(self) cdef _get_minimal_ident(self)
cdef bint __started_but_aborted(self) cdef bint __started_but_aborted(self)
cdef bint __start_cancelled_by_kill(self) cdef bint __start_cancelled_by_kill(self)
cdef bint __start_pending(self) cdef bint __start_pending(self)
...@@ -132,8 +158,6 @@ cdef class Greenlet(greenlet): ...@@ -132,8 +158,6 @@ cdef class Greenlet(greenlet):
# TypeError: wrap() takes exactly one argument (0 given) # TypeError: wrap() takes exactly one argument (0 given)
# cpdef _raise_exception(self) # cpdef _raise_exception(self)
# Declare a bunch of imports as cdefs so they can # Declare a bunch of imports as cdefs so they can
# be accessed directly as static vars without # be accessed directly as static vars without
# doing a module global lookup. This is especially important # doing a module global lookup. This is especially important
...@@ -165,6 +189,7 @@ cdef class _dummy_event: ...@@ -165,6 +189,7 @@ cdef class _dummy_event:
cdef _dummy_event _cancelled_start_event cdef _dummy_event _cancelled_start_event
cdef _dummy_event _start_completed_event cdef _dummy_event _start_completed_event
cpdef _kill(Greenlet glet, object exception, object waiter)
@cython.locals(diehards=list) @cython.locals(diehards=list)
cdef _killall3(list greenlets, object exception, object waiter) cdef _killall3(list greenlets, object exception, object waiter)
......
...@@ -54,9 +54,15 @@ class WaitOperationsGreenlet(SwitchOutGreenletWithLoop): # pylint:disable=undefi ...@@ -54,9 +54,15 @@ class WaitOperationsGreenlet(SwitchOutGreenletWithLoop): # pylint:disable=undefi
try: try:
result = waiter.get() result = waiter.get()
if result is not waiter: if result is not waiter:
raise InvalidSwitchError('Invalid switch into %s: %r (expected %r)' % ( raise InvalidSwitchError(
'Invalid switch into %s: got %r (expected %r; waiting on %r with %r)' % (
getcurrent(), # pylint:disable=undefined-variable getcurrent(), # pylint:disable=undefined-variable
result, waiter)) result,
waiter,
self,
watcher
)
)
finally: finally:
watcher.stop() watcher.stop()
......
...@@ -27,7 +27,7 @@ class ValuedWeakRef(ref): ...@@ -27,7 +27,7 @@ class ValuedWeakRef(ref):
class IdentRegistry(object): class IdentRegistry(object):
""" """
Maintains a unique mapping of (small) positive integer identifiers Maintains a unique mapping of (small) non-negative integer identifiers
to objects that can be weakly referenced. to objects that can be weakly referenced.
It is guaranteed that no two objects will have the the same It is guaranteed that no two objects will have the the same
......
This diff is collapsed.
...@@ -531,22 +531,22 @@ class TestBasic(greentest.TestCase): ...@@ -531,22 +531,22 @@ class TestBasic(greentest.TestCase):
g = gevent.Greenlet(func, 0.01, return_value=5) g = gevent.Greenlet(func, 0.01, return_value=5)
g.rawlink(link_test.append) # use rawlink to avoid timing issues on Appveyor/Travis (not always successful) g.rawlink(link_test.append) # use rawlink to avoid timing issues on Appveyor/Travis (not always successful)
assert not g, bool(g) self.assertFalse(g, g)
assert not g.dead self.assertFalse(g.dead, g)
assert not g.started self.assertFalse(g.started, g)
assert not g.ready() self.assertFalse(g.ready(), g)
assert not g.successful() self.assertFalse(g.successful(), g)
assert g.value is None self.assertIsNone(g.value, g)
assert g.exception is None self.assertIsNone(g.exception, g)
g.start() g.start()
assert g # changed self.assertTrue(g, g) # changed
assert not g.dead self.assertFalse(g.dead, g)
assert g.started # changed self.assertTrue(g.started, g) # changed
assert not g.ready() self.assertFalse(g.ready(), g)
assert not g.successful() self.assertFalse(g.successful(), g)
assert g.value is None self.assertIsNone(g.value, g)
assert g.exception is None self.assertIsNone(g.exception, g)
gevent.sleep(0.001) gevent.sleep(0.001)
self.assertTrue(g) self.assertTrue(g)
...@@ -559,14 +559,15 @@ class TestBasic(greentest.TestCase): ...@@ -559,14 +559,15 @@ class TestBasic(greentest.TestCase):
self.assertFalse(link_test) self.assertFalse(link_test)
gevent.sleep(0.02) gevent.sleep(0.02)
assert not g self.assertFalse(g, g) # changed
assert g.dead self.assertTrue(g.dead, g) # changed
assert not g.started self.assertFalse(g.started, g) # changed
assert g.ready() self.assertTrue(g.ready(), g) # changed
assert g.successful() self.assertTrue(g.successful(), g) # changed
assert g.value == 5 self.assertEqual(g.value, 5) # changed
assert g.exception is None # not changed self.assertIsNone(g.exception, g)
assert link_test == [g] or greentest.RUNNING_ON_CI, link_test # changed
self.assertTrue(link_test == [g] or greentest.RUNNING_ON_CI, link_test) # changed
def test_error_exit(self): def test_error_exit(self):
link_test = [] link_test = []
...@@ -755,28 +756,24 @@ class TestBasic(greentest.TestCase): ...@@ -755,28 +756,24 @@ class TestBasic(greentest.TestCase):
finally: finally:
greenlet.sys_getframe = ogf greenlet.sys_getframe = ogf
def test_spawn_length_nested_doesnt_grow(self): def test_minimal_ident_parent_not_hub(self):
# https://github.com/gevent/gevent/pull/1374
def doit1():
getcurrent()._spawning_stack_frames.append('not copied')
return gevent.spawn(doit2)
def doit2():
return gevent.spawn(doit3)
def doit3(): g = gevent.spawn(lambda: 1)
current = gevent.getcurrent() self.assertIs(g.parent, gevent.get_hub())
g.parent = getcurrent()
return current try:
self.assertIsNot(g.parent, gevent.get_hub())
g1 = gevent.spawn(doit1)
g1.join()
g2 = g1.value
g2.join()
g3 = g2.value with self.assertRaisesRegex((TypeError, # Cython
self.assertNotIn('not copied', g3._spawning_stack_frames) AttributeError), # PyPy
'Cannot convert|ident_registry'):
getattr(g, 'minimal_ident')
finally:
# Attempting to switch into this later, when we next cycle the
# loop, would raise an InvalidSwitchError if we don't put
# things back the way they were (or kill the greenlet)
g.parent = gevent.get_hub()
g.kill()
......
...@@ -417,9 +417,11 @@ class GreenletTree(object): ...@@ -417,9 +417,11 @@ class GreenletTree(object):
def __render_children(self, tree): def __render_children(self, tree):
children = sorted(self.child_trees, children = sorted(self.child_trees,
key=lambda c: ( key=lambda c: (
# raw greenlets first # raw greenlets first. Note that we could be accessing
# minimal_ident for a hub from a different thread, which isn't
# technically thread safe.
getattr(c, 'minimal_ident', -1), getattr(c, 'minimal_ident', -1),
# running greenlets first # running greenlets next
getattr(c, 'ready', _ready)(), getattr(c, 'ready', _ready)(),
id(c.parent))) id(c.parent)))
for n, child in enumerate(children): for n, child in enumerate(children):
......
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