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).
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).
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.
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.
The function-level NOLINTBEGIN/NOLINTEND on start_session_main_loop_()
only covers bug paths whose primary or note locations fall inside that
function. clang-analyzer now traces the leak through the caller chain
(AsyncEventSource::loop -> adopt_pending_sessions_main_loop_ ->
start_session_main_loop_ -> ArduinoJson), with notes at lines 497/498
and 530/537 that sit outside the wrapped range. Add a NOLINTNEXTLINE
at the call site in adopt_pending_sessions_main_loop_ so the caller's
bug-path location is covered too.
On ESPHOME_THREAD_SINGLE builds (ESP8266, RP2040), defer() does not
route through defer_queue_ — set_timer_common_ treats it as an ordinary
0-delay set_timeout that stages in to_add_ (scheduler.cpp:197). Since
next_schedule_in only inspects items_[0], any defer posted from a
component's loop() sits in to_add_ invisible to the sleep calculation
until the next scheduler.call() runs process_to_add().
The symmetric fix checks to_add_empty_() on single-threaded and
short-circuits sleep the same way. This also costs one "skipped sleep"
for any non-zero-delay timer added at runtime, but the next tick
produces a correct next_schedule_in reading and the cost is a single
yield — negligible vs. the stall this closes.
Suggested by Copilot review on #15968.
The previous narrow NOLINT only wrapped the sorting_groups JSON loop.
Now that the function is invoked through an extra call layer
(loop() -> adopt_pending_sessions_main_loop_ -> start_session_main_loop_),
clang-analyzer traces a leak path through ws->get_config_json() as
well -- also a false positive inside ArduinoJson's VariantData.
Move the NOLINTBEGIN/NOLINTEND to bracket the entire function body.
On multi-thread platforms (ESP32/LibreTiny/host), defer() items go
directly into a separate defer_queue_ instead of the main items_ heap
(scheduler.cpp:200). next_schedule_in previously only looked at
items_[0], so a pending defer posted from a background task was
invisible to the loop's sleep-duration calculation: if items_[0] fired
seconds away, the main loop would sleep that long on ulTaskNotifyTake
while the defer sat unexecuted.
In practice this rarely bit anyone because concurrent socket activity
usually woke the loop via the lwip fast-select path, but a defer with
no accompanying network traffic could stall for seconds.
Check defer_empty_() (cheap atomic/volatile load, scheduler.h:594)
before the items_ check and return 0 if pending. process_defer_queue_
runs at the top of every scheduler.call(), so "defer pending" == "run
immediately."
Single-threaded platforms funnel defers into to_add_ like any other
timeout, so the fix is gated on !ESPHOME_THREAD_SINGLE.
destroy() already logs the close with the fd on the httpd task, so the
follow-up log when the main loop reaps the session adds no information
and doubles log volume during reconnect storms.
Pre-refactor, on_connect_ ran after the ctor had already sent the
initial ping/config/sorting_groups. Call start_session_main_loop_()
before on_connect_ so the callback still observes a primed session.
Also document at onConnect() that the callback now runs on the main
loop instead of the httpd task.