Merge remote-tracking branch 'upstream/api/peel-first-write-iteration' into integration

This commit is contained in:
J. Nick Koston
2026-03-29 15:33:19 -10:00
6 changed files with 84 additions and 57 deletions
+20 -19
View File
@@ -119,32 +119,29 @@ 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) {
#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
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 (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);
sent = this->write_iov_to_socket_(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
} else {
// Overflow backlog remains after drain; skip socket write, enqueue everything
sent = 0;
}
}
if (sent == -1) {
// WRITE_FAILED (-1): fast path or retry write returned -1, check errno
if (sent == WRITE_FAILED) {
int err = errno;
if (err != EWOULDBLOCK && err != EAGAIN) {
this->state_ = State::FAILED;
@@ -155,6 +152,10 @@ 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_)
if (sent == static_cast<ssize_t>(total_write_len))
return APIError::OK;
// 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");
+21 -8
View File
@@ -192,32 +192,45 @@ 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_FAILED = -1; // Fast path: write()/writev() returned -1
static constexpr ssize_t WRITE_NOT_ATTEMPTED = -2; // Cold path: no write attempted yet
// Dispatch to write() or writev() based on iovec count
inline ssize_t ESPHOME_ALWAYS_INLINE write_iov_to_socket_(const struct iovec *iov, int iovcnt) {
return (iovcnt == 1) ? this->socket_->write(iov[0].iov_base, iov[0].iov_len) : this->socket_->writev(iov, iovcnt);
}
// 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;
// sent is -1 (WRITE_FAILED) or partial write count
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;
// 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, 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_;
@@ -497,14 +497,15 @@ APIError APINoiseFrameHelper::write_protobuf_packet(uint8_t type, ProtoWriteBuff
if (frame_footer_size_)
buffer.get_buffer()->resize(buffer.get_buffer()->size() + frame_footer_size_);
MessageInfo msg{type, 0,
static_cast<uint16_t>(buffer.get_buffer()->size() - frame_header_padding_ - frame_footer_size_)};
MessageInfo msg{type, 0, static_cast<uint16_t>(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);
if (aerr != APIError::OK)
return aerr;
return this->write_raw_fast_buf_(iov.iov_base, static_cast<uint16_t>(iov.iov_len));
// 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<uint16_t>(iov.iov_len));
}
APIError APINoiseFrameHelper::write_protobuf_messages(ProtoWriteBuffer buffer, std::span<const MessageInfo> messages) {
@@ -527,6 +528,11 @@ APIError APINoiseFrameHelper::write_protobuf_messages(ProtoWriteBuffer buffer, s
total_write_len += iov.iov_len;
}
#ifdef HELPER_LOG_PACKETS
for (const auto &iov : iovs) {
LOG_PACKET_SENDING(reinterpret_cast<uint8_t *>(iov.iov_base), iov.iov_len);
}
#endif
return this->write_raw_fast_iov_(iovs.data(), iovs.size(), total_write_len);
}
@@ -9,14 +9,16 @@ namespace esphome::api {
class APINoiseFrameHelper final : public APIFrameHelper {
public:
// Noise header structure:
// Pos 0: indicator (0x01)
// Pos 1-2: encrypted payload size (16-bit big-endian)
// Pos 3-6: encrypted type (16-bit) + data_len (16-bit)
// Pos 7+: actual payload data
static constexpr uint8_t HEADER_PADDING = 1 + 2 + 2 + 2; // indicator + size + type + data_len
APINoiseFrameHelper(std::unique_ptr<socket::Socket> socket, APINoiseContext &ctx)
: APIFrameHelper(std::move(socket)), ctx_(ctx) {
// Noise header structure:
// Pos 0: indicator (0x01)
// Pos 1-2: encrypted payload size (16-bit big-endian)
// Pos 3-6: encrypted type (16-bit) + data_len (16-bit)
// Pos 7+: actual payload data
frame_header_padding_ = 7;
frame_header_padding_ = HEADER_PADDING;
}
~APINoiseFrameHelper() override;
APIError init() override;
@@ -235,9 +235,8 @@ APIError APIPlaintextFrameHelper::read_packet(ReadPacketBuffer *buffer) {
return APIError::OK;
}
// Write plaintext header into pre-allocated padding before payload.
// Returns pointer to start of frame (header + payload are contiguous).
ESPHOME_ALWAYS_INLINE static inline uint8_t *write_plaintext_header(uint8_t *buf_start, const MessageInfo &msg,
uint8_t frame_header_padding) {
// 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
@@ -269,8 +268,8 @@ ESPHOME_ALWAYS_INLINE static inline uint8_t *write_plaintext_header(uint8_t *buf
// [6...] - Actual payload data
//
// The message starts at offset + frame_header_padding
// So we write the header starting at offset + frame_header_padding - total_header_len
uint32_t header_offset = frame_header_padding - total_header_len;
// 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
@@ -279,7 +278,7 @@ ESPHOME_ALWAYS_INLINE static inline uint8_t *write_plaintext_header(uint8_t *buf
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);
return buf_start + header_offset;
return total_header_len;
}
APIError APIPlaintextFrameHelper::write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) {
@@ -287,11 +286,12 @@ APIError APIPlaintextFrameHelper::write_protobuf_packet(uint8_t type, ProtoWrite
assert(this->state_ == State::DATA);
#endif
MessageInfo msg{type, 0, static_cast<uint16_t>(buffer.get_buffer()->size() - frame_header_padding_)};
MessageInfo msg{type, 0, static_cast<uint16_t>(buffer.get_buffer()->size() - HEADER_PADDING)};
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<uint8_t>(buffer_data + frame_header_padding_ - msg_start);
uint16_t msg_len = static_cast<uint16_t>(msg_header_len + msg.payload_size);
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<uint16_t>(header_len + msg.payload_size);
LOG_PACKET_SENDING(msg_start, msg_len);
return this->write_raw_fast_buf_(msg_start, msg_len);
}
@@ -301,20 +301,23 @@ APIError APIPlaintextFrameHelper::write_protobuf_messages(ProtoWriteBuffer buffe
assert(this->state_ == State::DATA);
assert(!messages.empty());
#endif
uint8_t *buffer_data = buffer.get_buffer()->data();
StaticVector<struct iovec, MAX_MESSAGES_PER_BATCH> iovs;
uint16_t total_write_len = 0;
const uint8_t padding = frame_header_padding_;
for (const auto &msg : messages) {
uint8_t *msg_start = write_plaintext_header(buffer_data + msg.offset, msg, padding);
uint8_t msg_header_len = static_cast<uint8_t>((buffer_data + msg.offset + padding) - msg_start);
size_t msg_len = static_cast<size_t>(msg_header_len + msg.payload_size);
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<size_t>(header_len + msg.payload_size);
iovs.push_back({msg_start, msg_len});
total_write_len += msg_len;
}
#ifdef HELPER_LOG_PACKETS
for (const auto &iov : iovs) {
LOG_PACKET_SENDING(reinterpret_cast<uint8_t *>(iov.iov_base), iov.iov_len);
}
#endif
return this->write_raw_fast_iov_(iovs.data(), iovs.size(), total_write_len);
}
@@ -7,13 +7,15 @@ namespace esphome::api {
class APIPlaintextFrameHelper final : public APIFrameHelper {
public:
// Plaintext header structure (worst case):
// Pos 0: indicator (0x00)
// Pos 1-3: payload size varint (up to 3 bytes)
// Pos 4-5: message type varint (up to 2 bytes)
// Pos 6+: actual payload data
static constexpr uint8_t HEADER_PADDING = 1 + 3 + 2; // indicator + size varint + type varint
explicit APIPlaintextFrameHelper(std::unique_ptr<socket::Socket> socket) : APIFrameHelper(std::move(socket)) {
// Plaintext header structure (worst case):
// Pos 0: indicator (0x00)
// Pos 1-3: payload size varint (up to 3 bytes)
// Pos 4-5: message type varint (up to 2 bytes)
// Pos 6+: actual payload data
frame_header_padding_ = 6;
frame_header_padding_ = HEADER_PADDING;
}
~APIPlaintextFrameHelper() override = default;
APIError init() override;