mirror of
https://github.com/esphome/esphome.git
synced 2026-10-06 10:56:43 +00:00
[api] Pass the encode cursor by value through the protobuf helpers
The ProtoEncode helpers took the write cursor by reference and a bool force flag. At -Os the compiler outlines most of them, so every call site had to keep pos in a stack slot and pass its address, plus a constant for the flag. The helpers now take the cursor by value and return the advanced cursor, so consecutive calls chain through the return register; forced fields call a _force overload instead of passing a flag. The fixed32 writers use __builtin_memcpy, which stays a builtin under ESP-IDF's -fno-builtin-memcpy, and are outlined on embedded targets so each fixed32 or float field is a short call instead of an inline memcpy call. Non-forced float and fixed32 fields with a single-byte tag share the same writer behind a zero check. Generated encode bodies shrink by 18 percent on an ESP32 IDF proxy build (2360 to 1932 bytes for 27 messages); entity messages gain the most, for example ListEntitiesSensorResponse::encode 190 to 134 bytes and SensorStateResponse::encode 78 to 49 bytes.
This commit is contained in:
@@ -59,7 +59,7 @@ static void verify_mac(uint64_t mac, size_t expected_bytes) {
|
||||
#ifdef ESPHOME_DEBUG_API
|
||||
uint8_t *proto_debug_end_ = api_buf.data() + api_buf.size();
|
||||
#endif
|
||||
ProtoEncode::encode_varint_raw_48bit(pos PROTO_ENCODE_DEBUG_ARG, mac);
|
||||
pos = ProtoEncode::encode_varint_raw_48bit(pos PROTO_ENCODE_DEBUG_ARG, mac);
|
||||
size_t new_len = pos - api_buf.data();
|
||||
|
||||
EXPECT_EQ(new_len, expected_bytes) << "mac=0x" << std::hex << mac << std::dec;
|
||||
|
||||
@@ -15,9 +15,11 @@ import pytest
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).parents[4] / "script" / "api_protobuf"))
|
||||
|
||||
import aioesphomeapi.api_options_pb2 as pb # noqa: E402
|
||||
from api_protobuf import ( # noqa: E402
|
||||
MAX_MESSAGE_ID,
|
||||
_make_ifdef_line,
|
||||
create_field_type_info,
|
||||
get_varint64_ifdef,
|
||||
validate_message_id,
|
||||
)
|
||||
@@ -107,3 +109,80 @@ def test_message_id_at_maximum_is_accepted() -> None:
|
||||
def test_message_id_above_maximum_is_rejected() -> None:
|
||||
with pytest.raises(ValueError, match="exceeds the plaintext"):
|
||||
validate_message_id(MAX_MESSAGE_ID + 1, "TooBigMessage")
|
||||
|
||||
|
||||
def _encode_field(
|
||||
field_type: int, number: int = 1, force: bool = False, repeated: bool = False
|
||||
) -> str:
|
||||
"""Return the encode statement the generator emits for one encode-only field."""
|
||||
field = descriptor_pb2.FieldDescriptorProto(
|
||||
name="value", number=number, type=field_type
|
||||
)
|
||||
if repeated:
|
||||
field.label = descriptor_pb2.FieldDescriptorProto.LABEL_REPEATED
|
||||
if force:
|
||||
field.options.Extensions[pb.force] = True
|
||||
ti = create_field_type_info(field, needs_decode=False, needs_encode=True)
|
||||
return ti.encode_content
|
||||
|
||||
|
||||
SCALAR_TYPES = [
|
||||
descriptor_pb2.FieldDescriptorProto.TYPE_BOOL,
|
||||
descriptor_pb2.FieldDescriptorProto.TYPE_UINT32,
|
||||
descriptor_pb2.FieldDescriptorProto.TYPE_INT32,
|
||||
descriptor_pb2.FieldDescriptorProto.TYPE_UINT64,
|
||||
descriptor_pb2.FieldDescriptorProto.TYPE_INT64,
|
||||
descriptor_pb2.FieldDescriptorProto.TYPE_SINT32,
|
||||
descriptor_pb2.FieldDescriptorProto.TYPE_FLOAT,
|
||||
descriptor_pb2.FieldDescriptorProto.TYPE_FIXED32,
|
||||
descriptor_pb2.FieldDescriptorProto.TYPE_STRING,
|
||||
descriptor_pb2.FieldDescriptorProto.TYPE_BYTES,
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("field_type", SCALAR_TYPES)
|
||||
@pytest.mark.parametrize("force", [False, True])
|
||||
@pytest.mark.parametrize("repeated", [False, True])
|
||||
def test_encode_statements_assign_the_returned_cursor(
|
||||
field_type: int, force: bool, repeated: bool
|
||||
) -> None:
|
||||
"""Every ProtoEncode call must take pos by value and store the returned cursor."""
|
||||
content = _encode_field(field_type, force=force, repeated=repeated)
|
||||
calls = [line.strip() for line in content.splitlines() if "ProtoEncode::" in line]
|
||||
assert calls, content
|
||||
for call in calls:
|
||||
assert call.startswith("pos = ProtoEncode::"), call
|
||||
assert ", true)" not in content, content
|
||||
|
||||
|
||||
@pytest.mark.parametrize("field_type", SCALAR_TYPES)
|
||||
def test_forced_fields_use_the_force_overload_or_raw_writes(field_type: int) -> None:
|
||||
content = _encode_field(field_type, force=True)
|
||||
assert (
|
||||
"_force(" in content
|
||||
or "write_raw_byte(" in content
|
||||
or "write_tag_and_fixed32(" in content
|
||||
), content
|
||||
|
||||
|
||||
FLOAT = descriptor_pb2.FieldDescriptorProto.TYPE_FLOAT
|
||||
FIXED32 = descriptor_pb2.FieldDescriptorProto.TYPE_FIXED32
|
||||
|
||||
|
||||
@pytest.mark.parametrize("field_type", [FLOAT, FIXED32])
|
||||
def test_single_byte_tag_fixed32_shares_the_outlined_writer(field_type: int) -> None:
|
||||
unconditional = _encode_field(field_type, force=True)
|
||||
assert unconditional.count("write_tag_and_fixed32(pos, 13,") == 1, unconditional
|
||||
guarded = _encode_field(field_type, force=False)
|
||||
assert guarded.startswith("if ("), guarded
|
||||
assert "[[likely]]" in guarded
|
||||
assert "write_tag_and_fixed32(pos, 13," in guarded
|
||||
|
||||
|
||||
@pytest.mark.parametrize("field_type", [FLOAT, FIXED32])
|
||||
def test_multi_byte_tag_fixed32_falls_back_to_the_generic_helper(
|
||||
field_type: int,
|
||||
) -> None:
|
||||
content = _encode_field(field_type, number=16)
|
||||
assert "write_tag_and_fixed32" not in content, content
|
||||
assert content.startswith("pos = ProtoEncode::encode_"), content
|
||||
|
||||
Reference in New Issue
Block a user