From 6383fe4598a07057cbede7760c9e735b7bc4ff04 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 22 Dec 2025 07:56:33 -1000 Subject: [PATCH] [core] Add zero-allocation object_id methods (#12578) --- esphome/components/api/api_connection.h | 15 ++---- esphome/components/web_server/web_server.cpp | 8 ++-- esphome/components/web_server/web_server.h | 12 ++--- .../components/web_server/web_server_v1.cpp | 3 +- esphome/core/entity_base.cpp | 46 ++++++++++++++----- esphome/core/entity_base.h | 29 ++++++------ esphome/core/helpers.cpp | 8 ---- esphome/core/helpers.h | 7 +++ 8 files changed, 70 insertions(+), 58 deletions(-) diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index 6753e68749..6363116900 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -323,17 +323,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 6870a1dc87..207eafad5c 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -404,9 +404,11 @@ 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 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()); + char id_buf[160]; // prefix + dash + object_id (up to 128) + null + size_t len = strlen(prefix); + memcpy(id_buf, prefix, len); // NOLINT(bugprone-not-null-terminated-result) - null added by write_object_id_to + id_buf[len++] = '-'; + obj->write_object_id_to(id_buf + len, sizeof(id_buf) - len); root[ESPHOME_F("id")] = id_buf; if (start_config == DETAIL_ALL) { root[ESPHOME_F("name")] = obj->get_name(); 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..b7616a9ad3 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,41 @@ 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()); +} + +// Format dynamic object_id: sanitized snake_case of friendly_name +static size_t format_dynamic_object_id(char *buf, size_t buf_size) { + 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 len; +} + +size_t EntityBase::write_object_id_to(char *buf, size_t buf_size) const { + if (this->is_object_id_dynamic_()) { + return format_dynamic_object_id(buf, buf_size); + } + const char *src = this->object_id_c_str_ == nullptr ? "" : this->object_id_c_str_; + size_t len = strlen(src); + if (len >= buf_size) + len = buf_size - 1; + memcpy(buf, src, len); + buf[len] = '\0'; + return len; +} + +StringRef EntityBase::get_object_id_to(std::span buf) const { + if (this->is_object_id_dynamic_()) { + size_t len = format_dynamic_object_id(buf.data(), buf.size()); + return StringRef(buf.data(), len); + } + return this->object_id_c_str_ == nullptr ? StringRef() : StringRef(this->object_id_c_str_); } 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..eb1ba46c94 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,15 @@ 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; + + /// Write object_id directly to buffer, returns length written (excluding null) + /// Useful for building compound strings without intermediate buffer + size_t write_object_id_to(char *buf, size_t buf_size) 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 +129,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 2f76b6c17d..f55f53f16b 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 02d050d2d1..b575a14d14 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);