[api] Inline write_raw_ fast paths into header with single slow path

Move the happy path (overflow empty + full write succeeds) inline into
the header so it gets inlined at each call site. Both overloads share
a single out-of-line write_raw_slow_ that handles partial writes,
errors, and overflow buffering.
This commit is contained in:
J. Nick Koston
2026-03-29 10:45:01 -10:00
parent f272826274
commit ea7dc663f6
2 changed files with 56 additions and 84 deletions
+30 -70
View File
@@ -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<const uint8_t *>(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<uint16_t>(sent) >= len) [[likely]] {
return APIError::OK;
} else {
// Partial write — queue remainder
struct iovec iov = {const_cast<void *>(data), len};
return this->enqueue_overflow_(&iov, 1, len, static_cast<uint16_t>(sent));
}
}
// Socket not ready — queue all data
struct iovec iov = {const_cast<void *>(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<uint8_t *>(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<ssize_t>(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<uint16_t>(sent) >= total_write_len) [[likely]] {
return APIError::OK;
} else {
return this->enqueue_overflow_(iov, iovcnt, total_write_len, static_cast<uint16_t>(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<uint16_t>(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<char, socket::SOCKADDR_STR_LEN> buf) const {
+26 -14
View File
@@ -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<ssize_t>(len)) [[likely]]
return APIError::OK;
// Slow path: wrap in iovec and handle error/overflow
struct iovec iov = {const_cast<void *>(data), len};
return this->write_raw_slow_(&iov, 1, len, sent);
}
struct iovec iov = {const_cast<void *>(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<ssize_t>(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::Socket> socket_;