[core] Trim scheduler freelist of post-boot peak 10s after setup

Boot churn (component init, first sensor reads, retries) inflates the freelist
beyond the steady-state high-water mark. Without a one-shot trim that peak
would be retained forever. Adds Scheduler::trim_freelist() and schedules it
from Application::setup() to fire SCHEDULER_FREELIST_TRIM_DELAY_MS (10s) after
setup completes -- well past the bulk of post-setup async work.

Items currently in items_/to_add_/defer_queue_ are untouched; only the
freelist's recycled items are deleted. Post-trim, the freelist regrows to
the new (post-startup) high-water mark.
This commit is contained in:
J. Nick Koston
2026-04-30 11:29:32 -05:00
parent 2a451cb870
commit d36c56d3f5
3 changed files with 36 additions and 0 deletions
+7
View File
@@ -25,6 +25,10 @@ namespace esphome {
static const char *const TAG = "app";
// Delay after setup() finishes before trimming the scheduler freelist of its post-boot peak.
// 10 s is well past the bulk of post-setup async work (Wi-Fi/MQTT connects, first-read latency).
static constexpr uint32_t SCHEDULER_FREELIST_TRIM_DELAY_MS = 10000;
// Helper function for insertion sort of components by priority
// Using insertion sort instead of std::stable_sort saves ~1.3KB of flash
// by avoiding template instantiations (std::rotate, std::stable_sort, lambdas)
@@ -112,6 +116,9 @@ void Application::setup() {
ESP_LOGI(TAG, "setup() finished successfully!");
// Trim the scheduler freelist of its post-boot peak once startup churn settles.
this->scheduler.set_timeout(this, SCHEDULER_FREELIST_TRIM_DELAY_MS, [this]() { this->scheduler.trim_freelist(); });
#ifdef USE_SETUP_PRIORITY_OVERRIDE
// Clear setup priority overrides to free memory
clear_setup_priority_overrides();
+22
View File
@@ -899,6 +899,28 @@ void Scheduler::recycle_item_main_loop_(SchedulerItem *item) {
#endif
}
void Scheduler::trim_freelist() {
LockGuard guard{this->lock_};
SchedulerItem *item = this->scheduler_item_pool_head_;
size_t freed = 0;
while (item != nullptr) {
SchedulerItem *next = item->next_free;
delete item;
#ifdef ESPHOME_DEBUG_SCHEDULER
this->debug_live_items_--;
#endif
item = next;
freed++;
}
this->scheduler_item_pool_head_ = nullptr;
this->scheduler_item_pool_size_ = 0;
#ifdef ESPHOME_DEBUG_SCHEDULER
ESP_LOGD(TAG, "Freelist trimmed (%zu items freed)", freed);
#else
(void) freed;
#endif
}
#ifdef ESPHOME_DEBUG_SCHEDULER
void Scheduler::debug_log_timer_(const SchedulerItem *item, NameType name_type, const char *static_name,
uint32_t hash_or_id, SchedulerItem::Type type, uint32_t delay, uint64_t now) {
+7
View File
@@ -132,6 +132,13 @@ class Scheduler {
// @return Timestamp of the last item that ran, or `now` unchanged if none ran.
uint32_t call(uint32_t now);
// Free every SchedulerItem currently sitting in the recycle freelist. Items in
// items_/to_add_/defer_queue_ are untouched. Intended to claim the post-boot
// peak: boot churn (component setup, first sensor reads, retries) inflates the
// freelist beyond what steady-state needs, and without a one-shot trim that
// peak would be retained forever.
void trim_freelist();
// Move items from to_add_ into the main heap.
// IMPORTANT: This method should only be called from the main thread (loop task).
// Inlined: the fast path (nothing to add) is just an atomic load / empty check.