Skip to content
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
12 changes: 6 additions & 6 deletions cyclonedds/idl/_machinery.py
Original file line number Diff line number Diff line change
Expand Up @@ -663,21 +663,21 @@ def default_initialize(self):

class PlainCdrV2ArrayOfPrimitiveMachine(Machine):
def __init__(self, type, length):
code, self.alignment, size, default = types._type_code_align_size_default_mapping[type]
self.elem_code, self.alignment, size, default = types._type_code_align_size_default_mapping[type]
self.length = length
self.size = size * length
self.code = str(length) + code
self.code = str(length) + self.elem_code
self.default = [default] * length
self.subtype = type

def serialize(self, buffer, value, for_key=False):
assert len(value) == self.length
buffer.align(self.alignment)
buffer.write_multi(self.code, self.size, *value)
buffer.write_sequence(self.elem_code, value)

def deserialize(self, buffer):
buffer.align(self.alignment)
return list(buffer.read_multi(self.code, self.size))
return buffer.read_sequence(self.elem_code, self.length)

def key_scan(self) -> KeyScanner:
return KeyScanner.simple(self.alignment, self.size)
Expand All @@ -703,14 +703,14 @@ def serialize(self, buffer, value, for_key=False):
buffer.write('I', 4, len(value))
if value:
buffer.align(self.alignment)
buffer.write_multi(f"{len(value)}{self.code}", self.size * len(value), *value)
buffer.write_sequence(self.code, value)

def deserialize(self, buffer):
buffer.align(4)
length = buffer.read('I', 4)
if length:
buffer.align(self.alignment)
return list(buffer.read_multi(f"{length}{self.code}", self.size * length))
return buffer.read_sequence(self.code, length)
else:
return []

Expand Down
35 changes: 35 additions & 0 deletions cyclonedds/idl/_support.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
* SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Meta: Some tests are supposed to be run. Can we verify that all tests still pass CI? And add tests if needed? Should we set up a github action to run these tests?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, let me take a look at it

"""

import array as _array
import sys
import struct

Expand Down Expand Up @@ -143,6 +144,40 @@ def read_multi(self, pack: str, size: int) -> Tuple[Any, ...]:
self._pos += size
return v

def write_sequence(self, code: str, values) -> 'Buffer':

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The (de)serialization improvements look good. Could you add comments or docstrings to describe why these functions are necessary and perhaps how they address the byte/list issue?

"""Serialize a homogeneous sequence of primitives in one shot.

Using array.array instead of struct.pack with a length-prefixed format
string (e.g. "100f") avoids building that string on every call and lets
the C array machinery do the copy in bulk. byteswap() is applied when
the buffer's wire endianness differs from the host's native order.
'B' (uint8) is special-cased because array.array('B').tobytes() and
bytes() are equivalent but bytes() is marginally faster for that type.
"""
if code == 'B':
return self.write_bytes(bytes(values))
a = _array.array(code, values)
if (self.endianness == Endianness.Little) != (sys.byteorder == 'little'):

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems this check is redundant, right?

See the class definition:

    self.set_endianness(Endianness.native())

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like even when the endianness is being initialized in the constructor, there are parts of the code that can set it again, based on the CDR encapsulation header in _main.py:

if has_header and buffer.tell() == 0:
            buffer.read('b', 1)
            v = buffer.read('b', 1)
            if (v & 1) > 0:
                buffer.set_endianness(Endianness.Little)
            else:
                buffer.set_endianness(Endianness.Big)

a.byteswap()
return self.write_bytes(a.tobytes())

def read_sequence(self, code: str, length: int) -> list:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have you considered adding unit tests for these new functions? This PR is making performance improvement assertions but lacks a concrete metric showing how it improves the situation. Consider adding a metric to show the read/write performance with & without the write/read_sequence functions.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done! I added some tests, as well as some metrics in the PR description

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for adding these comments and tests.

"""Deserialize a homogeneous sequence of primitives in one shot.

Mirrors write_sequence: frombytes() fills the array directly from the
buffer slice without per-element struct.unpack calls, then byteswap()
fixes up the byte order when wire and host endianness differ.
"""
if code == 'B':
return list(self.read_bytes(length))
a = _array.array(code)
nbytes = a.itemsize * length
a.frombytes(self._bytes[self._pos:self._pos + nbytes])
self._pos += nbytes
if (self.endianness == Endianness.Little) != (sys.byteorder == 'little'):
a.byteswap()
return a.tolist()

def asbytes(self) -> bytes:
return bytes(self._bytes[0:self._pos])

Expand Down
112 changes: 112 additions & 0 deletions tests/test_encoding_machinery.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import array
import struct
import pytest

from dataclasses import dataclass
Expand Down Expand Up @@ -212,5 +214,115 @@ def test_all_machine_serializers():
assert b.asbytes() == b"\x04\x00\x00\x00\x01\x00\x34\x12\x02\x00\x00\x00\x00\x77"


# All array.array-compatible primitive typecodes used by the IDL machinery.
_SEQUENCE_CASES = [
('b', [0, -1, 127, -128]),
('B', [0, 1, 127, 255]),
('h', [0, -1, 32767, -32768]),
('H', [0, 1, 32767, 65535]),
('i', [0, -1, 2**31 - 1, -(2**31)]),
('I', [0, 1, 2**31 - 1, 2**32 - 1]),
('q', [0, -1, 2**63 - 1, -(2**63)]),
('Q', [0, 1, 2**63 - 1, 2**64 - 1]),
('f', [0.0, 1.5, -1.5, 3.14]),
('d', [0.0, 1.5, -1.5, 3.14159265358979]),
]


@pytest.mark.parametrize("code,values", _SEQUENCE_CASES)
def test_write_read_sequence_roundtrip_native(code, values):
"""write_sequence followed by read_sequence returns the original values."""
b = Buffer()
b.write_sequence(code, values)
b.seek(0)
result = b.read_sequence(code, len(values))
if code in ('f',):
assert result == pytest.approx(values, rel=1e-6)
else:
assert result == values


@pytest.mark.parametrize("code,values", _SEQUENCE_CASES)
def test_write_read_sequence_roundtrip_non_native(code, values):
"""Round-trip is correct when buffer endianness is opposite to the host."""
non_native = Endianness.Big if Endianness.native() == Endianness.Little else Endianness.Little
b = Buffer()
b.set_endianness(non_native)
b.write_sequence(code, values)
b.seek(0)
result = b.read_sequence(code, len(values))
if code in ('f',):
assert result == pytest.approx(values, rel=1e-6)
else:
assert result == values


@pytest.mark.parametrize("code,values", _SEQUENCE_CASES)
def test_write_sequence_matches_struct_little_endian(code, values):
"""write_sequence produces the same bytes as struct.pack with '<' prefix."""
b = Buffer()
b.set_endianness(Endianness.Little)
b.write_sequence(code, values)
expected = struct.pack(f"<{len(values)}{code}", *values)
assert b.asbytes() == expected


@pytest.mark.parametrize("code,values", _SEQUENCE_CASES)
def test_write_sequence_matches_struct_big_endian(code, values):
"""write_sequence produces the same bytes as struct.pack with '>' prefix."""
b = Buffer()
b.set_endianness(Endianness.Big)
b.write_sequence(code, values)
expected = struct.pack(f">{len(values)}{code}", *values)
assert b.asbytes() == expected


def test_write_sequence_uint8_special_case():
"""'B' (uint8) fast path produces identical bytes to the generic array path."""
values = list(range(256))
b_special = Buffer()
b_special.write_sequence('B', values)

b_generic = Buffer()
a = array.array('B', values)
b_generic.write_bytes(a.tobytes())

assert b_special.asbytes() == b_generic.asbytes()


@pytest.mark.parametrize("idl_type,code,values", [
(tp.uint16, 'H', [0x0102, 0x0304, 0x0506]),
(tp.int32, 'i', [1, -1, 2**16]),
(tp.float64,'d', [1.0, -1.0, 0.5]),
])
def test_plain_cdr_v2_array_roundtrip(idl_type, code, values):
"""PlainCdrV2ArrayOfPrimitiveMachine serializes and deserializes correctly."""
m = mc.PlainCdrV2ArrayOfPrimitiveMachine(idl_type, len(values))
b = Buffer()
b.set_endianness(Endianness.Little)
m.serialize(b, values)
b.seek(0)
result = m.deserialize(b)
if code == 'd':
assert result == pytest.approx(values)
else:
assert result == values


@pytest.mark.parametrize("idl_type,code,values", [
(tp.uint16, 'H', [0x0102, 0x0304, 0x0506]),
(tp.int32, 'i', [1, -1, 2**16]),
(tp.float64,'d', [1.0, -1.0, 0.5]),
])
def test_plain_cdr_v2_sequence_roundtrip(idl_type, code, values):
"""PlainCdrV2SequenceOfPrimitiveMachine serializes and deserializes correctly."""
m = mc.PlainCdrV2SequenceOfPrimitiveMachine(idl_type)
b = Buffer()
b.set_endianness(Endianness.Little)
m.serialize(b, values)
b.seek(0)
result = m.deserialize(b)
if code == 'd':
assert result == pytest.approx(values)
else:
assert result == values
Loading