From 79e3d2b2d78c52e4a00f0e2a177736c1f489bf4b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 26 Jun 2025 03:55:12 +0200 Subject: [PATCH] Optimize API connection memory with tagged pointers (#9203) --- esphome/components/api/api_connection.cpp | 35 +++++----- esphome/components/api/api_connection.h | 83 ++++++++++++----------- 2 files changed, 64 insertions(+), 54 deletions(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index e9b46853f4..b7f6f04be0 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -1432,7 +1432,7 @@ void APIConnection::alarm_control_panel_command(const AlarmControlPanelCommandRe #ifdef USE_EVENT void APIConnection::send_event(event::Event *event, const std::string &event_type) { - this->schedule_message_(event, MessageCreator(event_type, EventResponse::MESSAGE_TYPE), EventResponse::MESSAGE_TYPE); + this->schedule_message_(event, MessageCreator(event_type), EventResponse::MESSAGE_TYPE); } void APIConnection::send_event_info(event::Event *event) { this->schedule_message_(event, &APIConnection::try_send_event_info, ListEntitiesEventResponse::MESSAGE_TYPE); @@ -1787,7 +1787,8 @@ void APIConnection::process_batch_() { const auto &item = this->deferred_batch_.items[0]; // Let the creator calculate size and encode if it fits - uint16_t payload_size = item.creator(item.entity, this, std::numeric_limits::max(), true); + uint16_t payload_size = + item.creator(item.entity, this, std::numeric_limits::max(), true, item.message_type); if (payload_size > 0 && this->send_buffer(ProtoWriteBuffer{&this->parent_->get_shared_buffer_ref()}, item.message_type)) { @@ -1837,7 +1838,7 @@ void APIConnection::process_batch_() { for (const auto &item : this->deferred_batch_.items) { // Try to encode message // The creator will calculate overhead to determine if the message fits - uint16_t payload_size = item.creator(item.entity, this, remaining_size, false); + uint16_t payload_size = item.creator(item.entity, this, remaining_size, false, item.message_type); if (payload_size == 0) { // Message won't fit, stop processing @@ -1900,21 +1901,23 @@ void APIConnection::process_batch_() { } uint16_t APIConnection::MessageCreator::operator()(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single) const { - switch (message_type_) { - case 0: // Function pointer - return data_.ptr(entity, conn, remaining_size, is_single); - + bool is_single, uint16_t message_type) const { + if (has_tagged_string_ptr_()) { + // Handle string-based messages + switch (message_type) { #ifdef USE_EVENT - case EventResponse::MESSAGE_TYPE: { - auto *e = static_cast(entity); - return APIConnection::try_send_event_response(e, *data_.string_ptr, conn, remaining_size, is_single); - } + case EventResponse::MESSAGE_TYPE: { + auto *e = static_cast(entity); + return APIConnection::try_send_event_response(e, *get_string_ptr_(), conn, remaining_size, is_single); + } #endif - - default: - // Should not happen, return 0 to indicate no message - return 0; + default: + // Should not happen, return 0 to indicate no message + return 0; + } + } else { + // Function pointer case + return data_.ptr(entity, conn, remaining_size, is_single); } } diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index da12a3e449..e872711e95 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -483,55 +483,57 @@ class APIConnection : public APIServerConnection { // Function pointer type for message encoding using MessageCreatorPtr = uint16_t (*)(EntityBase *, APIConnection *, uint32_t remaining_size, bool is_single); - // Optimized MessageCreator class using union dispatch + // Optimized MessageCreator class using tagged pointer class MessageCreator { + // Ensure pointer alignment allows LSB tagging + static_assert(alignof(std::string *) > 1, "String pointer alignment must be > 1 for LSB tagging"); + public: - // Constructor for function pointer (message_type = 0) - MessageCreator(MessageCreatorPtr ptr) : message_type_(0) { data_.ptr = ptr; } + // Constructor for function pointer + MessageCreator(MessageCreatorPtr ptr) { + // Function pointers are always aligned, so LSB is 0 + data_.ptr = ptr; + } // Constructor for string state capture - MessageCreator(const std::string &value, uint16_t msg_type) : message_type_(msg_type) { - data_.string_ptr = new std::string(value); + explicit MessageCreator(const std::string &str_value) { + // Allocate string and tag the pointer + auto *str = new std::string(str_value); + // Set LSB to 1 to indicate string pointer + data_.tagged = reinterpret_cast(str) | 1; } // Destructor ~MessageCreator() { - // Clean up string data for string-based message types - if (uses_string_data_()) { - delete data_.string_ptr; + if (has_tagged_string_ptr_()) { + delete get_string_ptr_(); } } // Copy constructor - MessageCreator(const MessageCreator &other) : message_type_(other.message_type_) { - if (message_type_ == 0) { - data_.ptr = other.data_.ptr; - } else if (uses_string_data_()) { - data_.string_ptr = new std::string(*other.data_.string_ptr); + MessageCreator(const MessageCreator &other) { + if (other.has_tagged_string_ptr_()) { + auto *str = new std::string(*other.get_string_ptr_()); + data_.tagged = reinterpret_cast(str) | 1; } else { - data_ = other.data_; // For POD types + data_ = other.data_; } } // Move constructor - MessageCreator(MessageCreator &&other) noexcept : data_(other.data_), message_type_(other.message_type_) { - other.message_type_ = 0; // Reset other to function pointer type - other.data_.ptr = nullptr; - } + MessageCreator(MessageCreator &&other) noexcept : data_(other.data_) { other.data_.ptr = nullptr; } // Assignment operators (needed for batch deduplication) MessageCreator &operator=(const MessageCreator &other) { if (this != &other) { // Clean up current string data if needed - if (uses_string_data_()) { - delete data_.string_ptr; + if (has_tagged_string_ptr_()) { + delete get_string_ptr_(); } // Copy new data - message_type_ = other.message_type_; - if (other.message_type_ == 0) { - data_.ptr = other.data_.ptr; - } else if (other.uses_string_data_()) { - data_.string_ptr = new std::string(*other.data_.string_ptr); + if (other.has_tagged_string_ptr_()) { + auto *str = new std::string(*other.get_string_ptr_()); + data_.tagged = reinterpret_cast(str) | 1; } else { data_ = other.data_; } @@ -542,30 +544,35 @@ class APIConnection : public APIServerConnection { MessageCreator &operator=(MessageCreator &&other) noexcept { if (this != &other) { // Clean up current string data if needed - if (uses_string_data_()) { - delete data_.string_ptr; + if (has_tagged_string_ptr_()) { + delete get_string_ptr_(); } // Move data - message_type_ = other.message_type_; data_ = other.data_; // Reset other to safe state - other.message_type_ = 0; other.data_.ptr = nullptr; } return *this; } - // Call operator - uint16_t operator()(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single) const; + // Call operator - now accepts message_type as parameter + uint16_t operator()(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single, + uint16_t message_type) const; private: - // Helper to check if this message type uses heap-allocated strings - bool uses_string_data_() const { return message_type_ == EventResponse::MESSAGE_TYPE; } - union CreatorData { - MessageCreatorPtr ptr; // 8 bytes - std::string *string_ptr; // 8 bytes - } data_; // 8 bytes - uint16_t message_type_; // 2 bytes (0 = function ptr, >0 = state capture) + // Check if this contains a string pointer + bool has_tagged_string_ptr_() const { return (data_.tagged & 1) != 0; } + + // Get the actual string pointer (clears the tag bit) + std::string *get_string_ptr_() const { + // NOLINTNEXTLINE(performance-no-int-to-ptr) + return reinterpret_cast(data_.tagged & ~uintptr_t(1)); + } + + union { + MessageCreatorPtr ptr; + uintptr_t tagged; + } data_; // 4 bytes on 32-bit }; // Generic batching mechanism for both state updates and entity info