Skip to content

Commit b2c13fc

Browse files
committed
gh-153534: Broaden _asyncio _call_soon fast path
1 parent c22e9c9 commit b2c13fc

2 files changed

Lines changed: 147 additions & 17 deletions

File tree

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Broaden ``_asyncio``'s private ``_call_soon()`` fast path to cover safe
2+
subclasses of ``asyncio.base_events.BaseEventLoop``, improving hot task
3+
and future scheduling paths without changing customized ``call_soon()``
4+
behavior.

Modules/_asynciomodule.c

Lines changed: 143 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,7 @@ typedef struct {
149149
PyTypeObject *TaskType;
150150

151151
PyObject *asyncio_mod;
152+
PyObject *call_soon_private_name;
152153
PyObject *context_kwname;
153154

154155
/* WeakSet containing scheduled 3rd party tasks which don't
@@ -355,31 +356,149 @@ get_event_loop(asyncio_state *state)
355356
}
356357

357358

359+
static int
360+
is_asyncio_base_event_loop_type(PyTypeObject *loop_type)
361+
{
362+
if (strcmp(loop_type->tp_name, "BaseEventLoop") != 0) {
363+
return 0;
364+
}
365+
366+
PyObject *module_name =
367+
PyDict_GetItemWithError(loop_type->tp_dict, &_Py_ID(__module__));
368+
if (module_name == NULL) {
369+
if (PyErr_Occurred()) {
370+
return -1;
371+
}
372+
return 0;
373+
}
374+
375+
return (
376+
PyUnicode_Check(module_name) &&
377+
PyUnicode_CompareWithASCIIString(module_name, "asyncio.base_events") == 0
378+
);
379+
}
380+
381+
static int
382+
is_asyncio_base_event_loop_subclass(PyTypeObject *loop_type)
383+
{
384+
// The _asyncio module is imported from asyncio.events while
385+
// asyncio.base_events is still initializing, so we can't cache the
386+
// BaseEventLoop type during module init without creating an import cycle.
387+
for (PyTypeObject *base = loop_type; base != NULL; base = base->tp_base) {
388+
int is_base_event_loop = is_asyncio_base_event_loop_type(base);
389+
if (is_base_event_loop < 0) {
390+
return -1;
391+
}
392+
if (is_base_event_loop) {
393+
return 1;
394+
}
395+
}
396+
397+
return 0;
398+
}
399+
400+
static int
401+
loop_has_instance_call_soon_shadow(PyObject *loop)
402+
{
403+
// Respect instance-level monkeypatching such as:
404+
// loop.call_soon = custom_call_soon
405+
// These must keep using the public slow path.
406+
PyObject *attr = NULL;
407+
PyObject *dict = PyObject_GenericGetDict(loop, NULL);
408+
if (dict == NULL) {
409+
if (PyErr_ExceptionMatches(PyExc_AttributeError)) {
410+
PyErr_Clear();
411+
return 0;
412+
}
413+
return -1;
414+
}
415+
416+
int rc = PyDict_GetItemRef(dict, &_Py_ID(call_soon), &attr);
417+
Py_DECREF(dict);
418+
if (rc < 0) {
419+
return -1;
420+
}
421+
Py_XDECREF(attr);
422+
return rc > 0;
423+
}
424+
358425
static int
359426
call_soon(asyncio_state *state, PyObject *loop, PyObject *func, PyObject *arg,
360427
PyObject *ctx)
361428
{
362-
PyObject *handle;
429+
PyObject *handle = NULL;
363430

364-
if (ctx == NULL) {
365-
PyObject *stack[] = {loop, func, arg};
366-
size_t nargsf = 3 | PY_VECTORCALL_ARGUMENTS_OFFSET;
367-
handle = PyObject_VectorcallMethod(&_Py_ID(call_soon), stack, nargsf, NULL);
431+
PyTypeObject *loop_type = Py_TYPE(loop);
432+
int is_base_event_loop_subclass =
433+
is_asyncio_base_event_loop_subclass(loop_type);
434+
int is_stdlib_base_event_loop =
435+
is_asyncio_base_event_loop_type(loop_type);
436+
if (is_base_event_loop_subclass < 0 || is_stdlib_base_event_loop < 0) {
437+
return -1;
368438
}
369-
else {
370-
/* All refs in 'stack' are borrowed. */
371-
PyObject *stack[4];
372-
size_t nargs = 2;
373-
stack[0] = loop;
374-
stack[1] = func;
439+
PyObject *call_soon_attr =
440+
PyDict_GetItemWithError(loop_type->tp_dict, &_Py_ID(call_soon));
441+
if (call_soon_attr == NULL && PyErr_Occurred()) {
442+
return -1;
443+
}
444+
int has_instance_call_soon_shadow = loop_has_instance_call_soon_shadow(loop);
445+
if (has_instance_call_soon_shadow < 0) {
446+
return -1;
447+
}
448+
449+
int use_fastpath = (
450+
is_base_event_loop_subclass &&
451+
// Only use the private fast path when the subclass keeps the public
452+
// call_soon() contract unchanged at both the type and instance level.
453+
!has_instance_call_soon_shadow &&
454+
(is_stdlib_base_event_loop || call_soon_attr == NULL)
455+
);
456+
457+
if (use_fastpath) {
458+
PyObject *args;
459+
if (arg == NULL) {
460+
args = Py_GetConstantBorrowed(Py_CONSTANT_EMPTY_TUPLE);
461+
}
462+
else {
463+
args = PyTuple_Pack(1, arg);
464+
if (args == NULL) {
465+
return -1;
466+
}
467+
}
468+
469+
PyObject *stack[] = {loop, func, args, ctx ? ctx : Py_None};
470+
size_t nargsf = 4 | PY_VECTORCALL_ARGUMENTS_OFFSET;
471+
handle = PyObject_VectorcallMethod(state->call_soon_private_name, stack,
472+
nargsf, NULL);
375473
if (arg != NULL) {
376-
stack[2] = arg;
377-
nargs++;
474+
Py_DECREF(args);
475+
}
476+
477+
if (handle == NULL && PyErr_ExceptionMatches(PyExc_AttributeError)) {
478+
PyErr_Clear();
479+
}
480+
}
481+
482+
if (handle == NULL) {
483+
if (ctx == NULL) {
484+
PyObject *stack[] = {loop, func, arg};
485+
size_t nargsf = 3 | PY_VECTORCALL_ARGUMENTS_OFFSET;
486+
handle = PyObject_VectorcallMethod(&_Py_ID(call_soon), stack, nargsf, NULL);
487+
}
488+
else {
489+
PyObject *stack[4];
490+
size_t nargs = 2;
491+
stack[0] = loop;
492+
stack[1] = func;
493+
if (arg != NULL) {
494+
stack[2] = arg;
495+
nargs++;
496+
}
497+
stack[nargs] = (PyObject *)ctx;
498+
size_t nargsf = nargs | PY_VECTORCALL_ARGUMENTS_OFFSET;
499+
handle = PyObject_VectorcallMethod(&_Py_ID(call_soon), stack, nargsf,
500+
state->context_kwname);
378501
}
379-
stack[nargs] = (PyObject *)ctx;
380-
size_t nargsf = nargs | PY_VECTORCALL_ARGUMENTS_OFFSET;
381-
handle = PyObject_VectorcallMethod(&_Py_ID(call_soon), stack, nargsf,
382-
state->context_kwname);
383502
}
384503

385504
if (handle == NULL) {
@@ -4183,6 +4302,7 @@ module_traverse(PyObject *mod, visitproc visit, void *arg)
41834302
Py_VISIT(state->TaskType);
41844303

41854304
Py_VISIT(state->asyncio_mod);
4305+
Py_VISIT(state->call_soon_private_name);
41864306
Py_VISIT(state->traceback_extract_stack);
41874307
Py_VISIT(state->asyncio_future_repr_func);
41884308
Py_VISIT(state->asyncio_get_event_loop);
@@ -4213,6 +4333,7 @@ module_clear(PyObject *mod)
42134333
Py_CLEAR(state->TaskType);
42144334

42154335
Py_CLEAR(state->asyncio_mod);
4336+
Py_CLEAR(state->call_soon_private_name);
42164337
Py_CLEAR(state->traceback_extract_stack);
42174338
Py_CLEAR(state->asyncio_future_repr_func);
42184339
Py_CLEAR(state->asyncio_get_event_loop);
@@ -4264,6 +4385,11 @@ module_init(asyncio_state *state)
42644385
goto fail;
42654386
}
42664387

4388+
state->call_soon_private_name = PyUnicode_InternFromString("_call_soon");
4389+
if (state->call_soon_private_name == NULL) {
4390+
goto fail;
4391+
}
4392+
42674393
#define WITH_MOD(NAME) \
42684394
Py_CLEAR(module); \
42694395
module = PyImport_ImportModule(NAME); \

0 commit comments

Comments
 (0)