Merge remote-tracking branch 'origin/api-sint32-short-varint' into integration

This commit is contained in:
J. Nick Koston
2026-03-28 21:43:28 -10:00
8 changed files with 246 additions and 36 deletions
+1 -1
View File
@@ -2248,7 +2248,7 @@ void BluetoothLERawAdvertisement::encode(ProtoWriteBuffer &buffer) const {
buffer.write_raw_byte(8);
buffer.encode_varint_raw_64(this->address);
buffer.write_raw_byte(16);
buffer.encode_varint_raw(encode_zigzag32(this->rssi));
buffer.encode_varint_raw_short(encode_zigzag32(this->rssi));
buffer.encode_uint32(3, this->address_type);
buffer.write_raw_byte(34);
buffer.encode_varint_raw(this->data_len);
+8 -4
View File
@@ -11,13 +11,17 @@ static const char *const TAG = "api.proto";
uint32_t ProtoSize::varint_slow(uint32_t value) { return varint_wide(value); }
void ProtoWriteBuffer::encode_varint_raw_slow_(uint32_t value) {
// Use __restrict__ so the compiler knows pos doesn't alias this->
// and can keep it in a register across the loop.
uint8_t *__restrict__ pos = this->pos_;
do {
this->debug_check_bounds_(1);
*this->pos_++ = static_cast<uint8_t>(value | 0x80);
this->sync_debug_check_bounds_(pos, 1);
*pos++ = static_cast<uint8_t>(value | 0x80);
value >>= 7;
} while (value > 0x7F);
this->debug_check_bounds_(1);
*this->pos_++ = static_cast<uint8_t>(value);
this->sync_debug_check_bounds_(pos, 1);
*pos++ = static_cast<uint8_t>(value);
this->pos_ = pos;
}
ProtoVarIntResult ProtoVarInt::parse_slow(const uint8_t *buffer, uint32_t len) {
+90 -30
View File
@@ -213,19 +213,45 @@ class ProtoWriteBuffer {
inline void ESPHOME_ALWAYS_INLINE encode_varint_raw(uint32_t value) {
if (value < 128) [[likely]] {
this->debug_check_bounds_(1);
*this->pos_++ = static_cast<uint8_t>(value);
uint8_t *__restrict__ pos = this->pos_;
*pos++ = static_cast<uint8_t>(value);
this->pos_ = pos;
return;
}
this->encode_varint_raw_slow_(value);
}
/// Encode a varint that is expected to be 1-2 bytes (e.g. zigzag RSSI, small lengths).
/// Inlines both the 1-byte and 2-byte paths; falls back to slow path for 3+ bytes.
inline void ESPHOME_ALWAYS_INLINE encode_varint_raw_short(uint32_t value) {
if (value < 128) [[likely]] {
this->debug_check_bounds_(1);
uint8_t *__restrict__ pos = this->pos_;
*pos++ = static_cast<uint8_t>(value);
this->pos_ = pos;
return;
}
if (value < 16384) [[likely]] {
this->debug_check_bounds_(2);
uint8_t *__restrict__ pos = this->pos_;
*pos++ = static_cast<uint8_t>(value | 0x80);
*pos++ = static_cast<uint8_t>(value >> 7);
this->pos_ = pos;
return;
}
this->encode_varint_raw_slow_(value);
}
void encode_varint_raw_64(uint64_t value) {
// Use __restrict__ so the compiler knows pos doesn't alias this->
// and can keep it in a register across the loop.
uint8_t *__restrict__ pos = this->pos_;
while (value > 0x7F) {
this->debug_check_bounds_(1);
*this->pos_++ = static_cast<uint8_t>(value | 0x80);
this->sync_debug_check_bounds_(pos, 1);
*pos++ = static_cast<uint8_t>(value | 0x80);
value >>= 7;
}
this->debug_check_bounds_(1);
*this->pos_++ = static_cast<uint8_t>(value);
this->sync_debug_check_bounds_(pos, 1);
*pos++ = static_cast<uint8_t>(value);
this->pos_ = pos;
}
/**
* Encode a field key (tag/wire type combination).
@@ -243,40 +269,52 @@ class ProtoWriteBuffer {
/// Write a single precomputed tag byte. Tag must be < 128.
inline void write_raw_byte(uint8_t b) ESPHOME_ALWAYS_INLINE {
this->debug_check_bounds_(1);
*this->pos_++ = b;
uint8_t *__restrict__ pos = this->pos_;
*pos++ = b;
this->pos_ = pos;
}
/// Write raw bytes to the buffer (no tag, no length prefix).
inline void encode_raw(const void *data, size_t len) ESPHOME_ALWAYS_INLINE {
this->debug_check_bounds_(len);
std::memcpy(this->pos_, data, len);
this->pos_ += len;
uint8_t *__restrict__ pos = this->pos_;
std::memcpy(pos, data, len);
this->pos_ = pos + len;
}
/// Write a precomputed tag byte + 32-bit value in one operation.
/// Tag must be a single-byte varint (< 128). No zero check.
inline void write_tag_and_fixed32(uint8_t tag, uint32_t value) ESPHOME_ALWAYS_INLINE {
this->debug_check_bounds_(5);
this->pos_[0] = tag;
uint8_t *__restrict__ pos = this->pos_;
pos[0] = tag;
#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__
std::memcpy(this->pos_ + 1, &value, 4);
std::memcpy(pos + 1, &value, 4);
#else
this->pos_[1] = static_cast<uint8_t>(value & 0xFF);
this->pos_[2] = static_cast<uint8_t>((value >> 8) & 0xFF);
this->pos_[3] = static_cast<uint8_t>((value >> 16) & 0xFF);
this->pos_[4] = static_cast<uint8_t>((value >> 24) & 0xFF);
pos[1] = static_cast<uint8_t>(value & 0xFF);
pos[2] = static_cast<uint8_t>((value >> 8) & 0xFF);
pos[3] = static_cast<uint8_t>((value >> 16) & 0xFF);
pos[4] = static_cast<uint8_t>((value >> 24) & 0xFF);
#endif
this->pos_ += 5;
this->pos_ = pos + 5;
}
void encode_string(uint32_t field_id, const char *string, size_t len, bool force = false) {
if (len == 0 && !force)
return;
this->encode_field_raw(field_id, 2); // type 2: Length-delimited string
this->encode_varint_raw(len);
// Direct memcpy into pre-sized buffer — avoids push_back() per-byte capacity checks
// and vector::insert() iterator overhead. ~10-11x faster for 16-32 byte strings.
this->debug_check_bounds_(len);
std::memcpy(this->pos_, string, len);
this->pos_ += len;
// Inline the length varint + memcpy under a single __restrict__ pos
// to avoid a store-load pair between encode_varint_raw and encode_raw.
uint8_t *__restrict__ pos = this->pos_;
if (len < 128) [[likely]] {
this->debug_check_bounds_(1 + len);
*pos++ = static_cast<uint8_t>(len);
} else {
// Length >= 128: use slow path for the length varint, then re-hoist pos
this->encode_varint_raw_slow_(len);
pos = this->pos_;
this->debug_check_bounds_(len);
}
std::memcpy(pos, string, len);
this->pos_ = pos + len;
}
void encode_string(uint32_t field_id, const std::string &value, bool force = false) {
this->encode_string(field_id, value.data(), value.size(), force);
@@ -304,7 +342,9 @@ class ProtoWriteBuffer {
return;
this->encode_field_raw(field_id, 0); // type 0: Varint - bool
this->debug_check_bounds_(1);
*this->pos_++ = value ? 0x01 : 0x00;
uint8_t *__restrict__ pos = this->pos_;
*pos++ = value ? 0x01 : 0x00;
this->pos_ = pos;
}
void encode_fixed32(uint32_t field_id, uint32_t value, bool force = false) {
if (value == 0 && !force)
@@ -312,15 +352,17 @@ class ProtoWriteBuffer {
this->encode_field_raw(field_id, 5); // type 5: 32-bit fixed32
this->debug_check_bounds_(4);
uint8_t *__restrict__ pos = this->pos_;
#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__
// Protobuf fixed32 is little-endian, so direct copy works
std::memcpy(this->pos_, &value, 4);
this->pos_ += 4;
std::memcpy(pos, &value, 4);
this->pos_ = pos + 4;
#else
*this->pos_++ = (value >> 0) & 0xFF;
*this->pos_++ = (value >> 8) & 0xFF;
*this->pos_++ = (value >> 16) & 0xFF;
*this->pos_++ = (value >> 24) & 0xFF;
*pos++ = (value >> 0) & 0xFF;
*pos++ = (value >> 8) & 0xFF;
*pos++ = (value >> 16) & 0xFF;
*pos++ = (value >> 24) & 0xFF;
this->pos_ = pos;
#endif
}
// NOTE: Wire type 1 (64-bit fixed: double, fixed64, sfixed64) is intentionally
@@ -372,9 +414,16 @@ class ProtoWriteBuffer {
#ifdef ESPHOME_DEBUG_API
void debug_check_bounds_(size_t bytes, const char *caller = __builtin_FUNCTION());
/// Sync pos_ from a local pointer, then check bounds. For use in __restrict__ loops
/// where pos_ is hoisted into a local but debug checks need the current position.
void sync_debug_check_bounds_(uint8_t *pos, size_t bytes, const char *caller = __builtin_FUNCTION()) {
this->pos_ = pos;
this->debug_check_bounds_(bytes, caller);
}
void debug_check_encode_size_(uint32_t field_id, uint32_t expected, ptrdiff_t actual);
#else
void debug_check_bounds_([[maybe_unused]] size_t bytes) {}
void sync_debug_check_bounds_(uint8_t *pos, [[maybe_unused]] size_t bytes) {}
#endif
APIBuffer *buffer_;
@@ -537,6 +586,17 @@ class ProtoSize {
return varint_wide(value);
return varint_slow(value);
}
/// Size of a varint expected to be 1-2 bytes (e.g. zigzag RSSI, small lengths).
/// Inlines both checks; falls back to slow path for 3+ bytes.
static constexpr inline uint32_t ESPHOME_ALWAYS_INLINE varint_short(uint32_t value) {
if (value < VARINT_THRESHOLD_1_BYTE) [[likely]]
return 1;
if (value < VARINT_THRESHOLD_2_BYTE) [[likely]]
return 2;
if (__builtin_is_constant_evaluated())
return varint_wide(value);
return varint_slow(value);
}
private:
// Slow path for varint >= 128, outlined to keep fast path small
@@ -651,10 +711,10 @@ class ProtoSize {
return value ? field_id_size + 4 : 0;
}
static constexpr uint32_t calc_sint32(uint32_t field_id_size, int32_t value) {
return value ? field_id_size + varint(encode_zigzag32(value)) : 0;
return value ? field_id_size + varint_short(encode_zigzag32(value)) : 0;
}
static constexpr inline uint32_t ESPHOME_ALWAYS_INLINE calc_sint32_force(uint32_t field_id_size, int32_t value) {
return field_id_size + varint(encode_zigzag32(value));
return field_id_size + varint_short(encode_zigzag32(value));
}
static constexpr uint32_t calc_int64(uint32_t field_id_size, int64_t value) {
return value ? field_id_size + varint(value) : 0;
+1 -1
View File
@@ -229,7 +229,7 @@ class TypeInfo(ABC):
RAW_ENCODE_MAP: dict[str, str] = {
"encode_uint32": "buffer.encode_varint_raw({value});",
"encode_uint64": "buffer.encode_varint_raw_64({value});",
"encode_sint32": "buffer.encode_varint_raw(encode_zigzag32({value}));",
"encode_sint32": "buffer.encode_varint_raw_short(encode_zigzag32({value}));",
"encode_sint64": "buffer.encode_varint_raw_64(encode_zigzag64({value}));",
"encode_int64": "buffer.encode_varint_raw_64(static_cast<uint64_t>({value}));",
"encode_bool": "buffer.write_raw_byte({value} ? 0x01 : 0x00);",
+5
View File
@@ -21,6 +21,10 @@ BENCHMARKS_DIR: Path = Path(root_path) / "tests" / "benchmarks" / "components"
# Path to /tests/benchmarks/core (always included, not a component)
CORE_BENCHMARKS_DIR: Path = Path(root_path) / "tests" / "benchmarks" / "core"
# Stub headers for ESP32-only components (e.g. bluetooth_proxy) that
# allow benchmarks to compile on the host platform.
STUBS_DIR: Path = Path(root_path) / "tests" / "benchmarks" / "stubs"
PLATFORMIO_OPTIONS = {
"build_unflags": [
"-Os", # remove default size-opt
@@ -29,6 +33,7 @@ PLATFORMIO_OPTIONS = {
"-O2", # optimize for speed (CodSpeed recommends RelWithDebInfo)
"-g", # debug symbols for profiling
"-DUSE_BENCHMARK", # disable WarnIfComponentBlockingGuard in finish()
f"-I{STUBS_DIR}", # stub headers for ESP32-only components
],
# Use deep+ LDF mode to ensure PlatformIO detects the benchmark
# library dependency from nested includes.
@@ -1,3 +1,4 @@
import esphome.codegen as cg
from tests.testing_helpers import ComponentManifestOverride
@@ -5,3 +6,16 @@ def override_manifest(manifest: ComponentManifestOverride) -> None:
# api must run its to_code to define USE_API, USE_API_PLAINTEXT,
# and add the noise-c library dependency.
manifest.enable_codegen()
original_to_code = manifest.to_code
async def to_code(config):
await original_to_code(config)
# Enable BLE proto message types for benchmarks. The real
# bluetooth_proxy component is ESP32-only; a lightweight stub
# header in tests/benchmarks/stubs/ satisfies the include.
cg.add_define("USE_BLUETOOTH_PROXY")
cg.add_define("BLUETOOTH_PROXY_MAX_CONNECTIONS", 3)
cg.add_define("BLUETOOTH_PROXY_ADVERTISEMENT_BATCH_SIZE", 16)
manifest.to_code = to_code
@@ -295,4 +295,93 @@ static void CalcAndEncode_DeviceInfoResponse_Fresh(benchmark::State &state) {
}
BENCHMARK(CalcAndEncode_DeviceInfoResponse_Fresh);
// --- BluetoothLERawAdvertisementsResponse (12 adverts, highest-volume BLE message) ---
#ifdef USE_BLUETOOTH_PROXY
static BluetoothLERawAdvertisementsResponse make_ble_raw_advs_12() {
static const uint8_t fake_adv_data[] = {
0x02, 0x01, 0x06, 0x03, 0x03, 0x9F, 0xFE, 0x17, 0x16, 0x9F, 0xFE, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
};
BluetoothLERawAdvertisementsResponse msg;
msg.advertisements_len = 12;
for (int i = 0; i < 12; i++) {
auto &adv = msg.advertisements[i];
adv.address = 0xAABBCCDD0000ULL + i;
adv.rssi = -60 - i;
adv.address_type = 1;
memcpy(adv.data, fake_adv_data, sizeof(fake_adv_data));
adv.data_len = sizeof(fake_adv_data);
}
return msg;
}
static void CalculateSize_BLERawAdvs12(benchmark::State &state) {
auto msg = make_ble_raw_advs_12();
for (auto _ : state) {
uint32_t result = 0;
for (int i = 0; i < kInnerIterations; i++) {
result += msg.calculate_size();
}
benchmark::DoNotOptimize(result);
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(CalculateSize_BLERawAdvs12);
static void Encode_BLERawAdvs12(benchmark::State &state) {
auto msg = make_ble_raw_advs_12();
APIBuffer buffer;
uint32_t total_size = msg.calculate_size();
buffer.resize(total_size);
for (auto _ : state) {
for (int i = 0; i < kInnerIterations; i++) {
ProtoWriteBuffer writer(&buffer, 0);
msg.encode(writer);
}
benchmark::DoNotOptimize(buffer.data());
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(Encode_BLERawAdvs12);
static void CalcAndEncode_BLERawAdvs12(benchmark::State &state) {
auto msg = make_ble_raw_advs_12();
APIBuffer buffer;
for (auto _ : state) {
for (int i = 0; i < kInnerIterations; i++) {
uint32_t size = msg.calculate_size();
buffer.resize(size);
ProtoWriteBuffer writer(&buffer, 0);
msg.encode(writer);
}
benchmark::DoNotOptimize(buffer.data());
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(CalcAndEncode_BLERawAdvs12);
static void CalcAndEncode_BLERawAdvs12_Fresh(benchmark::State &state) {
auto msg = make_ble_raw_advs_12();
for (auto _ : state) {
for (int i = 0; i < kInnerIterations; i++) {
APIBuffer buffer;
uint32_t size = msg.calculate_size();
buffer.resize(size);
ProtoWriteBuffer writer(&buffer, 0);
msg.encode(writer);
benchmark::DoNotOptimize(buffer.data());
}
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(CalcAndEncode_BLERawAdvs12_Fresh);
#endif // USE_BLUETOOTH_PROXY
} // namespace esphome::api::benchmarks
@@ -0,0 +1,38 @@
// Stub for benchmark builds — provides the minimal interface that
// api_connection.cpp needs when USE_BLUETOOTH_PROXY is defined,
// without pulling in ESP32 BLE dependencies.
#pragma once
#include "esphome/components/api/api_pb2.h"
namespace esphome {
namespace api {
class APIConnection;
} // namespace api
namespace bluetooth_proxy {
class BluetoothProxy {
public:
api::APIConnection *get_api_connection() const { return nullptr; }
void subscribe_api_connection(api::APIConnection *conn, uint32_t flags) {}
void unsubscribe_api_connection(api::APIConnection *conn) {}
void bluetooth_device_request(const api::BluetoothDeviceRequest &msg) {}
void bluetooth_gatt_read(const api::BluetoothGATTReadRequest &msg) {}
void bluetooth_gatt_write(const api::BluetoothGATTWriteRequest &msg) {}
void bluetooth_gatt_read_descriptor(const api::BluetoothGATTReadDescriptorRequest &msg) {}
void bluetooth_gatt_write_descriptor(const api::BluetoothGATTWriteDescriptorRequest &msg) {}
void bluetooth_gatt_send_services(const api::BluetoothGATTGetServicesRequest &msg) {}
void bluetooth_gatt_notify(const api::BluetoothGATTNotifyRequest &msg) {}
void send_connections_free(api::APIConnection *conn) {}
void bluetooth_scanner_set_mode(bool active) {}
void bluetooth_set_connection_params(const api::BluetoothSetConnectionParamsRequest &msg) {}
uint32_t get_feature_flags() const { return 0; }
void get_bluetooth_mac_address_pretty(char *buf) const { buf[0] = '\0'; }
};
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
extern BluetoothProxy *global_bluetooth_proxy;
} // namespace bluetooth_proxy
} // namespace esphome