From 885d7c3938c31d7d6c5afdef76368021e1eadf9b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 17 Mar 2026 01:10:57 -1000 Subject: [PATCH] [core] Fix uint32_t underflow in WarnIfComponentBlockingGuard::finish() When curr_time (from millis()) is less than started_ (the `now` passed to scheduler.call()), the subtraction wraps to a huge value (~4 billion). This triggers spurious blocking warnings with nonsensical times. This can happen when the scheduler's execute_item_() returns a millis() value that subsequent items use as their guard start, but the next scheduler.call() passes a `now` value from a slightly different source. Fix by skipping the blocking check when curr_time < started_ (underflow). Also restore the scheduler firing benchmark to use intervals with monotonically increasing fake time, now that the guard handles underflow. --- esphome/core/component.h | 11 ++++++++--- tests/benchmarks/core/bench_scheduler.cpp | 11 +++-------- 2 files changed, 11 insertions(+), 11 deletions(-) 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);