[api] Split write_protobuf_packet into dedicated virtual for single messages

Instead of routing single messages through write_protobuf_messages and
branching internally, make write_protobuf_packet a separate virtual
override in each frame helper. The single-message path gets its own
minimal stack frame with no StaticVector allocation, and the batch
path in write_protobuf_messages has no size==1 branch — each caller
picks the right method upfront.
This commit is contained in:
J. Nick Koston
2026-03-29 10:32:00 -10:00
parent 9a89641377
commit 481c0688ad
5 changed files with 52 additions and 66 deletions
+3 -9
View File
@@ -161,15 +161,9 @@ class APIFrameHelper {
this->nodelay_counter_ = 0;
}
}
APIError write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) {
// Resize buffer to include footer space if needed (e.g. Noise MAC)
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_)};
return write_protobuf_messages(buffer, std::span<const MessageInfo>(&msg, 1));
}
// Write multiple protobuf messages in a single operation
// Write a single protobuf message - the hot path (87-100% of all writes)
virtual APIError write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) = 0;
// Write multiple protobuf messages in a single batched operation
// messages contains (message_type, offset, length) for each message in the buffer
// The buffer contains all messages with appropriate padding before each
virtual APIError write_protobuf_messages(ProtoWriteBuffer buffer, std::span<const MessageInfo> messages) = 0;
@@ -488,23 +488,22 @@ APIError APINoiseFrameHelper::encrypt_noise_message_(uint8_t *buf_start, const M
return APIError::OK;
}
// Outlined multi-message path to keep the single-message fast path's stack frame small.
APIError __attribute__((noinline, flatten))
APINoiseFrameHelper::write_protobuf_messages_batch_(uint8_t *buffer_data, std::span<const MessageInfo> messages) {
StaticVector<struct iovec, MAX_MESSAGES_PER_BATCH> iovs;
uint16_t total_write_len = 0;
APIError APINoiseFrameHelper::write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) {
APIError aerr = this->check_data_state_();
if (aerr != APIError::OK)
return aerr;
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);
if (aerr != APIError::OK)
return aerr;
iovs.push_back(iov);
total_write_len += iov.iov_len;
}
// Resize buffer to include footer space for Noise MAC
if (frame_footer_size_)
buffer.get_buffer()->resize(buffer.get_buffer()->size() + frame_footer_size_);
return this->write_raw_(iovs.data(), iovs.size(), total_write_len);
MessageInfo msg{type, 0,
static_cast<uint16_t>(buffer.get_buffer()->size() - frame_header_padding_ - frame_footer_size_)};
struct iovec iov;
aerr = this->encrypt_noise_message_(buffer.get_buffer()->data(), msg, iov);
if (aerr != APIError::OK)
return aerr;
return this->write_raw_(&iov, 1, static_cast<uint16_t>(iov.iov_len));
}
APIError APINoiseFrameHelper::write_protobuf_messages(ProtoWriteBuffer buffer, std::span<const MessageInfo> messages) {
@@ -517,20 +516,20 @@ APIError APINoiseFrameHelper::write_protobuf_messages(ProtoWriteBuffer buffer, s
}
uint8_t *buffer_data = buffer.get_buffer()->data();
StaticVector<struct iovec, MAX_MESSAGES_PER_BATCH> iovs;
uint16_t total_write_len = 0;
if (messages.size() == 1) [[likely]] {
// Peeled first iteration: single-message case (most common path via write_protobuf_packet)
// avoids StaticVector stack allocation and loop overhead
const auto &first = messages[0];
for (const auto &msg : messages) {
uint8_t *buf_start = buffer_data + msg.offset;
struct iovec iov;
aerr = this->encrypt_noise_message_(buffer_data + first.offset, first, iov);
aerr = this->encrypt_noise_message_(buf_start, msg, iov);
if (aerr != APIError::OK)
return aerr;
return this->write_raw_(&iov, 1, static_cast<uint16_t>(iov.iov_len));
iovs.push_back(iov);
total_write_len += iov.iov_len;
}
// Multiple messages: outlined to avoid large stack frame on single-message path
return this->write_protobuf_messages_batch_(buffer_data, messages);
return this->write_raw_(iovs.data(), iovs.size(), total_write_len);
}
APIError APINoiseFrameHelper::write_frame_(const uint8_t *data, uint16_t len) {
@@ -22,6 +22,7 @@ class APINoiseFrameHelper final : public APIFrameHelper {
APIError init() override;
APIError loop() override;
APIError read_packet(ReadPacketBuffer *buffer) override;
APIError write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) override;
APIError write_protobuf_messages(ProtoWriteBuffer buffer, std::span<const MessageInfo> messages) override;
protected:
@@ -29,7 +30,6 @@ class APINoiseFrameHelper final : public APIFrameHelper {
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 write_protobuf_messages_batch_(uint8_t *buffer_data, std::span<const MessageInfo> messages);
APIError init_handshake_();
APIError check_handshake_finished_();
void send_explicit_handshake_reject_(const LogString *reason);
@@ -285,11 +285,31 @@ static inline uint8_t *write_plaintext_header(uint8_t *buf_start, const MessageI
return buf_start + header_offset;
}
// Outlined multi-message path to keep the single-message fast path's stack frame small.
// The StaticVector<iovec, MAX_MESSAGES_PER_BATCH> would force a ~300-byte stack frame
// even when only sending one message if it were in the same function.
APIError __attribute__((noinline, flatten))
APIPlaintextFrameHelper::write_protobuf_messages_batch_(uint8_t *buffer_data, std::span<const MessageInfo> messages) {
APIError APIPlaintextFrameHelper::write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) {
APIError aerr = this->check_data_state_();
if (aerr != APIError::OK)
return aerr;
MessageInfo msg{type, 0, static_cast<uint16_t>(buffer.get_buffer()->size() - frame_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);
size_t msg_len = static_cast<size_t>(msg_header_len + msg.payload_size);
struct iovec iov = {msg_start, msg_len};
return write_raw_(&iov, 1, static_cast<uint16_t>(msg_len));
}
APIError APIPlaintextFrameHelper::write_protobuf_messages(ProtoWriteBuffer buffer,
std::span<const MessageInfo> messages) {
APIError aerr = this->check_data_state_();
if (aerr != APIError::OK)
return aerr;
if (messages.empty()) {
return APIError::OK;
}
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_;
@@ -305,33 +325,6 @@ APIPlaintextFrameHelper::write_protobuf_messages_batch_(uint8_t *buffer_data, st
return write_raw_(iovs.data(), iovs.size(), total_write_len);
}
APIError APIPlaintextFrameHelper::write_protobuf_messages(ProtoWriteBuffer buffer,
std::span<const MessageInfo> messages) {
APIError aerr = this->check_data_state_();
if (aerr != APIError::OK)
return aerr;
if (messages.empty()) {
return APIError::OK;
}
uint8_t *buffer_data = buffer.get_buffer()->data();
if (messages.size() == 1) [[likely]] {
// Peeled first iteration: single-message case (most common path via write_protobuf_packet)
// avoids StaticVector stack allocation and loop overhead
const auto &first = messages[0];
uint8_t *first_start = write_plaintext_header(buffer_data + first.offset, first, frame_header_padding_);
uint8_t first_header_len = static_cast<uint8_t>((buffer_data + first.offset + frame_header_padding_) - first_start);
size_t first_len = static_cast<size_t>(first_header_len + first.payload_size);
struct iovec iov = {first_start, first_len};
return write_raw_(&iov, 1, static_cast<uint16_t>(first_len));
}
// Multiple messages: outlined to avoid large stack frame on single-message path
return write_protobuf_messages_batch_(buffer_data, messages);
}
} // namespace esphome::api
#endif // USE_API_PLAINTEXT
#endif // USE_API
@@ -19,11 +19,11 @@ class APIPlaintextFrameHelper final : public APIFrameHelper {
APIError init() override;
APIError loop() override;
APIError read_packet(ReadPacketBuffer *buffer) override;
APIError write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) override;
APIError write_protobuf_messages(ProtoWriteBuffer buffer, std::span<const MessageInfo> messages) override;
protected:
APIError try_read_frame_();
APIError write_protobuf_messages_batch_(uint8_t *buffer_data, std::span<const MessageInfo> messages);
// Group 2-byte aligned types
uint16_t rx_header_parsed_type_ = 0;