[scheduler] Add self-keyed timer API for callers without a Component (#16127)

This commit is contained in:
J. Nick Koston
2026-04-29 13:24:37 -05:00
committed by GitHub
parent 0ad8a071a7
commit c41f38e16d
4 changed files with 295 additions and 26 deletions
+31 -2
View File
@@ -35,7 +35,9 @@ static constexpr uint32_t MAX_INTERVAL_DELAY = 5000;
// Uses a stack buffer to avoid heap allocation
// Uses ESPHOME_snprintf_P/ESPHOME_PSTR for ESP8266 to keep format strings in flash
struct SchedulerNameLog {
char buffer[20]; // Enough for "id:4294967295" or "hash:0xFFFFFFFF" or "(null)"
// Sized for the widest formatted output: "self:0x" + 16 hex digits (64-bit pointer) + nul.
// Also covers "id:4294967295", "hash:0xFFFFFFFF", "iid:4294967295", "(null)".
char buffer[28];
// Format a scheduler item name for logging
// Returns pointer to formatted string (either static_name or internal buffer)
@@ -53,9 +55,15 @@ struct SchedulerNameLog {
} else if (name_type == NameType::NUMERIC_ID) {
ESPHOME_snprintf_P(buffer, sizeof(buffer), ESPHOME_PSTR("id:%" PRIu32), hash_or_id);
return buffer;
} else { // NUMERIC_ID_INTERNAL
} else if (name_type == NameType::NUMERIC_ID_INTERNAL) {
ESPHOME_snprintf_P(buffer, sizeof(buffer), ESPHOME_PSTR("iid:%" PRIu32), hash_or_id);
return buffer;
} else { // SELF_POINTER
// static_name carries the void* key for SELF_POINTER (pointer-width union slot).
// %p is specified as void* (not const void*), so strip const for the varargs call.
ESPHOME_snprintf_P(buffer, sizeof(buffer), ESPHOME_PSTR("self:%p"),
const_cast<void *>(static_cast<const void *>(static_name)));
return buffer;
}
}
};
@@ -293,6 +301,27 @@ bool HOT Scheduler::cancel_interval(Component *component, uint32_t id) {
return this->cancel_item_(component, NameType::NUMERIC_ID, nullptr, id, SchedulerItem::INTERVAL);
}
// Self-keyed scheduler API. The cancellation key is `self` (typically the caller's `this`),
// passed through the existing static_name pointer slot. Matching is by raw pointer equality
// (see matches_item_locked_'s SELF_POINTER branch). No Component pointer is stored, so
// is_failed() skip and component-based log attribution don't apply.
void HOT Scheduler::set_timeout(const void *self, uint32_t timeout, std::function<void()> &&func) {
this->set_timer_common_(nullptr, SchedulerItem::TIMEOUT, NameType::SELF_POINTER, static_cast<const char *>(self), 0,
timeout, std::move(func));
}
void HOT Scheduler::set_interval(const void *self, uint32_t interval, std::function<void()> &&func) {
this->set_timer_common_(nullptr, SchedulerItem::INTERVAL, NameType::SELF_POINTER, static_cast<const char *>(self), 0,
interval, std::move(func));
}
bool HOT Scheduler::cancel_timeout(const void *self) {
return this->cancel_item_(nullptr, NameType::SELF_POINTER, static_cast<const char *>(self), 0,
SchedulerItem::TIMEOUT);
}
bool HOT Scheduler::cancel_interval(const void *self) {
return this->cancel_item_(nullptr, NameType::SELF_POINTER, static_cast<const char *>(self), 0,
SchedulerItem::INTERVAL);
}
// Suppress deprecation warnings for RetryResult usage in the still-present (but deprecated) retry implementation.
// Remove before 2026.8.0 along with all retry code.
#pragma GCC diagnostic push
+56 -24
View File
@@ -146,22 +146,43 @@ class Scheduler {
}
// Name storage type discriminator for SchedulerItem
// Used to distinguish between static strings, hashed strings, numeric IDs, and internal numeric IDs
// Used to distinguish between static strings, hashed strings, numeric IDs, internal numeric IDs,
// and self-keyed pointers (caller-supplied `void *`, typically `this`).
enum class NameType : uint8_t {
STATIC_STRING = 0, // const char* pointer to static/flash storage
HASHED_STRING = 1, // uint32_t FNV-1a hash of a runtime string
NUMERIC_ID = 2, // uint32_t numeric identifier (component-level)
NUMERIC_ID_INTERNAL = 3 // uint32_t numeric identifier (core/internal, separate namespace)
STATIC_STRING = 0, // const char* pointer to static/flash storage
HASHED_STRING = 1, // uint32_t FNV-1a hash of a runtime string
NUMERIC_ID = 2, // uint32_t numeric identifier (component-level)
NUMERIC_ID_INTERNAL = 3, // uint32_t numeric identifier (core/internal, separate namespace)
SELF_POINTER = 4 // void* caller-supplied key (typically `this`); pointer equality
};
/** Self-keyed timeout. The cancellation key is `self` (typically the caller's `this`).
*
* Use this when the caller schedules at most one timer of a single purpose at a time and
* does not need a `Component` for `is_failed()` skip or log source attribution. Lets
* small classes drop `Component` inheritance entirely when their only Component dependency
* was the per-instance scheduler key.
*
* NOT applied for self-keyed items:
* - `is_failed()` skip — callbacks always fire (no Component to consult).
* - Log source attribution — logs use a generic "self:0x…" label.
*
* If you need either of those, use the existing `(Component *, id)` overloads.
*/
void set_timeout(const void *self, uint32_t timeout, std::function<void()> &&func);
/// Self-keyed interval. See set_timeout(const void *, ...) for semantics.
void set_interval(const void *self, uint32_t interval, std::function<void()> &&func);
bool cancel_timeout(const void *self);
bool cancel_interval(const void *self);
protected:
struct SchedulerItem {
// Ordered by size to minimize padding
Component *component;
// Optimized name storage using tagged union - zero heap allocation
union {
const char *static_name; // For STATIC_STRING (string literals, no allocation)
uint32_t hash_or_id; // For HASHED_STRING or NUMERIC_ID
const char *static_name; // For STATIC_STRING (string literals) and SELF_POINTER (caller's `this`)
uint32_t hash_or_id; // For HASHED_STRING, NUMERIC_ID, and NUMERIC_ID_INTERNAL
} name_;
uint32_t interval;
// Split time to handle millis() rollover. The scheduler combines the 32-bit millis()
@@ -182,19 +203,19 @@ class Scheduler {
// std::atomic<uint8_t> inlines correctly on all platforms.
std::atomic<uint8_t> remove{0};
// Bit-packed fields (4 bits used, 4 bits padding in 1 byte)
enum Type : uint8_t { TIMEOUT, INTERVAL } type : 1;
NameType name_type_ : 2; // Discriminator for name_ union (0–3, see NameType enum)
bool is_retry : 1; // True if this is a retry timeout
// 4 bits padding
#else
// Single-threaded or multi-threaded without atomics: can pack all fields together
// Bit-packed fields (5 bits used, 3 bits padding in 1 byte)
enum Type : uint8_t { TIMEOUT, INTERVAL } type : 1;
bool remove : 1;
NameType name_type_ : 2; // Discriminator for name_ union (0–3, see NameType enum)
NameType name_type_ : 3; // Discriminator for name_ union (0–4, see NameType enum)
bool is_retry : 1; // True if this is a retry timeout
// 3 bits padding
#else
// Single-threaded or multi-threaded without atomics: can pack all fields together
// Bit-packed fields (6 bits used, 2 bits padding in 1 byte)
enum Type : uint8_t { TIMEOUT, INTERVAL } type : 1;
bool remove : 1;
NameType name_type_ : 3; // Discriminator for name_ union (0–4, see NameType enum)
bool is_retry : 1; // True if this is a retry timeout
// 2 bits padding
#endif
// Constructor
@@ -228,19 +249,26 @@ class Scheduler {
SchedulerItem(SchedulerItem &&) = delete;
SchedulerItem &operator=(SchedulerItem &&) = delete;
// Helper to get the static name (only valid for STATIC_STRING type)
const char *get_name() const { return (name_type_ == NameType::STATIC_STRING) ? name_.static_name : nullptr; }
// Helper to get the pointer-slot value (valid for STATIC_STRING and SELF_POINTER types).
// Both share the same union member, so callers (e.g. log formatters) can read either uniformly.
const char *get_name() const {
return (name_type_ == NameType::STATIC_STRING || name_type_ == NameType::SELF_POINTER) ? name_.static_name
: nullptr;
}
// Helper to get the hash or numeric ID (only valid for HASHED_STRING or NUMERIC_ID types)
uint32_t get_name_hash_or_id() const { return (name_type_ != NameType::STATIC_STRING) ? name_.hash_or_id : 0; }
// Helper to get the hash or numeric ID (only valid for HASHED_STRING / NUMERIC_ID / NUMERIC_ID_INTERNAL types)
uint32_t get_name_hash_or_id() const {
return (name_type_ != NameType::STATIC_STRING && name_type_ != NameType::SELF_POINTER) ? name_.hash_or_id : 0;
}
// Helper to get the name type
NameType get_name_type() const { return name_type_; }
// Set name storage: for STATIC_STRING stores the pointer, for all other types stores hash_or_id.
// Both union members occupy the same offset, so only one store is needed.
// Set name storage. STATIC_STRING/SELF_POINTER use the static_name pointer slot
// (both are pointer-width); other types use hash_or_id. Both union members occupy
// the same offset, so only one store is needed.
void set_name(NameType type, const char *static_name, uint32_t hash_or_id) {
if (type == NameType::STATIC_STRING) {
if (type == NameType::STATIC_STRING || type == NameType::SELF_POINTER) {
name_.static_name = static_name;
} else {
name_.hash_or_id = hash_or_id;
@@ -367,10 +395,14 @@ class Scheduler {
// Name type must match
if (item->get_name_type() != name_type)
return false;
// For static strings, compare the string content; for hash/ID, compare the value
// STATIC_STRING: compare string content. SELF_POINTER: raw pointer equality (no strcmp).
// Other types: compare hash/ID value.
if (name_type == NameType::STATIC_STRING) {
return this->names_match_static_(item->get_name(), static_name);
}
if (name_type == NameType::SELF_POINTER) {
return item->name_.static_name == static_name;
}
return item->get_name_hash_or_id() == hash_or_id;
}