Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Lib/asyncio/selector_events.py
Original file line number Diff line number Diff line change
Expand Up @@ -1143,7 +1143,7 @@ def _adjust_leftover_buffer(self, nbytes: int) -> None:
if b_len <= nbytes:
nbytes -= b_len
else:
buffer.appendleft(b[nbytes:])
buffer.appendleft(memoryview(b)[nbytes:])
break

def _write_send(self):
Expand All @@ -1155,7 +1155,7 @@ def _write_send(self):
n = self._sock.send(buffer)
if n != len(buffer):
# Not all data was written
self._buffer.appendleft(buffer[n:])
self._buffer.appendleft(memoryview(buffer)[n:])
self._buffer_size -= n
except (BlockingIOError, InterruptedError):
self._buffer.appendleft(buffer)
Expand Down
30 changes: 30 additions & 0 deletions Lib/test/test_asyncio/test_selector_events.py
Original file line number Diff line number Diff line change
Expand Up @@ -972,6 +972,22 @@ def test_write_sendmsg_partial(self):
self.assertTrue(self.loop.writers)
self.assertEqual(list_to_buffer([b'ta']), transport._buffer)

@unittest.skipUnless(selector_events._HAS_SENDMSG, 'no sendmsg')
Comment thread
Ar-maan05 marked this conversation as resolved.
Outdated
def test_write_sendmsg_partial_bytes_no_copy(self):
# gh-159503: the unsent remainder of a bytes object must not be
# copied, otherwise a large write costs O(n**2).
data = b'data'
self.sock.sendmsg = mock.Mock()
self.sock.sendmsg.return_value = 2

transport = self.socket_transport(sendmsg=True)
transport._buffer.append(data)
self.loop._add_writer(7, transport._write_ready)
transport._write_ready()
self.assertEqual(list_to_buffer([b'ta']), transport._buffer)
self.assertIsInstance(transport._buffer[0], memoryview)
self.assertIs(transport._buffer[0].obj, data)

@unittest.skipUnless(selector_events._HAS_SENDMSG, 'no sendmsg')
def test_write_sendmsg_half_buffer(self):
data = [memoryview(b'data1'), memoryview(b'data2')]
Expand Down Expand Up @@ -1081,6 +1097,20 @@ def test_write_ready_partial(self):
self.loop.assert_writer(7, transport._write_ready)
self.assertEqual(list_to_buffer([b'ta']), transport._buffer)

def test_write_ready_partial_bytes_no_copy(self):
# gh-159503: the unsent remainder of a bytes object must not be
# copied, otherwise a large write costs O(n**2).
data = b'data'
self.sock.send.return_value = 2

transport = self.socket_transport()
transport._buffer.append(data)
self.loop._add_writer(7, transport._write_ready)
transport._write_ready()
self.assertEqual(list_to_buffer([b'ta']), transport._buffer)
self.assertIsInstance(transport._buffer[0], memoryview)
self.assertIs(transport._buffer[0].obj, data)

def test_write_ready_partial_none(self):
data = b'data'
self.sock.send.return_value = 0
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fix quadratic copying in :mod:`asyncio` selector socket transports when a large :class:`bytes` write is only partly sent while other data is buffered. The unsent remainder is now kept as a :class:`memoryview` instead of being copied on every partial send.
Comment thread
Ar-maan05 marked this conversation as resolved.
Outdated
Loading