diff --git a/esphome/core/application.cpp b/esphome/core/application.cpp index c977fd66b3a..26cd6706295 100644 --- a/esphome/core/application.cpp +++ b/esphome/core/application.cpp @@ -79,7 +79,12 @@ static void insertion_sort_by_priority(Iterator first, Iterator last) { } } -void Application::register_component_(Component *comp) { this->components_.push_back(comp); } +void Application::register_component_impl_(Component *comp, bool has_loop) { + if (has_loop) { + comp->component_state_ |= COMPONENT_HAS_LOOP; + } + this->components_.push_back(comp); +} void Application::setup() { ESP_LOGI(TAG, "Running through setup()"); ESP_LOGV(TAG, "Sorting components by setup priority"); @@ -382,16 +387,8 @@ void Application::teardown_components(uint32_t timeout_ms) { } void Application::calculate_looping_components_() { - // Count total components that need looping - size_t total_looping = 0; - for (auto *obj : this->components_) { - if (obj->has_overridden_loop()) { - total_looping++; - } - } - - // Initialize FixedVector with exact size - no reallocation possible - this->looping_components_.init(total_looping); + // FixedVector capacity was pre-initialized by codegen with the exact count + // of components that override loop(), computed at C++ compile time. // Add all components with loop override that aren't already LOOP_DONE // Some components (like logger) may call disable_loop() during initialization diff --git a/esphome/core/application.h b/esphome/core/application.h index 659b60222d9..44e8de7ee9f 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -5,6 +5,7 @@ #include #include #include +#include #include #include "esphome/core/component.h" #include "esphome/core/defines.h" @@ -542,7 +543,14 @@ class Application { #endif #endif - void register_component_(Component *comp); + /// Register a component, detecting loop() override at compile time. + /// The template resolves &T::loop vs &Component::loop as a constexpr bool + /// and forwards it to register_component_impl_ which stores it in component_state_. + template void register_component_(T *comp) { + this->register_component_impl_(comp, !std::is_same_v); + } + + void register_component_impl_(Component *comp, bool has_loop); void calculate_looping_components_(); void add_looping_components_by_state_(bool match_loop_done); diff --git a/esphome/core/component.cpp b/esphome/core/component.cpp index 5afd901da23..a71aa8b3a31 100644 --- a/esphome/core/component.cpp +++ b/esphome/core/component.cpp @@ -496,18 +496,6 @@ void Component::set_setup_priority(float priority) { } #endif -bool Component::has_overridden_loop() const { -#if defined(USE_HOST) || defined(CLANG_TIDY) - return true; -#else -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wpmf-conversions" - bool loop_overridden = (void *) (this->*(&Component::loop)) != (void *) (&Component::loop); -#pragma GCC diagnostic pop - return loop_overridden; -#endif -} - PollingComponent::PollingComponent(uint32_t update_interval) : update_interval_(update_interval) {} void PollingComponent::call_setup() { diff --git a/esphome/core/component.h b/esphome/core/component.h index 6b920da2901..d8102ea6708 100644 --- a/esphome/core/component.h +++ b/esphome/core/component.h @@ -76,6 +76,8 @@ inline constexpr uint8_t STATUS_LED_MASK = 0x18; inline constexpr uint8_t STATUS_LED_OK = 0x00; inline constexpr uint8_t STATUS_LED_WARNING = 0x08; inline constexpr uint8_t STATUS_LED_ERROR = 0x10; +// Component loop override flag uses bit 5 (set at registration time) +inline constexpr uint8_t COMPONENT_HAS_LOOP = 0x20; // Remove before 2026.8.0 enum class RetryResult { DONE, RETRY }; @@ -271,7 +273,7 @@ class Component { */ void status_momentary_error(const char *name, uint32_t length = 5000); - bool has_overridden_loop() const; + bool has_overridden_loop() const { return (this->component_state_ & COMPONENT_HAS_LOOP) != 0; } /** Set where this component was loaded from for some debug messages. * @@ -510,7 +512,8 @@ class Component { /// Bits 0-2: Component state (0x00=CONSTRUCTION, 0x01=SETUP, 0x02=LOOP, 0x03=FAILED, 0x04=LOOP_DONE) /// Bit 3: STATUS_LED_WARNING /// Bit 4: STATUS_LED_ERROR - /// Bits 5-7: Unused - reserved for future expansion + /// Bit 5: Has overridden loop() (set at registration time) + /// Bits 6-7: Unused - reserved for future expansion uint8_t component_state_{0x00}; volatile bool pending_enable_loop_{false}; ///< ISR-safe flag for enable_loop_soon_any_context }; diff --git a/esphome/core/config.py b/esphome/core/config.py index 593b4022303..3835fd3875f 100644 --- a/esphome/core/config.py +++ b/esphome/core/config.py @@ -1,5 +1,6 @@ from __future__ import annotations +from collections import Counter import logging import os from pathlib import Path @@ -504,6 +505,40 @@ async def _add_controller_registry_define() -> None: cg.add_define("CONTROLLER_REGISTRY_MAX", controller_count) +@coroutine_with_priority(CoroPriority.FINAL) +async def _add_looping_components() -> None: + # Emit a constexpr that computes the looping component count at C++ compile time + # and pre-init the FixedVector with the exact capacity. Uses std::is_same_v to + # detect loop() overrides. The constexpr goes in main.cpp's global section where + # all component types are in scope. calculate_looping_components_() then skips + # the counting pass and only does the two population passes. + entries = CORE.data.get("looping_component_entries", []) + if not entries: + return + + # Build constexpr sum for the exact count, deduplicating by type + type_counts = Counter(entries) + terms = [ + f"({count} * !std::is_same_v)" + for cpp_type, count in type_counts.items() + ] + constexpr_expr = " + \\\n ".join(terms) + cg.add_global( + cg.RawStatement( + f"static constexpr size_t ESPHOME_LOOPING_COMPONENT_COUNT = \\\n" + f" {constexpr_expr};" + ) + ) + + # Pre-init FixedVector with exact capacity so calculate_looping_components_() + # can skip the counting pass + cg.add( + cg.RawExpression( + "App.looping_components_.init(ESPHOME_LOOPING_COMPONENT_COUNT)" + ) + ) + + @coroutine_with_priority(CoroPriority.CORE) async def to_code(config: ConfigType) -> None: cg.add_global(cg.global_ns.namespace("esphome").using) @@ -527,6 +562,7 @@ async def to_code(config: ConfigType) -> None: CORE.add_job(_add_platform_defines) CORE.add_job(_add_controller_registry_define) + CORE.add_job(_add_looping_components) CORE.add_job(_add_automations, config) diff --git a/esphome/cpp_helpers.py b/esphome/cpp_helpers.py index b673eaa7e1f..8f8c6931403 100644 --- a/esphome/cpp_helpers.py +++ b/esphome/cpp_helpers.py @@ -80,6 +80,11 @@ async def register_component(var, config): add(var.set_component_source(LogStringLiteral(name))) add(App.register_component_(var)) + + # Collect C++ type for compile-time looping component count + comp_entries = CORE.data.setdefault("looping_component_entries", []) + comp_entries.append(str(var.base.type)) + return var diff --git a/tests/unit_tests/test_cpp_helpers.py b/tests/unit_tests/test_cpp_helpers.py index 82ded409c7c..5b6eed156fa 100644 --- a/tests/unit_tests/test_cpp_helpers.py +++ b/tests/unit_tests/test_cpp_helpers.py @@ -14,7 +14,11 @@ async def test_gpio_pin_expression__conf_is_none(monkeypatch): @pytest.mark.asyncio async def test_register_component(monkeypatch): - var = Mock(base="foo.bar") + base_mock = Mock() + base_mock.__str__ = lambda self: "foo.bar" + base_mock.type = Mock() + base_mock.type.__str__ = lambda self: "foo::Bar" + var = Mock(base=base_mock) app_mock = Mock(register_component_=Mock(return_value=var)) monkeypatch.setattr(ch, "App", app_mock) @@ -46,7 +50,11 @@ async def test_register_component__no_component_id(monkeypatch): @pytest.mark.asyncio async def test_register_component__with_setup_priority(monkeypatch): - var = Mock(base="foo.bar") + base_mock = Mock() + base_mock.__str__ = lambda self: "foo.bar" + base_mock.type = Mock() + base_mock.type.__str__ = lambda self: "foo::Bar" + var = Mock(base=base_mock) app_mock = Mock(register_component_=Mock(return_value=var)) monkeypatch.setattr(ch, "App", app_mock)