Replace two out-of-line encode_varint_to_buffer calls with direct
byte writes using the already-computed varint lengths. Eliminates
function call overhead (register save/restore) from the batch loop
and removes the 34-byte out-of-line encode_varint_to_buffer function
which had no other callers.
Pre-compute the first message's header length before the loop to
initialize write_start/write_end. The first loop iteration naturally
skips memmove since src == write_end.
Extract varint_encoded_length_16/8 and plaintext_header_length as
reusable inline helpers from write_plaintext_header.
Use a single loop for all messages instead of separate first-message
and loop paths. The first iteration skips memmove via the null check.
Eliminates duplicated write_plaintext_header inlining, reducing flash
from 326 to 229 bytes (-30%) while keeping the 64-byte stack frame.
Replace StaticVector<iovec> + writev() scatter-gather in the batch write
path with contiguous single-buffer write() calls.
Plaintext: compact messages via memmove to close 0-3 byte varint header
gaps, then write_raw_fast_buf_. Noise: messages are already contiguous
(fixed 7-byte header + 16-byte MAC fills all reserved space), switch
directly to write_raw_fast_buf_.
Fix LOG_PACKET_SENDING to log after write/enqueue to prevent re-entrant
log sends from corrupting the shared buffer before data is sent.
Consolidate the macro to api_frame_helper.cpp and expose via out-of-line
log_packet_sending_() helper.
Remove write_raw_fast_iov_ (no remaining callers) and change
encrypt_noise_message_ to return uint16_t length instead of iovec.
Move get_component_log_str() from component.cpp into the header
as an always_inline method. This is a trivial one-liner that just
calls component_source_lookup(), but the compiler was keeping it
out-of-line across 8 call sites (13 bytes).
Saves 8 bytes on ESP32.
Inline record_runtime_stats_() to eliminate the non-inlined function
call overhead per component per loop iteration. Extract check_blocking_()
as an inline helper for readability.
The millis() return value stays in the caller's clock domain — only
the micros()-based stats recording is inlined alongside it.
Use a single micros() call in WarnIfComponentBlockingGuard::finish()
for both runtime stats recording and blocking detection. Previously
finish() called millis() for blocking detection and a separate
non-inlined record_runtime_stats_() that called micros() again.
Changes:
- Inline record_runtime_stats_() to avoid function call overhead
- Derive curr_time from micros()/1000 instead of separate millis() call
- Override started_ from micros()/1000 in constructor so both timestamps
use the same clock source (fixes unsigned underflow on platforms where
millis() and micros()/1000 can diverge)
- Extract check_blocking_() as inline helper for readability
After wifi_loop_() processes the STA_CONNECTED event, state_ is still
CONNECTING (state machine hasn't run yet). update_connected_state_()
runs but is_connected_() returns false. Then check_connecting_finished()
transitions to STA_CONNECTED. Next iteration: no events, state is
STA_CONNECTED, so we skipped update — connected_ stayed false forever.
Fix: check connected_ instead of state_. Only skip when connected_ is
already true (steady state). When connected_ is false, always re-evaluate
so the flag gets set after the state machine transitions.
After wifi_loop_() processes the STA_CONNECTED event, state_ is still
CONNECTING (state machine hasn't run yet). update_connected_state_()
runs but is_connected_() returns false. Then check_connecting_finished()
transitions to STA_CONNECTED. Next iteration: no events, state is
STA_CONNECTED, so we skipped update — connected_ stayed false forever.
Fix: check connected_ instead of state_. Only skip when connected_ is
already true (steady state). When connected_ is false, always re-evaluate
so the flag gets set after the state machine transitions.
wifi_sta_connect_status_() is a non-inlined function call that reads
3 static globals through literal pool indirections on every loop
iteration. Since those globals are only modified during event
processing, skip the call entirely when wifi_loop_() found no
events and we're already in STA_CONNECTED state.
wifi_loop_() now returns bool (true if events were processed) on
all platforms. ESP8266 and Pico W always return true since they
poll state directly rather than using an event queue.
wifi_sta_connect_status_() is a non-inlined function call that reads
3 static globals through literal pool indirections on every loop
iteration. Since those globals are only modified during event
processing, skip the call entirely when wifi_loop_() found no
events and we're already in STA_CONNECTED state.
wifi_loop_() now returns bool (true if events were processed) on
all platforms. ESP8266 and Pico W always return true since they
poll state directly rather than using an event queue.
GCC on Xtensa emits memw for relaxed atomic loads too, so the
savings from relaxed vs acquire are only 2 memw (~4 cycles) — not
worth the weaker correctness guarantees. The real optimization is
the early return that skips get_and_reset_dropped_count() and the
pop() loop entirely.
On Xtensa (ESP32), even relaxed atomic loads emit memw instructions,
but they avoid the more expensive acquire fences. The pop() path
required acquire loads + release stores just to discover the queue
was empty. By checking empty_relaxed() first, the steady-state
connected path (queue always empty) returns in ~7 instructions
instead of falling through to get_and_reset_dropped_count() and pop().
For non-first batch messages, fold the payload resize into the
reserve_and_resize call. This eliminates a redundant size() read,
a redundant capacity check, and an extra jump on the hot path.
Saves 17 bytes in encode_to_buffer (130 → 113) and ~5 instructions
on the second-message path.