Skip to content

Commit

Permalink
Improved serializers performances (#391)
Browse files Browse the repository at this point in the history
  • Loading branch information
francis-clairicia authored Jan 25, 2025
1 parent cb48fd3 commit cde3ff2
Show file tree
Hide file tree
Showing 14 changed files with 388 additions and 141 deletions.
27 changes: 23 additions & 4 deletions micro_benchmarks/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,28 @@

import os

import pytest

def pytest_report_header() -> list[str]:
PAYLOAD_SIZE_LEVEL = "payload_size_level"


def pytest_addoption(parser: pytest.Parser) -> None:
group = parser.getgroup("benchmark")
group.addoption(
"--payload-size-level",
dest=PAYLOAD_SIZE_LEVEL,
type=lambda x: tuple(map(int, x.split(","))),
default=(1, 10),
help="a list of message size levels to use (in kilobytes)",
)


def pytest_report_header(config: pytest.Config) -> list[str]:
headers: list[str] = []
addopts: str = os.environ.get("PYTEST_ADDOPTS", "")
if not addopts:
return []
return [f"PYTEST_ADDOPTS: {addopts}"]
if addopts:
headers.append(f"PYTEST_ADDOPTS: {addopts}")
headers.append(
f"Payload size levels: {', '.join(f'{payload_size}kb' for payload_size in config.getoption(PAYLOAD_SIZE_LEVEL))}"
)
return headers
11 changes: 9 additions & 2 deletions micro_benchmarks/serializers/bench_cbor.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,20 @@

from __future__ import annotations

from collections import deque
from typing import TYPE_CHECKING, Any

from easynetwork.serializers.cbor import CBORSerializer

import pytest

from .groups import SerializerGroup

if TYPE_CHECKING:
from pytest_benchmark.fixture import BenchmarkFixture


@pytest.mark.benchmark(group=SerializerGroup.JSON_SERIALIZE)
def bench_CBORSerializer_serialize(
benchmark: BenchmarkFixture,
json_object: Any,
Expand All @@ -21,6 +25,7 @@ def bench_CBORSerializer_serialize(
benchmark(serializer.serialize, json_object)


@pytest.mark.benchmark(group=SerializerGroup.JSON_DESERIALIZE)
def bench_CBORSerializer_deserialize(
benchmark: BenchmarkFixture,
json_object: Any,
Expand All @@ -33,15 +38,17 @@ def bench_CBORSerializer_deserialize(
assert result == json_object


@pytest.mark.benchmark(group=SerializerGroup.JSON_INCREMENTAL_SERIALIZE)
def bench_CBORSerializer_incremental_serialize(
benchmark: BenchmarkFixture,
json_object: Any,
) -> None:
serializer = CBORSerializer()

benchmark(lambda: b"".join(serializer.incremental_serialize(json_object)))
benchmark(lambda: deque(serializer.incremental_serialize(json_object)))


@pytest.mark.benchmark(group=SerializerGroup.JSON_INCREMENTAL_DESERIALIZE)
@pytest.mark.parametrize("buffered", [False, True], ids=lambda p: f"buffered=={p}")
def bench_CBORSerializer_incremental_deserialize(
buffered: bool,
Expand All @@ -54,11 +61,11 @@ def bench_CBORSerializer_incremental_deserialize(
if buffered:
nbytes = len(cbor_data)
buffer: memoryview = serializer.create_deserializer_buffer(nbytes)
buffer[:nbytes] = cbor_data

def deserialize() -> Any:
consumer = serializer.buffered_incremental_deserialize(buffer)
next(consumer)
buffer[:nbytes] = cbor_data
try:
consumer.send(nbytes)
except StopIteration as exc:
Expand Down
9 changes: 8 additions & 1 deletion micro_benchmarks/serializers/bench_json.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,20 @@

from __future__ import annotations

from collections import deque
from typing import TYPE_CHECKING, Any

from easynetwork.serializers.json import JSONSerializer

import pytest

from .groups import SerializerGroup

if TYPE_CHECKING:
from pytest_benchmark.fixture import BenchmarkFixture


@pytest.mark.benchmark(group=SerializerGroup.JSON_SERIALIZE)
def bench_JSONSerializer_serialize(
benchmark: BenchmarkFixture,
json_object: Any,
Expand All @@ -21,6 +25,7 @@ def bench_JSONSerializer_serialize(
benchmark(serializer.serialize, json_object)


@pytest.mark.benchmark(group=SerializerGroup.JSON_DESERIALIZE)
def bench_JSONSerializer_deserialize(
benchmark: BenchmarkFixture,
json_object: Any,
Expand All @@ -33,6 +38,7 @@ def bench_JSONSerializer_deserialize(
assert result == json_object


@pytest.mark.benchmark(group=SerializerGroup.JSON_INCREMENTAL_SERIALIZE)
@pytest.mark.parametrize("use_lines", [False, True], ids=lambda p: f"use_lines=={p}")
def bench_JSONSerializer_incremental_serialize(
use_lines: bool,
Expand All @@ -41,9 +47,10 @@ def bench_JSONSerializer_incremental_serialize(
) -> None:
serializer = JSONSerializer(use_lines=use_lines)

benchmark(lambda: b"".join(serializer.incremental_serialize(json_object)))
benchmark(lambda: deque(serializer.incremental_serialize(json_object)))


@pytest.mark.benchmark(group=SerializerGroup.JSON_INCREMENTAL_DESERIALIZE)
@pytest.mark.parametrize("use_lines", [False, True], ids=lambda p: f"use_lines=={p}")
def bench_JSONSerializer_incremental_deserialize(
use_lines: bool,
Expand Down
26 changes: 22 additions & 4 deletions micro_benchmarks/serializers/bench_line.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,31 @@

from __future__ import annotations

from collections import deque
from typing import TYPE_CHECKING, Any

from easynetwork.serializers.line import StringLineSerializer

import pytest

from ..conftest import PAYLOAD_SIZE_LEVEL
from .groups import SerializerGroup

if TYPE_CHECKING:
from pytest_benchmark.fixture import BenchmarkFixture


@pytest.fixture(scope="module", params=[1, 10], ids=lambda p: f"size=={p}kb")
def pytest_generate_tests(metafunc: pytest.Metafunc) -> None:
if "line_str" in metafunc.fixturenames:
metafunc.parametrize(
"line_str",
list(metafunc.config.getoption(PAYLOAD_SIZE_LEVEL)),
indirect=True,
ids=lambda p: f"size=={p}kb",
)


@pytest.fixture(scope="module")
def line_str(request: pytest.FixtureRequest) -> str:
size: int = request.param * 1024
return ("x" * (size - 1)) + "\n"
Expand All @@ -23,6 +37,7 @@ def line_bytes(line_str: str) -> bytes:
return bytes(line_str, "utf-8")


@pytest.mark.benchmark(group=SerializerGroup.LINE_SERIALIZE)
def bench_StringLineSerializer_serialize(
benchmark: BenchmarkFixture,
line_str: str,
Expand All @@ -35,6 +50,7 @@ def bench_StringLineSerializer_serialize(
assert result == line_bytes


@pytest.mark.benchmark(group=SerializerGroup.LINE_DESERIALIZE)
@pytest.mark.parametrize("keep_end", [False, True], ids=lambda p: f"keep_end=={p}")
def bench_StringLineSerializer_deserialize(
keep_end: bool,
Expand All @@ -52,18 +68,20 @@ def bench_StringLineSerializer_deserialize(
assert result == line_str.removesuffix("\n")


@pytest.mark.benchmark(group=SerializerGroup.LINE_INCREMENTAL_SERIALIZE)
def bench_StringLineSerializer_incremental_serialize(
benchmark: BenchmarkFixture,
line_str: str,
line_bytes: bytes,
) -> None:
serializer = StringLineSerializer()

result = benchmark(lambda: b"".join(serializer.incremental_serialize(line_str)))
result = b"".join(benchmark(lambda: deque(serializer.incremental_serialize(line_str))))

assert result == line_bytes


@pytest.mark.benchmark(group=SerializerGroup.LINE_INCREMENTAL_DESERIALIZE)
@pytest.mark.parametrize("keep_end", [False, True], ids=lambda p: f"keep_end=={p}")
@pytest.mark.parametrize("buffered", [False, True], ids=lambda p: f"buffered=={p}")
def bench_StringLineSerializer_incremental_deserialize(
Expand All @@ -78,11 +96,11 @@ def bench_StringLineSerializer_incremental_deserialize(
if buffered:
nbytes = len(line_bytes)
buffer: bytearray = serializer.create_deserializer_buffer(nbytes)
buffer[:nbytes] = line_bytes

def deserialize() -> Any:
consumer = serializer.buffered_incremental_deserialize(buffer)
start_index: int = next(consumer)
buffer[start_index : start_index + nbytes] = line_bytes
next(consumer)
try:
consumer.send(nbytes)
except StopIteration as exc:
Expand Down
11 changes: 9 additions & 2 deletions micro_benchmarks/serializers/bench_msgpack.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,20 @@

from __future__ import annotations

from collections import deque
from typing import TYPE_CHECKING, Any

from easynetwork.serializers.msgpack import MessagePackSerializer

import pytest

from .groups import SerializerGroup

if TYPE_CHECKING:
from pytest_benchmark.fixture import BenchmarkFixture


@pytest.mark.benchmark(group=SerializerGroup.JSON_SERIALIZE)
def bench_MessagePackSerializer_serialize(
benchmark: BenchmarkFixture,
json_object: Any,
Expand All @@ -21,6 +25,7 @@ def bench_MessagePackSerializer_serialize(
benchmark(serializer.serialize, json_object)


@pytest.mark.benchmark(group=SerializerGroup.JSON_DESERIALIZE)
def bench_MessagePackSerializer_deserialize(
benchmark: BenchmarkFixture,
json_object: Any,
Expand All @@ -33,15 +38,17 @@ def bench_MessagePackSerializer_deserialize(
assert result == json_object


@pytest.mark.benchmark(group=SerializerGroup.JSON_INCREMENTAL_SERIALIZE)
def bench_MessagePackSerializer_incremental_serialize(
benchmark: BenchmarkFixture,
json_object: Any,
) -> None:
serializer = MessagePackSerializer()

benchmark(lambda: b"".join(serializer.incremental_serialize(json_object)))
benchmark(lambda: deque(serializer.incremental_serialize(json_object)))


@pytest.mark.benchmark(group=SerializerGroup.JSON_INCREMENTAL_DESERIALIZE)
@pytest.mark.parametrize("buffered", [False, True], ids=lambda p: f"buffered=={p}")
def bench_MessagePackSerializer_incremental_deserialize(
buffered: bool,
Expand All @@ -54,11 +61,11 @@ def bench_MessagePackSerializer_incremental_deserialize(
if buffered:
nbytes = len(msgpack_data)
buffer: memoryview = serializer.create_deserializer_buffer(nbytes)
buffer[:nbytes] = msgpack_data

def deserialize() -> Any:
consumer = serializer.buffered_incremental_deserialize(buffer)
next(consumer)
buffer[:nbytes] = msgpack_data
try:
consumer.send(nbytes)
except StopIteration as exc:
Expand Down
4 changes: 4 additions & 0 deletions micro_benchmarks/serializers/bench_pickle.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,13 @@

import pytest

from .groups import SerializerGroup

if TYPE_CHECKING:
from pytest_benchmark.fixture import BenchmarkFixture


@pytest.mark.benchmark(group=SerializerGroup.PICKLE_SERIALIZE)
@pytest.mark.parametrize("pickler_optimize", [False, True], ids=lambda p: f"pickler_optimize=={p}")
def bench_PickleSerializer_serialize(
pickler_optimize: bool,
Expand All @@ -23,6 +26,7 @@ def bench_PickleSerializer_serialize(
benchmark(serializer.serialize, json_object)


@pytest.mark.benchmark(group=SerializerGroup.PICKLE_DESERIALIZE)
def bench_PickleSerializer_deserialize(
benchmark: BenchmarkFixture,
json_object: Any,
Expand Down
21 changes: 16 additions & 5 deletions micro_benchmarks/serializers/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,35 +6,46 @@

import pytest

from ..conftest import PAYLOAD_SIZE_LEVEL

SAMPLES = importlib.resources.files(__package__) / "samples"


@pytest.fixture(scope="package", params=["data_1kb.json", "data_10kb.json"])
def pytest_generate_tests(metafunc: pytest.Metafunc) -> None:
if "json_object" in metafunc.fixturenames:
metafunc.parametrize(
"json_object",
[f"data_{payload_size}kb.json" for payload_size in metafunc.config.getoption(PAYLOAD_SIZE_LEVEL)],
indirect=True,
)


@pytest.fixture(scope="package")
def json_object(request: pytest.FixtureRequest) -> Any:
file = SAMPLES / str(request.param)
return json.loads(file.read_text())


@pytest.fixture(scope="module")
@pytest.fixture(scope="package")
def json_data(json_object: Any) -> bytes:
return json.dumps(json_object, indent=None, separators=(",", ":")).encode() + b"\n"


@pytest.fixture(scope="module")
@pytest.fixture(scope="package")
def cbor_data(json_object: Any) -> bytes:
import cbor2

return cbor2.dumps(json_object)


@pytest.fixture(scope="module")
@pytest.fixture(scope="package")
def msgpack_data(json_object: Any) -> bytes:
import msgpack

return msgpack.packb(json_object)


@pytest.fixture(scope="module")
@pytest.fixture(scope="package")
def pickle_data(json_object: Any) -> bytes:
import pickle

Expand Down
25 changes: 25 additions & 0 deletions micro_benchmarks/serializers/groups.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
from __future__ import annotations

import enum


@enum.unique
class SerializerGroup(enum.StrEnum):
@staticmethod
def _generate_next_value_(name: str, start: int, count: int, last_values: list[str]) -> str:
return name.lower().replace("_", "-")

JSON_SERIALIZE = enum.auto()
JSON_DESERIALIZE = enum.auto()

JSON_INCREMENTAL_SERIALIZE = enum.auto()
JSON_INCREMENTAL_DESERIALIZE = enum.auto()

LINE_SERIALIZE = enum.auto()
LINE_DESERIALIZE = enum.auto()

LINE_INCREMENTAL_SERIALIZE = enum.auto()
LINE_INCREMENTAL_DESERIALIZE = enum.auto()

PICKLE_SERIALIZE = enum.auto()
PICKLE_DESERIALIZE = enum.auto()
Loading

0 comments on commit cde3ff2

Please sign in to comment.