[scheduler] Feed watchdog after each scheduled item, drop top-of-loop feed

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.
This commit is contained in:
J. Nick Koston
2026-04-11 14:51:59 -10:00
parent ddbf6f2347
commit a70ec9ec06
2 changed files with 12 additions and 5 deletions
+5 -4
View File
@@ -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_) {
+7 -1
View File
@@ -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