From 2c0f4d8f806ca17069112be5098dedf56805be5a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 9 Dec 2025 16:35:14 +0100 Subject: [PATCH] [api] Reduce heap usage for Home Assistant service call string storage (#12151) --- .../components/api/homeassistant_service.h | 32 ++++++++++------ esphome/core/automation.h | 37 ++++++++++++++----- 2 files changed, 48 insertions(+), 21 deletions(-) diff --git a/esphome/components/api/homeassistant_service.h b/esphome/components/api/homeassistant_service.h index 397520fa2..2da6e1536 100644 --- a/esphome/components/api/homeassistant_service.h +++ b/esphome/components/api/homeassistant_service.h @@ -17,6 +17,12 @@ namespace esphome::api { template class TemplatableStringValue : public TemplatableValue { + // Verify that const char* uses the base class STATIC_STRING optimization (no heap allocation) + // rather than being wrapped in a lambda. The base class constructor for const char* is more + // specialized than the templated constructor here, so it should be selected. + static_assert(std::is_constructible_v, const char *>, + "Base class must have const char* constructor for STATIC_STRING optimization"); + private: // Helper to convert value to string - handles the case where value is already a string template static std::string value_to_string(T &&val) { return to_string(std::forward(val)); } @@ -47,10 +53,10 @@ template class TemplatableKeyValuePair { // Keys are always string literals from YAML dictionary keys (e.g., "code", "event") // and never templatable values or lambdas. Only the value parameter can be a lambda/template. - // Using pass-by-value with std::move allows optimal performance for both lvalues and rvalues. - template TemplatableKeyValuePair(std::string key, T value) : key(std::move(key)), value(value) {} + // Using const char* avoids std::string heap allocation - keys remain in flash. + template TemplatableKeyValuePair(const char *key, T value) : key(key), value(value) {} - std::string key; + const char *key{nullptr}; TemplatableStringValue value; }; @@ -109,14 +115,15 @@ template class HomeAssistantServiceCallAction : public Action void add_data(K &&key, V &&value) { - this->add_kv_(this->data_, std::forward(key), std::forward(value)); + // Using const char* for keys avoids std::string heap allocation - keys remain in flash. + template void add_data(const char *key, V &&value) { + this->add_kv_(this->data_, key, std::forward(value)); } - template void add_data_template(K &&key, V &&value) { - this->add_kv_(this->data_template_, std::forward(key), std::forward(value)); + template void add_data_template(const char *key, V &&value) { + this->add_kv_(this->data_template_, key, std::forward(value)); } - template void add_variable(K &&key, V &&value) { - this->add_kv_(this->variables_, std::forward(key), std::forward(value)); + template void add_variable(const char *key, V &&value) { + this->add_kv_(this->variables_, key, std::forward(value)); } #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES @@ -189,10 +196,11 @@ template class HomeAssistantServiceCallAction : public Action void add_kv_(FixedVector> &vec, K &&key, V &&value) { + // Helper to add key-value pairs to FixedVectors + // Keys are always string literals (const char*), values can be lambdas/templates + template void add_kv_(FixedVector> &vec, const char *key, V &&value) { auto &kv = vec.emplace_back(); - kv.key = std::forward(key); + kv.key = key; kv.value = std::forward(value); } diff --git a/esphome/core/automation.h b/esphome/core/automation.h index dacadd35e..61d2944ac 100644 --- a/esphome/core/automation.h +++ b/esphome/core/automation.h @@ -45,6 +45,12 @@ template class TemplatableValue { public: TemplatableValue() : type_(NONE) {} + // For const char* when T is std::string: store pointer directly, no heap allocation + // String remains in flash and is only converted to std::string when value() is called + TemplatableValue(const char *str) requires std::same_as : type_(STATIC_STRING) { + this->static_str_ = str; + } + template TemplatableValue(F value) requires(!std::invocable) : type_(VALUE) { new (&this->value_) T(std::move(value)); } @@ -64,24 +70,28 @@ template class TemplatableValue { // Copy constructor TemplatableValue(const TemplatableValue &other) : type_(other.type_) { - if (type_ == VALUE) { + if (this->type_ == VALUE) { new (&this->value_) T(other.value_); - } else if (type_ == LAMBDA) { + } else if (this->type_ == LAMBDA) { this->f_ = new std::function(*other.f_); - } else if (type_ == STATELESS_LAMBDA) { + } else if (this->type_ == STATELESS_LAMBDA) { this->stateless_f_ = other.stateless_f_; + } else if (this->type_ == STATIC_STRING) { + this->static_str_ = other.static_str_; } } // Move constructor TemplatableValue(TemplatableValue &&other) noexcept : type_(other.type_) { - if (type_ == VALUE) { + if (this->type_ == VALUE) { new (&this->value_) T(std::move(other.value_)); - } else if (type_ == LAMBDA) { + } else if (this->type_ == LAMBDA) { this->f_ = other.f_; other.f_ = nullptr; - } else if (type_ == STATELESS_LAMBDA) { + } else if (this->type_ == STATELESS_LAMBDA) { this->stateless_f_ = other.stateless_f_; + } else if (this->type_ == STATIC_STRING) { + this->static_str_ = other.static_str_; } other.type_ = NONE; } @@ -104,12 +114,12 @@ template class TemplatableValue { } ~TemplatableValue() { - if (type_ == VALUE) { + if (this->type_ == VALUE) { this->value_.~T(); - } else if (type_ == LAMBDA) { + } else if (this->type_ == LAMBDA) { delete this->f_; } - // STATELESS_LAMBDA/NONE: no cleanup needed (function pointer or empty, not heap-allocated) + // STATELESS_LAMBDA/STATIC_STRING/NONE: no cleanup needed (pointers, not heap-allocated) } bool has_value() { return this->type_ != NONE; } @@ -122,6 +132,13 @@ template class TemplatableValue { return (*this->f_)(x...); // std::function call case VALUE: return this->value_; + case STATIC_STRING: + // if constexpr required: code must compile for all T, but STATIC_STRING + // can only be set when T is std::string (enforced by constructor constraint) + if constexpr (std::same_as) { + return std::string(this->static_str_); + } + __builtin_unreachable(); case NONE: default: return T{}; @@ -148,12 +165,14 @@ template class TemplatableValue { VALUE, LAMBDA, STATELESS_LAMBDA, + STATIC_STRING, // For const char* when T is std::string - avoids heap allocation } type_; union { T value_; std::function *f_; T (*stateless_f_)(X...); + const char *static_str_; // For STATIC_STRING type }; };