ESPHome 2026.6.2
Loading...
Searching...
No Matches
scheduler.h
Go to the documentation of this file.
1#pragma once
2
4#include <cstring>
5#include <string>
6#include <vector>
7#ifdef ESPHOME_THREAD_MULTI_ATOMICS
8#include <atomic>
9#endif
10
12#include "esphome/core/hal.h"
15
16namespace esphome {
17
18class Component;
19struct RetryArgs;
20
21// Forward declaration of retry_handler - needs to be non-static for friend declaration
22void retry_handler(const std::shared_ptr<RetryArgs> &args);
23
24class Scheduler {
25 // Allow retry_handler to access protected members for internal retry mechanism
26 friend void ::esphome::retry_handler(const std::shared_ptr<RetryArgs> &args);
27 // Allow DelayAction to call set_timer_common_ with skip_cancel=true for parallel script delays.
28 // This is needed to fix issue #10264 where parallel scripts with delays interfere with each other.
29 // We use friend instead of a public API because skip_cancel is dangerous - it can cause delays
30 // to accumulate and overload the scheduler if misused.
31 template<typename... Ts> friend class DelayAction;
32
33 public:
34 // std::string overload - deprecated, use const char* or uint32_t instead
35 // Remove before 2026.7.0
36 ESPDEPRECATED("Use const char* or uint32_t overload instead. Removed in 2026.7.0", "2026.1.0")
37 void set_timeout(Component *component, const std::string &name, uint32_t timeout, std::function<void()> &&func);
38
47 void set_timeout(Component *component, const char *name, uint32_t timeout, std::function<void()> &&func);
49 void set_timeout(Component *component, uint32_t id, uint32_t timeout, std::function<void()> &&func);
51 void set_timeout(Component *component, InternalSchedulerID id, uint32_t timeout, std::function<void()> &&func) {
52 this->set_timer_common_(component, SchedulerItem::TIMEOUT, NameType::NUMERIC_ID_INTERNAL, nullptr,
53 static_cast<uint32_t>(id), timeout, std::move(func));
54 }
55
56 ESPDEPRECATED("Use const char* or uint32_t overload instead. Removed in 2026.7.0", "2026.1.0")
57 bool cancel_timeout(Component *component, const std::string &name);
58 bool cancel_timeout(Component *component, const char *name);
59 bool cancel_timeout(Component *component, uint32_t id);
60 bool cancel_timeout(Component *component, InternalSchedulerID id) {
61 return this->cancel_item_(component, NameType::NUMERIC_ID_INTERNAL, nullptr, static_cast<uint32_t>(id),
62 SchedulerItem::TIMEOUT);
63 }
64
65 ESPDEPRECATED("Use const char* or uint32_t overload instead. Removed in 2026.7.0", "2026.1.0")
66 void set_interval(Component *component, const std::string &name, uint32_t interval, std::function<void()> &&func);
67
76 void set_interval(Component *component, const char *name, uint32_t interval, std::function<void()> &&func);
78 void set_interval(Component *component, uint32_t id, uint32_t interval, std::function<void()> &&func);
80 void set_interval(Component *component, InternalSchedulerID id, uint32_t interval, std::function<void()> &&func) {
81 this->set_timer_common_(component, SchedulerItem::INTERVAL, NameType::NUMERIC_ID_INTERNAL, nullptr,
82 static_cast<uint32_t>(id), interval, std::move(func));
83 }
84
85 ESPDEPRECATED("Use const char* or uint32_t overload instead. Removed in 2026.7.0", "2026.1.0")
86 bool cancel_interval(Component *component, const std::string &name);
87 bool cancel_interval(Component *component, const char *name);
88 bool cancel_interval(Component *component, uint32_t id);
89 bool cancel_interval(Component *component, InternalSchedulerID id) {
90 return this->cancel_item_(component, NameType::NUMERIC_ID_INTERNAL, nullptr, static_cast<uint32_t>(id),
91 SchedulerItem::INTERVAL);
92 }
93
94 // Remove before 2026.8.0
95 ESPDEPRECATED("set_retry is deprecated and will be removed in 2026.8.0. Use set_timeout or set_interval instead.",
96 "2026.2.0")
97 void set_retry(Component *component, const std::string &name, uint32_t initial_wait_time, uint8_t max_attempts,
98 std::function<RetryResult(uint8_t)> func, float backoff_increase_factor = 1.0f);
99 // Remove before 2026.8.0
100 ESPDEPRECATED("set_retry is deprecated and will be removed in 2026.8.0. Use set_timeout or set_interval instead.",
101 "2026.2.0")
102 void set_retry(Component *component, const char *name, uint32_t initial_wait_time, uint8_t max_attempts,
103 std::function<RetryResult(uint8_t)> func, float backoff_increase_factor = 1.0f);
104 // Remove before 2026.8.0
105 ESPDEPRECATED("set_retry is deprecated and will be removed in 2026.8.0. Use set_timeout or set_interval instead.",
106 "2026.2.0")
107 void set_retry(Component *component, uint32_t id, uint32_t initial_wait_time, uint8_t max_attempts,
108 std::function<RetryResult(uint8_t)> func, float backoff_increase_factor = 1.0f);
109
110 // Remove before 2026.8.0
111 ESPDEPRECATED("cancel_retry is deprecated and will be removed in 2026.8.0.", "2026.2.0")
112 bool cancel_retry(Component *component, const std::string &name);
113 // Remove before 2026.8.0
114 ESPDEPRECATED("cancel_retry is deprecated and will be removed in 2026.8.0.", "2026.2.0")
115 bool cancel_retry(Component *component, const char *name);
116 // Remove before 2026.8.0
117 ESPDEPRECATED("cancel_retry is deprecated and will be removed in 2026.8.0.", "2026.2.0")
118 bool cancel_retry(Component *component, uint32_t id);
119
121 uint64_t millis_64() { return esphome::millis_64(); }
122
123 // Calculate when the next scheduled item should run.
124 // @param now On ESP32, unused for 64-bit extension (native); on other platforms, extended to 64-bit via rollover.
125 // Returns the time in milliseconds until the next scheduled item, or nullopt if no items.
126 // This method performs cleanup of removed items before checking the schedule.
127 // IMPORTANT: This method should only be called from the main thread (loop task).
128 optional<uint32_t> next_schedule_in(uint32_t now);
129
130 // Execute all scheduled items that are ready
131 // @param now Fresh timestamp from millis() - must not be stale/cached
132 // @return Timestamp of the last item that ran, or `now` unchanged if none ran.
133 uint32_t call(uint32_t now);
134
135 // Reclaim memory held by the post-boot peak. Frees every SchedulerItem in the
136 // recycle freelist and shrinks items_/to_add_/defer_queue_ vector capacity to
137 // their current sizes (std::vector grows by doubling and otherwise retains the
138 // peak). Live items in those vectors are preserved.
139 void trim_freelist();
140
141 // Move items from to_add_ into the main heap.
142 // IMPORTANT: This method should only be called from the main thread (loop task).
143 // Inlined: the fast path (nothing to add) is just an atomic load / empty check.
144 // The lock-free fast path uses to_add_count_ (atomic) or to_add_.empty()
145 // (single-threaded). This is safe because the main loop is the only thread
146 // that reads to_add_ without holding lock_; other threads may read it only
147 // while holding the mutex (e.g. cancel_item_locked_).
148 inline void ESPHOME_ALWAYS_INLINE HOT process_to_add() {
149 if (this->to_add_empty_())
150 return;
151 this->process_to_add_slow_path_();
152 }
153
154 // Name storage type discriminator for SchedulerItem
155 // Used to distinguish between static strings, hashed strings, numeric IDs, internal numeric IDs,
156 // and self-keyed pointers (caller-supplied `void *`, typically `this`).
157 enum class NameType : uint8_t {
158 STATIC_STRING = 0, // const char* pointer to static/flash storage
159 HASHED_STRING = 1, // uint32_t FNV-1a hash of a runtime string
160 NUMERIC_ID = 2, // uint32_t numeric identifier (component-level)
161 NUMERIC_ID_INTERNAL = 3, // uint32_t numeric identifier (core/internal, separate namespace)
162 SELF_POINTER = 4 // void* caller-supplied key (typically `this`); pointer equality
163 };
164
178 void set_timeout(const void *self, uint32_t timeout, std::function<void()> &&func);
180 void set_interval(const void *self, uint32_t interval, std::function<void()> &&func);
181 bool cancel_timeout(const void *self);
182 bool cancel_interval(const void *self);
183
184 protected:
185 struct SchedulerItem {
186 // Ordered by size to minimize padding. Mutually exclusive by state; read the component via
187 // get_component() so SELF_POINTER items read as component-less.
188 union {
189 Component *component; // live, non-SELF_POINTER: owning component
190 const LogString *source_name; // live SELF_POINTER: owning script name (log attribution)
191 SchedulerItem *next_free; // while pooled
192 };
193 // Optimized name storage using tagged union - zero heap allocation
194 union {
195 const char *static_name; // For STATIC_STRING (string literals) and SELF_POINTER (caller's `this`)
196 uint32_t hash_or_id; // For HASHED_STRING, NUMERIC_ID, and NUMERIC_ID_INTERNAL
197 } name_;
198 uint32_t interval;
199 // Split time to handle millis() rollover. The scheduler combines the 32-bit millis()
200 // with a 16-bit rollover counter to create a 48-bit time space (using 32+16 bits).
201 // This is intentionally limited to 48 bits, not stored as a full 64-bit value.
202 // With 49.7 days per 32-bit rollover, the 16-bit counter supports
203 // 49.7 days × 65536 = ~8900 years. This ensures correct scheduling
204 // even when devices run for months. Split into two fields for better memory
205 // alignment on 32-bit systems.
206 uint32_t next_execution_low_; // Lower 32 bits of execution time (millis value)
207 std::function<void()> callback;
208 uint16_t next_execution_high_; // Upper 16 bits (millis_major counter)
209
210#ifdef ESPHOME_THREAD_MULTI_ATOMICS
211 // Multi-threaded with atomics: use atomic uint8_t for lock-free access.
212 // std::atomic<bool> is not used because GCC on Xtensa generates an indirect
213 // function call for std::atomic<bool>::load() instead of inlining it.
214 // std::atomic<uint8_t> inlines correctly on all platforms.
215 std::atomic<uint8_t> remove{0};
216
217 // Bit-packed fields (5 bits used, 3 bits padding in 1 byte)
218 enum Type : uint8_t { TIMEOUT, INTERVAL } type : 1;
219 NameType name_type_ : 3; // Discriminator for name_ union (0–4, see NameType enum)
220 bool is_retry : 1; // True if this is a retry timeout
221 // 3 bits padding
222#else
223 // Single-threaded or multi-threaded without atomics: can pack all fields together
224 // Bit-packed fields (6 bits used, 2 bits padding in 1 byte)
225 enum Type : uint8_t { TIMEOUT, INTERVAL } type : 1;
226 bool remove : 1;
227 NameType name_type_ : 3; // Discriminator for name_ union (0–4, see NameType enum)
228 bool is_retry : 1; // True if this is a retry timeout
229 // 2 bits padding
230#endif
231
232 // Constructor
233 SchedulerItem()
234 : component(nullptr),
235 interval(0),
236 next_execution_low_(0),
237 next_execution_high_(0),
238#ifdef ESPHOME_THREAD_MULTI_ATOMICS
239 // remove is initialized in the member declaration
240 type(TIMEOUT),
241 name_type_(NameType::STATIC_STRING),
242 is_retry(false) {
243#else
244 type(TIMEOUT),
245 remove(false),
246 name_type_(NameType::STATIC_STRING),
247 is_retry(false) {
248#endif
249 name_.static_name = nullptr;
250 }
251
252 // Destructor - no dynamic memory to clean up (callback's std::function handles its own)
253 ~SchedulerItem() = default;
254
255 // Delete copy operations to prevent accidental copies
256 SchedulerItem(const SchedulerItem &) = delete;
257 SchedulerItem &operator=(const SchedulerItem &) = delete;
258
259 // Delete move operations: SchedulerItem objects are managed via raw pointers, never moved directly
260 SchedulerItem(SchedulerItem &&) = delete;
261 SchedulerItem &operator=(SchedulerItem &&) = delete;
262
263 // Helper to get the pointer-slot value (valid for STATIC_STRING and SELF_POINTER types).
264 // Both share the same union member, so callers (e.g. log formatters) can read either uniformly.
265 const char *get_name() const {
266 return (name_type_ == NameType::STATIC_STRING || name_type_ == NameType::SELF_POINTER) ? name_.static_name
267 : nullptr;
268 }
269
270 // Helper to get the hash or numeric ID (only valid for HASHED_STRING / NUMERIC_ID / NUMERIC_ID_INTERNAL types)
271 uint32_t get_name_hash_or_id() const {
272 return (name_type_ != NameType::STATIC_STRING && name_type_ != NameType::SELF_POINTER) ? name_.hash_or_id : 0;
273 }
274
275 // Helper to get the name type
276 NameType get_name_type() const { return name_type_; }
277
278 // Set name storage. STATIC_STRING/SELF_POINTER use the static_name pointer slot
279 // (both are pointer-width); other types use hash_or_id. Both union members occupy
280 // the same offset, so only one store is needed.
281 void set_name(NameType type, const char *static_name, uint32_t hash_or_id) {
282 if (type == NameType::STATIC_STRING || type == NameType::SELF_POINTER) {
283 name_.static_name = static_name;
284 } else {
285 name_.hash_or_id = hash_or_id;
286 }
287 name_type_ = type;
288 }
289
290 static bool cmp(SchedulerItem *a, SchedulerItem *b);
291
292 // Note: We use 48 bits total (32 + 16), stored in a 64-bit value for API compatibility.
293 // The upper 16 bits of the 64-bit value are always zero, which is fine since
294 // millis_major_ is also 16 bits and they must match.
295 constexpr uint64_t get_next_execution() const {
296 return (static_cast<uint64_t>(next_execution_high_) << 32) | next_execution_low_;
297 }
298
299 constexpr void set_next_execution(uint64_t value) {
300 next_execution_low_ = static_cast<uint32_t>(value);
301 // Cast to uint16_t intentionally truncates to lower 16 bits of the upper 32 bits.
302 // This is correct because millis_major_ that creates these values is also 16 bits.
303 next_execution_high_ = static_cast<uint16_t>(value >> 32);
304 }
305 constexpr const char *get_type_str() const { return (type == TIMEOUT) ? "timeout" : "interval"; }
306 // The owning component, or nullptr for SELF_POINTER items (whose slot holds source_name instead).
307 // All component access goes through this so SELF_POINTER items read as component-less.
308 Component *get_component() const { return name_type_ == NameType::SELF_POINTER ? nullptr : component; }
309 const LogString *get_source() const {
310 // Same no-source label as warn_blocking, for consistent log vocabulary.
311 if (name_type_ == NameType::SELF_POINTER)
312 return source_name != nullptr ? source_name : LOG_STR("a scheduled task");
313 return component != nullptr ? component->get_component_log_str() : LOG_STR("unknown");
314 }
315 };
316
317 // Common implementation for both timeout and interval
318 // name_type determines storage type: STATIC_STRING uses static_name, others use hash_or_id
319 // `source` is stored (in the union slot) only for SELF_POINTER items; ignored otherwise.
320 void set_timer_common_(Component *component, SchedulerItem::Type type, NameType name_type, const char *static_name,
321 uint32_t hash_or_id, uint32_t delay, std::function<void()> &&func, bool is_retry = false,
322 bool skip_cancel = false, const LogString *source = nullptr);
323
324 // Common implementation for retry - Remove before 2026.8.0
325 // name_type determines storage type: STATIC_STRING uses static_name, others use hash_or_id
326#pragma GCC diagnostic push
327#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
328 void set_retry_common_(Component *component, NameType name_type, const char *static_name, uint32_t hash_or_id,
329 uint32_t initial_wait_time, uint8_t max_attempts, std::function<RetryResult(uint8_t)> func,
330 float backoff_increase_factor);
331#pragma GCC diagnostic pop
332 // Common implementation for cancel_retry
333 bool cancel_retry_(Component *component, NameType name_type, const char *static_name, uint32_t hash_or_id);
334
335 // Extend a 32-bit millis() value to 64-bit. Use when the caller already has a fresh now.
336 // On platforms with native 64-bit time (ESP32, Host, Zephyr, RP2040 — see
337 // USE_NATIVE_64BIT_TIME in defines.h), ignores now and uses millis_64() directly, so the
338 // Scheduler always works in 64-bit time regardless of what the caller's 32-bit now came
339 // from. On ESP32 specifically, millis() comes from xTaskGetTickCount while millis_64()
340 // comes from esp_timer — two different clocks — but that is safe because scheduling
341 // compares millis_64 values against millis_64 only, never against millis().
342 // On platforms without native 64-bit time (e.g. ESP8266), extends now to 64-bit using
343 // rollover tracking, so both millis() and scheduling use the same underlying clock.
344 uint64_t ESPHOME_ALWAYS_INLINE millis_64_from_(uint32_t now) {
345#ifdef USE_NATIVE_64BIT_TIME
346 (void) now;
347 return millis_64();
348#else
349 return Millis64Impl::compute(now);
350#endif
351 }
352 // Cleanup logically deleted items from the scheduler
353 // Returns true if items remain after cleanup
354 // IMPORTANT: This method should only be called from the main thread (loop task).
355 // Inlined: the fast path (nothing to remove) is just an atomic load + empty check.
356 // Reading items_.empty() without the lock is safe here because only the main
357 // loop thread structurally modifies items_ (push/pop/erase). Other threads may
358 // iterate items_ and mark items removed under lock_, but never change the
359 // vector's size or data pointer.
360 inline bool ESPHOME_ALWAYS_INLINE HOT cleanup_() {
361 if (this->to_remove_empty_())
362 return !this->items_.empty();
363 return this->cleanup_slow_path_();
364 }
365 // Slow path for cleanup_() when there are items to remove - defined in scheduler.cpp
366 bool cleanup_slow_path_();
367 // Slow path for process_to_add() when there are items to merge - defined in scheduler.cpp
368 void process_to_add_slow_path_();
369 // Remove and return the front item from the heap as a raw pointer.
370 // Caller takes ownership and must either recycle or delete the item.
371 // IMPORTANT: Caller must hold the scheduler lock before calling this function.
372 SchedulerItem *pop_raw_locked_();
373 // Get or create a scheduler item from the pool
374 // IMPORTANT: Caller must hold the scheduler lock before calling this function.
375 SchedulerItem *get_item_from_pool_locked_();
376
377 private:
378 // Out-of-line helper that shrinks a SchedulerItem* vector's capacity to its current
379 // size. Centralised so trim_freelist() doesn't pay flash cost per call site.
380 void shrink_scheduler_vector_(std::vector<SchedulerItem *> *v);
381
382 // Helper to cancel matching items - must be called with lock held.
383 // When find_first=true, stops after the first match (used by set_timer_common_ where
384 // the cancel-before-add invariant guarantees at most one match).
385 // When find_first=false (default), cancels ALL matches (needed for DelayAction parallel
386 // mode where skip_cancel=true allows multiple items with the same key).
387 // name_type determines matching: STATIC_STRING uses static_name, others use hash_or_id
388 bool cancel_item_locked_(Component *component, NameType name_type, const char *static_name, uint32_t hash_or_id,
389 SchedulerItem::Type type, bool match_retry = false, bool find_first = false);
390
391 // Common implementation for cancel operations - handles locking
392 bool cancel_item_(Component *component, NameType name_type, const char *static_name, uint32_t hash_or_id,
393 SchedulerItem::Type type, bool match_retry = false);
394
395 // Helper to check if two static string names match
396 inline bool HOT names_match_static_(const char *name1, const char *name2) const {
397 // Check pointer equality first (common for static strings), then string contents
398 // The core ESPHome codebase uses static strings (const char*) for component names,
399 // making pointer comparison effective. The std::string overloads exist only for
400 // compatibility with external components but are rarely used in practice.
401 return (name1 != nullptr && name2 != nullptr) && ((name1 == name2) || (strcmp(name1, name2) == 0));
402 }
403
404 // Helper function to check if item matches criteria for cancellation
405 // name_type determines matching: STATIC_STRING uses static_name, others use hash_or_id
406 // IMPORTANT: Must be called with scheduler lock held
407 inline bool HOT matches_item_locked_(SchedulerItem *item, Component *component, NameType name_type,
408 const char *static_name, uint32_t hash_or_id, SchedulerItem::Type type,
409 bool match_retry, bool skip_removed = true) const {
410 // THREAD SAFETY: Check for nullptr first to prevent LoadProhibited crashes. On multi-threaded
411 // platforms, items can be nulled in defer_queue_ during processing.
412 // Fixes: https://github.com/esphome/esphome/issues/11940
413 if (item == nullptr)
414 return false;
415 // get_component() is nullptr for SELF_POINTER items (their cancels pass nullptr too), so they
416 // match by the `this` key alone.
417 if (item->get_component() != component || item->type != type ||
418 (skip_removed && this->is_item_removed_locked_(item)) || (match_retry && !item->is_retry)) {
419 return false;
420 }
421 // Name type must match
422 if (item->get_name_type() != name_type)
423 return false;
424 // STATIC_STRING: compare string content. SELF_POINTER: raw pointer equality (no strcmp).
425 // Other types: compare hash/ID value.
426 if (name_type == NameType::STATIC_STRING) {
427 return this->names_match_static_(item->get_name(), static_name);
428 }
429 if (name_type == NameType::SELF_POINTER) {
430 return item->name_.static_name == static_name;
431 }
432 return item->get_name_hash_or_id() == hash_or_id;
433 }
434
435 // Helper to execute a scheduler item
436 uint32_t execute_item_(SchedulerItem *item, uint32_t now);
437
438 // True if the item's component is failed (so it must not run). SELF_POINTER delays have no
439 // component (get_component() == nullptr) and always fire.
440 bool is_item_failed_(SchedulerItem *item) const {
441 Component *component = item->get_component();
442 return component != nullptr && component->is_failed();
443 }
444
445 // Helper to check if item should be skipped
446 bool should_skip_item_(SchedulerItem *item) const { return is_item_removed_(item) || this->is_item_failed_(item); }
447
448 // Helper to recycle a SchedulerItem back to the pool.
449 // Takes a raw pointer — caller transfers ownership. The item is either added to the
450 // pool or deleted if the pool is full.
451 // IMPORTANT: Only call from main loop context! Recycling clears the callback,
452 // so calling from another thread while the callback is executing causes use-after-free.
453 // IMPORTANT: Caller must hold the scheduler lock before calling this function.
454 void recycle_item_main_loop_(SchedulerItem *item);
455
456 // Helper to perform full cleanup when too many items are cancelled
457 void full_cleanup_removed_items_();
458
459 // Helper to calculate random offset for interval timers - extracted to reduce code size of set_timer_common_
460 // IMPORTANT: Must not be inlined - called only for intervals, keeping it out of the hot path saves flash.
461 uint32_t __attribute__((noinline)) calculate_interval_offset_(uint32_t delay);
462
463 // Helper to check if a retry was already cancelled - extracted to reduce code size of set_timer_common_
464 // Remove before 2026.8.0 along with all retry code.
465 // IMPORTANT: Must not be inlined - retry path is cold and deprecated.
466 // IMPORTANT: Caller must hold the scheduler lock before calling this function.
467 bool __attribute__((noinline))
468 is_retry_cancelled_locked_(Component *component, NameType name_type, const char *static_name, uint32_t hash_or_id);
469
470#ifdef ESPHOME_DEBUG_SCHEDULER
471 // Helper for debug logging in set_timer_common_ - extracted to reduce code size
472 void debug_log_timer_(const SchedulerItem *item, NameType name_type, const char *static_name, uint32_t hash_or_id,
473 SchedulerItem::Type type, uint32_t delay, uint64_t now);
474#endif /* ESPHOME_DEBUG_SCHEDULER */
475
476#ifndef ESPHOME_THREAD_SINGLE
477 // Process defer queue for FIFO execution of deferred items.
478 // IMPORTANT: This method should only be called from the main thread (loop task).
479 // Inlined: the fast path (nothing deferred) is just an atomic load check.
480 inline void ESPHOME_ALWAYS_INLINE HOT process_defer_queue_(uint32_t &now) {
481 // Fast path: nothing to process, avoid lock entirely.
482 // Worst case is a one-loop-iteration delay before newly deferred items are processed.
483 if (this->defer_empty_())
484 return;
485 this->process_defer_queue_slow_path_(now);
486 }
487
488 // Slow path for process_defer_queue_() - defined in scheduler.cpp
489 void process_defer_queue_slow_path_(uint32_t &now);
490
491 // Helper to cleanup defer_queue_ after processing.
492 // Keeps the common clear() path inline, outlines the rare compaction to keep
493 // cold code out of the hot instruction cache lines.
494 // IMPORTANT: Caller must hold the scheduler lock before calling this function.
495 inline void cleanup_defer_queue_locked_() {
496 // Check if new items were added by producers during processing
497 if (this->defer_queue_front_ >= this->defer_queue_.size()) {
498 // Common case: no new items - clear everything
499 this->defer_queue_.clear();
500 } else {
501 // Rare case: new items were added during processing - outlined to keep cold code
502 // out of the hot instruction cache lines
503 this->compact_defer_queue_locked_();
504 }
505 this->defer_queue_front_ = 0;
506 }
507
508 // Cold path for compacting defer_queue_ when new items were added during processing.
509 // IMPORTANT: Caller must hold the scheduler lock before calling this function.
510 // IMPORTANT: Must not be inlined - rare path, outlined to keep it out of the hot instruction cache lines.
511 void __attribute__((noinline)) compact_defer_queue_locked_();
512#endif /* not ESPHOME_THREAD_SINGLE */
513
514 // Helper to check if item is marked for removal (platform-specific)
515 // Returns true if item should be skipped, handles platform-specific synchronization
516 // For ESPHOME_THREAD_MULTI_NO_ATOMICS platforms, the caller must hold the scheduler lock before calling this
517 // function.
518 bool is_item_removed_(SchedulerItem *item) const {
519#ifdef ESPHOME_THREAD_MULTI_ATOMICS
520 // Multi-threaded with atomics: use atomic load for lock-free access
521 return item->remove.load(std::memory_order_acquire);
522#else
523 // Single-threaded (ESPHOME_THREAD_SINGLE) or
524 // multi-threaded without atomics (ESPHOME_THREAD_MULTI_NO_ATOMICS): direct read
525 // For ESPHOME_THREAD_MULTI_NO_ATOMICS, caller MUST hold lock!
526 return item->remove;
527#endif
528 }
529
530 // Helper to check if item is marked for removal when lock is already held.
531 // Uses relaxed ordering since the mutex provides all necessary synchronization.
532 // IMPORTANT: Caller must hold the scheduler lock before calling this function.
533 bool is_item_removed_locked_(SchedulerItem *item) const {
534#ifdef ESPHOME_THREAD_MULTI_ATOMICS
535 // Lock already held - relaxed is sufficient, mutex provides ordering
536 return item->remove.load(std::memory_order_relaxed);
537#else
538 return item->remove;
539#endif
540 }
541
542 // Helper to set item removal flag (platform-specific)
543 // For ESPHOME_THREAD_MULTI_NO_ATOMICS platforms, the caller must hold the scheduler lock before calling this
544 // function. Uses memory_order_release when setting to true (for cancellation synchronization),
545 // and memory_order_relaxed when setting to false (for initialization).
546 void set_item_removed_(SchedulerItem *item, bool removed) {
547#ifdef ESPHOME_THREAD_MULTI_ATOMICS
548 // Multi-threaded with atomics: use atomic store with appropriate ordering
549 // Release ordering when setting to true ensures cancellation is visible to other threads
550 // Relaxed ordering when setting to false is sufficient for initialization
551 item->remove.store(removed ? 1 : 0, removed ? std::memory_order_release : std::memory_order_relaxed);
552#else
553 // Single-threaded (ESPHOME_THREAD_SINGLE) or
554 // multi-threaded without atomics (ESPHOME_THREAD_MULTI_NO_ATOMICS): direct write
555 // For ESPHOME_THREAD_MULTI_NO_ATOMICS, caller MUST hold lock!
556 item->remove = removed;
557#endif
558 }
559
560 // Helper to mark matching items in a container as removed.
561 // When find_first=true, stops after the first match (used by set_timer_common_ where
562 // the cancel-before-add invariant guarantees at most one match).
563 // When find_first=false, marks ALL matches (needed for public cancel path where
564 // DelayAction parallel mode with skip_cancel=true can create multiple items with the same key).
565 // name_type determines matching: STATIC_STRING uses static_name, others use hash_or_id
566 // Returns the number of items marked for removal.
567 // IMPORTANT: Must be called with scheduler lock held
568 // Inlined: the fast path (empty container) avoids calling the out-of-line scan.
569 inline size_t HOT mark_matching_items_removed_locked_(std::vector<SchedulerItem *> &container, Component *component,
570 NameType name_type, const char *static_name,
571 uint32_t hash_or_id, SchedulerItem::Type type, bool match_retry,
572 bool find_first = false) {
573 if (container.empty())
574 return 0;
575 return this->mark_matching_items_removed_slow_locked_(container, component, name_type, static_name, hash_or_id,
576 type, match_retry, find_first);
577 }
578
579 // Out-of-line slow path for mark_matching_items_removed_locked_ when container is non-empty.
580 // IMPORTANT: Must be called with scheduler lock held
581 __attribute__((noinline)) size_t mark_matching_items_removed_slow_locked_(
582 std::vector<SchedulerItem *> &container, Component *component, NameType name_type, const char *static_name,
583 uint32_t hash_or_id, SchedulerItem::Type type, bool match_retry, bool find_first);
584
585 Mutex lock_;
586 std::vector<SchedulerItem *> items_;
587 std::vector<SchedulerItem *> to_add_;
588
589#ifndef ESPHOME_THREAD_SINGLE
590 // Fast-path counter for process_to_add() to skip taking the lock when there
591 // is nothing to add. std::atomic on ATOMICS; plain uint32_t on NO_ATOMICS
592 // (BK72xx — ARMv5TE single-core, lacks LDREX/STREX so std::atomic RMW would
593 // require libatomic). Reads use __atomic_load_n(__ATOMIC_RELAXED) on
594 // NO_ATOMICS — compiles to a plain LDR (aligned 32-bit load is naturally
595 // atomic on ARMv5TE) but expresses the concurrent-access intent in the C++
596 // memory model. Writes live behind *_locked_ helpers and must hold lock_.
597#ifdef ESPHOME_THREAD_MULTI_ATOMICS
598 std::atomic<uint32_t> to_add_count_{0};
599#else
600 uint32_t to_add_count_{0};
601#endif
602#endif /* ESPHOME_THREAD_SINGLE */
603
604 // Fast-path helper for process_to_add() to decide if it can skip the lock.
605 bool to_add_empty_() const {
606#ifdef ESPHOME_THREAD_SINGLE
607 return this->to_add_.empty();
608#elif defined(ESPHOME_THREAD_MULTI_ATOMICS)
609 return this->to_add_count_.load(std::memory_order_relaxed) == 0;
610#else
611 return __atomic_load_n(&this->to_add_count_, __ATOMIC_RELAXED) == 0;
612#endif
613 }
614
615 // Increment to_add_count_ (no-op on single-threaded platforms).
616 // On NO_ATOMICS the caller must hold lock_; both load and store go through
617 // __atomic_*_n with __ATOMIC_RELAXED to keep every access to the counter
618 // explicitly atomic in the C++ memory model (same ARMv5TE codegen as
619 // plain LDR+STR).
620 void to_add_count_increment_locked_() {
621#if defined(ESPHOME_THREAD_SINGLE)
622 // No counter needed — to_add_empty_() checks the vector directly
623#elif defined(ESPHOME_THREAD_MULTI_ATOMICS)
624 this->to_add_count_.fetch_add(1, std::memory_order_relaxed);
625#else
626 uint32_t v = __atomic_load_n(&this->to_add_count_, __ATOMIC_RELAXED);
627 __atomic_store_n(&this->to_add_count_, v + 1, __ATOMIC_RELAXED);
628#endif
629 }
630
631 // Reset to_add_count_ (no-op on single-threaded platforms)
632 void to_add_count_clear_locked_() {
633#if defined(ESPHOME_THREAD_SINGLE)
634 // No counter needed — to_add_empty_() checks the vector directly
635#elif defined(ESPHOME_THREAD_MULTI_ATOMICS)
636 this->to_add_count_.store(0, std::memory_order_relaxed);
637#else
638 __atomic_store_n(&this->to_add_count_, 0, __ATOMIC_RELAXED);
639#endif
640 }
641
642#ifndef ESPHOME_THREAD_SINGLE
643 // Single-core platforms don't need the defer queue and save ~32 bytes of RAM
644 // Using std::vector instead of std::deque avoids 512-byte chunked allocations
645 // Index tracking avoids O(n) erase() calls when draining the queue each loop
646 std::vector<SchedulerItem *> defer_queue_; // FIFO queue for defer() calls
647 size_t defer_queue_front_{0}; // Index of first valid item in defer_queue_ (tracks consumed items)
648
649 // Fast-path counter for process_defer_queue_() to skip lock when nothing to
650 // process. See to_add_count_ above for the NO_ATOMICS rationale.
651#ifdef ESPHOME_THREAD_MULTI_ATOMICS
652 std::atomic<uint32_t> defer_count_{0};
653#else
654 uint32_t defer_count_{0};
655#endif
656
657 bool defer_empty_() const {
658 // defer_queue_ only exists on multi-threaded platforms, so no ESPHOME_THREAD_SINGLE path
659#ifdef ESPHOME_THREAD_MULTI_ATOMICS
660 return this->defer_count_.load(std::memory_order_relaxed) == 0;
661#else
662 return __atomic_load_n(&this->defer_count_, __ATOMIC_RELAXED) == 0;
663#endif
664 }
665
666 void defer_count_increment_locked_() {
667#ifdef ESPHOME_THREAD_MULTI_ATOMICS
668 this->defer_count_.fetch_add(1, std::memory_order_relaxed);
669#else
670 uint32_t v = __atomic_load_n(&this->defer_count_, __ATOMIC_RELAXED);
671 __atomic_store_n(&this->defer_count_, v + 1, __ATOMIC_RELAXED);
672#endif
673 }
674
675 void defer_count_clear_locked_() {
676#ifdef ESPHOME_THREAD_MULTI_ATOMICS
677 this->defer_count_.store(0, std::memory_order_relaxed);
678#else
679 __atomic_store_n(&this->defer_count_, 0, __ATOMIC_RELAXED);
680#endif
681 }
682
683#endif /* ESPHOME_THREAD_SINGLE */
684
685 // Counter for items marked for removal. Incremented cross-thread in
686 // cancel_item_locked_(). See to_add_count_ above for the NO_ATOMICS
687 // rationale.
688#ifdef ESPHOME_THREAD_MULTI_ATOMICS
689 std::atomic<uint32_t> to_remove_{0};
690#else
691 uint32_t to_remove_{0};
692#endif
693
694 // Lock-free check if there are items to remove (for fast-path in cleanup_)
695 bool to_remove_empty_() const {
696#if defined(ESPHOME_THREAD_MULTI_ATOMICS)
697 return this->to_remove_.load(std::memory_order_relaxed) == 0;
698#elif defined(ESPHOME_THREAD_MULTI_NO_ATOMICS)
699 return __atomic_load_n(&this->to_remove_, __ATOMIC_RELAXED) == 0;
700#else
701 return this->to_remove_ == 0;
702#endif
703 }
704
705 void to_remove_add_locked_(uint32_t count) {
706#if defined(ESPHOME_THREAD_MULTI_ATOMICS)
707 this->to_remove_.fetch_add(count, std::memory_order_relaxed);
708#elif defined(ESPHOME_THREAD_MULTI_NO_ATOMICS)
709 uint32_t v = __atomic_load_n(&this->to_remove_, __ATOMIC_RELAXED);
710 __atomic_store_n(&this->to_remove_, v + count, __ATOMIC_RELAXED);
711#else
712 this->to_remove_ += count;
713#endif
714 }
715
716 void to_remove_decrement_locked_() {
717#if defined(ESPHOME_THREAD_MULTI_ATOMICS)
718 this->to_remove_.fetch_sub(1, std::memory_order_relaxed);
719#elif defined(ESPHOME_THREAD_MULTI_NO_ATOMICS)
720 uint32_t v = __atomic_load_n(&this->to_remove_, __ATOMIC_RELAXED);
721 __atomic_store_n(&this->to_remove_, v - 1, __ATOMIC_RELAXED);
722#else
723 this->to_remove_--;
724#endif
725 }
726
727 void to_remove_clear_locked_() {
728#if defined(ESPHOME_THREAD_MULTI_ATOMICS)
729 this->to_remove_.store(0, std::memory_order_relaxed);
730#elif defined(ESPHOME_THREAD_MULTI_NO_ATOMICS)
731 __atomic_store_n(&this->to_remove_, 0, __ATOMIC_RELAXED);
732#else
733 this->to_remove_ = 0;
734#endif
735 }
736
737 uint32_t to_remove_count_() const {
738#if defined(ESPHOME_THREAD_MULTI_ATOMICS)
739 return this->to_remove_.load(std::memory_order_relaxed);
740#elif defined(ESPHOME_THREAD_MULTI_NO_ATOMICS)
741 return __atomic_load_n(&this->to_remove_, __ATOMIC_RELAXED);
742#else
743 return this->to_remove_;
744#endif
745 }
746
747 // Intrusive freelist threaded through SchedulerItem::next_free. Unbounded so it quiesces at the
748 // app's concurrent-timer high-water mark; the previous fixed cap caused steady-state new/delete
749 // churn on devices with many timers (see https://github.com/esphome/backlog/issues/52).
750 SchedulerItem *scheduler_item_pool_head_{nullptr};
751 size_t scheduler_item_pool_size_{0};
752
753#ifdef ESPHOME_DEBUG_SCHEDULER
754 // Leak detection: tracks total live SchedulerItem allocations.
755 // Invariant: debug_live_items_ == items_.size() + to_add_.size() + defer_queue_.size() + scheduler_item_pool_size_
756 // Verified periodically in call() to catch leaks early.
757 size_t debug_live_items_{0};
758
759 // Verify the scheduler memory invariant: all allocated items are accounted for.
760 // Returns true if no leak detected. Logs an error and asserts on failure.
761 bool debug_verify_no_leak_() const;
762#endif
763};
764
765} // namespace esphome
struct @65::@66 __attribute__
Wake the main loop task from an ISR. ISR-safe.
Definition main_task.h:32
const Component * component
Definition component.cpp:34
void delay(unsigned long ms)
uint16_t type
uint8_t source_name[64]
void retry_handler(const std::shared_ptr< RetryArgs > &args)
const char int const __FlashStringHelper va_list args
Definition log.h:74
uint64_t millis_64()
Definition hal.cpp:29
static void uint32_t