From b1c86fe30ed10f0e1af85195ebc5b2775a97bed8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 15 Jul 2025 15:41:55 -1000 Subject: [PATCH] Optimize scheduler timing by reducing millis() calls (#9524) --- esphome/core/application.cpp | 8 +++--- esphome/core/scheduler.cpp | 50 +++++++++++++++++------------------- esphome/core/scheduler.h | 8 +++--- 3 files changed, 31 insertions(+), 35 deletions(-) diff --git a/esphome/core/application.cpp b/esphome/core/application.cpp index 123d6d01f4..748c8f2237 100644 --- a/esphome/core/application.cpp +++ b/esphome/core/application.cpp @@ -71,7 +71,7 @@ void Application::setup() { do { uint8_t new_app_state = STATUS_LED_WARNING; - this->scheduler.call(); + this->scheduler.call(millis()); this->feed_wdt(); for (uint32_t j = 0; j <= i; j++) { // Update loop_component_start_time_ right before calling each component @@ -97,11 +97,11 @@ void Application::setup() { void Application::loop() { uint8_t new_app_state = 0; - this->scheduler.call(); - // Get the initial loop time at the start uint32_t last_op_end_time = millis(); + this->scheduler.call(last_op_end_time); + // Feed WDT with time this->feed_wdt(last_op_end_time); @@ -160,7 +160,7 @@ void Application::loop() { this->yield_with_select_(0); } else { 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 // otherwise interval=0 schedules result in constant looping with almost no sleep next_schedule = std::max(next_schedule, delay_time / 2); diff --git a/esphome/core/scheduler.cpp b/esphome/core/scheduler.cpp index c6893b128f..1c37a1617d 100644 --- a/esphome/core/scheduler.cpp +++ b/esphome/core/scheduler.cpp @@ -91,7 +91,7 @@ void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type } #endif - const auto now = this->millis_(); + const auto now = this->millis_64_(millis()); // Type-specific setup 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); if (backoff_increase_factor < 0.0001) { - ESP_LOGE(TAG, - "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); + ESP_LOGE(TAG, "backoff_factor %0.1f too small, using 1.0: %s", backoff_increase_factor, name.c_str()); 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); } -optional HOT Scheduler::next_schedule_in() { +optional HOT Scheduler::next_schedule_in(uint32_t now) { // 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 // safe when called from the main thread. Other threads must not call this method. if (this->empty_()) return {}; auto &item = this->items_[0]; - const auto now = this->millis_(); - if (item->next_execution_ < now) + const auto now_64 = this->millis_64_(now); + if (item->next_execution_ < now_64) 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) // Process defer queue first to guarantee FIFO execution order for deferred items. // 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 // if the callback tries to call defer() again if (!this->should_skip_item_(item.get())) { - this->execute_item_(item.get()); + this->execute_item_(item.get(), now); } } #endif - const auto now = this->millis_(); + const auto now_64 = this->millis_64_(now); this->process_to_add(); #ifdef ESPHOME_DEBUG_SCHEDULER static uint64_t last_print = 0; - if (now - last_print > 2000) { - last_print = now; + if (now_64 - last_print > 2000) { + last_print = now_64; std::vector> old_items; - ESP_LOGD(TAG, "Items: count=%zu, now=%" PRIu64 " (%u, %" PRIu32 ")", this->items_.size(), now, this->millis_major_, - this->last_millis_); + ESP_LOGD(TAG, "Items: count=%zu, now=%" PRIu64 " (%u, %" PRIu32 ")", this->items_.size(), now_64, + this->millis_major_, this->last_millis_); while (!this->empty_()) { std::unique_ptr item; { @@ -283,7 +281,7 @@ void HOT Scheduler::call() { const char *name = item->get_name(); 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->next_execution_ - now, item->next_execution_); + item->next_execution_ - now_64, item->next_execution_); old_items.push_back(std::move(item)); } @@ -328,7 +326,7 @@ void HOT Scheduler::call() { { // Don't copy-by value yet auto &item = this->items_[0]; - if (item->next_execution_ > now) { + if (item->next_execution_ > now_64) { // Not reached timeout yet, done for this call break; } @@ -342,13 +340,13 @@ void HOT Scheduler::call() { const char *item_name = item->get_name(); 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->next_execution_, now); + item->next_execution_, now_64); #endif // Warning: During callback(), a lot of stuff can happen, including: // - timeouts/intervals get added, potentially invalidating vector pointers // - 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) { - item->next_execution_ = now + item->interval; + item->next_execution_ = now_64 + item->interval; // Add new item directly to to_add_ // since we have the lock held this->to_add_.push_back(std::move(item)); @@ -423,11 +421,9 @@ void HOT Scheduler::pop_raw_() { } // 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); - - uint32_t now_ms = millis(); - WarnIfComponentBlockingGuard guard{item->component, now_ms}; + WarnIfComponentBlockingGuard guard{item->component, now}; item->callback(); guard.finish(); } @@ -486,15 +482,15 @@ bool HOT Scheduler::cancel_item_locked_(Component *component, const char *name_c return total_cancelled > 0; } -uint64_t Scheduler::millis_() { - // Get the current 32-bit millis value - const uint32_t now = millis(); +uint64_t Scheduler::millis_64_(uint32_t now) { // Check for rollover by comparing with last value if (now < this->last_millis_) { // Detected rollover (happens every ~49.7 days) this->millis_major_++; +#ifdef ESPHOME_DEBUG_SCHEDULER ESP_LOGD(TAG, "Incrementing scheduler major at %" PRIu64 "ms", now + (static_cast(this->millis_major_) << 32)); +#endif } this->last_millis_ = now; // Combine major (high 32 bits) and now (low 32 bits) into 64-bit time diff --git a/esphome/core/scheduler.h b/esphome/core/scheduler.h index 39cee5a876..ea5ac2e5f3 100644 --- a/esphome/core/scheduler.h +++ b/esphome/core/scheduler.h @@ -52,9 +52,9 @@ class Scheduler { std::function func, float backoff_increase_factor = 1.0f); bool cancel_retry(Component *component, const std::string &name); - optional next_schedule_in(); + optional next_schedule_in(uint32_t now); - void call(); + void call(uint32_t now); 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, uint32_t delay, std::function func); - uint64_t millis_(); + uint64_t millis_64_(uint32_t now); void cleanup_(); void pop_raw_(); @@ -175,7 +175,7 @@ class Scheduler { } // 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 bool should_skip_item_(const SchedulerItem *item) const {