mirror of
https://github.com/esphome/esphome.git
synced 2026-09-01 18:46:02 +00:00
Restructures Application::loop() into two independent phases to stop the scheduler from silently pulling the component loop cadence forward. Before: Application::loop() bounded its sleep by min(loop_interval_ - elapsed, next_schedule_in()) with a delay_time/2 floor. Any scheduler item due sooner than loop_interval_/2 dragged the whole component phase with it. On a typical ESP32 config with default loop_interval_=16ms, combined scheduler activity from api / esp32_ble / esp32_ble_tracker / debug was keeping every component's loop() running at ~128 Hz instead of the documented ~62 Hz. This has become more visible recently as more components convert to PollingComponent (which uses set_interval internally) and more in-tree code uses set_interval / set_timeout directly. Adding or removing any scheduled item silently changed every other component's loop cadence. App.set_loop_interval() for power savings was also silently defeated. After: - Phase A (every tick): drain wake notifications, run scheduler.call(), feed WDT - Phase B (gated by loop_interval_ or HighFrequencyLoopRequester): iterate registered components and update last_loop_ Sleep = min(time-until-next-component-phase, next_schedule_in()). When a scheduler event wakes us early, Phase A services it and the component phase stays gated independently. loop_interval_ is now a true minimum interval between component phases. The delay_time/2 floor is removed. Any legitimate need to wake faster than loop_interval_ has proper mechanisms: - HighFrequencyLoopRequester for sustained fast-loop needs - Application::wake_loop_threadsafe() from any context (new in 2026.4.0) for one-shot wake-on-event Also guards against set_interval(0) misuse — it asks the main loop to spin forever, which was never the intended API. Warns at creation time pointing authors at HighFrequencyLoopRequester. set_timeout(0)/defer() is unaffected; zero-delay one-shots remain legitimate. Runtime stats: process_pending_stats is now called on every tick (not just when the component phase runs) so log_interval_ isn't quantized to the component-phase cadence. Added an inline fast-path gate in runtime_stats.h that early-outs unless now >= next_log_time_, keeping Application::loop() slim; the log_stats_ work stays out-of-line. Ordering constraints preserved: - defer() callbacks still FIFO before components same-tick (Phase A runs before Phase B) - Scheduled items still execute before components when both due - Scheduled callbacks still run on main thread only - loop_component_start_time_ is still set fresh at each component's loop - WDT is still fed at least once per tick
61 lines
1.7 KiB
YAML
61 lines
1.7 KiB
YAML
esphome:
|
|
name: loop-interval-decouple
|
|
on_boot:
|
|
priority: -100
|
|
then:
|
|
- lambda: |-
|
|
// Raise loop_interval_ to 500ms. With the decoupling fix the
|
|
// component phase should run ~twice per second while the 50ms
|
|
// scheduler interval below still fires at its requested cadence.
|
|
App.set_loop_interval(500);
|
|
# Start measurement after 1s so boot transients settle.
|
|
- delay: 1000ms
|
|
- lambda: |-
|
|
id(loop_at_start) = id(loop_counter)->get_loop_count();
|
|
id(sched_at_start) = id(sched_count);
|
|
ESP_LOGI("test", "MEASUREMENT_STARTED loop=%d sched=%d",
|
|
id(loop_at_start), id(sched_at_start));
|
|
# Observe for 2s.
|
|
- delay: 2000ms
|
|
- lambda: |-
|
|
int loop_delta = id(loop_counter)->get_loop_count() - id(loop_at_start);
|
|
int sched_delta = id(sched_count) - id(sched_at_start);
|
|
ESP_LOGI("test", "MEASUREMENT_DONE loop_delta=%d sched_delta=%d",
|
|
loop_delta, sched_delta);
|
|
|
|
host:
|
|
api:
|
|
logger:
|
|
level: INFO
|
|
logs:
|
|
loop_test_component: WARN # Silence per-loop log spam
|
|
|
|
external_components:
|
|
- source:
|
|
type: local
|
|
path: EXTERNAL_COMPONENT_PATH
|
|
|
|
globals:
|
|
- id: sched_count
|
|
type: int
|
|
initial_value: "0"
|
|
- id: loop_at_start
|
|
type: int
|
|
initial_value: "0"
|
|
- id: sched_at_start
|
|
type: int
|
|
initial_value: "0"
|
|
|
|
loop_test_component:
|
|
components:
|
|
- id: loop_counter
|
|
name: loop_counter
|
|
|
|
interval:
|
|
# Fast scheduler interval — with the decoupling fix this should fire at
|
|
# its requested 50ms cadence regardless of loop_interval_.
|
|
- interval: 50ms
|
|
then:
|
|
- lambda: |-
|
|
id(sched_count) += 1;
|