Add static_asserts that the SEND_ACK/CAN/NAK states stay contiguous, so the
inline range check in response_handler_() fails at compile time if the enum
is ever reordered.
Document that process_uart_slow_() requires available() > 0 at the
declaration (the .cpp definition already carries the rationale).
Move the api_is_connected() definition from util.cpp to util.h and mark it
ESPHOME_ALWAYS_INLINE. The body is trivial — a nullptr check on
global_api_server plus APIServer::is_connected() (which is
!clients_.empty()) — so the out-of-line call8 was pure overhead for hot
paths that check connectivity every loop tick (e.g. zwave_proxy::loop,
serial_proxy::loop).
With USE_API disabled the function collapses to "return false" at compile
time and folds away entirely.
util.h now pulls in api_server.h under USE_API. Only 25 .cpp files include
util.h and all of them are on USE_API builds in practice (wifi, safe_mode,
nextion, web_server, etc.), so the additional transitive include is
essentially free on the platforms that matter.
Measured on ESP32-S3: the call8 in ZWaveProxy::loop() is replaced by
5 inline Xtensa instructions (load global_api_server, deref, null check,
clients_ start/finish compare) — no call overhead, no stack frame.
Split response_handler_ and process_uart_ into tiny inline wrappers in the
header that short-circuit the common case, plus _slow_ bodies in the .cpp.
- response_handler_: most loop() ticks have parsing_state_ outside the three
SEND_* states; inline the range check and skip the call8 entirely.
- process_uart_: most ticks have no UART bytes pending; inline an available()
check and skip the out-of-line call + its stack frame. Inside the slow
path, switch the while() to do/while() since the caller has already
confirmed available() > 0.
ESPHOME_ALWAYS_INLINE is required — with -Os gcc otherwise clones the
wrapper into a shared \$isra\$ outline and keeps the call8.
Measured on ESP32-S3 zwave-proxy-seeedw5500 build: idle-tick out-of-line
calls in ZWaveProxy::loop() drop from 4 (response_handler_, api_is_connected,
virtual parent_->is_connected, process_uart_ which nested available()) to 3
(api_is_connected, virtual is_connected, available).
Convert buf_append_printf calls that use only literal format strings
or %s specifiers over to buf_append_str, which avoids pulling in
printf machinery on all platforms and keeps literals in flash on
ESP8266 via the PSTR()-wrapping macro introduced in #15738.
Format strings with numeric specifiers (PRIu32, %u, PRIX32, etc.)
are kept on buf_append_printf. Runtime %s calls in debug_esp8266.cpp
stay as-is because the ESP8266 buf_append_str macro requires a
string literal for PSTR().
Previously loop_tail_start_us defaulted to loop_before_end_us, so on
ticks where Phase B was gated out (loop_interval_ not yet elapsed, no
wake, no high-frequency request) tail_us measured "time from end of
Phase A to stats recording" — i.e. gate-check + stats-prefix overhead —
and was accumulated into the per-tick "tail" bucket even though no
component tail had actually run.
That mis-attributed gate-check + stats-prefix overhead to the tail
metric, which is supposed to represent trailing overhead of the
component phase specifically (after_component_phase_ + the little bit
before record_loop_active). On a device whose loop_interval_ is raised
for power savings, Phase A-only ticks dominate and the mis-attributed
tail would skew the stats report.
Fix: gate the tail_us computation on do_component_phase. Initialize
loop_tail_start_us to 0 (unused on Phase A-only ticks) and only
subtract from loop_now_us when Phase B ran. The overhead that used to
land in "tail" now falls into "residual" (active − before − components
− tail), which is the correct bucket for per-iteration bookkeeping that
is not phase-specific.
Matches the before_component_phase_ / after_component_phase_ symmetry
now that Phase A and Phase B are separated. Also move the call to the
very end of the `if (do_component_phase) { ... }` block so the helper
semantically closes out the phase, rather than sitting before the
last_loop_ / now bookkeeping.
No functional change: in_loop_ is read only inside the component
iteration (disable_looping_component's swap fixup), so setting it false
at any point after the for-loop ends is equivalent. Runtime-stats tail
timing still captures the same region (the micros() sample happens
before any of the moved lines).
After the Phase A / Phase B split in this PR, an external producer that
called wake_loop_threadsafe() (MQTT RX, USB RX, BLE event, espnow,
camera, mWW, speakers, USB host/CDC, lwip socket, enable_loop_soon_any_context)
only got Phase A — the component phase stayed gated by loop_interval_,
so the producer's component loop() could be delayed by up to
loop_interval_ ms before draining its queued work. That breaks the
long-standing semantic of wake_loop_threadsafe().
Add a wake_request flag set by every wake_loop_* entry point and
exchange-cleared at the gate in Application::loop(). When the flag is
set, force Phase B regardless of loop_interval_.
Storage is conditional on the threading model:
- ESPHOME_THREAD_MULTI_ATOMICS: std::atomic<uint8_t> (uint8_t, not
bool, because GCC on Xtensa generates an indirect call for
atomic<bool> ops — same workaround as scheduler.h)
- ESPHOME_THREAD_SINGLE / ESPHOME_THREAD_MULTI_NO_ATOMICS: volatile
uint8_t (8-bit aligned loads/stores are atomic on every supported
MCU; the platform signal that follows wake_request_set provides the
cross-thread/cross-core memory barrier)
Helpers (wake_request_set / wake_request_take) are always_inline so
IRAM_ATTR call sites stay in IRAM. Set BEFORE the platform signal so the
consumer is guaranteed to see the flag on its next gate check.
Adds an integration test that raises loop_interval_ to 2s, snapshots a
counting component's loop count, spawns a std::thread that calls
App.wake_loop_threadsafe() after 50ms, and asserts the count increments
inside a 500ms observation window. Without the fix the count would not
move for ~2s.