From 0651a905bcb25e0dcc2926899b4bdac747b61229 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 30 Apr 2026 12:02:55 -0500 Subject: [PATCH] [core] Avoid std::bad_array_new_length pull in shrink_scheduler_vector_ The classic std::vector swap-with-copy idiom (vector(other).swap(other)) instantiates the iterator-range copy constructor, which pulls in std::__throw_bad_array_new_length and the related typeinfo + vtable + dtor + what() method (~118 B of stdlib RTTI). Build into a temp via reserve + push_back instead, then move-assign: - reserve uses ::operator new (throws bad_alloc, already linked). - push_back without growth is the noexcept tail path. - move-assign just swaps pointers, no allocation. Same shrink semantics, saves ~128 B flash on ESP32. Also adds a fast-path early return for the case where capacity already equals size (common after a quiet period, since vector capacity only grows when crossing the doubling threshold). --- esphome/core/scheduler.cpp | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/esphome/core/scheduler.cpp b/esphome/core/scheduler.cpp index ccbdf3dc053..a7c624486db 100644 --- a/esphome/core/scheduler.cpp +++ b/esphome/core/scheduler.cpp @@ -899,12 +899,22 @@ void Scheduler::recycle_item_main_loop_(SchedulerItem *item) { #endif } -// Shrink a SchedulerItem* vector's capacity to its current size via the swap-with-copy -// idiom (std::vector::shrink_to_fit() is non-binding and our toolchain ignores it). -// Out-of-line + noinline so the callers in trim_freelist() share one body instead of -// inlining the construct-swap-destruct dance per call site (~440 B flash on ESP32). +// Shrink a SchedulerItem* vector's capacity to its current size. +// std::vector::shrink_to_fit() is non-binding and our toolchain ignores it; the classic +// swap-with-copy idiom (std::vector(other).swap(other)) instantiates the iterator-range +// constructor which pulls in std::__throw_bad_array_new_length and ~120 B of related +// stdlib RTTI/typeinfo. Build into a temp via reserve + push_back instead, then move-assign: +// reserve uses operator new (throws bad_alloc, already linked) and push_back without growth +// is the noexcept tail path. Move-assign just swaps pointers. +// Out-of-line + noinline so the callers in trim_freelist() share one body. void __attribute__((noinline)) Scheduler::shrink_scheduler_vector_(std::vector *v) { - std::vector(*v).swap(*v); + if (v->capacity() == v->size()) + return; // already exact, common after a quiet period + std::vector tmp; + tmp.reserve(v->size()); + for (SchedulerItem *p : *v) + tmp.push_back(p); + *v = std::move(tmp); } void Scheduler::trim_freelist() {