mirror of
https://github.com/esphome/esphome.git
synced 2026-09-01 10:36:01 +00:00
Merge remote-tracking branch 'upstream-ssh/scheduler-call-fastpath' into integration
# Conflicts: # esphome/core/application.h
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
|
||||
@@ -513,10 +513,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();
|
||||
@@ -561,11 +562,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;
|
||||
#ifdef USE_RUNTIME_STATS
|
||||
// Capture immediately after scheduler_tick_ returns so the post-scheduler
|
||||
// feed_wdt_with_time slice can be separated from the scheduler slice in
|
||||
@@ -685,7 +687,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.
|
||||
@@ -455,7 +455,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;
|
||||
@@ -566,7 +571,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 */
|
||||
@@ -577,6 +582,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;
|
||||
@@ -689,6 +698,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_};
|
||||
|
||||
@@ -745,7 +755,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