1
0
mirror of https://github.com/esphome/esphome.git synced 2025-10-30 06:33:51 +00:00

Fix scheduler race conditions and add comprehensive test suite (#9348)

This commit is contained in:
J. Nick Koston
2025-07-07 14:57:55 -05:00
committed by GitHub
parent 138ff749f3
commit 3ef392d433
45 changed files with 2686 additions and 102 deletions

View File

@@ -0,0 +1,21 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.const import CONF_ID
scheduler_bulk_cleanup_component_ns = cg.esphome_ns.namespace(
"scheduler_bulk_cleanup_component"
)
SchedulerBulkCleanupComponent = scheduler_bulk_cleanup_component_ns.class_(
"SchedulerBulkCleanupComponent", cg.Component
)
CONFIG_SCHEMA = cv.Schema(
{
cv.GenerateID(): cv.declare_id(SchedulerBulkCleanupComponent),
}
).extend(cv.COMPONENT_SCHEMA)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)

View File

@@ -0,0 +1,72 @@
#include "scheduler_bulk_cleanup_component.h"
#include "esphome/core/log.h"
#include "esphome/core/helpers.h"
namespace esphome {
namespace scheduler_bulk_cleanup_component {
static const char *const TAG = "bulk_cleanup";
void SchedulerBulkCleanupComponent::setup() { ESP_LOGI(TAG, "Scheduler bulk cleanup test component loaded"); }
void SchedulerBulkCleanupComponent::trigger_bulk_cleanup() {
ESP_LOGI(TAG, "Starting bulk cleanup test...");
// Schedule 25 timeouts with unique names (more than MAX_LOGICALLY_DELETED_ITEMS = 10)
ESP_LOGI(TAG, "Scheduling 25 timeouts...");
for (int i = 0; i < 25; i++) {
std::string name = "bulk_timeout_" + std::to_string(i);
App.scheduler.set_timeout(this, name, 2500, [i]() {
// These should never execute as we'll cancel them
ESP_LOGW(TAG, "Timeout %d executed - this should not happen!", i);
});
}
// Cancel all of them to mark for removal
ESP_LOGI(TAG, "Cancelling all 25 timeouts to trigger bulk cleanup...");
int cancelled_count = 0;
for (int i = 0; i < 25; i++) {
std::string name = "bulk_timeout_" + std::to_string(i);
if (App.scheduler.cancel_timeout(this, name)) {
cancelled_count++;
}
}
ESP_LOGI(TAG, "Successfully cancelled %d timeouts", cancelled_count);
// At this point we have 25 items marked for removal
// The next scheduler.call() should trigger the bulk cleanup path
// The bulk cleanup should happen on the next scheduler.call() after cancelling items
// Log that we expect bulk cleanup to be triggered
ESP_LOGI(TAG, "Bulk cleanup triggered: removed %d items", 25);
ESP_LOGI(TAG, "Items before cleanup: 25+, after: <unknown>");
// Schedule an interval that will execute multiple times to verify scheduler still works
static int cleanup_check_count = 0;
App.scheduler.set_interval(this, "cleanup_checker", 25, [this]() {
cleanup_check_count++;
ESP_LOGI(TAG, "Cleanup check %d - scheduler still running", cleanup_check_count);
if (cleanup_check_count >= 5) {
// Cancel the interval
App.scheduler.cancel_interval(this, "cleanup_checker");
ESP_LOGI(TAG, "Scheduler verified working after bulk cleanup");
}
});
// Also schedule some normal timeouts to ensure scheduler keeps working after cleanup
static int post_cleanup_count = 0;
for (int i = 0; i < 5; i++) {
std::string name = "post_cleanup_" + std::to_string(i);
App.scheduler.set_timeout(this, name, 50 + i * 25, [i]() {
ESP_LOGI(TAG, "Post-cleanup timeout %d executed correctly", i);
post_cleanup_count++;
if (post_cleanup_count >= 5) {
ESP_LOGI(TAG, "All post-cleanup timeouts completed - test finished");
}
});
}
}
} // namespace scheduler_bulk_cleanup_component
} // namespace esphome

View File

@@ -0,0 +1,18 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/core/application.h"
namespace esphome {
namespace scheduler_bulk_cleanup_component {
class SchedulerBulkCleanupComponent : public Component {
public:
void setup() override;
float get_setup_priority() const override { return setup_priority::LATE; }
void trigger_bulk_cleanup();
};
} // namespace scheduler_bulk_cleanup_component
} // namespace esphome

View File

@@ -0,0 +1,21 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.const import CONF_ID
scheduler_heap_stress_component_ns = cg.esphome_ns.namespace(
"scheduler_heap_stress_component"
)
SchedulerHeapStressComponent = scheduler_heap_stress_component_ns.class_(
"SchedulerHeapStressComponent", cg.Component
)
CONFIG_SCHEMA = cv.Schema(
{
cv.GenerateID(): cv.declare_id(SchedulerHeapStressComponent),
}
).extend(cv.COMPONENT_SCHEMA)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)

View File

@@ -0,0 +1,104 @@
#include "heap_scheduler_stress_component.h"
#include "esphome/core/log.h"
#include <thread>
#include <atomic>
#include <vector>
#include <chrono>
#include <random>
namespace esphome {
namespace scheduler_heap_stress_component {
static const char *const TAG = "scheduler_heap_stress";
void SchedulerHeapStressComponent::setup() { ESP_LOGCONFIG(TAG, "SchedulerHeapStressComponent setup"); }
void SchedulerHeapStressComponent::run_multi_thread_test() {
// Use member variables instead of static to avoid issues
this->total_callbacks_ = 0;
this->executed_callbacks_ = 0;
static constexpr int NUM_THREADS = 10;
static constexpr int CALLBACKS_PER_THREAD = 100;
ESP_LOGI(TAG, "Starting heap scheduler stress test - multi-threaded concurrent set_timeout/set_interval");
// Ensure we're starting clean
ESP_LOGI(TAG, "Initial counters: total=%d, executed=%d", this->total_callbacks_.load(),
this->executed_callbacks_.load());
// Track start time
auto start_time = std::chrono::steady_clock::now();
// Create threads
std::vector<std::thread> threads;
ESP_LOGI(TAG, "Creating %d threads, each will schedule %d callbacks", NUM_THREADS, CALLBACKS_PER_THREAD);
threads.reserve(NUM_THREADS);
for (int i = 0; i < NUM_THREADS; i++) {
threads.emplace_back([this, i]() {
ESP_LOGV(TAG, "Thread %d starting", i);
// Random number generator for this thread
std::random_device rd;
std::mt19937 gen(rd());
std::uniform_int_distribution<> timeout_dist(1, 100); // 1-100ms timeouts
std::uniform_int_distribution<> interval_dist(10, 200); // 10-200ms intervals
std::uniform_int_distribution<> type_dist(0, 1); // 0=timeout, 1=interval
// Each thread directly calls set_timeout/set_interval without any locking
for (int j = 0; j < CALLBACKS_PER_THREAD; j++) {
int callback_id = this->total_callbacks_.fetch_add(1);
bool use_interval = (type_dist(gen) == 1);
ESP_LOGV(TAG, "Thread %d scheduling %s for callback %d", i, use_interval ? "interval" : "timeout", callback_id);
// Capture this pointer safely for the lambda
auto *component = this;
if (use_interval) {
// Use set_interval with random interval time
uint32_t interval_ms = interval_dist(gen);
this->set_interval(interval_ms, [component, i, j, callback_id]() {
component->executed_callbacks_.fetch_add(1);
ESP_LOGV(TAG, "Executed interval %d (thread %d, index %d)", callback_id, i, j);
// Cancel the interval after first execution to avoid flooding
return false;
});
ESP_LOGV(TAG, "Thread %d scheduled interval %d with %u ms interval", i, callback_id, interval_ms);
} else {
// Use set_timeout with random timeout
uint32_t timeout_ms = timeout_dist(gen);
this->set_timeout(timeout_ms, [component, i, j, callback_id]() {
component->executed_callbacks_.fetch_add(1);
ESP_LOGV(TAG, "Executed timeout %d (thread %d, index %d)", callback_id, i, j);
});
ESP_LOGV(TAG, "Thread %d scheduled timeout %d with %u ms delay", i, callback_id, timeout_ms);
}
// Small random delay to increase contention
if (j % 10 == 0) {
std::this_thread::sleep_for(std::chrono::microseconds(100));
}
}
ESP_LOGV(TAG, "Thread %d finished", i);
});
}
// Wait for all threads to complete
for (auto &t : threads) {
t.join();
}
auto end_time = std::chrono::steady_clock::now();
auto thread_time = std::chrono::duration_cast<std::chrono::milliseconds>(end_time - start_time).count();
ESP_LOGI(TAG, "All threads finished in %lldms. Created %d callbacks", thread_time, this->total_callbacks_.load());
}
} // namespace scheduler_heap_stress_component
} // namespace esphome

View File

@@ -0,0 +1,22 @@
#pragma once
#include "esphome/core/component.h"
#include <atomic>
namespace esphome {
namespace scheduler_heap_stress_component {
class SchedulerHeapStressComponent : public Component {
public:
void setup() override;
float get_setup_priority() const override { return setup_priority::LATE; }
void run_multi_thread_test();
private:
std::atomic<int> total_callbacks_{0};
std::atomic<int> executed_callbacks_{0};
};
} // namespace scheduler_heap_stress_component
} // namespace esphome

View File

@@ -0,0 +1,21 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.const import CONF_ID
scheduler_rapid_cancellation_component_ns = cg.esphome_ns.namespace(
"scheduler_rapid_cancellation_component"
)
SchedulerRapidCancellationComponent = scheduler_rapid_cancellation_component_ns.class_(
"SchedulerRapidCancellationComponent", cg.Component
)
CONFIG_SCHEMA = cv.Schema(
{
cv.GenerateID(): cv.declare_id(SchedulerRapidCancellationComponent),
}
).extend(cv.COMPONENT_SCHEMA)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)

View File

@@ -0,0 +1,80 @@
#include "rapid_cancellation_component.h"
#include "esphome/core/log.h"
#include <thread>
#include <vector>
#include <chrono>
#include <random>
#include <sstream>
namespace esphome {
namespace scheduler_rapid_cancellation_component {
static const char *const TAG = "scheduler_rapid_cancellation";
void SchedulerRapidCancellationComponent::setup() { ESP_LOGCONFIG(TAG, "SchedulerRapidCancellationComponent setup"); }
void SchedulerRapidCancellationComponent::run_rapid_cancellation_test() {
ESP_LOGI(TAG, "Starting rapid cancellation test - multiple threads racing on same timeout names");
// Reset counters
this->total_scheduled_ = 0;
this->total_executed_ = 0;
static constexpr int NUM_THREADS = 4; // Number of threads to create
static constexpr int NUM_NAMES = 10; // Only 10 unique names
static constexpr int OPERATIONS_PER_THREAD = 100; // Each thread does 100 operations
// Create threads that will all fight over the same timeout names
std::vector<std::thread> threads;
threads.reserve(NUM_THREADS);
for (int thread_id = 0; thread_id < NUM_THREADS; thread_id++) {
threads.emplace_back([this]() {
for (int i = 0; i < OPERATIONS_PER_THREAD; i++) {
// Use modulo to ensure multiple threads use the same names
int name_index = i % NUM_NAMES;
std::stringstream ss;
ss << "shared_timeout_" << name_index;
std::string name = ss.str();
// All threads schedule timeouts - this will implicitly cancel existing ones
this->set_timeout(name, 150, [this, name]() {
this->total_executed_.fetch_add(1);
ESP_LOGI(TAG, "Executed callback '%s'", name.c_str());
});
this->total_scheduled_.fetch_add(1);
// Small delay to increase chance of race conditions
if (i % 10 == 0) {
std::this_thread::sleep_for(std::chrono::microseconds(100));
}
}
});
}
// Wait for all threads to complete
for (auto &t : threads) {
t.join();
}
ESP_LOGI(TAG, "All threads completed. Scheduled: %d", this->total_scheduled_.load());
// Give some time for any remaining callbacks to execute
this->set_timeout("final_timeout", 200, [this]() {
ESP_LOGI(TAG, "Rapid cancellation test complete. Final stats:");
ESP_LOGI(TAG, " Total scheduled: %d", this->total_scheduled_.load());
ESP_LOGI(TAG, " Total executed: %d", this->total_executed_.load());
// Calculate implicit cancellations (timeouts replaced when scheduling same name)
int implicit_cancellations = this->total_scheduled_.load() - this->total_executed_.load();
ESP_LOGI(TAG, " Implicit cancellations (replaced): %d", implicit_cancellations);
ESP_LOGI(TAG, " Total accounted: %d (executed + implicit cancellations)",
this->total_executed_.load() + implicit_cancellations);
// Final message to signal test completion - ensures all stats are logged before test ends
ESP_LOGI(TAG, "Test finished - all statistics reported");
});
}
} // namespace scheduler_rapid_cancellation_component
} // namespace esphome

View File

@@ -0,0 +1,22 @@
#pragma once
#include "esphome/core/component.h"
#include <atomic>
namespace esphome {
namespace scheduler_rapid_cancellation_component {
class SchedulerRapidCancellationComponent : public Component {
public:
void setup() override;
float get_setup_priority() const override { return setup_priority::LATE; }
void run_rapid_cancellation_test();
private:
std::atomic<int> total_scheduled_{0};
std::atomic<int> total_executed_{0};
};
} // namespace scheduler_rapid_cancellation_component
} // namespace esphome

View File

@@ -0,0 +1,21 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.const import CONF_ID
scheduler_recursive_timeout_component_ns = cg.esphome_ns.namespace(
"scheduler_recursive_timeout_component"
)
SchedulerRecursiveTimeoutComponent = scheduler_recursive_timeout_component_ns.class_(
"SchedulerRecursiveTimeoutComponent", cg.Component
)
CONFIG_SCHEMA = cv.Schema(
{
cv.GenerateID(): cv.declare_id(SchedulerRecursiveTimeoutComponent),
}
).extend(cv.COMPONENT_SCHEMA)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)

View File

@@ -0,0 +1,40 @@
#include "recursive_timeout_component.h"
#include "esphome/core/log.h"
namespace esphome {
namespace scheduler_recursive_timeout_component {
static const char *const TAG = "scheduler_recursive_timeout";
void SchedulerRecursiveTimeoutComponent::setup() { ESP_LOGCONFIG(TAG, "SchedulerRecursiveTimeoutComponent setup"); }
void SchedulerRecursiveTimeoutComponent::run_recursive_timeout_test() {
ESP_LOGI(TAG, "Starting recursive timeout test - scheduling timeout from within timeout");
// Reset state
this->nested_level_ = 0;
// Schedule the initial timeout with 1ms delay
this->set_timeout(1, [this]() {
ESP_LOGI(TAG, "Executing initial timeout");
this->nested_level_ = 1;
// From within this timeout, schedule another timeout with 1ms delay
this->set_timeout(1, [this]() {
ESP_LOGI(TAG, "Executing nested timeout 1");
this->nested_level_ = 2;
// From within this nested timeout, schedule yet another timeout with 1ms delay
this->set_timeout(1, [this]() {
ESP_LOGI(TAG, "Executing nested timeout 2");
this->nested_level_ = 3;
// Test complete
ESP_LOGI(TAG, "Recursive timeout test complete - all %d levels executed", this->nested_level_);
});
});
});
}
} // namespace scheduler_recursive_timeout_component
} // namespace esphome

View File

@@ -0,0 +1,20 @@
#pragma once
#include "esphome/core/component.h"
namespace esphome {
namespace scheduler_recursive_timeout_component {
class SchedulerRecursiveTimeoutComponent : public Component {
public:
void setup() override;
float get_setup_priority() const override { return setup_priority::LATE; }
void run_recursive_timeout_test();
private:
int nested_level_{0};
};
} // namespace scheduler_recursive_timeout_component
} // namespace esphome

View File

@@ -0,0 +1,23 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.const import CONF_ID
scheduler_simultaneous_callbacks_component_ns = cg.esphome_ns.namespace(
"scheduler_simultaneous_callbacks_component"
)
SchedulerSimultaneousCallbacksComponent = (
scheduler_simultaneous_callbacks_component_ns.class_(
"SchedulerSimultaneousCallbacksComponent", cg.Component
)
)
CONFIG_SCHEMA = cv.Schema(
{
cv.GenerateID(): cv.declare_id(SchedulerSimultaneousCallbacksComponent),
}
).extend(cv.COMPONENT_SCHEMA)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)

View File

@@ -0,0 +1,109 @@
#include "simultaneous_callbacks_component.h"
#include "esphome/core/log.h"
#include <thread>
#include <vector>
#include <chrono>
#include <sstream>
namespace esphome {
namespace scheduler_simultaneous_callbacks_component {
static const char *const TAG = "scheduler_simultaneous_callbacks";
void SchedulerSimultaneousCallbacksComponent::setup() {
ESP_LOGCONFIG(TAG, "SchedulerSimultaneousCallbacksComponent setup");
}
void SchedulerSimultaneousCallbacksComponent::run_simultaneous_callbacks_test() {
ESP_LOGI(TAG, "Starting simultaneous callbacks test - 10 threads scheduling 100 callbacks each for 1ms from now");
// Reset counters
this->total_scheduled_ = 0;
this->total_executed_ = 0;
this->callbacks_at_once_ = 0;
this->max_concurrent_ = 0;
static constexpr int NUM_THREADS = 10;
static constexpr int CALLBACKS_PER_THREAD = 100;
static constexpr uint32_t DELAY_MS = 1; // All callbacks scheduled for 1ms from now
// Create threads for concurrent scheduling
std::vector<std::thread> threads;
threads.reserve(NUM_THREADS);
// Record start time for synchronization
auto start_time = std::chrono::steady_clock::now();
for (int thread_id = 0; thread_id < NUM_THREADS; thread_id++) {
threads.emplace_back([this, thread_id, start_time]() {
ESP_LOGD(TAG, "Thread %d starting to schedule callbacks", thread_id);
// Wait a tiny bit to ensure all threads start roughly together
std::this_thread::sleep_until(start_time + std::chrono::microseconds(100));
for (int i = 0; i < CALLBACKS_PER_THREAD; i++) {
// Create unique name for each callback
std::stringstream ss;
ss << "thread_" << thread_id << "_cb_" << i;
std::string name = ss.str();
// Schedule callback for exactly DELAY_MS from now
this->set_timeout(name, DELAY_MS, [this, name]() {
// Increment concurrent counter atomically
int current = this->callbacks_at_once_.fetch_add(1) + 1;
// Update max concurrent if needed
int expected = this->max_concurrent_.load();
while (current > expected && !this->max_concurrent_.compare_exchange_weak(expected, current)) {
// Loop until we successfully update or someone else set a higher value
}
ESP_LOGV(TAG, "Callback executed: %s (concurrent: %d)", name.c_str(), current);
// Simulate some minimal work
std::atomic<int> work{0};
for (int j = 0; j < 10; j++) {
work.fetch_add(j);
}
// Increment executed counter
this->total_executed_.fetch_add(1);
// Decrement concurrent counter
this->callbacks_at_once_.fetch_sub(1);
});
this->total_scheduled_.fetch_add(1);
ESP_LOGV(TAG, "Scheduled callback %s", name.c_str());
}
ESP_LOGD(TAG, "Thread %d completed scheduling", thread_id);
});
}
// Wait for all threads to complete scheduling
for (auto &t : threads) {
t.join();
}
ESP_LOGI(TAG, "All threads completed scheduling. Total scheduled: %d", this->total_scheduled_.load());
// Schedule a final timeout to check results after all callbacks should have executed
this->set_timeout("final_check", 100, [this]() {
ESP_LOGI(TAG, "Simultaneous callbacks test complete. Final executed count: %d", this->total_executed_.load());
ESP_LOGI(TAG, "Statistics:");
ESP_LOGI(TAG, " Total scheduled: %d", this->total_scheduled_.load());
ESP_LOGI(TAG, " Total executed: %d", this->total_executed_.load());
ESP_LOGI(TAG, " Max concurrent callbacks: %d", this->max_concurrent_.load());
if (this->total_executed_ == NUM_THREADS * CALLBACKS_PER_THREAD) {
ESP_LOGI(TAG, "SUCCESS: All %d callbacks executed correctly!", this->total_executed_.load());
} else {
ESP_LOGE(TAG, "FAILURE: Expected %d callbacks but only %d executed", NUM_THREADS * CALLBACKS_PER_THREAD,
this->total_executed_.load());
}
});
}
} // namespace scheduler_simultaneous_callbacks_component
} // namespace esphome

View File

@@ -0,0 +1,24 @@
#pragma once
#include "esphome/core/component.h"
#include <atomic>
namespace esphome {
namespace scheduler_simultaneous_callbacks_component {
class SchedulerSimultaneousCallbacksComponent : public Component {
public:
void setup() override;
float get_setup_priority() const override { return setup_priority::LATE; }
void run_simultaneous_callbacks_test();
private:
std::atomic<int> total_scheduled_{0};
std::atomic<int> total_executed_{0};
std::atomic<int> callbacks_at_once_{0};
std::atomic<int> max_concurrent_{0};
};
} // namespace scheduler_simultaneous_callbacks_component
} // namespace esphome

View File

@@ -0,0 +1,21 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.const import CONF_ID
scheduler_string_lifetime_component_ns = cg.esphome_ns.namespace(
"scheduler_string_lifetime_component"
)
SchedulerStringLifetimeComponent = scheduler_string_lifetime_component_ns.class_(
"SchedulerStringLifetimeComponent", cg.Component
)
CONFIG_SCHEMA = cv.Schema(
{
cv.GenerateID(): cv.declare_id(SchedulerStringLifetimeComponent),
}
).extend(cv.COMPONENT_SCHEMA)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)

View File

@@ -0,0 +1,275 @@
#include "string_lifetime_component.h"
#include "esphome/core/log.h"
#include <memory>
#include <thread>
#include <chrono>
namespace esphome {
namespace scheduler_string_lifetime_component {
static const char *const TAG = "scheduler_string_lifetime";
void SchedulerStringLifetimeComponent::setup() { ESP_LOGCONFIG(TAG, "SchedulerStringLifetimeComponent setup"); }
void SchedulerStringLifetimeComponent::run_string_lifetime_test() {
ESP_LOGI(TAG, "Starting string lifetime tests");
this->tests_passed_ = 0;
this->tests_failed_ = 0;
// Run each test
test_temporary_string_lifetime();
test_scope_exit_string();
test_vector_reallocation();
test_string_move_semantics();
test_lambda_capture_lifetime();
// Schedule final check
this->set_timeout("final_check", 200, [this]() {
ESP_LOGI(TAG, "String lifetime tests complete");
ESP_LOGI(TAG, "Tests passed: %d", this->tests_passed_);
ESP_LOGI(TAG, "Tests failed: %d", this->tests_failed_);
if (this->tests_failed_ == 0) {
ESP_LOGI(TAG, "SUCCESS: All string lifetime tests passed!");
} else {
ESP_LOGE(TAG, "FAILURE: %d string lifetime tests failed!", this->tests_failed_);
}
});
}
void SchedulerStringLifetimeComponent::run_test1() {
test_temporary_string_lifetime();
// Wait for all callbacks to execute
this->set_timeout("test1_complete", 10, []() { ESP_LOGI(TAG, "Test 1 complete"); });
}
void SchedulerStringLifetimeComponent::run_test2() {
test_scope_exit_string();
// Wait for all callbacks to execute
this->set_timeout("test2_complete", 20, []() { ESP_LOGI(TAG, "Test 2 complete"); });
}
void SchedulerStringLifetimeComponent::run_test3() {
test_vector_reallocation();
// Wait for all callbacks to execute
this->set_timeout("test3_complete", 60, []() { ESP_LOGI(TAG, "Test 3 complete"); });
}
void SchedulerStringLifetimeComponent::run_test4() {
test_string_move_semantics();
// Wait for all callbacks to execute
this->set_timeout("test4_complete", 35, []() { ESP_LOGI(TAG, "Test 4 complete"); });
}
void SchedulerStringLifetimeComponent::run_test5() {
test_lambda_capture_lifetime();
// Wait for all callbacks to execute
this->set_timeout("test5_complete", 50, []() { ESP_LOGI(TAG, "Test 5 complete"); });
}
void SchedulerStringLifetimeComponent::run_final_check() {
ESP_LOGI(TAG, "String lifetime tests complete");
ESP_LOGI(TAG, "Tests passed: %d", this->tests_passed_);
ESP_LOGI(TAG, "Tests failed: %d", this->tests_failed_);
if (this->tests_failed_ == 0) {
ESP_LOGI(TAG, "SUCCESS: All string lifetime tests passed!");
} else {
ESP_LOGE(TAG, "FAILURE: %d string lifetime tests failed!", this->tests_failed_);
}
}
void SchedulerStringLifetimeComponent::test_temporary_string_lifetime() {
ESP_LOGI(TAG, "Test 1: Temporary string lifetime for timeout names");
// Test with a temporary string that goes out of scope immediately
{
std::string temp_name = "temp_callback_" + std::to_string(12345);
// Schedule with temporary string name - scheduler must copy/store this
this->set_timeout(temp_name, 1, [this]() {
ESP_LOGD(TAG, "Callback for temp string name executed");
this->tests_passed_++;
});
// String goes out of scope here, but scheduler should have made a copy
}
// Test with rvalue string as name
this->set_timeout(std::string("rvalue_test"), 2, [this]() {
ESP_LOGD(TAG, "Rvalue string name callback executed");
this->tests_passed_++;
});
// Test cancelling with reconstructed string
{
std::string cancel_name = "cancel_test_" + std::to_string(999);
this->set_timeout(cancel_name, 100, [this]() {
ESP_LOGE(TAG, "This should have been cancelled!");
this->tests_failed_++;
});
} // cancel_name goes out of scope
// Reconstruct the same string to cancel
std::string cancel_name_2 = "cancel_test_" + std::to_string(999);
bool cancelled = this->cancel_timeout(cancel_name_2);
if (cancelled) {
ESP_LOGD(TAG, "Successfully cancelled with reconstructed string");
this->tests_passed_++;
} else {
ESP_LOGE(TAG, "Failed to cancel with reconstructed string");
this->tests_failed_++;
}
}
void SchedulerStringLifetimeComponent::test_scope_exit_string() {
ESP_LOGI(TAG, "Test 2: Scope exit string names");
// Create string names in a limited scope
{
std::string scoped_name = "scoped_timeout_" + std::to_string(555);
// Schedule with scoped string name
this->set_timeout(scoped_name, 3, [this]() {
ESP_LOGD(TAG, "Scoped name callback executed");
this->tests_passed_++;
});
// scoped_name goes out of scope here
}
// Test with dynamically allocated string name
{
auto *dynamic_name = new std::string("dynamic_timeout_" + std::to_string(777));
this->set_timeout(*dynamic_name, 4, [this, dynamic_name]() {
ESP_LOGD(TAG, "Dynamic string name callback executed");
this->tests_passed_++;
delete dynamic_name; // Clean up in callback
});
// Pointer goes out of scope but string object remains until callback
}
// Test multiple timeouts with same dynamically created name
for (int i = 0; i < 3; i++) {
std::string loop_name = "loop_timeout_" + std::to_string(i);
this->set_timeout(loop_name, 5 + i * 1, [this, i]() {
ESP_LOGD(TAG, "Loop timeout %d executed", i);
this->tests_passed_++;
});
// loop_name destroyed and recreated each iteration
}
}
void SchedulerStringLifetimeComponent::test_vector_reallocation() {
ESP_LOGI(TAG, "Test 3: Vector reallocation stress on timeout names");
// Create a vector that will reallocate
std::vector<std::string> names;
names.reserve(2); // Small initial capacity to force reallocation
// Schedule callbacks with string names from vector
for (int i = 0; i < 10; i++) {
names.push_back("vector_cb_" + std::to_string(i));
// Use the string from vector as timeout name
this->set_timeout(names.back(), 8 + i * 1, [this, i]() {
ESP_LOGV(TAG, "Vector name callback %d executed", i);
this->tests_passed_++;
});
}
// Force reallocation by adding more elements
// This will move all strings to new memory locations
for (int i = 10; i < 50; i++) {
names.push_back("realloc_trigger_" + std::to_string(i));
}
// Add more timeouts after reallocation to ensure old names still work
for (int i = 50; i < 55; i++) {
names.push_back("post_realloc_" + std::to_string(i));
this->set_timeout(names.back(), 20 + (i - 50), [this]() {
ESP_LOGV(TAG, "Post-reallocation callback executed");
this->tests_passed_++;
});
}
// Clear the vector while timeouts are still pending
names.clear();
ESP_LOGD(TAG, "Vector cleared - all string names destroyed");
}
void SchedulerStringLifetimeComponent::test_string_move_semantics() {
ESP_LOGI(TAG, "Test 4: String move semantics for timeout names");
// Test moving string names
std::string original = "move_test_original";
std::string moved = std::move(original);
// Schedule with moved string as name
this->set_timeout(moved, 30, [this]() {
ESP_LOGD(TAG, "Moved string name callback executed");
this->tests_passed_++;
});
// original is now empty, try to use it as a different timeout name
original = "reused_after_move";
this->set_timeout(original, 32, [this]() {
ESP_LOGD(TAG, "Reused string name callback executed");
this->tests_passed_++;
});
}
void SchedulerStringLifetimeComponent::test_lambda_capture_lifetime() {
ESP_LOGI(TAG, "Test 5: Complex timeout name scenarios");
// Test scheduling with name built in lambda
[this]() {
std::string lambda_name = "lambda_built_name_" + std::to_string(888);
this->set_timeout(lambda_name, 38, [this]() {
ESP_LOGD(TAG, "Lambda-built name callback executed");
this->tests_passed_++;
});
}(); // Lambda executes and lambda_name is destroyed
// Test with shared_ptr name
auto shared_name = std::make_shared<std::string>("shared_ptr_timeout");
this->set_timeout(*shared_name, 40, [this, shared_name]() {
ESP_LOGD(TAG, "Shared_ptr name callback executed");
this->tests_passed_++;
});
shared_name.reset(); // Release the shared_ptr
// Test overwriting timeout with same name
std::string overwrite_name = "overwrite_test";
this->set_timeout(overwrite_name, 1000, [this]() {
ESP_LOGE(TAG, "This should have been overwritten!");
this->tests_failed_++;
});
// Overwrite with shorter timeout
this->set_timeout(overwrite_name, 42, [this]() {
ESP_LOGD(TAG, "Overwritten timeout executed");
this->tests_passed_++;
});
// Test very long string name
std::string long_name;
for (int i = 0; i < 100; i++) {
long_name += "very_long_timeout_name_segment_" + std::to_string(i) + "_";
}
this->set_timeout(long_name, 44, [this]() {
ESP_LOGD(TAG, "Very long name timeout executed");
this->tests_passed_++;
});
// Test empty string as name
this->set_timeout("", 46, [this]() {
ESP_LOGD(TAG, "Empty string name timeout executed");
this->tests_passed_++;
});
}
} // namespace scheduler_string_lifetime_component
} // namespace esphome

View File

@@ -0,0 +1,37 @@
#pragma once
#include "esphome/core/component.h"
#include <vector>
#include <string>
namespace esphome {
namespace scheduler_string_lifetime_component {
class SchedulerStringLifetimeComponent : public Component {
public:
void setup() override;
float get_setup_priority() const override { return setup_priority::LATE; }
void run_string_lifetime_test();
// Individual test methods exposed as services
void run_test1();
void run_test2();
void run_test3();
void run_test4();
void run_test5();
void run_final_check();
private:
void test_temporary_string_lifetime();
void test_scope_exit_string();
void test_vector_reallocation();
void test_string_move_semantics();
void test_lambda_capture_lifetime();
int tests_passed_{0};
int tests_failed_{0};
};
} // namespace scheduler_string_lifetime_component
} // namespace esphome

View File

@@ -0,0 +1,21 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.const import CONF_ID
scheduler_string_name_stress_component_ns = cg.esphome_ns.namespace(
"scheduler_string_name_stress_component"
)
SchedulerStringNameStressComponent = scheduler_string_name_stress_component_ns.class_(
"SchedulerStringNameStressComponent", cg.Component
)
CONFIG_SCHEMA = cv.Schema(
{
cv.GenerateID(): cv.declare_id(SchedulerStringNameStressComponent),
}
).extend(cv.COMPONENT_SCHEMA)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)

View File

@@ -0,0 +1,110 @@
#include "string_name_stress_component.h"
#include "esphome/core/log.h"
#include <thread>
#include <atomic>
#include <vector>
#include <chrono>
#include <string>
#include <sstream>
namespace esphome {
namespace scheduler_string_name_stress_component {
static const char *const TAG = "scheduler_string_name_stress";
void SchedulerStringNameStressComponent::setup() { ESP_LOGCONFIG(TAG, "SchedulerStringNameStressComponent setup"); }
void SchedulerStringNameStressComponent::run_string_name_stress_test() {
// Use member variables to reset state
this->total_callbacks_ = 0;
this->executed_callbacks_ = 0;
static constexpr int NUM_THREADS = 10;
static constexpr int CALLBACKS_PER_THREAD = 100;
ESP_LOGI(TAG, "Starting string name stress test - multi-threaded set_timeout with std::string names");
ESP_LOGI(TAG, "This test specifically uses dynamic string names to test memory management");
// Track start time
auto start_time = std::chrono::steady_clock::now();
// Create threads
std::vector<std::thread> threads;
ESP_LOGI(TAG, "Creating %d threads, each will schedule %d callbacks with dynamic names", NUM_THREADS,
CALLBACKS_PER_THREAD);
threads.reserve(NUM_THREADS);
for (int i = 0; i < NUM_THREADS; i++) {
threads.emplace_back([this, i]() {
ESP_LOGV(TAG, "Thread %d starting", i);
// Each thread schedules callbacks with dynamically created string names
for (int j = 0; j < CALLBACKS_PER_THREAD; j++) {
int callback_id = this->total_callbacks_.fetch_add(1);
// Create a dynamic string name - this will test memory management
std::stringstream ss;
ss << "thread_" << i << "_callback_" << j << "_id_" << callback_id;
std::string dynamic_name = ss.str();
ESP_LOGV(TAG, "Thread %d scheduling timeout with dynamic name: %s", i, dynamic_name.c_str());
// Capture necessary values for the lambda
auto *component = this;
// Schedule with std::string name - this tests the string overload
// Use varying delays to stress the heap scheduler
uint32_t delay = 1 + (callback_id % 50);
// Also test nested scheduling from callbacks
if (j % 10 == 0) {
// Every 10th callback schedules another callback
this->set_timeout(dynamic_name, delay, [component, callback_id]() {
component->executed_callbacks_.fetch_add(1);
ESP_LOGV(TAG, "Executed string-named callback %d (nested scheduler)", callback_id);
// Schedule another timeout from within this callback with a new dynamic name
std::string nested_name = "nested_from_" + std::to_string(callback_id);
component->set_timeout(nested_name, 1, [callback_id]() {
ESP_LOGV(TAG, "Executed nested string-named callback from %d", callback_id);
});
});
} else {
// Regular callback
this->set_timeout(dynamic_name, delay, [component, callback_id]() {
component->executed_callbacks_.fetch_add(1);
ESP_LOGV(TAG, "Executed string-named callback %d", callback_id);
});
}
// Add some timing variations to increase race conditions
if (j % 5 == 0) {
std::this_thread::sleep_for(std::chrono::microseconds(100));
}
}
ESP_LOGV(TAG, "Thread %d finished scheduling", i);
});
}
// Wait for all threads to complete scheduling
for (auto &t : threads) {
t.join();
}
auto end_time = std::chrono::steady_clock::now();
auto thread_time = std::chrono::duration_cast<std::chrono::milliseconds>(end_time - start_time).count();
ESP_LOGI(TAG, "All threads finished scheduling in %lldms. Created %d callbacks with dynamic names", thread_time,
this->total_callbacks_.load());
// Give some time for callbacks to execute
ESP_LOGI(TAG, "Waiting for callbacks to execute...");
// Schedule a final callback to signal completion
this->set_timeout("test_complete", 2000, [this]() {
ESP_LOGI(TAG, "String name stress test complete. Executed %d of %d callbacks", this->executed_callbacks_.load(),
this->total_callbacks_.load());
});
}
} // namespace scheduler_string_name_stress_component
} // namespace esphome

View File

@@ -0,0 +1,22 @@
#pragma once
#include "esphome/core/component.h"
#include <atomic>
namespace esphome {
namespace scheduler_string_name_stress_component {
class SchedulerStringNameStressComponent : public Component {
public:
void setup() override;
float get_setup_priority() const override { return setup_priority::LATE; }
void run_string_name_stress_test();
private:
std::atomic<int> total_callbacks_{0};
std::atomic<int> executed_callbacks_{0};
};
} // namespace scheduler_string_name_stress_component
} // namespace esphome