1
0
mirror of https://github.com/esphome/esphome.git synced 2026-02-10 01:32:06 +00:00

Compare commits

...

1 Commits

Author SHA1 Message Date
J. Nick Koston
c1328f1b3a [scheduler] Reduce set_timer_common_ hot path size by 25%
Restructure set_timer_common_ to reduce icache pressure on the hot path
(538 → 405 bytes on ESP32):

- Extract calculate_interval_offset_ as noinline helper - float math and
  random_float() only needed for intervals, not timeouts
- Extract is_retry_cancelled_locked_ as noinline helper - retry path is
  cold and deprecated (removal planned for 2026.8.0)
- Merge duplicated cancel+push_back epilogue by computing a target vector
  pointer (defer_queue_ vs to_add_) before the branch, converging both
  paths at a single cancel+push sequence
- Replace 4-way switch on name_type with unified set_name() method that
  does a single branch + store instead of 4 separate bitfield RMW
  sequences
- Remove now-unused individual name setters (set_static_name,
  set_hashed_name, set_numeric_id, set_internal_id)
2026-02-09 19:24:15 -06:00
2 changed files with 73 additions and 75 deletions

View File

@@ -107,6 +107,24 @@ static void validate_static_string(const char *name) {
// iterating over them from the loop task is fine; but iterating from any other context requires the lock to be held to
// avoid the main thread modifying the list while it is being accessed.
// Calculate random offset for interval timers
// Extracted from set_timer_common_ to reduce code size - float math + random_float()
// only needed for intervals, not timeouts
uint32_t Scheduler::calculate_interval_offset_(uint32_t delay) {
return static_cast<uint32_t>(std::min(delay / 2, MAX_INTERVAL_DELAY) * random_float());
}
// Check if a retry was already cancelled in items_ or to_add_
// Extracted from set_timer_common_ to reduce code size - retry path is cold and deprecated
// Remove before 2026.8.0 along with all retry code
bool Scheduler::is_retry_cancelled_locked_(Component *component, NameType name_type, const char *static_name,
uint32_t hash_or_id) {
return has_cancelled_timeout_in_container_locked_(this->items_, component, name_type, static_name, hash_or_id,
/* match_retry= */ true) ||
has_cancelled_timeout_in_container_locked_(this->to_add_, component, name_type, static_name, hash_or_id,
/* match_retry= */ true);
}
// Common implementation for both timeout and interval
// name_type determines storage type: STATIC_STRING uses static_name, others use hash_or_id
void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type type, NameType name_type,
@@ -130,84 +148,66 @@ void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type
// Create and populate the scheduler item
auto item = this->get_item_from_pool_locked_();
item->component = component;
switch (name_type) {
case NameType::STATIC_STRING:
item->set_static_name(static_name);
break;
case NameType::HASHED_STRING:
item->set_hashed_name(hash_or_id);
break;
case NameType::NUMERIC_ID:
item->set_numeric_id(hash_or_id);
break;
case NameType::NUMERIC_ID_INTERNAL:
item->set_internal_id(hash_or_id);
break;
}
item->set_name(name_type, static_name, hash_or_id);
item->type = type;
item->callback = std::move(func);
// Reset remove flag - recycled items may have been cancelled (remove=true) in previous use
this->set_item_removed_(item.get(), false);
item->is_retry = is_retry;
// Determine target container: defer_queue_ for deferred items, to_add_ for everything else.
// Using a pointer lets both paths share the cancel + push_back epilogue.
auto *target = &this->to_add_;
#ifndef ESPHOME_THREAD_SINGLE
// Special handling for defer() (delay = 0, type = TIMEOUT)
// Single-core platforms don't need thread-safe defer handling
if (delay == 0 && type == SchedulerItem::TIMEOUT) {
// Put in defer queue for guaranteed FIFO execution
if (!skip_cancel) {
this->cancel_item_locked_(component, name_type, static_name, hash_or_id, type);
}
this->defer_queue_.push_back(std::move(item));
return;
}
target = &this->defer_queue_;
} else
#endif /* not ESPHOME_THREAD_SINGLE */
// Type-specific setup
if (type == SchedulerItem::INTERVAL) {
item->interval = delay;
// first execution happens immediately after a random smallish offset
// Calculate random offset (0 to min(interval/2, 5s))
uint32_t offset = (uint32_t) (std::min(delay / 2, MAX_INTERVAL_DELAY) * random_float());
item->set_next_execution(now + offset);
{
// Type-specific setup
if (type == SchedulerItem::INTERVAL) {
item->interval = delay;
// first execution happens immediately after a random smallish offset
uint32_t offset = this->calculate_interval_offset_(delay);
item->set_next_execution(now + offset);
#ifdef ESPHOME_LOG_HAS_VERBOSE
SchedulerNameLog name_log;
ESP_LOGV(TAG, "Scheduler interval for %s is %" PRIu32 "ms, offset %" PRIu32 "ms",
name_log.format(name_type, static_name, hash_or_id), delay, offset);
SchedulerNameLog name_log;
ESP_LOGV(TAG, "Scheduler interval for %s is %" PRIu32 "ms, offset %" PRIu32 "ms",
name_log.format(name_type, static_name, hash_or_id), delay, offset);
#endif
} else {
item->interval = 0;
item->set_next_execution(now + delay);
}
} else {
item->interval = 0;
item->set_next_execution(now + delay);
}
#ifdef ESPHOME_DEBUG_SCHEDULER
this->debug_log_timer_(item.get(), name_type, static_name, hash_or_id, type, delay, now);
this->debug_log_timer_(item.get(), name_type, static_name, hash_or_id, type, delay, now);
#endif /* ESPHOME_DEBUG_SCHEDULER */
// For retries, check if there's a cancelled timeout first
// Skip check for anonymous retries (STATIC_STRING with nullptr) - they can't be cancelled by name
if (is_retry && (name_type != NameType::STATIC_STRING || static_name != nullptr) && type == SchedulerItem::TIMEOUT &&
(has_cancelled_timeout_in_container_locked_(this->items_, component, name_type, static_name, hash_or_id,
/* match_retry= */ true) ||
has_cancelled_timeout_in_container_locked_(this->to_add_, component, name_type, static_name, hash_or_id,
/* match_retry= */ true))) {
// Skip scheduling - the retry was cancelled
// For retries, check if there's a cancelled timeout first
// Skip check for anonymous retries (STATIC_STRING with nullptr) - they can't be cancelled by name
if (is_retry && (name_type != NameType::STATIC_STRING || static_name != nullptr) &&
type == SchedulerItem::TIMEOUT &&
this->is_retry_cancelled_locked_(component, name_type, static_name, hash_or_id)) {
// Skip scheduling - the retry was cancelled
#ifdef ESPHOME_DEBUG_SCHEDULER
SchedulerNameLog skip_name_log;
ESP_LOGD(TAG, "Skipping retry '%s' - found cancelled item",
skip_name_log.format(name_type, static_name, hash_or_id));
SchedulerNameLog skip_name_log;
ESP_LOGD(TAG, "Skipping retry '%s' - found cancelled item",
skip_name_log.format(name_type, static_name, hash_or_id));
#endif
return;
return;
}
}
// If name is provided, do atomic cancel-and-add (unless skip_cancel is true)
// Cancel existing items
// Common epilogue: atomic cancel-and-add (unless skip_cancel is true)
if (!skip_cancel) {
this->cancel_item_locked_(component, name_type, static_name, hash_or_id, type);
}
// Add new item directly to to_add_
// since we have the lock held
this->to_add_.push_back(std::move(item));
target->push_back(std::move(item));
}
void HOT Scheduler::set_timeout(Component *component, const char *name, uint32_t timeout, std::function<void()> func) {

View File

@@ -219,28 +219,15 @@ class Scheduler {
// Helper to get the name type
NameType get_name_type() const { return name_type_; }
// Helper to set a static string name (no allocation)
void set_static_name(const char *name) {
name_.static_name = name;
name_type_ = NameType::STATIC_STRING;
}
// Helper to set a hashed string name (hash computed from std::string)
void set_hashed_name(uint32_t hash) {
name_.hash_or_id = hash;
name_type_ = NameType::HASHED_STRING;
}
// Helper to set a numeric ID name
void set_numeric_id(uint32_t id) {
name_.hash_or_id = id;
name_type_ = NameType::NUMERIC_ID;
}
// Helper to set an internal numeric ID (separate namespace from NUMERIC_ID)
void set_internal_id(uint32_t id) {
name_.hash_or_id = id;
name_type_ = NameType::NUMERIC_ID_INTERNAL;
// Set name storage: for STATIC_STRING stores the pointer, for all other types stores hash_or_id.
// Both union members occupy the same offset, so only one store is needed.
void set_name(NameType type, const char *static_name, uint32_t hash_or_id) {
if (type == NameType::STATIC_STRING) {
name_.static_name = static_name;
} else {
name_.hash_or_id = hash_or_id;
}
name_type_ = type;
}
static bool cmp(const std::unique_ptr<SchedulerItem> &a, const std::unique_ptr<SchedulerItem> &b);
@@ -355,6 +342,17 @@ class Scheduler {
// Helper to perform full cleanup when too many items are cancelled
void full_cleanup_removed_items_();
// Helper to calculate random offset for interval timers - extracted to reduce code size of set_timer_common_
// IMPORTANT: Must not be inlined - called only for intervals, keeping it out of the hot path saves flash.
uint32_t __attribute__((noinline)) calculate_interval_offset_(uint32_t delay);
// Helper to check if a retry was already cancelled - extracted to reduce code size of set_timer_common_
// Remove before 2026.8.0 along with all retry code.
// IMPORTANT: Must not be inlined - retry path is cold and deprecated.
// IMPORTANT: Caller must hold the scheduler lock before calling this function.
bool __attribute__((noinline))
is_retry_cancelled_locked_(Component *component, NameType name_type, const char *static_name, uint32_t hash_or_id);
#ifdef ESPHOME_DEBUG_SCHEDULER
// Helper for debug logging in set_timer_common_ - extracted to reduce code size
void debug_log_timer_(const SchedulerItem *item, NameType name_type, const char *static_name, uint32_t hash_or_id,