[api] Add max_value proto option for constant-size varint codegen

Add a max_value field option to api_options.proto that tells the code
generator the maximum value a field can have. When max_value < 128,
the generated calculate_size() uses constant arithmetic instead of
calling varint size functions, and encode() uses direct byte writes
instead of varint encoding.

Also optimize FixedArrayBytesType: when fixed_array_size < 128, the
length varint is always 1 byte, so calculate_size() uses constant
arithmetic and encode() uses write_raw_byte for the length.

Applied to BluetoothLERawAdvertisement.address_type (max_value=4).

Measured on ESP32 (upstairsdesk89proxy):
- BluetoothLERawAdvertisement::calculate_size: 88 → 71 bytes (-19%)
- BluetoothLERawAdvertisement::encode: 199 → 179 bytes (-10%)
- Total BLE proxy hot path: 1807 → 1770 bytes (-37 bytes)
This commit is contained in:
J. Nick Koston
2026-04-03 12:21:37 -10:00
parent f8f65c1a7b
commit e35aa729f3
4 changed files with 54 additions and 7 deletions
+1 -1
View File
@@ -1606,7 +1606,7 @@ message BluetoothLEAdvertisementResponse {
message BluetoothLERawAdvertisement {
uint64 address = 1 [(force) = true];
sint32 rssi = 2 [(force) = true];
uint32 address_type = 3;
uint32 address_type = 3 [(max_value) = 4];
bytes data = 4 [(fixed_array_size) = 62, (force) = true];
}
+6
View File
@@ -96,4 +96,10 @@ extend google.protobuf.FieldOptions {
// variant of the calc_ method. Use on fields that are almost always non-default
// to eliminate dead branches on hot paths.
optional bool force = 50016 [default=false];
// max_value: Maximum value a field can have.
// When max_value < 128, the code generator emits constant-size calculations
// and direct byte writes instead of varint branching, since the encoded varint
// is guaranteed to be 1 byte.
optional uint32 max_value = 50017;
}
+3 -3
View File
@@ -2255,15 +2255,15 @@ void BluetoothLERawAdvertisement::encode(ProtoWriteBuffer &buffer) const {
buffer.encode_varint_raw(encode_zigzag32(this->rssi));
buffer.encode_uint32(3, this->address_type);
buffer.write_raw_byte(34);
buffer.encode_varint_raw(this->data_len);
buffer.write_raw_byte(static_cast<uint8_t>(this->data_len));
buffer.encode_raw(this->data, this->data_len);
}
uint32_t BluetoothLERawAdvertisement::calculate_size() const {
uint32_t size = 0;
size += ProtoSize::calc_uint64_force(1, this->address);
size += ProtoSize::calc_sint32_force(1, this->rssi);
size += ProtoSize::calc_uint32(1, this->address_type);
size += ProtoSize::calc_length_force(1, this->data_len);
size += this->address_type ? 2 : 0;
size += 2 + this->data_len;
return size;
}
void BluetoothLERawAdvertisementsResponse::encode(ProtoWriteBuffer &buffer) const {
+44 -3
View File
@@ -156,6 +156,11 @@ class TypeInfo(ABC):
"""Check if this field should always be encoded (skip zero/empty check)."""
return get_field_opt(self._field, pb.force, False)
@property
def max_value(self) -> int | None:
"""Get the max_value option for this field, or None if not set."""
return get_field_opt(self._field, pb.max_value, None)
@property
def wire_type(self) -> WireType:
"""Get the wire type for the field."""
@@ -240,32 +245,55 @@ class TypeInfo(ABC):
Returns the raw encode string if the tag is a single byte and the
encode_func has a known raw equivalent, or None otherwise.
When max_value < 128, uses direct byte write instead of varint encoding.
"""
if not self.force:
return None
tag = self.calculate_tag()
if tag >= 128:
return None
# When max_value < 128, varint is always 1 byte - use direct byte write
max_val = self.max_value
if (
max_val is not None
and max_val < 128
and self.encode_func
in (
"encode_uint32",
"encode_uint64",
)
):
return (
f"buffer.write_raw_byte({tag});\n"
f"buffer.write_raw_byte(static_cast<uint8_t>({value_expr}));"
)
raw_expr = self.RAW_ENCODE_MAP.get(self.encode_func)
if raw_expr is None:
return None
return f"buffer.write_raw_byte({tag});\n{raw_expr.format(value=value_expr)}"
def _encode_bytes_with_precomputed_tag(
self, data_expr: str, len_expr: str
self, data_expr: str, len_expr: str, max_len: int | None = None
) -> str | None:
"""Try to emit a precomputed-tag encode for a forced bytes/string field.
Returns the raw encode string if the tag is a single byte, or None.
When max_len < 128, uses direct byte write for the length varint.
"""
if not self.force:
return None
tag = self.calculate_tag()
if tag >= 128:
return None
# When max_len < 128, length varint is always 1 byte
len_encode = (
f"buffer.write_raw_byte(static_cast<uint8_t>({len_expr}));"
if max_len is not None and max_len < 128
else f"buffer.encode_varint_raw({len_expr});"
)
return (
f"buffer.write_raw_byte({tag});\n"
f"buffer.encode_varint_raw({len_expr});\n"
f"{len_encode}\n"
f"buffer.encode_raw({data_expr}, {len_expr});"
)
@@ -1191,8 +1219,9 @@ class FixedArrayBytesType(TypeInfo):
@property
def encode_content(self) -> str:
max_len = self.array_size if isinstance(self.array_size, int) else None
if result := self._encode_bytes_with_precomputed_tag(
f"this->{self.field_name}", f"this->{self.field_name}_len"
f"this->{self.field_name}", f"this->{self.field_name}_len", max_len=max_len
):
return result
if self.force:
@@ -1214,6 +1243,12 @@ class FixedArrayBytesType(TypeInfo):
length_field = f"this->{self.field_name}_len"
field_id_size = self.calculate_field_id_size()
# When array_size < 128, length varint is always 1 byte
if isinstance(self.array_size, int) and self.array_size < 128:
if force:
return f"size += {field_id_size + 1} + {length_field};"
return f"size += {length_field} ? {field_id_size + 1} + {length_field} : 0;"
if force:
# For repeated fields, always calculate size (no zero check)
return f"size += ProtoSize::calc_length_force({field_id_size}, {length_field});"
@@ -1245,6 +1280,12 @@ class UInt32Type(TypeInfo):
return o
def get_size_calculation(self, name: str, force: bool = False) -> str:
max_val = self.max_value
if max_val is not None and max_val < 128:
field_id_size = self.calculate_field_id_size()
if force:
return f"size += {field_id_size + 1};"
return f"size += {name} ? {field_id_size + 1} : 0;"
return self._get_simple_size_calculation(name, force, "uint32")
def get_estimated_size(self) -> int: