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.
Reduces per-instantiation cost of send_message<T> and
encode_message_to_buffer<T> by moving the DumpBuffer/log code
into send_message_() and encode_to_buffer(). The dump methods
(message_name, dump_to) are still virtual on ProtoMessage, so
they work correctly through the void* → ProtoMessage* cast.