mirror of
https://github.com/esphome/esphome.git
synced 2026-09-15 09:08:41 +00:00
[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:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user