mirror of
https://github.com/esphome/esphome.git
synced 2026-07-10 17:05:36 +00:00
[core] Hoist Scheduler::next_schedule_in zero-check to caller
Follow-up to the CallResult threading: move the 0-sentinel fallback
(if now_64 == 0, read the clock fresh) out of next_schedule_in and into
the single main-loop caller.
Effect on next_schedule_in: drops the uint32_t now parameter entirely
and removes the cold millis_64_from_(now) inlined clock read from the
function body. Verified in disassembly:
Scheduler::next_schedule_in 159 B -> 86 B (-46%)
The hot path is now just defer_empty + cleanup_ + top-of-heap compare +
return. No clock read anywhere in the function.
Application::loop handles the 0-sentinel with a single inline branch
right before the call; when call() fired items it invokes the public
Scheduler::millis_64_from() wrapper (new, exposes the existing protected
millis_64_from_ so callers can do 64-bit extension without platform
knowledge). Fast path (no items fired) stays branch-free.
Net code size: loop_task +64 B, next_schedule_in -73 B,
Scheduler::call unchanged -> -9 B total.
This commit is contained in:
@@ -664,7 +664,11 @@ 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);
|
||||
const uint32_t until_sched = this->scheduler.next_schedule_in(now, sched_result.now_64).value_or(until_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);
|
||||
delay_time = std::min(until_phase, until_sched);
|
||||
}
|
||||
// All platforms route loop yields through the platform wake primitive.
|
||||
|
||||
@@ -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<uint32_t> HOT Scheduler::next_schedule_in(uint32_t now, uint64_t now_64) {
|
||||
optional<uint32_t> HOT Scheduler::next_schedule_in(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.
|
||||
@@ -441,12 +441,9 @@ optional<uint32_t> HOT Scheduler::next_schedule_in(uint32_t now, uint64_t now_64
|
||||
return {};
|
||||
|
||||
SchedulerItem *item = this->items_[0];
|
||||
// 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);
|
||||
// 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.
|
||||
const uint64_t next_exec = item->get_next_execution();
|
||||
if (next_exec < now_64)
|
||||
return 0;
|
||||
|
||||
@@ -120,16 +120,21 @@ 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 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.
|
||||
// @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.
|
||||
// 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<uint32_t> next_schedule_in(uint32_t now, uint64_t now_64 = 0);
|
||||
optional<uint32_t> next_schedule_in(uint64_t now_64);
|
||||
|
||||
// Result of Scheduler::call().
|
||||
// now – advanced uint32 millis timestamp (monotonic with the caller's wdt feed).
|
||||
|
||||
Reference in New Issue
Block a user