[api] Use named sentinels to avoid redundant syscall on write failure

WRITE_NOT_ATTEMPTED (-1): cold path, no write tried yet — try write
WRITE_FAILED (-2): fast path write() returned -1 — skip retry, check errno

This avoids a redundant write() syscall when the fast path already
got EWOULDBLOCK and fell through to the slow path.
This commit is contained in:
J. Nick Koston
2026-03-29 14:10:11 -10:00
parent 2e1d6e4d58
commit 5f2ad82a67
2 changed files with 36 additions and 25 deletions
+18 -17
View File
@@ -119,26 +119,27 @@ APIError APIFrameHelper::write_raw_buf_(const void *data, uint16_t len, ssize_t
}
// Handles partial writes, errors, and overflow buffering.
// Called when the inline fast path in the header couldn't complete the write,
// Called when the inline fast path couldn't complete the write,
// or directly from cold paths (handshake, error handling).
// sent == -1 means either the fast path write returned -1, or there was overflow backlog.
APIError APIFrameHelper::write_raw_iov_(const struct iovec *iov, int iovcnt, uint16_t total_write_len, ssize_t sent) {
if (sent == -1) {
// Either the fast path write returned -1, or we were called directly (cold path)
if (!this->overflow_buf_.empty()) {
// Drain existing backlog first
APIError err = this->drain_overflow_and_handle_errors_();
if (err != APIError::OK)
return err;
if (sent <= 0) {
if (sent == WRITE_NOT_ATTEMPTED) {
// Cold path: no write attempted yet, drain overflow and try
if (!this->overflow_buf_.empty()) {
APIError err = this->drain_overflow_and_handle_errors_();
if (err != APIError::OK)
return err;
}
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;
// Partial write or -1: fall through to error check / enqueue below
}
}
// Try write if backlog is clear (either was empty, or drain succeeded)
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;
// Partial write or -1: fall through to error check / enqueue below
}
if (sent == -1) {
// WRITE_FAILED or write above returned -1: check errno
if (sent == WRITE_FAILED || sent == -1) {
int err = errno;
if (err != EWOULDBLOCK && err != EAGAIN) {
this->state_ = State::FAILED;
+18 -8
View File
@@ -192,32 +192,42 @@ class APIFrameHelper {
// Returns OK for transient errors (WOULD_BLOCK), SOCKET_WRITE_FAILED for hard errors.
APIError drain_overflow_and_handle_errors_();
// Sentinel values for the sent parameter in write_raw_ methods
static constexpr ssize_t WRITE_NOT_ATTEMPTED = -1; // Cold path: no write attempted yet
static constexpr ssize_t WRITE_FAILED = -2; // Fast path: write() returned -1
// Inlined write methods — used by hot paths (write_protobuf_packet, write_protobuf_messages)
// These inline the fast path (overflow empty + full write) and tail-call the out-of-line
// slow path only on failure/partial write.
inline APIError ESPHOME_ALWAYS_INLINE write_raw_fast_buf_(const void *data, uint16_t len) {
ssize_t sent = -1;
if (this->overflow_buf_.empty()) [[likely]] {
sent = this->socket_->write(data, len);
ssize_t sent = this->socket_->write(data, len);
if (sent == static_cast<ssize_t>(len)) [[likely]]
return APIError::OK;
if (sent == -1)
return this->write_raw_buf_(data, len, WRITE_FAILED);
return this->write_raw_buf_(data, len, sent);
}
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) {
ssize_t sent = -1;
if (this->overflow_buf_.empty()) [[likely]] {
sent = this->socket_->writev(iov, iovcnt);
ssize_t sent = this->socket_->writev(iov, iovcnt);
if (sent == static_cast<ssize_t>(total_write_len)) [[likely]]
return APIError::OK;
if (sent == -1)
return this->write_raw_iov_(iov, iovcnt, total_write_len, WRITE_FAILED);
return this->write_raw_iov_(iov, iovcnt, total_write_len, sent);
}
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
APIError write_raw_buf_(const void *data, uint16_t len, ssize_t sent = -1);
APIError write_raw_iov_(const struct iovec *iov, int iovcnt, uint16_t total_write_len, ssize_t sent = -1);
// 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);
// Socket ownership (4 bytes on 32-bit, 8 bytes on 64-bit)
std::unique_ptr<socket::Socket> socket_;