From cfda592de91d11ed0d0ce9e70845766921c97f4e Mon Sep 17 00:00:00 2001 From: Timofey Ivankov Date: Sat, 30 May 2026 20:16:11 +0300 Subject: [PATCH 1/3] 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 09987b164c66e57..1ead5b15539ba0f 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/3] 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 000000000000000..a266ca4dcadeac6 --- /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/3] 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 29d3bd22705c6af..9e4c1b390be6c9d 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()