8000 Avoid scheduling the writer if it can finish synchronously by bdraco · Pull Request #8510 · aio-libs/aiohttp · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

Avoid scheduling the writer if it can finish synchronously #8510

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Jul 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
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
1 change: 1 addition & 0 deletions CHANGES/8510.misc.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
When using Python 3.12 or later, the writer is no longer scheduled on the event loop if it can finish synchronously. Avoiding event loop scheduling reduces latency and improves performance. -- by :user:`bdraco`.
24 changes: 21 additions & 3 deletions aiohttp/client_reqrep.py
Original file line number Diff line number Diff line change
Expand Up @@ -283,7 +283,12 @@ def _writer(self, writer: Optional["asyncio.Task[None]"]) -> None:
if self.__writer is not None:
self.__writer.remove_done_callback(self.__reset_writer)
self.__writer = writer
if writer is not None:
if writer is None:
return
if writer.done():
# The writer is already done, so we can reset it immediately.
self.__reset_writer()
else:
writer.add_done_callback(self.__reset_writer)

def is_ssl(self) -> bool:
Expand Down Expand Up @@ -657,9 +662,17 @@ async def send(self, conn: "Connection") -> "ClientResponse":
self.method, path, v=self.version
)
await writer.write_headers(status_line, self.headers)
coro = self.write_bytes(writer, conn)

self._writer = self.loop.create_task(self.write_bytes(writer, conn))
if sys.version_info >= (3, 12):
# Optimization for Python 3.12, try to write
# bytes immediately to avoid having to schedule
# the task on the event loop.
task = asyncio.Task(coro, loop=self.loop, eager_start=True)
else:
task = self.loop.create_task(coro)

self._writer = task
response_class = self.response_class
assert response_class is not None
self.response = response_class(
Expand Down Expand Up @@ -776,7 +789,12 @@ def _writer(self, writer: Optional["asyncio.Task[None]"]) -> None:
if self.__writer is not None:
self.__writer.remove_done_callback(self.__reset_writer)
self.__writer = writer
if writer is not None:
if writer is None:
return
if writer.done():
# The writer is already done, so we can reset it immediately.
self.__reset_writer()
else:
writer.add_done_callback(self.__reset_writer)

@reify
Expand Down
43 changes: 37 additions & 6 deletions tests/test_client_request.py
Original file line number Diff line number Diff line change
Expand Up @@ -972,8 +972,15 @@ async def gen():
req = ClientRequest("POST", URL("http://python.org/"), data=gen(), loop=loop)
assert req.chunked
assert req.headers["TRANSFER-ENCODING"] == "chunked"
original_write_bytes = req.write_bytes

resp = await req.send(conn)
async def _mock_write_bytes(*args, **kwargs):
# Ensure the task is scheduled
await asyncio.sleep(0)
return await original_write_bytes(*args, **kwargs)

with mock.patch.object(req, "write_bytes", _mock_write_bytes):
resp = await req.send(conn)
assert asyncio.isfuture(req._writer)
await resp.wait_for_close()
assert req._writer is None
Expand Down Expand Up @@ -1155,14 +1162,28 @@ async def test_oserror_on_write_bytes(loop: Any, conn: Any) -> None:

async def test_terminate(loop: Any, conn: Any) -> None:
req = ClientRequest("get", URL("http://python.org"), loop=loop)
resp = await req.send(conn)

async def _mock_write_bytes(*args, **kwargs):
# Ensure the task is scheduled
await asyncio.sleep(0)

with mock.patch.object(req, "write_bytes", _mock_write_bytes):
resp = await req.send(conn)

assert req._writer is not None
writer = req._writer = WriterMock()
assert resp._writer is not None
await resp._writer
writer = WriterMock()
writer.done = mock.Mock(return_value=False)
writer.cancel = mock.Mock()
req._writer = writer
resp._writer = writer

assert req._writer is not None
assert resp._writer is not None
req.terminate()
assert req._writer is None
writer.cancel.assert_called_with()
writer.done.assert_called_with()
resp.close()


Expand All @@ -1172,9 +1193,19 @@ def test_terminate_with_closed_loop(loop: Any, conn: Any) -> None:
async def go():
nonlocal req, resp, writer
req = ClientRequest("get", URL("http://python.org"), loop=loop)
resp = await req.send(conn)

async def _mock_write_bytes(*args, **kwargs):
# Ensure the task is scheduled
await asyncio.sleep(0)

with mock.patch.object(req, "write_bytes", _mock_write_bytes):
resp = await req.send(conn)

assert req._writer is not None
writer = req._writer = WriterMock()
writer = WriterMock()< 568F /td>
writer.done = mock.Mock(return_value=False)
req._writer = writer
resp._writer = writer

await asyncio.sleep(0.05)

Expand Down
Loading
0