Compare commits

...

3 Commits

Author SHA1 Message Date
J. Nick Koston
d91cad1636 tweak 2025-12-04 20:49:11 -06:00
J. Nick Koston
54d0328002 Merge remote-tracking branch 'upstream/dev' into scheduler_no64bit 2025-12-04 19:25:12 -06:00
J. Nick Koston
865312ff60 merge 2025-12-04 19:25:03 -06:00
2 changed files with 88 additions and 63 deletions

View File

@@ -91,8 +91,8 @@ void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type
return;
}
// Get fresh timestamp BEFORE taking lock - millis_64_ may need to acquire lock itself
const uint64_t now = this->millis_64_(millis());
// Get fresh timestamp BEFORE taking lock - millis_48_ may need to acquire lock itself
const Time48 now = this->millis_48_(millis());
// Take lock early to protect scheduler_item_pool_ access
LockGuard guard{this->lock_};
@@ -291,12 +291,12 @@ optional<uint32_t> HOT Scheduler::next_schedule_in(uint32_t now) {
return {};
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
const uint64_t next_exec = item->get_next_execution();
if (next_exec < now_64)
// Convert the fresh timestamp from caller (usually Application::loop()) to 48-bit
const auto now_48 = this->millis_48_(now); // 'now' from parameter - fresh from caller
const Time48 next_exec = item->get_next_execution();
if (next_exec < now_48)
return 0;
return next_exec - now_64;
return next_exec - now_48;
}
void Scheduler::full_cleanup_removed_items_() {
@@ -331,28 +331,21 @@ void HOT Scheduler::call(uint32_t now) {
this->process_defer_queue_(now);
#endif /* not ESPHOME_THREAD_SINGLE */
// Convert the fresh timestamp from main loop to 64-bit for scheduler operations
const auto now_64 = this->millis_64_(now); // 'now' from parameter - fresh from Application::loop()
// Convert the fresh timestamp from main loop to 48-bit for scheduler operations
const auto now_48 = this->millis_48_(now); // 'now' from parameter - fresh from Application::loop()
this->process_to_add();
// Track if any items were added to to_add_ during this call (intervals or from callbacks)
bool has_added_items = false;
#ifdef ESPHOME_DEBUG_SCHEDULER
static uint64_t last_print = 0;
static Time48 last_print{};
if (now_64 - last_print > 2000) {
last_print = now_64;
if ((now_48 - last_print) > 2000) {
last_print = now_48;
std::vector<std::unique_ptr<SchedulerItem>> old_items;
#ifdef ESPHOME_THREAD_MULTI_ATOMICS
const auto last_dbg = this->last_millis_.load(std::memory_order_relaxed);
const auto major_dbg = this->millis_major_.load(std::memory_order_relaxed);
ESP_LOGD(TAG, "Items: count=%zu, pool=%zu, now=%" PRIu64 " (%" PRIu16 ", %" PRIu32 ")", this->items_.size(),
this->scheduler_item_pool_.size(), now_64, major_dbg, last_dbg);
#else /* not ESPHOME_THREAD_MULTI_ATOMICS */
ESP_LOGD(TAG, "Items: count=%zu, pool=%zu, now=%" PRIu64 " (%" PRIu16 ", %" PRIu32 ")", this->items_.size(),
this->scheduler_item_pool_.size(), now_64, this->millis_major_, this->last_millis_);
#endif /* else ESPHOME_THREAD_MULTI_ATOMICS */
ESP_LOGD(TAG, "Items: count=%zu, pool=%zu, now=(%" PRIu16 ", %" PRIu32 ")", this->items_.size(),
this->scheduler_item_pool_.size(), now_48.major, now_48.millis);
// Cleanup before debug output
this->cleanup_();
while (!this->items_.empty()) {
@@ -364,9 +357,10 @@ void HOT Scheduler::call(uint32_t now) {
const char *name = item->get_name();
bool is_cancelled = is_item_removed_(item.get());
ESP_LOGD(TAG, " %s '%s/%s' interval=%" PRIu32 " next_execution in %" PRIu64 "ms at %" PRIu64 "%s",
const Time48 next_exec = item->get_next_execution();
ESP_LOGD(TAG, " %s '%s/%s' interval=%" PRIu32 " next_execution in %" PRIu32 "ms at (%" PRIu16 ", %" PRIu32 ")%s",
item->get_type_str(), LOG_STR_ARG(item->get_source()), name ? name : "(null)", item->interval,
item->get_next_execution() - now_64, item->get_next_execution(), is_cancelled ? " [CANCELLED]" : "");
next_exec - now_48, next_exec.major, next_exec.millis, is_cancelled ? " [CANCELLED]" : "");
old_items.push_back(std::move(item));
}
@@ -393,7 +387,7 @@ void HOT Scheduler::call(uint32_t now) {
while (!this->items_.empty()) {
// Don't copy-by value yet
auto &item = this->items_[0];
if (item->get_next_execution() > now_64) {
if (item->get_next_execution() > now_48) {
// Not reached timeout yet, done for this call
break;
}
@@ -430,9 +424,12 @@ void HOT Scheduler::call(uint32_t now) {
#ifdef ESPHOME_DEBUG_SCHEDULER
const char *item_name = item->get_name();
ESP_LOGV(TAG, "Running %s '%s/%s' with interval=%" PRIu32 " next_execution=%" PRIu64 " (now=%" PRIu64 ")",
const Time48 next_exec_dbg = item->get_next_execution();
ESP_LOGV(TAG,
"Running %s '%s/%s' with interval=%" PRIu32 " next_execution=(%" PRIu16 ", %" PRIu32 ") now=(%" PRIu16
", %" PRIu32 ")",
item->get_type_str(), LOG_STR_ARG(item->get_source()), item_name ? item_name : "(null)", item->interval,
item->get_next_execution(), now_64);
next_exec_dbg.major, next_exec_dbg.millis, now_48.major, now_48.millis);
#endif /* ESPHOME_DEBUG_SCHEDULER */
// Warning: During callback(), a lot of stuff can happen, including:
@@ -454,7 +451,7 @@ void HOT Scheduler::call(uint32_t now) {
}
if (executed_item->type == SchedulerItem::INTERVAL) {
executed_item->set_next_execution(now_64 + executed_item->interval);
executed_item->set_next_execution(now_48 + executed_item->interval);
// Add new item directly to to_add_
// since we have the lock held
this->to_add_.push_back(std::move(executed_item));
@@ -579,7 +576,7 @@ bool HOT Scheduler::cancel_item_locked_(Component *component, const char *name_c
return total_cancelled > 0;
}
uint64_t Scheduler::millis_64_(uint32_t now) {
Time48 Scheduler::millis_48_(uint32_t now) {
// THREAD SAFETY NOTE:
// This function has three implementations, based on the precompiler flags
// - ESPHOME_THREAD_SINGLE - Runs on single-threaded platforms (ESP8266, RP2040, etc.)
@@ -615,8 +612,7 @@ uint64_t Scheduler::millis_64_(uint32_t now) {
this->last_millis_ = now;
}
// Combine major (high 32 bits) and now (low 32 bits) into 64-bit time
return now + (static_cast<uint64_t>(major) << 32);
return {now, major};
#elif defined(ESPHOME_THREAD_MULTI_NO_ATOMICS)
// This is the multi core no atomics implementation.
@@ -664,8 +660,7 @@ uint64_t Scheduler::millis_64_(uint32_t 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<uint64_t>(major) << 32);
return {now, major};
#elif defined(ESPHOME_THREAD_MULTI_ATOMICS)
// This is the multi core with atomics implementation.
@@ -725,7 +720,7 @@ uint64_t Scheduler::millis_64_(uint32_t now) {
}
uint16_t major_end = this->millis_major_.load(std::memory_order_relaxed);
if (major_end == major)
return now + (static_cast<uint64_t>(major) << 32);
return {now, major};
}
// Unreachable - the loop always returns when major_end == major
__builtin_unreachable();
@@ -738,10 +733,7 @@ uint64_t Scheduler::millis_64_(uint32_t now) {
bool HOT Scheduler::SchedulerItem::cmp(const std::unique_ptr<SchedulerItem> &a,
const std::unique_ptr<SchedulerItem> &b) {
// 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_);
return a->get_next_execution() > b->get_next_execution();
}
void Scheduler::recycle_item_main_loop_(std::unique_ptr<SchedulerItem> item) {
@@ -767,7 +759,7 @@ void Scheduler::recycle_item_main_loop_(std::unique_ptr<SchedulerItem> item) {
#ifdef ESPHOME_DEBUG_SCHEDULER
void Scheduler::debug_log_timer_(const SchedulerItem *item, bool is_static_string, const char *name_cstr,
SchedulerItem::Type type, uint32_t delay, uint64_t now) {
SchedulerItem::Type type, uint32_t delay, Time48 now) {
// Validate static strings in debug mode
if (is_static_string && name_cstr != nullptr) {
validate_static_string(name_cstr);
@@ -780,8 +772,7 @@ void Scheduler::debug_log_timer_(const SchedulerItem *item, bool is_static_strin
name_cstr ? name_cstr : "(null)", type_str, delay);
} else {
ESP_LOGD(TAG, "set_%s(name='%s/%s', %s=%" PRIu32 ", offset=%" PRIu32 ")", type_str, LOG_STR_ARG(item->get_source()),
name_cstr ? name_cstr : "(null)", type_str, delay,
static_cast<uint32_t>(item->get_next_execution() - now));
name_cstr ? name_cstr : "(null)", type_str, delay, item->get_next_execution() - now);
}
}
#endif /* ESPHOME_DEBUG_SCHEDULER */

View File

@@ -13,6 +13,49 @@
namespace esphome {
// 48-bit timestamp for scheduler operations.
// Uses 32-bit math with manual carry detection to avoid expensive 64-bit
// emulation on ESP8266 and other 32-bit platforms without native 64-bit support.
struct Time48 {
uint32_t millis{0}; // Low 32 bits - milliseconds within current epoch
uint16_t major{0}; // High 16 bits - rollover counter (~49.7 days each)
constexpr Time48() = default;
constexpr Time48(uint32_t millis, uint16_t major) : millis(millis), major(major) {}
// Add a 32-bit offset (delay/interval)
constexpr Time48 operator+(uint32_t offset) const {
uint32_t new_millis = millis + offset;
uint16_t new_major = major;
if (new_millis < millis)
new_major++; // Overflow occurred
return {new_millis, new_major};
}
// Compare operators - optimized for 32-bit platforms
// Written to generate same control flow as GCC's native 64-bit comparison:
// Uses cascading < comparisons instead of != to produce tighter branch sequences
constexpr bool operator<(const Time48 &other) const {
if (major < other.major)
return true;
if (other.major < major)
return false;
return millis < other.millis;
}
constexpr bool operator>(const Time48 &other) const { return other < *this; }
constexpr bool operator<=(const Time48 &other) const { return !(other < *this); }
constexpr bool operator>=(const Time48 &other) const { return !(*this < other); }
// Subtract two Time48 values, returning 32-bit result
// PRECONDITION: *this >= other AND difference fits in 32 bits
// This is always true for scheduler since max delay is ~49 days
constexpr uint32_t operator-(const Time48 &other) const {
// Two's complement subtraction works even across major boundary
// when the result is known to fit in 32 bits
return millis - other.millis;
}
};
class Component;
struct RetryArgs;
@@ -94,15 +137,13 @@ class Scheduler {
} 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)
// with a 16-bit rollover counter to create a 48-bit time space. 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_millis_; // Lower 32 bits of execution time (millis value)
std::function<void()> callback;
uint16_t next_execution_high_; // Upper 16 bits (millis_major counter)
uint16_t next_execution_major_; // Upper 16 bits (millis_major counter)
#ifdef ESPHOME_THREAD_MULTI_ATOMICS
// Multi-threaded with atomics: use atomic for lock-free access
@@ -128,8 +169,8 @@ class Scheduler {
SchedulerItem()
: component(nullptr),
interval(0),
next_execution_low_(0),
next_execution_high_(0),
next_execution_millis_(0),
next_execution_major_(0),
#ifdef ESPHOME_THREAD_MULTI_ATOMICS
// remove is initialized in the member declaration as std::atomic<bool>{false}
type(TIMEOUT),
@@ -189,18 +230,11 @@ class Scheduler {
static bool cmp(const std::unique_ptr<SchedulerItem> &a, const std::unique_ptr<SchedulerItem> &b);
// 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<uint64_t>(next_execution_high_) << 32) | next_execution_low_;
}
constexpr Time48 get_next_execution() const { return {next_execution_millis_, next_execution_major_}; }
constexpr void set_next_execution(uint64_t value) {
next_execution_low_ = static_cast<uint32_t>(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<uint16_t>(value >> 32);
constexpr void set_next_execution(Time48 value) {
next_execution_millis_ = value.millis;
next_execution_major_ = value.major;
}
constexpr const char *get_type_str() const { return (type == TIMEOUT) ? "timeout" : "interval"; }
const LogString *get_source() const { return component ? component->get_component_log_str() : LOG_STR("unknown"); }
@@ -214,7 +248,7 @@ class Scheduler {
void set_retry_common_(Component *component, bool is_static_string, const void *name_ptr, uint32_t initial_wait_time,
uint8_t max_attempts, std::function<RetryResult(uint8_t)> func, float backoff_increase_factor);
uint64_t millis_64_(uint32_t now);
Time48 millis_48_(uint32_t now);
// Cleanup logically deleted items from the scheduler
// Returns the number of items remaining after cleanup
// IMPORTANT: This method should only be called from the main thread (loop task).
@@ -283,7 +317,7 @@ class Scheduler {
#ifdef ESPHOME_DEBUG_SCHEDULER
// Helper for debug logging in set_timer_common_ - extracted to reduce code size
void debug_log_timer_(const SchedulerItem *item, bool is_static_string, const char *name_cstr,
SchedulerItem::Type type, uint32_t delay, uint64_t now);
SchedulerItem::Type type, uint32_t delay, Time48 now);
#endif /* ESPHOME_DEBUG_SCHEDULER */
#ifndef ESPHOME_THREAD_SINGLE