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.
schedule_message_front_ is only called from cold paths (on_shutdown,
check_keepalive_). Moving it out-of-line prevents add_item_front and
push_back from being inlined into those callers.
add_item_front is only called from on_shutdown and check_keepalive_
which are cold paths. Keeping it out-of-line prevents inlining
push_back into those callers.
Both are only called from one site each. Moving them inline lets the
compiler inline push_back directly into the caller without a function
call barrier. Removes the now-unnecessary push_item wrapper.
Add __attribute__((flatten)) to add_item and add_item_front so the
compiler inlines push_item → push_back and all their callees directly
into these functions, eliminating the function call barrier.
The out-of-line push_item with __attribute__((flatten)) inlined
push_back's callees into push_item, but push_item itself remained
a function call barrier. Moving push_item inline lets the compiler
inline push_back into the actual call sites (add_item, add_item_front).
Inlining push_item into add_item bloated the dedup loop, causing
the compiler to make worse inlining decisions. Total time went from
44µs to 64.5µs. Restore out-of-line with __attribute__((flatten))
which keeps add_item's dedup loop tight while still inlining
push_back's callees into push_item.
push_item was defined out-of-line in the .cpp to avoid duplicate
_M_realloc_insert instantiation, but the function call overhead
on every add_item was visible in benchmarks. Move it inline since
it's a one-liner wrapper around push_back.
should_send_immediately_ requires both should_try_send_immediately
flag AND batch_delay==0. Without setting batch_delay to 0, all
sends were falling back to the batch path.
Increase socket buffer to 16MB so benchmarks never hit WOULD_BLOCK
during an inner loop iteration. Remove per-iteration drain_socket
calls that were adding noise and causing the immediate path to
fall back to batching. Drain only between outer iterations.
Add benchmarks for the full send_sensor_state path covering both
immediate send and batch paths. This measures the end-to-end cost
of sending a sensor state update through an APIConnection including
entity field population, proto encode, framing, and TCP write.
bluetooth_proxy's loop() ran every main loop iteration (~7400 times/60s)
but only flushed advertisements every 100ms (~600 times/60s). The remaining
~6800 iterations just checked a timestamp and returned, wasting loop
framework overhead.
Replace with set_interval(100ms) so the component only runs when there's
work to do. Move flush_pending_advertisements to an inline method in the
header to avoid out-of-line call overhead in the hot parse_devices path.
Remove redundant is_connected/api_connection checks from flush since all
callers already guard.