From 70652f04d2d0600b5ff11afb3f324adfa1fd6caa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Francis=20Clairicia-Rose-Claire-Jos=C3=A9phine?= Date: Fri, 14 Jun 2024 13:00:43 +0200 Subject: [PATCH] Benchmarks: Add benches for SSL and easynetwork + buffered serializers (#296) --- benchmark_server/run_benchmark | 202 +++++++++++++++++- .../servers/asyncio_tcp_echoserver.py | 2 + benchmark_server/stream_echoclient.py | 4 + src/easynetwork/serializers/base_stream.py | 52 ++--- src/easynetwork/serializers/struct.py | 13 +- tox.ini | 3 +- 6 files changed, 223 insertions(+), 53 deletions(-) diff --git a/benchmark_server/run_benchmark b/benchmark_server/run_benchmark index 0fdec6bd..a67e44f7 100755 --- a/benchmark_server/run_benchmark +++ b/benchmark_server/run_benchmark @@ -31,12 +31,17 @@ ROOT_DIR: Final[Path] = Path(__file__).parent EXPOSED_PORT: Final[int] = 25000 +class _PingOptions(TypedDict, total=False): + ssl: bool + + class _BenchmarkDef(TypedDict): name: str title: str server: list[str] server_address: str | tuple[str, int] ping_request: bytes + ping_options: NotRequired[_PingOptions] client: list[str] type: int @@ -79,6 +84,7 @@ _generic_stream_echoclient: Final[list[str]] = [ _tcp_server_address: Final[tuple[str, int]] = ("127.0.0.1", EXPOSED_PORT) _tcp_echoclient: Final[list[str]] = _generic_stream_echoclient + [f"--addr=127.0.0.1:{EXPOSED_PORT}"] _tcp_readline_client: Final[list[str]] = _tcp_echoclient + ["--mpr=5"] +_ssl_over_tcp_echoclient: Final[list[str]] = _tcp_echoclient + ["--ssl"] _generic_datagram_echoclient: Final[list[str]] = [ os.fspath(ROOT_DIR / "datagram_echoclient.py"), @@ -90,6 +96,9 @@ _udp_echoclient: Final[list[str]] = _generic_datagram_echoclient + [f"--addr=127 BENCHMARKS_DEF: Final[Sequence[_BenchmarkDef]] = ( + ########################################################################## + ################################ TCP echo ################################ + ########################################################################## { "name": "tcpecho-easynetwork-asyncio", "title": "TCP echo server (easynetwork+asyncio)", @@ -117,6 +126,35 @@ BENCHMARKS_DEF: Final[Sequence[_BenchmarkDef]] = ( "client": _tcp_echoclient, "type": SOCK_STREAM, }, + { + "name": "tcpecho-easynetwork-buffered-asyncio", + "title": "TCP echo server (easynetwork+buffered+asyncio)", + "server": _python_cmd + + [ + "/usr/src/servers/easynetwork_tcp_echoserver.py", + f"--port={EXPOSED_PORT}", + "--buffered", + ], + "server_address": _tcp_server_address, + "ping_request": b"ping\n", + "client": _tcp_echoclient, + "type": SOCK_STREAM, + }, + { + "name": "tcpecho-easynetwork-buffered-uvloop", + "title": "TCP echo server (easynetwork+buffered+uvloop)", + "server": _python_cmd + + [ + "/usr/src/servers/easynetwork_tcp_echoserver.py", + f"--port={EXPOSED_PORT}", + "--buffered", + "--uvloop", + ], + "server_address": _tcp_server_address, + "ping_request": b"ping\n", + "client": _tcp_echoclient, + "type": SOCK_STREAM, + }, { "name": "tcpecho-asyncio-sockets", "title": "TCP echo server (asyncio/sockets)", @@ -173,6 +211,9 @@ BENCHMARKS_DEF: Final[Sequence[_BenchmarkDef]] = ( "client": _tcp_echoclient, "type": SOCK_STREAM, }, + ############################################################################## + ################################ TCP readline ################################ + ############################################################################## { "name": "readline-easynetwork-asyncio", "title": "TCP readline server (easynetwork+asyncio)", @@ -202,6 +243,37 @@ BENCHMARKS_DEF: Final[Sequence[_BenchmarkDef]] = ( "client": _tcp_readline_client, "type": SOCK_STREAM, }, + { + "name": "readline-easynetwork-buffered-asyncio", + "title": "TCP readline server (easynetwork+buffered+asyncio)", + "server": _python_cmd + + [ + "/usr/src/servers/easynetwork_tcp_echoserver.py", + f"--port={EXPOSED_PORT}", + "--readline", + "--buffered", + ], + "server_address": _tcp_server_address, + "ping_request": b"ping\n", + "client": _tcp_readline_client, + "type": SOCK_STREAM, + }, + { + "name": "readline-easynetwork-buffered-uvloop", + "title": "TCP readline server (easynetwork+buffered+uvloop)", + "server": _python_cmd + + [ + "/usr/src/servers/easynetwork_tcp_echoserver.py", + f"--port={EXPOSED_PORT}", + "--readline", + "--buffered", + "--uvloop", + ], + "server_address": _tcp_server_address, + "ping_request": b"ping\n", + "client": _tcp_readline_client, + "type": SOCK_STREAM, + }, { "name": "readline-asyncio-streams", "title": "TCP readline server (asyncio/streams)", @@ -231,6 +303,109 @@ BENCHMARKS_DEF: Final[Sequence[_BenchmarkDef]] = ( "client": _tcp_readline_client, "type": SOCK_STREAM, }, + ################################################################################### + ################################ SSL over TCP echo ################################ + ################################################################################### + { + "name": "sslecho-easynetwork-asyncio", + "title": "TCP+SSL echo server (easynetwork+asyncio)", + "server": _python_cmd + + [ + "/usr/src/servers/easynetwork_tcp_echoserver.py", + f"--port={EXPOSED_PORT}", + "--ssl", + ], + "server_address": _tcp_server_address, + "ping_request": b"ping\n", + "ping_options": {"ssl": True}, + "client": _ssl_over_tcp_echoclient, + "type": SOCK_STREAM, + }, + { + "name": "sslecho-easynetwork-uvloop", + "title": "TCP+SSL echo server (easynetwork+uvloop)", + "server": _python_cmd + + [ + "/usr/src/servers/easynetwork_tcp_echoserver.py", + f"--port={EXPOSED_PORT}", + "--ssl", + "--uvloop", + ], + "server_address": _tcp_server_address, + "ping_request": b"ping\n", + "ping_options": {"ssl": True}, + "client": _ssl_over_tcp_echoclient, + "type": SOCK_STREAM, + }, + { + "name": "sslecho-easynetwork-buffered-asyncio", + "title": "TCP+SSL echo server (easynetwork+buffered+asyncio)", + "server": _python_cmd + + [ + "/usr/src/servers/easynetwork_tcp_echoserver.py", + f"--port={EXPOSED_PORT}", + "--ssl", + "--buffered", + ], + "server_address": _tcp_server_address, + "ping_request": b"ping\n", + "ping_options": {"ssl": True}, + "client": _ssl_over_tcp_echoclient, + "type": SOCK_STREAM, + }, + { + "name": "sslecho-easynetwork-buffered-uvloop", + "title": "TCP+SSL echo server (easynetwork+buffered+uvloop)", + "server": _python_cmd + + [ + "/usr/src/servers/easynetwork_tcp_echoserver.py", + f"--port={EXPOSED_PORT}", + "--ssl", + "--buffered", + "--uvloop", + ], + "server_address": _tcp_server_address, + "ping_request": b"ping\n", + "ping_options": {"ssl": True}, + "client": _ssl_over_tcp_echoclient, + "type": SOCK_STREAM, + }, + { + "name": "sslecho-asyncio-streams", + "title": "TCP+SSL echo server (asyncio/streams)", + "server": _python_cmd + + [ + "/usr/src/servers/asyncio_tcp_echoserver.py", + f"--port={EXPOSED_PORT}", + "--ssl", + "--streams", + ], + "server_address": _tcp_server_address, + "ping_request": b"ping\n", + "ping_options": {"ssl": True}, + "client": _ssl_over_tcp_echoclient, + "type": SOCK_STREAM, + }, + { + "name": "sslecho-uvloop-streams", + "title": "TCP+SSL echo server (uvloop/streams)", + "server": _python_cmd + + [ + "/usr/src/servers/asyncio_tcp_echoserver.py", + f"--port={EXPOSED_PORT}", + "--ssl", + "--streams", + "--uvloop", + ], + "server_address": _tcp_server_address, + "ping_request": b"ping\n", + "ping_options": {"ssl": True}, + "client": _ssl_over_tcp_echoclient, + "type": SOCK_STREAM, + }, + ########################################################################## + ################################ UDP echo ################################ + ########################################################################## { "name": "udpecho-easynetwork-asyncio", "title": "UDP echo server (easynetwork+asyncio)", @@ -330,13 +505,14 @@ def _stop_container(container: Container, timeout: int) -> None: def _start_docker_instance( + *, client: docker.DockerClient, image_tag: str, server_cmd: Sequence[str], server_address: str | tuple[str, int], ping_request: bytes, + ping_options: _PingOptions, socket_type: int, - *, timeout: float, docker_wait: float, ) -> Container: @@ -391,8 +567,18 @@ def _start_docker_instance( sock.bind(("127.0.0.1", 0)) sock.connect(server_address) if socket_type == SOCK_DGRAM: + if ping_options.get("ssl", False): + raise OSError("SSL not supported for SOCK_DGRAM sockets") sock.send(ping_request) else: + if ping_options.get("ssl", False): + import ssl + + client_context = ssl.create_default_context() + client_context.check_hostname = False + client_context.verify_mode = ssl.CERT_NONE + sock = client_context.wrap_socket(sock, do_handshake_on_connect=True) + sock.sendall(ping_request) if sock.recv(65536): print("Server is up and running.") @@ -405,11 +591,12 @@ def _start_docker_instance( sock.shutdown(socket.SHUT_RDWR) except OSError: pass - sock.close() # There was no errors, unwind all on_error_stack.pop_all() return container + finally: + sock.close() logs: bytes | str = container.logs() if not isinstance(logs, str): @@ -535,11 +722,12 @@ def main() -> None: server_cmd = benchmark["server"] print(" " + " ".join(server_cmd)) container: Container = _start_docker_instance( - client, - image_tag, - server_cmd, - benchmark["server_address"], - benchmark["ping_request"], + client=client, + image_tag=image_tag, + server_cmd=server_cmd, + server_address=benchmark["server_address"], + ping_request=benchmark["ping_request"], + ping_options=benchmark.get("ping_options", {}), socket_type=benchmark["type"], timeout=args.docker_timeout, docker_wait=args.docker_wait, diff --git a/benchmark_server/servers/asyncio_tcp_echoserver.py b/benchmark_server/servers/asyncio_tcp_echoserver.py index 7b560b8f..7eb9e388 100755 --- a/benchmark_server/servers/asyncio_tcp_echoserver.py +++ b/benchmark_server/servers/asyncio_tcp_echoserver.py @@ -187,6 +187,8 @@ def main() -> None: LOGGER.info(f"Server listening at {', '.join(str(s.getsockname()) for s in server.sockets)}") runner.run(server.serve_forever()) else: + if ssl_context: + raise OSError("loop.sock_sendall() and loop.sock_recv() do not support SSL") runner.run(echo_server(("0.0.0.0", port))) diff --git a/benchmark_server/stream_echoclient.py b/benchmark_server/stream_echoclient.py index afce2d11..43ce2aa4 100755 --- a/benchmark_server/stream_echoclient.py +++ b/benchmark_server/stream_echoclient.py @@ -58,6 +58,10 @@ def run_test( sock.settimeout(socket_timeout) sock.connect(address) + sock.sendall(b"ping\n") + if sock.recv(128) != b"ping\n": + raise OSError("socket read") + times_per_request: collections.deque[RequestReport] = collections.deque() recv_buf = bytearray(REQSIZE) diff --git a/src/easynetwork/serializers/base_stream.py b/src/easynetwork/serializers/base_stream.py index 387d0f05..efcd01e9 100644 --- a/src/easynetwork/serializers/base_stream.py +++ b/src/easynetwork/serializers/base_stream.py @@ -120,15 +120,6 @@ def deserialize(self, data: bytes, /) -> _T_ReceivedDTOPacket: """ raise NotImplementedError - def deserialize_from_buffer(self, data: ReadableBuffer, /) -> _T_ReceivedDTOPacket: - """ - Called by :meth:`buffered_incremental_deserialize` and must have the same behavior as :meth:`deserialize`. - - The default implementation creates a :class:`bytes` object from `data` and calls :meth:`deserialize`. - """ - data = bytes(data) - return self.deserialize(data) - @final def incremental_deserialize(self) -> Generator[None, bytes, tuple[_T_ReceivedDTOPacket, bytes]]: """ @@ -169,29 +160,29 @@ def create_deserializer_buffer(self, sizehint: int) -> bytearray: @final def buffered_incremental_deserialize(self, buffer: bytearray) -> Generator[int, int, tuple[_T_ReceivedDTOPacket, memoryview]]: """ - Yields until `separator` is found and calls :meth:`deserialize_from_buffer` **without** `separator`. + Yields until `separator` is found and calls :meth:`deserialize` **without** `separator`. See :meth:`.BufferedIncrementalPacketSerializer.buffered_incremental_deserialize` documentation for details. Raises: LimitOverrunError: Reached buffer size limit. - IncrementalDeserializeError: :meth:`deserialize_from_buffer` raised :exc:`.DeserializeError`. - Exception: Any error raised by :meth:`deserialize_from_buffer`. + IncrementalDeserializeError: :meth:`deserialize` raised :exc:`.DeserializeError`. + Exception: Any error raised by :meth:`deserialize`. """ with memoryview(buffer) as buffer_view: sepidx, offset, buflen = yield from _buffered_readuntil(buffer, self.__separator) del buffer + data = bytes(buffer_view[:sepidx]) remainder: memoryview = buffer_view[offset:buflen] - with buffer_view[:sepidx] as data: - try: - packet = self.deserialize_from_buffer(data) - except DeserializeError as exc: - raise IncrementalDeserializeError( - f"Error when deserializing data: {exc}", - remaining_data=remainder, - error_info=exc.error_info, - ) from exc + try: + packet = self.deserialize(data) + except DeserializeError as exc: + raise IncrementalDeserializeError( + f"Error when deserializing data: {exc}", + remaining_data=remainder, + error_info=exc.error_info, + ) from exc return packet, remainder @property @@ -274,15 +265,6 @@ def deserialize(self, data: bytes, /) -> _T_ReceivedDTOPacket: """ raise NotImplementedError - def deserialize_from_buffer(self, data: ReadableBuffer, /) -> _T_ReceivedDTOPacket: - """ - Called by :meth:`buffered_incremental_deserialize` and must have the same behavior as :meth:`deserialize`. - - The default implementation creates a :class:`bytes` object from `data` and calls :meth:`deserialize`. - """ - data = bytes(data) - return self.deserialize(data) - @final def incremental_deserialize(self) -> Generator[None, bytes, tuple[_T_ReceivedDTOPacket, bytes]]: """ @@ -325,13 +307,13 @@ def buffered_incremental_deserialize( /, ) -> Generator[int, int, tuple[_T_ReceivedDTOPacket, memoryview]]: """ - Yields until there is enough data and calls :meth:`deserialize_from_buffer`. + Yields until there is enough data and calls :meth:`deserialize`. See :meth:`.BufferedIncrementalPacketSerializer.buffered_incremental_deserialize` documentation for details. Raises: - IncrementalDeserializeError: :meth:`deserialize_from_buffer` raised :exc:`.DeserializeError`. - Exception: Any error raised by :meth:`deserialize_from_buffer`. + IncrementalDeserializeError: :meth:`deserialize` raised :exc:`.DeserializeError`. + Exception: Any error raised by :meth:`deserialize`. """ packet_size: int = self.__size assert len(buffer) >= packet_size # nosec assert_used @@ -340,12 +322,12 @@ def buffered_incremental_deserialize( while nread < packet_size: nread += yield nread - data = buffer[:packet_size] + data = bytes(buffer[:packet_size]) remainder = buffer[packet_size:nread] del buffer try: - packet = self.deserialize_from_buffer(data) + packet = self.deserialize(data) except DeserializeError as exc: raise IncrementalDeserializeError( f"Error when deserializing data: {exc}", diff --git a/src/easynetwork/serializers/struct.py b/src/easynetwork/serializers/struct.py index ec838364..77a34dfa 100644 --- a/src/easynetwork/serializers/struct.py +++ b/src/easynetwork/serializers/struct.py @@ -30,7 +30,7 @@ if TYPE_CHECKING: from struct import Struct - from _typeshed import ReadableBuffer, SupportsKeysAndGetItem + from _typeshed import SupportsKeysAndGetItem _ENDIANNESS_CHARACTERS: frozenset[str] = frozenset({"@", "=", "<", ">", "!"}) @@ -143,7 +143,7 @@ def serialize(self, packet): return self.__s.pack(*self.iter_values(packet)) @final - def deserialize(self, data: ReadableBuffer) -> _T_ReceivedDTOPacket: + def deserialize(self, data: bytes) -> _T_ReceivedDTOPacket: """ Creates a Python object representing the structure from `data`. @@ -171,13 +171,6 @@ def deserialize(self, data): raise DeserializeError(msg) from exc return self.from_tuple(packet_tuple) - @final - def deserialize_from_buffer(self, data: ReadableBuffer) -> _T_ReceivedDTOPacket: - """ - Calls :meth:`deserialize`. - """ - return self.deserialize(data) - @property @final def struct(self) -> Struct: @@ -347,7 +340,7 @@ def iter_values(self, packet: _T_NamedTuple) -> _T_NamedTuple: if (encoding := self.__encoding) is not None and self.__string_fields: string_fields: dict[str, str] = {field: getattr(packet, field) for field in self.__string_fields} unicode_errors: str = self.__unicode_errors - packet = packet._replace(**{field: value.encode(encoding, unicode_errors) for field, value in string_fields.items()}) + packet = packet._replace(**{field: bytes(value, encoding, unicode_errors) for field, value in string_fields.items()}) return packet @final diff --git a/tox.ini b/tox.ini index 4b72bee4..b27ccd48 100644 --- a/tox.ini +++ b/tox.ini @@ -224,7 +224,7 @@ setenv = commands = pytest -c pytest-benchmark.ini {posargs:--benchmark-histogram=benchmark_reports{/}benchmark} -[testenv:benchmark-server-{tcpecho,readline,udpecho}] +[testenv:benchmark-server-{tcpecho,sslecho,readline,udpecho}] skip_install = true groups = benchmark-servers @@ -239,6 +239,7 @@ setenv = # Benchmark name tcpecho: BENCHMARK_PATTERN = ^tcpecho + sslecho: BENCHMARK_PATTERN = ^sslecho udpecho: BENCHMARK_PATTERN = ^udpecho readline: BENCHMARK_PATTERN = ^readline