mirror of
https://github.com/esphome/esphome.git
synced 2026-10-14 10:12:52 +00:00
[core] Add UnavoidableBlockingScope for blocking that cannot be shortened (#19020)
This commit is contained in:
@@ -390,6 +390,7 @@ class Application {
|
||||
friend Component;
|
||||
friend class Scheduler;
|
||||
friend class LoopBlockingGuard;
|
||||
friend class UnavoidableBlockingScope;
|
||||
#ifdef USE_RUNTIME_STATS
|
||||
friend class runtime_stats::RuntimeStatsCollector;
|
||||
#endif
|
||||
@@ -632,6 +633,38 @@ 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.
|
||||
///
|
||||
/// Only for work that cannot be made shorter or split across passes: radio
|
||||
/// bring-up, a first network connect, a key generation. Work that could be
|
||||
/// cached, deferred, polled or turned into a state machine stays outside, so
|
||||
/// the warning keeps finding it.
|
||||
///
|
||||
/// Affects only work timed by LoopBlockingGuard: loop() and scheduler
|
||||
/// callbacks, not setup(). Main loop task only. The watchdog is not fed, so
|
||||
/// pair long work with watchdog::WatchdogManager. Scopes may nest.
|
||||
/// Afterwards App.get_loop_component_start_time() can be earlier than a
|
||||
/// millis() read taken before the scope; time the work with millis() at both ends.
|
||||
///
|
||||
/// UnavoidableBlockingScope scope;
|
||||
/// this->generate_key_();
|
||||
class UnavoidableBlockingScope {
|
||||
public:
|
||||
UnavoidableBlockingScope() : started_(MillisInternal::get()), pass_start_(App.get_loop_component_start_time()) {}
|
||||
~UnavoidableBlockingScope() {
|
||||
// Move the pass start seen at entry forward by the time spent here, clamped to now
|
||||
const uint32_t now = MillisInternal::get();
|
||||
const uint32_t moved = this->pass_start_ + (now - this->started_);
|
||||
App.set_loop_component_start_time_(static_cast<int32_t>(now - moved) < 0 ? now : moved);
|
||||
}
|
||||
UnavoidableBlockingScope(const UnavoidableBlockingScope &) = delete;
|
||||
UnavoidableBlockingScope &operator=(const UnavoidableBlockingScope &) = delete;
|
||||
|
||||
private:
|
||||
uint32_t started_;
|
||||
uint32_t pass_start_;
|
||||
};
|
||||
|
||||
// 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
|
||||
|
||||
@@ -51,6 +51,7 @@ class MillisInternal {
|
||||
}
|
||||
friend class Application;
|
||||
friend class LoopBlockingGuard;
|
||||
friend class UnavoidableBlockingScope;
|
||||
};
|
||||
|
||||
} // namespace esphome
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#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);
|
||||
const uint32_t before = millis();
|
||||
{ UnavoidableBlockingScope scope; }
|
||||
EXPECT_LE(App.get_loop_component_start_time() - pass_start, millis() - before);
|
||||
}
|
||||
|
||||
// Nested scopes leave out the outer span exactly once, and never move the
|
||||
// start past now
|
||||
TEST(UnavoidableBlockingScope, NestedScopesExcuseTheOuterSpanOnce) {
|
||||
const uint32_t pass_start = millis();
|
||||
LoopBlockingGuard guard(nullptr, nullptr, pass_start);
|
||||
const uint32_t before = millis();
|
||||
{
|
||||
UnavoidableBlockingScope outer;
|
||||
{
|
||||
UnavoidableBlockingScope inner;
|
||||
delay(30);
|
||||
}
|
||||
delay(5);
|
||||
}
|
||||
const uint32_t excused = millis() - before;
|
||||
const uint32_t moved = App.get_loop_component_start_time() - pass_start;
|
||||
EXPECT_GE(moved, 35u);
|
||||
EXPECT_LE(moved, excused);
|
||||
EXPECT_GE(static_cast<int32_t>(millis() - App.get_loop_component_start_time()), 0);
|
||||
}
|
||||
|
||||
namespace {
|
||||
// Static: the guard publishes the component to App and nothing clears it.
|
||||
// One instance per test, since a ratcheted threshold is permanent
|
||||
class DummyComponent : public Component {};
|
||||
DummyComponent &blocking_test_component(size_t index) {
|
||||
static DummyComponent components[2];
|
||||
return components[index];
|
||||
}
|
||||
} // namespace
|
||||
|
||||
// The excused stretch must neither warn nor ratchet the component's threshold
|
||||
TEST(UnavoidableBlockingScope, ExcusedStretchDoesNotRatchetTheThreshold) {
|
||||
DummyComponent &component = blocking_test_component(0);
|
||||
uint32_t threshold_before = 0;
|
||||
component.should_warn_of_blocking(0, threshold_before);
|
||||
{
|
||||
LoopBlockingGuard guard(&component, nullptr, millis());
|
||||
{
|
||||
UnavoidableBlockingScope scope;
|
||||
delay(WARN_IF_BLOCKING_OVER_CS * 10U + 20);
|
||||
}
|
||||
guard.finish();
|
||||
}
|
||||
uint32_t threshold_after = 0;
|
||||
component.should_warn_of_blocking(0, threshold_after);
|
||||
EXPECT_EQ(threshold_after, threshold_before);
|
||||
}
|
||||
|
||||
// Work outside the scope is still measured and still ratchets
|
||||
TEST(UnavoidableBlockingScope, WorkOutsideTheScopeStillRatchetsTheThreshold) {
|
||||
DummyComponent &component = blocking_test_component(1);
|
||||
uint32_t threshold_before = 0;
|
||||
component.should_warn_of_blocking(0, threshold_before);
|
||||
{
|
||||
LoopBlockingGuard guard(&component, nullptr, millis());
|
||||
{
|
||||
UnavoidableBlockingScope scope;
|
||||
delay(20);
|
||||
}
|
||||
delay(WARN_IF_BLOCKING_OVER_CS * 10U + 20);
|
||||
guard.finish();
|
||||
}
|
||||
uint32_t threshold_after = 0;
|
||||
component.should_warn_of_blocking(0, threshold_after);
|
||||
EXPECT_GT(threshold_after, threshold_before);
|
||||
}
|
||||
|
||||
} // namespace esphome
|
||||
Reference in New Issue
Block a user