From 4c027e87ba65730409fa1ed6e232f7892835732c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 29 Apr 2026 21:07:39 -0500 Subject: [PATCH 1/7] [api] Add encode/decode benchmarks for Z-Wave, IR/RF, and serial proxy messages Mirrors the existing BluetoothLERawAdvertisementsResponse benchmarks for the remaining proxy message families: ZWaveProxyFrame/ZWaveProxyRequest, SerialProxyDataReceived/SerialProxyWriteRequest, and InfraredRFReceiveEvent/InfraredRFTransmitRawTimingsRequest. Adds minimal stub headers under tests/benchmarks/stubs/ for the zwave_proxy, infrared, radio_frequency, and serial_proxy components so api_connection.cpp compiles without dragging in their UART/RMT/BLE hardware dependencies. --- tests/benchmarks/components/api/__init__.py | 14 +- .../components/api/bench_proto_decode.cpp | 175 ++++++++++++++++++ .../components/api/bench_proto_encode.cpp | 124 +++++++++++++ .../esphome/components/infrared/infrared.h | 45 +++++ .../radio_frequency/radio_frequency.h | 51 +++++ .../components/serial_proxy/serial_proxy.h | 46 +++++ .../components/zwave_proxy/zwave_proxy.h | 29 +++ 7 files changed, 481 insertions(+), 3 deletions(-) create mode 100644 tests/benchmarks/stubs/esphome/components/infrared/infrared.h create mode 100644 tests/benchmarks/stubs/esphome/components/radio_frequency/radio_frequency.h create mode 100644 tests/benchmarks/stubs/esphome/components/serial_proxy/serial_proxy.h create mode 100644 tests/benchmarks/stubs/esphome/components/zwave_proxy/zwave_proxy.h diff --git a/tests/benchmarks/components/api/__init__.py b/tests/benchmarks/components/api/__init__.py index eb86492964..0d02e0b054 100644 --- a/tests/benchmarks/components/api/__init__.py +++ b/tests/benchmarks/components/api/__init__.py @@ -11,11 +11,19 @@ def override_manifest(manifest: ComponentManifestOverride) -> None: 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. + # Enable proxy proto message types for benchmarks. The real + # components have hardware dependencies (BLE/UART/RMT); lightweight + # stub headers in tests/benchmarks/stubs/ satisfy the includes. cg.add_define("USE_BLUETOOTH_PROXY") cg.add_define("BLUETOOTH_PROXY_MAX_CONNECTIONS", 3) cg.add_define("BLUETOOTH_PROXY_ADVERTISEMENT_BATCH_SIZE", 16) + cg.add_define("USE_ZWAVE_PROXY") + cg.add_define("USE_INFRARED") + cg.add_define("USE_IR_RF") + cg.add_define("USE_RADIO_FREQUENCY") + cg.add_define("USE_SERIAL_PROXY") + cg.add_define("SERIAL_PROXY_COUNT", 0) + cg.add_define("ESPHOME_ENTITY_INFRARED_COUNT", 0) + cg.add_define("ESPHOME_ENTITY_RADIO_FREQUENCY_COUNT", 0) manifest.to_code = to_code diff --git a/tests/benchmarks/components/api/bench_proto_decode.cpp b/tests/benchmarks/components/api/bench_proto_decode.cpp index 961c629f2a..54ba00efce 100644 --- a/tests/benchmarks/components/api/bench_proto_decode.cpp +++ b/tests/benchmarks/components/api/bench_proto_decode.cpp @@ -77,6 +77,181 @@ static void Decode_SwitchCommandRequest(benchmark::State &state) { } BENCHMARK(Decode_SwitchCommandRequest); +// --- ZWaveProxyFrame decode (~16-byte data buffer) --- + +#ifdef USE_ZWAVE_PROXY + +static void Decode_ZWaveProxyFrame(benchmark::State &state) { + static const uint8_t frame_data[] = {0x01, 0x09, 0x00, 0x13, 0x01, 0x02, 0x00, 0x00, + 0x25, 0x00, 0x05, 0xC4, 0x00, 0x00, 0x00, 0x00}; + ZWaveProxyFrame source; + source.data = frame_data; + source.data_len = sizeof(frame_data); + auto encoded = encode_message(source); + auto *data = encoded.data(); + auto size = encoded.size(); + benchmark::DoNotOptimize(data); + benchmark::DoNotOptimize(size); + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + ZWaveProxyFrame msg; + escape(&msg); + msg.decode(data, size); + escape(&msg); + } + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(Decode_ZWaveProxyFrame); + +static void Decode_ZWaveProxyRequest(benchmark::State &state) { + static const uint8_t req_data[] = {0xDE, 0xAD, 0xBE, 0xEF}; + ZWaveProxyRequest source; + source.type = enums::ZWAVE_PROXY_REQUEST_TYPE_HOME_ID_CHANGE; + source.data = req_data; + source.data_len = sizeof(req_data); + auto encoded = encode_message(source); + auto *data = encoded.data(); + auto size = encoded.size(); + benchmark::DoNotOptimize(data); + benchmark::DoNotOptimize(size); + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + ZWaveProxyRequest msg; + escape(&msg); + msg.decode(data, size); + escape(&msg); + } + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(Decode_ZWaveProxyRequest); + +#endif // USE_ZWAVE_PROXY + +// --- SerialProxyWriteRequest decode (instance + 64-byte data) --- +// +// SerialProxyWriteRequest is decode-only (SOURCE_CLIENT), so we encode via +// SerialProxyDataReceived which has identical wire format +// (uint32 instance = 1; bytes data = 2;). + +#ifdef USE_SERIAL_PROXY + +static void Decode_SerialProxyWriteRequest(benchmark::State &state) { + static constexpr size_t kPayloadSize = 64; + static uint8_t payload[kPayloadSize]; + for (size_t i = 0; i < kPayloadSize; i++) + payload[i] = static_cast(i); + + SerialProxyDataReceived source; + source.instance = 0; + source.set_data(payload, kPayloadSize); + auto encoded = encode_message(source); + auto *data = encoded.data(); + auto size = encoded.size(); + benchmark::DoNotOptimize(data); + benchmark::DoNotOptimize(size); + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + SerialProxyWriteRequest msg; + escape(&msg); + msg.decode(data, size); + escape(&msg); + } + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(Decode_SerialProxyWriteRequest); + +#endif // USE_SERIAL_PROXY + +// --- InfraredRFTransmitRawTimingsRequest decode (100 zigzag-encoded timings) --- +// +// Hand-built wire bytes since this message is decode-only and has no sister +// type with an identical layout. Wire format: +// field 2 (key, fixed32): tag=0x15, 4 LE bytes +// field 3 (carrier_frequency): tag=0x18, varint +// field 4 (repeat_count): tag=0x20, varint +// field 5 (timings, packed sint32): tag=0x2A, length varint, packed payload +// field 6 (modulation): tag=0x30, varint + +#if defined(USE_IR_RF) || defined(USE_RADIO_FREQUENCY) + +static APIBuffer build_infrared_rf_transmit_wire() { + APIBuffer buf; + + auto put_byte = [&](uint8_t b) { + size_t s = buf.size(); + buf.resize(s + 1); + buf.data()[s] = b; + }; + auto put_varint = [&](uint32_t v) { + while (v >= 0x80) { + put_byte(static_cast((v & 0x7F) | 0x80)); + v >>= 7; + } + put_byte(static_cast(v)); + }; + auto encode_zigzag = [](int32_t v) -> uint32_t { + return (static_cast(v) << 1) ^ static_cast(v >> 31); + }; + + // field 2: key (fixed32) = 0xDEADBEEF + put_byte(0x15); + put_byte(0xEF); + put_byte(0xBE); + put_byte(0xAD); + put_byte(0xDE); + // field 3: carrier_frequency = 38000 + put_byte(0x18); + put_varint(38000); + // field 4: repeat_count = 2 + put_byte(0x20); + put_varint(2); + // field 5: timings (packed sint32) — 100 entries alternating mark/space. + uint8_t packed[400]; + size_t packed_len = 0; + for (int i = 0; i < 100; i++) { + int32_t value = (i % 2 == 0) ? 560 : -560; + uint32_t zz = encode_zigzag(value); + while (zz >= 0x80) { + packed[packed_len++] = static_cast((zz & 0x7F) | 0x80); + zz >>= 7; + } + packed[packed_len++] = static_cast(zz); + } + put_byte(0x2A); + put_varint(static_cast(packed_len)); + for (size_t i = 0; i < packed_len; i++) + put_byte(packed[i]); + // field 6: modulation = 0 — skip (default value, not encoded by senders) + return buf; +} + +static void Decode_InfraredRFTransmitRawTimingsRequest(benchmark::State &state) { + auto encoded = build_infrared_rf_transmit_wire(); + auto *data = encoded.data(); + auto size = encoded.size(); + benchmark::DoNotOptimize(data); + benchmark::DoNotOptimize(size); + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + InfraredRFTransmitRawTimingsRequest msg; + escape(&msg); + msg.decode(data, size); + escape(&msg); + } + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(Decode_InfraredRFTransmitRawTimingsRequest); + +#endif // USE_IR_RF || USE_RADIO_FREQUENCY + // --- LightCommandRequest decode (complex command with many fields) --- static void Decode_LightCommandRequest(benchmark::State &state) { diff --git a/tests/benchmarks/components/api/bench_proto_encode.cpp b/tests/benchmarks/components/api/bench_proto_encode.cpp index 1e2efcd281..c5f2312481 100644 --- a/tests/benchmarks/components/api/bench_proto_encode.cpp +++ b/tests/benchmarks/components/api/bench_proto_encode.cpp @@ -384,4 +384,128 @@ BENCHMARK(CalcAndEncode_BLERawAdvs12_Fresh); #endif // USE_BLUETOOTH_PROXY +// --- ZWaveProxyFrame (Z-Wave frame, ~16 bytes payload) --- + +#ifdef USE_ZWAVE_PROXY + +static constexpr uint8_t kZWaveFrameData[] = {0x01, 0x09, 0x00, 0x13, 0x01, 0x02, 0x00, 0x00, + 0x25, 0x00, 0x05, 0xC4, 0x00, 0x00, 0x00, 0x00}; + +static ZWaveProxyFrame make_zwave_proxy_frame() { + ZWaveProxyFrame msg; + msg.data = kZWaveFrameData; + msg.data_len = sizeof(kZWaveFrameData); + return msg; +} + +static void Encode_ZWaveProxyFrame(benchmark::State &state) { + auto msg = make_zwave_proxy_frame(); + APIBuffer buffer; + buffer.resize(msg.calculate_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_ZWaveProxyFrame); + +#endif // USE_ZWAVE_PROXY + +// --- SerialProxyDataReceived (serial passthrough, 64-byte payload) --- + +#ifdef USE_SERIAL_PROXY + +static constexpr size_t kSerialPayloadSize = 64; +static const uint8_t kSerialPayload[kSerialPayloadSize] = { + 0x55, 0xAA, 0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC, 0xDE, 0xF0, 0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, + 0xCD, 0xEF, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, + 0xFF, 0x00, 0x10, 0x20, 0x30, 0x40, 0x50, 0x60, 0x70, 0x80, 0x90, 0xA0, 0xB0, 0xC0, 0xD0, 0xE0, + 0xF0, 0x0F, 0x1F, 0x2F, 0x3F, 0x4F, 0x5F, 0x6F, 0x7F, 0x8F, 0x9F, 0xAF, 0xBF, 0xCF, 0xDF, 0xEF}; + +static SerialProxyDataReceived make_serial_proxy_data_received() { + SerialProxyDataReceived msg; + msg.instance = 0; + msg.set_data(kSerialPayload, kSerialPayloadSize); + return msg; +} + +static void Encode_SerialProxyDataReceived(benchmark::State &state) { + auto msg = make_serial_proxy_data_received(); + APIBuffer buffer; + buffer.resize(msg.calculate_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_SerialProxyDataReceived); + +#endif // USE_SERIAL_PROXY + +// --- InfraredRFReceiveEvent (100 timings, typical IR/RF capture) --- + +#if defined(USE_IR_RF) || defined(USE_RADIO_FREQUENCY) + +static const std::vector &get_ir_timings_100() { + static const std::vector timings = [] { + std::vector v; + v.reserve(100); + // Mark/space pairs simulating a typical RC-5 / NEC capture. + for (int i = 0; i < 100; i++) { + v.push_back((i % 2 == 0) ? 560 : -560); + } + return v; + }(); + return timings; +} + +static InfraredRFReceiveEvent make_infrared_rf_receive_event() { + InfraredRFReceiveEvent msg; + msg.key = 0xDEADBEEF; + msg.timings = &get_ir_timings_100(); + return msg; +} + +static void Encode_InfraredRFReceiveEvent(benchmark::State &state) { + auto msg = make_infrared_rf_receive_event(); + APIBuffer buffer; + buffer.resize(msg.calculate_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_InfraredRFReceiveEvent); + +static void CalculateSize_InfraredRFReceiveEvent(benchmark::State &state) { + auto msg = make_infrared_rf_receive_event(); + + 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_InfraredRFReceiveEvent); + +#endif // USE_IR_RF || USE_RADIO_FREQUENCY + } // namespace esphome::api::benchmarks diff --git a/tests/benchmarks/stubs/esphome/components/infrared/infrared.h b/tests/benchmarks/stubs/esphome/components/infrared/infrared.h new file mode 100644 index 0000000000..874e7a270b --- /dev/null +++ b/tests/benchmarks/stubs/esphome/components/infrared/infrared.h @@ -0,0 +1,45 @@ +// Stub for benchmark builds — provides the minimal interface that +// api_connection.cpp and Application need when USE_INFRARED is defined, +// without pulling in the real remote_base/RMT dependencies. +#pragma once + +#include +#include "esphome/core/component.h" +#include "esphome/core/entity_base.h" + +namespace esphome::infrared { + +class Infrared; + +class InfraredCall { + public: + explicit InfraredCall(Infrared *parent) : parent_(parent) {} + InfraredCall &set_carrier_frequency(uint32_t /*frequency*/) { return *this; } + InfraredCall &set_raw_timings_packed(const uint8_t * /*data*/, uint16_t /*length*/, uint16_t /*count*/) { + return *this; + } + InfraredCall &set_repeat_count(uint32_t /*count*/) { return *this; } + void perform() {} + + protected: + Infrared *parent_; +}; + +class InfraredTraits { + public: + uint32_t get_receiver_frequency_hz() const { return 0; } +}; + +class Infrared : public Component, public EntityBase { + public: + Infrared() = default; + InfraredTraits &get_traits() { return this->traits_; } + const InfraredTraits &get_traits() const { return this->traits_; } + InfraredCall make_call() { return InfraredCall(this); } + uint32_t get_capability_flags() const { return 0; } + + protected: + InfraredTraits traits_; +}; + +} // namespace esphome::infrared diff --git a/tests/benchmarks/stubs/esphome/components/radio_frequency/radio_frequency.h b/tests/benchmarks/stubs/esphome/components/radio_frequency/radio_frequency.h new file mode 100644 index 0000000000..72fc08034b --- /dev/null +++ b/tests/benchmarks/stubs/esphome/components/radio_frequency/radio_frequency.h @@ -0,0 +1,51 @@ +// Stub for benchmark builds — provides the minimal interface that +// api_connection.cpp and Application need when USE_RADIO_FREQUENCY is defined. +#pragma once + +#include +#include "esphome/core/component.h" +#include "esphome/core/entity_base.h" + +namespace esphome::radio_frequency { + +enum RadioFrequencyModulation : uint32_t { + RADIO_FREQUENCY_MODULATION_OOK = 0, +}; + +class RadioFrequency; + +class RadioFrequencyCall { + public: + explicit RadioFrequencyCall(RadioFrequency *parent) : parent_(parent) {} + RadioFrequencyCall &set_frequency(uint32_t /*frequency*/) { return *this; } + RadioFrequencyCall &set_modulation(RadioFrequencyModulation /*mod*/) { return *this; } + RadioFrequencyCall &set_repeat_count(uint32_t /*count*/) { return *this; } + RadioFrequencyCall &set_raw_timings_packed(const uint8_t * /*data*/, uint16_t /*length*/, uint16_t /*count*/) { + return *this; + } + void perform() {} + + protected: + RadioFrequency *parent_; +}; + +class RadioFrequencyTraits { + public: + uint32_t get_frequency_min_hz() const { return 0; } + uint32_t get_frequency_max_hz() const { return 0; } + uint32_t get_supported_modulations() const { return 0; } +}; + +class RadioFrequency : public Component, public EntityBase { + public: + RadioFrequency() = default; + RadioFrequencyTraits &get_traits() { return this->traits_; } + const RadioFrequencyTraits &get_traits() const { return this->traits_; } + RadioFrequencyCall make_call() { return RadioFrequencyCall(this); } + uint32_t get_capability_flags() const { return 0; } + + protected: + RadioFrequencyTraits traits_; +}; + +} // namespace esphome::radio_frequency diff --git a/tests/benchmarks/stubs/esphome/components/serial_proxy/serial_proxy.h b/tests/benchmarks/stubs/esphome/components/serial_proxy/serial_proxy.h new file mode 100644 index 0000000000..d967ad58b1 --- /dev/null +++ b/tests/benchmarks/stubs/esphome/components/serial_proxy/serial_proxy.h @@ -0,0 +1,46 @@ +// Stub for benchmark builds — provides the minimal interface that +// api_connection.cpp and Application need when USE_SERIAL_PROXY is defined, +// without pulling in the real UART implementation. +#pragma once + +#include +#include +#include "esphome/components/api/api_pb2.h" + +namespace esphome { + +namespace api { +class APIConnection; +} // namespace api + +namespace uart { +enum UARTFlushResult : uint8_t { + UART_FLUSH_RESULT_SUCCESS, + UART_FLUSH_RESULT_ASSUMED_SUCCESS, + UART_FLUSH_RESULT_TIMEOUT, + UART_FLUSH_RESULT_FAILED, +}; +} // namespace uart + +namespace serial_proxy { + +class SerialProxy { + public: + void set_instance_index(uint32_t index) { this->instance_index_ = index; } + uint32_t get_instance_index() const { return this->instance_index_; } + const char *get_name() const { return ""; } + api::enums::SerialProxyPortType get_port_type() const { return {}; } + api::APIConnection *get_api_connection() { return nullptr; } + void serial_proxy_request(api::APIConnection *conn, api::enums::SerialProxyRequestType type) {} + void configure(uint32_t baudrate, bool flow_control, uint8_t parity, uint32_t stop_bits, uint32_t data_size) {} + void write_from_client(const uint8_t *data, size_t len) {} + void set_modem_pins(uint32_t line_states) {} + uint32_t get_modem_pins() const { return 0; } + uart::UARTFlushResult flush_port() { return uart::UART_FLUSH_RESULT_SUCCESS; } + + protected: + uint32_t instance_index_{0}; +}; + +} // namespace serial_proxy +} // namespace esphome diff --git a/tests/benchmarks/stubs/esphome/components/zwave_proxy/zwave_proxy.h b/tests/benchmarks/stubs/esphome/components/zwave_proxy/zwave_proxy.h new file mode 100644 index 0000000000..ba97e81236 --- /dev/null +++ b/tests/benchmarks/stubs/esphome/components/zwave_proxy/zwave_proxy.h @@ -0,0 +1,29 @@ +// Stub for benchmark builds — provides the minimal interface that +// api_connection.cpp needs when USE_ZWAVE_PROXY is defined, +// without pulling in the real UART-based ZWaveProxy implementation. +#pragma once + +#include "esphome/components/api/api_pb2.h" + +namespace esphome { +namespace api { +class APIConnection; +} // namespace api + +namespace zwave_proxy { + +class ZWaveProxy { + public: + api::APIConnection *get_api_connection() { return nullptr; } + void zwave_proxy_request(api::APIConnection *conn, api::enums::ZWaveProxyRequestType type) {} + void send_frame(const uint8_t *data, size_t length) {} + void api_connection_authenticated(api::APIConnection *conn) {} + uint32_t get_feature_flags() const { return 0; } + uint32_t get_home_id() { return 0; } +}; + +// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) +extern ZWaveProxy *global_zwave_proxy; + +} // namespace zwave_proxy +} // namespace esphome From f841de0664035892771c3c4807dabeabc16d2add Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 29 Apr 2026 21:25:46 -0500 Subject: [PATCH 2/7] [api] Avoid lambda IIFE and per-byte APIBuffer growth in proxy benchmarks The InfraredRFReceiveEvent encode benchmark used a C++17 lambda IIFE (`[]{...}()`) to seed a function-static vector, and the InfraredRFTransmitRawTimingsRequest decode benchmark grew its APIBuffer one byte at a time (~210 grow_() calls), each allocating a fresh exact-fit buffer and memcpy'ing the prior contents. Both patterns are fine under direct execution but appear to hit a CodSpeed/valgrind edge case during the simulated benchmark run. Switch to a plain heap-init pattern for the vector and build the wire bytes into a stack array first, then resize+memcpy into the APIBuffer once. --- .../components/api/bench_proto_decode.cpp | 31 ++++++++++++------- .../components/api/bench_proto_encode.cpp | 16 +++++----- 2 files changed, 28 insertions(+), 19 deletions(-) diff --git a/tests/benchmarks/components/api/bench_proto_decode.cpp b/tests/benchmarks/components/api/bench_proto_decode.cpp index 54ba00efce..0951c521be 100644 --- a/tests/benchmarks/components/api/bench_proto_decode.cpp +++ b/tests/benchmarks/components/api/bench_proto_decode.cpp @@ -1,5 +1,7 @@ #include +#include + #include "esphome/components/api/api_pb2.h" #include "esphome/components/api/api_buffer.h" @@ -181,19 +183,19 @@ BENCHMARK(Decode_SerialProxyWriteRequest); #if defined(USE_IR_RF) || defined(USE_RADIO_FREQUENCY) static APIBuffer build_infrared_rf_transmit_wire() { - APIBuffer buf; + // Build the entire wire payload into a stack buffer, then copy into the + // returned APIBuffer in a single resize+memcpy. Keeps allocation count + // low so callgrind/valgrind doesn't churn through hundreds of grow_()s. + uint8_t bytes[256]; + size_t len = 0; - auto put_byte = [&](uint8_t b) { - size_t s = buf.size(); - buf.resize(s + 1); - buf.data()[s] = b; - }; + auto put_byte = [&](uint8_t b) { bytes[len++] = b; }; auto put_varint = [&](uint32_t v) { while (v >= 0x80) { - put_byte(static_cast((v & 0x7F) | 0x80)); + bytes[len++] = static_cast((v & 0x7F) | 0x80); v >>= 7; } - put_byte(static_cast(v)); + bytes[len++] = static_cast(v); }; auto encode_zigzag = [](int32_t v) -> uint32_t { return (static_cast(v) << 1) ^ static_cast(v >> 31); @@ -212,7 +214,10 @@ static APIBuffer build_infrared_rf_transmit_wire() { put_byte(0x20); put_varint(2); // field 5: timings (packed sint32) — 100 entries alternating mark/space. - uint8_t packed[400]; + // Each entry encodes to 2 bytes (zigzag(560)=1120 → varint 0xE0 0x08), so + // packed payload is 200 bytes; with tag (1) + length varint (2) it fits in + // the 256-byte stack buffer. + uint8_t packed[200]; size_t packed_len = 0; for (int i = 0; i < 100; i++) { int32_t value = (i % 2 == 0) ? 560 : -560; @@ -225,9 +230,13 @@ static APIBuffer build_infrared_rf_transmit_wire() { } put_byte(0x2A); put_varint(static_cast(packed_len)); - for (size_t i = 0; i < packed_len; i++) - put_byte(packed[i]); + std::memcpy(bytes + len, packed, packed_len); + len += packed_len; // field 6: modulation = 0 — skip (default value, not encoded by senders) + + APIBuffer buf; + buf.resize(len); + std::memcpy(buf.data(), bytes, len); return buf; } diff --git a/tests/benchmarks/components/api/bench_proto_encode.cpp b/tests/benchmarks/components/api/bench_proto_encode.cpp index c5f2312481..d1f85574f9 100644 --- a/tests/benchmarks/components/api/bench_proto_encode.cpp +++ b/tests/benchmarks/components/api/bench_proto_encode.cpp @@ -456,17 +456,17 @@ BENCHMARK(Encode_SerialProxyDataReceived); #if defined(USE_IR_RF) || defined(USE_RADIO_FREQUENCY) +// Mark/space pairs simulating a typical RC-5 / NEC capture (100 timings). static const std::vector &get_ir_timings_100() { - static const std::vector timings = [] { - std::vector v; - v.reserve(100); - // Mark/space pairs simulating a typical RC-5 / NEC capture. + static std::vector *timings = nullptr; + if (timings == nullptr) { + timings = new std::vector(); + timings->reserve(100); for (int i = 0; i < 100; i++) { - v.push_back((i % 2 == 0) ? 560 : -560); + timings->push_back((i % 2 == 0) ? 560 : -560); } - return v; - }(); - return timings; + } + return *timings; } static InfraredRFReceiveEvent make_infrared_rf_receive_event() { From 483d294ef6f7d08eaa3a3349c89a204b628b68e7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 29 Apr 2026 21:38:01 -0500 Subject: [PATCH 3/7] [api] Move proxy message benchmarks into bench_proto_proxy.cpp Splitting these out from bench_proto_encode.cpp and bench_proto_decode.cpp moves them to the end of the linker's static-init order. CodSpeed's callgrind runner has been segfaulting immediately after measuring the last existing decode benchmark (Decode_SwitchCommandRequest), and isolating the new code into its own translation unit lets us see whether the crash is triggered by one of the new benchmarks or by something about the new USE_*_PROXY/USE_INFRARED/USE_RADIO_FREQUENCY defines changing how api_pb2.cpp compiles. --- .../components/api/bench_proto_decode.cpp | 184 ----------- .../components/api/bench_proto_encode.cpp | 124 -------- .../components/api/bench_proto_proxy.cpp | 287 ++++++++++++++++++ 3 files changed, 287 insertions(+), 308 deletions(-) create mode 100644 tests/benchmarks/components/api/bench_proto_proxy.cpp diff --git a/tests/benchmarks/components/api/bench_proto_decode.cpp b/tests/benchmarks/components/api/bench_proto_decode.cpp index 0951c521be..961c629f2a 100644 --- a/tests/benchmarks/components/api/bench_proto_decode.cpp +++ b/tests/benchmarks/components/api/bench_proto_decode.cpp @@ -1,7 +1,5 @@ #include -#include - #include "esphome/components/api/api_pb2.h" #include "esphome/components/api/api_buffer.h" @@ -79,188 +77,6 @@ static void Decode_SwitchCommandRequest(benchmark::State &state) { } BENCHMARK(Decode_SwitchCommandRequest); -// --- ZWaveProxyFrame decode (~16-byte data buffer) --- - -#ifdef USE_ZWAVE_PROXY - -static void Decode_ZWaveProxyFrame(benchmark::State &state) { - static const uint8_t frame_data[] = {0x01, 0x09, 0x00, 0x13, 0x01, 0x02, 0x00, 0x00, - 0x25, 0x00, 0x05, 0xC4, 0x00, 0x00, 0x00, 0x00}; - ZWaveProxyFrame source; - source.data = frame_data; - source.data_len = sizeof(frame_data); - auto encoded = encode_message(source); - auto *data = encoded.data(); - auto size = encoded.size(); - benchmark::DoNotOptimize(data); - benchmark::DoNotOptimize(size); - - for (auto _ : state) { - for (int i = 0; i < kInnerIterations; i++) { - ZWaveProxyFrame msg; - escape(&msg); - msg.decode(data, size); - escape(&msg); - } - } - state.SetItemsProcessed(state.iterations() * kInnerIterations); -} -BENCHMARK(Decode_ZWaveProxyFrame); - -static void Decode_ZWaveProxyRequest(benchmark::State &state) { - static const uint8_t req_data[] = {0xDE, 0xAD, 0xBE, 0xEF}; - ZWaveProxyRequest source; - source.type = enums::ZWAVE_PROXY_REQUEST_TYPE_HOME_ID_CHANGE; - source.data = req_data; - source.data_len = sizeof(req_data); - auto encoded = encode_message(source); - auto *data = encoded.data(); - auto size = encoded.size(); - benchmark::DoNotOptimize(data); - benchmark::DoNotOptimize(size); - - for (auto _ : state) { - for (int i = 0; i < kInnerIterations; i++) { - ZWaveProxyRequest msg; - escape(&msg); - msg.decode(data, size); - escape(&msg); - } - } - state.SetItemsProcessed(state.iterations() * kInnerIterations); -} -BENCHMARK(Decode_ZWaveProxyRequest); - -#endif // USE_ZWAVE_PROXY - -// --- SerialProxyWriteRequest decode (instance + 64-byte data) --- -// -// SerialProxyWriteRequest is decode-only (SOURCE_CLIENT), so we encode via -// SerialProxyDataReceived which has identical wire format -// (uint32 instance = 1; bytes data = 2;). - -#ifdef USE_SERIAL_PROXY - -static void Decode_SerialProxyWriteRequest(benchmark::State &state) { - static constexpr size_t kPayloadSize = 64; - static uint8_t payload[kPayloadSize]; - for (size_t i = 0; i < kPayloadSize; i++) - payload[i] = static_cast(i); - - SerialProxyDataReceived source; - source.instance = 0; - source.set_data(payload, kPayloadSize); - auto encoded = encode_message(source); - auto *data = encoded.data(); - auto size = encoded.size(); - benchmark::DoNotOptimize(data); - benchmark::DoNotOptimize(size); - - for (auto _ : state) { - for (int i = 0; i < kInnerIterations; i++) { - SerialProxyWriteRequest msg; - escape(&msg); - msg.decode(data, size); - escape(&msg); - } - } - state.SetItemsProcessed(state.iterations() * kInnerIterations); -} -BENCHMARK(Decode_SerialProxyWriteRequest); - -#endif // USE_SERIAL_PROXY - -// --- InfraredRFTransmitRawTimingsRequest decode (100 zigzag-encoded timings) --- -// -// Hand-built wire bytes since this message is decode-only and has no sister -// type with an identical layout. Wire format: -// field 2 (key, fixed32): tag=0x15, 4 LE bytes -// field 3 (carrier_frequency): tag=0x18, varint -// field 4 (repeat_count): tag=0x20, varint -// field 5 (timings, packed sint32): tag=0x2A, length varint, packed payload -// field 6 (modulation): tag=0x30, varint - -#if defined(USE_IR_RF) || defined(USE_RADIO_FREQUENCY) - -static APIBuffer build_infrared_rf_transmit_wire() { - // Build the entire wire payload into a stack buffer, then copy into the - // returned APIBuffer in a single resize+memcpy. Keeps allocation count - // low so callgrind/valgrind doesn't churn through hundreds of grow_()s. - uint8_t bytes[256]; - size_t len = 0; - - auto put_byte = [&](uint8_t b) { bytes[len++] = b; }; - auto put_varint = [&](uint32_t v) { - while (v >= 0x80) { - bytes[len++] = static_cast((v & 0x7F) | 0x80); - v >>= 7; - } - bytes[len++] = static_cast(v); - }; - auto encode_zigzag = [](int32_t v) -> uint32_t { - return (static_cast(v) << 1) ^ static_cast(v >> 31); - }; - - // field 2: key (fixed32) = 0xDEADBEEF - put_byte(0x15); - put_byte(0xEF); - put_byte(0xBE); - put_byte(0xAD); - put_byte(0xDE); - // field 3: carrier_frequency = 38000 - put_byte(0x18); - put_varint(38000); - // field 4: repeat_count = 2 - put_byte(0x20); - put_varint(2); - // field 5: timings (packed sint32) — 100 entries alternating mark/space. - // Each entry encodes to 2 bytes (zigzag(560)=1120 → varint 0xE0 0x08), so - // packed payload is 200 bytes; with tag (1) + length varint (2) it fits in - // the 256-byte stack buffer. - uint8_t packed[200]; - size_t packed_len = 0; - for (int i = 0; i < 100; i++) { - int32_t value = (i % 2 == 0) ? 560 : -560; - uint32_t zz = encode_zigzag(value); - while (zz >= 0x80) { - packed[packed_len++] = static_cast((zz & 0x7F) | 0x80); - zz >>= 7; - } - packed[packed_len++] = static_cast(zz); - } - put_byte(0x2A); - put_varint(static_cast(packed_len)); - std::memcpy(bytes + len, packed, packed_len); - len += packed_len; - // field 6: modulation = 0 — skip (default value, not encoded by senders) - - APIBuffer buf; - buf.resize(len); - std::memcpy(buf.data(), bytes, len); - return buf; -} - -static void Decode_InfraredRFTransmitRawTimingsRequest(benchmark::State &state) { - auto encoded = build_infrared_rf_transmit_wire(); - auto *data = encoded.data(); - auto size = encoded.size(); - benchmark::DoNotOptimize(data); - benchmark::DoNotOptimize(size); - - for (auto _ : state) { - for (int i = 0; i < kInnerIterations; i++) { - InfraredRFTransmitRawTimingsRequest msg; - escape(&msg); - msg.decode(data, size); - escape(&msg); - } - } - state.SetItemsProcessed(state.iterations() * kInnerIterations); -} -BENCHMARK(Decode_InfraredRFTransmitRawTimingsRequest); - -#endif // USE_IR_RF || USE_RADIO_FREQUENCY - // --- LightCommandRequest decode (complex command with many fields) --- static void Decode_LightCommandRequest(benchmark::State &state) { diff --git a/tests/benchmarks/components/api/bench_proto_encode.cpp b/tests/benchmarks/components/api/bench_proto_encode.cpp index d1f85574f9..1e2efcd281 100644 --- a/tests/benchmarks/components/api/bench_proto_encode.cpp +++ b/tests/benchmarks/components/api/bench_proto_encode.cpp @@ -384,128 +384,4 @@ BENCHMARK(CalcAndEncode_BLERawAdvs12_Fresh); #endif // USE_BLUETOOTH_PROXY -// --- ZWaveProxyFrame (Z-Wave frame, ~16 bytes payload) --- - -#ifdef USE_ZWAVE_PROXY - -static constexpr uint8_t kZWaveFrameData[] = {0x01, 0x09, 0x00, 0x13, 0x01, 0x02, 0x00, 0x00, - 0x25, 0x00, 0x05, 0xC4, 0x00, 0x00, 0x00, 0x00}; - -static ZWaveProxyFrame make_zwave_proxy_frame() { - ZWaveProxyFrame msg; - msg.data = kZWaveFrameData; - msg.data_len = sizeof(kZWaveFrameData); - return msg; -} - -static void Encode_ZWaveProxyFrame(benchmark::State &state) { - auto msg = make_zwave_proxy_frame(); - APIBuffer buffer; - buffer.resize(msg.calculate_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_ZWaveProxyFrame); - -#endif // USE_ZWAVE_PROXY - -// --- SerialProxyDataReceived (serial passthrough, 64-byte payload) --- - -#ifdef USE_SERIAL_PROXY - -static constexpr size_t kSerialPayloadSize = 64; -static const uint8_t kSerialPayload[kSerialPayloadSize] = { - 0x55, 0xAA, 0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC, 0xDE, 0xF0, 0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, - 0xCD, 0xEF, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, - 0xFF, 0x00, 0x10, 0x20, 0x30, 0x40, 0x50, 0x60, 0x70, 0x80, 0x90, 0xA0, 0xB0, 0xC0, 0xD0, 0xE0, - 0xF0, 0x0F, 0x1F, 0x2F, 0x3F, 0x4F, 0x5F, 0x6F, 0x7F, 0x8F, 0x9F, 0xAF, 0xBF, 0xCF, 0xDF, 0xEF}; - -static SerialProxyDataReceived make_serial_proxy_data_received() { - SerialProxyDataReceived msg; - msg.instance = 0; - msg.set_data(kSerialPayload, kSerialPayloadSize); - return msg; -} - -static void Encode_SerialProxyDataReceived(benchmark::State &state) { - auto msg = make_serial_proxy_data_received(); - APIBuffer buffer; - buffer.resize(msg.calculate_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_SerialProxyDataReceived); - -#endif // USE_SERIAL_PROXY - -// --- InfraredRFReceiveEvent (100 timings, typical IR/RF capture) --- - -#if defined(USE_IR_RF) || defined(USE_RADIO_FREQUENCY) - -// Mark/space pairs simulating a typical RC-5 / NEC capture (100 timings). -static const std::vector &get_ir_timings_100() { - static std::vector *timings = nullptr; - if (timings == nullptr) { - timings = new std::vector(); - timings->reserve(100); - for (int i = 0; i < 100; i++) { - timings->push_back((i % 2 == 0) ? 560 : -560); - } - } - return *timings; -} - -static InfraredRFReceiveEvent make_infrared_rf_receive_event() { - InfraredRFReceiveEvent msg; - msg.key = 0xDEADBEEF; - msg.timings = &get_ir_timings_100(); - return msg; -} - -static void Encode_InfraredRFReceiveEvent(benchmark::State &state) { - auto msg = make_infrared_rf_receive_event(); - APIBuffer buffer; - buffer.resize(msg.calculate_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_InfraredRFReceiveEvent); - -static void CalculateSize_InfraredRFReceiveEvent(benchmark::State &state) { - auto msg = make_infrared_rf_receive_event(); - - 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_InfraredRFReceiveEvent); - -#endif // USE_IR_RF || USE_RADIO_FREQUENCY - } // namespace esphome::api::benchmarks diff --git a/tests/benchmarks/components/api/bench_proto_proxy.cpp b/tests/benchmarks/components/api/bench_proto_proxy.cpp new file mode 100644 index 0000000000..612d8e69f3 --- /dev/null +++ b/tests/benchmarks/components/api/bench_proto_proxy.cpp @@ -0,0 +1,287 @@ +// Encode/decode microbenchmarks for proxy message families that carry +// high-volume traffic (Z-Wave, IR/RF, serial). Mirrors the existing +// BluetoothLERawAdvertisementsResponse benchmarks in bench_proto_encode.cpp. + +#include + +#include + +#include "esphome/components/api/api_pb2.h" +#include "esphome/components/api/api_buffer.h" + +namespace esphome::api::benchmarks { + +static constexpr int kInnerIterations = 2000; + +template static APIBuffer encode_message_for_proxy(const T &msg) { + APIBuffer buffer; + uint32_t size = msg.calculate_size(); + buffer.resize(size); + ProtoWriteBuffer writer(&buffer, 0); + msg.encode(writer); + return buffer; +} + +static void escape_proxy(void *p) { asm volatile("" : : "g"(p) : "memory"); } + +// --- ZWaveProxyFrame (Z-Wave frame, ~16 bytes payload) --- + +#ifdef USE_ZWAVE_PROXY + +static const uint8_t kZWaveFrameData[] = {0x01, 0x09, 0x00, 0x13, 0x01, 0x02, 0x00, 0x00, + 0x25, 0x00, 0x05, 0xC4, 0x00, 0x00, 0x00, 0x00}; + +static void Encode_ZWaveProxyFrame(benchmark::State &state) { + ZWaveProxyFrame msg; + msg.data = kZWaveFrameData; + msg.data_len = sizeof(kZWaveFrameData); + APIBuffer buffer; + buffer.resize(msg.calculate_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_ZWaveProxyFrame); + +static void Decode_ZWaveProxyFrame(benchmark::State &state) { + ZWaveProxyFrame source; + source.data = kZWaveFrameData; + source.data_len = sizeof(kZWaveFrameData); + auto encoded = encode_message_for_proxy(source); + auto *data = encoded.data(); + auto size = encoded.size(); + benchmark::DoNotOptimize(data); + benchmark::DoNotOptimize(size); + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + ZWaveProxyFrame msg; + escape_proxy(&msg); + msg.decode(data, size); + escape_proxy(&msg); + } + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(Decode_ZWaveProxyFrame); + +static const uint8_t kZWaveRequestData[] = {0xDE, 0xAD, 0xBE, 0xEF}; + +static void Decode_ZWaveProxyRequest(benchmark::State &state) { + ZWaveProxyRequest source; + source.type = enums::ZWAVE_PROXY_REQUEST_TYPE_HOME_ID_CHANGE; + source.data = kZWaveRequestData; + source.data_len = sizeof(kZWaveRequestData); + auto encoded = encode_message_for_proxy(source); + auto *data = encoded.data(); + auto size = encoded.size(); + benchmark::DoNotOptimize(data); + benchmark::DoNotOptimize(size); + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + ZWaveProxyRequest msg; + escape_proxy(&msg); + msg.decode(data, size); + escape_proxy(&msg); + } + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(Decode_ZWaveProxyRequest); + +#endif // USE_ZWAVE_PROXY + +// --- SerialProxyDataReceived encode + SerialProxyWriteRequest decode --- +// +// SerialProxyWriteRequest is decode-only (SOURCE_CLIENT) but has the same +// wire layout as SerialProxyDataReceived, so we encode via the latter and +// decode as the former. + +#ifdef USE_SERIAL_PROXY + +static constexpr size_t kSerialPayloadSize = 64; +static const uint8_t kSerialPayload[kSerialPayloadSize] = { + 0x55, 0xAA, 0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC, 0xDE, 0xF0, 0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, + 0xCD, 0xEF, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, + 0xFF, 0x00, 0x10, 0x20, 0x30, 0x40, 0x50, 0x60, 0x70, 0x80, 0x90, 0xA0, 0xB0, 0xC0, 0xD0, 0xE0, + 0xF0, 0x0F, 0x1F, 0x2F, 0x3F, 0x4F, 0x5F, 0x6F, 0x7F, 0x8F, 0x9F, 0xAF, 0xBF, 0xCF, 0xDF, 0xEF}; + +static void Encode_SerialProxyDataReceived(benchmark::State &state) { + SerialProxyDataReceived msg; + msg.instance = 0; + msg.set_data(kSerialPayload, kSerialPayloadSize); + APIBuffer buffer; + buffer.resize(msg.calculate_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_SerialProxyDataReceived); + +static void Decode_SerialProxyWriteRequest(benchmark::State &state) { + SerialProxyDataReceived source; + source.instance = 0; + source.set_data(kSerialPayload, kSerialPayloadSize); + auto encoded = encode_message_for_proxy(source); + auto *data = encoded.data(); + auto size = encoded.size(); + benchmark::DoNotOptimize(data); + benchmark::DoNotOptimize(size); + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + SerialProxyWriteRequest msg; + escape_proxy(&msg); + msg.decode(data, size); + escape_proxy(&msg); + } + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(Decode_SerialProxyWriteRequest); + +#endif // USE_SERIAL_PROXY + +// --- InfraredRFReceiveEvent encode (100 sint32 timings) + +// InfraredRFTransmitRawTimingsRequest decode (hand-built wire bytes) --- + +#if defined(USE_IR_RF) || defined(USE_RADIO_FREQUENCY) + +// Heap-allocated on first use to avoid C++17 lambda IIFE patterns that some +// callgrind/valgrind versions handle awkwardly during benchmark init. +static const std::vector &get_ir_timings_100() { + static std::vector *timings = nullptr; + if (timings == nullptr) { + timings = new std::vector(); + timings->reserve(100); + for (int i = 0; i < 100; i++) { + timings->push_back((i % 2 == 0) ? 560 : -560); + } + } + return *timings; +} + +static void Encode_InfraredRFReceiveEvent(benchmark::State &state) { + InfraredRFReceiveEvent msg; + msg.key = 0xDEADBEEF; + msg.timings = &get_ir_timings_100(); + APIBuffer buffer; + buffer.resize(msg.calculate_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_InfraredRFReceiveEvent); + +static void CalculateSize_InfraredRFReceiveEvent(benchmark::State &state) { + InfraredRFReceiveEvent msg; + msg.key = 0xDEADBEEF; + msg.timings = &get_ir_timings_100(); + + 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_InfraredRFReceiveEvent); + +// Hand-built wire bytes for InfraredRFTransmitRawTimingsRequest (decode-only, +// no sister message with identical wire layout). +// field 2 (key, fixed32): tag=0x15, 4 LE bytes +// field 3 (carrier_frequency): tag=0x18, varint +// field 4 (repeat_count): tag=0x20, varint +// field 5 (timings, packed sint32): tag=0x2A, length varint, packed payload +// field 6 (modulation): tag=0x30, varint +static APIBuffer build_infrared_rf_transmit_wire() { + uint8_t bytes[256]; + size_t len = 0; + + auto put_byte = [&](uint8_t b) { bytes[len++] = b; }; + auto put_varint = [&](uint32_t v) { + while (v >= 0x80) { + bytes[len++] = static_cast((v & 0x7F) | 0x80); + v >>= 7; + } + bytes[len++] = static_cast(v); + }; + auto encode_zigzag = [](int32_t v) -> uint32_t { + return (static_cast(v) << 1) ^ static_cast(v >> 31); + }; + + put_byte(0x15); + put_byte(0xEF); + put_byte(0xBE); + put_byte(0xAD); + put_byte(0xDE); + put_byte(0x18); + put_varint(38000); + put_byte(0x20); + put_varint(2); + + uint8_t packed[200]; + size_t packed_len = 0; + for (int i = 0; i < 100; i++) { + int32_t value = (i % 2 == 0) ? 560 : -560; + uint32_t zz = encode_zigzag(value); + while (zz >= 0x80) { + packed[packed_len++] = static_cast((zz & 0x7F) | 0x80); + zz >>= 7; + } + packed[packed_len++] = static_cast(zz); + } + put_byte(0x2A); + put_varint(static_cast(packed_len)); + std::memcpy(bytes + len, packed, packed_len); + len += packed_len; + + APIBuffer buf; + buf.resize(len); + std::memcpy(buf.data(), bytes, len); + return buf; +} + +static void Decode_InfraredRFTransmitRawTimingsRequest(benchmark::State &state) { + auto encoded = build_infrared_rf_transmit_wire(); + auto *data = encoded.data(); + auto size = encoded.size(); + benchmark::DoNotOptimize(data); + benchmark::DoNotOptimize(size); + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + InfraredRFTransmitRawTimingsRequest msg; + escape_proxy(&msg); + msg.decode(data, size); + escape_proxy(&msg); + } + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(Decode_InfraredRFTransmitRawTimingsRequest); + +#endif // USE_IR_RF || USE_RADIO_FREQUENCY + +} // namespace esphome::api::benchmarks From a0532d657f02f276ed247f2b3944fa04dcbfcd71 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 29 Apr 2026 21:46:32 -0500 Subject: [PATCH 4/7] [api] Drop escape() helper and return-by-value APIBuffer in proxy decode benchmarks Simplifies the decode benchmarks to mirror the encode pattern more closely: no per-iteration asm volatile barrier, no return-by-value of APIBuffer through encode_message_for_proxy. CodSpeed callgrind has been crashing inside Decode_ZWaveProxyFrame and the previous setup was the main thing it had that the (passing) Encode_ZWaveProxyFrame did not. --- .../components/api/bench_proto_proxy.cpp | 60 ++++++++----------- 1 file changed, 24 insertions(+), 36 deletions(-) diff --git a/tests/benchmarks/components/api/bench_proto_proxy.cpp b/tests/benchmarks/components/api/bench_proto_proxy.cpp index 612d8e69f3..ae7fc0722c 100644 --- a/tests/benchmarks/components/api/bench_proto_proxy.cpp +++ b/tests/benchmarks/components/api/bench_proto_proxy.cpp @@ -13,17 +13,14 @@ namespace esphome::api::benchmarks { static constexpr int kInnerIterations = 2000; -template static APIBuffer encode_message_for_proxy(const T &msg) { - APIBuffer buffer; - uint32_t size = msg.calculate_size(); - buffer.resize(size); - ProtoWriteBuffer writer(&buffer, 0); - msg.encode(writer); - return buffer; +// Encodes `src` into `out`. Caller owns `out` and must keep it alive across +// the decode loop (decoded messages may store pointers back into its bytes). +template static void encode_into(APIBuffer &out, const T &src) { + out.resize(src.calculate_size()); + ProtoWriteBuffer writer(&out, 0); + src.encode(writer); } -static void escape_proxy(void *p) { asm volatile("" : : "g"(p) : "memory"); } - // --- ZWaveProxyFrame (Z-Wave frame, ~16 bytes payload) --- #ifdef USE_ZWAVE_PROXY @@ -53,18 +50,16 @@ static void Decode_ZWaveProxyFrame(benchmark::State &state) { ZWaveProxyFrame source; source.data = kZWaveFrameData; source.data_len = sizeof(kZWaveFrameData); - auto encoded = encode_message_for_proxy(source); - auto *data = encoded.data(); - auto size = encoded.size(); - benchmark::DoNotOptimize(data); - benchmark::DoNotOptimize(size); + APIBuffer encoded; + encode_into(encoded, source); + const uint8_t *data = encoded.data(); + size_t size = encoded.size(); for (auto _ : state) { for (int i = 0; i < kInnerIterations; i++) { ZWaveProxyFrame msg; - escape_proxy(&msg); msg.decode(data, size); - escape_proxy(&msg); + benchmark::DoNotOptimize(msg); } } state.SetItemsProcessed(state.iterations() * kInnerIterations); @@ -78,18 +73,16 @@ static void Decode_ZWaveProxyRequest(benchmark::State &state) { source.type = enums::ZWAVE_PROXY_REQUEST_TYPE_HOME_ID_CHANGE; source.data = kZWaveRequestData; source.data_len = sizeof(kZWaveRequestData); - auto encoded = encode_message_for_proxy(source); - auto *data = encoded.data(); - auto size = encoded.size(); - benchmark::DoNotOptimize(data); - benchmark::DoNotOptimize(size); + APIBuffer encoded; + encode_into(encoded, source); + const uint8_t *data = encoded.data(); + size_t size = encoded.size(); for (auto _ : state) { for (int i = 0; i < kInnerIterations; i++) { ZWaveProxyRequest msg; - escape_proxy(&msg); msg.decode(data, size); - escape_proxy(&msg); + benchmark::DoNotOptimize(msg); } } state.SetItemsProcessed(state.iterations() * kInnerIterations); @@ -135,18 +128,16 @@ static void Decode_SerialProxyWriteRequest(benchmark::State &state) { SerialProxyDataReceived source; source.instance = 0; source.set_data(kSerialPayload, kSerialPayloadSize); - auto encoded = encode_message_for_proxy(source); - auto *data = encoded.data(); - auto size = encoded.size(); - benchmark::DoNotOptimize(data); - benchmark::DoNotOptimize(size); + APIBuffer encoded; + encode_into(encoded, source); + const uint8_t *data = encoded.data(); + size_t size = encoded.size(); for (auto _ : state) { for (int i = 0; i < kInnerIterations; i++) { SerialProxyWriteRequest msg; - escape_proxy(&msg); msg.decode(data, size); - escape_proxy(&msg); + benchmark::DoNotOptimize(msg); } } state.SetItemsProcessed(state.iterations() * kInnerIterations); @@ -265,17 +256,14 @@ static APIBuffer build_infrared_rf_transmit_wire() { static void Decode_InfraredRFTransmitRawTimingsRequest(benchmark::State &state) { auto encoded = build_infrared_rf_transmit_wire(); - auto *data = encoded.data(); - auto size = encoded.size(); - benchmark::DoNotOptimize(data); - benchmark::DoNotOptimize(size); + const uint8_t *data = encoded.data(); + size_t size = encoded.size(); for (auto _ : state) { for (int i = 0; i < kInnerIterations; i++) { InfraredRFTransmitRawTimingsRequest msg; - escape_proxy(&msg); msg.decode(data, size); - escape_proxy(&msg); + benchmark::DoNotOptimize(msg); } } state.SetItemsProcessed(state.iterations() * kInnerIterations); From 8c0e5e9d9a180f8331dd04eb310fa9e8563e18b7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 29 Apr 2026 22:40:45 -0500 Subject: [PATCH 5/7] [api] Address Copilot review on proxy benchmarks PR - Make UARTFlushResult in the serial_proxy stub a scoped enum class with matching scoped enumerator return in flush_port(), so the stub signature lines up with the real esphome::uart::UARTFlushResult. - Replace heap-leaking lazy-init in get_ir_timings_100() with a function-local static const std::vector populated by a regular helper function. Same lazy-init behavior, no leak in valgrind/ASan, no lambda IIFE. - Emit field 6 (modulation = 1) in build_infrared_rf_transmit_wire() so the bytes match the documented field list and the decode benchmark also exercises the field-6 decode_varint path. --- .../components/api/bench_proto_proxy.cpp | 27 +++++++++++-------- .../components/serial_proxy/serial_proxy.h | 4 +-- 2 files changed, 18 insertions(+), 13 deletions(-) diff --git a/tests/benchmarks/components/api/bench_proto_proxy.cpp b/tests/benchmarks/components/api/bench_proto_proxy.cpp index ae7fc0722c..fa3191a969 100644 --- a/tests/benchmarks/components/api/bench_proto_proxy.cpp +++ b/tests/benchmarks/components/api/bench_proto_proxy.cpp @@ -151,18 +151,19 @@ BENCHMARK(Decode_SerialProxyWriteRequest); #if defined(USE_IR_RF) || defined(USE_RADIO_FREQUENCY) -// Heap-allocated on first use to avoid C++17 lambda IIFE patterns that some -// callgrind/valgrind versions handle awkwardly during benchmark init. -static const std::vector &get_ir_timings_100() { - static std::vector *timings = nullptr; - if (timings == nullptr) { - timings = new std::vector(); - timings->reserve(100); - for (int i = 0; i < 100; i++) { - timings->push_back((i % 2 == 0) ? 560 : -560); - } +// Mark/space pairs simulating a typical RC-5 / NEC capture (100 timings). +static std::vector make_ir_timings_100() { + std::vector v; + v.reserve(100); + for (int i = 0; i < 100; i++) { + v.push_back((i % 2 == 0) ? 560 : -560); } - return *timings; + return v; +} + +static const std::vector &get_ir_timings_100() { + static const std::vector timings = make_ir_timings_100(); + return timings; } static void Encode_InfraredRFReceiveEvent(benchmark::State &state) { @@ -247,6 +248,10 @@ static APIBuffer build_infrared_rf_transmit_wire() { put_varint(static_cast(packed_len)); std::memcpy(bytes + len, packed, packed_len); len += packed_len; + // field 6: modulation = 1 (non-zero so it's actually emitted and exercises + // decode_varint for this field, matching the documented layout above). + put_byte(0x30); + put_varint(1); APIBuffer buf; buf.resize(len); diff --git a/tests/benchmarks/stubs/esphome/components/serial_proxy/serial_proxy.h b/tests/benchmarks/stubs/esphome/components/serial_proxy/serial_proxy.h index d967ad58b1..bab27549e7 100644 --- a/tests/benchmarks/stubs/esphome/components/serial_proxy/serial_proxy.h +++ b/tests/benchmarks/stubs/esphome/components/serial_proxy/serial_proxy.h @@ -14,7 +14,7 @@ class APIConnection; } // namespace api namespace uart { -enum UARTFlushResult : uint8_t { +enum class UARTFlushResult : uint8_t { UART_FLUSH_RESULT_SUCCESS, UART_FLUSH_RESULT_ASSUMED_SUCCESS, UART_FLUSH_RESULT_TIMEOUT, @@ -36,7 +36,7 @@ class SerialProxy { void write_from_client(const uint8_t *data, size_t len) {} void set_modem_pins(uint32_t line_states) {} uint32_t get_modem_pins() const { return 0; } - uart::UARTFlushResult flush_port() { return uart::UART_FLUSH_RESULT_SUCCESS; } + uart::UARTFlushResult flush_port() { return uart::UARTFlushResult::UART_FLUSH_RESULT_SUCCESS; } protected: uint32_t instance_index_{0}; From 1d4dbf5476c56dd4740a7bb5ac405a13550968f2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 29 Apr 2026 22:05:45 -0500 Subject: [PATCH 6/7] [api] Mark InfraredRFReceiveEvent encode/calculate_size as speed_optimized MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This message is emitted on every IR/RF receive event with a packed sint32 timings array (typically ~50-200 entries). The encode walks the vector calling ProtoEncode::encode_sint32 per element; under -Os GCC does not inline those helpers, so each element pays the call overhead. Adding option (speed_optimized) = true to the proto definition causes the codegen to emit __attribute__((optimize("O2"))) on encode and calculate_size, matching what already exists on SensorStateResponse, SubscribeLogsResponse, and BluetoothLERawAdvertisementsResponse — the other high-volume server-emitted messages. The CodSpeed Encode_InfraredRFReceiveEvent / CalculateSize_InfraredRFReceiveEvent benchmarks added in the parent PR will quantify the improvement. --- esphome/components/api/api.proto | 1 + esphome/components/api/api_pb2.cpp | 8 ++++++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index 391efbd6eb..6705a30526 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -2571,6 +2571,7 @@ message InfraredRFReceiveEvent { option (source) = SOURCE_SERVER; option (ifdef) = "USE_IR_RF || USE_RADIO_FREQUENCY"; option (no_delay) = true; + option (speed_optimized) = true; uint32 device_id = 1 [(field_ifdef) = "USE_DEVICES"]; fixed32 key = 2 [(force) = true]; // Key identifying the receiver instance diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index eb25bf7461..95bdb994d3 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -3910,7 +3910,9 @@ bool InfraredRFTransmitRawTimingsRequest::decode_32bit(uint32_t field_id, Proto3 } return true; } -uint8_t *InfraredRFReceiveEvent::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { +__attribute__((optimize("O2"))) // NOLINT(clang-diagnostic-unknown-attributes) +uint8_t * +InfraredRFReceiveEvent::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { uint8_t *__restrict__ pos = buffer.get_pos(); #ifdef USE_DEVICES ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 1, this->device_id); @@ -3921,7 +3923,9 @@ uint8_t *InfraredRFReceiveEvent::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DE } return pos; } -uint32_t InfraredRFReceiveEvent::calculate_size() const { +__attribute__((optimize("O2"))) // NOLINT(clang-diagnostic-unknown-attributes) +uint32_t +InfraredRFReceiveEvent::calculate_size() const { uint32_t size = 0; #ifdef USE_DEVICES size += ProtoSize::calc_uint32(1, this->device_id); From 663f8560763ca17ed405c0b0ecec15c1381eb3bf Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 29 Apr 2026 22:15:09 -0500 Subject: [PATCH 7/7] [api] Mark ZWaveProxyFrame and SerialProxyDataReceived as speed_optimized Both messages are unconditionally exercised in the proxy hot paths (every Z-Wave frame received from the controller; every UART read on a configured serial proxy), so the modest flash cost of forcing -O2 on their encode/calculate_size pays off on every device that has the proxy enabled. Same treatment as InfraredRFReceiveEvent in the prior commit and the other high-volume server-emitted messages. --- esphome/components/api/api.proto | 2 ++ esphome/components/api/api_pb2.cpp | 16 ++++++++++++---- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index 6705a30526..4d72be5407 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -2511,6 +2511,7 @@ message ZWaveProxyFrame { option (source) = SOURCE_BOTH; option (ifdef) = "USE_ZWAVE_PROXY"; option (no_delay) = true; + option (speed_optimized) = true; bytes data = 1; } @@ -2628,6 +2629,7 @@ message SerialProxyDataReceived { option (source) = SOURCE_SERVER; option (ifdef) = "USE_SERIAL_PROXY"; option (no_delay) = true; + option (speed_optimized) = true; uint32 instance = 1; // Instance index (0-based) bytes data = 2; // Raw data received from the serial device diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index 95bdb994d3..68be7550ee 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -3784,12 +3784,16 @@ bool ZWaveProxyFrame::decode_length(uint32_t field_id, ProtoLengthDelimited valu } return true; } -uint8_t *ZWaveProxyFrame::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { +__attribute__((optimize("O2"))) // NOLINT(clang-diagnostic-unknown-attributes) +uint8_t * +ZWaveProxyFrame::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { uint8_t *__restrict__ pos = buffer.get_pos(); ProtoEncode::encode_bytes(pos PROTO_ENCODE_DEBUG_ARG, 1, this->data, this->data_len); return pos; } -uint32_t ZWaveProxyFrame::calculate_size() const { +__attribute__((optimize("O2"))) // NOLINT(clang-diagnostic-unknown-attributes) +uint32_t +ZWaveProxyFrame::calculate_size() const { uint32_t size = 0; size += ProtoSize::calc_length(1, this->data_len); return size; @@ -4005,13 +4009,17 @@ bool SerialProxyConfigureRequest::decode_varint(uint32_t field_id, proto_varint_ } return true; } -uint8_t *SerialProxyDataReceived::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { +__attribute__((optimize("O2"))) // NOLINT(clang-diagnostic-unknown-attributes) +uint8_t * +SerialProxyDataReceived::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { uint8_t *__restrict__ pos = buffer.get_pos(); ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 1, this->instance); ProtoEncode::encode_bytes(pos PROTO_ENCODE_DEBUG_ARG, 2, this->data_ptr_, this->data_len_); return pos; } -uint32_t SerialProxyDataReceived::calculate_size() const { +__attribute__((optimize("O2"))) // NOLINT(clang-diagnostic-unknown-attributes) +uint32_t +SerialProxyDataReceived::calculate_size() const { uint32_t size = 0; size += ProtoSize::calc_uint32(1, this->instance); size += ProtoSize::calc_length(1, this->data_len_);