mirror of
https://github.com/esphome/esphome.git
synced 2026-09-11 15:27:33 +00:00
[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.
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
#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);
|
||||
{ UnavoidableBlockingScope scope; }
|
||||
EXPECT_LE(App.get_loop_component_start_time() - pass_start, 1u);
|
||||
}
|
||||
|
||||
} // namespace esphome
|
||||
Reference in New Issue
Block a user