Clamp the moved pass start to now, document the watchdog and task constraints, test the threshold

This commit is contained in:
J. Nick Koston
2026-09-07 16:57:03 +02:00
parent 717fbb1373
commit 86997a438c
2 changed files with 48 additions and 4 deletions
+9 -3
View File
@@ -646,6 +646,9 @@ class LoopBlockingGuard {
/// the warning exists to find, and wrapping them in this scope hides the
/// bug instead of fixing it. If in doubt, leave the warning in.
///
/// Main loop task only. The watchdog is not fed inside the scope, so the
/// work must still finish within the watchdog timeout.
///
/// {
/// UnavoidableBlockingScope scope;
/// bring_up_radio();
@@ -654,9 +657,12 @@ 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_));
// Move the pass start forward by the time spent here, but never past now:
// nested scopes count the inner stretch twice, and a start in the future
// would underflow the guard's subtraction
const uint32_t now = millis();
const uint32_t moved = App.get_loop_component_start_time() + (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;
+39 -1
View File
@@ -27,8 +27,46 @@ TEST(UnavoidableBlockingScope, ExcludesItsDurationFromThePass) {
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, 1u);
EXPECT_LE(App.get_loop_component_start_time() - pass_start, millis() - before);
}
// Nesting counts the inner stretch twice; the start must still never pass now
TEST(UnavoidableBlockingScope, NestedScopesNeverMoveTheStartPastNow) {
const uint32_t pass_start = millis();
LoopBlockingGuard guard(nullptr, nullptr, pass_start);
{
UnavoidableBlockingScope outer;
{
UnavoidableBlockingScope inner;
delay(30);
}
delay(5);
}
const uint32_t now = millis();
EXPECT_GE(static_cast<int32_t>(now - App.get_loop_component_start_time()), 0);
EXPECT_GE(App.get_loop_component_start_time() - pass_start, 35u);
}
class DummyComponent : public Component {};
// The excused stretch must neither warn nor ratchet the component's threshold
TEST(UnavoidableBlockingScope, ExcusedStretchDoesNotRatchetTheThreshold) {
DummyComponent component;
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);
}
} // namespace esphome