mirror of
https://github.com/esphome/esphome.git
synced 2026-01-13 21:47:40 -07:00
Compare commits
3 Commits
memory_api
...
scheduler_
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d91cad1636 | ||
|
|
54d0328002 | ||
|
|
865312ff60 |
@@ -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 */
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user