diff --git a/esphome/core/component.h b/esphome/core/component.h index 5fdf23e128..64f9971627 100644 --- a/esphome/core/component.h +++ b/esphome/core/component.h @@ -594,12 +594,17 @@ class WarnIfComponentBlockingGuard { // Inlined: the fast path is just millis() + subtract + compare inline uint32_t HOT finish() { uint32_t curr_time = millis(); - uint32_t blocking_time = curr_time - this->started_; #ifdef USE_RUNTIME_STATS this->record_runtime_stats_(); #endif - if (blocking_time > WARN_IF_BLOCKING_OVER_MS) [[unlikely]] { - warn_blocking(this->component_, blocking_time); + // Guard against underflow: if curr_time < started_, the subtraction wraps + // to a huge value. This can happen when the scheduler passes a `now` value + // slightly ahead of real millis() (e.g. from execute_item_ return values). + if (curr_time >= this->started_) [[likely]] { + uint32_t blocking_time = curr_time - this->started_; + if (blocking_time > WARN_IF_BLOCKING_OVER_MS) [[unlikely]] { + warn_blocking(this->component_, blocking_time); + } } return curr_time; } diff --git a/tests/benchmarks/core/bench_scheduler.cpp b/tests/benchmarks/core/bench_scheduler.cpp index 1b78c44376..160ab3c123 100644 --- a/tests/benchmarks/core/bench_scheduler.cpp +++ b/tests/benchmarks/core/bench_scheduler.cpp @@ -67,21 +67,16 @@ static void Scheduler_Call_5IntervalsFiring(benchmark::State &state) { int fire_count = 0; // Add 5 intervals with 1ms period — they fire every call when time advances. - // No inner loop needed: 5 heap pops + 5 callbacks + 5 heap pushes per call - // is well above CodSpeed's ~60ns instrumentation overhead. + // We use monotonically increasing fake time (now++) so intervals reliably fire. + // The underflow guard in WarnIfComponentBlockingGuard::finish() (curr_time >= started_) + // prevents warn_blocking from firing when fake time exceeds real millis(). // Note: interval=0 causes infinite loop (reschedules at same now, never breaks). for (int i = 0; i < 5; i++) { scheduler.set_interval(&dummy_component, static_cast(i), 1, [&fire_count]() { fire_count++; }); } scheduler.process_to_add(); - // Monotonically increasing fake time so intervals are due every call. - // Can't use real millis() — it doesn't advance fast enough between calls. - // Warm-up call outside the benchmark to trigger the blocking guard once - // and ramp the component's warn_if_blocking_over_ threshold to max. uint32_t now = millis() + 100; - scheduler.call(now); - now++; for (auto _ : state) { scheduler.call(now);