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