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_proxy.cpp b/tests/benchmarks/components/api/bench_proto_proxy.cpp new file mode 100644 index 0000000000..fa3191a969 --- /dev/null +++ b/tests/benchmarks/components/api/bench_proto_proxy.cpp @@ -0,0 +1,280 @@ +// 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; + +// 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); +} + +// --- 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); + 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; + msg.decode(data, size); + benchmark::DoNotOptimize(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); + 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; + msg.decode(data, size); + benchmark::DoNotOptimize(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); + 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; + msg.decode(data, size); + benchmark::DoNotOptimize(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) + +// 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 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) { + 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; + // 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); + std::memcpy(buf.data(), bytes, len); + return buf; +} + +static void Decode_InfraredRFTransmitRawTimingsRequest(benchmark::State &state) { + auto encoded = build_infrared_rf_transmit_wire(); + const uint8_t *data = encoded.data(); + size_t size = encoded.size(); + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + InfraredRFTransmitRawTimingsRequest msg; + msg.decode(data, size); + benchmark::DoNotOptimize(msg); + } + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(Decode_InfraredRFTransmitRawTimingsRequest); + +#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..bab27549e7 --- /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 class 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::UARTFlushResult::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