[core] Scheduler::next_schedule_in: also handle single-threaded defer path

On ESPHOME_THREAD_SINGLE builds (ESP8266, RP2040), defer() does not
route through defer_queue_ — set_timer_common_ treats it as an ordinary
0-delay set_timeout that stages in to_add_ (scheduler.cpp:197). Since
next_schedule_in only inspects items_[0], any defer posted from a
component's loop() sits in to_add_ invisible to the sleep calculation
until the next scheduler.call() runs process_to_add().

The symmetric fix checks to_add_empty_() on single-threaded and
short-circuits sleep the same way. This also costs one "skipped sleep"
for any non-zero-delay timer added at runtime, but the next tick
produces a correct next_schedule_in reading and the cost is a single
yield — negligible vs. the stall this closes.

Suggested by Copilot review on #15968.
This commit is contained in:
J. Nick Koston
2026-04-24 03:37:11 -05:00
parent da5c31cb27
commit d507feaf81
+6
View File
@@ -423,6 +423,12 @@ optional<uint32_t> HOT Scheduler::next_schedule_in(uint32_t now) {
// iteration has work to do right now -- don't let the caller sleep.
if (!this->defer_empty_())
return 0;
#else
// On single-threaded builds, defer() routes through set_timeout(..., 0) which
// stages in to_add_. process_to_add() runs at the top of every scheduler.call(),
// so anything in to_add_ becomes runnable on the next iteration; don't sleep.
if (!this->to_add_empty_())
return 0;
#endif
// If no items, return empty optional