mirror of
https://github.com/esphome/esphome.git
synced 2026-09-09 22:38:48 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e7a5258118 | ||
|
|
d7dea9e74a | ||
|
|
2307d811bb | ||
|
|
d6d2540823 | ||
|
|
c1272a72aa | ||
|
|
a4ec1791e2 | ||
|
|
86997a438c | ||
|
|
717fbb1373 | ||
|
|
56f6d7bc6d |
@@ -389,6 +389,7 @@ class Application {
|
|||||||
friend Component;
|
friend Component;
|
||||||
friend class Scheduler;
|
friend class Scheduler;
|
||||||
friend class LoopBlockingGuard;
|
friend class LoopBlockingGuard;
|
||||||
|
friend class UnavoidableBlockingScope;
|
||||||
#ifdef USE_RUNTIME_STATS
|
#ifdef USE_RUNTIME_STATS
|
||||||
friend class runtime_stats::RuntimeStatsCollector;
|
friend class runtime_stats::RuntimeStatsCollector;
|
||||||
#endif
|
#endif
|
||||||
@@ -631,6 +632,55 @@ class LoopBlockingGuard {
|
|||||||
static void __attribute__((noinline, cold)) warn_blocking(uint32_t blocking_time);
|
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 done from a loop pass that cannot be made shorter and
|
||||||
|
/// cannot be split across passes: turning on a radio, the first Wi-Fi
|
||||||
|
/// connect, 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 the one step nothing can be done about.
|
||||||
|
///
|
||||||
|
/// Never use it to paper over a problem that can be solved. A slow driver
|
||||||
|
/// call, a loop that could be a state machine, a computation that could be
|
||||||
|
/// cached or deferred, a blocking read that could be polled: those are what
|
||||||
|
/// 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.
|
||||||
|
///
|
||||||
|
/// Only work timed by a LoopBlockingGuard is affected, that is a component's
|
||||||
|
/// loop() or a scheduler callback; setup() is not timed by the guard, so the
|
||||||
|
/// scope has no effect on the warning there. Main loop task only. The watchdog is not fed inside the
|
||||||
|
/// scope, so the work must finish within the watchdog timeout, or be paired
|
||||||
|
/// with a watchdog::WatchdogManager that raises the timeout for the same
|
||||||
|
/// stretch. Scopes may nest; the outermost one decides how much of the pass
|
||||||
|
/// is left out.
|
||||||
|
/// App.get_loop_component_start_time() reads later in the same pass return
|
||||||
|
/// the moved start, so elapsed time across the scope needs millis().
|
||||||
|
///
|
||||||
|
/// void MyComponent::loop() {
|
||||||
|
/// if (this->needs_key_) {
|
||||||
|
/// 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, so an
|
||||||
|
// outer scope overrides an inner one instead of adding to it; never past
|
||||||
|
// now, which would underflow the guard's subtraction
|
||||||
|
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
|
// Phase A: drain wake notifications and run the scheduler. Invoked on every
|
||||||
// Application::loop() tick regardless of whether a component phase runs, so
|
// Application::loop() tick regardless of whether a component phase runs, so
|
||||||
// scheduler items fire at their requested cadence even when the caller has
|
// scheduler items fire at their requested cadence even when the caller has
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ class MillisInternal {
|
|||||||
}
|
}
|
||||||
friend class Application;
|
friend class Application;
|
||||||
friend class LoopBlockingGuard;
|
friend class LoopBlockingGuard;
|
||||||
|
friend class UnavoidableBlockingScope;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace esphome
|
} // 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