From 044d42d395dde299143fdce51597e8c9ef3aca79 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 24 Apr 2026 16:32:15 -0500 Subject: [PATCH] Revert "[core] Switch Scheduler::call now_64 plumbing from struct to out-param" This reverts commit 4d7033df4f1abecd0b811ed80a337a070f9303ce. --- esphome/core/application.cpp | 3 +-- esphome/core/application.h | 26 ++++++++++++-------------- esphome/core/scheduler.cpp | 11 +++++------ esphome/core/scheduler.h | 26 ++++++++++++++++---------- 4 files changed, 34 insertions(+), 32 deletions(-) diff --git a/esphome/core/application.cpp b/esphome/core/application.cpp index b35db45e26..c390b26cd4 100644 --- a/esphome/core/application.cpp +++ b/esphome/core/application.cpp @@ -83,8 +83,7 @@ void Application::setup() { // Service scheduler and process pending loop enables to handle GPIO // interrupts during setup. During setup we always run the component // phase (no loop_interval_ gate), so call both helpers unconditionally. - uint64_t setup_sched_now_64_unused; - this->scheduler_tick_(MillisInternal::get(), setup_sched_now_64_unused); + this->scheduler_tick_(MillisInternal::get()); { ComponentPhaseGuard phase_guard{*this}; diff --git a/esphome/core/application.h b/esphome/core/application.h index 20a4ffa9a3..af68d0e051 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -410,7 +410,7 @@ class Application { void enable_component_loop_(Component *component); void enable_pending_loops_(); void activate_looping_component_(uint16_t index); - inline uint32_t ESPHOME_ALWAYS_INLINE scheduler_tick_(uint32_t now, uint64_t &now_64_out); + inline Scheduler::CallResult ESPHOME_ALWAYS_INLINE scheduler_tick_(uint32_t now); // RAII guard for a component loop phase. Constructor processes any pending // enable_loop requests from ISRs and marks in_loop_ so reentrant @@ -535,12 +535,12 @@ extern Application App; // NOLINT(cppcoreguidelines-avoid-non-const-global-vari // 64-bit `now_64` (or 0 if any item fired — then stale). The main loop forwards // now_64 to next_schedule_in() to avoid a second esp_timer_get_time() MMIO read // per iteration; 0 means "compute fresh". -inline uint32_t ESPHOME_ALWAYS_INLINE Application::scheduler_tick_(uint32_t now, uint64_t &now_64_out) { +inline Scheduler::CallResult ESPHOME_ALWAYS_INLINE Application::scheduler_tick_(uint32_t now) { #ifdef USE_HOST // Drain wake notifications first to clear socket for next wake. wake_drain_notifications(); #endif - return this->scheduler.call(now, now_64_out); + return this->scheduler.call(now); } // Phase B entry: only invoked when a component loop phase is about to run. @@ -580,14 +580,12 @@ inline void ESPHOME_ALWAYS_INLINE Application::loop() { // Phase A: always service the scheduler. Decouples scheduler cadence from // loop_interval_ so raised intervals (for power savings) don't drag scheduled // items forward. A tick that only runs the scheduler is cheap. - // scheduler_tick_ returns the advanced `now` (for wdt monotonicity) and fills - // sched_now_64 with the 64-bit timestamp Scheduler::call() already computed — - // forward that to the next_schedule_in() call below so we don't re-read - // esp_timer_get_time() a second time per iteration. 0 means "stale, recompute". - // Out-param keeps the return path in a single register and avoids a 16-byte - // struct-copy on the caller's stack. - uint64_t sched_now_64_raw; - uint32_t now = this->scheduler_tick_(MillisInternal::get(), sched_now_64_raw); + // scheduler_tick_ returns the advanced `now` (for wdt monotonicity) plus the + // 64-bit `now_64` Scheduler::call() already computed — forward that to the + // next_schedule_in() call below so we don't re-read esp_timer_get_time() a + // second time per iteration. 0 means "stale, recompute". + const Scheduler::CallResult sched_result = this->scheduler_tick_(MillisInternal::get()); + uint32_t now = sched_result.now; #ifdef USE_RUNTIME_STATS // Capture immediately after scheduler_tick_ returns so the post-scheduler // feed_wdt_with_time slice can be separated from the scheduler slice in @@ -707,10 +705,10 @@ 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_now_64_raw sentinel here so next_schedule_in() can assume a valid + // 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_now_64_raw is 0 (stale) and we read the clock fresh; otherwise we reuse it. - const uint64_t sched_now_64 = sched_now_64_raw != 0 ? sched_now_64_raw : this->scheduler.millis_64_from(now); + // 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); delay_time = std::min(until_phase, until_sched); } diff --git a/esphome/core/scheduler.cpp b/esphome/core/scheduler.cpp index 803807d707..46c798eee0 100644 --- a/esphome/core/scheduler.cpp +++ b/esphome/core/scheduler.cpp @@ -568,7 +568,7 @@ void HOT Scheduler::process_defer_queue_slow_path_(uint32_t &now) { } #endif /* not ESPHOME_THREAD_SINGLE */ -uint32_t HOT Scheduler::call(uint32_t now, uint64_t &now_64_out) { +Scheduler::CallResult HOT Scheduler::call(uint32_t now) { #ifndef ESPHOME_THREAD_SINGLE this->process_defer_queue_(now); #endif /* not ESPHOME_THREAD_SINGLE */ @@ -752,11 +752,10 @@ uint32_t HOT Scheduler::call(uint32_t now, uint64_t &now_64_out) { #endif // execute_item_() advances `now` as items fire; return it so the caller // stays monotonic with last_wdt_feed_. - // The now_64 we pass back is only usable if NO items fired — execute_item_ - // only advances the uint32 `now`, leaving our local now_64 stale. Signal - // stale with the 0 sentinel so next_schedule_in() reads the clock fresh. - now_64_out = executed_any_item ? 0 : now_64; - return now; + // The returned now_64 is only usable if NO items fired — execute_item_ only + // advances the uint32 `now`, leaving our local now_64 stale. Signal stale + // with the 0 sentinel so next_schedule_in() reads the clock fresh. + return CallResult{now, executed_any_item ? 0 : now_64}; } void HOT Scheduler::process_to_add_slow_path_() { LockGuard guard{this->lock_}; diff --git a/esphome/core/scheduler.h b/esphome/core/scheduler.h index 2cf559cc9c..c169d458d7 100644 --- a/esphome/core/scheduler.h +++ b/esphome/core/scheduler.h @@ -136,17 +136,23 @@ class Scheduler { // IMPORTANT: This method should only be called from the main thread (loop task). optional next_schedule_in(uint64_t now_64); - // Execute all scheduled items that are ready. + // Result of Scheduler::call(). + // now – advanced uint32 millis timestamp (monotonic with the caller's wdt feed). + // now_64 – 64-bit millis value Scheduler used for item dispatch, OR 0 if any item + // fired (in that case now_64 is stale because execute_item_ only advances + // the uint32 `now`). A subsequent next_schedule_in() treats 0 as "compute + // fresh" and non-zero as "reuse this value", saving a second millis_64() + // / esp_timer_get_time() call per main-loop iteration. + struct CallResult { + uint32_t now; + uint64_t now_64; + }; + + // Execute all scheduled items that are ready // @param now Fresh timestamp from millis() - must not be stale/cached - // @param now_64_out Out-param set to the 64-bit `now` that Scheduler used for item - // dispatch, OR 0 if any item fired (in that case the value is stale because - // execute_item_ only advances the uint32 `now`). A subsequent next_schedule_in() - // treats 0 as "compute fresh" and non-zero as "reuse this value", saving a - // second millis_64() / esp_timer_get_time() call per main-loop iteration. - // Out-param (rather than struct return) keeps the return path in a single - // register and avoids a 16-byte struct-copy on the loop caller's stack. - // @return Advanced `now` (monotonic with the caller's wdt feed). - uint32_t call(uint32_t now, uint64_t &now_64_out); + // @return Both advanced `now` (for wdt monotonicity) and `now_64` (for pipe-through + // to next_schedule_in()). See CallResult. + CallResult call(uint32_t now); // Move items from to_add_ into the main heap. // IMPORTANT: This method should only be called from the main thread (loop task).