From 56f6d7bc6dae12aa329f4ce26b27a0e7a45b9ae9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 7 Sep 2026 16:38:56 +0200 Subject: [PATCH] [core] Add UnavoidableBlockingScope for blocking that cannot be shortened Some work has no shorter form: bringing up a radio, the first connect of a network stack, a key generation whose cost is the algorithm. Wrapping it in this scope moves the loop pass start forward by its duration, so the blocking warning keeps reporting everything else in the pass and the component's threshold does not ratchet over it. The comment says what it is for and that it must never hide code that could be made faster. --- esphome/core/application.h | 30 ++++++++++++++++ tests/components/core/test_blocking_scope.cpp | 34 +++++++++++++++++++ 2 files changed, 64 insertions(+) create mode 100644 tests/components/core/test_blocking_scope.cpp diff --git a/esphome/core/application.h b/esphome/core/application.h index a12cdc4ac8..20fdef190d 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -389,6 +389,7 @@ class Application { friend Component; friend class Scheduler; friend class LoopBlockingGuard; + friend class UnavoidableBlockingScope; #ifdef USE_RUNTIME_STATS friend class runtime_stats::RuntimeStatsCollector; #endif @@ -631,6 +632,35 @@ class LoopBlockingGuard { static void __attribute__((noinline, cold)) warn_blocking(uint32_t blocking_time); }; +/// Leaves a stretch of the current loop pass out of the blocking warning. +/// +/// For work that cannot be made shorter or split across passes, such as +/// bringing up a radio, the initial connect of a network stack, or a key +/// generation whose cost is the algorithm itself; the warning then keeps +/// reporting everything else in the pass, and the component's threshold does +/// not ratchet up over such an operation. It must never wrap work that could +/// be made faster or moved off the loop: that is exactly what the warning +/// exists to find. +/// +/// { +/// UnavoidableBlockingScope scope; +/// bring_up_radio(); +/// } +class UnavoidableBlockingScope { + public: + UnavoidableBlockingScope() : started_(millis()) {} + ~UnavoidableBlockingScope() { + // Move the pass start forward by the time spent here; the guard measures + // from that start, and components reading it as "now" get a fresher value + App.set_loop_component_start_time_(App.get_loop_component_start_time() + (millis() - this->started_)); + } + UnavoidableBlockingScope(const UnavoidableBlockingScope &) = delete; + UnavoidableBlockingScope &operator=(const UnavoidableBlockingScope &) = delete; + + private: + uint32_t started_; +}; + // Phase A: drain wake notifications and run the scheduler. Invoked on every // Application::loop() tick regardless of whether a component phase runs, so // scheduler items fire at their requested cadence even when the caller has diff --git a/tests/components/core/test_blocking_scope.cpp b/tests/components/core/test_blocking_scope.cpp new file mode 100644 index 0000000000..aa2895f8e3 --- /dev/null +++ b/tests/components/core/test_blocking_scope.cpp @@ -0,0 +1,34 @@ +#include + +#include "esphome/core/application.h" +#include "esphome/core/hal.h" + +namespace esphome { + +// The scope must push the pass start forward by the time it covers and by +// nothing else, so the blocking guard sees only the work outside it +TEST(UnavoidableBlockingScope, ExcludesItsDurationFromThePass) { + const uint32_t pass_start = millis(); + LoopBlockingGuard guard(nullptr, nullptr, pass_start); + ASSERT_EQ(App.get_loop_component_start_time(), pass_start); + + const uint32_t before = millis(); + { + UnavoidableBlockingScope scope; + delay(30); + } + const uint32_t excused = millis() - before; + + const uint32_t moved = App.get_loop_component_start_time() - pass_start; + EXPECT_GE(moved, 30u); + EXPECT_LE(moved, excused); +} + +TEST(UnavoidableBlockingScope, ZeroLengthScopeLeavesTheStartAlone) { + const uint32_t pass_start = millis(); + LoopBlockingGuard guard(nullptr, nullptr, pass_start); + { UnavoidableBlockingScope scope; } + EXPECT_LE(App.get_loop_component_start_time() - pass_start, 1u); +} + +} // namespace esphome