mirror of
https://github.com/esphome/esphome.git
synced 2026-09-01 10:36:01 +00:00
Merge remote-tracking branch 'upstream/component-8byte-optimization' into integration
This commit is contained in:
@@ -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
@@ -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
@@ -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))
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user