[core] Add zero-allocation object_id methods (#12578)

This commit is contained in:
J. Nick Koston
2025-12-22 07:56:33 -10:00
committed by GitHub
parent 265ad9d264
commit 6383fe4598
8 changed files with 70 additions and 58 deletions

View File

@@ -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());

View File

@@ -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();

View File

@@ -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 {

View File

@@ -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("\"><td>");
stream->print(obj->get_name().c_str());
stream->print("</td><td></td><td>");

View File

@@ -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<char, OBJECT_ID_MAX_LEN> 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_; }

View File

@@ -1,7 +1,8 @@
#pragma once
#include <string>
#include <cstdint>
#include <span>
#include <string>
#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<char, OBJECT_ID_MAX_LEN> 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)

View File

@@ -189,14 +189,6 @@ template<int (*fn)(int)> std::string str_ctype_transform(const std::string &str)
}
std::string str_lower_case(const std::string &str) { return str_ctype_transform<std::tolower>(str); }
std::string str_upper_case(const std::string &str) { return str_ctype_transform<std::toupper>(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) {

View File

@@ -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);