Commit Graph
27962 Commits
Author SHA1 Message Date
J. Nick Koston edf884d86d Merge branch 'esp32-wdt-1000ms' into integration 2026-04-24 16:40:27 -05:00
J. Nick Koston c7dc55ea33 Revert "[core] Trim Scheduler::call fast path"
This reverts commit c2e6787e23.
2026-04-24 16:39:27 -05:00
J. Nick Koston 54fdc06322 Revert "[core] Thread Scheduler now_64 into next_schedule_in to skip duplicate clock read"
This reverts commit 75f5b17937.
2026-04-24 16:32:45 -05:00
J. Nick Koston d7e755fb83 Revert "[core] Hoist Scheduler::next_schedule_in zero-check to caller"
This reverts commit dfe591c5eb.
2026-04-24 16:32:15 -05:00
J. Nick Koston 044d42d395 Revert "[core] Switch Scheduler::call now_64 plumbing from struct to out-param"
This reverts commit 4d7033df4f.
2026-04-24 16:32:15 -05:00
J. Nick Koston c71f8b5ba9 [core] Raise ESP32 WDT feed interval to 1/5 of configured timeout
Mirrors the existing bk72xx platform override, but auto-scales to the
user-configurable esp32.watchdog_timeout (CONFIG_ESP_TASK_WDT_TIMEOUT_S)
instead of hard-coding a constant: the feed interval is always 1/5 of
the configured task WDT timeout so the safety margin stays constant
across user configurations.

  - default 5 s WDT  -> 1000 ms feed interval (was 300 ms, -70% hits)
  - 10 s WDT         -> 2000 ms feed interval
  - 60 s WDT (max)   -> 12000 ms feed interval

esp_task_wdt_reset() takes a spinlock and walks the WDT task list, so
every call costs tens of microseconds. At the normal ~62 Hz main loop
the old 300 ms cadence produced ~200 feed_wdt_slow_ hits per 60 s
period; the default 5s -> 1000 ms cuts that to ~60 hits (-70%).

Component-level feeds inside Component::loop() and scheduler items are
unaffected; they continue to call arch_feed_wdt after every operation,
so any op exceeding this rate-limit triggers a real feed naturally.
The rate-limit only applies to the outer guard in Application::loop()
that fires when nothing else fed recently.

esp32/__init__.py already constrains watchdog_timeout to >= 5 s (range
5-60 s), and a static_assert guards against anyone who tweaks sdkconfig
below that floor, ensuring the feed interval never drops below the
prior hardcoded 1000 ms value.

Measured on a live ESP32 IDF build (gatetrigger.yaml, default 5 s WDT)
via runtime_stats: wdt bucket dropped from 2.24 us/iter to 1.56 us/iter
- about 2.5 ms saved per 60 s window.
2026-04-24 16:24:55 -05:00
J. Nick Koston 0709ba613f [core] Raise ESP32 WDT feed interval to 1/5 of configured timeout
Mirrors the existing bk72xx platform override, but auto-scales to the
user-configurable esp32.watchdog_timeout (CONFIG_ESP_TASK_WDT_TIMEOUT_S)
instead of hard-coding a constant: the feed interval is always 1/5 of
the configured task WDT timeout so the safety margin stays constant
across user configurations.

  - default 5 s WDT  -> 1000 ms feed interval (was 300 ms, -70% hits)
  - 10 s WDT         -> 2000 ms feed interval
  - 60 s WDT (max)   -> 12000 ms feed interval

esp_task_wdt_reset() takes a spinlock and walks the WDT task list, so
every call costs tens of microseconds. At the normal ~62 Hz main loop
the old 300 ms cadence produced ~200 feed_wdt_slow_ hits per 60 s
period; the default 5s -> 1000 ms cuts that to ~60 hits (-70%).

Component-level feeds inside Component::loop() and scheduler items are
unaffected; they continue to call arch_feed_wdt after every operation,
so any op exceeding this rate-limit triggers a real feed naturally.
The rate-limit only applies to the outer guard in Application::loop()
that fires when nothing else fed recently.

esp32/__init__.py already constrains watchdog_timeout to >= 5 s (range
5-60 s), and a static_assert guards against anyone who tweaks sdkconfig
below that floor, ensuring the feed interval never drops below the
prior hardcoded 1000 ms value.

Measured on a live ESP32 IDF build (gatetrigger.yaml, default 5 s WDT)
via runtime_stats: wdt bucket dropped from 2.24 us/iter to 1.56 us/iter
- about 2.5 ms saved per 60 s window.
2026-04-24 16:24:07 -05:00
J. Nick Koston 864f096df3 [core] Raise ESP32 WDT feed interval to 1000ms
Mirrors the bk72xx override: the ESP32 task WDT default timeout is 5s
(CONFIG_ESP_TASK_WDT_TIMEOUT_S), so feeding every 1000ms keeps a ~5x
safety margin while cutting per-iteration feed overhead substantially.

esp_task_wdt_reset() takes a spinlock and walks the WDT task list, so
every call costs tens of microseconds. At the normal ~62 Hz main loop
the old 300ms cadence produced ~200 hits per 60s period; 1000ms cuts
that to ~60 hits per 60s (-70%). Measured on a live ESP32 IDF build
(gatetrigger.yaml) via runtime_stats: wdt bucket dropped from 2.24
us/iter to 1.56 us/iter - about 2.5 ms saved per 60s window, or
~0.7us/iter average.

Component-level feeds inside component loop() and scheduler items are
unaffected; they continue to call arch_feed_wdt after every operation,
so any operation that exceeds this rate-limit triggers a real feed
naturally. The rate-limit only applies to the outer guard in
Application::loop() that fires when nothing else fed recently.
2026-04-24 16:16:23 -05:00
J. Nick Koston c5e2f15889 [core] Raise ESP32 WDT feed interval to 1000ms
Mirrors the bk72xx override: the ESP32 task WDT default timeout is 5s
(CONFIG_ESP_TASK_WDT_TIMEOUT_S), so feeding every 1000ms keeps a ~5x
safety margin while cutting per-iteration feed overhead substantially.

esp_task_wdt_reset() takes a spinlock and walks the WDT task list, so
every call costs tens of microseconds. At the normal ~62 Hz main loop
the old 300ms cadence produced ~200 hits per 60s period; 1000ms cuts
that to ~60 hits per 60s (-70%). Measured on a live ESP32 IDF build
(gatetrigger.yaml) via runtime_stats: wdt bucket dropped from 2.24
us/iter to 1.56 us/iter - about 2.5 ms saved per 60s window, or
~0.7us/iter average.

Component-level feeds inside component loop() and scheduler items are
unaffected; they continue to call arch_feed_wdt after every operation,
so any operation that exceeds this rate-limit triggers a real feed
naturally. The rate-limit only applies to the outer guard in
Application::loop() that fires when nothing else fed recently.
2026-04-24 16:16:11 -05:00
J. Nick Koston 4d7033df4f [core] Switch Scheduler::call now_64 plumbing from struct to out-param
Replace the CallResult struct return with a uint64_t &now_64_out
reference parameter. The struct return forced GCC on Xtensa to emit 8
redundant s32i stores after every call() (once into the sched_result
local plus once into a compiler-chosen temp slot), and the extra ABI
shuffling around the 16-byte return value was tight enough with the
esp_timer_get_time() MMIOs bracketing the wdt bucket that the wdt
measurement regressed from ~21 us/hit to ~36 us/hit on ESP32.

Out-param keeps the scheduler's now_64 write local to Scheduler::call
(written once to *now_64_out) and leaves the return path a single
uint32 in a10. The caller passes &sched_now_64_raw directly; no struct
copy. loop_task shrinks 988 B -> 960 B.

Disassembly verified: after Scheduler::call returns the only
instructions before the next micros() capture are:
    mov.n  a6, a10                ; save now
    call8  esp_timer_get_time     ; loop_after_sched_us

No duplicate struct stores.
2026-04-24 15:59:38 -05:00
J. Nick Koston 30ff363095 Merge remote-tracking branch 'upstream-ssh/scheduler-call-fastpath' into integration 2026-04-24 15:45:53 -05:00
J. Nick Koston b659eaf494 [core] Hoist Scheduler::next_schedule_in zero-check to caller
Follow-up to the CallResult threading: move the 0-sentinel fallback
(if now_64 == 0, read the clock fresh) out of next_schedule_in and into
the single main-loop caller.

Effect on next_schedule_in: drops the uint32_t now parameter entirely
and removes the cold millis_64_from_(now) inlined clock read from the
function body. Verified in disassembly:

    Scheduler::next_schedule_in  159 B  ->  86 B  (-46%)

The hot path is now just defer_empty + cleanup_ + top-of-heap compare +
return. No clock read anywhere in the function.

Application::loop handles the 0-sentinel with a single inline branch
right before the call; when call() fired items it invokes the public
Scheduler::millis_64_from() wrapper (new, exposes the existing protected
millis_64_from_ so callers can do 64-bit extension without platform
knowledge). Fast path (no items fired) stays branch-free.

Net code size: loop_task +64 B, next_schedule_in -73 B,
Scheduler::call unchanged -> -9 B total.
2026-04-24 15:45:20 -05:00
J. Nick Koston dfe591c5eb [core] Hoist Scheduler::next_schedule_in zero-check to caller
Follow-up to the CallResult threading: move the 0-sentinel fallback
(if now_64 == 0, read the clock fresh) out of next_schedule_in and into
the single main-loop caller.

Effect on next_schedule_in: drops the uint32_t now parameter entirely
and removes the cold millis_64_from_(now) inlined clock read from the
function body. Verified in disassembly:

    Scheduler::next_schedule_in  159 B  ->  86 B  (-46%)

The hot path is now just defer_empty + cleanup_ + top-of-heap compare +
return. No clock read anywhere in the function.

Application::loop handles the 0-sentinel with a single inline branch
right before the call; when call() fired items it invokes the public
Scheduler::millis_64_from() wrapper (new, exposes the existing protected
millis_64_from_ so callers can do 64-bit extension without platform
knowledge). Fast path (no items fired) stays branch-free.

Net code size: loop_task +64 B, next_schedule_in -73 B,
Scheduler::call unchanged -> -9 B total.
2026-04-24 15:45:15 -05:00
J. Nick Koston 0220e19680 Merge remote-tracking branch 'upstream-ssh/scheduler-call-fastpath' into integration
# Conflicts:
#	esphome/core/application.h
2026-04-24 15:34:18 -05:00
J. Nick Koston 75f5b17937 [core] Thread Scheduler now_64 into next_schedule_in to skip duplicate clock read
Scheduler::call() and Scheduler::next_schedule_in() both computed now_64 via
millis_64_from_(now) at the top of every main-loop iteration. On ESP32 with
USE_NATIVE_64BIT_TIME this is an esp_timer_get_time() MMIO read (~1us);
doing it twice per iteration is wasted work since the two calls happen
microseconds apart.

Thread the value through:

- Scheduler::call() now returns a CallResult { now, now_64 } struct instead
  of just the advanced uint32_t now. When items fired, now_64 is set to 0
  (sentinel) because execute_item_() only advances the uint32 and the local
  now_64 is stale by the time we return.
- Scheduler::next_schedule_in() takes an optional now_64 parameter
  (default 0). Non-zero values are trusted and used directly; the 0
  sentinel triggers a fresh millis_64_from_(now) read.
- Application::scheduler_tick_() forwards the CallResult through.
- Application::loop() passes sched_result.now_64 to next_schedule_in().

Verified in the disassembly: on the fast path (no items fired),
next_schedule_in branches over its esp_timer_get_time call entirely via
`or a8, a4, a5; bnez a8, ...` on the two halves of now_64, jumping
straight to the next_exec comparison. When call() returned the 0 sentinel,
next_schedule_in falls through to the existing inlined
micros_to_millis(esp_timer_get_time()) sequence as before.

Bench callers (tests/benchmarks/core/bench_scheduler.cpp) and the external
test component (tests/integration/fixtures/.../scheduler_bulk_cleanup_component)
ignore the return value — no changes needed there.
2026-04-24 15:32:09 -05:00
J. Nick Koston 13ec0fac73 Merge remote-tracking branch 'upstream-ssh/scheduler-call-fastpath' into integration 2026-04-24 15:17:15 -05:00
J. Nick Koston c2e6787e23 [core] Trim Scheduler::call fast path
Two small wins on the per-loop Scheduler::call path:

1. Mark the scheduler's `millis_64()` wrapper `ESPHOME_ALWAYS_INLINE`.
   The inner `esphome::millis_64()` is already always_inline, but the
   wrapper was not, so GCC emitted an out-of-line `$isra$0` clone with
   its own `entry`/`retw` register-window prologue/epilogue at every
   call site. Inlining removes the wrapper's call overhead and the
   register-window thunk on ESP32.

2. Collapse the two atomic reads of `to_remove_` in `Scheduler::call`
   into one. The previous sequence
       cleanup_();                              // loads to_remove_
       if (to_remove_count_() >= MAX_...) ...   // reloads to_remove_
   produced `memw; l32i; beqz; memw; l32i; bltui` on the fast path
   because the compiler cannot CSE across the `memw` barriers that
   std::atomic<uint32_t>::load emits on Xtensa. Reading the counter
   once and branching on the result leaves a single
   `memw; l32i; beqz` on the common zero-case; the slow path
   (cleanup_slow_path_ + re-read + optional full_cleanup) pays an
   extra read but already holds the scheduler mutex.
2026-04-24 15:16:03 -05:00
J. Nick Koston 249d428a10 Merge remote-tracking branch 'upstream/dev' into integration 2026-04-24 14:57:06 -05:00
dependabot[bot]dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>J. Nick Koston
f62972c2c6 Bump ruff from 0.15.11 to 0.15.12 (#15981)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-04-24 19:34:00 +00:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> f36efbc762 Update tzdata requirement from >=2026.1 to >=2026.2 (#15980)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-24 19:27:12 +00:00
J. Nick Koston 3c16cbf8e2 Merge branch 'inline-micros-esp32' into integration 2026-04-24 12:33:48 -05:00
J. Nick Koston 9f5121e271 [core] Suppress redundant-declaration warning for ESP32 esp_timer_get_time forward decl 2026-04-24 12:33:41 -05:00
J. Nick Koston bca5bc08a7 [core] Remove leftover -O2 attributes (writer.py loop, host/zephyr main) 2026-04-24 12:08:11 -05:00
J. Nick Koston c4829e0bc5 Merge branch 'inline-micros-esp32' into integration
Re-merge the updated PR (now keeps esp8266 millis/delay out-of-line so
integration's accumulator + custom delay overlay cleanly, while still
inlining yield/micros/millis_64 on esp8266).
2026-04-24 12:06:08 -05:00
J. Nick Koston a1e3ec7118 [core] Keep esp8266 millis/delay out-of-line for #15662 compatibility 2026-04-24 12:02:57 -05:00
J. Nick Koston 2d4dcc6f90 Merge branch 'inline-micros-esp32' into integration
Resolves esp8266/core.cpp conflict by keeping integration's custom
millis() accumulator, custom delay(), and __wrap_millis machinery
out-of-line — those cannot be inlined safely:

  - esphome::millis() is the body of __wrap_millis (-Wl,--wrap=millis),
    so it must remain a real symbol; inlining would cause infinite
    recursion at the wrapped call site.
  - delay() intentionally avoids Arduino's __delay → millis path so the
    slow Arduino millis body can be stripped from IRAM.
  - millis_64() depends on millis() and stays out-of-line for symmetry.

esp8266 yield() and micros() are removed from core.cpp (now inlined in
hal.h alongside the other Arduino-flavored platforms — both are simple
::yield()/::micros() passthroughs).

hal.h is patched so the USE_ESP8266 branch only inlines yield() and
micros(), while delay()/millis()/millis_64() stay as out-of-line
declarations (defined in esp8266/core.cpp).
2026-04-24 11:59:40 -05:00
J. Nick Koston 47b998d7f5 [core] Drop -O2 attribute hack in favor of inline HAL wrappers (will merge inline-micros-esp32 next) 2026-04-24 11:55:14 -05:00
Kevin Ahrendt 9caf9ee023 [sendspin] Bumps sendspin-cpp library for a bugfix (#15976) 2026-04-24 11:53:03 -05:00
J. Nick Koston d3bae21d13 [core] Extend HAL inlining to yield/delay/millis_64 + libretiny + rp2040
Extends the prior commit to cover more wrappers and platforms:

- ESP32: also inline yield() and delay()
- ESP8266: also inline yield(), delay(), millis(), millis_64()
- LibreTiny: inline yield(), delay(), micros(), per-variant millis()
  fast paths, and millis_64() (via Millis64Impl::compute, now reachable
  from hal.h since time_64.h dropped its helpers.h dep)
- RP2040: also inline yield(), delay(), micros()

Consolidates the ESP8266/LibreTiny/RP2040 Arduino-flavored ::yield /
::delay / ::micros wrappers into a single shared block in hal.h.

LibreTiny note: the prior IRAM_ATTR on the wrapper was decorative —
::micros(), ::yield(), ::delay() and xTaskGetTickCount all live in
flash on every libretiny family (realtek-amb, beken-72xx,
lightning-ln882h all checked), so an IRAM ISR call would have crashed
the same way an inlined direct call does.

Also drops the helpers.h include from time_64.h (only used for the
ESPHOME_ALWAYS_INLINE macro, replaced with the raw attribute) so
time_64.h is light enough for hal.h to include.
2026-04-24 11:51:23 -05:00
J. Nick Koston 9d138e73c9 [core] Suppress redundant-declaration warning for ESP8266 micros() forward decl 2026-04-24 11:35:03 -05:00
J. Nick Koston e23a6bf59f [core] Inline micros()/millis_64() at the HAL layer
Replaces the per-function ``__attribute__((optimize("O2")))`` approach in
#15693 with a direct inline definition of micros() / millis_64() in hal.h
for ESP32, plus inline definitions for ESP8266 micros() and RP2040
millis()/millis_64().

The original goal of #15693 was to inline micros() into the main loop so
that ``call esphome::micros() → call esp_timer_get_time()`` collapses to
a single ``call esp_timer_get_time``. That wrapper-collapse benefits
every micros() call site, not just loop_task — so the real fix is to
mark the wrapper inline, not to bump the loop's optimization level.

Doing this at the HAL layer also makes runtime_stats measurements more
accurate: each timing read no longer hides a wrapper call/return between
the component end-time capture and the underlying clock read.

Refactor: move ``micros_to_millis<>()`` from helpers.h to a new
lightweight ``time_conversion.h`` so hal.h can include it without
pulling the rest of helpers.h into every TU that includes hal.h.
2026-04-24 11:23:16 -05:00
J. Nick Koston be8e677085 Merge remote-tracking branch 'upstream/dev' into integration 2026-04-24 10:46:31 -05:00
Kevin Ahrendt 94e300389c [sendspin] remove year and track number text sensors and refactor (#15975) 2026-04-24 15:35:32 +00:00
J. Nick Koston 49584c1f15 Merge remote-tracking branch 'upstream/dev' into integration 2026-04-24 09:56:10 -05:00
Kevin Ahrendt 55bcf33446 [sendspin] Add metadata sensor component (#15971) 2026-04-24 14:32:47 +00:00
Kevin Ahrendt f132b7dc07 [media_player][speaker][speaker_source] Centralize preferred format codegen (#14771) 2026-04-24 14:09:03 +00:00
J. Nick Koston 6cee6d68f4 Merge remote-tracking branch 'upstream/sendspin-sensors' into integration 2026-04-24 09:06:41 -05:00
Kevin Ahrendt 61de24a279 drop bound check on year and track number as they aren't relevant (we only checked text sensors so we wouldn't overflow the character buffer) 2026-04-24 09:56:09 -04:00
J. Nick Koston e694873aa5 Merge remote-tracking branch 'upstream/sendspin-sensors' into integration 2026-04-24 08:52:29 -05:00
Kevin Ahrendt 66019feb8f add year and track number sensors (will remove them from text_sensor in a future PR) 2026-04-24 09:27:37 -04:00
J. Nick Koston baa6d5f96b [web_server_idf] Fix cross-thread race on SSE session state (#15967) 2026-04-24 08:11:47 -05:00
J. Nick Koston 773b4d887b [core] Scheduler: don't sleep while defer queue is non-empty (#15968) 2026-04-24 08:11:29 -05:00
Kevin Ahrendt a179a0de22 log the update interval for the track progress sensor 2026-04-24 08:46:11 -04:00
Kevin Ahrendt f395e29c59 publish immediately on resume 2026-04-24 08:44:41 -04:00
Kevin Ahrendt 6b278e3eb4 make both sensors inherit from the hub's defined child classes to enforce setup priority 2026-04-24 08:44:24 -04:00
Kevin Ahrendt 8f3a2691fa Add a SendspinPolloingChild class to enforce uniform setup priority 2026-04-24 08:43:42 -04:00
Kevin Ahrendt 52eeb52d0e mark get_track_progress_ms as const 2026-04-24 08:43:06 -04:00
Kevin Ahrendt 42f0b1a7e2 add Sendspin sensor component 2026-04-24 08:21:29 -04:00
Kevin Ahrendt ac7f0f0b74 [sendspin] Add a metadata text sensor component (#15969) 2026-04-24 11:07:00 +00:00
J. Nick Koston 761e32bdc5 Merge remote-tracking branch 'upstream/dev' into integration 2026-04-24 05:53:44 -05:00