diff --git a/esphome/components/sensor/sensor.cpp b/esphome/components/sensor/sensor.cpp index 3364798e55a..c1d28bf260b 100644 --- a/esphome/components/sensor/sensor.cpp +++ b/esphome/components/sensor/sensor.cpp @@ -76,9 +76,7 @@ StateClass Sensor::get_state_class() { void Sensor::publish_state(float state) { this->raw_state = state; - - // Call raw callbacks (before filters) - this->callbacks_.call_first(this->raw_count_, state); + this->raw_callback_.call(state); ESP_LOGV(TAG, "'%s': Received new state %f", this->name_.c_str(), state); @@ -89,12 +87,9 @@ void Sensor::publish_state(float state) { } } -void Sensor::add_on_state_callback(std::function &&callback) { - this->callbacks_.add_second(std::move(callback)); -} - +void Sensor::add_on_state_callback(std::function &&callback) { this->callback_.add(std::move(callback)); } void Sensor::add_on_raw_state_callback(std::function &&callback) { - this->callbacks_.add_first(std::move(callback), &this->raw_count_); + this->raw_callback_.add(std::move(callback)); } void Sensor::add_filter(Filter *filter) { @@ -134,10 +129,7 @@ void Sensor::internal_send_state_to_frontend(float state) { this->state = state; ESP_LOGD(TAG, "'%s': Sending state %.5f %s with %d decimals of accuracy", this->get_name().c_str(), state, this->get_unit_of_measurement_ref().c_str(), this->get_accuracy_decimals()); - - // Call filtered callbacks (after filters) - this->callbacks_.call_second(this->raw_count_, state); - + this->callback_.call(state); #if defined(USE_SENSOR) && defined(USE_CONTROLLER_REGISTRY) ControllerRegistry::notify_sensor_update(this); #endif diff --git a/esphome/components/sensor/sensor.h b/esphome/components/sensor/sensor.h index 6f0412842a1..a792c0d3fd6 100644 --- a/esphome/components/sensor/sensor.h +++ b/esphome/components/sensor/sensor.h @@ -125,7 +125,8 @@ class Sensor : public EntityBase, public EntityBase_DeviceClass, public EntityBa void internal_send_state_to_frontend(float state); protected: - PartitionedCallbackManager callbacks_; + LazyCallbackManager raw_callback_; ///< Storage for raw state callbacks. + LazyCallbackManager callback_; ///< Storage for filtered state callbacks. Filter *filter_list_{nullptr}; ///< Store all active filters. @@ -140,8 +141,6 @@ class Sensor : public EntityBase, public EntityBase_DeviceClass, public EntityBa uint8_t force_update : 1; uint8_t reserved : 5; // Reserved for future use } sensor_flags_{}; - - uint8_t raw_count_{0}; ///< Number of raw callbacks (partition point in callbacks_ vector) }; } // namespace sensor diff --git a/esphome/components/text_sensor/text_sensor.cpp b/esphome/components/text_sensor/text_sensor.cpp index c147e595961..ad1dc0f5217 100644 --- a/esphome/components/text_sensor/text_sensor.cpp +++ b/esphome/components/text_sensor/text_sensor.cpp @@ -30,9 +30,7 @@ void TextSensor::publish_state(const std::string &state) { #pragma GCC diagnostic ignored "-Wdeprecated-declarations" this->raw_state = state; #pragma GCC diagnostic pop - - // Call raw callbacks (before filters) - this->callbacks_.call_first(this->raw_count_, state); + this->raw_callback_.call(state); ESP_LOGV(TAG, "'%s': Received new state %s", this->name_.c_str(), state.c_str()); @@ -74,11 +72,10 @@ void TextSensor::clear_filters() { } void TextSensor::add_on_state_callback(std::function callback) { - this->callbacks_.add_second(std::move(callback)); + this->callback_.add(std::move(callback)); } - void TextSensor::add_on_raw_state_callback(std::function callback) { - this->callbacks_.add_first(std::move(callback), &this->raw_count_); + this->raw_callback_.add(std::move(callback)); } std::string TextSensor::get_state() const { return this->state; } @@ -93,10 +90,7 @@ void TextSensor::internal_send_state_to_frontend(const std::string &state) { this->state = state; this->set_has_state(true); ESP_LOGD(TAG, "'%s': Sending state '%s'", this->name_.c_str(), state.c_str()); - - // Call filtered callbacks (after filters) - this->callbacks_.call_second(this->raw_count_, state); - + this->callback_.call(state); #if defined(USE_TEXT_SENSOR) && defined(USE_CONTROLLER_REGISTRY) ControllerRegistry::notify_text_sensor_update(this); #endif diff --git a/esphome/components/text_sensor/text_sensor.h b/esphome/components/text_sensor/text_sensor.h index 177c9badafe..919bf81c8c2 100644 --- a/esphome/components/text_sensor/text_sensor.h +++ b/esphome/components/text_sensor/text_sensor.h @@ -65,11 +65,10 @@ class TextSensor : public EntityBase, public EntityBase_DeviceClass { void internal_send_state_to_frontend(const std::string &state); protected: - PartitionedCallbackManager callbacks_; + LazyCallbackManager raw_callback_; ///< Storage for raw state callbacks. + LazyCallbackManager callback_; ///< Storage for filtered state callbacks. Filter *filter_list_{nullptr}; ///< Store all active filters. - - uint8_t raw_count_{0}; ///< Number of raw callbacks (partition point in callbacks_ vector) }; } // namespace text_sensor diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index d9296ede043..9c6c983fe86 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -964,71 +964,48 @@ template class CallbackManager { std::vector> callbacks_; }; -template class PartitionedCallbackManager; +template class LazyCallbackManager; -/** Helper class for callbacks partitioned into two sections. +/** Lazy-allocating callback manager that only allocates memory when callbacks are registered. * - * Uses a single vector partitioned into two sections: [first_0, ..., first_m-1, second_0, ..., second_n-1] - * The partition point is tracked externally by the caller (typically stored in the entity class for optimal alignment). + * This is a drop-in replacement for CallbackManager that saves memory when no callbacks + * are registered (common case after the Controller Registry eliminated per-entity callbacks + * from API and web_server components). * - * Memory efficient: Only stores a single pointer (4 bytes on 32-bit platforms, 8 bytes on 64-bit platforms). - * The partition count lives in the entity class where it can be packed with other small fields to avoid padding waste. - * - * Design rationale: The asymmetric API (add_first takes first_count*, while call_first/call_second take it by value) - * is intentional - add_first must increment the count, while call methods only read it. This avoids storing first_count - * internally, saving memory per instance. + * Memory overhead comparison (32-bit systems): + * - CallbackManager: 12 bytes (empty std::vector) + * - LazyCallbackManager: 4 bytes (nullptr unique_ptr) * * @tparam Ts The arguments for the callbacks, wrapped in void(). */ -template class PartitionedCallbackManager { +template class LazyCallbackManager { public: - /// Add a callback to the first partition. - void add_first(std::function &&callback, uint8_t *first_count) { + /// Add a callback to the list. Allocates the underlying CallbackManager on first use. + void add(std::function &&callback) { if (!this->callbacks_) { - this->callbacks_ = make_unique>>(); + this->callbacks_ = make_unique>(); } - - // Add to first partition: append then rotate into position - this->callbacks_->push_back(std::move(callback)); - // Avoid potential underflow: rewrite comparison to not subtract from size() - if (*first_count + 1 < this->callbacks_->size()) { - // Use std::rotate to maintain registration order in second partition - std::rotate(this->callbacks_->begin() + *first_count, this->callbacks_->end() - 1, this->callbacks_->end()); - } - (*first_count)++; + this->callbacks_->add(std::move(callback)); } - /// Add a callback to the second partition. - void add_second(std::function &&callback) { - if (!this->callbacks_) { - this->callbacks_ = make_unique>>(); - } - - // Add to second partition: just append (already at end after first partition) - this->callbacks_->push_back(std::move(callback)); - } - - /// Call all callbacks in the first partition. - void call_first(uint8_t first_count, Ts... args) { + /// Call all callbacks in this manager. No-op if no callbacks registered. + void call(Ts... args) { if (this->callbacks_) { - for (size_t i = 0; i < first_count; i++) { - (*this->callbacks_)[i](args...); - } + this->callbacks_->call(args...); } } - /// Call all callbacks in the second partition. - void call_second(uint8_t first_count, Ts... args) { - if (this->callbacks_) { - for (size_t i = first_count; i < this->callbacks_->size(); i++) { - (*this->callbacks_)[i](args...); - } - } - } + /// Return the number of registered callbacks. + size_t size() const { return this->callbacks_ ? this->callbacks_->size() : 0; } + + /// Check if any callbacks are registered. + bool empty() const { return !this->callbacks_ || this->callbacks_->size() == 0; } + + /// Call all callbacks in this manager. + void operator()(Ts... args) { this->call(args...); } protected: - /// Partitioned callback storage: [first_0, ..., first_m-1, second_0, ..., second_n-1] - std::unique_ptr>> callbacks_; + std::unique_ptr> callbacks_; }; template class LazyCallbackManager;