[core] Pre-compute looping component count at compile time via codegen

Emit a static constexpr in main.cpp that uses std::is_same_v to count
components with overridden loop() at C++ compile time. Pre-init the
FixedVector with the exact capacity so calculate_looping_components_()
can skip its counting pass entirely.
This commit is contained in:
J. Nick Koston
2026-03-01 11:54:47 -10:00
parent 9621c5a3d3
commit 5f606a4154
3 changed files with 41 additions and 10 deletions
+2 -10
View File
@@ -387,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
+34
View File
@@ -504,6 +504,39 @@ 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
terms = [
f"(!std::is_same_v<decltype(&{cpp_type}::loop), decltype(&Component::loop)>)"
for cpp_type in entries
]
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 +560,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)
+5
View File
@@ -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