From 27d7b2caec9a9e79779c9c637ffe013ab4745952 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 30 Apr 2026 11:58:44 -0500 Subject: [PATCH 1/2] [core] Drop static on shrink_scheduler_vector_ for clang-tidy compliance Project's .clang-tidy applies different naming rules to static methods (ClassMethodCase, no suffix) vs instance methods (PrivateMethodCase / ProtectedMethodCase, _ suffix required). The helper had a trailing _ but was static, so clang-tidy flagged it. Drop static -- the helper is already noinline'd and called only at trim time, so the hidden this arg is free in any meaningful sense. --- esphome/core/scheduler.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/core/scheduler.h b/esphome/core/scheduler.h index ac8ef724c2..b640aa86fe 100644 --- a/esphome/core/scheduler.h +++ b/esphome/core/scheduler.h @@ -367,7 +367,7 @@ class Scheduler { private: // Out-of-line helper that shrinks a SchedulerItem* vector's capacity to its current // size. Centralised so trim_freelist() doesn't pay flash cost per call site. - static void shrink_scheduler_vector_(std::vector *v); + void shrink_scheduler_vector_(std::vector *v); // Helper to cancel matching items - must be called with lock held. // When find_first=true, stops after the first match (used by set_timer_common_ where From 0651a905bcb25e0dcc2926899b4bdac747b61229 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 30 Apr 2026 12:02:55 -0500 Subject: [PATCH 2/2] [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 ccbdf3dc05..a7c624486d 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() {