From f0edf10f95727ce5f422137848c967ec3982bd92 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 1 Apr 2026 21:45:18 -1000 Subject: [PATCH 1/9] [api] Pack batch messages contiguously for write() instead of writev() Replace StaticVector + writev() scatter-gather in the batch write path with contiguous single-buffer write() calls. Plaintext: compact messages via memmove to close 0-3 byte varint header gaps, then write_raw_fast_buf_. Noise: messages are already contiguous (fixed 7-byte header + 16-byte MAC fills all reserved space), switch directly to write_raw_fast_buf_. Fix LOG_PACKET_SENDING to log after write/enqueue to prevent re-entrant log sends from corrupting the shared buffer before data is sent. Consolidate the macro to api_frame_helper.cpp and expose via out-of-line log_packet_sending_() helper. Remove write_raw_fast_iov_ (no remaining callers) and change encrypt_noise_message_ to return uint16_t length instead of iovec. --- esphome/components/api/api_frame_helper.cpp | 16 ++++++- esphome/components/api/api_frame_helper.h | 21 ++++------ .../components/api/api_frame_helper_noise.cpp | 42 +++++++------------ .../components/api/api_frame_helper_noise.h | 2 +- .../api/api_frame_helper_plaintext.cpp | 39 ++++++++--------- 5 files changed, 54 insertions(+), 66 deletions(-) diff --git a/esphome/components/api/api_frame_helper.cpp b/esphome/components/api/api_frame_helper.cpp index 06fcb5beec..90353b6402 100644 --- a/esphome/components/api/api_frame_helper.cpp +++ b/esphome/components/api/api_frame_helper.cpp @@ -100,6 +100,12 @@ const LogString *api_error_to_logstr(APIError err) { return LOG_STR("UNKNOWN"); } +#ifdef HELPER_LOG_PACKETS +void APIFrameHelper::log_packet_sending_(const void *data, uint16_t len) { + LOG_PACKET_SENDING(reinterpret_cast(data), len); +} +#endif + APIError APIFrameHelper::drain_overflow_and_handle_errors_() { if (this->overflow_buf_.try_drain(this->socket_.get()) == -1) { int err = errno; @@ -115,7 +121,13 @@ APIError APIFrameHelper::drain_overflow_and_handle_errors_() { // Single-buffer write path: wraps in iovec and delegates. APIError APIFrameHelper::write_raw_buf_(const void *data, uint16_t len, ssize_t sent) { struct iovec iov = {const_cast(data), len}; - return this->write_raw_iov_(&iov, 1, len, sent); + APIError err = this->write_raw_iov_(&iov, 1, len, sent); +#ifdef HELPER_LOG_PACKETS + // Log after write/enqueue so re-entrant log sends can't corrupt data before it's sent + if (err == APIError::OK) + LOG_PACKET_SENDING(reinterpret_cast(data), len); +#endif + return err; } // Handles partial writes, errors, and overflow buffering. @@ -152,7 +164,7 @@ APIError APIFrameHelper::write_raw_iov_(const struct iovec *iov, int iovcnt, uin } } - // Full write completed (possible when called directly, not via write_raw_fast_iov_) + // Full write completed (possible when called directly, not via write_raw_fast_buf_) if (sent == static_cast(total_write_len)) return APIError::OK; diff --git a/esphome/components/api/api_frame_helper.h b/esphome/components/api/api_frame_helper.h index 9c0f072e3b..e22b959909 100644 --- a/esphome/components/api/api_frame_helper.h +++ b/esphome/components/api/api_frame_helper.h @@ -207,30 +207,25 @@ class APIFrameHelper { inline APIError ESPHOME_ALWAYS_INLINE write_raw_fast_buf_(const void *data, uint16_t len) { if (this->overflow_buf_.empty()) [[likely]] { ssize_t sent = this->socket_->write(data, len); - if (sent == static_cast(len)) [[likely]] + if (sent == static_cast(len)) [[likely]] { +#ifdef HELPER_LOG_PACKETS + this->log_packet_sending_(data, len); +#endif return APIError::OK; + } // sent is -1 (WRITE_FAILED) or partial write count return this->write_raw_buf_(data, len, sent); } return this->write_raw_buf_(data, len, WRITE_NOT_ATTEMPTED); } - inline APIError ESPHOME_ALWAYS_INLINE write_raw_fast_iov_(const struct iovec *iov, int iovcnt, - uint16_t total_write_len) { - if (this->overflow_buf_.empty()) [[likely]] { - ssize_t sent = this->socket_->writev(iov, iovcnt); - if (sent == static_cast(total_write_len)) [[likely]] - return APIError::OK; - // sent is -1 (WRITE_FAILED) or partial write count - return this->write_raw_iov_(iov, iovcnt, total_write_len, sent); - } - return this->write_raw_iov_(iov, iovcnt, total_write_len, WRITE_NOT_ATTEMPTED); - } - // Out-of-line write paths: handle partial writes, errors, overflow buffering // sent: WRITE_NOT_ATTEMPTED (cold path), WRITE_FAILED (fast path write returned -1), or bytes sent (partial write) APIError write_raw_buf_(const void *data, uint16_t len, ssize_t sent = WRITE_NOT_ATTEMPTED); APIError write_raw_iov_(const struct iovec *iov, int iovcnt, uint16_t total_write_len, ssize_t sent = WRITE_NOT_ATTEMPTED); +#ifdef HELPER_LOG_PACKETS + void log_packet_sending_(const void *data, uint16_t len); +#endif // Socket ownership (4 bytes on 32-bit, 8 bytes on 64-bit) std::unique_ptr socket_; diff --git a/esphome/components/api/api_frame_helper_noise.cpp b/esphome/components/api/api_frame_helper_noise.cpp index 62073bb440..5223ccc970 100644 --- a/esphome/components/api/api_frame_helper_noise.cpp +++ b/esphome/components/api/api_frame_helper_noise.cpp @@ -47,15 +47,8 @@ static constexpr size_t API_MAX_LOG_BYTES = 168; format_hex_pretty_to(hex_buf_, (buffer).data(), \ (buffer).size() < API_MAX_LOG_BYTES ? (buffer).size() : API_MAX_LOG_BYTES)); \ } while (0) -#define LOG_PACKET_SENDING(data, len) \ - do { \ - char hex_buf_[format_hex_pretty_size(API_MAX_LOG_BYTES)]; \ - ESP_LOGVV(TAG, "Sending raw: %s", \ - format_hex_pretty_to(hex_buf_, data, (len) < API_MAX_LOG_BYTES ? (len) : API_MAX_LOG_BYTES)); \ - } while (0) #else #define LOG_PACKET_RECEIVED(buffer) ((void) 0) -#define LOG_PACKET_SENDING(data, len) ((void) 0) #endif /// Convert a noise error code to a readable error @@ -452,10 +445,10 @@ APIError APINoiseFrameHelper::read_packet(ReadPacketBuffer *buffer) { buffer->type = type; return APIError::OK; } -// Encrypt a single noise message in place and populate the iovec. +// Encrypt a single noise message in place and return the encrypted frame length. // Returns APIError::OK on success. APIError APINoiseFrameHelper::encrypt_noise_message_(uint8_t *buf_start, const MessageInfo &msg, - struct iovec &iov_out) { + uint16_t &encrypted_len_out) { // Write noise header buf_start[0] = 0x01; // indicator // buf_start[1], buf_start[2] to be set after encryption @@ -482,9 +475,7 @@ APIError APINoiseFrameHelper::encrypt_noise_message_(uint8_t *buf_start, const M buf_start[1] = static_cast(mbuf.size >> 8); buf_start[2] = static_cast(mbuf.size); - // Populate iovec for this encrypted message - size_t msg_len = static_cast(3 + mbuf.size); // indicator + size + encrypted data - iov_out = {buf_start, msg_len}; + encrypted_len_out = static_cast(3 + mbuf.size); // indicator + size + encrypted data return APIError::OK; } @@ -499,13 +490,11 @@ APIError APINoiseFrameHelper::write_protobuf_packet(uint8_t type, ProtoWriteBuff MessageInfo msg{type, 0, static_cast(buffer.get_buffer()->size() - HEADER_PADDING - frame_footer_size_)}; uint8_t *buf_start = buffer.get_buffer()->data(); - struct iovec iov; - APIError aerr = this->encrypt_noise_message_(buf_start, msg, iov); + uint16_t encrypted_len; + APIError aerr = this->encrypt_noise_message_(buf_start, msg, encrypted_len); if (aerr != APIError::OK) return aerr; - // buf_start and iov.iov_base point to the same location - LOG_PACKET_SENDING(buf_start, iov.iov_len); - return this->write_raw_fast_buf_(buf_start, static_cast(iov.iov_len)); + return this->write_raw_fast_buf_(buf_start, encrypted_len); } APIError APINoiseFrameHelper::write_protobuf_messages(ProtoWriteBuffer buffer, std::span messages) { @@ -514,26 +503,23 @@ APIError APINoiseFrameHelper::write_protobuf_messages(ProtoWriteBuffer buffer, s assert(!messages.empty()); #endif + // Noise messages are already contiguous in the buffer: + // HEADER_PADDING (7) exactly matches the fixed header size, and + // footer space (16) is consumed by the encryption MAC. uint8_t *buffer_data = buffer.get_buffer()->data(); - StaticVector iovs; + uint8_t *write_start = buffer_data + messages[0].offset; uint16_t total_write_len = 0; for (const auto &msg : messages) { uint8_t *buf_start = buffer_data + msg.offset; - struct iovec iov; - APIError aerr = this->encrypt_noise_message_(buf_start, msg, iov); + uint16_t encrypted_len; + APIError aerr = this->encrypt_noise_message_(buf_start, msg, encrypted_len); if (aerr != APIError::OK) return aerr; - iovs.push_back(iov); - total_write_len += iov.iov_len; + total_write_len += encrypted_len; } -#ifdef HELPER_LOG_PACKETS - for (const auto &iov : iovs) { - LOG_PACKET_SENDING(reinterpret_cast(iov.iov_base), iov.iov_len); - } -#endif - return this->write_raw_fast_iov_(iovs.data(), iovs.size(), total_write_len); + return this->write_raw_fast_buf_(write_start, total_write_len); } APIError APINoiseFrameHelper::write_frame_(const uint8_t *data, uint16_t len) { diff --git a/esphome/components/api/api_frame_helper_noise.h b/esphome/components/api/api_frame_helper_noise.h index 53c18431d6..76c4d1637b 100644 --- a/esphome/components/api/api_frame_helper_noise.h +++ b/esphome/components/api/api_frame_helper_noise.h @@ -31,7 +31,7 @@ class APINoiseFrameHelper final : public APIFrameHelper { APIError state_action_(); APIError try_read_frame_(); APIError write_frame_(const uint8_t *data, uint16_t len); - APIError encrypt_noise_message_(uint8_t *buf_start, const MessageInfo &msg, struct iovec &iov_out); + APIError encrypt_noise_message_(uint8_t *buf_start, const MessageInfo &msg, uint16_t &encrypted_len_out); APIError init_handshake_(); APIError check_handshake_finished_(); void send_explicit_handshake_reject_(const LogString *reason); diff --git a/esphome/components/api/api_frame_helper_plaintext.cpp b/esphome/components/api/api_frame_helper_plaintext.cpp index 06b8dd6e7b..89a8b63910 100644 --- a/esphome/components/api/api_frame_helper_plaintext.cpp +++ b/esphome/components/api/api_frame_helper_plaintext.cpp @@ -39,15 +39,8 @@ static constexpr size_t API_MAX_LOG_BYTES = 168; format_hex_pretty_to(hex_buf_, (buffer).data(), \ (buffer).size() < API_MAX_LOG_BYTES ? (buffer).size() : API_MAX_LOG_BYTES)); \ } while (0) -#define LOG_PACKET_SENDING(data, len) \ - do { \ - char hex_buf_[format_hex_pretty_size(API_MAX_LOG_BYTES)]; \ - ESP_LOGVV(TAG, "Sending raw: %s", \ - format_hex_pretty_to(hex_buf_, data, (len) < API_MAX_LOG_BYTES ? (len) : API_MAX_LOG_BYTES)); \ - } while (0) #else #define LOG_PACKET_RECEIVED(buffer) ((void) 0) -#define LOG_PACKET_SENDING(data, len) ((void) 0) #endif /// Initialize the frame helper, returns OK if successful. @@ -291,7 +284,6 @@ APIError APIPlaintextFrameHelper::write_protobuf_packet(uint8_t type, ProtoWrite uint8_t header_len = write_plaintext_header(buffer_data, msg); uint8_t *msg_start = buffer_data + HEADER_PADDING - header_len; uint16_t msg_len = static_cast(header_len + msg.payload_size); - LOG_PACKET_SENDING(msg_start, msg_len); return this->write_raw_fast_buf_(msg_start, msg_len); } @@ -302,23 +294,26 @@ APIError APIPlaintextFrameHelper::write_protobuf_messages(ProtoWriteBuffer buffe assert(!messages.empty()); #endif uint8_t *buffer_data = buffer.get_buffer()->data(); - StaticVector iovs; - uint16_t total_write_len = 0; - for (const auto &msg : messages) { - uint8_t header_len = write_plaintext_header(buffer_data + msg.offset, msg); - uint8_t *msg_start = buffer_data + msg.offset + HEADER_PADDING - header_len; - size_t msg_len = static_cast(header_len + msg.payload_size); - iovs.push_back({msg_start, msg_len}); - total_write_len += msg_len; + // First message: write header, record start position + const auto &first = messages[0]; + uint8_t header_len = write_plaintext_header(buffer_data + first.offset, first); + uint8_t *write_start = buffer_data + first.offset + HEADER_PADDING - header_len; + uint8_t *write_end = write_start + header_len + first.payload_size; + + // Subsequent messages: write header, then compact to close 0-3 byte gaps + for (size_t i = 1; i < messages.size(); i++) { + const auto &msg = messages[i]; + header_len = write_plaintext_header(buffer_data + msg.offset, msg); + uint8_t *src = buffer_data + msg.offset + HEADER_PADDING - header_len; + uint16_t msg_len = header_len + msg.payload_size; + if (src != write_end) { + memmove(write_end, src, msg_len); + } + write_end += msg_len; } -#ifdef HELPER_LOG_PACKETS - for (const auto &iov : iovs) { - LOG_PACKET_SENDING(reinterpret_cast(iov.iov_base), iov.iov_len); - } -#endif - return this->write_raw_fast_iov_(iovs.data(), iovs.size(), total_write_len); + return this->write_raw_fast_buf_(write_start, static_cast(write_end - write_start)); } } // namespace esphome::api From 06bc9462f16cae7693ad7902a7f6d63bf5ba5009 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 1 Apr 2026 22:10:17 -1000 Subject: [PATCH 2/9] [api] Deduplicate header code in plaintext write_protobuf_messages Use a single loop for all messages instead of separate first-message and loop paths. The first iteration skips memmove via the null check. Eliminates duplicated write_plaintext_header inlining, reducing flash from 326 to 229 bytes (-30%) while keeping the 64-byte stack frame. --- .../api/api_frame_helper_plaintext.cpp | 21 +++++++++---------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/esphome/components/api/api_frame_helper_plaintext.cpp b/esphome/components/api/api_frame_helper_plaintext.cpp index 89a8b63910..ff682b0185 100644 --- a/esphome/components/api/api_frame_helper_plaintext.cpp +++ b/esphome/components/api/api_frame_helper_plaintext.cpp @@ -294,20 +294,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; + uint8_t *write_end = nullptr; - // First message: write header, record start position - const auto &first = messages[0]; - uint8_t header_len = write_plaintext_header(buffer_data + first.offset, first); - uint8_t *write_start = buffer_data + first.offset + HEADER_PADDING - header_len; - uint8_t *write_end = write_start + header_len + first.payload_size; - - // Subsequent messages: write header, then compact to close 0-3 byte gaps - for (size_t i = 1; i < messages.size(); i++) { - const auto &msg = messages[i]; - header_len = write_plaintext_header(buffer_data + msg.offset, msg); + // Write headers and compact messages to close 0-3 byte varint padding gaps. + for (const auto &msg : messages) { + uint8_t header_len = write_plaintext_header(buffer_data + msg.offset, msg); uint8_t *src = buffer_data + msg.offset + HEADER_PADDING - header_len; uint16_t msg_len = header_len + msg.payload_size; - if (src != write_end) { + if (write_end == nullptr) { + // First message: record start, no compaction needed + write_start = src; + write_end = src; + } else if (src != write_end) { memmove(write_end, src, msg_len); } write_end += msg_len; From cdf1adaa39ce07f64cdb6c75ce7ec8292c84df4e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 1 Apr 2026 22:20:56 -1000 Subject: [PATCH 3/9] [api] Eliminate nullptr branch from write_protobuf_messages loop Pre-compute the first message's header length before the loop to initialize write_start/write_end. The first loop iteration naturally skips memmove since src == write_end. Extract varint_encoded_length_16/8 and plaintext_header_length as reusable inline helpers from write_plaintext_header. --- .../api/api_frame_helper_plaintext.cpp | 36 ++++++++++++------- 1 file changed, 24 insertions(+), 12 deletions(-) diff --git a/esphome/components/api/api_frame_helper_plaintext.cpp b/esphome/components/api/api_frame_helper_plaintext.cpp index ff682b0185..4d8169e86d 100644 --- a/esphome/components/api/api_frame_helper_plaintext.cpp +++ b/esphome/components/api/api_frame_helper_plaintext.cpp @@ -227,14 +227,26 @@ 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; +} + +// Compute plaintext header length: indicator (1) + size varint + type varint. +ESPHOME_ALWAYS_INLINE static inline uint8_t plaintext_header_length(const MessageInfo &msg) { + return 1 + varint_encoded_length_16(msg.payload_size) + varint_encoded_length_8(msg.message_type); +} + // Write plaintext header into pre-allocated padding before payload. // Returns the total header length (indicator + varints). ESPHOME_ALWAYS_INLINE static inline uint8_t write_plaintext_header(uint8_t *buf_start, const MessageInfo &msg) { - // 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_THRESHOLD_1_BYTE - ? 1 - : (msg.payload_size < ProtoSize::VARINT_THRESHOLD_2_BYTE ? 2 : 3); - uint8_t type_varint_len = msg.message_type < ProtoSize::VARINT_THRESHOLD_1_BYTE ? 1 : 2; + uint8_t size_varint_len = varint_encoded_length_16(msg.payload_size); + uint8_t type_varint_len = varint_encoded_length_8(msg.message_type); uint8_t total_header_len = 1 + size_varint_len + type_varint_len; // Calculate where to start writing the header @@ -294,19 +306,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; - uint8_t *write_end = nullptr; + + // Compute first message's write position from header length (cheap arithmetic, no buffer writes). + // This lets the loop body skip a nullptr check — first iteration has src == write_end naturally. + uint8_t *write_start = buffer_data + messages[0].offset + HEADER_PADDING - plaintext_header_length(messages[0]); + uint8_t *write_end = write_start; // Write headers and compact messages to close 0-3 byte varint padding gaps. + // First iteration: src == write_end so memmove is skipped. for (const auto &msg : messages) { uint8_t header_len = write_plaintext_header(buffer_data + msg.offset, msg); uint8_t *src = buffer_data + msg.offset + HEADER_PADDING - header_len; uint16_t msg_len = header_len + msg.payload_size; - if (write_end == nullptr) { - // First message: record start, no compaction needed - write_start = src; - write_end = src; - } else if (src != write_end) { + if (src != write_end) { memmove(write_end, src, msg_len); } write_end += msg_len; From ebefe76362c19e2d7247863b08d21af499363b11 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 1 Apr 2026 22:30:48 -1000 Subject: [PATCH 4/9] adjust --- esphome/components/api/api_frame_helper_plaintext.cpp | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/esphome/components/api/api_frame_helper_plaintext.cpp b/esphome/components/api/api_frame_helper_plaintext.cpp index 4d8169e86d..513274f75f 100644 --- a/esphome/components/api/api_frame_helper_plaintext.cpp +++ b/esphome/components/api/api_frame_helper_plaintext.cpp @@ -309,8 +309,8 @@ APIError APIPlaintextFrameHelper::write_protobuf_messages(ProtoWriteBuffer buffe // Compute first message's write position from header length (cheap arithmetic, no buffer writes). // This lets the loop body skip a nullptr check — first iteration has src == write_end naturally. - uint8_t *write_start = buffer_data + messages[0].offset + HEADER_PADDING - plaintext_header_length(messages[0]); - uint8_t *write_end = write_start; + 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: src == write_end so memmove is skipped. @@ -318,6 +318,11 @@ APIError APIPlaintextFrameHelper::write_protobuf_messages(ProtoWriteBuffer buffe uint8_t header_len = write_plaintext_header(buffer_data + msg.offset, msg); 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); } From 6576ebc2949ddf2ee4e7f82ec7815d9bc09f8d93 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 1 Apr 2026 22:31:58 -1000 Subject: [PATCH 5/9] [api] Update stale comments in write_protobuf_messages --- esphome/components/api/api_frame_helper_plaintext.cpp | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/esphome/components/api/api_frame_helper_plaintext.cpp b/esphome/components/api/api_frame_helper_plaintext.cpp index 513274f75f..3f33d0e9d8 100644 --- a/esphome/components/api/api_frame_helper_plaintext.cpp +++ b/esphome/components/api/api_frame_helper_plaintext.cpp @@ -307,13 +307,12 @@ APIError APIPlaintextFrameHelper::write_protobuf_messages(ProtoWriteBuffer buffe #endif uint8_t *buffer_data = buffer.get_buffer()->data(); - // Compute first message's write position from header length (cheap arithmetic, no buffer writes). - // This lets the loop body skip a nullptr check — first iteration has src == write_end naturally. 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: src == write_end so memmove is skipped. + // 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); uint8_t *src = buffer_data + msg.offset + HEADER_PADDING - header_len; From 1f455e7737de8d58d6fffc6a4619161e1b51f1a5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 1 Apr 2026 22:32:08 -1000 Subject: [PATCH 6/9] adjust --- esphome/components/api/api_frame_helper_plaintext.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/esphome/components/api/api_frame_helper_plaintext.cpp b/esphome/components/api/api_frame_helper_plaintext.cpp index 3f33d0e9d8..0384384154 100644 --- a/esphome/components/api/api_frame_helper_plaintext.cpp +++ b/esphome/components/api/api_frame_helper_plaintext.cpp @@ -306,7 +306,6 @@ 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; From a8c6c55de2c08151797fc8a760b95a81cb2214c6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 1 Apr 2026 22:43:58 -1000 Subject: [PATCH 7/9] [api] Inline varint encoding in write_plaintext_header Replace two out-of-line encode_varint_to_buffer calls with direct byte writes using the already-computed varint lengths. Eliminates function call overhead (register save/restore) from the batch loop and removes the 34-byte out-of-line encode_varint_to_buffer function which had no other callers. --- .../api/api_frame_helper_plaintext.cpp | 29 +++++++++++++++---- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/esphome/components/api/api_frame_helper_plaintext.cpp b/esphome/components/api/api_frame_helper_plaintext.cpp index 0384384154..69c291d0f0 100644 --- a/esphome/components/api/api_frame_helper_plaintext.cpp +++ b/esphome/components/api/api_frame_helper_plaintext.cpp @@ -276,12 +276,31 @@ ESPHOME_ALWAYS_INLINE static inline uint8_t write_plaintext_header(uint8_t *buf_ // So we write the header starting at offset + HEADER_PADDING - total_header_len uint32_t header_offset = APIPlaintextFrameHelper::HEADER_PADDING - total_header_len; - // Write the plaintext header - buf_start[header_offset] = 0x00; // indicator + // Write the plaintext header with inline varint encoding. + // We already know the varint lengths so we can write the exact bytes directly + // without a generic loop, avoiding function call overhead. + uint8_t *p = buf_start + header_offset; + *p++ = 0x00; // indicator - // Encode varints directly into buffer - encode_varint_to_buffer(msg.payload_size, buf_start + header_offset + 1); - encode_varint_to_buffer(msg.message_type, buf_start + header_offset + 1 + size_varint_len); + // Encode payload size varint (1-3 bytes) + uint16_t size_val = msg.payload_size; + if (size_varint_len >= 2) { + *p++ = static_cast(size_val | 0x80); + size_val >>= 7; + if (size_varint_len == 3) { + *p++ = static_cast(size_val | 0x80); + size_val >>= 7; + } + } + *p++ = static_cast(size_val); + + // Encode message type varint (1-2 bytes) + if (type_varint_len == 2) { + *p++ = static_cast(msg.message_type | 0x80); + *p = static_cast(msg.message_type >> 7); + } else { + *p = msg.message_type; + } return total_header_len; } From 3ca5e9a3f561f46ff88d5d11ac0cd95d20da92de Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 1 Apr 2026 22:47:13 -1000 Subject: [PATCH 8/9] [api] Extract inline varint encoders as named ALWAYS_INLINE helpers Move the inline varint byte writes into encode_varint_16_ and encode_varint_8_ for readability. Same generated code since both are ALWAYS_INLINE. --- .../api/api_frame_helper_plaintext.cpp | 52 ++++++++++--------- 1 file changed, 28 insertions(+), 24 deletions(-) diff --git a/esphome/components/api/api_frame_helper_plaintext.cpp b/esphome/components/api/api_frame_helper_plaintext.cpp index 69c291d0f0..5668592a2f 100644 --- a/esphome/components/api/api_frame_helper_plaintext.cpp +++ b/esphome/components/api/api_frame_helper_plaintext.cpp @@ -237,6 +237,29 @@ ESPHOME_ALWAYS_INLINE static inline uint8_t varint_encoded_length_8(uint8_t valu 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) { + if (varint_len >= 2) { + *p++ = static_cast(value | 0x80); + value >>= 7; + if (varint_len == 3) { + *p++ = static_cast(value | 0x80); + value >>= 7; + } + } + *p = static_cast(value); +} + +// Encode an 8-bit varint (1-2 bytes) using pre-computed length. +ESPHOME_ALWAYS_INLINE static inline void encode_varint_8_(uint8_t value, uint8_t varint_len, uint8_t *p) { + if (varint_len == 2) { + *p++ = static_cast(value | 0x80); + *p = static_cast(value >> 7); + } else { + *p = value; + } +} + // Compute plaintext header length: indicator (1) + size varint + type varint. ESPHOME_ALWAYS_INLINE static inline uint8_t plaintext_header_length(const MessageInfo &msg) { return 1 + varint_encoded_length_16(msg.payload_size) + varint_encoded_length_8(msg.message_type); @@ -276,31 +299,12 @@ ESPHOME_ALWAYS_INLINE static inline uint8_t write_plaintext_header(uint8_t *buf_ // So we write the header starting at offset + HEADER_PADDING - total_header_len uint32_t header_offset = APIPlaintextFrameHelper::HEADER_PADDING - total_header_len; - // Write the plaintext header with inline varint encoding. - // We already know the varint lengths so we can write the exact bytes directly - // without a generic loop, avoiding function call overhead. - uint8_t *p = buf_start + header_offset; - *p++ = 0x00; // indicator + // Write the plaintext header + buf_start[header_offset] = 0x00; // indicator - // Encode payload size varint (1-3 bytes) - uint16_t size_val = msg.payload_size; - if (size_varint_len >= 2) { - *p++ = static_cast(size_val | 0x80); - size_val >>= 7; - if (size_varint_len == 3) { - *p++ = static_cast(size_val | 0x80); - size_val >>= 7; - } - } - *p++ = static_cast(size_val); - - // Encode message type varint (1-2 bytes) - if (type_varint_len == 2) { - *p++ = static_cast(msg.message_type | 0x80); - *p = static_cast(msg.message_type >> 7); - } else { - *p = msg.message_type; - } + // Encode varints directly into buffer using pre-computed lengths + encode_varint_16_(msg.payload_size, size_varint_len, buf_start + header_offset + 1); + encode_varint_8_(msg.message_type, type_varint_len, buf_start + header_offset + 1 + size_varint_len); return total_header_len; } From 3f9d8ccf98f8eab96c8928ec37731f72b600aae9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 1 Apr 2026 22:58:08 -1000 Subject: [PATCH 9/9] [api] Fix clang-tidy: rename varint helpers, remove unused function Drop trailing underscore from static functions (readability-identifier-naming) and remove unused plaintext_header_length. --- .../components/api/api_frame_helper_plaintext.cpp | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/esphome/components/api/api_frame_helper_plaintext.cpp b/esphome/components/api/api_frame_helper_plaintext.cpp index 5668592a2f..a9e294de73 100644 --- a/esphome/components/api/api_frame_helper_plaintext.cpp +++ b/esphome/components/api/api_frame_helper_plaintext.cpp @@ -238,7 +238,7 @@ ESPHOME_ALWAYS_INLINE static inline uint8_t varint_encoded_length_8(uint8_t valu } // 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) { +ESPHOME_ALWAYS_INLINE static inline void encode_varint_16(uint16_t value, uint8_t varint_len, uint8_t *p) { if (varint_len >= 2) { *p++ = static_cast(value | 0x80); value >>= 7; @@ -251,7 +251,7 @@ ESPHOME_ALWAYS_INLINE static inline void encode_varint_16_(uint16_t value, uint8 } // Encode an 8-bit varint (1-2 bytes) using pre-computed length. -ESPHOME_ALWAYS_INLINE static inline void encode_varint_8_(uint8_t value, uint8_t varint_len, uint8_t *p) { +ESPHOME_ALWAYS_INLINE static inline void encode_varint_8(uint8_t value, uint8_t varint_len, uint8_t *p) { if (varint_len == 2) { *p++ = static_cast(value | 0x80); *p = static_cast(value >> 7); @@ -260,11 +260,6 @@ ESPHOME_ALWAYS_INLINE static inline void encode_varint_8_(uint8_t value, uint8_t } } -// Compute plaintext header length: indicator (1) + size varint + type varint. -ESPHOME_ALWAYS_INLINE static inline uint8_t plaintext_header_length(const MessageInfo &msg) { - return 1 + varint_encoded_length_16(msg.payload_size) + varint_encoded_length_8(msg.message_type); -} - // Write plaintext header into pre-allocated padding before payload. // Returns the total header length (indicator + varints). ESPHOME_ALWAYS_INLINE static inline uint8_t write_plaintext_header(uint8_t *buf_start, const MessageInfo &msg) { @@ -303,8 +298,8 @@ ESPHOME_ALWAYS_INLINE static inline uint8_t write_plaintext_header(uint8_t *buf_ buf_start[header_offset] = 0x00; // indicator // Encode varints directly into buffer using pre-computed lengths - encode_varint_16_(msg.payload_size, size_varint_len, buf_start + header_offset + 1); - encode_varint_8_(msg.message_type, type_varint_len, buf_start + header_offset + 1 + size_varint_len); + encode_varint_16(msg.payload_size, size_varint_len, buf_start + header_offset + 1); + encode_varint_8(msg.message_type, type_varint_len, buf_start + header_offset + 1 + size_varint_len); return total_header_len; }