From 5c4527905c99199e6fbeba88ca6eaeda751bfd6a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 12 Mar 2026 22:09:03 -1000 Subject: [PATCH 1/2] [api] Optimize plaintext varint encoding and devirtualize write_protobuf_packet - Replace ProtoSize::varint() call for message_type with inline ternary using named constants, avoiding the noinline varint_slow call overhead in the write_protobuf_messages loop (-32 bytes on ESP8266 Xtensa). - Add named varint threshold constants (VARINT_MAX_1_BYTE, etc.) to ProtoSize class, used by both varint() and varint_wide(). - Devirtualize write_protobuf_packet by moving it to the base class as a non-virtual inline method. The plaintext and noise implementations only differed in footer handling, which is now unified via a conditional resize based on frame_footer_size_. This eliminates one vtable slot and allows the compiler to fully inline the thin wrapper into callers (-192 bytes total flash on ESP8266). - Include proto.h from api_frame_helper.h (no circular dependency) to support the inline definition, replacing the forward declaration of ProtoWriteBuffer. --- esphome/components/api/api_frame_helper.h | 12 ++++++++--- .../components/api/api_frame_helper_noise.cpp | 8 ------- .../components/api/api_frame_helper_noise.h | 1 - .../api/api_frame_helper_plaintext.cpp | 21 +++++++++---------- .../api/api_frame_helper_plaintext.h | 1 - esphome/components/api/proto.h | 14 +++++++++---- 6 files changed, 29 insertions(+), 28 deletions(-) diff --git a/esphome/components/api/api_frame_helper.h b/esphome/components/api/api_frame_helper.h index 98de24501ea..3886f8768fc 100644 --- a/esphome/components/api/api_frame_helper.h +++ b/esphome/components/api/api_frame_helper.h @@ -12,6 +12,7 @@ #include "esphome/components/socket/socket.h" #include "esphome/core/application.h" #include "esphome/core/log.h" +#include "proto.h" namespace esphome::api { @@ -37,8 +38,6 @@ static constexpr uint16_t RX_BUF_NULL_TERMINATOR = 1; // Must be >= MAX_INITIAL_PER_BATCH in api_connection.h (enforced by static_assert there) static constexpr size_t MAX_MESSAGES_PER_BATCH = 34; -class ProtoWriteBuffer; - // Max client name length (e.g., "Home Assistant 2026.1.0.dev0" = 28 chars) static constexpr size_t CLIENT_INFO_NAME_MAX_LEN = 32; @@ -161,7 +160,14 @@ class APIFrameHelper { this->nodelay_state_++; } } - virtual APIError write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) = 0; + APIError write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) { + // Resize buffer to include footer space if needed (e.g. Noise MAC) + if (frame_footer_size_) + buffer.get_buffer()->resize(buffer.get_buffer()->size() + frame_footer_size_); + MessageInfo msg{type, 0, + static_cast(buffer.get_buffer()->size() - frame_header_padding_ - frame_footer_size_)}; + return write_protobuf_messages(buffer, std::span(&msg, 1)); + } // Write multiple protobuf messages in a single operation // messages contains (message_type, offset, length) for each message in the buffer // The buffer contains all messages with appropriate padding before each diff --git a/esphome/components/api/api_frame_helper_noise.cpp b/esphome/components/api/api_frame_helper_noise.cpp index f945253c89d..b635d84f168 100644 --- a/esphome/components/api/api_frame_helper_noise.cpp +++ b/esphome/components/api/api_frame_helper_noise.cpp @@ -450,14 +450,6 @@ APIError APINoiseFrameHelper::read_packet(ReadPacketBuffer *buffer) { buffer->type = type; return APIError::OK; } -APIError APINoiseFrameHelper::write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) { - // Resize to include MAC space (required for Noise encryption) - buffer.get_buffer()->resize(buffer.get_buffer()->size() + frame_footer_size_); - MessageInfo msg{type, 0, - static_cast(buffer.get_buffer()->size() - frame_header_padding_ - frame_footer_size_)}; - return write_protobuf_messages(buffer, std::span(&msg, 1)); -} - APIError APINoiseFrameHelper::write_protobuf_messages(ProtoWriteBuffer buffer, std::span messages) { APIError aerr = this->check_data_state_(); if (aerr != APIError::OK) diff --git a/esphome/components/api/api_frame_helper_noise.h b/esphome/components/api/api_frame_helper_noise.h index 83410febb26..a6b17ff3b92 100644 --- a/esphome/components/api/api_frame_helper_noise.h +++ b/esphome/components/api/api_frame_helper_noise.h @@ -22,7 +22,6 @@ class APINoiseFrameHelper final : public APIFrameHelper { APIError init() override; APIError loop() override; APIError read_packet(ReadPacketBuffer *buffer) override; - APIError write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) override; APIError write_protobuf_messages(ProtoWriteBuffer buffer, std::span messages) override; protected: diff --git a/esphome/components/api/api_frame_helper_plaintext.cpp b/esphome/components/api/api_frame_helper_plaintext.cpp index 007da7ef2b1..63c537a8da2 100644 --- a/esphome/components/api/api_frame_helper_plaintext.cpp +++ b/esphome/components/api/api_frame_helper_plaintext.cpp @@ -235,11 +235,6 @@ APIError APIPlaintextFrameHelper::read_packet(ReadPacketBuffer *buffer) { buffer->type = this->rx_header_parsed_type_; return APIError::OK; } -APIError APIPlaintextFrameHelper::write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) { - MessageInfo msg{type, 0, static_cast(buffer.get_buffer()->size() - frame_header_padding_)}; - return write_protobuf_messages(buffer, std::span(&msg, 1)); -} - APIError APIPlaintextFrameHelper::write_protobuf_messages(ProtoWriteBuffer buffer, std::span messages) { APIError aerr = this->check_data_state_(); @@ -257,9 +252,11 @@ APIError APIPlaintextFrameHelper::write_protobuf_messages(ProtoWriteBuffer buffe uint16_t total_write_len = 0; for (const auto &msg : messages) { - // Calculate varint sizes for header layout - uint8_t size_varint_len = api::ProtoSize::varint(static_cast(msg.payload_size)); - uint8_t type_varint_len = api::ProtoSize::varint(static_cast(msg.message_type)); + // Calculate varint sizes for header layout using inline ternary to avoid varint_slow call overhead + uint8_t size_varint_len = + msg.payload_size < ProtoSize::VARINT_MAX_1_BYTE ? 1 : (msg.payload_size < ProtoSize::VARINT_MAX_2_BYTE ? 2 : 3); + uint8_t type_varint_len = + msg.message_type < ProtoSize::VARINT_MAX_1_BYTE ? 1 : (msg.message_type < ProtoSize::VARINT_MAX_2_BYTE ? 2 : 3); uint8_t total_header_len = 1 + size_varint_len + type_varint_len; // Calculate where to start writing the header @@ -281,8 +278,8 @@ APIError APIPlaintextFrameHelper::write_protobuf_messages(ProtoWriteBuffer buffe // // Example 3 (large values): total_header_len = 6, header_offset = 6 - 6 = 0 // [0] - 0x00 indicator byte - // [1-3] - Payload size varint (3 bytes, for sizes 16384-2097151) - // [4-5] - Message type varint (2 bytes, for types 128-32767) + // [1-3] - Payload size varint (3 bytes, for sizes 16384-65535) + // [4-5] - Message type varint (2 bytes, for types 128-16383) // [6...] - Actual payload data // // The message starts at offset + frame_header_padding_ @@ -293,8 +290,10 @@ APIError APIPlaintextFrameHelper::write_protobuf_messages(ProtoWriteBuffer buffe // Write the plaintext header buf_start[header_offset] = 0x00; // indicator - // Encode varints directly into buffer + // Encode payload size varint encode_varint_to_buffer(msg.payload_size, buf_start + header_offset + 1); + + // Encode message type varint encode_varint_to_buffer(msg.message_type, buf_start + header_offset + 1 + size_varint_len); // Add iovec for this message (header + payload) diff --git a/esphome/components/api/api_frame_helper_plaintext.h b/esphome/components/api/api_frame_helper_plaintext.h index 96d47e9c7bf..f8161c039d3 100644 --- a/esphome/components/api/api_frame_helper_plaintext.h +++ b/esphome/components/api/api_frame_helper_plaintext.h @@ -19,7 +19,6 @@ class APIPlaintextFrameHelper final : public APIFrameHelper { APIError init() override; APIError loop() override; APIError read_packet(ReadPacketBuffer *buffer) override; - APIError write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) override; APIError write_protobuf_messages(ProtoWriteBuffer buffer, std::span messages) override; protected: diff --git a/esphome/components/api/proto.h b/esphome/components/api/proto.h index d1c955b1fb9..0ffabe22a9a 100644 --- a/esphome/components/api/proto.h +++ b/esphome/components/api/proto.h @@ -473,6 +473,12 @@ class ProtoDecodableMessage : public ProtoMessage { class ProtoSize { public: + // Varint encoding thresholds: values below each threshold fit in N bytes + static constexpr uint32_t VARINT_MAX_1_BYTE = 1 << 7; // 128 + static constexpr uint32_t VARINT_MAX_2_BYTE = 1 << 14; // 16384 + static constexpr uint32_t VARINT_MAX_3_BYTE = 1 << 21; // 2097152 + static constexpr uint32_t VARINT_MAX_4_BYTE = 1 << 28; // 268435456 + /** * @brief Calculates the size in bytes needed to encode a uint32_t value as a varint * @@ -480,7 +486,7 @@ class ProtoSize { * @return The number of bytes needed to encode the value */ static constexpr inline uint32_t ESPHOME_ALWAYS_INLINE varint(uint32_t value) { - if (value < 128) [[likely]] + if (value < VARINT_MAX_1_BYTE) [[likely]] return 1; // Fast path: 7 bits, most common case if (__builtin_is_constant_evaluated()) return varint_wide(value); @@ -492,11 +498,11 @@ class ProtoSize { static uint32_t varint_slow(uint32_t value) __attribute__((noinline)); // Shared cascade for values >= 128 (used by both constexpr and noinline paths) static constexpr inline uint32_t ESPHOME_ALWAYS_INLINE varint_wide(uint32_t value) { - if (value < 16384) + if (value < VARINT_MAX_2_BYTE) return 2; - if (value < 2097152) + if (value < VARINT_MAX_3_BYTE) return 3; - if (value < 268435456) + if (value < VARINT_MAX_4_BYTE) return 4; return 5; } From c7af451a56907e9cada0a41bc0473615514df408 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 12 Mar 2026 22:20:11 -1000 Subject: [PATCH 2/2] [api] Fix type_varint_len ternary for uint8_t message_type message_type is uint8_t (max 255), which fits in at most 2 varint bytes. Simplify the ternary to reflect this constraint. Also revert unnecessary comment split. --- esphome/components/api/api_frame_helper_plaintext.cpp | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/esphome/components/api/api_frame_helper_plaintext.cpp b/esphome/components/api/api_frame_helper_plaintext.cpp index 63c537a8da2..3c57821595d 100644 --- a/esphome/components/api/api_frame_helper_plaintext.cpp +++ b/esphome/components/api/api_frame_helper_plaintext.cpp @@ -255,8 +255,7 @@ APIError APIPlaintextFrameHelper::write_protobuf_messages(ProtoWriteBuffer buffe // Calculate varint sizes for header layout using inline ternary to avoid varint_slow call overhead uint8_t size_varint_len = msg.payload_size < ProtoSize::VARINT_MAX_1_BYTE ? 1 : (msg.payload_size < ProtoSize::VARINT_MAX_2_BYTE ? 2 : 3); - uint8_t type_varint_len = - msg.message_type < ProtoSize::VARINT_MAX_1_BYTE ? 1 : (msg.message_type < ProtoSize::VARINT_MAX_2_BYTE ? 2 : 3); + uint8_t type_varint_len = msg.message_type < ProtoSize::VARINT_MAX_1_BYTE ? 1 : 2; uint8_t total_header_len = 1 + size_varint_len + type_varint_len; // Calculate where to start writing the header @@ -290,10 +289,8 @@ APIError APIPlaintextFrameHelper::write_protobuf_messages(ProtoWriteBuffer buffe // Write the plaintext header buf_start[header_offset] = 0x00; // indicator - // Encode payload size varint + // Encode varints directly into buffer encode_varint_to_buffer(msg.payload_size, buf_start + header_offset + 1); - - // Encode message type varint encode_varint_to_buffer(msg.message_type, buf_start + header_offset + 1 + size_varint_len); // Add iovec for this message (header + payload)