From 5d8f67c8199893af0b616a6e0dbbdbd717984ffc Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 29 Mar 2026 10:42:20 -1000 Subject: [PATCH] [api] Add single-buffer write_raw_ overload for single-message path The existing write_raw_ takes iovec array + count, but single-message writes (87-100% of traffic) always pass iovcnt=1. Adding a dedicated overload that takes (data, len) eliminates iovec construction, the iovcnt==1 branch, and pointer indirection on the hot path. --- esphome/components/api/api_frame_helper.cpp | 50 ++++++++++++++++++- esphome/components/api/api_frame_helper.h | 4 +- .../components/api/api_frame_helper_noise.cpp | 11 ++-- .../api/api_frame_helper_plaintext.cpp | 12 ++--- 4 files changed, 61 insertions(+), 16 deletions(-) diff --git a/esphome/components/api/api_frame_helper.cpp b/esphome/components/api/api_frame_helper.cpp index 6d3bd51b58..e81981980f 100644 --- a/esphome/components/api/api_frame_helper.cpp +++ b/esphome/components/api/api_frame_helper.cpp @@ -112,8 +112,54 @@ APIError APIFrameHelper::drain_overflow_and_handle_errors_() { } // Write data to socket, overflow to backlog buffer if LWIP TCP send buffer is full. -// Returns OK if all data was sent or successfully queued. -// Returns SOCKET_WRITE_FAILED on hard error (sets state to FAILED). +// Single-buffer write path — avoids iovec setup for the common single-message case. +APIError APIFrameHelper::write_raw_(const void *data, uint16_t len) { +#ifdef HELPER_LOG_PACKETS + LOG_PACKET_SENDING(reinterpret_cast(data), len); +#endif + + // Drain any existing backlog first + if (!this->overflow_buf_.empty()) [[unlikely]] { + APIError err = this->drain_overflow_and_handle_errors_(); + if (err != APIError::OK) + return err; + } + + // If backlog is clear, try direct send + if (this->overflow_buf_.empty()) [[likely]] { + ssize_t sent = this->socket_->write(data, len); + + if (sent == -1) [[unlikely]] { + int err = errno; + if (this->check_socket_write_err_(err) != APIError::WOULD_BLOCK) { + HELPER_LOG("Socket write failed with errno %d", err); + return APIError::SOCKET_WRITE_FAILED; + } + } else if (static_cast(sent) >= len) [[likely]] { + return APIError::OK; + } else { + // Partial write — queue remainder into overflow buffer + struct iovec iov = {const_cast(data), len}; + if (!this->overflow_buf_.enqueue_iov(&iov, 1, len, static_cast(sent))) { + HELPER_LOG("Overflow buffer full, dropping connection"); + this->state_ = State::FAILED; + return APIError::SOCKET_WRITE_FAILED; + } + return APIError::OK; + } + } + + // Socket not ready — queue all data into overflow buffer + struct iovec iov = {const_cast(data), len}; + if (!this->overflow_buf_.enqueue_iov(&iov, 1, len, 0)) { + HELPER_LOG("Overflow buffer full, dropping connection"); + this->state_ = State::FAILED; + return APIError::SOCKET_WRITE_FAILED; + } + return APIError::OK; +} + +// Multi-buffer write path for batched messages. APIError APIFrameHelper::write_raw_(const struct iovec *iov, int iovcnt, uint16_t total_write_len) { #ifdef HELPER_LOG_PACKETS for (int i = 0; i < iovcnt; i++) { diff --git a/esphome/components/api/api_frame_helper.h b/esphome/components/api/api_frame_helper.h index 3507b4d22a..9520cf9c14 100644 --- a/esphome/components/api/api_frame_helper.h +++ b/esphome/components/api/api_frame_helper.h @@ -190,7 +190,9 @@ class APIFrameHelper { // Returns OK for transient errors (WOULD_BLOCK), SOCKET_WRITE_FAILED for hard errors. APIError drain_overflow_and_handle_errors_(); - // Common implementation for writing raw data to socket + // Write a single contiguous buffer to the socket + APIError write_raw_(const void *data, uint16_t len); + // Write multiple iovec buffers to the socket in one writev call APIError write_raw_(const struct iovec *iov, int iovcnt, uint16_t total_write_len); // Check if a socket write errno is a hard error (not WOULD_BLOCK/EAGAIN). diff --git a/esphome/components/api/api_frame_helper_noise.cpp b/esphome/components/api/api_frame_helper_noise.cpp index 565a96678e..0471713c54 100644 --- a/esphome/components/api/api_frame_helper_noise.cpp +++ b/esphome/components/api/api_frame_helper_noise.cpp @@ -499,11 +499,12 @@ APIError APINoiseFrameHelper::write_protobuf_packet(uint8_t type, ProtoWriteBuff MessageInfo msg{type, 0, static_cast(buffer.get_buffer()->size() - frame_header_padding_ - frame_footer_size_)}; + uint8_t *buf_start = buffer.get_buffer()->data(); struct iovec iov; - aerr = this->encrypt_noise_message_(buffer.get_buffer()->data(), msg, iov); + aerr = this->encrypt_noise_message_(buf_start, msg, iov); if (aerr != APIError::OK) return aerr; - return this->write_raw_(&iov, 1, static_cast(iov.iov_len)); + return this->write_raw_(iov.iov_base, static_cast(iov.iov_len)); } APIError APINoiseFrameHelper::write_protobuf_messages(ProtoWriteBuffer buffer, std::span messages) { @@ -538,12 +539,12 @@ APIError APINoiseFrameHelper::write_frame_(const uint8_t *data, uint16_t len) { header[1] = (uint8_t) (len >> 8); header[2] = (uint8_t) len; + if (len == 0) { + return this->write_raw_(header, 3); // Just header + } struct iovec iov[2]; iov[0].iov_base = header; iov[0].iov_len = 3; - if (len == 0) { - return this->write_raw_(iov, 1, 3); // Just header - } iov[1].iov_base = const_cast(data); iov[1].iov_len = len; diff --git a/esphome/components/api/api_frame_helper_plaintext.cpp b/esphome/components/api/api_frame_helper_plaintext.cpp index e670cca255..79ccc83c4b 100644 --- a/esphome/components/api/api_frame_helper_plaintext.cpp +++ b/esphome/components/api/api_frame_helper_plaintext.cpp @@ -205,7 +205,6 @@ APIError APIPlaintextFrameHelper::read_packet(ReadPacketBuffer *buffer) { // Make sure to tell the remote that we don't // understand the indicator byte so it knows // we do not support it. - struct iovec iov[1]; // The \x00 first byte is the marker for plaintext. // // The remote will know how to handle the indicator byte, @@ -220,14 +219,12 @@ APIError APIPlaintextFrameHelper::read_packet(ReadPacketBuffer *buffer) { "Bad indicator byte"; char msg[INDICATOR_MSG_SIZE]; memcpy_P(msg, MSG_PROGMEM, INDICATOR_MSG_SIZE); - iov[0].iov_base = (void *) msg; + this->write_raw_(msg, INDICATOR_MSG_SIZE); #else static const char MSG[] = "\x00" "Bad indicator byte"; - iov[0].iov_base = (void *) MSG; + this->write_raw_(MSG, INDICATOR_MSG_SIZE); #endif - iov[0].iov_len = INDICATOR_MSG_SIZE; - this->write_raw_(iov, 1, INDICATOR_MSG_SIZE); } return aerr; } @@ -294,9 +291,8 @@ APIError APIPlaintextFrameHelper::write_protobuf_packet(uint8_t type, ProtoWrite uint8_t *buffer_data = buffer.get_buffer()->data(); uint8_t *msg_start = write_plaintext_header(buffer_data, msg, frame_header_padding_); uint8_t msg_header_len = static_cast(buffer_data + frame_header_padding_ - msg_start); - size_t msg_len = static_cast(msg_header_len + msg.payload_size); - struct iovec iov = {msg_start, msg_len}; - return write_raw_(&iov, 1, static_cast(msg_len)); + uint16_t msg_len = static_cast(msg_header_len + msg.payload_size); + return write_raw_(msg_start, msg_len); } APIError APIPlaintextFrameHelper::write_protobuf_messages(ProtoWriteBuffer buffer,