Files
esphome/tests/integration/fixtures/scheduler_null_name.yaml
T
J. Nick Koston 982998c8fb [scheduler] Replace unique_ptr with raw pointers, add leak detection
The scheduler was already managing SchedulerItem lifecycle explicitly
through its object pool (recycle_item_main_loop_ / get_item_from_pool_locked_).
The unique_ptr wrapper added overhead (11 destructor call sites on the hot path)
without providing safety — if a lifecycle path was missed, the unique_ptr would
silently delete the item and cause needless heap allocations instead of pool reuse.

Replace unique_ptr<SchedulerItem, SchedulerItemDeleter> with raw SchedulerItem*
throughout. Every item is now explicitly recycled to the pool or deleted via
delete_item_(). This eliminates all 11 unique_ptr destructor calls from the hot
path and saves ~256 bytes of firmware.

Add debug leak detection under ESPHOME_DEBUG_SCHEDULER: a live-item counter
verified at the end of every call() cycle asserts that all allocated items are
accounted for in items_, to_add_, defer_queue_, or the pool. This turns silent
heap churn from missed lifecycle management into an immediate assert failure
caught by integration tests.

Also moves the retry-cancelled check before item allocation in set_timer_common_
to avoid needless alloc+delete on the cold retry path, and fixes a thread-safety
issue where recycle_item_main_loop_ (main-loop-only) was called from
set_timer_common_ which can run on non-main-loop threads.

Enable debug_scheduler: true in all 18 scheduler integration test fixtures.
2026-03-08 00:08:22 -10:00

45 lines
1.7 KiB
YAML

esphome:
debug_scheduler: true # Enable scheduler leak detection
name: scheduler-null-name
host:
logger:
level: DEBUG
api:
services:
- service: test_null_name
then:
- lambda: |-
// First, create a scenario that would trigger the crash
// The crash happens when defer() is called with a name that would be cancelled
// Test 1: Create a defer with a valid name
App.scheduler.set_timeout(nullptr, "test_defer", 0, []() {
ESP_LOGI("TEST", "First defer should be cancelled");
});
// Test 2: Create another defer with the same name - this triggers cancel_item_locked_
// In the unfixed code, this would crash if the name was NULL
App.scheduler.set_timeout(nullptr, "test_defer", 0, []() {
ESP_LOGI("TEST", "Second defer executed");
});
// Test 3: Now test with nullptr - this is the actual crash scenario
// Create a defer item without a name (like voice assistant does)
const char* null_name = nullptr;
App.scheduler.set_timeout(nullptr, null_name, 0, []() {
ESP_LOGI("TEST", "Defer with null name executed");
});
// Test 4: Create another defer with null name - this would trigger the crash
App.scheduler.set_timeout(nullptr, null_name, 0, []() {
ESP_LOGI("TEST", "Second null defer executed");
});
// Test 5: Verify scheduler still works
App.scheduler.set_timeout(nullptr, "valid_timeout", 50, []() {
ESP_LOGI("TEST", "Test completed successfully");
});