From d7e755fb833e9d4b549845642e8e81e750d5db2f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 24 Apr 2026 16:32:15 -0500 Subject: [PATCH] Revert "[core] Hoist Scheduler::next_schedule_in zero-check to caller" This reverts commit dfe591c5eb6ed271540ad36a4249b530a7fd65d3. --- esphome/core/application.h | 6 +----- esphome/core/scheduler.cpp | 11 +++++++---- esphome/core/scheduler.h | 17 ++++++----------- 3 files changed, 14 insertions(+), 20 deletions(-) diff --git a/esphome/core/application.h b/esphome/core/application.h index af68d0e0514..a907c970357 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -705,11 +705,7 @@ inline void ESPHOME_ALWAYS_INLINE Application::loop() { const uint32_t elapsed_since_phase = now - this->last_loop_; const uint32_t until_phase = (elapsed_since_phase >= this->loop_interval_) ? 0 : (this->loop_interval_ - elapsed_since_phase); - // Resolve the sched_result.now_64 sentinel here so next_schedule_in() can assume a valid - // non-zero input and keep the cold clock-read path out of its body. When call() fired items - // sched_result.now_64 is 0 (stale) and we read the clock fresh; otherwise we reuse it. - const uint64_t sched_now_64 = sched_result.now_64 != 0 ? sched_result.now_64 : this->scheduler.millis_64_from(now); - const uint32_t until_sched = this->scheduler.next_schedule_in(sched_now_64).value_or(until_phase); + const uint32_t until_sched = this->scheduler.next_schedule_in(now, sched_result.now_64).value_or(until_phase); delay_time = std::min(until_phase, until_sched); } // All platforms route loop yields through the platform wake primitive. diff --git a/esphome/core/scheduler.cpp b/esphome/core/scheduler.cpp index 46c798eee09..d6d3ef2d2d4 100644 --- a/esphome/core/scheduler.cpp +++ b/esphome/core/scheduler.cpp @@ -412,7 +412,7 @@ bool HOT Scheduler::cancel_retry(Component *component, uint32_t id) { #pragma GCC diagnostic pop // End suppression of deprecated RetryResult warnings -optional HOT Scheduler::next_schedule_in(uint64_t now_64) { +optional HOT Scheduler::next_schedule_in(uint32_t now, uint64_t now_64) { // IMPORTANT: This method should only be called from the main thread (loop task). // Accesses items_[0] and the fast-path empty checks without holding a lock, which // is only safe from the main thread. Other threads must not call this method. @@ -455,9 +455,12 @@ optional HOT Scheduler::next_schedule_in(uint64_t now_64) { return {}; SchedulerItem *item = this->items_[0]; - // Caller is responsible for passing a fresh non-zero now_64 (see header doc). - // Hoisting the 0-sentinel resolution to the caller keeps the cold fallback - // clock-read out of this function's fast path. + // If the caller did not pass a pre-computed now_64 (or passed the 0 + // sentinel, meaning call() advanced past it by firing items), read the + // clock fresh. Otherwise reuse the passed value to avoid a second + // esp_timer_get_time() MMIO read per main-loop iteration. + if (now_64 == 0) + now_64 = this->millis_64_from_(now); const uint64_t next_exec = item->get_next_execution(); if (next_exec < now_64) return 0; diff --git a/esphome/core/scheduler.h b/esphome/core/scheduler.h index c169d458d74..7073507a997 100644 --- a/esphome/core/scheduler.h +++ b/esphome/core/scheduler.h @@ -120,21 +120,16 @@ class Scheduler { /// Get 64-bit millisecond timestamp (handles 32-bit millis() rollover) uint64_t ESPHOME_ALWAYS_INLINE millis_64() { return esphome::millis_64(); } - // Extend a 32-bit `now` timestamp to a 64-bit one. Public wrapper so callers that need to - // materialize a valid now_64 for next_schedule_in() (after call() returned the 0 sentinel) - // can do so without knowing the platform-specific 64-bit extension strategy. - // See millis_64_from_ for platform notes. - uint64_t ESPHOME_ALWAYS_INLINE millis_64_from(uint32_t now) { return this->millis_64_from_(now); } - // Calculate when the next scheduled item should run. - // @param now_64 64-bit millisecond timestamp. Must be non-zero and fresh — the caller is responsible - // for resolving the 0 sentinel that Scheduler::call() may return (via millis_64_from above). - // Hoisting the zero-check out of this function keeps the cold clock-read path out of the - // hot-path's icache line and simplifies register allocation. + // @param now On ESP32, unused for 64-bit extension (native); on other platforms, extended to 64-bit via rollover. + // @param now_64 Optional pre-computed 64-bit timestamp from a recent call() (see CallResult). + // Pass 0 (sentinel) to have this method compute now_64 freshly. Non-zero values are trusted and + // used directly, saving a millis_64() / esp_timer_get_time() MMIO read when the caller already + // has a fresh value from the same loop iteration. // Returns the time in milliseconds until the next scheduled item, or nullopt if no items. // This method performs cleanup of removed items before checking the schedule. // IMPORTANT: This method should only be called from the main thread (loop task). - optional next_schedule_in(uint64_t now_64); + optional next_schedule_in(uint32_t now, uint64_t now_64 = 0); // Result of Scheduler::call(). // now – advanced uint32 millis timestamp (monotonic with the caller's wdt feed).