diff --git a/esphome/components/api/api_frame_helper.cpp b/esphome/components/api/api_frame_helper.cpp index 900f2e1f4b..93119187d1 100644 --- a/esphome/components/api/api_frame_helper.cpp +++ b/esphome/components/api/api_frame_helper.cpp @@ -112,89 +112,49 @@ APIError APIFrameHelper::drain_overflow_and_handle_errors_() { } // Write data to socket, overflow to backlog buffer if LWIP TCP send buffer is full. -// Slow path: queue data into overflow buffer when socket can't accept it all. -// Out-of-line to keep the fast paths small. -APIError __attribute__((noinline)) -APIFrameHelper::enqueue_overflow_(const struct iovec *iov, int iovcnt, uint16_t total_write_len, uint16_t skip) { - if (!this->overflow_buf_.enqueue_iov(iov, iovcnt, total_write_len, skip)) { - HELPER_LOG("Overflow buffer full, dropping connection"); - this->state_ = State::FAILED; - return APIError::SOCKET_WRITE_FAILED; - } - return APIError::OK; -} - -// 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 - struct iovec iov = {const_cast(data), len}; - return this->enqueue_overflow_(&iov, 1, len, static_cast(sent)); - } - } - - // Socket not ready — queue all data - struct iovec iov = {const_cast(data), len}; - return this->enqueue_overflow_(&iov, 1, len, 0); -} - -// Multi-buffer write path for batched messages. -APIError APIFrameHelper::write_raw_(const struct iovec *iov, int iovcnt, uint16_t total_write_len) { +// Slow path: handles partial writes, errors, and overflow buffering. +// Called when the inline fast path in the header couldn't complete the write. +// sent == -1 means either the fast path write returned -1, or there was overflow backlog. +APIError APIFrameHelper::write_raw_slow_(const struct iovec *iov, int iovcnt, uint16_t total_write_len, ssize_t sent) { #ifdef HELPER_LOG_PACKETS for (int i = 0; i < iovcnt; i++) { LOG_PACKET_SENDING(reinterpret_cast(iov[i].iov_base), iov[i].iov_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_->writev(iov, iovcnt); - - if (sent == -1) [[unlikely]] { + if (sent == -1) { + // Either the fast path got -1, or we were called with overflow backlog + if (!this->overflow_buf_.empty()) { + // Drain existing backlog first + APIError err = this->drain_overflow_and_handle_errors_(); + if (err != APIError::OK) + return err; + // Try again after drain + if (this->overflow_buf_.empty()) { + sent = + (iovcnt == 1) ? this->socket_->write(iov[0].iov_base, iov[0].iov_len) : this->socket_->writev(iov, iovcnt); + if (sent == static_cast(total_write_len)) + return APIError::OK; + } + } + if (sent == -1) { int err = errno; - if (this->check_socket_write_err_(err) != APIError::WOULD_BLOCK) { + if (err != EWOULDBLOCK && err != EAGAIN) { + this->state_ = State::FAILED; HELPER_LOG("Socket write failed with errno %d", err); return APIError::SOCKET_WRITE_FAILED; } - } else if (static_cast(sent) >= total_write_len) [[likely]] { - return APIError::OK; - } else { - return this->enqueue_overflow_(iov, iovcnt, total_write_len, static_cast(sent)); + sent = 0; // Treat WOULD_BLOCK as zero bytes sent } } - // Socket not ready — queue all data - return this->enqueue_overflow_(iov, iovcnt, total_write_len, 0); + // Queue unsent data into overflow buffer + if (!this->overflow_buf_.enqueue_iov(iov, iovcnt, total_write_len, static_cast(sent))) { + HELPER_LOG("Overflow buffer full, dropping connection"); + this->state_ = State::FAILED; + return APIError::SOCKET_WRITE_FAILED; + } + return APIError::OK; } const char *APIFrameHelper::get_peername_to(std::span buf) const { diff --git a/esphome/components/api/api_frame_helper.h b/esphome/components/api/api_frame_helper.h index 68284529a0..e8e27f8d98 100644 --- a/esphome/components/api/api_frame_helper.h +++ b/esphome/components/api/api_frame_helper.h @@ -190,21 +190,33 @@ class APIFrameHelper { // Returns OK for transient errors (WOULD_BLOCK), SOCKET_WRITE_FAILED for hard errors. APIError drain_overflow_and_handle_errors_(); - // 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); - // Slow path: queue unsent data into overflow buffer - APIError enqueue_overflow_(const struct iovec *iov, int iovcnt, uint16_t total_write_len, uint16_t skip); - - // Check if a socket write errno is a hard error (not WOULD_BLOCK/EAGAIN). - // Returns WOULD_BLOCK for transient errors, SOCKET_WRITE_FAILED for hard errors. - APIError check_socket_write_err_(int err) { - if (err == EWOULDBLOCK || err == EAGAIN) - return APIError::WOULD_BLOCK; - this->state_ = State::FAILED; - return APIError::SOCKET_WRITE_FAILED; + // Write a single contiguous buffer to the socket (inlined fast path) + inline APIError ESPHOME_ALWAYS_INLINE write_raw_(const void *data, uint16_t len) { + // Fast path: no overflow backlog and full write succeeds + if (this->overflow_buf_.empty()) [[likely]] { + ssize_t sent = this->socket_->write(data, len); + if (sent == static_cast(len)) [[likely]] + return APIError::OK; + // Slow path: wrap in iovec and handle error/overflow + struct iovec iov = {const_cast(data), len}; + return this->write_raw_slow_(&iov, 1, len, sent); + } + struct iovec iov = {const_cast(data), len}; + return this->write_raw_slow_(&iov, 1, len, -1); } + // Write multiple iovec buffers to the socket (inlined fast path) + inline APIError ESPHOME_ALWAYS_INLINE write_raw_(const struct iovec *iov, int iovcnt, uint16_t total_write_len) { + // Fast path: no overflow backlog and full writev succeeds + 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; + return this->write_raw_slow_(iov, iovcnt, total_write_len, sent); + } + return this->write_raw_slow_(iov, iovcnt, total_write_len, -1); + } + // Slow path (out-of-line): handle partial writes, errors, overflow buffering + APIError write_raw_slow_(const struct iovec *iov, int iovcnt, uint16_t total_write_len, ssize_t sent); // Socket ownership (4 bytes on 32-bit, 8 bytes on 64-bit) std::unique_ptr socket_;