From 564394e4c6e5bb9670702d384c135ef7551824f0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 10 Aug 2024 08:02:33 -0500 Subject: [PATCH 01/10] Reduce asyncio heapq scheduling overhead Wrap the TimerHandle in tuples with the when value at the front to avoid having to call `TimerHandle.__lt__` when working with the `heapq` --- Lib/asyncio/base_events.py | 26 ++++++++++++----------- Lib/test/test_asyncio/test_base_events.py | 12 +++++------ 2 files changed, 20 insertions(+), 18 deletions(-) diff --git a/Lib/asyncio/base_events.py b/Lib/asyncio/base_events.py index e4a39f4d345c79..d89324b59fc850 100644 --- a/Lib/asyncio/base_events.py +++ b/Lib/asyncio/base_events.py @@ -812,7 +812,7 @@ def call_at(self, when, callback, *args, context=None): timer = events.TimerHandle(when, callback, args, self, context) if timer._source_traceback: del timer._source_traceback[-1] - heapq.heappush(self._scheduled, timer) + heapq.heappush(self._scheduled, (when, timer)) timer._scheduled = True return timer @@ -1959,20 +1959,21 @@ def _run_once(self): # Remove delayed calls that were cancelled if their number # is too high new_scheduled = [] - for handle in self._scheduled: + for when_handle in self._scheduled: + handle = when_handle[1] if handle._cancelled: handle._scheduled = False else: - new_scheduled.append(handle) + new_scheduled.append(when_handle) heapq.heapify(new_scheduled) self._scheduled = new_scheduled self._timer_cancelled_count = 0 else: # Remove delayed calls that were cancelled from head of queue. - while self._scheduled and self._scheduled[0]._cancelled: + while self._scheduled and self._scheduled[0][1]._cancelled: self._timer_cancelled_count -= 1 - handle = heapq.heappop(self._scheduled) + _, handle = heapq.heappop(self._scheduled) handle._scheduled = False timeout = None @@ -1980,7 +1981,7 @@ def _run_once(self): timeout = 0 elif self._scheduled: # Compute the desired timeout. - timeout = self._scheduled[0]._when - self.time() + timeout = self._scheduled[0][0] - self.time() if timeout > MAXIMUM_SELECT_TIMEOUT: timeout = MAXIMUM_SELECT_TIMEOUT elif timeout < 0: @@ -1993,13 +1994,14 @@ def _run_once(self): # Handle 'later' callbacks that are ready. end_time = self.time() + self._clock_resolution + ready = self._ready while self._scheduled: - handle = self._scheduled[0] - if handle._when >= end_time: + when, handle = self._scheduled[0] + if when >= end_time: break - handle = heapq.heappop(self._scheduled) + heapq.heappop(self._scheduled) handle._scheduled = False - self._ready.append(handle) + ready.append(handle) # This is the only place where callbacks are actually *called*. # All other places just add them to ready. @@ -2007,9 +2009,9 @@ def _run_once(self): # callbacks scheduled by callbacks run this time around -- # they will be run the next time (after another I/O poll). # Use an idiom that is thread-safe without using locks. - ntodo = len(self._ready) + ntodo = len(ready) for i in range(ntodo): - handle = self._ready.popleft() + handle = ready.popleft() if handle._cancelled: continue if self._debug: diff --git a/Lib/test/test_asyncio/test_base_events.py b/Lib/test/test_asyncio/test_base_events.py index c14a0bb180d79b..60fe0633934eae 100644 --- a/Lib/test/test_asyncio/test_base_events.py +++ b/Lib/test/test_asyncio/test_base_events.py @@ -268,7 +268,7 @@ def cb(): h = self.loop.call_later(10.0, cb) self.assertIsInstance(h, asyncio.TimerHandle) - self.assertIn(h, self.loop._scheduled) + self.assertIn((h.when(), h), self.loop._scheduled) self.assertNotIn(h, self.loop._ready) with self.assertRaises(TypeError, msg="delay must not be None"): self.loop.call_later(None, cb) @@ -378,13 +378,13 @@ def test__run_once(self): h1.cancel() self.loop._process_events = mock.Mock() - self.loop._scheduled.append(h1) - self.loop._scheduled.append(h2) + self.loop._scheduled.append((h1.when(), h1)) + self.loop._scheduled.append((h2.when(), h2)) self.loop._run_once() t = self.loop._selector.select.call_args[0][0] self.assertTrue(9.5 < t < 10.5, t) - self.assertEqual([h2], self.loop._scheduled) + self.assertEqual([(h2.when(), h2)], self.loop._scheduled) self.assertTrue(self.loop._process_events.called) def test_set_debug(self): @@ -406,7 +406,7 @@ def cb(loop): self.loop, None) self.loop._process_events = mock.Mock() - self.loop._scheduled.append(h) + self.loop._scheduled.append((h.when(), h)) self.loop._run_once() self.assertTrue(processed) @@ -486,7 +486,7 @@ def cb(): self.assertEqual(len(self.loop._scheduled), not_cancelled_count) # Ensure only uncancelled events remain scheduled - self.assertTrue(all([not x._cancelled for x in self.loop._scheduled])) + self.assertTrue(all([not x._cancelled for _, x in self.loop._scheduled])) def test_run_until_complete_type_error(self): self.assertRaises(TypeError, From 3b46d71425de88235147381375846ad6aa706206 Mon Sep 17 00:00:00 2001 From: "blurb-it[bot]" <43283697+blurb-it[bot]@users.noreply.github.com> Date: Sat, 10 Aug 2024 13:10:47 +0000 Subject: [PATCH 02/10] =?UTF-8?q?=F0=9F=93=9C=F0=9F=A4=96=20Added=20by=20b?= =?UTF-8?q?lurb=5Fit.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../next/Library/2024-08-10-13-10-45.gh-issue-122881.z_n6W-.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 Misc/NEWS.d/next/Library/2024-08-10-13-10-45.gh-issue-122881.z_n6W-.rst diff --git a/Misc/NEWS.d/next/Library/2024-08-10-13-10-45.gh-issue-122881.z_n6W-.rst b/Misc/NEWS.d/next/Library/2024-08-10-13-10-45.gh-issue-122881.z_n6W-.rst new file mode 100644 index 00000000000000..5e50d743e53dc3 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2024-08-10-13-10-45.gh-issue-122881.z_n6W-.rst @@ -0,0 +1 @@ +Reduced ``asyncio`` ``heapq`` scheduling overhead From f01857d4c4d535efe9b5e8d5f183f0f083ef548e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 10 Aug 2024 08:35:43 -0500 Subject: [PATCH 03/10] Update Lib/asyncio/base_events.py --- Lib/asyncio/base_events.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Lib/asyncio/base_events.py b/Lib/asyncio/base_events.py index d89324b59fc850..5ec6f90bc35a11 100644 --- a/Lib/asyncio/base_events.py +++ b/Lib/asyncio/base_events.py @@ -812,6 +812,9 @@ def call_at(self, when, callback, *args, context=None): timer = events.TimerHandle(when, callback, args, self, context) if timer._source_traceback: del timer._source_traceback[-1] + # The `TimerHandle` is wrapped in a tuple to avoid calling the + # `TimerHandle.__lt__` method since the `heapq` operations + # will need to compare against many other `TimerHandler` objects. heapq.heappush(self._scheduled, (when, timer)) timer._scheduled = True return timer From a866d3303022e9a726a0369b1bd496b2546fc2b7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 10 Aug 2024 10:14:29 -0500 Subject: [PATCH 04/10] Update Misc/NEWS.d/next/Library/2024-08-10-13-10-45.gh-issue-122881.z_n6W-.rst Co-authored-by: Peter Bierma --- .../next/Library/2024-08-10-13-10-45.gh-issue-122881.z_n6W-.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Misc/NEWS.d/next/Library/2024-08-10-13-10-45.gh-issue-122881.z_n6W-.rst b/Misc/NEWS.d/next/Library/2024-08-10-13-10-45.gh-issue-122881.z_n6W-.rst index 5e50d743e53dc3..5da4f8325914b7 100644 --- a/Misc/NEWS.d/next/Library/2024-08-10-13-10-45.gh-issue-122881.z_n6W-.rst +++ b/Misc/NEWS.d/next/Library/2024-08-10-13-10-45.gh-issue-122881.z_n6W-.rst @@ -1 +1 @@ -Reduced ``asyncio`` ``heapq`` scheduling overhead +Reduced :mod:`asyncio` :mod:`heapq` scheduling overhead From 0205d0b87fa7d5713e3981cf129fa0f7794d5805 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 11 Aug 2024 06:42:07 -0500 Subject: [PATCH 05/10] Update Misc/NEWS.d/next/Library/2024-08-10-13-10-45.gh-issue-122881.z_n6W-.rst MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bénédikt Tran <10796600+picnixz@users.noreply.github.com> --- .../next/Library/2024-08-10-13-10-45.gh-issue-122881.z_n6W-.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Misc/NEWS.d/next/Library/2024-08-10-13-10-45.gh-issue-122881.z_n6W-.rst b/Misc/NEWS.d/next/Library/2024-08-10-13-10-45.gh-issue-122881.z_n6W-.rst index 5da4f8325914b7..5b205ff5afe8e3 100644 --- a/Misc/NEWS.d/next/Library/2024-08-10-13-10-45.gh-issue-122881.z_n6W-.rst +++ b/Misc/NEWS.d/next/Library/2024-08-10-13-10-45.gh-issue-122881.z_n6W-.rst @@ -1 +1 @@ -Reduced :mod:`asyncio` :mod:`heapq` scheduling overhead +Reduced :mod:`asyncio` :mod:`heapq` scheduling overhead. From fdda41627c72209fe1ed5c4db16489da427d0eb2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 11 Aug 2024 06:46:11 -0500 Subject: [PATCH 06/10] tweaks from review comments --- Lib/asyncio/base_events.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/Lib/asyncio/base_events.py b/Lib/asyncio/base_events.py index d89324b59fc850..067a29c827094c 100644 --- a/Lib/asyncio/base_events.py +++ b/Lib/asyncio/base_events.py @@ -1977,11 +1977,14 @@ def _run_once(self): handle._scheduled = False timeout = None - if self._ready or self._stopping: + ready = self._ready + scheduled = self._scheduled + + if ready or self._stopping: timeout = 0 - elif self._scheduled: + elif scheduled: # Compute the desired timeout. - timeout = self._scheduled[0][0] - self.time() + timeout = scheduled[0][0] - self.time() if timeout > MAXIMUM_SELECT_TIMEOUT: timeout = MAXIMUM_SELECT_TIMEOUT elif timeout < 0: @@ -1994,12 +1997,11 @@ def _run_once(self): # Handle 'later' callbacks that are ready. end_time = self.time() + self._clock_resolution - ready = self._ready - while self._scheduled: - when, handle = self._scheduled[0] + while scheduled: + when, handle = scheduled[0] if when >= end_time: break - heapq.heappop(self._scheduled) + heapq.heappop(scheduled) handle._scheduled = False ready.append(handle) @@ -2010,8 +2012,9 @@ def _run_once(self): # they will be run the next time (after another I/O poll). # Use an idiom that is thread-safe without using locks. ntodo = len(ready) + ready_popleft = ready.popleft for i in range(ntodo): - handle = ready.popleft() + handle = ready_popleft() if handle._cancelled: continue if self._debug: From 4b9aed8b11005900d15bd44d91511845212ed432 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 12 Aug 2024 07:33:31 -0500 Subject: [PATCH 07/10] tweak from suggestion MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bénédikt Tran <10796600+picnixz@users.noreply.github.com> --- Lib/asyncio/base_events.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/Lib/asyncio/base_events.py b/Lib/asyncio/base_events.py index 9be2e013e0eaf4..423681d457ed71 100644 --- a/Lib/asyncio/base_events.py +++ b/Lib/asyncio/base_events.py @@ -2000,11 +2000,8 @@ def _run_once(self): # Handle 'later' callbacks that are ready. end_time = self.time() + self._clock_resolution - while scheduled: - when, handle = scheduled[0] - if when >= end_time: - break - heapq.heappop(scheduled) + while scheduled and scheduled[0][0] < end_time: + _, handle = heapq.heappop(scheduled) handle._scheduled = False ready.append(handle) From ca5cc2515572d70e11690bec03775af344bce2c8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 12 Aug 2024 08:45:14 -0500 Subject: [PATCH 08/10] TimerHandle scheduling can be quantified, _run_once imporvement will vary greatly --- .../next/Library/2024-08-10-13-10-45.gh-issue-122881.z_n6W-.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Misc/NEWS.d/next/Library/2024-08-10-13-10-45.gh-issue-122881.z_n6W-.rst b/Misc/NEWS.d/next/Library/2024-08-10-13-10-45.gh-issue-122881.z_n6W-.rst index 5b205ff5afe8e3..0f9a291e09efd9 100644 --- a/Misc/NEWS.d/next/Library/2024-08-10-13-10-45.gh-issue-122881.z_n6W-.rst +++ b/Misc/NEWS.d/next/Library/2024-08-10-13-10-45.gh-issue-122881.z_n6W-.rst @@ -1 +1 @@ -Reduced :mod:`asyncio` :mod:`heapq` scheduling overhead. +Reduced :mod:`asyncio` :mod:`heapq` scheduling overhead. Scheduling and running an :class:`asyncio.TimerHandle` is now roughly 9.5% faster. From a5b66476566019f2b643a61b5fd815d5b3276e84 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 12 Aug 2024 13:06:58 -0500 Subject: [PATCH 09/10] access self more often --- Lib/asyncio/base_events.py | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/Lib/asyncio/base_events.py b/Lib/asyncio/base_events.py index 9c3039e001d5c9..e366e3f5276b55 100644 --- a/Lib/asyncio/base_events.py +++ b/Lib/asyncio/base_events.py @@ -1980,14 +1980,12 @@ def _run_once(self): handle._scheduled = False timeout = None - ready = self._ready - scheduled = self._scheduled - if ready or self._stopping: + if self._ready or self._stopping: timeout = 0 - elif scheduled: + elif self._scheduled: # Compute the desired timeout. - timeout = scheduled[0][0] - self.time() + timeout = self._scheduled[0][0] - self.time() if timeout > MAXIMUM_SELECT_TIMEOUT: timeout = MAXIMUM_SELECT_TIMEOUT elif timeout < 0: @@ -2000,10 +1998,10 @@ def _run_once(self): # Handle 'later' callbacks that are ready. end_time = self.time() + self._clock_resolution - while scheduled and scheduled[0][0] < end_time: - _, handle = heapq.heappop(scheduled) + while self._scheduled and self._scheduled[0][0] < end_time: + _, handle = heapq.heappop(self._scheduled) handle._scheduled = False - ready.append(handle) + self._ready.append(handle) # This is the only place where callbacks are actually *called*. # All other places just add them to ready. @@ -2011,10 +2009,9 @@ def _run_once(self): # callbacks scheduled by callbacks run this time around -- # they will be run the next time (after another I/O poll). # Use an idiom that is thread-safe without using locks. - ntodo = len(ready) - ready_popleft = ready.popleft + ntodo = len(self._ready) for i in range(ntodo): - handle = ready_popleft() + handle = self._ready.popleft() if handle._cancelled: continue if self._debug: From f78838c11c57ed53f07e5c472b9f560ebe3be22a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 24 Aug 2024 11:31:59 -1000 Subject: [PATCH 10/10] Update Lib/asyncio/base_events.py --- Lib/asyncio/base_events.py | 1 - 1 file changed, 1 deletion(-) diff --git a/Lib/asyncio/base_events.py b/Lib/asyncio/base_events.py index e366e3f5276b55..7ed4766ef26453 100644 --- a/Lib/asyncio/base_events.py +++ b/Lib/asyncio/base_events.py @@ -1980,7 +1980,6 @@ def _run_once(self): handle._scheduled = False timeout = None - if self._ready or self._stopping: timeout = 0 elif self._scheduled: