From dcbb0204794d672fcfc66f4174bb4ca0a1e9dc9f Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Mon, 9 Feb 2026 20:02:41 -0500 Subject: [PATCH 1/2] [uart] Fix available() return type to size_t across components (#13898) Co-authored-by: Claude Opus 4.6 --- esphome/components/cse7766/cse7766.cpp | 6 +++--- esphome/components/dfplayer/dfplayer.cpp | 4 ++-- esphome/components/dsmr/dsmr.cpp | 12 ++++++------ esphome/components/ld2410/ld2410.cpp | 4 ++-- esphome/components/ld2412/ld2412.cpp | 4 ++-- esphome/components/ld2450/ld2450.cpp | 4 ++-- esphome/components/modbus/modbus.cpp | 4 ++-- esphome/components/nextion/nextion.cpp | 4 ++-- esphome/components/pipsolar/pipsolar.cpp | 8 ++++---- esphome/components/pylontech/pylontech.cpp | 4 ++-- esphome/components/rd03d/rd03d.cpp | 4 ++-- esphome/components/rf_bridge/rf_bridge.cpp | 4 ++-- esphome/components/seeed_mr24hpc1/seeed_mr24hpc1.cpp | 4 ++-- esphome/components/seeed_mr60bha2/seeed_mr60bha2.cpp | 4 ++-- esphome/components/seeed_mr60fda2/seeed_mr60fda2.cpp | 4 ++-- esphome/components/tuya/tuya.cpp | 4 ++-- 16 files changed, 39 insertions(+), 39 deletions(-) diff --git a/esphome/components/cse7766/cse7766.cpp b/esphome/components/cse7766/cse7766.cpp index 45abd3ca3d..7ffdf757a0 100644 --- a/esphome/components/cse7766/cse7766.cpp +++ b/esphome/components/cse7766/cse7766.cpp @@ -16,8 +16,8 @@ void CSE7766Component::loop() { } // Early return prevents updating last_transmission_ when no data is available. - int avail = this->available(); - if (avail <= 0) { + size_t avail = this->available(); + if (avail == 0) { return; } @@ -27,7 +27,7 @@ void CSE7766Component::loop() { // At 4800 baud (~480 bytes/sec) with ~122 Hz loop rate, typically ~4 bytes per call. uint8_t buf[CSE7766_RAW_DATA_SIZE]; while (avail > 0) { - size_t to_read = std::min(static_cast(avail), sizeof(buf)); + size_t to_read = std::min(avail, sizeof(buf)); if (!this->read_array(buf, to_read)) { break; } diff --git a/esphome/components/dfplayer/dfplayer.cpp b/esphome/components/dfplayer/dfplayer.cpp index 48c06be558..79f8fd03c3 100644 --- a/esphome/components/dfplayer/dfplayer.cpp +++ b/esphome/components/dfplayer/dfplayer.cpp @@ -133,10 +133,10 @@ void DFPlayer::send_cmd_(uint8_t cmd, uint16_t argument) { void DFPlayer::loop() { // Read all available bytes in batches to reduce UART call overhead. - int avail = this->available(); + size_t avail = this->available(); uint8_t buf[64]; while (avail > 0) { - size_t to_read = std::min(static_cast(avail), sizeof(buf)); + size_t to_read = std::min(avail, sizeof(buf)); if (!this->read_array(buf, to_read)) { break; } diff --git a/esphome/components/dsmr/dsmr.cpp b/esphome/components/dsmr/dsmr.cpp index 20fbd20cd6..baf7f59314 100644 --- a/esphome/components/dsmr/dsmr.cpp +++ b/esphome/components/dsmr/dsmr.cpp @@ -120,9 +120,9 @@ void Dsmr::stop_requesting_data_() { void Dsmr::drain_rx_buffer_() { uint8_t buf[64]; - int avail; + size_t avail; while ((avail = this->available()) > 0) { - if (!this->read_array(buf, std::min(static_cast(avail), sizeof(buf)))) { + if (!this->read_array(buf, std::min(avail, sizeof(buf)))) { break; } } @@ -140,9 +140,9 @@ void Dsmr::receive_telegram_() { while (this->available_within_timeout_()) { // Read all available bytes in batches to reduce UART call overhead. uint8_t buf[64]; - int avail = this->available(); + size_t avail = this->available(); while (avail > 0) { - size_t to_read = std::min(static_cast(avail), sizeof(buf)); + size_t to_read = std::min(avail, sizeof(buf)); if (!this->read_array(buf, to_read)) return; avail -= to_read; @@ -206,9 +206,9 @@ void Dsmr::receive_encrypted_telegram_() { while (this->available_within_timeout_()) { // Read all available bytes in batches to reduce UART call overhead. uint8_t buf[64]; - int avail = this->available(); + size_t avail = this->available(); while (avail > 0) { - size_t to_read = std::min(static_cast(avail), sizeof(buf)); + size_t to_read = std::min(avail, sizeof(buf)); if (!this->read_array(buf, to_read)) return; avail -= to_read; diff --git a/esphome/components/ld2410/ld2410.cpp b/esphome/components/ld2410/ld2410.cpp index b57b1d9978..95a04f768a 100644 --- a/esphome/components/ld2410/ld2410.cpp +++ b/esphome/components/ld2410/ld2410.cpp @@ -276,10 +276,10 @@ void LD2410Component::restart_and_read_all_info() { void LD2410Component::loop() { // Read all available bytes in batches to reduce UART call overhead. - int avail = this->available(); + size_t avail = this->available(); uint8_t buf[MAX_LINE_LENGTH]; while (avail > 0) { - size_t to_read = std::min(static_cast(avail), sizeof(buf)); + size_t to_read = std::min(avail, sizeof(buf)); if (!this->read_array(buf, to_read)) { break; } diff --git a/esphome/components/ld2412/ld2412.cpp b/esphome/components/ld2412/ld2412.cpp index f8ceee78eb..95e19e0d5f 100644 --- a/esphome/components/ld2412/ld2412.cpp +++ b/esphome/components/ld2412/ld2412.cpp @@ -311,10 +311,10 @@ void LD2412Component::restart_and_read_all_info() { void LD2412Component::loop() { // Read all available bytes in batches to reduce UART call overhead. - int avail = this->available(); + size_t avail = this->available(); uint8_t buf[MAX_LINE_LENGTH]; while (avail > 0) { - size_t to_read = std::min(static_cast(avail), sizeof(buf)); + size_t to_read = std::min(avail, sizeof(buf)); if (!this->read_array(buf, to_read)) { break; } diff --git a/esphome/components/ld2450/ld2450.cpp b/esphome/components/ld2450/ld2450.cpp index 38ba0d7f96..b04b509a16 100644 --- a/esphome/components/ld2450/ld2450.cpp +++ b/esphome/components/ld2450/ld2450.cpp @@ -277,10 +277,10 @@ void LD2450Component::dump_config() { void LD2450Component::loop() { // Read all available bytes in batches to reduce UART call overhead. - int avail = this->available(); + size_t avail = this->available(); uint8_t buf[MAX_LINE_LENGTH]; while (avail > 0) { - size_t to_read = std::min(static_cast(avail), sizeof(buf)); + size_t to_read = std::min(avail, sizeof(buf)); if (!this->read_array(buf, to_read)) { break; } diff --git a/esphome/components/modbus/modbus.cpp b/esphome/components/modbus/modbus.cpp index c1f5635028..d40343db33 100644 --- a/esphome/components/modbus/modbus.cpp +++ b/esphome/components/modbus/modbus.cpp @@ -20,10 +20,10 @@ void Modbus::loop() { const uint32_t now = App.get_loop_component_start_time(); // Read all available bytes in batches to reduce UART call overhead. - int avail = this->available(); + size_t avail = this->available(); uint8_t buf[64]; while (avail > 0) { - size_t to_read = std::min(static_cast(avail), sizeof(buf)); + size_t to_read = std::min(avail, sizeof(buf)); if (!this->read_array(buf, to_read)) { break; } diff --git a/esphome/components/nextion/nextion.cpp b/esphome/components/nextion/nextion.cpp index 56bbc840fb..9f1ce47837 100644 --- a/esphome/components/nextion/nextion.cpp +++ b/esphome/components/nextion/nextion.cpp @@ -398,10 +398,10 @@ bool Nextion::remove_from_q_(bool report_empty) { void Nextion::process_serial_() { // Read all available bytes in batches to reduce UART call overhead. - int avail = this->available(); + size_t avail = this->available(); uint8_t buf[64]; while (avail > 0) { - size_t to_read = std::min(static_cast(avail), sizeof(buf)); + size_t to_read = std::min(avail, sizeof(buf)); if (!this->read_array(buf, to_read)) { break; } diff --git a/esphome/components/pipsolar/pipsolar.cpp b/esphome/components/pipsolar/pipsolar.cpp index d7b37f6130..e6831ad19e 100644 --- a/esphome/components/pipsolar/pipsolar.cpp +++ b/esphome/components/pipsolar/pipsolar.cpp @@ -14,9 +14,9 @@ void Pipsolar::setup() { void Pipsolar::empty_uart_buffer_() { uint8_t buf[64]; - int avail; + size_t avail; while ((avail = this->available()) > 0) { - if (!this->read_array(buf, std::min(static_cast(avail), sizeof(buf)))) { + if (!this->read_array(buf, std::min(avail, sizeof(buf)))) { break; } } @@ -97,10 +97,10 @@ void Pipsolar::loop() { } if (this->state_ == STATE_COMMAND || this->state_ == STATE_POLL) { - int avail = this->available(); + size_t avail = this->available(); while (avail > 0) { uint8_t buf[64]; - size_t to_read = std::min(static_cast(avail), sizeof(buf)); + size_t to_read = std::min(avail, sizeof(buf)); if (!this->read_array(buf, to_read)) { break; } diff --git a/esphome/components/pylontech/pylontech.cpp b/esphome/components/pylontech/pylontech.cpp index d724253256..7eb89d5b32 100644 --- a/esphome/components/pylontech/pylontech.cpp +++ b/esphome/components/pylontech/pylontech.cpp @@ -56,14 +56,14 @@ void PylontechComponent::setup() { void PylontechComponent::update() { this->write_str("pwr\n"); } void PylontechComponent::loop() { - int avail = this->available(); + size_t avail = this->available(); if (avail > 0) { // pylontech sends a lot of data very suddenly // we need to quickly put it all into our own buffer, otherwise the uart's buffer will overflow int recv = 0; uint8_t buf[64]; while (avail > 0) { - size_t to_read = std::min(static_cast(avail), sizeof(buf)); + size_t to_read = std::min(avail, sizeof(buf)); if (!this->read_array(buf, to_read)) { break; } diff --git a/esphome/components/rd03d/rd03d.cpp b/esphome/components/rd03d/rd03d.cpp index e4dbdf41cb..d47347fcfa 100644 --- a/esphome/components/rd03d/rd03d.cpp +++ b/esphome/components/rd03d/rd03d.cpp @@ -82,10 +82,10 @@ void RD03DComponent::dump_config() { void RD03DComponent::loop() { // Read all available bytes in batches to reduce UART call overhead. - int avail = this->available(); + size_t avail = this->available(); uint8_t buf[64]; while (avail > 0) { - size_t to_read = std::min(static_cast(avail), sizeof(buf)); + size_t to_read = std::min(avail, sizeof(buf)); if (!this->read_array(buf, to_read)) { break; } diff --git a/esphome/components/rf_bridge/rf_bridge.cpp b/esphome/components/rf_bridge/rf_bridge.cpp index e33c13aafe..d8c148145c 100644 --- a/esphome/components/rf_bridge/rf_bridge.cpp +++ b/esphome/components/rf_bridge/rf_bridge.cpp @@ -136,10 +136,10 @@ void RFBridgeComponent::loop() { this->last_bridge_byte_ = now; } - int avail = this->available(); + size_t avail = this->available(); while (avail > 0) { uint8_t buf[64]; - size_t to_read = std::min(static_cast(avail), sizeof(buf)); + size_t to_read = std::min(avail, sizeof(buf)); if (!this->read_array(buf, to_read)) { break; } diff --git a/esphome/components/seeed_mr24hpc1/seeed_mr24hpc1.cpp b/esphome/components/seeed_mr24hpc1/seeed_mr24hpc1.cpp index 3f2103b401..99d519b434 100644 --- a/esphome/components/seeed_mr24hpc1/seeed_mr24hpc1.cpp +++ b/esphome/components/seeed_mr24hpc1/seeed_mr24hpc1.cpp @@ -107,10 +107,10 @@ void MR24HPC1Component::update_() { // main loop void MR24HPC1Component::loop() { // Read all available bytes in batches to reduce UART call overhead. - int avail = this->available(); + size_t avail = this->available(); uint8_t buf[64]; while (avail > 0) { - size_t to_read = std::min(static_cast(avail), sizeof(buf)); + size_t to_read = std::min(avail, sizeof(buf)); if (!this->read_array(buf, to_read)) { break; } diff --git a/esphome/components/seeed_mr60bha2/seeed_mr60bha2.cpp b/esphome/components/seeed_mr60bha2/seeed_mr60bha2.cpp index d95e13241d..12f188fe03 100644 --- a/esphome/components/seeed_mr60bha2/seeed_mr60bha2.cpp +++ b/esphome/components/seeed_mr60bha2/seeed_mr60bha2.cpp @@ -31,10 +31,10 @@ void MR60BHA2Component::dump_config() { // main loop void MR60BHA2Component::loop() { // Read all available bytes in batches to reduce UART call overhead. - int avail = this->available(); + size_t avail = this->available(); uint8_t buf[64]; while (avail > 0) { - size_t to_read = std::min(static_cast(avail), sizeof(buf)); + size_t to_read = std::min(avail, sizeof(buf)); if (!this->read_array(buf, to_read)) { break; } diff --git a/esphome/components/seeed_mr60fda2/seeed_mr60fda2.cpp b/esphome/components/seeed_mr60fda2/seeed_mr60fda2.cpp index 441ee2b5c2..5d571618d3 100644 --- a/esphome/components/seeed_mr60fda2/seeed_mr60fda2.cpp +++ b/esphome/components/seeed_mr60fda2/seeed_mr60fda2.cpp @@ -50,10 +50,10 @@ void MR60FDA2Component::setup() { // main loop void MR60FDA2Component::loop() { // Read all available bytes in batches to reduce UART call overhead. - int avail = this->available(); + size_t avail = this->available(); uint8_t buf[64]; while (avail > 0) { - size_t to_read = std::min(static_cast(avail), sizeof(buf)); + size_t to_read = std::min(avail, sizeof(buf)); if (!this->read_array(buf, to_read)) { break; } diff --git a/esphome/components/tuya/tuya.cpp b/esphome/components/tuya/tuya.cpp index 9ee4c09b86..a1acbf2f56 100644 --- a/esphome/components/tuya/tuya.cpp +++ b/esphome/components/tuya/tuya.cpp @@ -32,10 +32,10 @@ void Tuya::setup() { void Tuya::loop() { // Read all available bytes in batches to reduce UART call overhead. - int avail = this->available(); + size_t avail = this->available(); uint8_t buf[64]; while (avail > 0) { - size_t to_read = std::min(static_cast(avail), sizeof(buf)); + size_t to_read = std::min(avail, sizeof(buf)); if (!this->read_array(buf, to_read)) { break; } From c1328f1b3a4fc56b1010d7fe34c435dba56b179e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 9 Feb 2026 19:24:15 -0600 Subject: [PATCH 2/2] [scheduler] Reduce set_timer_common_ hot path size by 25% MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Restructure set_timer_common_ to reduce icache pressure on the hot path (538 → 405 bytes on ESP32): - Extract calculate_interval_offset_ as noinline helper - float math and random_float() only needed for intervals, not timeouts - Extract is_retry_cancelled_locked_ as noinline helper - retry path is cold and deprecated (removal planned for 2026.8.0) - Merge duplicated cancel+push_back epilogue by computing a target vector pointer (defer_queue_ vs to_add_) before the branch, converging both paths at a single cancel+push sequence - Replace 4-way switch on name_type with unified set_name() method that does a single branch + store instead of 4 separate bitfield RMW sequences - Remove now-unused individual name setters (set_static_name, set_hashed_name, set_numeric_id, set_internal_id) --- esphome/core/scheduler.cpp | 106 ++++++++++++++++++------------------- esphome/core/scheduler.h | 42 +++++++-------- 2 files changed, 73 insertions(+), 75 deletions(-) diff --git a/esphome/core/scheduler.cpp b/esphome/core/scheduler.cpp index 97ac28b623..4194c3aa9e 100644 --- a/esphome/core/scheduler.cpp +++ b/esphome/core/scheduler.cpp @@ -107,6 +107,24 @@ static void validate_static_string(const char *name) { // iterating over them from the loop task is fine; but iterating from any other context requires the lock to be held to // avoid the main thread modifying the list while it is being accessed. +// Calculate random offset for interval timers +// Extracted from set_timer_common_ to reduce code size - float math + random_float() +// only needed for intervals, not timeouts +uint32_t Scheduler::calculate_interval_offset_(uint32_t delay) { + return static_cast(std::min(delay / 2, MAX_INTERVAL_DELAY) * random_float()); +} + +// Check if a retry was already cancelled in items_ or to_add_ +// Extracted from set_timer_common_ to reduce code size - retry path is cold and deprecated +// Remove before 2026.8.0 along with all retry code +bool Scheduler::is_retry_cancelled_locked_(Component *component, NameType name_type, const char *static_name, + uint32_t hash_or_id) { + return has_cancelled_timeout_in_container_locked_(this->items_, component, name_type, static_name, hash_or_id, + /* match_retry= */ true) || + has_cancelled_timeout_in_container_locked_(this->to_add_, component, name_type, static_name, hash_or_id, + /* match_retry= */ true); +} + // Common implementation for both timeout and interval // name_type determines storage type: STATIC_STRING uses static_name, others use hash_or_id void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type type, NameType name_type, @@ -130,84 +148,66 @@ void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type // Create and populate the scheduler item auto item = this->get_item_from_pool_locked_(); item->component = component; - switch (name_type) { - case NameType::STATIC_STRING: - item->set_static_name(static_name); - break; - case NameType::HASHED_STRING: - item->set_hashed_name(hash_or_id); - break; - case NameType::NUMERIC_ID: - item->set_numeric_id(hash_or_id); - break; - case NameType::NUMERIC_ID_INTERNAL: - item->set_internal_id(hash_or_id); - break; - } + item->set_name(name_type, static_name, hash_or_id); item->type = type; item->callback = std::move(func); // Reset remove flag - recycled items may have been cancelled (remove=true) in previous use this->set_item_removed_(item.get(), false); item->is_retry = is_retry; + // Determine target container: defer_queue_ for deferred items, to_add_ for everything else. + // Using a pointer lets both paths share the cancel + push_back epilogue. + auto *target = &this->to_add_; + #ifndef ESPHOME_THREAD_SINGLE // Special handling for defer() (delay = 0, type = TIMEOUT) // Single-core platforms don't need thread-safe defer handling if (delay == 0 && type == SchedulerItem::TIMEOUT) { // Put in defer queue for guaranteed FIFO execution - if (!skip_cancel) { - this->cancel_item_locked_(component, name_type, static_name, hash_or_id, type); - } - this->defer_queue_.push_back(std::move(item)); - return; - } + target = &this->defer_queue_; + } else #endif /* not ESPHOME_THREAD_SINGLE */ - - // Type-specific setup - if (type == SchedulerItem::INTERVAL) { - item->interval = delay; - // first execution happens immediately after a random smallish offset - // Calculate random offset (0 to min(interval/2, 5s)) - uint32_t offset = (uint32_t) (std::min(delay / 2, MAX_INTERVAL_DELAY) * random_float()); - item->set_next_execution(now + offset); + { + // Type-specific setup + if (type == SchedulerItem::INTERVAL) { + item->interval = delay; + // first execution happens immediately after a random smallish offset + uint32_t offset = this->calculate_interval_offset_(delay); + item->set_next_execution(now + offset); #ifdef ESPHOME_LOG_HAS_VERBOSE - SchedulerNameLog name_log; - ESP_LOGV(TAG, "Scheduler interval for %s is %" PRIu32 "ms, offset %" PRIu32 "ms", - name_log.format(name_type, static_name, hash_or_id), delay, offset); + SchedulerNameLog name_log; + ESP_LOGV(TAG, "Scheduler interval for %s is %" PRIu32 "ms, offset %" PRIu32 "ms", + name_log.format(name_type, static_name, hash_or_id), delay, offset); #endif - } else { - item->interval = 0; - item->set_next_execution(now + delay); - } + } else { + item->interval = 0; + item->set_next_execution(now + delay); + } #ifdef ESPHOME_DEBUG_SCHEDULER - this->debug_log_timer_(item.get(), name_type, static_name, hash_or_id, type, delay, now); + this->debug_log_timer_(item.get(), name_type, static_name, hash_or_id, type, delay, now); #endif /* ESPHOME_DEBUG_SCHEDULER */ - // For retries, check if there's a cancelled timeout first - // Skip check for anonymous retries (STATIC_STRING with nullptr) - they can't be cancelled by name - if (is_retry && (name_type != NameType::STATIC_STRING || static_name != nullptr) && type == SchedulerItem::TIMEOUT && - (has_cancelled_timeout_in_container_locked_(this->items_, component, name_type, static_name, hash_or_id, - /* match_retry= */ true) || - has_cancelled_timeout_in_container_locked_(this->to_add_, component, name_type, static_name, hash_or_id, - /* match_retry= */ true))) { - // Skip scheduling - the retry was cancelled + // For retries, check if there's a cancelled timeout first + // Skip check for anonymous retries (STATIC_STRING with nullptr) - they can't be cancelled by name + if (is_retry && (name_type != NameType::STATIC_STRING || static_name != nullptr) && + type == SchedulerItem::TIMEOUT && + this->is_retry_cancelled_locked_(component, name_type, static_name, hash_or_id)) { + // Skip scheduling - the retry was cancelled #ifdef ESPHOME_DEBUG_SCHEDULER - SchedulerNameLog skip_name_log; - ESP_LOGD(TAG, "Skipping retry '%s' - found cancelled item", - skip_name_log.format(name_type, static_name, hash_or_id)); + SchedulerNameLog skip_name_log; + ESP_LOGD(TAG, "Skipping retry '%s' - found cancelled item", + skip_name_log.format(name_type, static_name, hash_or_id)); #endif - return; + return; + } } - // If name is provided, do atomic cancel-and-add (unless skip_cancel is true) - // Cancel existing items + // Common epilogue: atomic cancel-and-add (unless skip_cancel is true) if (!skip_cancel) { this->cancel_item_locked_(component, name_type, static_name, hash_or_id, type); } - // Add new item directly to to_add_ - // since we have the lock held - this->to_add_.push_back(std::move(item)); + target->push_back(std::move(item)); } void HOT Scheduler::set_timeout(Component *component, const char *name, uint32_t timeout, std::function func) { diff --git a/esphome/core/scheduler.h b/esphome/core/scheduler.h index ede729d164..394178a831 100644 --- a/esphome/core/scheduler.h +++ b/esphome/core/scheduler.h @@ -219,28 +219,15 @@ class Scheduler { // Helper to get the name type NameType get_name_type() const { return name_type_; } - // Helper to set a static string name (no allocation) - void set_static_name(const char *name) { - name_.static_name = name; - name_type_ = NameType::STATIC_STRING; - } - - // Helper to set a hashed string name (hash computed from std::string) - void set_hashed_name(uint32_t hash) { - name_.hash_or_id = hash; - name_type_ = NameType::HASHED_STRING; - } - - // Helper to set a numeric ID name - void set_numeric_id(uint32_t id) { - name_.hash_or_id = id; - name_type_ = NameType::NUMERIC_ID; - } - - // Helper to set an internal numeric ID (separate namespace from NUMERIC_ID) - void set_internal_id(uint32_t id) { - name_.hash_or_id = id; - name_type_ = NameType::NUMERIC_ID_INTERNAL; + // Set name storage: for STATIC_STRING stores the pointer, for all other types stores hash_or_id. + // Both union members occupy the same offset, so only one store is needed. + void set_name(NameType type, const char *static_name, uint32_t hash_or_id) { + if (type == NameType::STATIC_STRING) { + name_.static_name = static_name; + } else { + name_.hash_or_id = hash_or_id; + } + name_type_ = type; } static bool cmp(const std::unique_ptr &a, const std::unique_ptr &b); @@ -355,6 +342,17 @@ class Scheduler { // Helper to perform full cleanup when too many items are cancelled void full_cleanup_removed_items_(); + // Helper to calculate random offset for interval timers - extracted to reduce code size of set_timer_common_ + // IMPORTANT: Must not be inlined - called only for intervals, keeping it out of the hot path saves flash. + uint32_t __attribute__((noinline)) calculate_interval_offset_(uint32_t delay); + + // Helper to check if a retry was already cancelled - extracted to reduce code size of set_timer_common_ + // Remove before 2026.8.0 along with all retry code. + // IMPORTANT: Must not be inlined - retry path is cold and deprecated. + // IMPORTANT: Caller must hold the scheduler lock before calling this function. + bool __attribute__((noinline)) + is_retry_cancelled_locked_(Component *component, NameType name_type, const char *static_name, uint32_t hash_or_id); + #ifdef ESPHOME_DEBUG_SCHEDULER // Helper for debug logging in set_timer_common_ - extracted to reduce code size void debug_log_timer_(const SchedulerItem *item, NameType name_type, const char *static_name, uint32_t hash_or_id,