From a70ec9ec06931956d8ea8279e8820b147cebfdf8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 11 Apr 2026 14:51:59 -1000 Subject: [PATCH] [scheduler] Feed watchdog after each scheduled item, drop top-of-loop feed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The main loop used to feed the watchdog unconditionally right after Scheduler::call() returned, regardless of whether the scheduler had any actual work to do. On an idle device this meant every outer loop iteration paid the inline rate-limit check (load + sub + branch) for no benefit. Move the feed into Scheduler::execute_item_() so it fires only after a scheduled callback actually runs, and covers both the main heap path and the defer queue path (both go through execute_item_). This also bounds the max feed gap during a burst of back-to-back scheduled items by max(item_runtime) instead of sum(item_runtime). The top-of-loop feed in Application::before_loop_tasks_() is now unnecessary — when Scheduler::call does no work, the only elapsed time is the sleep wake plus a few instructions, and when it does have work, it fed the wdt as it went. --- esphome/core/application.h | 9 +++++---- esphome/core/scheduler.cpp | 8 +++++++- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/esphome/core/application.h b/esphome/core/application.h index 923cbfdb03..f8e9b0e360 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -829,12 +829,13 @@ inline void ESPHOME_ALWAYS_INLINE Application::before_loop_tasks_(uint32_t loop_ this->drain_wake_notifications_(); #endif - // Process scheduled tasks + // Process scheduled tasks. Scheduler::call now feeds the watchdog itself + // after each scheduled item that actually runs, so we no longer need an + // unconditional feed here — when Scheduler::call has no work to do, the + // only elapsed time is a sleep wake + a few instructions, and when it does + // have work, it fed the wdt as it went. this->scheduler.call(loop_start_time); - // Feed the watchdog timer - this->feed_wdt(loop_start_time); - // Process any pending enable_loop requests from ISRs // This must be done before marking in_loop_ = true to avoid race conditions if (this->has_pending_enable_loop_requests_) { diff --git a/esphome/core/scheduler.cpp b/esphome/core/scheduler.cpp index dff50b03ef..7c9263583d 100644 --- a/esphome/core/scheduler.cpp +++ b/esphome/core/scheduler.cpp @@ -739,7 +739,13 @@ uint32_t HOT Scheduler::execute_item_(SchedulerItem *item, uint32_t now) { App.set_current_component(item->component); WarnIfComponentBlockingGuard guard{item->component, now}; item->callback(); - return guard.finish(); + uint32_t end = guard.finish(); + // Feed the watchdog after each scheduled item (both main heap and defer + // queue paths go through here). A run of back-to-back callbacks cannot + // starve the wdt. The inline fast path is a load + sub + branch — nearly + // free when the 3 ms rate limit hasn't elapsed. + App.feed_wdt(end); + return end; } // Common implementation for cancel operations - handles locking