From cfda592de91d11ed0d0ce9e70845766921c97f4e Mon Sep 17 00:00:00 2001 From: Timofey Ivankov Date: Sat, 30 May 2026 20:16:11 +0300 Subject: [PATCH 1/6] gh-150621: Avoid O(N^2) bytes slicing in asyncio _feed_data_to_buffered_proto --- Lib/asyncio/protocols.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/Lib/asyncio/protocols.py b/Lib/asyncio/protocols.py index 09987b164c66e5..1ead5b15539ba0 100644 --- a/Lib/asyncio/protocols.py +++ b/Lib/asyncio/protocols.py @@ -199,6 +199,7 @@ def process_exited(self): def _feed_data_to_buffered_proto(proto, data): data_len = len(data) + start = 0 while data_len: buf = proto.get_buffer(data_len) buf_len = len(buf) @@ -206,11 +207,11 @@ def _feed_data_to_buffered_proto(proto, data): raise RuntimeError('get_buffer() returned an empty buffer') if buf_len >= data_len: - buf[:data_len] = data + buf[:data_len] = data[start:start + data_len] if start else data proto.buffer_updated(data_len) return else: - buf[:buf_len] = data[:buf_len] + buf[:buf_len] = data[start:start + buf_len] proto.buffer_updated(buf_len) - data = data[buf_len:] - data_len = len(data) + start += buf_len + data_len -= buf_len From 9e684bb30f772712b6ce72a48f133bd0f764b595 Mon Sep 17 00:00:00 2001 From: Timofey Ivankov Date: Sat, 30 May 2026 22:35:22 +0300 Subject: [PATCH 2/6] add NEWS entry --- .../Library/2026-05-30-22-32-49.gh-issue-150621.6RHpsO.rst | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2026-05-30-22-32-49.gh-issue-150621.6RHpsO.rst diff --git a/Misc/NEWS.d/next/Library/2026-05-30-22-32-49.gh-issue-150621.6RHpsO.rst b/Misc/NEWS.d/next/Library/2026-05-30-22-32-49.gh-issue-150621.6RHpsO.rst new file mode 100644 index 00000000000000..a266ca4dcadeac --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-05-30-22-32-49.gh-issue-150621.6RHpsO.rst @@ -0,0 +1,6 @@ +Improve performance of +:func:`!asyncio.protocols._feed_data_to_buffered_proto`, used by the +proactor event loop's read transports (sockets, pipes, subprocess +stdout/stderr) when the protocol is a :class:`~asyncio.BufferedProtocol`. +Total work drops from ``O(N²)`` to ``O(N)`` when the payload exceeds a +single buffer. From 05989d36ae3e957187dcd0ec917fde0c89aa45f3 Mon Sep 17 00:00:00 2001 From: Timofey Ivankov Date: Sat, 30 May 2026 23:01:53 +0300 Subject: [PATCH 3/6] Add tests for _feed_data_to_buffered_proto --- Lib/test/test_asyncio/test_protocols.py | 30 ++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/Lib/test/test_asyncio/test_protocols.py b/Lib/test/test_asyncio/test_protocols.py index 29d3bd22705c6a..9e4c1b390be6c9 100644 --- a/Lib/test/test_asyncio/test_protocols.py +++ b/Lib/test/test_asyncio/test_protocols.py @@ -2,6 +2,7 @@ from unittest import mock import asyncio +from asyncio import protocols def tearDownModule(): @@ -63,5 +64,32 @@ def test_subprocess_protocol(self): self.assertNotHasAttr(sp, '__dict__') -if __name__ == '__main__': +class FeedDataToBufferedProtoTests(unittest.TestCase): + def _make_proto(self, bufsize): + received = bytearray() + buf = bytearray(bufsize) + + class P(asyncio.BufferedProtocol): + def get_buffer(self, sizehint): + return buf + + def buffer_updated(self, nbytes): + received.extend(buf[:nbytes]) + + return P(), received + + def test_large_multi_iteration(self): + proto, received = self._make_proto(64) + data = bytes(range(256)) * 16 + protocols._feed_data_to_buffered_proto(proto, data) + self.assertEqual(bytes(received), data) + + def test_memoryview_input(self): + proto, received = self._make_proto(64) + payload = b"y" * 200 + protocols._feed_data_to_buffered_proto(proto, memoryview(payload)) + self.assertEqual(bytes(received), payload) + + +if __name__ == "__main__": unittest.main() From 5672b15ca7e9ce443bc8d63b4c8a6d6375902223 Mon Sep 17 00:00:00 2001 From: Timofey Ivankov Date: Tue, 21 Jul 2026 21:45:06 +0300 Subject: [PATCH 4/6] review fix --- Lib/asyncio/protocols.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/asyncio/protocols.py b/Lib/asyncio/protocols.py index 1ead5b15539ba0..24ac2bd4a9d5a6 100644 --- a/Lib/asyncio/protocols.py +++ b/Lib/asyncio/protocols.py @@ -207,7 +207,7 @@ def _feed_data_to_buffered_proto(proto, data): raise RuntimeError('get_buffer() returned an empty buffer') if buf_len >= data_len: - buf[:data_len] = data[start:start + data_len] if start else data + buf[:data_len] = data[start:] proto.buffer_updated(data_len) return else: From 8d2854275bcaf2ac061936cde2da9ff71164afbb Mon Sep 17 00:00:00 2001 From: Timofei <128279579+deadlovelll@users.noreply.github.com> Date: Wed, 22 Jul 2026 10:42:03 +0300 Subject: [PATCH 5/6] Delete Misc/NEWS.d/next/Library/2026-05-30-22-32-49.gh-issue-150621.6RHpsO.rst delete news entry --- .../Library/2026-05-30-22-32-49.gh-issue-150621.6RHpsO.rst | 6 ------ 1 file changed, 6 deletions(-) delete mode 100644 Misc/NEWS.d/next/Library/2026-05-30-22-32-49.gh-issue-150621.6RHpsO.rst diff --git a/Misc/NEWS.d/next/Library/2026-05-30-22-32-49.gh-issue-150621.6RHpsO.rst b/Misc/NEWS.d/next/Library/2026-05-30-22-32-49.gh-issue-150621.6RHpsO.rst deleted file mode 100644 index a266ca4dcadeac..00000000000000 --- a/Misc/NEWS.d/next/Library/2026-05-30-22-32-49.gh-issue-150621.6RHpsO.rst +++ /dev/null @@ -1,6 +0,0 @@ -Improve performance of -:func:`!asyncio.protocols._feed_data_to_buffered_proto`, used by the -proactor event loop's read transports (sockets, pipes, subprocess -stdout/stderr) when the protocol is a :class:`~asyncio.BufferedProtocol`. -Total work drops from ``O(N²)`` to ``O(N)`` when the payload exceeds a -single buffer. From f1b97ca9037a2b1ca96158c9b1743d4dcb8c2897 Mon Sep 17 00:00:00 2001 From: Timofey Ivankov Date: Sat, 25 Jul 2026 11:38:32 +0300 Subject: [PATCH 6/6] restore start==0 fast path to avoid needless copy --- Lib/asyncio/protocols.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/asyncio/protocols.py b/Lib/asyncio/protocols.py index 24ac2bd4a9d5a6..c67a26c9e2986c 100644 --- a/Lib/asyncio/protocols.py +++ b/Lib/asyncio/protocols.py @@ -207,7 +207,7 @@ def _feed_data_to_buffered_proto(proto, data): raise RuntimeError('get_buffer() returned an empty buffer') if buf_len >= data_len: - buf[:data_len] = data[start:] + buf[:data_len] = data[start:] if start else data proto.buffer_updated(data_len) return else: