The pre-sleep scan of all monitored sockets was added to preserve
select() semantics by checking for pending data before sleeping.
However, this is unnecessary with the FreeRTOS task notification
approach:
- xTaskNotifyGive from the lwip callback persists until consumed
by ulTaskNotifyTake, so notifications received while the task
is running (not sleeping) are not lost.
- The only case the scan caught was intentionally undrained sockets
(e.g., API's MAX_MESSAGES_PER_LOOP=5 throttle). Adding up to
16ms (loop_interval) latency before re-checking undrained data
is the desired behavior — waking immediately would defeat the
purpose of the throttle which exists to let other components run.
This removes N volatile cross-core reads (one per monitored socket)
from every loop iteration.
Each platform defines exactly one OTA backend subclass (marked final),
so using concrete types in unique_ptr eliminates virtual dispatch overhead.
- Remove OTABackend base class - no longer needed since all consumers
use concrete types directly
- Move make_ota_backend() declaration to each concrete backend header
with concrete return type
- Add ota_backend_factory.h convenience header for consumers
- Use concrete unique_ptr types in ota_esphome, http_request, and
web_server consumers
Each platform defines exactly one OTA backend subclass (marked final),
so using concrete types in unique_ptr eliminates virtual dispatch overhead.
- Move make_ota_backend() declaration from base header to each concrete
backend header with concrete return type
- Add ota_backend_factory.h convenience header for consumers
- Use concrete unique_ptr types in ota_esphome, http_request, and
web_server consumers
When only one API protocol is configured (plaintext-only or noise-only),
use the concrete frame helper type in unique_ptr instead of the base
class. Since both APIPlaintextFrameHelper and APINoiseFrameHelper are
marked final, the compiler can devirtualize all virtual calls
(read_packet, write_protobuf_packet, loop, etc.), eliminating vtable
dispatch overhead in the hot APIConnection::loop() path.
When both protocols are enabled (encryption key set with plaintext
fallback), the polymorphic base pointer is used as before.
Move is_connected() to the header as an inline method that returns a
cached bool field. The previous implementation called
wifi_sta_connect_status_() on every invocation, which makes SDK calls
on ESP8266 (wifi_station_get_connect_status) and RP2040
(cyw43_wifi_link_status + WiFi.status), preventing inlining and adding
overhead for the many callers that check it every loop iteration
(network::is_connected, API server, MQTT, status sensor, etc.).
The cached state is updated once per loop() after wifi_loop_() processes
platform events. Internal call sites that need a live SDK query
(STA_CONNECTED loss detection, RP2040 can_proceed) use the new
is_connected_() private method directly.
In debug builds (HAS_PROTO_MESSAGE_DUMP), skip dump logging for
SubscribeLogsResponse (recursive logging risk) and CameraImageResponse
(high-frequency image data noise). This matches the base branch behavior
where both bypassed dump logging via direct send_message_impl() calls.
The previous millis()-based timing had insufficient resolution. Most
components complete their loop() in microseconds, but millis() only
has 1ms granularity. Components taking <1ms would show either 0ms or
1ms depending on whether a millisecond boundary happened to tick over
during execution — essentially random noise rather than useful data.
Switch to self-timed micros() per guard (only when USE_RUNTIME_STATS
is compiled in — zero cost in production builds). Track internally in
microseconds, display in milliseconds with fractional precision.
Use uint64_t for total_time_us_ to avoid overflow (uint32_t would wrap
after ~10 hours at typical loop rates).
Components after the last blocking component in setup only receive
one call() (CONSTRUCTION→SETUP) and never get the second call()
that would transition them to LOOP state. Explicitly transition all
active looping components to LOOP state at the end of setup() so
the main loop can call loop() directly without the call() state
machine wrapper.
In the main loop, components in looping_components_ active section are
guaranteed to be in LOOP state. The call() method's state machine
dispatch (checking CONSTRUCTION, SETUP, FAILED, LOOP_DONE) is only
needed during Application::setup(). In the main loop it adds two
unnecessary function call frames per component per iteration
(call() -> call_loop_() -> loop()).
This became dead weight when looping_components_ partitioning was
introduced in June 2025 (8a06c4380d). Before that, Application::loop()
iterated components_[] which contained all states, so the state check
was necessary.