feat: improve asyncio write performance (#29)

This commit is contained in:
J. Nick Koston 2022-09-23 21:47:17 -10:00 committed by GitHub
parent 61c5f6d0b7
commit 016e71ef6d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 57 additions and 30 deletions

View File

@ -45,15 +45,16 @@ class _MessageWriter:
self.fd = bus._fd
self.offset = 0
self.unix_fds = None
self.fut = None
self.fut: Optional[asyncio.Future] = None
def write_callback(self):
def write_callback(self, remove_writer: bool = True) -> None:
try:
while True:
if self.buf is None:
if self.messages.qsize() == 0:
# nothing more to write
self.loop.remove_writer(self.fd)
if remove_writer:
self.loop.remove_writer(self.fd)
return
buf, unix_fds, fut = self.messages.get_nowait()
self.unix_fds = unix_fds
@ -97,12 +98,28 @@ class _MessageWriter:
)
)
def _write_without_remove_writer(self):
"""Call the write callback without removing the writer."""
self.write_callback(remove_writer=False)
def schedule_write(self, msg: Message = None, future=None):
queue_is_empty = self.messages.qsize() == 0
if msg is not None:
self.buffer_message(msg, future)
if self.bus.unique_name:
# don't run the writer until the bus is ready to send messages
self.loop.add_writer(self.fd, self.write_callback)
# Optimization: try to send now if the queue
# is empty. With bleak this usually means we
# can send right away 99% of the time which
# is a huge improvement in latency.
if queue_is_empty:
self._write_without_remove_writer()
if (
self.buf is not None
or self.messages.qsize() != 0
or not self.fut
or not self.fut.done()
):
self.loop.add_writer(self.fd, self.write_callback)
class MessageBus(BaseMessageBus):

View File

@ -646,12 +646,19 @@ class BaseMessageBus:
self._name_owners[msg.destination] = reply.sender
callback(reply, err)
no_reply_expected = msg.flags & MessageFlag.NO_REPLY_EXPECTED
# Make sure the return reply handler is installed
# before sending the message to avoid a race condition
# where the reply is lost in case the backend can
# send it right away.
if not no_reply_expected:
self._method_return_handlers[msg.serial] = reply_notify
self.send(msg)
if msg.flags & MessageFlag.NO_REPLY_EXPECTED:
if no_reply_expected:
callback(None, None)
else:
self._method_return_handlers[msg.serial] = reply_notify
@staticmethod
def _check_callback_type(callback):

View File

@ -1,5 +1,6 @@
import functools
import os
from unittest.mock import patch
import pytest
@ -16,19 +17,20 @@ async def test_bus_disconnect_before_reply(event_loop):
await bus.connect()
assert bus.connected
ping = bus.call(
Message(
destination="org.freedesktop.DBus",
path="/org/freedesktop/DBus",
interface="org.freedesktop.DBus",
member="Ping",
with patch.object(bus._writer, "_write_without_remove_writer"):
ping = bus.call(
Message(
destination="org.freedesktop.DBus",
path="/org/freedesktop/DBus",
interface="org.freedesktop.DBus",
member="Ping",
)
)
)
event_loop.call_soon(bus.disconnect)
event_loop.call_soon(bus.disconnect)
with pytest.raises((EOFError, BrokenPipeError)):
await ping
with pytest.raises((EOFError, BrokenPipeError)):
await ping
assert bus._disconnected
assert not bus.connected
@ -42,22 +44,23 @@ async def test_unexpected_disconnect(event_loop):
await bus.connect()
assert bus.connected
ping = bus.call(
Message(
destination="org.freedesktop.DBus",
path="/org/freedesktop/DBus",
interface="org.freedesktop.DBus",
member="Ping",
with patch.object(bus._writer, "_write_without_remove_writer"):
ping = bus.call(
Message(
destination="org.freedesktop.DBus",
path="/org/freedesktop/DBus",
interface="org.freedesktop.DBus",
member="Ping",
)
)
)
event_loop.call_soon(functools.partial(os.close, bus._fd))
event_loop.call_soon(functools.partial(os.close, bus._fd))
with pytest.raises(OSError):
await ping
with pytest.raises(OSError):
await ping
assert bus._disconnected
assert not bus.connected
assert bus._disconnected
assert not bus.connected
with pytest.raises(OSError):
await bus.wait_for_disconnect()