From cd6240541b08de166f1ba8f8e366def0daaf6231 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 19 Dec 2025 15:03:27 -1000 Subject: [PATCH] [core] Add zero-allocation get_object_id_to() method --- esphome/components/api/api_connection.h | 15 +++------- esphome/components/web_server/web_server.cpp | 3 +- esphome/components/web_server/web_server.h | 12 +++----- .../components/web_server/web_server_v1.cpp | 3 +- esphome/core/entity_base.cpp | 29 ++++++++++++------- esphome/core/entity_base.h | 25 ++++++---------- esphome/core/helpers.cpp | 8 ----- esphome/core/helpers.h | 7 +++++ 8 files changed, 46 insertions(+), 56 deletions(-) diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index b50be5d0d4..268d3f4b05 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -310,17 +310,10 @@ class APIConnection final : public APIServerConnection { APIConnection *conn, uint32_t remaining_size, bool is_single) { // Set common fields that are shared by all entity types msg.key = entity->get_object_id_hash(); - // Try to use static reference first to avoid allocation - StringRef static_ref = entity->get_object_id_ref_for_api_(); - // Store dynamic string outside the if-else to maintain lifetime - std::string object_id; - if (!static_ref.empty()) { - msg.set_object_id(static_ref); - } else { - // Dynamic case - need to allocate - object_id = entity->get_object_id(); - msg.set_object_id(StringRef(object_id)); - } + // Get object_id with zero heap allocation + // Static case returns direct reference, dynamic case uses buffer + char object_id_buf[OBJECT_ID_MAX_LEN]; + msg.set_object_id(entity->get_object_id_to(object_id_buf)); if (entity->has_own_name()) { msg.set_name(entity->get_name()); diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index 0c22c2f08d..e6a7b48fbd 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -404,8 +404,9 @@ void WebServer::handle_js_request(AsyncWebServerRequest *request) { // Helper functions to reduce code size by avoiding macro expansion static void set_json_id(JsonObject &root, EntityBase *obj, const char *prefix, JsonDetail start_config) { + char object_id_buf[OBJECT_ID_MAX_LEN]; + StringRef object_id = obj->get_object_id_to(object_id_buf); char id_buf[160]; // object_id can be up to 128 chars + prefix + dash + null - const auto &object_id = obj->get_object_id(); snprintf(id_buf, sizeof(id_buf), "%s-%s", prefix, object_id.c_str()); root[ESPHOME_F("id")] = id_buf; if (start_config == DETAIL_ALL) { diff --git a/esphome/components/web_server/web_server.h b/esphome/components/web_server/web_server.h index bb69d57872..98234ec1ae 100644 --- a/esphome/components/web_server/web_server.h +++ b/esphome/components/web_server/web_server.h @@ -52,14 +52,10 @@ struct UrlMatch { } bool id_equals_entity(EntityBase *entity) const { - // Zero-copy comparison using StringRef - StringRef static_ref = entity->get_object_id_ref_for_api_(); - if (!static_ref.empty()) { - return id && id_len == static_ref.size() && memcmp(id, static_ref.c_str(), id_len) == 0; - } - // Fallback to allocation (rare) - const auto &obj_id = entity->get_object_id(); - return id && id_len == obj_id.length() && memcmp(id, obj_id.c_str(), id_len) == 0; + // Get object_id with zero heap allocation + char object_id_buf[OBJECT_ID_MAX_LEN]; + StringRef object_id = entity->get_object_id_to(object_id_buf); + return id && id_len == object_id.size() && memcmp(id, object_id.c_str(), id_len) == 0; } bool method_equals(const char *str) const { diff --git a/esphome/components/web_server/web_server_v1.cpp b/esphome/components/web_server/web_server_v1.cpp index 4f0d0cd1a9..cbc25b9dec 100644 --- a/esphome/components/web_server/web_server_v1.cpp +++ b/esphome/components/web_server/web_server_v1.cpp @@ -15,7 +15,8 @@ void write_row(AsyncResponseStream *stream, EntityBase *obj, const std::string & stream->print("\" id=\""); stream->print(klass.c_str()); stream->print("-"); - stream->print(obj->get_object_id().c_str()); + char object_id_buf[OBJECT_ID_MAX_LEN]; + stream->print(obj->get_object_id_to(object_id_buf).c_str()); stream->print("\">"); stream->print(obj->get_name().c_str()); stream->print(""); diff --git a/esphome/core/entity_base.cpp b/esphome/core/entity_base.cpp index 046f99d8cc..98fb957971 100644 --- a/esphome/core/entity_base.cpp +++ b/esphome/core/entity_base.cpp @@ -60,15 +60,6 @@ std::string EntityBase::get_object_id() const { // `App.get_friendly_name()` is constant. return this->object_id_c_str_ == nullptr ? "" : this->object_id_c_str_; } -StringRef EntityBase::get_object_id_ref_for_api_() const { - static constexpr auto EMPTY_STRING = StringRef::from_lit(""); - // Return empty for dynamic case (MAC suffix) - if (this->is_object_id_dynamic_()) { - return EMPTY_STRING; - } - // For static case, return the string or empty if null - return this->object_id_c_str_ == nullptr ? EMPTY_STRING : StringRef(this->object_id_c_str_); -} void EntityBase::set_object_id(const char *object_id) { this->object_id_c_str_ = object_id; this->calc_object_id_(); @@ -82,8 +73,24 @@ void EntityBase::set_name_and_object_id(const char *name, const char *object_id) // Calculate Object ID Hash from Entity Name void EntityBase::calc_object_id_() { - this->object_id_hash_ = - fnv1_hash(this->is_object_id_dynamic_() ? this->get_object_id().c_str() : this->object_id_c_str_); + char buf[OBJECT_ID_MAX_LEN]; + StringRef object_id = this->get_object_id_to(buf); + this->object_id_hash_ = fnv1_hash(object_id.c_str()); +} + +StringRef EntityBase::get_object_id_to(std::span buf) const { + if (!this->is_object_id_dynamic_()) { + // Static case: return direct reference, buffer unused + return this->object_id_c_str_ == nullptr ? StringRef() : StringRef(this->object_id_c_str_); + } + // Dynamic case: format into buffer + const std::string &name = App.get_friendly_name(); + size_t len = std::min(name.size(), buf.size() - 1); + for (size_t i = 0; i < len; i++) { + buf[i] = to_sanitized_char(to_snake_case_char(name[i])); + } + buf[len] = '\0'; + return StringRef(buf.data(), len); } uint32_t EntityBase::get_object_id_hash() { return this->object_id_hash_; } diff --git a/esphome/core/entity_base.h b/esphome/core/entity_base.h index fdf3f6300a..1714194802 100644 --- a/esphome/core/entity_base.h +++ b/esphome/core/entity_base.h @@ -1,7 +1,8 @@ #pragma once -#include #include +#include +#include #include "string_ref.h" #include "helpers.h" #include "log.h" @@ -12,14 +13,8 @@ namespace esphome { -// Forward declaration for friend access -namespace api { -class APIConnection; -} // namespace api - -namespace web_server { -struct UrlMatch; -} // namespace web_server +// Maximum size for object_id buffer (friendly_name max ~120 + margin) +static constexpr size_t OBJECT_ID_MAX_LEN = 128; enum EntityCategory : uint8_t { ENTITY_CATEGORY_NONE = 0, @@ -47,6 +42,11 @@ class EntityBase { // Get the unique Object ID of this Entity uint32_t get_object_id_hash(); + /// Get object_id with zero heap allocation + /// For static case: returns StringRef to internal storage (buffer unused) + /// For dynamic case: formats into buffer and returns StringRef to buffer + StringRef get_object_id_to(std::span buf) const; + // Get/set whether this Entity should be hidden outside ESPHome bool is_internal() const { return this->flags_.internal; } void set_internal(bool internal) { this->flags_.internal = internal; } @@ -125,13 +125,6 @@ class EntityBase { } protected: - friend class api::APIConnection; - friend struct web_server::UrlMatch; - - // Get object_id as StringRef when it's static (for API usage) - // Returns empty StringRef if object_id is dynamic (needs allocation) - StringRef get_object_id_ref_for_api_() const; - void calc_object_id_(); /// Check if the object_id is dynamic (changes with MAC suffix) diff --git a/esphome/core/helpers.cpp b/esphome/core/helpers.cpp index bbe59e53f1..4b905553ba 100644 --- a/esphome/core/helpers.cpp +++ b/esphome/core/helpers.cpp @@ -189,14 +189,6 @@ template std::string str_ctype_transform(const std::string &str) } std::string str_lower_case(const std::string &str) { return str_ctype_transform(str); } std::string str_upper_case(const std::string &str) { return str_ctype_transform(str); } -// Convert char to snake_case: lowercase and spaces to underscores -static constexpr char to_snake_case_char(char c) { - return (c == ' ') ? '_' : (c >= 'A' && c <= 'Z') ? c + ('a' - 'A') : c; -} -// Sanitize char: keep alphanumerics, dashes, underscores; replace others with underscore -static constexpr char to_sanitized_char(char c) { - return (c == '-' || c == '_' || (c >= '0' && c <= '9') || (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')) ? c : '_'; -} std::string str_snake_case(const std::string &str) { std::string result = str; for (char &c : result) { diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index f9dcfccb45..21e910916e 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -516,9 +516,16 @@ std::string str_until(const std::string &str, char ch); std::string str_lower_case(const std::string &str); /// Convert the string to upper case. std::string str_upper_case(const std::string &str); + +/// Convert a single char to snake_case: lowercase and space to underscore. +constexpr char to_snake_case_char(char c) { return (c == ' ') ? '_' : (c >= 'A' && c <= 'Z') ? c + ('a' - 'A') : c; } /// Convert the string to snake case (lowercase with underscores). std::string str_snake_case(const std::string &str); +/// Sanitize a single char: keep alphanumerics, dashes, underscores; replace others with underscore. +constexpr char to_sanitized_char(char c) { + return (c == '-' || c == '_' || (c >= '0' && c <= '9') || (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')) ? c : '_'; +} /// Sanitizes the input string by removing all characters but alphanumerics, dashes and underscores. std::string str_sanitize(const std::string &str);