diff --git a/esphome/components/api/api_frame_helper.h b/esphome/components/api/api_frame_helper.h index 72ccf8aa56..3507b4d22a 100644 --- a/esphome/components/api/api_frame_helper.h +++ b/esphome/components/api/api_frame_helper.h @@ -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(buffer.get_buffer()->size() - frame_header_padding_ - frame_footer_size_)}; - return write_protobuf_messages(buffer, std::span(&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 messages) = 0; diff --git a/esphome/components/api/api_frame_helper_noise.cpp b/esphome/components/api/api_frame_helper_noise.cpp index 0781c501e5..565a96678e 100644 --- a/esphome/components/api/api_frame_helper_noise.cpp +++ b/esphome/components/api/api_frame_helper_noise.cpp @@ -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 messages) { - StaticVector 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(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(iov.iov_len)); } APIError APINoiseFrameHelper::write_protobuf_messages(ProtoWriteBuffer buffer, std::span messages) { @@ -517,20 +516,20 @@ APIError APINoiseFrameHelper::write_protobuf_messages(ProtoWriteBuffer buffer, s } uint8_t *buffer_data = buffer.get_buffer()->data(); + StaticVector 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(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) { diff --git a/esphome/components/api/api_frame_helper_noise.h b/esphome/components/api/api_frame_helper_noise.h index 7a81d2032e..e56006d955 100644 --- a/esphome/components/api/api_frame_helper_noise.h +++ b/esphome/components/api/api_frame_helper_noise.h @@ -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 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 messages); APIError init_handshake_(); APIError check_handshake_finished_(); void send_explicit_handshake_reject_(const LogString *reason); diff --git a/esphome/components/api/api_frame_helper_plaintext.cpp b/esphome/components/api/api_frame_helper_plaintext.cpp index 9d560a0333..0dc5690b8f 100644 --- a/esphome/components/api/api_frame_helper_plaintext.cpp +++ b/esphome/components/api/api_frame_helper_plaintext.cpp @@ -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 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 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(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(buffer_data + frame_header_padding_ - msg_start); + size_t msg_len = static_cast(msg_header_len + msg.payload_size); + struct iovec iov = {msg_start, msg_len}; + return write_raw_(&iov, 1, static_cast(msg_len)); +} + +APIError APIPlaintextFrameHelper::write_protobuf_messages(ProtoWriteBuffer buffer, + std::span 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 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 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((buffer_data + first.offset + frame_header_padding_) - first_start); - size_t first_len = static_cast(first_header_len + first.payload_size); - struct iovec iov = {first_start, first_len}; - return write_raw_(&iov, 1, static_cast(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 diff --git a/esphome/components/api/api_frame_helper_plaintext.h b/esphome/components/api/api_frame_helper_plaintext.h index 84311cb93a..96d47e9c7b 100644 --- a/esphome/components/api/api_frame_helper_plaintext.h +++ b/esphome/components/api/api_frame_helper_plaintext.h @@ -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 messages) override; protected: APIError try_read_frame_(); - APIError write_protobuf_messages_batch_(uint8_t *buffer_data, std::span messages); // Group 2-byte aligned types uint16_t rx_header_parsed_type_ = 0;