[core] refresh HighFrequencyLoopRequester state at sleep decision

The Phase A/B split cached `HighFrequencyLoopRequester::is_high_frequency()`
once at the top of Application::loop() and reused that value at the sleep
decision after Phase B. If a component calls
HighFrequencyLoopRequester::start() from inside its own loop(), the cached
value is stale when we pick the sleep length, so the request doesn't take
effect until the next tick.

Before the decoupling, the sleep decision called the function fresh at the
same point in the code, so an in-loop HF request was honored immediately.
This caching silently regressed that behavior. The regression matters a lot
more now than it would have previously: loop_interval_ is the power-saving
knob this PR was written to enable (raised multi-second values are a
documented use case), so under the new freedom the worst-case HF-request
latency can stretch from ~16 ms to several seconds.

Re-read is_high_frequency() fresh at the sleep decision. The gate check
above continues to use the cached value — there's no correctness benefit
to re-reading it there, and the "one read covers the whole tick" design
intent is preserved for the gate. The function is a trivial atomic-read
static; a second call is cheaper than the latency it prevents.

Flagged by Copilot review on application.h:728.
This commit is contained in:
J. Nick Koston
2026-04-18 17:51:19 -05:00
parent 93e99efc2d
commit ffe5db8c7d
+11 -1
View File
@@ -718,8 +718,18 @@ inline void ESPHOME_ALWAYS_INLINE Application::loop() {
// Compute sleep: bounded by time-until-next-component-phase and the
// scheduler's next deadline. When a scheduler event wakes us early we
// re-enter loop(), Phase A services it, and the component phase stays gated.
//
// Re-read HighFrequencyLoopRequester::is_high_frequency() here instead of
// reusing the cached `high_frequency` captured above: a component calling
// HighFrequencyLoopRequester::start() from within its loop() would
// otherwise sit under the stale value and sleep for up to loop_interval_
// before the request took effect. That was fine pre-decoupling (the old
// main loop also called the function fresh at the sleep point) but now
// matters much more — loop_interval_ is a power-saving knob documented
// to accept multi-second values, so the stale path could add seconds of
// latency on an HF request. The call is a trivial atomic read.
uint32_t delay_time = 0;
if (!high_frequency) {
if (!HighFrequencyLoopRequester::is_high_frequency()) {
const uint32_t elapsed_since_phase = now - this->last_loop_;
const uint32_t until_phase =
(elapsed_since_phase >= this->loop_interval_) ? 0 : (this->loop_interval_ - elapsed_since_phase);