diff --git a/esphome/core/scheduler.cpp b/esphome/core/scheduler.cpp index 317426b51e..6834b72aab 100644 --- a/esphome/core/scheduler.cpp +++ b/esphome/core/scheduler.cpp @@ -511,15 +511,10 @@ uint64_t Scheduler::millis_64_(uint32_t now) { #ifdef ESPHOME_SINGLE_CORE // This is the single core implementation. // - // The implementation handles the 32-bit rollover (every 49.7 days) by: - // 1. Using a lock when detecting rollover to ensure atomic update - // 2. Restricting normal updates to forward movement within the same epoch - // This prevents race conditions at the rollover boundary without requiring - // 64-bit atomics or locking on every call. + // Single-core platforms have no concurrency, so this is a simple implementation + // that just tracks 32-bit rollover (every 49.7 days) without any locking or atomics. uint16_t major = this->millis_major_; - - // Single-core platforms: No atomics needed uint32_t last = this->last_millis_; // Check for rollover @@ -538,121 +533,119 @@ uint64_t Scheduler::millis_64_(uint32_t now) { // Combine major (high 32 bits) and now (low 32 bits) into 64-bit time return now + (static_cast(major) << 32); -} #endif // ESPHOME_SINGLE_CORE #ifdef ESPHOME_MULTI_CORE_NO_ATOMICS -// This is the multi core no atomics implementation. -// -// The implementation handles the 32-bit rollover (every 49.7 days) by: -// 1. Using a lock when detecting rollover to ensure atomic update -// 2. Restricting normal updates to forward movement within the same epoch -// This prevents race conditions at the rollover boundary without requiring -// 64-bit atomics or locking on every call. + // This is the multi core no atomics implementation. + // + // The implementation handles the 32-bit rollover (every 49.7 days) by: + // 1. Using a lock when detecting rollover to ensure atomic update + // 2. Restricting normal updates to forward movement within the same epoch + // This prevents race conditions at the rollover boundary without requiring + // 64-bit atomics or locking on every call. -uint16_t major = this->millis_major_; -uint32_t last = this->last_millis_; + uint16_t major = this->millis_major_; + uint32_t last = this->last_millis_; -// Define a safe window around the rollover point (10 seconds) -// This covers any reasonable scheduler delays or thread preemption -static const uint32_t ROLLOVER_WINDOW = 10000; // 10 seconds in milliseconds + // Define a safe window around the rollover point (10 seconds) + // This covers any reasonable scheduler delays or thread preemption + static const uint32_t ROLLOVER_WINDOW = 10000; // 10 seconds in milliseconds -// Check if we're near the rollover boundary (close to std::numeric_limits::max() or just past 0) -bool near_rollover = (last > (std::numeric_limits::max() - ROLLOVER_WINDOW)) || (now < ROLLOVER_WINDOW); + // Check if we're near the rollover boundary (close to std::numeric_limits::max() or just past 0) + bool near_rollover = (last > (std::numeric_limits::max() - ROLLOVER_WINDOW)) || (now < ROLLOVER_WINDOW); -if (near_rollover || (now < last && (last - now) > HALF_MAX_UINT32)) { - // Near rollover or detected a rollover - need lock for safety - LockGuard guard{this->lock_}; - // Re-read with lock held - last = this->last_millis_; - - if (now < last && (last - now) > HALF_MAX_UINT32) { - // True rollover detected (happens every ~49.7 days) - this->millis_major_++; - major++; -#ifdef ESPHOME_DEBUG_SCHEDULER - ESP_LOGD(TAG, "Detected true 32-bit rollover at %" PRIu32 "ms (was %" PRIu32 ")", now, last); -#endif /* ESPHOME_DEBUG_SCHEDULER */ - } - // Update last_millis_ while holding lock - this->last_millis_ = now; -} else if (now > last) { - // Normal case: Not near rollover and time moved forward - // Update without lock. While this may cause minor races (microseconds of - // backwards time movement), they're acceptable because: - // 1. The scheduler operates at millisecond resolution, not microsecond - // 2. We've already prevented the critical rollover race condition - // 3. Any backwards movement is orders of magnitude smaller than scheduler delays - this->last_millis_ = now; -} -// If now <= last and we're not near rollover, don't update -// This minimizes backwards time movement - -// Combine major (high 32 bits) and now (low 32 bits) into 64-bit time -return now + (static_cast(major) << 32); -#endif // ESPHOME_MULTI_CORE_NO_ATOMICS - -#ifdef ESPHOME_MULTI_CORE_ATOMICS -// This is the multi core with atomics implementation. -// -// The implementation handles the 32-bit rollover (every 49.7 days) by: -// 1. Using a lock when detecting rollover to ensure atomic update -// 2. Restricting normal updates to forward movement within the same epoch -// This prevents race conditions at the rollover boundary without requiring -// 64-bit atomics or locking on every call. - -for (;;) { - uint16_t major = this->millis_major_.load(std::memory_order_acquire); - - /* - * Acquire so that if we later decide **not** to take the lock we still - * observe a `millis_major_` value coherent with the loaded `last_millis_`. - * The acquire load ensures any later read of `millis_major_` sees its - * corresponding increment. - */ - uint32_t last = this->last_millis_.load(std::memory_order_acquire); - - // If we might be near a rollover (large backwards jump), take the lock for the entire operation - // This ensures rollover detection and last_millis_ update are atomic together - if (now < last && (last - now) > HALF_MAX_UINT32) { - // Potential rollover - need lock for atomic rollover detection + update + if (near_rollover || (now < last && (last - now) > HALF_MAX_UINT32)) { + // Near rollover or detected a rollover - need lock for safety LockGuard guard{this->lock_}; - // Re-read with lock held; mutex already provides ordering - last = this->last_millis_.load(std::memory_order_relaxed); + // Re-read with lock held + last = this->last_millis_; if (now < last && (last - now) > HALF_MAX_UINT32) { // True rollover detected (happens every ~49.7 days) - this->millis_major_.fetch_add(1, std::memory_order_relaxed); + this->millis_major_++; major++; #ifdef ESPHOME_DEBUG_SCHEDULER ESP_LOGD(TAG, "Detected true 32-bit rollover at %" PRIu32 "ms (was %" PRIu32 ")", now, last); #endif /* ESPHOME_DEBUG_SCHEDULER */ } - /* - * Update last_millis_ while holding the lock to prevent races - * Publish the new low-word *after* bumping `millis_major_` (done above) - * so readers never see a mismatched pair. - */ - this->last_millis_.store(now, std::memory_order_release); - } else { - // Normal case: Try lock-free update, but only allow forward movement within same epoch - // This prevents accidentally moving backwards across a rollover boundary - while (now > last && (now - last) < HALF_MAX_UINT32) { - if (this->last_millis_.compare_exchange_weak(last, now, - std::memory_order_release, // success - std::memory_order_relaxed)) { // failure - break; - } - // CAS failure means no data was published; relaxed is fine - // last is automatically updated by compare_exchange_weak if it fails - } + // Update last_millis_ while holding lock + this->last_millis_ = now; + } else if (now > last) { + // Normal case: Not near rollover and time moved forward + // Update without lock. While this may cause minor races (microseconds of + // backwards time movement), they're acceptable because: + // 1. The scheduler operates at millisecond resolution, not microsecond + // 2. We've already prevented the critical rollover race condition + // 3. Any backwards movement is orders of magnitude smaller than scheduler delays + this->last_millis_ = now; } - uint16_t major_end = this->millis_major_.load(std::memory_order_relaxed); - if (major_end == major) - return now + (static_cast(major) << 32); -} -#endif // ESPHOME_MULTI_CORE_ATOMICS + // If now <= last and we're not near rollover, don't update + // This minimizes backwards time movement + // Combine major (high 32 bits) and now (low 32 bits) into 64-bit time + return now + (static_cast(major) << 32); +#endif // ESPHOME_MULTI_CORE_NO_ATOMICS + +#ifdef ESPHOME_MULTI_CORE_ATOMICS + // This is the multi core with atomics implementation. + // + // The implementation handles the 32-bit rollover (every 49.7 days) by: + // 1. Using a lock when detecting rollover to ensure atomic update + // 2. Restricting normal updates to forward movement within the same epoch + // This prevents race conditions at the rollover boundary without requiring + // 64-bit atomics or locking on every call. + + for (;;) { + uint16_t major = this->millis_major_.load(std::memory_order_acquire); + + /* + * Acquire so that if we later decide **not** to take the lock we still + * observe a `millis_major_` value coherent with the loaded `last_millis_`. + * The acquire load ensures any later read of `millis_major_` sees its + * corresponding increment. + */ + uint32_t last = this->last_millis_.load(std::memory_order_acquire); + + // If we might be near a rollover (large backwards jump), take the lock for the entire operation + // This ensures rollover detection and last_millis_ update are atomic together + if (now < last && (last - now) > HALF_MAX_UINT32) { + // Potential rollover - need lock for atomic rollover detection + update + LockGuard guard{this->lock_}; + // Re-read with lock held; mutex already provides ordering + last = this->last_millis_.load(std::memory_order_relaxed); + + if (now < last && (last - now) > HALF_MAX_UINT32) { + // True rollover detected (happens every ~49.7 days) + this->millis_major_.fetch_add(1, std::memory_order_relaxed); + major++; +#ifdef ESPHOME_DEBUG_SCHEDULER + ESP_LOGD(TAG, "Detected true 32-bit rollover at %" PRIu32 "ms (was %" PRIu32 ")", now, last); +#endif /* ESPHOME_DEBUG_SCHEDULER */ + } + /* + * Update last_millis_ while holding the lock to prevent races + * Publish the new low-word *after* bumping `millis_major_` (done above) + * so readers never see a mismatched pair. + */ + this->last_millis_.store(now, std::memory_order_release); + } else { + // Normal case: Try lock-free update, but only allow forward movement within same epoch + // This prevents accidentally moving backwards across a rollover boundary + while (now > last && (now - last) < HALF_MAX_UINT32) { + if (this->last_millis_.compare_exchange_weak(last, now, + std::memory_order_release, // success + std::memory_order_relaxed)) { // failure + break; + } + // CAS failure means no data was published; relaxed is fine + // last is automatically updated by compare_exchange_weak if it fails + } + } + uint16_t major_end = this->millis_major_.load(std::memory_order_relaxed); + if (major_end == major) + return now + (static_cast(major) << 32); + } +#endif // ESPHOME_MULTI_CORE_ATOMICS } bool HOT Scheduler::SchedulerItem::cmp(const std::unique_ptr &a,