Skip to content

Commit b796c7f

Browse files
committed
Optimize asyncio internal call_soon scheduling
1 parent 09b44fb commit b796c7f

3 files changed

Lines changed: 138 additions & 17 deletions

File tree

Lib/test/test_asyncio/test_futures.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -823,6 +823,39 @@ def bag_appender(future):
823823
def _new_future(self):
824824
raise NotImplementedError
825825

826+
def test_set_result_uses_call_soon_override(self):
827+
bag = []
828+
base_loop_cls = type(self.loop)
829+
830+
class CountingLoop(base_loop_cls):
831+
def __init__(self):
832+
super().__init__()
833+
self.calls = []
834+
835+
def call_soon(self, callback, *args, context=None):
836+
self.calls.append((callback, args, context))
837+
return super().call_soon(callback, *args, context=context)
838+
839+
old_loop = self.loop
840+
loop = CountingLoop()
841+
self.loop = loop
842+
self.addCleanup(setattr, self, 'loop', old_loop)
843+
self.addCleanup(loop.close)
844+
845+
f = self._new_future()
846+
cb = self._make_callback(bag, 42)
847+
f.add_done_callback(cb)
848+
f.set_result('foo')
849+
850+
self.assertEqual(len(loop.calls), 1)
851+
callback, args, context = loop.calls[0]
852+
self.assertIs(callback, cb)
853+
self.assertEqual(args, (f,))
854+
self.assertIsNotNone(context)
855+
856+
self.run_briefly()
857+
self.assertEqual(bag, [42])
858+
826859
def test_callbacks_remove_first_callback(self):
827860
bag = []
828861
f = self._new_future()

Lib/test/test_asyncio/test_tasks.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,35 @@ async def notmuch():
189189
loop.run_until_complete(t)
190190
loop.close()
191191

192+
def test_task_constructor_uses_call_soon_override(self):
193+
base_loop_cls = type(self.loop)
194+
195+
class CountingLoop(base_loop_cls):
196+
def __init__(self):
197+
super().__init__()
198+
self.calls = []
199+
200+
def call_soon(self, callback, *args, context=None):
201+
self.calls.append((callback, args, context))
202+
return super().call_soon(callback, *args, context=context)
203+
204+
async def coro():
205+
return 42
206+
207+
loop = CountingLoop()
208+
self.addCleanup(loop.close)
209+
self.set_event_loop(loop)
210+
self.addCleanup(asyncio.set_event_loop, None)
211+
212+
task = self.new_task(loop, coro())
213+
self.assertEqual(len(loop.calls), 1)
214+
callback, args, context = loop.calls[0]
215+
self.assertIs(getattr(callback, '__self__', None), task)
216+
self.assertEqual(args, ())
217+
self.assertIs(task.get_context(), context)
218+
219+
loop.run_until_complete(task)
220+
192221
def test_ensure_future_coroutine(self):
193222
async def notmuch():
194223
return 'ok'

Modules/_asynciomodule.c

Lines changed: 76 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
@@ -370,27 +371,78 @@ call_soon(asyncio_state *state, PyObject *loop, PyObject *func, PyObject *arg,
370371
PyObject *ctx)
371372
{
372373
state->bench_call_soon_calls++;
373-
PyObject *handle;
374+
PyObject *handle = NULL;
374375

375-
if (ctx == NULL) {
376-
PyObject *stack[] = {loop, func, arg};
377-
size_t nargsf = 3 | PY_VECTORCALL_ARGUMENTS_OFFSET;
378-
handle = PyObject_VectorcallMethod(&_Py_ID(call_soon), stack, nargsf, NULL);
376+
PyTypeObject *loop_type = Py_TYPE(loop);
377+
PyObject *module_name = PyDict_GetItemWithError(loop_type->tp_dict, &_Py_ID(__module__));
378+
if (module_name == NULL && PyErr_Occurred()) {
379+
return -1;
379380
}
380-
else {
381-
/* All refs in 'stack' are borrowed. */
382-
PyObject *stack[4];
383-
size_t nargs = 2;
384-
stack[0] = loop;
385-
stack[1] = func;
381+
382+
int use_fastpath = (
383+
module_name != NULL &&
384+
PyUnicode_Check(module_name) &&
385+
(
386+
(strcmp(loop_type->tp_name, "BaseEventLoop") == 0 &&
387+
PyUnicode_CompareWithASCIIString(module_name, "asyncio.base_events") == 0) ||
388+
(strcmp(loop_type->tp_name, "BaseSelectorEventLoop") == 0 &&
389+
PyUnicode_CompareWithASCIIString(module_name, "asyncio.selector_events") == 0) ||
390+
(strcmp(loop_type->tp_name, "_UnixSelectorEventLoop") == 0 &&
391+
PyUnicode_CompareWithASCIIString(module_name, "asyncio.unix_events") == 0) ||
392+
(strcmp(loop_type->tp_name, "_WindowsSelectorEventLoop") == 0 &&
393+
PyUnicode_CompareWithASCIIString(module_name, "asyncio.windows_events") == 0) ||
394+
(strcmp(loop_type->tp_name, "BaseProactorEventLoop") == 0 &&
395+
PyUnicode_CompareWithASCIIString(module_name, "asyncio.proactor_events") == 0) ||
396+
(strcmp(loop_type->tp_name, "ProactorEventLoop") == 0 &&
397+
PyUnicode_CompareWithASCIIString(module_name, "asyncio.windows_events") == 0)
398+
)
399+
);
400+
401+
if (use_fastpath) {
402+
PyObject *args;
403+
if (arg == NULL) {
404+
args = Py_GetConstantBorrowed(Py_CONSTANT_EMPTY_TUPLE);
405+
}
406+
else {
407+
args = PyTuple_Pack(1, arg);
408+
if (args == NULL) {
409+
return -1;
410+
}
411+
}
412+
413+
PyObject *stack[] = {loop, func, args, ctx ? ctx : Py_None};
414+
size_t nargsf = 4 | PY_VECTORCALL_ARGUMENTS_OFFSET;
415+
handle = PyObject_VectorcallMethod(state->call_soon_private_name, stack,
416+
nargsf, NULL);
386417
if (arg != NULL) {
387-
stack[2] = arg;
388-
nargs++;
418+
Py_DECREF(args);
419+
}
420+
421+
if (handle == NULL && PyErr_ExceptionMatches(PyExc_AttributeError)) {
422+
PyErr_Clear();
423+
}
424+
}
425+
426+
if (handle == NULL) {
427+
if (ctx == NULL) {
428+
PyObject *stack[] = {loop, func, arg};
429+
size_t nargsf = 3 | PY_VECTORCALL_ARGUMENTS_OFFSET;
430+
handle = PyObject_VectorcallMethod(&_Py_ID(call_soon), stack, nargsf, NULL);
431+
}
432+
else {
433+
PyObject *stack[4];
434+
size_t nargs = 2;
435+
stack[0] = loop;
436+
stack[1] = func;
437+
if (arg != NULL) {
438+
stack[2] = arg;
439+
nargs++;
440+
}
441+
stack[nargs] = (PyObject *)ctx;
442+
size_t nargsf = nargs | PY_VECTORCALL_ARGUMENTS_OFFSET;
443+
handle = PyObject_VectorcallMethod(&_Py_ID(call_soon), stack, nargsf,
444+
state->context_kwname);
389445
}
390-
stack[nargs] = (PyObject *)ctx;
391-
size_t nargsf = nargs | PY_VECTORCALL_ARGUMENTS_OFFSET;
392-
handle = PyObject_VectorcallMethod(&_Py_ID(call_soon), stack, nargsf,
393-
state->context_kwname);
394446
}
395447

396448
if (handle == NULL) {
@@ -4204,6 +4256,7 @@ module_traverse(PyObject *mod, visitproc visit, void *arg)
42044256
Py_VISIT(state->TaskType);
42054257

42064258
Py_VISIT(state->asyncio_mod);
4259+
Py_VISIT(state->call_soon_private_name);
42074260
Py_VISIT(state->traceback_extract_stack);
42084261
Py_VISIT(state->asyncio_future_repr_func);
42094262
Py_VISIT(state->asyncio_get_event_loop);
@@ -4234,6 +4287,7 @@ module_clear(PyObject *mod)
42344287
Py_CLEAR(state->TaskType);
42354288

42364289
Py_CLEAR(state->asyncio_mod);
4290+
Py_CLEAR(state->call_soon_private_name);
42374291
Py_CLEAR(state->traceback_extract_stack);
42384292
Py_CLEAR(state->asyncio_future_repr_func);
42394293
Py_CLEAR(state->asyncio_get_event_loop);
@@ -4285,6 +4339,11 @@ module_init(asyncio_state *state)
42854339
goto fail;
42864340
}
42874341

4342+
state->call_soon_private_name = PyUnicode_InternFromString("_call_soon");
4343+
if (state->call_soon_private_name == NULL) {
4344+
goto fail;
4345+
}
4346+
42884347
#define WITH_MOD(NAME) \
42894348
Py_CLEAR(module); \
42904349
module = PyImport_ImportModule(NAME); \

0 commit comments

Comments
 (0)