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