mirror of
https://github.com/esphome/esphome.git
synced 2026-09-24 05:24:14 +00:00
[api] Pack batch messages contiguously for write() instead of writev()
Replace StaticVector<iovec> + writev() scatter-gather in the batch write path with contiguous single-buffer write() calls. Plaintext: compact messages via memmove to close 0-3 byte varint header gaps, then write_raw_fast_buf_. Noise: messages are already contiguous (fixed 7-byte header + 16-byte MAC fills all reserved space), switch directly to write_raw_fast_buf_. Fix LOG_PACKET_SENDING to log after write/enqueue to prevent re-entrant log sends from corrupting the shared buffer before data is sent. Consolidate the macro to api_frame_helper.cpp and expose via out-of-line log_packet_sending_() helper. Remove write_raw_fast_iov_ (no remaining callers) and change encrypt_noise_message_ to return uint16_t length instead of iovec.
This commit is contained in:
@@ -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;
|
||||
|
||||
|
||||
@@ -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.
|
||||
@@ -291,7 +284,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 +294,26 @@ 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;
|
||||
|
||||
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;
|
||||
// First message: write header, record start position
|
||||
const auto &first = messages[0];
|
||||
uint8_t header_len = write_plaintext_header(buffer_data + first.offset, first);
|
||||
uint8_t *write_start = buffer_data + first.offset + HEADER_PADDING - header_len;
|
||||
uint8_t *write_end = write_start + header_len + first.payload_size;
|
||||
|
||||
// Subsequent messages: write header, then compact to close 0-3 byte gaps
|
||||
for (size_t i = 1; i < messages.size(); i++) {
|
||||
const auto &msg = messages[i];
|
||||
header_len = write_plaintext_header(buffer_data + msg.offset, msg);
|
||||
uint8_t *src = buffer_data + msg.offset + HEADER_PADDING - header_len;
|
||||
uint16_t msg_len = header_len + msg.payload_size;
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user