Skip to content

Commit 1b7c11f

Browse files
authored
bpo-32348: Optimize asyncio.Future schedule/add/remove callback. (#4907)
1 parent 4c72bc4 commit 1b7c11f

File tree

4 files changed

+499
-102
lines changed

4 files changed

+499
-102
lines changed

Lib/test/test_asyncio/test_futures.py

Lines changed: 138 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -145,37 +145,60 @@ def test_constructor_positional(self):
145145
self.assertRaises(TypeError, self._new_future, 42)
146146

147147
def test_uninitialized(self):
148+
# Test that C Future doesn't crash when Future.__init__()
149+
# call was skipped.
150+
148151
fut = self.cls.__new__(self.cls, loop=self.loop)
149152
self.assertRaises(asyncio.InvalidStateError, fut.result)
153+
150154
fut = self.cls.__new__(self.cls, loop=self.loop)
151155
self.assertRaises(asyncio.InvalidStateError, fut.exception)
156+
152157
fut = self.cls.__new__(self.cls, loop=self.loop)
153158
with self.assertRaises((RuntimeError, AttributeError)):
154159
fut.set_result(None)
160+
155161
fut = self.cls.__new__(self.cls, loop=self.loop)
156162
with self.assertRaises((RuntimeError, AttributeError)):
157163
fut.set_exception(Exception)
164+
158165
fut = self.cls.__new__(self.cls, loop=self.loop)
159166
with self.assertRaises((RuntimeError, AttributeError)):
160167
fut.cancel()
168+
161169
fut = self.cls.__new__(self.cls, loop=self.loop)
162170
with self.assertRaises((RuntimeError, AttributeError)):
163171
fut.add_done_callback(lambda f: None)
172+
164173
fut = self.cls.__new__(self.cls, loop=self.loop)
165174
with self.assertRaises((RuntimeError, AttributeError)):
166175
fut.remove_done_callback(lambda f: None)
176+
167177
fut = self.cls.__new__(self.cls, loop=self.loop)
168178
with self.assertRaises((RuntimeError, AttributeError)):
169179
fut._schedule_callbacks()
180+
170181
fut = self.cls.__new__(self.cls, loop=self.loop)
171182
try:
172183
repr(fut)
173-
except AttributeError:
184+
except (RuntimeError, AttributeError):
185+
pass
186+
187+
fut = self.cls.__new__(self.cls, loop=self.loop)
188+
try:
189+
fut.__await__()
190+
except RuntimeError:
191+
pass
192+
193+
fut = self.cls.__new__(self.cls, loop=self.loop)
194+
try:
195+
iter(fut)
196+
except RuntimeError:
174197
pass
198+
175199
fut = self.cls.__new__(self.cls, loop=self.loop)
176-
fut.cancelled()
177-
fut.done()
178-
iter(fut)
200+
self.assertFalse(fut.cancelled())
201+
self.assertFalse(fut.done())
179202

180203
def test_cancel(self):
181204
f = self._new_future(loop=self.loop)
@@ -246,30 +269,32 @@ def test_future_repr(self):
246269
self.loop.set_debug(True)
247270
f_pending_debug = self._new_future(loop=self.loop)
248271
frame = f_pending_debug._source_traceback[-1]
249-
self.assertEqual(repr(f_pending_debug),
250-
'<Future pending created at %s:%s>'
251-
% (frame[0], frame[1]))
272+
self.assertEqual(
273+
repr(f_pending_debug),
274+
f'<{self.cls.__name__} pending created at {frame[0]}:{frame[1]}>')
252275
f_pending_debug.cancel()
253276

254277
self.loop.set_debug(False)
255278
f_pending = self._new_future(loop=self.loop)
256-
self.assertEqual(repr(f_pending), '<Future pending>')
279+
self.assertEqual(repr(f_pending), f'<{self.cls.__name__} pending>')
257280
f_pending.cancel()
258281

259282
f_cancelled = self._new_future(loop=self.loop)
260283
f_cancelled.cancel()
261-
self.assertEqual(repr(f_cancelled), '<Future cancelled>')
284+
self.assertEqual(repr(f_cancelled), f'<{self.cls.__name__} cancelled>')
262285

263286
f_result = self._new_future(loop=self.loop)
264287
f_result.set_result(4)
265-
self.assertEqual(repr(f_result), '<Future finished result=4>')
288+
self.assertEqual(
289+
repr(f_result), f'<{self.cls.__name__} finished result=4>')
266290
self.assertEqual(f_result.result(), 4)
267291

268292
exc = RuntimeError()
269293
f_exception = self._new_future(loop=self.loop)
270294
f_exception.set_exception(exc)
271-
self.assertEqual(repr(f_exception),
272-
'<Future finished exception=RuntimeError()>')
295+
self.assertEqual(
296+
repr(f_exception),
297+
f'<{self.cls.__name__} finished exception=RuntimeError()>')
273298
self.assertIs(f_exception.exception(), exc)
274299

275300
def func_repr(func):
@@ -280,19 +305,20 @@ def func_repr(func):
280305
f_one_callbacks = self._new_future(loop=self.loop)
281306
f_one_callbacks.add_done_callback(_fakefunc)
282307
fake_repr = func_repr(_fakefunc)
283-
self.assertRegex(repr(f_one_callbacks),
284-
r'<Future pending cb=\[%s\]>' % fake_repr)
308+
self.assertRegex(
309+
repr(f_one_callbacks),
310+
r'<' + self.cls.__name__ + r' pending cb=\[%s\]>' % fake_repr)
285311
f_one_callbacks.cancel()
286312
self.assertEqual(repr(f_one_callbacks),
287-
'<Future cancelled>')
313+
f'<{self.cls.__name__} cancelled>')
288314

289315
f_two_callbacks = self._new_future(loop=self.loop)
290316
f_two_callbacks.add_done_callback(first_cb)
291317
f_two_callbacks.add_done_callback(last_cb)
292318
first_repr = func_repr(first_cb)
293319
last_repr = func_repr(last_cb)
294320
self.assertRegex(repr(f_two_callbacks),
295-
r'<Future pending cb=\[%s, %s\]>'
321+
r'<' + self.cls.__name__ + r' pending cb=\[%s, %s\]>'
296322
% (first_repr, last_repr))
297323

298324
f_many_callbacks = self._new_future(loop=self.loop)
@@ -301,11 +327,12 @@ def func_repr(func):
301327
f_many_callbacks.add_done_callback(_fakefunc)
302328
f_many_callbacks.add_done_callback(last_cb)
303329
cb_regex = r'%s, <8 more>, %s' % (first_repr, last_repr)
304-
self.assertRegex(repr(f_many_callbacks),
305-
r'<Future pending cb=\[%s\]>' % cb_regex)
330+
self.assertRegex(
331+
repr(f_many_callbacks),
332+
r'<' + self.cls.__name__ + r' pending cb=\[%s\]>' % cb_regex)
306333
f_many_callbacks.cancel()
307334
self.assertEqual(repr(f_many_callbacks),
308-
'<Future cancelled>')
335+
f'<{self.cls.__name__} cancelled>')
309336

310337
def test_copy_state(self):
311338
from asyncio.futures import _copy_future_state
@@ -475,7 +502,7 @@ def memory_error():
475502
support.gc_collect()
476503

477504
if sys.version_info >= (3, 4):
478-
regex = r'^Future exception was never retrieved\n'
505+
regex = f'^{self.cls.__name__} exception was never retrieved\n'
479506
exc_info = (type(exc), exc, exc.__traceback__)
480507
m_log.error.assert_called_once_with(mock.ANY, exc_info=exc_info)
481508
else:
@@ -531,7 +558,16 @@ def __del__(self):
531558
@unittest.skipUnless(hasattr(futures, '_CFuture'),
532559
'requires the C _asyncio module')
533560
class CFutureTests(BaseFutureTests, test_utils.TestCase):
534-
cls = getattr(futures, '_CFuture')
561+
cls = futures._CFuture
562+
563+
564+
@unittest.skipUnless(hasattr(futures, '_CFuture'),
565+
'requires the C _asyncio module')
566+
class CSubFutureTests(BaseFutureTests, test_utils.TestCase):
567+
class CSubFuture(futures._CFuture):
568+
pass
569+
570+
cls = CSubFuture
535571

536572

537573
class PyFutureTests(BaseFutureTests, test_utils.TestCase):
@@ -556,6 +592,76 @@ def bag_appender(future):
556592
def _new_future(self):
557593
raise NotImplementedError
558594

595+
def test_callbacks_remove_first_callback(self):
596+
bag = []
597+
f = self._new_future()
598+
599+
cb1 = self._make_callback(bag, 42)
600+
cb2 = self._make_callback(bag, 17)
601+
cb3 = self._make_callback(bag, 100)
602+
603+
f.add_done_callback(cb1)
604+
f.add_done_callback(cb2)
605+
f.add_done_callback(cb3)
606+
607+
f.remove_done_callback(cb1)
608+
f.remove_done_callback(cb1)
609+
610+
self.assertEqual(bag, [])
611+
f.set_result('foo')
612+
613+
self.run_briefly()
614+
615+
self.assertEqual(bag, [17, 100])
616+
self.assertEqual(f.result(), 'foo')
617+
618+
def test_callbacks_remove_first_and_second_callback(self):
619+
bag = []
620+
f = self._new_future()
621+
622+
cb1 = self._make_callback(bag, 42)
623+
cb2 = self._make_callback(bag, 17)
624+
cb3 = self._make_callback(bag, 100)
625+
626+
f.add_done_callback(cb1)
627+
f.add_done_callback(cb2)
628+
f.add_done_callback(cb3)
629+
630+
f.remove_done_callback(cb1)
631+
f.remove_done_callback(cb2)
632+
f.remove_done_callback(cb1)
633+
634+
self.assertEqual(bag, [])
635+
f.set_result('foo')
636+
637+
self.run_briefly()
638+
639+
self.assertEqual(bag, [100])
640+
self.assertEqual(f.result(), 'foo')
641+
642+
def test_callbacks_remove_third_callback(self):
643+
bag = []
644+
f = self._new_future()
645+
646+
cb1 = self._make_callback(bag, 42)
647+
cb2 = self._make_callback(bag, 17)
648+
cb3 = self._make_callback(bag, 100)
649+
650+
f.add_done_callback(cb1)
651+
f.add_done_callback(cb2)
652+
f.add_done_callback(cb3)
653+
654+
f.remove_done_callback(cb3)
655+
f.remove_done_callback(cb3)
656+
657+
self.assertEqual(bag, [])
658+
f.set_result('foo')
659+
660+
self.run_briefly()
661+
662+
self.assertEqual(bag, [42, 17])
663+
self.assertEqual(f.result(), 'foo')
664+
559665
def test_callbacks_invoked_on_set_result(self):
560666
bag = []
561667
f = self._new_future()
@@ -678,6 +784,17 @@ def _new_future(self):
678784
return futures._CFuture(loop=self.loop)
679785

680786

787+
@unittest.skipUnless(hasattr(futures, '_CFuture'),
788+
'requires the C _asyncio module')
789+
class CSubFutureDoneCallbackTests(BaseFutureDoneCallbackTests,
790+
test_utils.TestCase):
791+
792+
def _new_future(self):
793+
class CSubFuture(futures._CFuture):
794+
pass
795+
return CSubFuture(loop=self.loop)
796+
797+
681798
class PyFutureDoneCallbackTests(BaseFutureDoneCallbackTests,
682799
test_utils.TestCase):
683800

Lib/test/test_asyncio/test_tasks.py

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2187,23 +2187,51 @@ async def func():
21872187
return cls
21882188

21892189

2190-
@unittest.skipUnless(hasattr(futures, '_CFuture'),
2190+
@unittest.skipUnless(hasattr(futures, '_CFuture') and
2191+
hasattr(tasks, '_CTask'),
21912192
'requires the C _asyncio module')
21922193
class CTask_CFuture_Tests(BaseTaskTests, test_utils.TestCase):
21932194
Task = getattr(tasks, '_CTask', None)
21942195
Future = getattr(futures, '_CFuture', None)
21952196

21962197

2197-
@unittest.skipUnless(hasattr(futures, '_CFuture'),
2198+
@unittest.skipUnless(hasattr(futures, '_CFuture') and
2199+
hasattr(tasks, '_CTask'),
21982200
'requires the C _asyncio module')
21992201
@add_subclass_tests
22002202
class CTask_CFuture_SubclassTests(BaseTaskTests, test_utils.TestCase):
2201-
Task = getattr(tasks, '_CTask', None)
2202-
Future = getattr(futures, '_CFuture', None)
2203+
2204+
class Task(tasks._CTask):
2205+
pass
2206+
2207+
class Future(futures._CFuture):
2208+
pass
2209+
2210+
2211+
@unittest.skipUnless(hasattr(tasks, '_CTask'),
2212+
'requires the C _asyncio module')
2213+
@add_subclass_tests
2214+
class CTaskSubclass_PyFuture_Tests(BaseTaskTests, test_utils.TestCase):
2215+
2216+
class Task(tasks._CTask):
2217+
pass
2218+
2219+
Future = futures._PyFuture
22032220

22042221

22052222
@unittest.skipUnless(hasattr(futures, '_CFuture'),
22062223
'requires the C _asyncio module')
2224+
@add_subclass_tests
2225+
class PyTask_CFutureSubclass_Tests(BaseTaskTests, test_utils.TestCase):
2226+
2227+
class Future(futures._CFuture):
2228+
pass
2229+
2230+
Task = tasks._PyTask
2231+
2232+
2233+
@unittest.skipUnless(hasattr(tasks, '_CTask'),
2234+
'requires the C _asyncio module')
22072235
class CTask_PyFuture_Tests(BaseTaskTests, test_utils.TestCase):
22082236
Task = getattr(tasks, '_CTask', None)
22092237
Future = futures._PyFuture
@@ -2223,8 +2251,11 @@ class PyTask_PyFuture_Tests(BaseTaskTests, test_utils.TestCase):
22232251

22242252
@add_subclass_tests
22252253
class PyTask_PyFuture_SubclassTests(BaseTaskTests, test_utils.TestCase):
2226-
Task = tasks._PyTask
2227-
Future = futures._PyFuture
2254+
class Task(tasks._PyTask):
2255+
pass
2256+
2257+
class Future(futures._PyFuture):
2258+
pass
22282259

22292260

22302261
class BaseTaskIntrospectionTests:
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Optimize asyncio.Future schedule/add/remove callback. The optimization
2+
shows 3-6% performance improvements of async/await code.

0 commit comments

Comments
 (0)