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