diff --git a/esphome/core/scheduler.cpp b/esphome/core/scheduler.cpp index 9a9e61d579..90d19e1ead 100644 --- a/esphome/core/scheduler.cpp +++ b/esphome/core/scheduler.cpp @@ -27,7 +27,6 @@ static constexpr size_t MAX_POOL_SIZE = 5; // Set to 5 to match the pool size - when we have as many cancelled items as our // pool can hold, it's time to clean up and recycle them. static constexpr uint32_t MAX_LOGICALLY_DELETED_ITEMS = 5; - // Half the 32-bit range - used to detect rollovers vs normal time progression static constexpr uint32_t HALF_MAX_UINT32 = std::numeric_limits::max() / 2; // max delay to start an interval sequence @@ -146,12 +145,12 @@ void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type // first execution happens immediately after a random smallish offset // Calculate random offset (0 to min(interval/2, 5s)) uint32_t offset = (uint32_t) (std::min(delay / 2, MAX_INTERVAL_DELAY) * random_float()); - item->next_execution_ = now + offset; + item->set_next_execution(now + offset); ESP_LOGV(TAG, "Scheduler interval for %s is %" PRIu32 "ms, offset %" PRIu32 "ms", name_cstr ? name_cstr : "", delay, offset); } else { item->interval = 0; - item->next_execution_ = now + delay; + item->set_next_execution(now + delay); } #ifdef ESPHOME_DEBUG_SCHEDULER @@ -167,7 +166,8 @@ void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type name_cstr ? name_cstr : "(null)", type_str, delay); } else { ESP_LOGD(TAG, "set_%s(name='%s/%s', %s=%" PRIu32 ", offset=%" PRIu32 ")", type_str, item->get_source(), - name_cstr ? name_cstr : "(null)", type_str, delay, static_cast(item->next_execution_ - now)); + name_cstr ? name_cstr : "(null)", type_str, delay, + static_cast(item->get_next_execution() - now)); } #endif /* ESPHOME_DEBUG_SCHEDULER */ @@ -312,9 +312,10 @@ optional HOT Scheduler::next_schedule_in(uint32_t now) { auto &item = this->items_[0]; // Convert the fresh timestamp from caller (usually Application::loop()) to 64-bit const auto now_64 = this->millis_64_(now); // 'now' from parameter - fresh from caller - if (item->next_execution_ < now_64) + const uint64_t next_exec = item->get_next_execution(); + if (next_exec < now_64) return 0; - return item->next_execution_ - now_64; + return next_exec - now_64; } void HOT Scheduler::call(uint32_t now) { #ifndef ESPHOME_THREAD_SINGLE @@ -387,7 +388,7 @@ void HOT Scheduler::call(uint32_t now) { bool is_cancelled = is_item_removed_(item.get()); ESP_LOGD(TAG, " %s '%s/%s' interval=%" PRIu32 " next_execution in %" PRIu64 "ms at %" PRIu64 "%s", item->get_type_str(), item->get_source(), name ? name : "(null)", item->interval, - item->next_execution_ - now_64, item->next_execution_, is_cancelled ? " [CANCELLED]" : ""); + item->get_next_execution() - now_64, item->get_next_execution(), is_cancelled ? " [CANCELLED]" : ""); old_items.push_back(std::move(item)); } @@ -439,7 +440,7 @@ void HOT Scheduler::call(uint32_t now) { { // Don't copy-by value yet auto &item = this->items_[0]; - if (item->next_execution_ > now_64) { + if (item->get_next_execution() > now_64) { // Not reached timeout yet, done for this call break; } @@ -478,7 +479,7 @@ void HOT Scheduler::call(uint32_t now) { const char *item_name = item->get_name(); ESP_LOGV(TAG, "Running %s '%s/%s' with interval=%" PRIu32 " next_execution=%" PRIu64 " (now=%" PRIu64 ")", item->get_type_str(), item->get_source(), item_name ? item_name : "(null)", item->interval, - item->next_execution_, now_64); + item->get_next_execution(), now_64); #endif /* ESPHOME_DEBUG_SCHEDULER */ // Warning: During callback(), a lot of stuff can happen, including: @@ -503,7 +504,7 @@ void HOT Scheduler::call(uint32_t now) { } if (item->type == SchedulerItem::INTERVAL) { - item->next_execution_ = now_64 + item->interval; + item->set_next_execution(now_64 + item->interval); // Add new item directly to to_add_ // since we have the lock held this->to_add_.push_back(std::move(item)); @@ -806,7 +807,10 @@ uint64_t Scheduler::millis_64_(uint32_t now) { bool HOT Scheduler::SchedulerItem::cmp(const std::unique_ptr &a, const std::unique_ptr &b) { - return a->next_execution_ > b->next_execution_; + // High bits are almost always equal (change only on 32-bit rollover ~49 days) + // Optimize for common case: check low bits first when high bits are equal + return (a->next_execution_high_ == b->next_execution_high_) ? (a->next_execution_low_ > b->next_execution_low_) + : (a->next_execution_high_ > b->next_execution_high_); } void Scheduler::recycle_item_(std::unique_ptr item) { diff --git a/esphome/core/scheduler.h b/esphome/core/scheduler.h index 85cfaab2e0..68ad64b9b1 100644 --- a/esphome/core/scheduler.h +++ b/esphome/core/scheduler.h @@ -88,19 +88,22 @@ class Scheduler { struct SchedulerItem { // Ordered by size to minimize padding Component *component; - uint32_t interval; - // 64-bit time to handle millis() rollover. The scheduler combines the 32-bit millis() - // with a 16-bit rollover counter to create a 64-bit time that won't roll over for - // billions of years. This ensures correct scheduling even when devices run for months. - uint64_t next_execution_; - // Optimized name storage using tagged union union { const char *static_name; // For string literals (no allocation) char *dynamic_name; // For allocated strings } name_; - + uint32_t interval; + // Split time to handle millis() rollover. The scheduler combines the 32-bit millis() + // with a 16-bit rollover counter to create a 48-bit time space (using 32+16 bits). + // This is intentionally limited to 48 bits, not stored as a full 64-bit value. + // With 49.7 days per 32-bit rollover, the 16-bit counter supports + // 49.7 days × 65536 = ~8900 years. This ensures correct scheduling + // even when devices run for months. Split into two fields for better memory + // alignment on 32-bit systems. + uint32_t next_execution_low_; // Lower 32 bits of execution time (millis value) std::function callback; + uint16_t next_execution_high_; // Upper 16 bits (millis_major counter) #ifdef ESPHOME_THREAD_MULTI_ATOMICS // Multi-threaded with atomics: use atomic for lock-free access @@ -126,7 +129,8 @@ class Scheduler { SchedulerItem() : component(nullptr), interval(0), - next_execution_(0), + next_execution_low_(0), + next_execution_high_(0), #ifdef ESPHOME_THREAD_MULTI_ATOMICS // remove is initialized in the member declaration as std::atomic{false} type(TIMEOUT), @@ -185,7 +189,21 @@ class Scheduler { } static bool cmp(const std::unique_ptr &a, const std::unique_ptr &b); - const char *get_type_str() const { return (type == TIMEOUT) ? "timeout" : "interval"; } + + // Note: We use 48 bits total (32 + 16), stored in a 64-bit value for API compatibility. + // The upper 16 bits of the 64-bit value are always zero, which is fine since + // millis_major_ is also 16 bits and they must match. + constexpr uint64_t get_next_execution() const { + return (static_cast(next_execution_high_) << 32) | next_execution_low_; + } + + constexpr void set_next_execution(uint64_t value) { + next_execution_low_ = static_cast(value); + // Cast to uint16_t intentionally truncates to lower 16 bits of the upper 32 bits. + // This is correct because millis_major_ that creates these values is also 16 bits. + next_execution_high_ = static_cast(value >> 32); + } + constexpr const char *get_type_str() const { return (type == TIMEOUT) ? "timeout" : "interval"; } const char *get_source() const { return component ? component->get_component_source() : "unknown"; } };