Merge remote-tracking branch 'upstream/component-8byte-optimization' into integration

This commit is contained in:
J. Nick Koston
2026-03-22 21:40:00 -10:00
4 changed files with 199 additions and 26 deletions
+16 -8
View File
@@ -87,6 +87,8 @@ void store_component_error_message(const Component *component, const char *messa
static constexpr uint16_t WARN_IF_BLOCKING_INCREMENT_MS =
10U; ///< How long the blocking time must be larger to warn again
// Threshold in ms (computed from centiseconds constant in component.h)
static constexpr uint32_t WARN_IF_BLOCKING_OVER_MS = static_cast<uint32_t>(WARN_IF_BLOCKING_OVER_CS) * 10U;
#ifdef USE_LOOP_PRIORITY
float Component::get_loop_priority() const { return 0.0f; }
@@ -285,15 +287,18 @@ void Component::call() {
break;
}
}
const LogString *Component::get_component_log_str() const {
return component_source_lookup(this->component_source_index_);
}
bool Component::should_warn_of_blocking(uint32_t blocking_time) {
if (blocking_time > this->warn_if_blocking_over_) {
// Prevent overflow when adding increment - if we're about to overflow, just max out
if (blocking_time + WARN_IF_BLOCKING_INCREMENT_MS < blocking_time ||
blocking_time + WARN_IF_BLOCKING_INCREMENT_MS > std::numeric_limits<uint16_t>::max()) {
this->warn_if_blocking_over_ = std::numeric_limits<uint16_t>::max();
} else {
this->warn_if_blocking_over_ = static_cast<uint16_t>(blocking_time + WARN_IF_BLOCKING_INCREMENT_MS);
}
// Convert centisecond threshold to milliseconds for comparison
uint32_t threshold_ms = static_cast<uint32_t>(this->warn_if_blocking_over_) * 10U;
if (blocking_time > threshold_ms) {
// Set new threshold: blocking_time + increment, converted back to centiseconds
uint32_t new_threshold_ms = blocking_time + WARN_IF_BLOCKING_INCREMENT_MS;
uint32_t new_cs = new_threshold_ms / 10U;
// Saturate at uint8_t max (255 = 2550ms)
this->warn_if_blocking_over_ = static_cast<uint8_t>(new_cs > 255U ? 255U : new_cs);
return true;
}
return false;
@@ -554,4 +559,7 @@ void clear_setup_priority_overrides() {
}
#endif
// Weak default for component_source_lookup - overridden by generated code
__attribute__((weak)) const LogString *component_source_lookup(uint8_t) { return LOG_STR("<unknown>"); }
} // namespace esphome
+24 -13
View File
@@ -11,6 +11,10 @@
#include "esphome/core/log.h"
#include "esphome/core/optional.h"
// Forward declarations for friend access from codegen-generated setup()
void setup(); // NOLINT(readability-redundant-declaration) - may be declared in Arduino.h
void original_setup(); // NOLINT(readability-redundant-declaration)
namespace esphome {
// Forward declaration for LogString
@@ -79,11 +83,14 @@ 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 };
inline constexpr uint16_t WARN_IF_BLOCKING_OVER_MS = 50U;
inline constexpr uint8_t WARN_IF_BLOCKING_OVER_CS = 5U; // 50ms in centiseconds (1cs = 10ms)
/// Lookup component source name by index (1-based). Generated by Python codegen.
/// Weak default returns "<unknown>" so builds without codegen still link.
const LogString *component_source_lookup(uint8_t index);
class Component {
public:
@@ -285,23 +292,25 @@ class Component {
bool has_overridden_loop() const { return (this->component_state_ & COMPONENT_HAS_LOOP) != 0; }
/** Set where this component was loaded from for some debug messages.
*
* This is set by the ESPHome core, and should not be called manually.
*/
void set_component_source(const LogString *source) { component_source_ = source; }
/** Get the integration where this component was declared as a LogString for logging.
*
* Returns LOG_STR("<unknown>") if source not set
*/
const LogString *get_component_log_str() const {
return this->component_source_ == nullptr ? LOG_STR("<unknown>") : this->component_source_;
}
const LogString *get_component_log_str() const;
bool should_warn_of_blocking(uint32_t blocking_time);
protected:
friend class Application;
friend void ::setup();
friend void ::original_setup();
/** Set where this component was loaded from for some debug messages.
*
* This is set by the ESPHome core during setup, and should not be called manually.
* @param index 1-based index into the component source lookup table (0 = not set)
*/
void set_component_source_(uint8_t index) { this->component_source_index_ = index; }
virtual void call_setup();
void call_dump_config_();
@@ -519,9 +528,9 @@ class Component {
void status_clear_warning_slow_path_();
void status_clear_error_slow_path_();
// Ordered for optimal packing on 32-bit systems
const LogString *component_source_{nullptr};
uint16_t warn_if_blocking_over_{WARN_IF_BLOCKING_OVER_MS}; ///< Warn if blocked for this many ms (max 65.5s)
// Ordered for optimal packing on 32-bit systems (8 bytes total with vtable)
uint8_t component_source_index_{0}; ///< Index into component source PROGMEM lookup table (0 = not set)
uint8_t warn_if_blocking_over_{WARN_IF_BLOCKING_OVER_CS}; ///< Warn threshold in centiseconds (max 2550ms)
/// State of this component - each bit has a purpose:
/// Bits 0-2: Component state (0x00=CONSTRUCTION, 0x01=SETUP, 0x02=LOOP, 0x03=FAILED, 0x04=LOOP_DONE)
/// Bit 3: STATUS_LED_WARNING
@@ -598,6 +607,8 @@ class WarnIfComponentBlockingGuard {
this->record_runtime_stats_();
#endif
#ifndef USE_BENCHMARK
// Fast path: compare against constant threshold in ms (computed at compile time from centiseconds)
static constexpr uint32_t WARN_IF_BLOCKING_OVER_MS = static_cast<uint32_t>(WARN_IF_BLOCKING_OVER_CS) * 10U;
if (blocking_time > WARN_IF_BLOCKING_OVER_MS) [[unlikely]] {
warn_blocking(this->component_, blocking_time);
}
+120 -3
View File
@@ -1,3 +1,4 @@
from dataclasses import dataclass, field
import logging
from esphome.const import (
@@ -7,15 +8,130 @@ from esphome.const import (
CONF_UPDATE_INTERVAL,
KEY_PAST_SAFE_MODE,
)
from esphome.core import CORE, ID, coroutine
from esphome.core import CORE, ID, CoroPriority, coroutine, coroutine_with_priority
from esphome.coroutine import FakeAwaitable
from esphome.cpp_generator import LogStringLiteral, add, add_define, get_variable
from esphome.cpp_generator import (
RawStatement,
add,
add_define,
add_global,
get_variable,
)
from esphome.cpp_types import App
from esphome.helpers import cpp_string_escape
from esphome.types import ConfigFragmentType, ConfigType
from esphome.util import Registry, RegistryEntry
_LOGGER = logging.getLogger(__name__)
_COMPONENT_SOURCE_DOMAIN = "component_source_pool"
# Maximum unique component source names (8-bit index, 0 = not set)
_MAX_COMPONENT_SOURCES = 0xFF # 255
@dataclass
class ComponentSourcePool:
"""Pool of component source names for PROGMEM lookup table.
Source names are registered during to_code() and assigned 1-based indices.
Index 0 means "not set" (returns LOG_STR("<unknown>")). At render time,
the pool generates a C++ PROGMEM table + lookup function.
"""
sources: dict[str, int] = field(default_factory=dict)
table_registered: bool = False
def _get_source_pool() -> ComponentSourcePool:
"""Get or create the component source pool from CORE.data."""
if _COMPONENT_SOURCE_DOMAIN not in CORE.data:
CORE.data[_COMPONENT_SOURCE_DOMAIN] = ComponentSourcePool()
return CORE.data[_COMPONENT_SOURCE_DOMAIN]
def _ensure_source_table_registered() -> None:
"""Schedule the table generation job (once)."""
pool = _get_source_pool()
if pool.table_registered:
return
pool.table_registered = True
CORE.add_job(_generate_component_source_table)
def register_component_source(name: str) -> int:
"""Register a component source name and return its 1-based index.
Deduplicates: multiple components from the same source share one index.
"""
if not name:
return 0
pool = _get_source_pool()
if name in pool.sources:
return pool.sources[name]
idx = len(pool.sources) + 1
if idx > _MAX_COMPONENT_SOURCES:
_LOGGER.warning(
"Too many unique component source names (max %d), '%s' will show as '<unknown>'",
_MAX_COMPONENT_SOURCES,
name,
)
return 0
pool.sources[name] = idx
_ensure_source_table_registered()
return idx
def _generate_source_table_code(
table_var: str,
lookup_fn: str,
strings: dict[str, int],
) -> str:
"""Generate C++ PROGMEM table + LogString* lookup for component sources.
Same pattern as entity_helpers._generate_category_code but returns
const LogString* instead of const char* (needed for LOG_STR_ARG).
"""
if not strings:
return ""
sorted_strings = sorted(strings.items(), key=lambda x: x[1])
count = len(sorted_strings)
# Emit individual PROGMEM char arrays so string data lives in flash on ESP8266
lines: list[str] = []
var_names: list[str] = []
for i, (s, _) in enumerate(sorted_strings):
var_name = f"{table_var}_STR_{i}"
var_names.append(var_name)
lines.append(
f"static const char {var_name}[] PROGMEM = {cpp_string_escape(s)};"
)
entries = ", ".join(var_names)
lines.append(f"static const char *const {table_var}[] PROGMEM = {{{entries}}};")
lines.append(f"const LogString *{lookup_fn}(uint8_t index) {{")
lines.append(f' if (index == 0 || index > {count}) return LOG_STR("<unknown>");')
lines.append(" return reinterpret_cast<const LogString *>(")
lines.append(f" progmem_read_ptr(&{table_var}[index - 1]));")
lines.append("}")
return "\n".join(lines) + "\n"
@coroutine_with_priority(CoroPriority.FINAL)
async def _generate_component_source_table() -> None:
"""Generate the component source lookup table as a FINAL-priority job.
Runs after all component to_code() calls have registered their sources.
"""
pool = _get_source_pool()
if code := _generate_source_table_code(
"COMP_SRC_TABLE", "component_source_lookup", pool.sources
):
add_global(
RawStatement(f"namespace esphome {{\n{code}}} // namespace esphome")
)
async def gpio_pin_expression(conf):
"""Generate an expression for the given pin option.
@@ -77,7 +193,8 @@ async def register_component(var, config):
"Error while finding name of component, please report this", exc_info=e
)
if name is not None:
add(var.set_component_source(LogStringLiteral(name)))
idx = register_component_source(name)
add(var.set_component_source_(idx))
add(App.register_component_(var))
+39 -2
View File
@@ -1,8 +1,10 @@
import logging
from unittest.mock import Mock
import pytest
from esphome import const, cpp_helpers as ch
from esphome.cpp_helpers import ComponentSourcePool, register_component_source
@pytest.mark.asyncio
@@ -23,7 +25,7 @@ async def test_register_component(monkeypatch):
app_mock = Mock(register_component_=Mock(return_value=var))
monkeypatch.setattr(ch, "App", app_mock)
core_mock = Mock(component_ids=["foo.bar"])
core_mock = Mock(component_ids=["foo.bar"], data={})
monkeypatch.setattr(ch, "CORE", core_mock)
add_mock = Mock()
@@ -59,7 +61,7 @@ async def test_register_component__with_setup_priority(monkeypatch):
app_mock = Mock(register_component_=Mock(return_value=var))
monkeypatch.setattr(ch, "App", app_mock)
core_mock = Mock(component_ids=["foo.bar"])
core_mock = Mock(component_ids=["foo.bar"], data={})
monkeypatch.setattr(ch, "CORE", core_mock)
add_mock = Mock()
@@ -78,3 +80,38 @@ async def test_register_component__with_setup_priority(monkeypatch):
assert add_mock.call_count == 4
app_mock.register_component_.assert_called_with(var)
assert core_mock.component_ids == []
def test_register_component_source_empty_name(monkeypatch):
monkeypatch.setattr(ch, "CORE", Mock(data={}))
assert register_component_source("") == 0
def test_register_component_source_deduplicates(monkeypatch):
monkeypatch.setattr(ch, "CORE", Mock(data={}))
idx1 = register_component_source("wifi")
idx2 = register_component_source("api")
idx3 = register_component_source("wifi")
assert idx1 == 1
assert idx2 == 2
assert idx3 == 1 # deduplicated
def test_generate_source_table_code_empty():
from esphome.cpp_helpers import _generate_source_table_code
assert _generate_source_table_code("TBL", "lookup", {}) == ""
def test_register_component_source_overflow_warns(monkeypatch, caplog):
# Pre-fill pool to max
pool = ComponentSourcePool(
sources={f"comp_{i}": i + 1 for i in range(0xFF)},
table_registered=True,
)
monkeypatch.setattr(ch, "CORE", Mock(data={ch._COMPONENT_SOURCE_DOMAIN: pool}))
with caplog.at_level(logging.WARNING):
idx = register_component_source("overflow_component")
assert idx == 0
assert "Too many unique component source names" in caplog.text
assert "overflow_component" in caplog.text