From 555804db143d035796cf16f3f9e165bc64fa3f05 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 3 Apr 2026 22:03:26 -1000 Subject: [PATCH 01/13] no gap --- esphome/components/api/api_connection.cpp | 34 +++++--- esphome/components/api/api_connection.h | 9 +- esphome/components/api/api_frame_helper.h | 20 ++++- .../api/api_frame_helper_plaintext.cpp | 86 +++++++------------ esphome/components/api/proto.h | 11 +++ 5 files changed, 86 insertions(+), 74 deletions(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 0f456ecd0c7..42a1615e1d0 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -2009,11 +2009,16 @@ uint16_t APIConnection::encode_to_buffer(uint32_t calculated_size, MessageEncode } #endif // Cache frame sizes to avoid repeated virtual calls - const uint8_t header_padding = conn->helper_->frame_header_padding(); const uint8_t footer_size = conn->helper_->frame_footer_size(); - // Calculate total size with padding for buffer allocation - size_t total_calculated_size = calculated_size + header_padding + footer_size; + // Use actual header size for all batch messages (eliminates gaps between messages). + const uint8_t header_size = conn->helper_->frame_header_size(calculated_size, conn->batch_message_type_); + + // Store for process_batch_multi_ to pass into MessageInfo + conn->batch_header_size_ = header_size; + + // Calculate total size with header for buffer allocation + size_t total_calculated_size = calculated_size + header_size + footer_size; // Check if it fits if (total_calculated_size > remaining_size) @@ -2023,12 +2028,13 @@ uint16_t APIConnection::encode_to_buffer(uint32_t calculated_size, MessageEncode size_t to_add; if (conn->flags_.batch_first_message) { - // First message - buffer already prepared by caller, just clear flag + // First message - buffer already prepared by caller with max header padding. + // We only add payload bytes since padding is already in the buffer. + // The unused leading bytes (max_padding - actual_header) are skipped at write time. conn->flags_.batch_first_message = false; to_add = calculated_size; } else { - // Batch message second or later - // Reserve for full message, resize to include footer gap + header padding + payload + // Subsequent batch messages use exact header size — no gaps to_add = total_calculated_size; } @@ -2164,17 +2170,15 @@ void APIConnection::process_batch_multi_(APIBuffer &shared_buf, size_t num_items "MessageInfo must remain trivially destructible with this placement-new approach"); const size_t messages_to_process = std::min(num_items, MAX_MESSAGES_PER_BATCH); - const uint8_t frame_overhead = header_padding + footer_size; // Stack-allocated array for message info alignas(MessageInfo) char message_info_storage[MAX_MESSAGES_PER_BATCH * sizeof(MessageInfo)]; MessageInfo *message_info = reinterpret_cast(message_info_storage); size_t items_processed = 0; uint16_t remaining_size = std::numeric_limits::max(); - // Track where each message's header padding begins in the buffer - // For plaintext: this is where the 6-byte header padding starts - // For noise: this is where the 7-byte header padding starts - // The actual message data follows after the header padding + // Track where each message's header begins in the buffer + // First message: offset 0 (max padding, may have unused leading bytes) + // Subsequent messages: offset points to exact header start (no gaps) uint32_t current_offset = 0; // Process items and encode directly to buffer (up to our limit) @@ -2190,13 +2194,14 @@ void APIConnection::process_batch_multi_(APIBuffer &shared_buf, size_t num_items } // Message was encoded successfully - // payload_size is header_padding + actual payload size + footer_size - uint16_t proto_payload_size = payload_size - frame_overhead; + // payload_size = header_size + proto_payload_size + footer_size + uint16_t proto_payload_size = payload_size - this->batch_header_size_ - footer_size; // Use placement new to construct MessageInfo in pre-allocated stack array // This avoids default-constructing all MAX_MESSAGES_PER_BATCH elements // Explicit destruction is not needed because MessageInfo is trivially destructible, // as ensured by the static_assert in its definition. - new (&message_info[items_processed++]) MessageInfo(item.message_type, current_offset, proto_payload_size); + new (&message_info[items_processed++]) + MessageInfo(item.message_type, current_offset, proto_payload_size, this->batch_header_size_); // After first message, set remaining size to MAX_BATCH_PACKET_SIZE to avoid fragmentation if (items_processed == 1) { remaining_size = MAX_BATCH_PACKET_SIZE; @@ -2246,6 +2251,7 @@ void APIConnection::process_batch_multi_(APIBuffer &shared_buf, size_t num_items uint16_t APIConnection::dispatch_message_(const DeferredBatch::BatchItem &item, uint32_t remaining_size, bool batch_first) { this->flags_.batch_first_message = batch_first; + this->batch_message_type_ = item.message_type; #ifdef USE_EVENT // Events need aux_data_index to look up event type from entity if (item.message_type == EventResponse::MESSAGE_TYPE) { diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index 13d5273ecbb..d0bccf418ff 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -726,9 +726,14 @@ class APIConnection final : public APIServerConnectionBase { // 2-byte types immediately after flags_ (no padding between them) uint16_t client_api_version_major_{0}; uint16_t client_api_version_minor_{0}; - // 1-byte type to fill padding + // 1-byte types to fill remaining space before next 4-byte boundary ActiveIterator active_iterator_{ActiveIterator::NONE}; - // Total: 2 (flags) + 2 + 2 + 1 = 7 bytes, then 1 byte padding to next 4-byte boundary + uint8_t batch_message_type_{0}; // Current message type during batch encoding + // Total: 2 (flags) + 2 + 2 + 1 + 1 = 8 bytes, aligned to 4-byte boundary + + // Actual header size used by encode_to_buffer for the current message. + // Read by process_batch_multi_ to pass into MessageInfo. + uint8_t batch_header_size_{0}; uint32_t get_batch_delay_ms_() const { return this->parent_->get_batch_delay(); } // Message will use 8 more bytes than the minimum size, and typical diff --git a/esphome/components/api/api_frame_helper.h b/esphome/components/api/api_frame_helper.h index e22b9599090..3dfe6f0a7bb 100644 --- a/esphome/components/api/api_frame_helper.h +++ b/esphome/components/api/api_frame_helper.h @@ -53,8 +53,10 @@ struct MessageInfo { uint16_t offset; // Offset in buffer where message starts uint16_t payload_size; // Size of the message payload uint8_t message_type; // Message type (0-255) + uint8_t header_size; // Actual header size used (avoids recomputation in write path) - MessageInfo(uint8_t type, uint16_t off, uint16_t size) : offset(off), payload_size(size), message_type(type) {} + MessageInfo(uint8_t type, uint16_t off, uint16_t size, uint8_t hdr) + : offset(off), payload_size(size), message_type(type), header_size(hdr) {} }; enum class APIError : uint16_t { @@ -169,8 +171,22 @@ class APIFrameHelper { // messages contains (message_type, offset, length) for each message in the buffer. // The buffer contains all messages with appropriate padding before each. virtual APIError write_protobuf_messages(ProtoWriteBuffer buffer, std::span messages) = 0; - // Get the frame header padding required by this protocol + // Get the maximum frame header padding required by this protocol (worst case) uint8_t frame_header_padding() const { return frame_header_padding_; } + // Get the actual frame header size for a specific message. + // For noise: always returns frame_header_padding_ (fixed 7-byte header). + // For plaintext: computes actual size from varint lengths (3-6 bytes). + uint8_t frame_header_size(uint16_t payload_size, uint8_t message_type) const { +#if defined(USE_API_NOISE) && defined(USE_API_PLAINTEXT) + return this->frame_footer_size_ + ? this->frame_header_padding_ + : static_cast(1 + ProtoSize::varint16(payload_size) + ProtoSize::varint8(message_type)); +#elif defined(USE_API_NOISE) + return this->frame_header_padding_; +#else // USE_API_PLAINTEXT only + return static_cast(1 + ProtoSize::varint16(payload_size) + ProtoSize::varint8(message_type)); +#endif + } // Get the frame footer size required by this protocol uint8_t frame_footer_size() const { return frame_footer_size_; } // Check if socket has data ready to read diff --git a/esphome/components/api/api_frame_helper_plaintext.cpp b/esphome/components/api/api_frame_helper_plaintext.cpp index c905bdf2c97..d263aac3f69 100644 --- a/esphome/components/api/api_frame_helper_plaintext.cpp +++ b/esphome/components/api/api_frame_helper_plaintext.cpp @@ -227,15 +227,6 @@ APIError APIPlaintextFrameHelper::read_packet(ReadPacketBuffer *buffer) { buffer->type = this->rx_header_parsed_type_; return APIError::OK; } -// Compute varint encoded length for a 16-bit value (1, 2, or 3 bytes). -ESPHOME_ALWAYS_INLINE static inline uint8_t varint_encoded_length_16(uint16_t value) { - return value < ProtoSize::VARINT_THRESHOLD_1_BYTE ? 1 : (value < ProtoSize::VARINT_THRESHOLD_2_BYTE ? 2 : 3); -} - -// Compute varint encoded length for an 8-bit value (1 or 2 bytes). -ESPHOME_ALWAYS_INLINE static inline uint8_t varint_encoded_length_8(uint8_t value) { - return value < ProtoSize::VARINT_THRESHOLD_1_BYTE ? 1 : 2; -} // Encode a 16-bit varint (1-3 bytes) using pre-computed length. ESPHOME_ALWAYS_INLINE static inline void encode_varint_16(uint16_t value, uint8_t varint_len, uint8_t *p) { @@ -261,39 +252,26 @@ ESPHOME_ALWAYS_INLINE static inline void encode_varint_8(uint8_t value, uint8_t } // Write plaintext header into pre-allocated padding before payload. +// padding_size: bytes reserved before payload (HEADER_PADDING for first/single msg, +// actual header size for contiguous batch messages). // Returns the total header length (indicator + varints). ESPHOME_ALWAYS_INLINE static inline uint8_t write_plaintext_header(uint8_t *buf_start, uint16_t payload_size, - uint8_t message_type) { - uint8_t size_varint_len = varint_encoded_length_16(payload_size); - uint8_t type_varint_len = varint_encoded_length_8(message_type); + uint8_t message_type, uint8_t padding_size) { + uint8_t size_varint_len = ProtoSize::varint16(payload_size); + uint8_t type_varint_len = ProtoSize::varint8(message_type); uint8_t total_header_len = 1 + size_varint_len + type_varint_len; - // Calculate where to start writing the header - // The header starts at the latest possible position to minimize unused padding + // The header is right-justified within the padding so it sits immediately before payload. // - // Example 1 (small values): total_header_len = 3, header_offset = 6 - 3 = 3 - // [0-2] - Unused padding - // [3] - 0x00 indicator byte - // [4] - Payload size varint (1 byte, for sizes 0-127) - // [5] - Message type varint (1 byte, for types 0-127) - // [6...] - Actual payload data + // Single/first message (padding_size = HEADER_PADDING = 6): + // Example (small, header=3): [0-2] unused | [3] 0x00 | [4] size | [5] type | [6...] payload + // Example (medium, header=4): [0-1] unused | [2] 0x00 | [3-4] size | [5] type | [6...] payload + // Example (large, header=6): [0] 0x00 | [1-3] size | [4-5] type | [6...] payload // - // Example 2 (medium values): total_header_len = 4, header_offset = 6 - 4 = 2 - // [0-1] - Unused padding - // [2] - 0x00 indicator byte - // [3-4] - Payload size varint (2 bytes, for sizes 128-16383) - // [5] - Message type varint (1 byte, for types 0-127) - // [6...] - Actual payload data - // - // 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-65535) - // [4-5] - Message type varint (2 bytes, for types 128-16383) - // [6...] - Actual payload data - // - // The message starts at offset + frame_header_padding - // So we write the header starting at offset + HEADER_PADDING - total_header_len - uint32_t header_offset = APIPlaintextFrameHelper::HEADER_PADDING - total_header_len; + // Batch messages 2+ (padding_size = actual header size, no unused bytes): + // Example (small, header=3): [0] 0x00 | [1] size | [2] type | [3...] payload + // Example (medium, header=4): [0] 0x00 | [1-2] size | [3] type | [4...] payload + uint32_t header_offset = padding_size - total_header_len; // Write the plaintext header buf_start[header_offset] = 0x00; // indicator @@ -312,7 +290,7 @@ APIError APIPlaintextFrameHelper::write_protobuf_packet(uint8_t type, ProtoWrite uint16_t payload_size = static_cast(buffer.get_buffer()->size() - HEADER_PADDING); uint8_t *buffer_data = buffer.get_buffer()->data(); - uint8_t header_len = write_plaintext_header(buffer_data, payload_size, type); + uint8_t header_len = write_plaintext_header(buffer_data, payload_size, type, HEADER_PADDING); return this->write_raw_fast_buf_(buffer_data + HEADER_PADDING - header_len, static_cast(header_len + payload_size)); } @@ -324,28 +302,24 @@ APIError APIPlaintextFrameHelper::write_protobuf_messages(ProtoWriteBuffer buffe assert(!messages.empty()); #endif uint8_t *buffer_data = buffer.get_buffer()->data(); - uint8_t *write_start = nullptr; - uint8_t *write_end = nullptr; - // Write headers and compact messages to close 0-3 byte varint padding gaps. - // First iteration records start position via continue; subsequent iterations - // memmove to close gaps between messages. - for (const auto &msg : messages) { - uint8_t header_len = write_plaintext_header(buffer_data + msg.offset, msg.payload_size, msg.message_type); - uint8_t *src = buffer_data + msg.offset + HEADER_PADDING - header_len; - uint16_t msg_len = header_len + msg.payload_size; - if (write_start == nullptr) { - write_start = src; - write_end = src + msg_len; - continue; - } - if (src != write_end) { - memmove(write_end, src, msg_len); - } - write_end += msg_len; + // First message has max padding (HEADER_PADDING), may have unused leading bytes. + // Subsequent messages were encoded with exact header sizes, so they are contiguous. + const auto &first = messages[0]; + uint8_t header_len = + write_plaintext_header(buffer_data + first.offset, first.payload_size, first.message_type, HEADER_PADDING); + uint8_t *write_start = buffer_data + first.offset + HEADER_PADDING - header_len; + uint16_t total_len = header_len + first.payload_size; + + // Write headers for remaining messages — padding equals actual header size + // (stored in msg.header_size), so write_plaintext_header writes at offset 0 with no gap. + for (size_t i = 1; i < messages.size(); i++) { + const auto &msg = messages[i]; + write_plaintext_header(buffer_data + msg.offset, msg.payload_size, msg.message_type, msg.header_size); + total_len += msg.header_size + msg.payload_size; } - return this->write_raw_fast_buf_(write_start, static_cast(write_end - write_start)); + return this->write_raw_fast_buf_(write_start, total_len); } } // namespace esphome::api diff --git a/esphome/components/api/proto.h b/esphome/components/api/proto.h index b629018a919..4e122ea71e2 100644 --- a/esphome/components/api/proto.h +++ b/esphome/components/api/proto.h @@ -518,6 +518,17 @@ class ProtoSize { static constexpr uint32_t VARINT_THRESHOLD_3_BYTE = 1 << 21; // 2097152 static constexpr uint32_t VARINT_THRESHOLD_4_BYTE = 1 << 28; // 268435456 + // Varint encoded length for a 16-bit value (1, 2, or 3 bytes). + // Fully inline — no slow path call for values >= 128. + static constexpr inline uint8_t ESPHOME_ALWAYS_INLINE varint16(uint16_t value) { + return value < VARINT_THRESHOLD_1_BYTE ? 1 : (value < VARINT_THRESHOLD_2_BYTE ? 2 : 3); + } + + // Varint encoded length for an 8-bit value (1 or 2 bytes). + static constexpr inline uint8_t ESPHOME_ALWAYS_INLINE varint8(uint8_t value) { + return value < VARINT_THRESHOLD_1_BYTE ? 1 : 2; + } + /** * @brief Calculates the size in bytes needed to encode a uint32_t value as a varint * From d795dc2f1c57c5d6f0c1228cfd1d6ccb79e43ac0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 3 Apr 2026 22:09:46 -1000 Subject: [PATCH 02/13] fix bench --- tests/benchmarks/components/api/bench_plaintext_frame.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/benchmarks/components/api/bench_plaintext_frame.cpp b/tests/benchmarks/components/api/bench_plaintext_frame.cpp index 0caa50c748f..74c640a093c 100644 --- a/tests/benchmarks/components/api/bench_plaintext_frame.cpp +++ b/tests/benchmarks/components/api/bench_plaintext_frame.cpp @@ -75,7 +75,7 @@ static void PlaintextFrame_WriteBatch5(benchmark::State &state) { for (auto _ : state) { for (int i = 0; i < kInnerIterations; i++) { buffer.clear(); - MessageInfo messages[5] = {{0, 0, 0}, {0, 0, 0}, {0, 0, 0}, {0, 0, 0}, {0, 0, 0}}; + MessageInfo messages[5] = {{0, 0, 0, 0}, {0, 0, 0, 0}, {0, 0, 0, 0}, {0, 0, 0, 0}, {0, 0, 0, 0}}; for (int j = 0; j < 5; j++) { uint16_t offset = buffer.size(); @@ -89,7 +89,7 @@ static void PlaintextFrame_WriteBatch5(benchmark::State &state) { ProtoWriteBuffer writer(&buffer, offset + padding); msg.encode(writer); - messages[j] = MessageInfo(SensorStateResponse::MESSAGE_TYPE, offset, size); + messages[j] = MessageInfo(SensorStateResponse::MESSAGE_TYPE, offset, size, padding); } helper->write_protobuf_messages(ProtoWriteBuffer(&buffer, 0), std::span(messages, 5)); From 7911b9f0292d6e54dd0fcf3b983cfe86ae0a4a73 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 3 Apr 2026 22:14:07 -1000 Subject: [PATCH 03/13] clean --- esphome/components/api/api_connection.cpp | 36 +++++++++-------------- 1 file changed, 14 insertions(+), 22 deletions(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 42a1615e1d0..a714df67909 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -2011,33 +2011,25 @@ uint16_t APIConnection::encode_to_buffer(uint32_t calculated_size, MessageEncode // Cache frame sizes to avoid repeated virtual calls const uint8_t footer_size = conn->helper_->frame_footer_size(); - // Use actual header size for all batch messages (eliminates gaps between messages). - const uint8_t header_size = conn->helper_->frame_header_size(calculated_size, conn->batch_message_type_); + size_t to_add; + if (conn->flags_.batch_first_message) { + // First message - buffer already prepared by caller with max header padding. + // Use max padding for header size (actual header computed at write time). + conn->flags_.batch_first_message = false; + conn->batch_header_size_ = conn->helper_->frame_header_padding(); + to_add = calculated_size; // Padding already in buffer + } else { + // Subsequent batch messages use exact header size — no gaps + conn->batch_header_size_ = conn->helper_->frame_header_size(calculated_size, conn->batch_message_type_); + to_add = calculated_size + conn->batch_header_size_ + footer_size; + } - // Store for process_batch_multi_ to pass into MessageInfo - conn->batch_header_size_ = header_size; - - // Calculate total size with header for buffer allocation - size_t total_calculated_size = calculated_size + header_size + footer_size; - - // Check if it fits + // total_calculated_size reflects wire size (header + payload + footer) + size_t total_calculated_size = calculated_size + conn->batch_header_size_ + footer_size; if (total_calculated_size > remaining_size) return 0; // Doesn't fit auto &shared_buf = conn->parent_->get_shared_buffer_ref(); - - size_t to_add; - if (conn->flags_.batch_first_message) { - // First message - buffer already prepared by caller with max header padding. - // We only add payload bytes since padding is already in the buffer. - // The unused leading bytes (max_padding - actual_header) are skipped at write time. - conn->flags_.batch_first_message = false; - to_add = calculated_size; - } else { - // Subsequent batch messages use exact header size — no gaps - to_add = total_calculated_size; - } - shared_buf.resize(shared_buf.size() + to_add); ProtoWriteBuffer buffer{&shared_buf, shared_buf.size() - calculated_size}; encode_fn(msg, buffer); From b5989e7a7b980bcc83d40842551131c2e2725c5c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 3 Apr 2026 22:15:16 -1000 Subject: [PATCH 04/13] clean --- esphome/components/api/api_frame_helper.h | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/esphome/components/api/api_frame_helper.h b/esphome/components/api/api_frame_helper.h index 3dfe6f0a7bb..d1215388d2c 100644 --- a/esphome/components/api/api_frame_helper.h +++ b/esphome/components/api/api_frame_helper.h @@ -49,6 +49,9 @@ struct ReadPacketBuffer { }; // Packed message info structure to minimize memory usage +// Note: message_type is uint8_t — all current protobuf message types fit in 8 bits. +// The noise wire format encodes types as 16-bit, but the high byte is always 0. +// If message types ever exceed 255, this and encrypt_noise_message_ must be updated. struct MessageInfo { uint16_t offset; // Offset in buffer where message starts uint16_t payload_size; // Size of the message payload @@ -176,6 +179,9 @@ class APIFrameHelper { // Get the actual frame header size for a specific message. // For noise: always returns frame_header_padding_ (fixed 7-byte header). // For plaintext: computes actual size from varint lengths (3-6 bytes). + // Distinguishes protocols via frame_footer_size_ (noise always has a non-zero MAC + // footer, plaintext has footer=0). If a protocol with a plaintext footer is ever + // added, this should become a virtual method. uint8_t frame_header_size(uint16_t payload_size, uint8_t message_type) const { #if defined(USE_API_NOISE) && defined(USE_API_PLAINTEXT) return this->frame_footer_size_ From 3a564633b773552c53769157ef5640e2ad188f2d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 3 Apr 2026 22:20:11 -1000 Subject: [PATCH 05/13] clean --- esphome/components/api/api_connection.cpp | 28 +++++++++++------------ 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index a714df67909..a25be074e30 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -2009,27 +2009,27 @@ uint16_t APIConnection::encode_to_buffer(uint32_t calculated_size, MessageEncode } #endif // Cache frame sizes to avoid repeated virtual calls + const uint8_t header_padding = conn->helper_->frame_header_padding(); const uint8_t footer_size = conn->helper_->frame_footer_size(); - size_t to_add; - if (conn->flags_.batch_first_message) { - // First message - buffer already prepared by caller with max header padding. - // Use max padding for header size (actual header computed at write time). - conn->flags_.batch_first_message = false; - conn->batch_header_size_ = conn->helper_->frame_header_padding(); - to_add = calculated_size; // Padding already in buffer - } else { - // Subsequent batch messages use exact header size — no gaps - conn->batch_header_size_ = conn->helper_->frame_header_size(calculated_size, conn->batch_message_type_); - to_add = calculated_size + conn->batch_header_size_ + footer_size; - } + // Calculate total size with padding for buffer allocation + size_t total_calculated_size = calculated_size + header_padding + footer_size; - // total_calculated_size reflects wire size (header + payload + footer) - size_t total_calculated_size = calculated_size + conn->batch_header_size_ + footer_size; + // Check if it fits if (total_calculated_size > remaining_size) return 0; // Doesn't fit auto &shared_buf = conn->parent_->get_shared_buffer_ref(); + + // First message: padding already in buffer, only add payload. + // Subsequent messages: use exact header size for gap-free packing. + bool first = conn->flags_.batch_first_message; + if (first) + conn->flags_.batch_first_message = false; + conn->batch_header_size_ = + first ? header_padding : conn->helper_->frame_header_size(calculated_size, conn->batch_message_type_); + size_t to_add = first ? calculated_size : (calculated_size + conn->batch_header_size_ + footer_size); + shared_buf.resize(shared_buf.size() + to_add); ProtoWriteBuffer buffer{&shared_buf, shared_buf.size() - calculated_size}; encode_fn(msg, buffer); From dee78b8a8e48e543f8d5f096d56f37e1bf9721fe Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 3 Apr 2026 22:39:19 -1000 Subject: [PATCH 06/13] fix slowdown --- esphome/components/api/api_connection.cpp | 42 +------------------ esphome/components/api/api_connection.h | 50 +++++++++++++++++++++-- 2 files changed, 48 insertions(+), 44 deletions(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index a25be074e30..9b8dce7b41a 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -1996,47 +1996,7 @@ bool APIConnection::send_message_(uint32_t payload_size, uint8_t message_type, M encode_fn(msg, buffer); return this->send_buffer(ProtoWriteBuffer{&shared_buf}, message_type); } -// Encodes a message to the buffer and returns the total number of bytes used, -// including header and footer overhead. Returns 0 if the message doesn't fit. -uint16_t APIConnection::encode_to_buffer(uint32_t calculated_size, MessageEncodeFn encode_fn, const void *msg, - APIConnection *conn, uint32_t remaining_size) { -#ifdef HAS_PROTO_MESSAGE_DUMP - if (conn->flags_.log_only_mode) { - auto *proto_msg = static_cast(msg); - DumpBuffer dump_buf; - conn->log_send_message_(proto_msg->message_name(), proto_msg->dump_to(dump_buf)); - return 1; - } -#endif - // Cache frame sizes to avoid repeated virtual calls - const uint8_t header_padding = conn->helper_->frame_header_padding(); - const uint8_t footer_size = conn->helper_->frame_footer_size(); - - // Calculate total size with padding for buffer allocation - size_t total_calculated_size = calculated_size + header_padding + footer_size; - - // Check if it fits - if (total_calculated_size > remaining_size) - return 0; // Doesn't fit - - auto &shared_buf = conn->parent_->get_shared_buffer_ref(); - - // First message: padding already in buffer, only add payload. - // Subsequent messages: use exact header size for gap-free packing. - bool first = conn->flags_.batch_first_message; - if (first) - conn->flags_.batch_first_message = false; - conn->batch_header_size_ = - first ? header_padding : conn->helper_->frame_header_size(calculated_size, conn->batch_message_type_); - size_t to_add = first ? calculated_size : (calculated_size + conn->batch_header_size_ + footer_size); - - shared_buf.resize(shared_buf.size() + to_add); - ProtoWriteBuffer buffer{&shared_buf, shared_buf.size() - calculated_size}; - encode_fn(msg, buffer); - - // Return total size (header + payload + footer) - return static_cast(total_calculated_size); -} +// encode_to_buffer is defined inline in api_connection.h (ESPHOME_ALWAYS_INLINE) bool APIConnection::send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) { const bool is_log_message = (message_type == SubscribeLogsResponse::MESSAGE_TYPE); diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index d0bccf418ff..037b13d17c1 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -402,9 +402,53 @@ class APIConnection final : public APIServerConnectionBase { // Non-template buffer management for send_message bool send_message_(uint32_t payload_size, uint8_t message_type, MessageEncodeFn encode_fn, const void *msg); - // Non-template buffer management for batch encoding - static uint16_t encode_to_buffer(uint32_t calculated_size, MessageEncodeFn encode_fn, const void *msg, - APIConnection *conn, uint32_t remaining_size); + // Non-template buffer management for batch encoding. + // ALWAYS_INLINE so it merges into each call site (encode_message_to_buffer, + // fill_and_encode_entity_state) — the compiler can then inline the encode_fn + // callback and eliminate the indirect call. + static inline uint16_t ESPHOME_ALWAYS_INLINE encode_to_buffer(uint32_t calculated_size, MessageEncodeFn encode_fn, + const void *msg, APIConnection *conn, + uint32_t remaining_size) { +#ifdef HAS_PROTO_MESSAGE_DUMP + if (conn->flags_.log_only_mode) { + auto *proto_msg = static_cast(msg); + DumpBuffer dump_buf; + conn->log_send_message_(proto_msg->message_name(), proto_msg->dump_to(dump_buf)); + return 1; + } +#endif + // Cache frame sizes to avoid repeated virtual calls + const uint8_t header_padding = conn->helper_->frame_header_padding(); + const uint8_t footer_size = conn->helper_->frame_footer_size(); + + // Calculate total size with padding for buffer allocation + size_t total_calculated_size = calculated_size + header_padding + footer_size; + + // Check if it fits + if (total_calculated_size > remaining_size) + return 0; // Doesn't fit + + auto &shared_buf = conn->parent_->get_shared_buffer_ref(); + + // First message: padding already in buffer, only add payload. + // Subsequent messages: use exact header size for gap-free packing. + size_t to_add; + if (conn->flags_.batch_first_message) { + conn->flags_.batch_first_message = false; + conn->batch_header_size_ = header_padding; + to_add = calculated_size; + } else { + conn->batch_header_size_ = conn->helper_->frame_header_size(calculated_size, conn->batch_message_type_); + to_add = calculated_size + conn->batch_header_size_ + footer_size; + } + + shared_buf.resize(shared_buf.size() + to_add); + ProtoWriteBuffer buffer{&shared_buf, shared_buf.size() - calculated_size}; + encode_fn(msg, buffer); + + // Return total size (header + payload + footer) + return static_cast(total_calculated_size); + } // Thin template wrapper — computes size, delegates buffer work to non-template helper template static uint16_t encode_message_to_buffer(T &msg, APIConnection *conn, uint32_t remaining_size) { From 599f784a9054f94c037961cdb2a231a923eb7ea7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 3 Apr 2026 22:46:36 -1000 Subject: [PATCH 07/13] fix bloat --- esphome/components/api/api_connection.cpp | 6 ++++++ esphome/components/api/api_connection.h | 11 +++++++---- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 9b8dce7b41a..2f8c6f4abd1 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -1997,6 +1997,12 @@ bool APIConnection::send_message_(uint32_t payload_size, uint8_t message_type, M return this->send_buffer(ProtoWriteBuffer{&shared_buf}, message_type); } // encode_to_buffer is defined inline in api_connection.h (ESPHOME_ALWAYS_INLINE) + +// Noinline wrapper for zero-payload messages — single shared copy instead of +// duplicating encode_to_buffer at each cold call site. +uint16_t APIConnection::encode_empty_to_buffer(const void *msg, APIConnection *conn, uint32_t remaining_size) { + return encode_to_buffer(0, &encode_msg_noop, msg, conn, remaining_size); +} bool APIConnection::send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) { const bool is_log_message = (message_type == SubscribeLogsResponse::MESSAGE_TYPE); diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index 037b13d17c1..10972e04863 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -403,9 +403,8 @@ class APIConnection final : public APIServerConnectionBase { bool send_message_(uint32_t payload_size, uint8_t message_type, MessageEncodeFn encode_fn, const void *msg); // Non-template buffer management for batch encoding. - // ALWAYS_INLINE so it merges into each call site (encode_message_to_buffer, - // fill_and_encode_entity_state) — the compiler can then inline the encode_fn - // callback and eliminate the indirect call. + // ALWAYS_INLINE so it merges into each call site — the compiler can then + // inline the encode_fn callback and eliminate the indirect call. static inline uint16_t ESPHOME_ALWAYS_INLINE encode_to_buffer(uint32_t calculated_size, MessageEncodeFn encode_fn, const void *msg, APIConnection *conn, uint32_t remaining_size) { @@ -450,10 +449,14 @@ class APIConnection final : public APIServerConnectionBase { return static_cast(total_calculated_size); } + // Noinline path for zero-payload messages (ping, disconnect, list_info_done). + // Avoids duplicating encode_to_buffer at each cold call site. + static uint16_t encode_empty_to_buffer(const void *msg, APIConnection *conn, uint32_t remaining_size); + // Thin template wrapper — computes size, delegates buffer work to non-template helper template static uint16_t encode_message_to_buffer(T &msg, APIConnection *conn, uint32_t remaining_size) { if constexpr (T::ESTIMATED_SIZE == 0) { - return encode_to_buffer(0, &encode_msg_noop, &msg, conn, remaining_size); + return encode_empty_to_buffer(&msg, conn, remaining_size); } else { return encode_to_buffer(msg.calculate_size(), &proto_encode_msg, &msg, conn, remaining_size); } From 72df34a507c22d115dfc4e9fd9fba6bcbc455ba2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 3 Apr 2026 23:07:06 -1000 Subject: [PATCH 08/13] fix bloat --- esphome/components/api/api_connection.cpp | 6 +- esphome/components/api/api_connection.h | 64 ++++++++++++++-------- esphome/components/api/api_pb2_service.cpp | 5 ++ esphome/components/api/api_pb2_service.h | 2 + 4 files changed, 49 insertions(+), 28 deletions(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 2f8c6f4abd1..bddfe759e16 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -1998,11 +1998,7 @@ bool APIConnection::send_message_(uint32_t payload_size, uint8_t message_type, M } // encode_to_buffer is defined inline in api_connection.h (ESPHOME_ALWAYS_INLINE) -// Noinline wrapper for zero-payload messages — single shared copy instead of -// duplicating encode_to_buffer at each cold call site. -uint16_t APIConnection::encode_empty_to_buffer(const void *msg, APIConnection *conn, uint32_t remaining_size) { - return encode_to_buffer(0, &encode_msg_noop, msg, conn, remaining_size); -} +// encode_empty_to_buffer is defined inline in api_connection.h bool APIConnection::send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) { const bool is_log_message = (message_type == SubscribeLogsResponse::MESSAGE_TYPE); diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index 10972e04863..0fb70256083 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -410,53 +410,71 @@ class APIConnection final : public APIServerConnectionBase { uint32_t remaining_size) { #ifdef HAS_PROTO_MESSAGE_DUMP if (conn->flags_.log_only_mode) { - auto *proto_msg = static_cast(msg); - DumpBuffer dump_buf; - conn->log_send_message_(proto_msg->message_name(), proto_msg->dump_to(dump_buf)); + conn->log_send_message_(msg); return 1; } #endif - // Cache frame sizes to avoid repeated virtual calls - const uint8_t header_padding = conn->helper_->frame_header_padding(); const uint8_t footer_size = conn->helper_->frame_footer_size(); - // Calculate total size with padding for buffer allocation - size_t total_calculated_size = calculated_size + header_padding + footer_size; - - // Check if it fits - if (total_calculated_size > remaining_size) - return 0; // Doesn't fit - - auto &shared_buf = conn->parent_->get_shared_buffer_ref(); - - // First message: padding already in buffer, only add payload. - // Subsequent messages: use exact header size for gap-free packing. + // First message uses max padding (already in buffer), subsequent use exact header size size_t to_add; if (conn->flags_.batch_first_message) { conn->flags_.batch_first_message = false; - conn->batch_header_size_ = header_padding; + conn->batch_header_size_ = conn->helper_->frame_header_padding(); to_add = calculated_size; } else { conn->batch_header_size_ = conn->helper_->frame_header_size(calculated_size, conn->batch_message_type_); to_add = calculated_size + conn->batch_header_size_ + footer_size; } + // Check if it fits (using actual header size, not max padding) + uint16_t total_calculated_size = calculated_size + conn->batch_header_size_ + footer_size; + if (total_calculated_size > remaining_size) + return 0; + + auto &shared_buf = conn->parent_->get_shared_buffer_ref(); shared_buf.resize(shared_buf.size() + to_add); ProtoWriteBuffer buffer{&shared_buf, shared_buf.size() - calculated_size}; encode_fn(msg, buffer); - // Return total size (header + payload + footer) - return static_cast(total_calculated_size); + return total_calculated_size; } - // Noinline path for zero-payload messages (ping, disconnect, list_info_done). - // Avoids duplicating encode_to_buffer at each cold call site. - static uint16_t encode_empty_to_buffer(const void *msg, APIConnection *conn, uint32_t remaining_size); + // Specialized path for zero-payload messages (ping, disconnect, list_info_done). + // No payload means no encode callback and no payload resize needed. + // Noinline — the three callers share a single copy via a call. + __attribute__((noinline)) static uint16_t encode_empty_to_buffer(const void *msg, uint8_t message_type, + APIConnection *conn, uint32_t remaining_size) { +#ifdef HAS_PROTO_MESSAGE_DUMP + if (conn->flags_.log_only_mode) { + conn->log_send_message_(msg); + return 1; + } +#endif + const uint8_t footer_size = conn->helper_->frame_footer_size(); + bool first = conn->flags_.batch_first_message; + if (first) { + conn->flags_.batch_first_message = false; + conn->batch_header_size_ = conn->helper_->frame_header_padding(); + } else { + conn->batch_header_size_ = conn->helper_->frame_header_size(0, message_type); + } + uint16_t total = conn->batch_header_size_ + footer_size; + if (total > remaining_size) + return 0; + if (!first) { + // Non-first: grow buffer by header + footer (no payload) + auto &shared_buf = conn->parent_->get_shared_buffer_ref(); + shared_buf.resize(shared_buf.size() + total); + } + // First message: padding already in buffer from prepare_first_message_buffer + return total; + } // Thin template wrapper — computes size, delegates buffer work to non-template helper template static uint16_t encode_message_to_buffer(T &msg, APIConnection *conn, uint32_t remaining_size) { if constexpr (T::ESTIMATED_SIZE == 0) { - return encode_empty_to_buffer(&msg, conn, remaining_size); + return encode_empty_to_buffer(&msg, T::MESSAGE_TYPE, conn, remaining_size); } else { return encode_to_buffer(msg.calculate_size(), &proto_encode_msg, &msg, conn, remaining_size); } diff --git a/esphome/components/api/api_pb2_service.cpp b/esphome/components/api/api_pb2_service.cpp index b41233eddd1..20b201fec11 100644 --- a/esphome/components/api/api_pb2_service.cpp +++ b/esphome/components/api/api_pb2_service.cpp @@ -12,6 +12,11 @@ static const char *const TAG = "api.service"; void APIServerConnectionBase::log_send_message_(const LogString *name, const char *dump) { ESP_LOGVV(TAG, "send_message %s: %s", LOG_STR_ARG(name), dump); } +void APIServerConnectionBase::log_send_message_(const void *msg) { + auto *proto_msg = static_cast(msg); + DumpBuffer dump_buf; + this->log_send_message_(proto_msg->message_name(), proto_msg->dump_to(dump_buf)); +} void APIServerConnectionBase::log_receive_message_(const LogString *name, const ProtoMessage &msg) { DumpBuffer dump_buf; ESP_LOGVV(TAG, "%s: %s", LOG_STR_ARG(name), msg.dump_to(dump_buf)); diff --git a/esphome/components/api/api_pb2_service.h b/esphome/components/api/api_pb2_service.h index 6ff988902f3..1ae085fb5e1 100644 --- a/esphome/components/api/api_pb2_service.h +++ b/esphome/components/api/api_pb2_service.h @@ -13,6 +13,8 @@ class APIServerConnectionBase { #ifdef HAS_PROTO_MESSAGE_DUMP protected: void log_send_message_(const LogString *name, const char *dump); + // Log a message being sent from a void pointer (for use in encode_to_buffer paths) + void log_send_message_(const void *msg); void log_receive_message_(const LogString *name, const ProtoMessage &msg); void log_receive_message_(const LogString *name); From 5553962eaebca24a78b48947d8ccb7b6071ef147 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 3 Apr 2026 23:16:56 -1000 Subject: [PATCH 09/13] fix bloat --- esphome/components/api/api_connection.cpp | 8 +++- esphome/components/api/api_connection.h | 48 ++++++----------------- 2 files changed, 18 insertions(+), 38 deletions(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index bddfe759e16..2ccb597bbce 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -398,7 +398,7 @@ uint16_t APIConnection::fill_and_encode_entity_info(EntityBase *entity, InfoResp #ifdef USE_DEVICES msg.device_id = entity->get_device_id(); #endif - return encode_to_buffer(size_fn(&msg), encode_fn, &msg, conn, remaining_size); + return encode_to_buffer_slow_(size_fn(&msg), encode_fn, &msg, conn, remaining_size); } uint16_t APIConnection::fill_and_encode_entity_info_with_device_class(EntityBase *entity, InfoResponseProtoMessage &msg, @@ -1998,7 +1998,11 @@ bool APIConnection::send_message_(uint32_t payload_size, uint8_t message_type, M } // encode_to_buffer is defined inline in api_connection.h (ESPHOME_ALWAYS_INLINE) -// encode_empty_to_buffer is defined inline in api_connection.h +// Noinline version for cold paths — single shared copy +uint16_t APIConnection::encode_to_buffer_slow_(uint32_t calculated_size, MessageEncodeFn encode_fn, const void *msg, + APIConnection *conn, uint32_t remaining_size) { + return encode_to_buffer(calculated_size, encode_fn, msg, conn, remaining_size); +} bool APIConnection::send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) { const bool is_log_message = (message_type == SubscribeLogsResponse::MESSAGE_TYPE); diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index 0fb70256083..6fbf24c215e 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -402,9 +402,8 @@ class APIConnection final : public APIServerConnectionBase { // Non-template buffer management for send_message bool send_message_(uint32_t payload_size, uint8_t message_type, MessageEncodeFn encode_fn, const void *msg); - // Non-template buffer management for batch encoding. - // ALWAYS_INLINE so it merges into each call site — the compiler can then - // inline the encode_fn callback and eliminate the indirect call. + // Core batch encoding logic. Computes header size, checks fit, resizes buffer, encodes. + // ALWAYS_INLINE so the compiler can devirtualize encode_fn at hot call sites. static inline uint16_t ESPHOME_ALWAYS_INLINE encode_to_buffer(uint32_t calculated_size, MessageEncodeFn encode_fn, const void *msg, APIConnection *conn, uint32_t remaining_size) { @@ -440,43 +439,20 @@ class APIConnection final : public APIServerConnectionBase { return total_calculated_size; } - // Specialized path for zero-payload messages (ping, disconnect, list_info_done). - // No payload means no encode callback and no payload resize needed. - // Noinline — the three callers share a single copy via a call. - __attribute__((noinline)) static uint16_t encode_empty_to_buffer(const void *msg, uint8_t message_type, - APIConnection *conn, uint32_t remaining_size) { -#ifdef HAS_PROTO_MESSAGE_DUMP - if (conn->flags_.log_only_mode) { - conn->log_send_message_(msg); - return 1; - } -#endif - const uint8_t footer_size = conn->helper_->frame_footer_size(); - bool first = conn->flags_.batch_first_message; - if (first) { - conn->flags_.batch_first_message = false; - conn->batch_header_size_ = conn->helper_->frame_header_padding(); - } else { - conn->batch_header_size_ = conn->helper_->frame_header_size(0, message_type); - } - uint16_t total = conn->batch_header_size_ + footer_size; - if (total > remaining_size) - return 0; - if (!first) { - // Non-first: grow buffer by header + footer (no payload) - auto &shared_buf = conn->parent_->get_shared_buffer_ref(); - shared_buf.resize(shared_buf.size() + total); - } - // First message: padding already in buffer from prepare_first_message_buffer - return total; - } + // Noinline version of encode_to_buffer for cold paths (entity info, zero-payload messages). + // All cold callers share this single copy instead of each getting an ALWAYS_INLINE expansion. + static uint16_t encode_to_buffer_slow_(uint32_t calculated_size, MessageEncodeFn encode_fn, const void *msg, + APIConnection *conn, uint32_t remaining_size); - // Thin template wrapper — computes size, delegates buffer work to non-template helper + // Thin template wrapper — uses noinline encode_to_buffer_slow_ since + // encode_message_to_buffer callers are cold paths (zero-payload control messages). + // Hot paths (state/info) go through fill_and_encode_entity_state/info instead. template static uint16_t encode_message_to_buffer(T &msg, APIConnection *conn, uint32_t remaining_size) { + conn->batch_message_type_ = T::MESSAGE_TYPE; if constexpr (T::ESTIMATED_SIZE == 0) { - return encode_empty_to_buffer(&msg, T::MESSAGE_TYPE, conn, remaining_size); + return encode_to_buffer_slow_(0, &encode_msg_noop, &msg, conn, remaining_size); } else { - return encode_to_buffer(msg.calculate_size(), &proto_encode_msg, &msg, conn, remaining_size); + return encode_to_buffer_slow_(msg.calculate_size(), &proto_encode_msg, &msg, conn, remaining_size); } } From a3febe4a9eebbe8a9481c63956e3f76c570e4f9d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 3 Apr 2026 23:20:05 -1000 Subject: [PATCH 10/13] fix bloat --- esphome/components/api/api_connection.h | 4 +++- esphome/components/api/api_pb2_service.cpp | 5 ----- esphome/components/api/api_pb2_service.h | 2 -- 3 files changed, 3 insertions(+), 8 deletions(-) diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index 6fbf24c215e..61168e43c8a 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -409,7 +409,9 @@ class APIConnection final : public APIServerConnectionBase { uint32_t remaining_size) { #ifdef HAS_PROTO_MESSAGE_DUMP if (conn->flags_.log_only_mode) { - conn->log_send_message_(msg); + auto *proto_msg = static_cast(msg); + DumpBuffer dump_buf; + conn->log_send_message_(proto_msg->message_name(), proto_msg->dump_to(dump_buf)); return 1; } #endif diff --git a/esphome/components/api/api_pb2_service.cpp b/esphome/components/api/api_pb2_service.cpp index 20b201fec11..b41233eddd1 100644 --- a/esphome/components/api/api_pb2_service.cpp +++ b/esphome/components/api/api_pb2_service.cpp @@ -12,11 +12,6 @@ static const char *const TAG = "api.service"; void APIServerConnectionBase::log_send_message_(const LogString *name, const char *dump) { ESP_LOGVV(TAG, "send_message %s: %s", LOG_STR_ARG(name), dump); } -void APIServerConnectionBase::log_send_message_(const void *msg) { - auto *proto_msg = static_cast(msg); - DumpBuffer dump_buf; - this->log_send_message_(proto_msg->message_name(), proto_msg->dump_to(dump_buf)); -} void APIServerConnectionBase::log_receive_message_(const LogString *name, const ProtoMessage &msg) { DumpBuffer dump_buf; ESP_LOGVV(TAG, "%s: %s", LOG_STR_ARG(name), msg.dump_to(dump_buf)); diff --git a/esphome/components/api/api_pb2_service.h b/esphome/components/api/api_pb2_service.h index 1ae085fb5e1..6ff988902f3 100644 --- a/esphome/components/api/api_pb2_service.h +++ b/esphome/components/api/api_pb2_service.h @@ -13,8 +13,6 @@ class APIServerConnectionBase { #ifdef HAS_PROTO_MESSAGE_DUMP protected: void log_send_message_(const LogString *name, const char *dump); - // Log a message being sent from a void pointer (for use in encode_to_buffer paths) - void log_send_message_(const void *msg); void log_receive_message_(const LogString *name, const ProtoMessage &msg); void log_receive_message_(const LogString *name); From 392aa0fcbc4510b2db05ea6852f82b061f622493 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 3 Apr 2026 23:27:19 -1000 Subject: [PATCH 11/13] preen --- .../api/api_frame_helper_plaintext.cpp | 25 ++++++++----------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/esphome/components/api/api_frame_helper_plaintext.cpp b/esphome/components/api/api_frame_helper_plaintext.cpp index d263aac3f69..f7c77a494bb 100644 --- a/esphome/components/api/api_frame_helper_plaintext.cpp +++ b/esphome/components/api/api_frame_helper_plaintext.cpp @@ -302,21 +302,18 @@ APIError APIPlaintextFrameHelper::write_protobuf_messages(ProtoWriteBuffer buffe assert(!messages.empty()); #endif uint8_t *buffer_data = buffer.get_buffer()->data(); + uint8_t *write_start = nullptr; + uint16_t total_len = 0; - // First message has max padding (HEADER_PADDING), may have unused leading bytes. - // Subsequent messages were encoded with exact header sizes, so they are contiguous. - const auto &first = messages[0]; - uint8_t header_len = - write_plaintext_header(buffer_data + first.offset, first.payload_size, first.message_type, HEADER_PADDING); - uint8_t *write_start = buffer_data + first.offset + HEADER_PADDING - header_len; - uint16_t total_len = header_len + first.payload_size; - - // Write headers for remaining messages — padding equals actual header size - // (stored in msg.header_size), so write_plaintext_header writes at offset 0 with no gap. - for (size_t i = 1; i < messages.size(); i++) { - const auto &msg = messages[i]; - write_plaintext_header(buffer_data + msg.offset, msg.payload_size, msg.message_type, msg.header_size); - total_len += msg.header_size + msg.payload_size; + // First message has max padding (header_size = HEADER_PADDING), may have unused leading bytes. + // Subsequent messages were encoded with exact header sizes (header_size = actual header len). + // write_plaintext_header right-justifies the header within header_size bytes of padding. + for (const auto &msg : messages) { + uint8_t header_len = + write_plaintext_header(buffer_data + msg.offset, msg.payload_size, msg.message_type, msg.header_size); + if (write_start == nullptr) + write_start = buffer_data + msg.offset + msg.header_size - header_len; + total_len += header_len + msg.payload_size; } return this->write_raw_fast_buf_(write_start, total_len); From 8b0e6a3324fae52b4a2af3f7312a9cf8e485283d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 3 Apr 2026 23:34:57 -1000 Subject: [PATCH 12/13] preen --- esphome/components/api/api_connection.cpp | 6 +++--- esphome/components/api/api_connection.h | 10 +++++----- .../api/api_frame_helper_plaintext.cpp | 16 +++++++++------- 3 files changed, 17 insertions(+), 15 deletions(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 2ccb597bbce..27d5f413ba6 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -398,7 +398,7 @@ uint16_t APIConnection::fill_and_encode_entity_info(EntityBase *entity, InfoResp #ifdef USE_DEVICES msg.device_id = entity->get_device_id(); #endif - return encode_to_buffer_slow_(size_fn(&msg), encode_fn, &msg, conn, remaining_size); + return encode_to_buffer_slow(size_fn(&msg), encode_fn, &msg, conn, remaining_size); } uint16_t APIConnection::fill_and_encode_entity_info_with_device_class(EntityBase *entity, InfoResponseProtoMessage &msg, @@ -1999,8 +1999,8 @@ bool APIConnection::send_message_(uint32_t payload_size, uint8_t message_type, M // encode_to_buffer is defined inline in api_connection.h (ESPHOME_ALWAYS_INLINE) // Noinline version for cold paths — single shared copy -uint16_t APIConnection::encode_to_buffer_slow_(uint32_t calculated_size, MessageEncodeFn encode_fn, const void *msg, - APIConnection *conn, uint32_t remaining_size) { +uint16_t APIConnection::encode_to_buffer_slow(uint32_t calculated_size, MessageEncodeFn encode_fn, const void *msg, + APIConnection *conn, uint32_t remaining_size) { return encode_to_buffer(calculated_size, encode_fn, msg, conn, remaining_size); } bool APIConnection::send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) { diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index 61168e43c8a..4a06371cd58 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -443,18 +443,18 @@ class APIConnection final : public APIServerConnectionBase { // Noinline version of encode_to_buffer for cold paths (entity info, zero-payload messages). // All cold callers share this single copy instead of each getting an ALWAYS_INLINE expansion. - static uint16_t encode_to_buffer_slow_(uint32_t calculated_size, MessageEncodeFn encode_fn, const void *msg, - APIConnection *conn, uint32_t remaining_size); + static uint16_t encode_to_buffer_slow(uint32_t calculated_size, MessageEncodeFn encode_fn, const void *msg, + APIConnection *conn, uint32_t remaining_size); - // Thin template wrapper — uses noinline encode_to_buffer_slow_ since + // Thin template wrapper — uses noinline encode_to_buffer_slow since // encode_message_to_buffer callers are cold paths (zero-payload control messages). // Hot paths (state/info) go through fill_and_encode_entity_state/info instead. template static uint16_t encode_message_to_buffer(T &msg, APIConnection *conn, uint32_t remaining_size) { conn->batch_message_type_ = T::MESSAGE_TYPE; if constexpr (T::ESTIMATED_SIZE == 0) { - return encode_to_buffer_slow_(0, &encode_msg_noop, &msg, conn, remaining_size); + return encode_to_buffer_slow(0, &encode_msg_noop, &msg, conn, remaining_size); } else { - return encode_to_buffer_slow_(msg.calculate_size(), &proto_encode_msg, &msg, conn, remaining_size); + return encode_to_buffer_slow(msg.calculate_size(), &proto_encode_msg, &msg, conn, remaining_size); } } diff --git a/esphome/components/api/api_frame_helper_plaintext.cpp b/esphome/components/api/api_frame_helper_plaintext.cpp index f7c77a494bb..5e3a4627c9e 100644 --- a/esphome/components/api/api_frame_helper_plaintext.cpp +++ b/esphome/components/api/api_frame_helper_plaintext.cpp @@ -302,17 +302,19 @@ APIError APIPlaintextFrameHelper::write_protobuf_messages(ProtoWriteBuffer buffe assert(!messages.empty()); #endif uint8_t *buffer_data = buffer.get_buffer()->data(); - uint8_t *write_start = nullptr; - uint16_t total_len = 0; // First message has max padding (header_size = HEADER_PADDING), may have unused leading bytes. // Subsequent messages were encoded with exact header sizes (header_size = actual header len). // write_plaintext_header right-justifies the header within header_size bytes of padding. - for (const auto &msg : messages) { - uint8_t header_len = - write_plaintext_header(buffer_data + msg.offset, msg.payload_size, msg.message_type, msg.header_size); - if (write_start == nullptr) - write_start = buffer_data + msg.offset + msg.header_size - header_len; + const auto &first = messages[0]; + uint8_t header_len = + write_plaintext_header(buffer_data + first.offset, first.payload_size, first.message_type, HEADER_PADDING); + uint8_t *write_start = buffer_data + first.offset + HEADER_PADDING - header_len; + uint16_t total_len = header_len + first.payload_size; + + for (size_t i = 1; i < messages.size(); i++) { + const auto &msg = messages[i]; + header_len = write_plaintext_header(buffer_data + msg.offset, msg.payload_size, msg.message_type, msg.header_size); total_len += header_len + msg.payload_size; } From 99d080b2a78b333b2e7547b13a1751bbc087368d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 3 Apr 2026 23:35:41 -1000 Subject: [PATCH 13/13] preen --- esphome/components/api/api_frame_helper_plaintext.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/esphome/components/api/api_frame_helper_plaintext.cpp b/esphome/components/api/api_frame_helper_plaintext.cpp index 5e3a4627c9e..3e33489cec4 100644 --- a/esphome/components/api/api_frame_helper_plaintext.cpp +++ b/esphome/components/api/api_frame_helper_plaintext.cpp @@ -307,9 +307,9 @@ APIError APIPlaintextFrameHelper::write_protobuf_messages(ProtoWriteBuffer buffe // Subsequent messages were encoded with exact header sizes (header_size = actual header len). // write_plaintext_header right-justifies the header within header_size bytes of padding. const auto &first = messages[0]; - uint8_t header_len = - write_plaintext_header(buffer_data + first.offset, first.payload_size, first.message_type, HEADER_PADDING); - uint8_t *write_start = buffer_data + first.offset + HEADER_PADDING - header_len; + uint8_t *first_start = buffer_data + first.offset; + uint8_t header_len = write_plaintext_header(first_start, first.payload_size, first.message_type, HEADER_PADDING); + uint8_t *write_start = first_start + HEADER_PADDING - header_len; uint16_t total_len = header_len + first.payload_size; for (size_t i = 1; i < messages.size(); i++) {