mirror of
https://github.com/esphome/esphome.git
synced 2025-09-07 05:42:20 +01:00
[scheduler] Reduce SchedulerItem memory usage by 7.4% on 32-bit platforms
This commit is contained in:
@@ -14,7 +14,7 @@ namespace esphome {
|
|||||||
|
|
||||||
static const char *const TAG = "scheduler";
|
static const char *const TAG = "scheduler";
|
||||||
|
|
||||||
static const uint32_t MAX_LOGICALLY_DELETED_ITEMS = 10;
|
static constexpr uint32_t MAX_LOGICALLY_DELETED_ITEMS = 10;
|
||||||
// Half the 32-bit range - used to detect rollovers vs normal time progression
|
// Half the 32-bit range - used to detect rollovers vs normal time progression
|
||||||
static constexpr uint32_t HALF_MAX_UINT32 = std::numeric_limits<uint32_t>::max() / 2;
|
static constexpr uint32_t HALF_MAX_UINT32 = std::numeric_limits<uint32_t>::max() / 2;
|
||||||
// max delay to start an interval sequence
|
// max delay to start an interval sequence
|
||||||
@@ -117,12 +117,12 @@ void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type
|
|||||||
// first execution happens immediately after a random smallish offset
|
// first execution happens immediately after a random smallish offset
|
||||||
// Calculate random offset (0 to min(interval/2, 5s))
|
// Calculate random offset (0 to min(interval/2, 5s))
|
||||||
uint32_t offset = (uint32_t) (std::min(delay / 2, MAX_INTERVAL_DELAY) * random_float());
|
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,
|
ESP_LOGV(TAG, "Scheduler interval for %s is %" PRIu32 "ms, offset %" PRIu32 "ms", name_cstr ? name_cstr : "", delay,
|
||||||
offset);
|
offset);
|
||||||
} else {
|
} else {
|
||||||
item->interval = 0;
|
item->interval = 0;
|
||||||
item->next_execution_ = now + delay;
|
item->set_next_execution(now + delay);
|
||||||
}
|
}
|
||||||
|
|
||||||
#ifdef ESPHOME_DEBUG_SCHEDULER
|
#ifdef ESPHOME_DEBUG_SCHEDULER
|
||||||
@@ -138,7 +138,8 @@ void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type
|
|||||||
name_cstr ? name_cstr : "(null)", type_str, delay);
|
name_cstr ? name_cstr : "(null)", type_str, delay);
|
||||||
} else {
|
} else {
|
||||||
ESP_LOGD(TAG, "set_%s(name='%s/%s', %s=%" PRIu32 ", offset=%" PRIu32 ")", type_str, item->get_source(),
|
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<uint32_t>(item->next_execution_ - now));
|
name_cstr ? name_cstr : "(null)", type_str, delay,
|
||||||
|
static_cast<uint32_t>(item->get_next_execution() - now));
|
||||||
}
|
}
|
||||||
#endif /* ESPHOME_DEBUG_SCHEDULER */
|
#endif /* ESPHOME_DEBUG_SCHEDULER */
|
||||||
|
|
||||||
@@ -285,9 +286,10 @@ optional<uint32_t> HOT Scheduler::next_schedule_in(uint32_t now) {
|
|||||||
auto &item = this->items_[0];
|
auto &item = this->items_[0];
|
||||||
// Convert the fresh timestamp from caller (usually Application::loop()) to 64-bit
|
// 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 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 0;
|
||||||
return item->next_execution_ - now_64;
|
return next_exec - now_64;
|
||||||
}
|
}
|
||||||
void HOT Scheduler::call(uint32_t now) {
|
void HOT Scheduler::call(uint32_t now) {
|
||||||
#ifndef ESPHOME_THREAD_SINGLE
|
#ifndef ESPHOME_THREAD_SINGLE
|
||||||
@@ -354,7 +356,7 @@ void HOT Scheduler::call(uint32_t now) {
|
|||||||
const char *name = item->get_name();
|
const char *name = item->get_name();
|
||||||
ESP_LOGD(TAG, " %s '%s/%s' interval=%" PRIu32 " next_execution in %" PRIu64 "ms at %" PRIu64,
|
ESP_LOGD(TAG, " %s '%s/%s' interval=%" PRIu32 " next_execution in %" PRIu64 "ms at %" PRIu64,
|
||||||
item->get_type_str(), item->get_source(), name ? name : "(null)", item->interval,
|
item->get_type_str(), item->get_source(), name ? name : "(null)", item->interval,
|
||||||
item->next_execution_ - now_64, item->next_execution_);
|
item->get_next_execution() - now_64, item->get_next_execution());
|
||||||
|
|
||||||
old_items.push_back(std::move(item));
|
old_items.push_back(std::move(item));
|
||||||
}
|
}
|
||||||
@@ -401,7 +403,7 @@ void HOT Scheduler::call(uint32_t now) {
|
|||||||
{
|
{
|
||||||
// Don't copy-by value yet
|
// Don't copy-by value yet
|
||||||
auto &item = this->items_[0];
|
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
|
// Not reached timeout yet, done for this call
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -440,7 +442,7 @@ void HOT Scheduler::call(uint32_t now) {
|
|||||||
const char *item_name = item->get_name();
|
const char *item_name = item->get_name();
|
||||||
ESP_LOGV(TAG, "Running %s '%s/%s' with interval=%" PRIu32 " next_execution=%" PRIu64 " (now=%" PRIu64 ")",
|
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->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 */
|
#endif /* ESPHOME_DEBUG_SCHEDULER */
|
||||||
|
|
||||||
// Warning: During callback(), a lot of stuff can happen, including:
|
// Warning: During callback(), a lot of stuff can happen, including:
|
||||||
@@ -465,7 +467,7 @@ void HOT Scheduler::call(uint32_t now) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (item->type == SchedulerItem::INTERVAL) {
|
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_
|
// Add new item directly to to_add_
|
||||||
// since we have the lock held
|
// since we have the lock held
|
||||||
this->to_add_.push_back(std::move(item));
|
this->to_add_.push_back(std::move(item));
|
||||||
@@ -744,7 +746,10 @@ uint64_t Scheduler::millis_64_(uint32_t now) {
|
|||||||
|
|
||||||
bool HOT Scheduler::SchedulerItem::cmp(const std::unique_ptr<SchedulerItem> &a,
|
bool HOT Scheduler::SchedulerItem::cmp(const std::unique_ptr<SchedulerItem> &a,
|
||||||
const std::unique_ptr<SchedulerItem> &b) {
|
const std::unique_ptr<SchedulerItem> &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_);
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace esphome
|
} // namespace esphome
|
||||||
|
@@ -88,19 +88,19 @@ class Scheduler {
|
|||||||
struct SchedulerItem {
|
struct SchedulerItem {
|
||||||
// Ordered by size to minimize padding
|
// Ordered by size to minimize padding
|
||||||
Component *component;
|
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
|
// Optimized name storage using tagged union
|
||||||
union {
|
union {
|
||||||
const char *static_name; // For string literals (no allocation)
|
const char *static_name; // For string literals (no allocation)
|
||||||
char *dynamic_name; // For allocated strings
|
char *dynamic_name; // For allocated strings
|
||||||
} name_;
|
} name_;
|
||||||
|
uint32_t interval;
|
||||||
|
// Split 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.
|
||||||
|
// Split into two fields for better memory alignment on 32-bit systems.
|
||||||
|
uint32_t next_execution_low_; // Lower 32 bits of next execution time
|
||||||
std::function<void()> callback;
|
std::function<void()> callback;
|
||||||
|
uint16_t next_execution_high_; // Upper 16 bits of next execution time
|
||||||
|
|
||||||
#ifdef ESPHOME_THREAD_MULTI_ATOMICS
|
#ifdef ESPHOME_THREAD_MULTI_ATOMICS
|
||||||
// Multi-threaded with atomics: use atomic for lock-free access
|
// Multi-threaded with atomics: use atomic for lock-free access
|
||||||
@@ -126,7 +126,8 @@ class Scheduler {
|
|||||||
SchedulerItem()
|
SchedulerItem()
|
||||||
: component(nullptr),
|
: component(nullptr),
|
||||||
interval(0),
|
interval(0),
|
||||||
next_execution_(0),
|
next_execution_low_(0),
|
||||||
|
next_execution_high_(0),
|
||||||
#ifdef ESPHOME_THREAD_MULTI_ATOMICS
|
#ifdef ESPHOME_THREAD_MULTI_ATOMICS
|
||||||
// remove is initialized in the member declaration as std::atomic<bool>{false}
|
// remove is initialized in the member declaration as std::atomic<bool>{false}
|
||||||
type(TIMEOUT),
|
type(TIMEOUT),
|
||||||
@@ -157,7 +158,7 @@ class Scheduler {
|
|||||||
SchedulerItem &operator=(SchedulerItem &&) = delete;
|
SchedulerItem &operator=(SchedulerItem &&) = delete;
|
||||||
|
|
||||||
// Helper to get the name regardless of storage type
|
// Helper to get the name regardless of storage type
|
||||||
const char *get_name() const { return name_is_dynamic ? name_.dynamic_name : name_.static_name; }
|
constexpr const char *get_name() const { return name_is_dynamic ? name_.dynamic_name : name_.static_name; }
|
||||||
|
|
||||||
// Helper to set name with proper ownership
|
// Helper to set name with proper ownership
|
||||||
void set_name(const char *name, bool make_copy = false) {
|
void set_name(const char *name, bool make_copy = false) {
|
||||||
@@ -183,7 +184,17 @@ class Scheduler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static bool cmp(const std::unique_ptr<SchedulerItem> &a, const std::unique_ptr<SchedulerItem> &b);
|
static bool cmp(const std::unique_ptr<SchedulerItem> &a, const std::unique_ptr<SchedulerItem> &b);
|
||||||
const char *get_type_str() const { return (type == TIMEOUT) ? "timeout" : "interval"; }
|
|
||||||
|
// Helper methods to work with split execution time (constexpr for optimization)
|
||||||
|
constexpr uint64_t get_next_execution() const {
|
||||||
|
return (static_cast<uint64_t>(next_execution_high_) << 32) | next_execution_low_;
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr void set_next_execution(uint64_t value) {
|
||||||
|
next_execution_low_ = static_cast<uint32_t>(value);
|
||||||
|
next_execution_high_ = static_cast<uint16_t>(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"; }
|
const char *get_source() const { return component ? component->get_component_source() : "unknown"; }
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user