mirror of
https://github.com/esphome/esphome.git
synced 2026-09-16 01:28:39 +00:00
[core] Shrink Component from 12 to 8 bytes per instance
Replace 4-byte component_source_ pointer with a 9-bit index into a PROGMEM lookup table generated by Python codegen. Source names are deduplicated at codegen time (same pattern as EntityStringPool). Shrink warn_if_blocking_over_ from uint16_t (ms) to uint8_t (centiseconds), saturating at 255 (2550ms). Saves 4 bytes per component instance (typical configs: 120-200 bytes).
This commit is contained in:
@@ -84,6 +84,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; }
|
||||
@@ -273,14 +275,14 @@ void Component::call() {
|
||||
}
|
||||
}
|
||||
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;
|
||||
@@ -541,4 +543,7 @@ void clear_setup_priority_overrides() {
|
||||
}
|
||||
#endif
|
||||
|
||||
// Weak default for component_source_lookup - overridden by generated code
|
||||
__attribute__((weak)) const LogString *component_source_lookup(uint16_t) { return LOG_STR("<unknown>"); }
|
||||
|
||||
} // namespace esphome
|
||||
|
||||
+38
-12
@@ -79,11 +79,19 @@ 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;
|
||||
// High bit of 9-bit component_source index (bit 6 of component_state_)
|
||||
inline constexpr uint8_t COMPONENT_SOURCE_HIGH_BIT = 0x40;
|
||||
// Mask for the 9th bit of the component source index (bit 8)
|
||||
inline constexpr uint16_t COMPONENT_SOURCE_INDEX_HIGH = 0x100;
|
||||
|
||||
// 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(uint16_t index);
|
||||
|
||||
class Component {
|
||||
public:
|
||||
@@ -143,7 +151,10 @@ class Component {
|
||||
*/
|
||||
virtual void on_powerdown() {}
|
||||
|
||||
uint8_t get_component_state() const { return this->component_state_; }
|
||||
uint8_t get_component_state() const {
|
||||
// Mask out COMPONENT_SOURCE_HIGH_BIT — it's internal to component source indexing
|
||||
return this->component_state_ & ~COMPONENT_SOURCE_HIGH_BIT;
|
||||
}
|
||||
|
||||
/** Reset this component back to the construction state to allow setup to run again.
|
||||
*
|
||||
@@ -285,17 +296,15 @@ 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_;
|
||||
uint16_t idx = this->component_source_index_;
|
||||
if (this->component_state_ & COMPONENT_SOURCE_HIGH_BIT)
|
||||
idx |= COMPONENT_SOURCE_INDEX_HIGH;
|
||||
return idx == 0 ? LOG_STR("<unknown>") : component_source_lookup(idx);
|
||||
}
|
||||
|
||||
bool should_warn_of_blocking(uint32_t blocking_time);
|
||||
@@ -303,6 +312,20 @@ class Component {
|
||||
protected:
|
||||
friend class Application;
|
||||
|
||||
/** 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_(uint16_t index) {
|
||||
this->component_source_index_ = static_cast<uint8_t>(index & 0xFF);
|
||||
if (index & COMPONENT_SOURCE_INDEX_HIGH) {
|
||||
this->component_state_ |= COMPONENT_SOURCE_HIGH_BIT;
|
||||
} else {
|
||||
this->component_state_ &= ~COMPONENT_SOURCE_HIGH_BIT;
|
||||
}
|
||||
}
|
||||
|
||||
virtual void call_setup();
|
||||
void call_dump_config_();
|
||||
|
||||
@@ -519,15 +542,16 @@ 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}; ///< Lower 8 bits of 9-bit component source index
|
||||
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
|
||||
/// Bit 4: STATUS_LED_ERROR
|
||||
/// Bit 5: Has overridden loop() (set at registration time)
|
||||
/// Bits 6-7: Unused - reserved for future expansion
|
||||
/// Bit 6: High bit of 9-bit component_source index
|
||||
/// Bit 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
|
||||
};
|
||||
@@ -598,6 +622,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);
|
||||
}
|
||||
|
||||
+118
-3
@@ -1,3 +1,4 @@
|
||||
from dataclasses import dataclass, field
|
||||
import logging
|
||||
|
||||
from esphome.const import (
|
||||
@@ -7,15 +8,128 @@ 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 (9-bit index, 0 = not set)
|
||||
_MAX_COMPONENT_SOURCES = 510
|
||||
|
||||
|
||||
@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:
|
||||
raise ValueError(
|
||||
f"Too many unique component source names (max {_MAX_COMPONENT_SOURCES}), got {idx}: '{name}'"
|
||||
)
|
||||
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}(uint16_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()
|
||||
code = _generate_source_table_code(
|
||||
"COMP_SRC_TABLE", "component_source_lookup", pool.sources
|
||||
)
|
||||
if code:
|
||||
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 +191,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))
|
||||
|
||||
|
||||
Reference in New Issue
Block a user