mirror of
https://github.com/esphome/esphome.git
synced 2026-09-11 23:37:34 +00:00
[core] Thread Scheduler now_64 into next_schedule_in to skip duplicate clock read
Scheduler::call() and Scheduler::next_schedule_in() both computed now_64 via
millis_64_from_(now) at the top of every main-loop iteration. On ESP32 with
USE_NATIVE_64BIT_TIME this is an esp_timer_get_time() MMIO read (~1us);
doing it twice per iteration is wasted work since the two calls happen
microseconds apart.
Thread the value through:
- Scheduler::call() now returns a CallResult { now, now_64 } struct instead
of just the advanced uint32_t now. When items fired, now_64 is set to 0
(sentinel) because execute_item_() only advances the uint32 and the local
now_64 is stale by the time we return.
- Scheduler::next_schedule_in() takes an optional now_64 parameter
(default 0). Non-zero values are trusted and used directly; the 0
sentinel triggers a fresh millis_64_from_(now) read.
- Application::scheduler_tick_() forwards the CallResult through.
- Application::loop() passes sched_result.now_64 to next_schedule_in().
Verified in the disassembly: on the fast path (no items fired),
next_schedule_in branches over its esp_timer_get_time call entirely via
`or a8, a4, a5; bnez a8, ...` on the two halves of now_64, jumping
straight to the next_exec comparison. When call() returned the 0 sentinel,
next_schedule_in falls through to the existing inlined
micros_to_millis(esp_timer_get_time()) sequence as before.
Bench callers (tests/benchmarks/core/bench_scheduler.cpp) and the external
test component (tests/integration/fixtures/.../scheduler_bulk_cleanup_component)
ignore the return value — no changes needed there.
This commit is contained in:
+13
-11
@@ -392,7 +392,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);
|
||||
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
|
||||
@@ -508,10 +508,11 @@ extern Application App; // NOLINT(cppcoreguidelines-avoid-non-const-global-vari
|
||||
// Application::loop() tick regardless of whether a component phase runs, so
|
||||
// scheduler items fire at their requested cadence even when the caller has
|
||||
// raised loop_interval_ for power savings (see Application::loop()).
|
||||
// Returns the timestamp of the last scheduler item that ran (or `now`
|
||||
// unchanged if none ran), so the caller's WDT feed stays monotonic with the
|
||||
// per-item feeds inside scheduler.call() without an extra millis().
|
||||
inline uint32_t ESPHOME_ALWAYS_INLINE Application::scheduler_tick_(uint32_t now) {
|
||||
// Returns the advanced `now` (for monotonic wdt feed) alongside the Scheduler's
|
||||
// 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 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();
|
||||
@@ -556,11 +557,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 timestamp of the last scheduler item that ran
|
||||
// (advanced by its per-item feeds) or `now` unchanged. We adopt it as `now`
|
||||
// so the gate check and WDT feed both reflect actual elapsed time after
|
||||
// scheduler dispatch, without an extra millis() call.
|
||||
uint32_t now = this->scheduler_tick_(MillisInternal::get());
|
||||
// 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;
|
||||
// Guarantee one WDT feed per tick even when the scheduler had nothing to
|
||||
// dispatch and the component phase is gated out — covers configs with no
|
||||
// looping components and no scheduler work (setup() has its own
|
||||
@@ -662,7 +664,7 @@ 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).value_or(until_phase);
|
||||
const uint32_t until_sched = this->scheduler.next_schedule_in(now, sched_result.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) {
|
||||
optional<uint32_t> HOT Scheduler::next_schedule_in(uint32_t now, 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,7 +441,12 @@ optional<uint32_t> HOT Scheduler::next_schedule_in(uint32_t now) {
|
||||
return {};
|
||||
|
||||
SchedulerItem *item = this->items_[0];
|
||||
const auto now_64 = this->millis_64_from_(now);
|
||||
// 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);
|
||||
const uint64_t next_exec = item->get_next_execution();
|
||||
if (next_exec < now_64)
|
||||
return 0;
|
||||
@@ -552,7 +557,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) {
|
||||
Scheduler::CallResult HOT Scheduler::call(uint32_t now) {
|
||||
#ifndef ESPHOME_THREAD_SINGLE
|
||||
this->process_defer_queue_(now);
|
||||
#endif /* not ESPHOME_THREAD_SINGLE */
|
||||
@@ -563,6 +568,10 @@ uint32_t HOT Scheduler::call(uint32_t now) {
|
||||
|
||||
// Track if any items were added to to_add_ during callbacks
|
||||
bool has_added_items = false;
|
||||
// Track whether we advanced `now` via execute_item_(). If we did, `now_64`
|
||||
// is stale by the time we return and next_schedule_in() must recompute —
|
||||
// signal that by writing 0 to *now_64_out.
|
||||
bool executed_any_item = false;
|
||||
|
||||
#ifdef ESPHOME_DEBUG_SCHEDULER
|
||||
static uint64_t last_print = 0;
|
||||
@@ -675,6 +684,7 @@ uint32_t HOT Scheduler::call(uint32_t now) {
|
||||
// - timeouts/intervals get added, potentially invalidating vector pointers
|
||||
// - timeouts/intervals get cancelled
|
||||
now = this->execute_item_(item, now);
|
||||
executed_any_item = true;
|
||||
|
||||
LockGuard guard{this->lock_};
|
||||
|
||||
@@ -731,7 +741,10 @@ uint32_t HOT Scheduler::call(uint32_t now) {
|
||||
#endif
|
||||
// execute_item_() advances `now` as items fire; return it so the caller
|
||||
// stays monotonic with last_wdt_feed_.
|
||||
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_};
|
||||
|
||||
@@ -122,15 +122,32 @@ class Scheduler {
|
||||
|
||||
// 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.
|
||||
// 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);
|
||||
optional<uint32_t> next_schedule_in(uint32_t now, uint64_t now_64 = 0);
|
||||
|
||||
// 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
|
||||
// @return Timestamp of the last item that ran, or `now` unchanged if none ran.
|
||||
uint32_t call(uint32_t now);
|
||||
// @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).
|
||||
|
||||
Reference in New Issue
Block a user