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

This commit is contained in:
J. Nick Koston
2026-04-01 22:59:27 -10:00
5 changed files with 92 additions and 72 deletions
+14 -2
View File
@@ -100,6 +100,12 @@ const LogString *api_error_to_logstr(APIError err) {
return LOG_STR("UNKNOWN");
}
#ifdef HELPER_LOG_PACKETS
void APIFrameHelper::log_packet_sending_(const void *data, uint16_t len) {
LOG_PACKET_SENDING(reinterpret_cast<const uint8_t *>(data), len);
}
#endif
APIError APIFrameHelper::drain_overflow_and_handle_errors_() {
if (this->overflow_buf_.try_drain(this->socket_.get()) == -1) {
int err = errno;
@@ -115,7 +121,13 @@ APIError APIFrameHelper::drain_overflow_and_handle_errors_() {
// Single-buffer write path: wraps in iovec and delegates.
APIError APIFrameHelper::write_raw_buf_(const void *data, uint16_t len, ssize_t sent) {
struct iovec iov = {const_cast<void *>(data), len};
return this->write_raw_iov_(&iov, 1, len, sent);
APIError err = this->write_raw_iov_(&iov, 1, len, sent);
#ifdef HELPER_LOG_PACKETS
// Log after write/enqueue so re-entrant log sends can't corrupt data before it's sent
if (err == APIError::OK)
LOG_PACKET_SENDING(reinterpret_cast<const uint8_t *>(data), len);
#endif
return err;
}
// Handles partial writes, errors, and overflow buffering.
@@ -152,7 +164,7 @@ 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_)
// Full write completed (possible when called directly, not via write_raw_fast_buf_)
if (sent == static_cast<ssize_t>(total_write_len))
return APIError::OK;
+8 -13
View File
@@ -207,30 +207,25 @@ class APIFrameHelper {
inline APIError ESPHOME_ALWAYS_INLINE write_raw_fast_buf_(const void *data, uint16_t len) {
if (this->overflow_buf_.empty()) [[likely]] {
ssize_t sent = this->socket_->write(data, len);
if (sent == static_cast<ssize_t>(len)) [[likely]]
if (sent == static_cast<ssize_t>(len)) [[likely]] {
#ifdef HELPER_LOG_PACKETS
this->log_packet_sending_(data, len);
#endif
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, WRITE_NOT_ATTEMPTED);
}
inline APIError ESPHOME_ALWAYS_INLINE write_raw_fast_iov_(const struct iovec *iov, int iovcnt,
uint16_t total_write_len) {
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;
// 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, WRITE_NOT_ATTEMPTED);
}
// Out-of-line write paths: handle partial writes, errors, overflow buffering
// 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);
#ifdef HELPER_LOG_PACKETS
void log_packet_sending_(const void *data, uint16_t len);
#endif
// Socket ownership (4 bytes on 32-bit, 8 bytes on 64-bit)
std::unique_ptr<socket::Socket> socket_;
@@ -47,15 +47,8 @@ static constexpr size_t API_MAX_LOG_BYTES = 168;
format_hex_pretty_to(hex_buf_, (buffer).data(), \
(buffer).size() < API_MAX_LOG_BYTES ? (buffer).size() : API_MAX_LOG_BYTES)); \
} while (0)
#define LOG_PACKET_SENDING(data, len) \
do { \
char hex_buf_[format_hex_pretty_size(API_MAX_LOG_BYTES)]; \
ESP_LOGVV(TAG, "Sending raw: %s", \
format_hex_pretty_to(hex_buf_, data, (len) < API_MAX_LOG_BYTES ? (len) : API_MAX_LOG_BYTES)); \
} while (0)
#else
#define LOG_PACKET_RECEIVED(buffer) ((void) 0)
#define LOG_PACKET_SENDING(data, len) ((void) 0)
#endif
/// Convert a noise error code to a readable error
@@ -452,10 +445,10 @@ APIError APINoiseFrameHelper::read_packet(ReadPacketBuffer *buffer) {
buffer->type = type;
return APIError::OK;
}
// Encrypt a single noise message in place and populate the iovec.
// Encrypt a single noise message in place and return the encrypted frame length.
// Returns APIError::OK on success.
APIError APINoiseFrameHelper::encrypt_noise_message_(uint8_t *buf_start, const MessageInfo &msg,
struct iovec &iov_out) {
uint16_t &encrypted_len_out) {
// Write noise header
buf_start[0] = 0x01; // indicator
// buf_start[1], buf_start[2] to be set after encryption
@@ -482,9 +475,7 @@ APIError APINoiseFrameHelper::encrypt_noise_message_(uint8_t *buf_start, const M
buf_start[1] = static_cast<uint8_t>(mbuf.size >> 8);
buf_start[2] = static_cast<uint8_t>(mbuf.size);
// Populate iovec for this encrypted message
size_t msg_len = static_cast<size_t>(3 + mbuf.size); // indicator + size + encrypted data
iov_out = {buf_start, msg_len};
encrypted_len_out = static_cast<uint16_t>(3 + mbuf.size); // indicator + size + encrypted data
return APIError::OK;
}
@@ -499,13 +490,11 @@ APIError APINoiseFrameHelper::write_protobuf_packet(uint8_t type, ProtoWriteBuff
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);
uint16_t encrypted_len;
APIError aerr = this->encrypt_noise_message_(buf_start, msg, encrypted_len);
if (aerr != APIError::OK)
return aerr;
// 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));
return this->write_raw_fast_buf_(buf_start, encrypted_len);
}
APIError APINoiseFrameHelper::write_protobuf_messages(ProtoWriteBuffer buffer, std::span<const MessageInfo> messages) {
@@ -514,26 +503,23 @@ APIError APINoiseFrameHelper::write_protobuf_messages(ProtoWriteBuffer buffer, s
assert(!messages.empty());
#endif
// Noise messages are already contiguous in the buffer:
// HEADER_PADDING (7) exactly matches the fixed header size, and
// footer space (16) is consumed by the encryption MAC.
uint8_t *buffer_data = buffer.get_buffer()->data();
StaticVector<struct iovec, MAX_MESSAGES_PER_BATCH> iovs;
uint8_t *write_start = buffer_data + messages[0].offset;
uint16_t total_write_len = 0;
for (const auto &msg : messages) {
uint8_t *buf_start = buffer_data + msg.offset;
struct iovec iov;
APIError aerr = this->encrypt_noise_message_(buf_start, msg, iov);
uint16_t encrypted_len;
APIError aerr = this->encrypt_noise_message_(buf_start, msg, encrypted_len);
if (aerr != APIError::OK)
return aerr;
iovs.push_back(iov);
total_write_len += iov.iov_len;
total_write_len += encrypted_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);
return this->write_raw_fast_buf_(write_start, total_write_len);
}
APIError APINoiseFrameHelper::write_frame_(const uint8_t *data, uint16_t len) {
@@ -31,7 +31,7 @@ class APINoiseFrameHelper final : public APIFrameHelper {
APIError state_action_();
APIError try_read_frame_();
APIError write_frame_(const uint8_t *data, uint16_t len);
APIError encrypt_noise_message_(uint8_t *buf_start, const MessageInfo &msg, struct iovec &iov_out);
APIError encrypt_noise_message_(uint8_t *buf_start, const MessageInfo &msg, uint16_t &encrypted_len_out);
APIError init_handshake_();
APIError check_handshake_finished_();
void send_explicit_handshake_reject_(const LogString *reason);
@@ -39,15 +39,8 @@ static constexpr size_t API_MAX_LOG_BYTES = 168;
format_hex_pretty_to(hex_buf_, (buffer).data(), \
(buffer).size() < API_MAX_LOG_BYTES ? (buffer).size() : API_MAX_LOG_BYTES)); \
} while (0)
#define LOG_PACKET_SENDING(data, len) \
do { \
char hex_buf_[format_hex_pretty_size(API_MAX_LOG_BYTES)]; \
ESP_LOGVV(TAG, "Sending raw: %s", \
format_hex_pretty_to(hex_buf_, data, (len) < API_MAX_LOG_BYTES ? (len) : API_MAX_LOG_BYTES)); \
} while (0)
#else
#define LOG_PACKET_RECEIVED(buffer) ((void) 0)
#define LOG_PACKET_SENDING(data, len) ((void) 0)
#endif
/// Initialize the frame helper, returns OK if successful.
@@ -234,14 +227,44 @@ APIError APIPlaintextFrameHelper::read_packet(ReadPacketBuffer *buffer) {
buffer->type = this->rx_header_parsed_type_;
return APIError::OK;
}
// Compute varint encoded length for a 16-bit value (1, 2, or 3 bytes).
ESPHOME_ALWAYS_INLINE static inline uint8_t varint_encoded_length_16(uint16_t value) {
return value < ProtoSize::VARINT_THRESHOLD_1_BYTE ? 1 : (value < ProtoSize::VARINT_THRESHOLD_2_BYTE ? 2 : 3);
}
// Compute varint encoded length for an 8-bit value (1 or 2 bytes).
ESPHOME_ALWAYS_INLINE static inline uint8_t varint_encoded_length_8(uint8_t value) {
return value < ProtoSize::VARINT_THRESHOLD_1_BYTE ? 1 : 2;
}
// Encode a 16-bit varint (1-3 bytes) using pre-computed length.
ESPHOME_ALWAYS_INLINE static inline void encode_varint_16(uint16_t value, uint8_t varint_len, uint8_t *p) {
if (varint_len >= 2) {
*p++ = static_cast<uint8_t>(value | 0x80);
value >>= 7;
if (varint_len == 3) {
*p++ = static_cast<uint8_t>(value | 0x80);
value >>= 7;
}
}
*p = static_cast<uint8_t>(value);
}
// Encode an 8-bit varint (1-2 bytes) using pre-computed length.
ESPHOME_ALWAYS_INLINE static inline void encode_varint_8(uint8_t value, uint8_t varint_len, uint8_t *p) {
if (varint_len == 2) {
*p++ = static_cast<uint8_t>(value | 0x80);
*p = static_cast<uint8_t>(value >> 7);
} else {
*p = value;
}
}
// Write plaintext header into pre-allocated padding before payload.
// 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
: (msg.payload_size < ProtoSize::VARINT_THRESHOLD_2_BYTE ? 2 : 3);
uint8_t type_varint_len = msg.message_type < ProtoSize::VARINT_THRESHOLD_1_BYTE ? 1 : 2;
uint8_t size_varint_len = varint_encoded_length_16(msg.payload_size);
uint8_t type_varint_len = varint_encoded_length_8(msg.message_type);
uint8_t total_header_len = 1 + size_varint_len + type_varint_len;
// Calculate where to start writing the header
@@ -274,9 +297,9 @@ ESPHOME_ALWAYS_INLINE static inline uint8_t write_plaintext_header(uint8_t *buf_
// Write the plaintext header
buf_start[header_offset] = 0x00; // indicator
// Encode varints directly into buffer
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);
// Encode varints directly into buffer using pre-computed lengths
encode_varint_16(msg.payload_size, size_varint_len, buf_start + header_offset + 1);
encode_varint_8(msg.message_type, type_varint_len, buf_start + header_offset + 1 + size_varint_len);
return total_header_len;
}
@@ -291,7 +314,6 @@ APIError APIPlaintextFrameHelper::write_protobuf_packet(uint8_t type, ProtoWrite
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);
}
@@ -302,23 +324,28 @@ APIError APIPlaintextFrameHelper::write_protobuf_messages(ProtoWriteBuffer buffe
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;
uint8_t *write_start = nullptr;
uint8_t *write_end = nullptr;
// Write headers and compact messages to close 0-3 byte varint padding gaps.
// First iteration records start position via continue; subsequent iterations
// memmove to close gaps between messages.
for (const auto &msg : messages) {
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;
uint8_t *src = buffer_data + msg.offset + HEADER_PADDING - header_len;
uint16_t msg_len = header_len + msg.payload_size;
if (write_start == nullptr) {
write_start = src;
write_end = src + msg_len;
continue;
}
if (src != write_end) {
memmove(write_end, src, msg_len);
}
write_end += 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);
return this->write_raw_fast_buf_(write_start, static_cast<uint16_t>(write_end - write_start));
}
} // namespace esphome::api