ESPHome 2025.9.0
Loading...
Searching...
No Matches
scheduler.h
Go to the documentation of this file.
1#pragma once
2
4#include <vector>
5#include <memory>
6#include <cstring>
7#include <deque>
8#ifdef ESPHOME_THREAD_MULTI_ATOMICS
9#include <atomic>
10#endif
11
14
15namespace esphome {
16
17class Component;
18struct RetryArgs;
19
20// Forward declaration of retry_handler - needs to be non-static for friend declaration
21void retry_handler(const std::shared_ptr<RetryArgs> &args);
22
23class Scheduler {
24 // Allow retry_handler to access protected members for internal retry mechanism
25 friend void ::esphome::retry_handler(const std::shared_ptr<RetryArgs> &args);
26 // Allow DelayAction to call set_timer_common_ with skip_cancel=true for parallel script delays.
27 // This is needed to fix issue #10264 where parallel scripts with delays interfere with each other.
28 // We use friend instead of a public API because skip_cancel is dangerous - it can cause delays
29 // to accumulate and overload the scheduler if misused.
30 template<typename... Ts> friend class DelayAction;
31
32 public:
33 // Public API - accepts std::string for backward compatibility
34 void set_timeout(Component *component, const std::string &name, uint32_t timeout, std::function<void()> func);
35
46 void set_timeout(Component *component, const char *name, uint32_t timeout, std::function<void()> func);
47
48 bool cancel_timeout(Component *component, const std::string &name);
49 bool cancel_timeout(Component *component, const char *name);
50
51 void set_interval(Component *component, const std::string &name, uint32_t interval, std::function<void()> func);
52
63 void set_interval(Component *component, const char *name, uint32_t interval, std::function<void()> func);
64
65 bool cancel_interval(Component *component, const std::string &name);
66 bool cancel_interval(Component *component, const char *name);
67 void set_retry(Component *component, const std::string &name, uint32_t initial_wait_time, uint8_t max_attempts,
68 std::function<RetryResult(uint8_t)> func, float backoff_increase_factor = 1.0f);
69 void set_retry(Component *component, const char *name, uint32_t initial_wait_time, uint8_t max_attempts,
70 std::function<RetryResult(uint8_t)> func, float backoff_increase_factor = 1.0f);
71 bool cancel_retry(Component *component, const std::string &name);
72 bool cancel_retry(Component *component, const char *name);
73
74 // Calculate when the next scheduled item should run
75 // @param now Fresh timestamp from millis() - must not be stale/cached
76 // Returns the time in milliseconds until the next scheduled item, or nullopt if no items
77 // This method performs cleanup of removed items before checking the schedule
78 // IMPORTANT: This method should only be called from the main thread (loop task).
79 optional<uint32_t> next_schedule_in(uint32_t now);
80
81 // Execute all scheduled items that are ready
82 // @param now Fresh timestamp from millis() - must not be stale/cached
83 void call(uint32_t now);
84
85 void process_to_add();
86
87 protected:
88 struct SchedulerItem {
89 // Ordered by size to minimize padding
90 Component *component;
91 // Optimized name storage using tagged union
92 union {
93 const char *static_name; // For string literals (no allocation)
94 char *dynamic_name; // For allocated strings
95 } name_;
96 uint32_t interval;
97 // Split time to handle millis() rollover. The scheduler combines the 32-bit millis()
98 // with a 16-bit rollover counter to create a 48-bit time space (using 32+16 bits).
99 // This is intentionally limited to 48 bits, not stored as a full 64-bit value.
100 // With 49.7 days per 32-bit rollover, the 16-bit counter supports
101 // 49.7 days × 65536 = ~8900 years. This ensures correct scheduling
102 // even when devices run for months. Split into two fields for better memory
103 // alignment on 32-bit systems.
104 uint32_t next_execution_low_; // Lower 32 bits of execution time (millis value)
105 std::function<void()> callback;
106 uint16_t next_execution_high_; // Upper 16 bits (millis_major counter)
107
108#ifdef ESPHOME_THREAD_MULTI_ATOMICS
109 // Multi-threaded with atomics: use atomic for lock-free access
110 // Place atomic<bool> separately since it can't be packed with bit fields
111 std::atomic<bool> remove{false};
112
113 // Bit-packed fields (3 bits used, 5 bits padding in 1 byte)
114 enum Type : uint8_t { TIMEOUT, INTERVAL } type : 1;
115 bool name_is_dynamic : 1; // True if name was dynamically allocated (needs delete[])
116 bool is_retry : 1; // True if this is a retry timeout
117 // 5 bits padding
118#else
119 // Single-threaded or multi-threaded without atomics: can pack all fields together
120 // Bit-packed fields (4 bits used, 4 bits padding in 1 byte)
121 enum Type : uint8_t { TIMEOUT, INTERVAL } type : 1;
122 bool remove : 1;
123 bool name_is_dynamic : 1; // True if name was dynamically allocated (needs delete[])
124 bool is_retry : 1; // True if this is a retry timeout
125 // 4 bits padding
126#endif
127
128 // Constructor
129 SchedulerItem()
130 : component(nullptr),
131 interval(0),
132 next_execution_low_(0),
133 next_execution_high_(0),
134#ifdef ESPHOME_THREAD_MULTI_ATOMICS
135 // remove is initialized in the member declaration as std::atomic<bool>{false}
136 type(TIMEOUT),
137 name_is_dynamic(false),
138 is_retry(false) {
139#else
140 type(TIMEOUT),
141 remove(false),
142 name_is_dynamic(false),
143 is_retry(false) {
144#endif
145 name_.static_name = nullptr;
146 }
147
148 // Destructor to clean up dynamic names
149 ~SchedulerItem() { clear_dynamic_name(); }
150
151 // Delete copy operations to prevent accidental copies
152 SchedulerItem(const SchedulerItem &) = delete;
153 SchedulerItem &operator=(const SchedulerItem &) = delete;
154
155 // Delete move operations: SchedulerItem objects are only managed via unique_ptr, never moved directly
156 SchedulerItem(SchedulerItem &&) = delete;
157 SchedulerItem &operator=(SchedulerItem &&) = delete;
158
159 // Helper to get the name regardless of storage type
160 const char *get_name() const { return name_is_dynamic ? name_.dynamic_name : name_.static_name; }
161
162 // Helper to clear dynamic name if allocated
163 void clear_dynamic_name() {
164 if (name_is_dynamic && name_.dynamic_name) {
165 delete[] name_.dynamic_name;
166 name_.dynamic_name = nullptr;
167 name_is_dynamic = false;
168 }
169 }
170
171 // Helper to set name with proper ownership
172 void set_name(const char *name, bool make_copy = false) {
173 // Clean up old dynamic name if any
174 clear_dynamic_name();
175
176 if (!name) {
177 // nullptr case - no name provided
178 name_.static_name = nullptr;
179 } else if (make_copy) {
180 // Make a copy for dynamic strings (including empty strings)
181 size_t len = strlen(name);
182 name_.dynamic_name = new char[len + 1];
183 memcpy(name_.dynamic_name, name, len + 1);
184 name_is_dynamic = true;
185 } else {
186 // Use static string directly (including empty strings)
187 name_.static_name = name;
188 }
189 }
190
191 static bool cmp(const std::unique_ptr<SchedulerItem> &a, const std::unique_ptr<SchedulerItem> &b);
192
193 // Note: We use 48 bits total (32 + 16), stored in a 64-bit value for API compatibility.
194 // The upper 16 bits of the 64-bit value are always zero, which is fine since
195 // millis_major_ is also 16 bits and they must match.
196 constexpr uint64_t get_next_execution() const {
197 return (static_cast<uint64_t>(next_execution_high_) << 32) | next_execution_low_;
198 }
199
200 constexpr void set_next_execution(uint64_t value) {
201 next_execution_low_ = static_cast<uint32_t>(value);
202 // Cast to uint16_t intentionally truncates to lower 16 bits of the upper 32 bits.
203 // This is correct because millis_major_ that creates these values is also 16 bits.
204 next_execution_high_ = static_cast<uint16_t>(value >> 32);
205 }
206 constexpr const char *get_type_str() const { return (type == TIMEOUT) ? "timeout" : "interval"; }
207 const LogString *get_source() const { return component ? component->get_component_log_str() : LOG_STR("unknown"); }
208 };
209
210 // Common implementation for both timeout and interval
211 void set_timer_common_(Component *component, SchedulerItem::Type type, bool is_static_string, const void *name_ptr,
212 uint32_t delay, std::function<void()> func, bool is_retry = false, bool skip_cancel = false);
213
214 // Common implementation for retry
215 void set_retry_common_(Component *component, bool is_static_string, const void *name_ptr, uint32_t initial_wait_time,
216 uint8_t max_attempts, std::function<RetryResult(uint8_t)> func, float backoff_increase_factor);
217
218 uint64_t millis_64_(uint32_t now);
219 // Cleanup logically deleted items from the scheduler
220 // Returns the number of items remaining after cleanup
221 // IMPORTANT: This method should only be called from the main thread (loop task).
222 size_t cleanup_();
223 void pop_raw_();
224
225 private:
226 // Helper to cancel items by name - must be called with lock held
227 bool cancel_item_locked_(Component *component, const char *name, SchedulerItem::Type type, bool match_retry = false);
228
229 // Helper to extract name as const char* from either static string or std::string
230 inline const char *get_name_cstr_(bool is_static_string, const void *name_ptr) {
231 return is_static_string ? static_cast<const char *>(name_ptr) : static_cast<const std::string *>(name_ptr)->c_str();
232 }
233
234 // Common implementation for cancel operations
235 bool cancel_item_(Component *component, bool is_static_string, const void *name_ptr, SchedulerItem::Type type);
236
237 // Helper to check if two scheduler item names match
238 inline bool HOT names_match_(const char *name1, const char *name2) const {
239 // Check pointer equality first (common for static strings), then string contents
240 // The core ESPHome codebase uses static strings (const char*) for component names,
241 // making pointer comparison effective. The std::string overloads exist only for
242 // compatibility with external components but are rarely used in practice.
243 return (name1 != nullptr && name2 != nullptr) && ((name1 == name2) || (strcmp(name1, name2) == 0));
244 }
245
246 // Helper function to check if item matches criteria for cancellation
247 inline bool HOT matches_item_(const std::unique_ptr<SchedulerItem> &item, Component *component, const char *name_cstr,
248 SchedulerItem::Type type, bool match_retry, bool skip_removed = true) const {
249 if (item->component != component || item->type != type || (skip_removed && item->remove) ||
250 (match_retry && !item->is_retry)) {
251 return false;
252 }
253 return this->names_match_(item->get_name(), name_cstr);
254 }
255
256 // Helper to execute a scheduler item
257 uint32_t execute_item_(SchedulerItem *item, uint32_t now);
258
259 // Helper to check if item should be skipped
260 bool should_skip_item_(SchedulerItem *item) const {
261 return is_item_removed_(item) || (item->component != nullptr && item->component->is_failed());
262 }
263
264 // Helper to recycle a SchedulerItem
265 void recycle_item_(std::unique_ptr<SchedulerItem> item);
266
267 // Helper to check if item is marked for removal (platform-specific)
268 // Returns true if item should be skipped, handles platform-specific synchronization
269 // For ESPHOME_THREAD_MULTI_NO_ATOMICS platforms, the caller must hold the scheduler lock before calling this
270 // function.
271 bool is_item_removed_(SchedulerItem *item) const {
272#ifdef ESPHOME_THREAD_MULTI_ATOMICS
273 // Multi-threaded with atomics: use atomic load for lock-free access
274 return item->remove.load(std::memory_order_acquire);
275#else
276 // Single-threaded (ESPHOME_THREAD_SINGLE) or
277 // multi-threaded without atomics (ESPHOME_THREAD_MULTI_NO_ATOMICS): direct read
278 // For ESPHOME_THREAD_MULTI_NO_ATOMICS, caller MUST hold lock!
279 return item->remove;
280#endif
281 }
282
283 // Helper to mark item for removal (platform-specific)
284 // For ESPHOME_THREAD_MULTI_NO_ATOMICS platforms, the caller must hold the scheduler lock before calling this
285 // function.
286 void mark_item_removed_(SchedulerItem *item) {
287#ifdef ESPHOME_THREAD_MULTI_ATOMICS
288 // Multi-threaded with atomics: use atomic store
289 item->remove.store(true, std::memory_order_release);
290#else
291 // Single-threaded (ESPHOME_THREAD_SINGLE) or
292 // multi-threaded without atomics (ESPHOME_THREAD_MULTI_NO_ATOMICS): direct write
293 // For ESPHOME_THREAD_MULTI_NO_ATOMICS, caller MUST hold lock!
294 item->remove = true;
295#endif
296 }
297
298 // Template helper to check if any item in a container matches our criteria
299 template<typename Container>
300 bool has_cancelled_timeout_in_container_(const Container &container, Component *component, const char *name_cstr,
301 bool match_retry) const {
302 for (const auto &item : container) {
303 if (is_item_removed_(item.get()) &&
304 this->matches_item_(item, component, name_cstr, SchedulerItem::TIMEOUT, match_retry,
305 /* skip_removed= */ false)) {
306 return true;
307 }
308 }
309 return false;
310 }
311
312 Mutex lock_;
313 std::vector<std::unique_ptr<SchedulerItem>> items_;
314 std::vector<std::unique_ptr<SchedulerItem>> to_add_;
315#ifndef ESPHOME_THREAD_SINGLE
316 // Single-core platforms don't need the defer queue and save 40 bytes of RAM
317 std::deque<std::unique_ptr<SchedulerItem>> defer_queue_; // FIFO queue for defer() calls
318#endif /* ESPHOME_THREAD_SINGLE */
319 uint32_t to_remove_{0};
320
321 // Memory pool for recycling SchedulerItem objects to reduce heap churn.
322 // Design decisions:
323 // - std::vector is used instead of a fixed array because many systems only need 1-2 scheduler items
324 // - The vector grows dynamically up to MAX_POOL_SIZE (5) only when needed, saving memory on simple setups
325 // - Pool size of 5 matches typical usage (2-4 timers) while keeping memory overhead low (~250 bytes on ESP32)
326 // - The pool significantly reduces heap fragmentation which is critical because heap allocation/deallocation
327 // can stall the entire system, causing timing issues and dropped events for any components that need
328 // to synchronize between tasks (see https://github.com/esphome/backlog/issues/52)
329 std::vector<std::unique_ptr<SchedulerItem>> scheduler_item_pool_;
330
331#ifdef ESPHOME_THREAD_MULTI_ATOMICS
332 /*
333 * Multi-threaded platforms with atomic support: last_millis_ needs atomic for lock-free updates
334 *
335 * MEMORY-ORDERING NOTE
336 * --------------------
337 * `last_millis_` and `millis_major_` form a single 64-bit timestamp split in half.
338 * Writers publish `last_millis_` with memory_order_release and readers use
339 * memory_order_acquire. This ensures that once a reader sees the new low word,
340 * it also observes the corresponding increment of `millis_major_`.
341 */
342 std::atomic<uint32_t> last_millis_{0};
343#else /* not ESPHOME_THREAD_MULTI_ATOMICS */
344 // Platforms without atomic support or single-threaded platforms
345 uint32_t last_millis_{0};
346#endif /* else ESPHOME_THREAD_MULTI_ATOMICS */
347
348 /*
349 * Upper 16 bits of the 64-bit millis counter. Incremented only while holding
350 * `lock_`; read concurrently. Atomic (relaxed) avoids a formal data race.
351 * Ordering relative to `last_millis_` is provided by its release store and the
352 * corresponding acquire loads.
353 */
354#ifdef ESPHOME_THREAD_MULTI_ATOMICS
355 std::atomic<uint16_t> millis_major_{0};
356#else /* not ESPHOME_THREAD_MULTI_ATOMICS */
357 uint16_t millis_major_{0};
358#endif /* else ESPHOME_THREAD_MULTI_ATOMICS */
359};
360
361} // namespace esphome
uint8_t type
Providing packet encoding functions for exchanging data with a remote host.
Definition a01nyub.cpp:7
void retry_handler(const std::shared_ptr< RetryArgs > &args)
uint32_t len