Optimize scheduler timing by reducing millis() calls (#9524)

This commit is contained in:
J. Nick Koston 2025-07-15 15:41:55 -10:00 committed by GitHub
parent b695f13f86
commit b1c86fe30e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 31 additions and 35 deletions

View File

@ -71,7 +71,7 @@ void Application::setup() {
do { do {
uint8_t new_app_state = STATUS_LED_WARNING; uint8_t new_app_state = STATUS_LED_WARNING;
this->scheduler.call(); this->scheduler.call(millis());
this->feed_wdt(); this->feed_wdt();
for (uint32_t j = 0; j <= i; j++) { for (uint32_t j = 0; j <= i; j++) {
// Update loop_component_start_time_ right before calling each component // Update loop_component_start_time_ right before calling each component
@ -97,11 +97,11 @@ void Application::setup() {
void Application::loop() { void Application::loop() {
uint8_t new_app_state = 0; uint8_t new_app_state = 0;
this->scheduler.call();
// Get the initial loop time at the start // Get the initial loop time at the start
uint32_t last_op_end_time = millis(); uint32_t last_op_end_time = millis();
this->scheduler.call(last_op_end_time);
// Feed WDT with time // Feed WDT with time
this->feed_wdt(last_op_end_time); this->feed_wdt(last_op_end_time);
@ -160,7 +160,7 @@ void Application::loop() {
this->yield_with_select_(0); this->yield_with_select_(0);
} else { } else {
uint32_t delay_time = this->loop_interval_ - elapsed; uint32_t delay_time = this->loop_interval_ - elapsed;
uint32_t next_schedule = this->scheduler.next_schedule_in().value_or(delay_time); uint32_t next_schedule = this->scheduler.next_schedule_in(last_op_end_time).value_or(delay_time);
// next_schedule is max 0.5*delay_time // next_schedule is max 0.5*delay_time
// otherwise interval=0 schedules result in constant looping with almost no sleep // otherwise interval=0 schedules result in constant looping with almost no sleep
next_schedule = std::max(next_schedule, delay_time / 2); next_schedule = std::max(next_schedule, delay_time / 2);

View File

@ -91,7 +91,7 @@ void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type
} }
#endif #endif
const auto now = this->millis_(); const auto now = this->millis_64_(millis());
// Type-specific setup // Type-specific setup
if (type == SchedulerItem::INTERVAL) { if (type == SchedulerItem::INTERVAL) {
@ -193,9 +193,7 @@ void HOT Scheduler::set_retry(Component *component, const std::string &name, uin
name.c_str(), initial_wait_time, max_attempts, backoff_increase_factor); name.c_str(), initial_wait_time, max_attempts, backoff_increase_factor);
if (backoff_increase_factor < 0.0001) { if (backoff_increase_factor < 0.0001) {
ESP_LOGE(TAG, ESP_LOGE(TAG, "backoff_factor %0.1f too small, using 1.0: %s", backoff_increase_factor, name.c_str());
"set_retry(name='%s'): backoff_factor cannot be close to zero nor negative (%0.1f). Using 1.0 instead",
name.c_str(), backoff_increase_factor);
backoff_increase_factor = 1; backoff_increase_factor = 1;
} }
@ -215,19 +213,19 @@ bool HOT Scheduler::cancel_retry(Component *component, const std::string &name)
return this->cancel_timeout(component, "retry$" + name); return this->cancel_timeout(component, "retry$" + name);
} }
optional<uint32_t> HOT Scheduler::next_schedule_in() { optional<uint32_t> HOT Scheduler::next_schedule_in(uint32_t now) {
// IMPORTANT: This method should only be called from the main thread (loop task). // IMPORTANT: This method should only be called from the main thread (loop task).
// It calls empty_() and accesses items_[0] without holding a lock, which is only // It calls empty_() and accesses items_[0] without holding a lock, which is only
// safe when called from the main thread. Other threads must not call this method. // safe when called from the main thread. Other threads must not call this method.
if (this->empty_()) if (this->empty_())
return {}; return {};
auto &item = this->items_[0]; auto &item = this->items_[0];
const auto now = this->millis_(); const auto now_64 = this->millis_64_(now);
if (item->next_execution_ < now) if (item->next_execution_ < now_64)
return 0; return 0;
return item->next_execution_ - now; return item->next_execution_ - now_64;
} }
void HOT Scheduler::call() { void HOT Scheduler::call(uint32_t now) {
#if !defined(USE_ESP8266) && !defined(USE_RP2040) #if !defined(USE_ESP8266) && !defined(USE_RP2040)
// Process defer queue first to guarantee FIFO execution order for deferred items. // Process defer queue first to guarantee FIFO execution order for deferred items.
// Previously, defer() used the heap which gave undefined order for equal timestamps, // Previously, defer() used the heap which gave undefined order for equal timestamps,
@ -256,22 +254,22 @@ void HOT Scheduler::call() {
// Execute callback without holding lock to prevent deadlocks // Execute callback without holding lock to prevent deadlocks
// if the callback tries to call defer() again // if the callback tries to call defer() again
if (!this->should_skip_item_(item.get())) { if (!this->should_skip_item_(item.get())) {
this->execute_item_(item.get()); this->execute_item_(item.get(), now);
} }
} }
#endif #endif
const auto now = this->millis_(); const auto now_64 = this->millis_64_(now);
this->process_to_add(); this->process_to_add();
#ifdef ESPHOME_DEBUG_SCHEDULER #ifdef ESPHOME_DEBUG_SCHEDULER
static uint64_t last_print = 0; static uint64_t last_print = 0;
if (now - last_print > 2000) { if (now_64 - last_print > 2000) {
last_print = now; last_print = now_64;
std::vector<std::unique_ptr<SchedulerItem>> old_items; std::vector<std::unique_ptr<SchedulerItem>> old_items;
ESP_LOGD(TAG, "Items: count=%zu, now=%" PRIu64 " (%u, %" PRIu32 ")", this->items_.size(), now, this->millis_major_, ESP_LOGD(TAG, "Items: count=%zu, now=%" PRIu64 " (%u, %" PRIu32 ")", this->items_.size(), now_64,
this->last_millis_); this->millis_major_, this->last_millis_);
while (!this->empty_()) { while (!this->empty_()) {
std::unique_ptr<SchedulerItem> item; std::unique_ptr<SchedulerItem> item;
{ {
@ -283,7 +281,7 @@ void HOT Scheduler::call() {
const char *name = item->get_name(); const char *name = item->get_name();
ESP_LOGD(TAG, " %s '%s/%s' interval=%" PRIu32 " next_execution in %" PRIu64 "ms at %" PRIu64, ESP_LOGD(TAG, " %s '%s/%s' interval=%" PRIu32 " next_execution in %" PRIu64 "ms at %" PRIu64,
item->get_type_str(), item->get_source(), name ? name : "(null)", item->interval, item->get_type_str(), item->get_source(), name ? name : "(null)", item->interval,
item->next_execution_ - now, item->next_execution_); item->next_execution_ - now_64, item->next_execution_);
old_items.push_back(std::move(item)); old_items.push_back(std::move(item));
} }
@ -328,7 +326,7 @@ void HOT Scheduler::call() {
{ {
// Don't copy-by value yet // Don't copy-by value yet
auto &item = this->items_[0]; auto &item = this->items_[0];
if (item->next_execution_ > now) { if (item->next_execution_ > now_64) {
// Not reached timeout yet, done for this call // Not reached timeout yet, done for this call
break; break;
} }
@ -342,13 +340,13 @@ void HOT Scheduler::call() {
const char *item_name = item->get_name(); const char *item_name = item->get_name();
ESP_LOGV(TAG, "Running %s '%s/%s' with interval=%" PRIu32 " next_execution=%" PRIu64 " (now=%" PRIu64 ")", ESP_LOGV(TAG, "Running %s '%s/%s' with interval=%" PRIu32 " next_execution=%" PRIu64 " (now=%" PRIu64 ")",
item->get_type_str(), item->get_source(), item_name ? item_name : "(null)", item->interval, item->get_type_str(), item->get_source(), item_name ? item_name : "(null)", item->interval,
item->next_execution_, now); item->next_execution_, now_64);
#endif #endif
// Warning: During callback(), a lot of stuff can happen, including: // Warning: During callback(), a lot of stuff can happen, including:
// - timeouts/intervals get added, potentially invalidating vector pointers // - timeouts/intervals get added, potentially invalidating vector pointers
// - timeouts/intervals get cancelled // - timeouts/intervals get cancelled
this->execute_item_(item.get()); this->execute_item_(item.get(), now);
} }
{ {
@ -367,7 +365,7 @@ void HOT Scheduler::call() {
} }
if (item->type == SchedulerItem::INTERVAL) { if (item->type == SchedulerItem::INTERVAL) {
item->next_execution_ = now + item->interval; item->next_execution_ = now_64 + item->interval;
// Add new item directly to to_add_ // Add new item directly to to_add_
// since we have the lock held // since we have the lock held
this->to_add_.push_back(std::move(item)); this->to_add_.push_back(std::move(item));
@ -423,11 +421,9 @@ void HOT Scheduler::pop_raw_() {
} }
// Helper to execute a scheduler item // Helper to execute a scheduler item
void HOT Scheduler::execute_item_(SchedulerItem *item) { void HOT Scheduler::execute_item_(SchedulerItem *item, uint32_t now) {
App.set_current_component(item->component); App.set_current_component(item->component);
WarnIfComponentBlockingGuard guard{item->component, now};
uint32_t now_ms = millis();
WarnIfComponentBlockingGuard guard{item->component, now_ms};
item->callback(); item->callback();
guard.finish(); guard.finish();
} }
@ -486,15 +482,15 @@ bool HOT Scheduler::cancel_item_locked_(Component *component, const char *name_c
return total_cancelled > 0; return total_cancelled > 0;
} }
uint64_t Scheduler::millis_() { uint64_t Scheduler::millis_64_(uint32_t now) {
// Get the current 32-bit millis value
const uint32_t now = millis();
// Check for rollover by comparing with last value // Check for rollover by comparing with last value
if (now < this->last_millis_) { if (now < this->last_millis_) {
// Detected rollover (happens every ~49.7 days) // Detected rollover (happens every ~49.7 days)
this->millis_major_++; this->millis_major_++;
#ifdef ESPHOME_DEBUG_SCHEDULER
ESP_LOGD(TAG, "Incrementing scheduler major at %" PRIu64 "ms", ESP_LOGD(TAG, "Incrementing scheduler major at %" PRIu64 "ms",
now + (static_cast<uint64_t>(this->millis_major_) << 32)); now + (static_cast<uint64_t>(this->millis_major_) << 32));
#endif
} }
this->last_millis_ = now; this->last_millis_ = now;
// Combine major (high 32 bits) and now (low 32 bits) into 64-bit time // Combine major (high 32 bits) and now (low 32 bits) into 64-bit time

View File

@ -52,9 +52,9 @@ class Scheduler {
std::function<RetryResult(uint8_t)> func, float backoff_increase_factor = 1.0f); std::function<RetryResult(uint8_t)> func, float backoff_increase_factor = 1.0f);
bool cancel_retry(Component *component, const std::string &name); bool cancel_retry(Component *component, const std::string &name);
optional<uint32_t> next_schedule_in(); optional<uint32_t> next_schedule_in(uint32_t now);
void call(); void call(uint32_t now);
void process_to_add(); void process_to_add();
@ -137,7 +137,7 @@ class Scheduler {
void set_timer_common_(Component *component, SchedulerItem::Type type, bool is_static_string, const void *name_ptr, void set_timer_common_(Component *component, SchedulerItem::Type type, bool is_static_string, const void *name_ptr,
uint32_t delay, std::function<void()> func); uint32_t delay, std::function<void()> func);
uint64_t millis_(); uint64_t millis_64_(uint32_t now);
void cleanup_(); void cleanup_();
void pop_raw_(); void pop_raw_();
@ -175,7 +175,7 @@ class Scheduler {
} }
// Helper to execute a scheduler item // Helper to execute a scheduler item
void execute_item_(SchedulerItem *item); void execute_item_(SchedulerItem *item, uint32_t now);
// Helper to check if item should be skipped // Helper to check if item should be skipped
bool should_skip_item_(const SchedulerItem *item) const { bool should_skip_item_(const SchedulerItem *item) const {