From 2e490d9121a6e4fb918f5e681b13d732bc51d19b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 27 Feb 2026 23:02:30 -1000 Subject: [PATCH] [api] Outline keepalive ping logic from APIConnection::loop() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extract the keepalive ping/disconnect cold path into a separate noinline check_keepalive_() method. This code only fires once per minute but was contributing ~110 bytes to the hot loop() body that runs every ~16ms. Results on ESP32 (xtensa): - loop(): 374 → 263 bytes (-111 bytes, -30%) - check_keepalive_(): 136 bytes (new, cold path) - Net: +25 bytes total (call overhead), but the hot path icache footprint is significantly reduced. --- esphome/components/api/api_connection.cpp | 46 ++++++++++++++--------- esphome/components/api/api_connection.h | 4 ++ 2 files changed, 32 insertions(+), 18 deletions(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 7bc9c45c05..8b2efdde51 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -242,24 +242,11 @@ void APIConnection::loop() { return; } - if (this->flags_.sent_ping) { - // Disconnect if not responded within 2.5*keepalive - if (now - this->last_traffic_ > KEEPALIVE_DISCONNECT_TIMEOUT) { - on_fatal_error(); - this->log_client_(ESPHOME_LOG_LEVEL_WARN, LOG_STR("is unresponsive; disconnecting")); - } - } else if (now - this->last_traffic_ > KEEPALIVE_TIMEOUT_MS && !this->flags_.remove) { - // Only send ping if we're not disconnecting - ESP_LOGVV(TAG, "Sending keepalive PING"); - PingRequest req; - this->flags_.sent_ping = this->send_message(req, PingRequest::MESSAGE_TYPE); - if (!this->flags_.sent_ping) { - // If we can't send the ping request directly (tx_buffer full), - // schedule it at the front of the batch so it will be sent with priority - ESP_LOGW(TAG, "Buffer full, ping queued"); - this->schedule_message_front_(nullptr, PingRequest::MESSAGE_TYPE, PingRequest::ESTIMATED_SIZE); - this->flags_.sent_ping = true; // Mark as sent to avoid scheduling multiple pings - } + // Keepalive: only call into the cold path when enough time has elapsed. + // When sent_ping is true, last_traffic_ hasn't been updated so this + // condition is already satisfied — covers both send-ping and disconnect cases. + if (now - this->last_traffic_ > KEEPALIVE_TIMEOUT_MS) { + this->check_keepalive_(now); } #ifdef USE_API_HOMEASSISTANT_STATES @@ -275,6 +262,29 @@ void APIConnection::loop() { #endif } +void APIConnection::check_keepalive_(uint32_t now) { + // Caller guarantees: now - last_traffic_ > KEEPALIVE_TIMEOUT_MS + if (this->flags_.sent_ping) { + // Disconnect if not responded within 2.5*keepalive + if (now - this->last_traffic_ > KEEPALIVE_DISCONNECT_TIMEOUT) { + on_fatal_error(); + this->log_client_(ESPHOME_LOG_LEVEL_WARN, LOG_STR("is unresponsive; disconnecting")); + } + } else if (!this->flags_.remove) { + // Only send ping if we're not disconnecting + ESP_LOGVV(TAG, "Sending keepalive PING"); + PingRequest req; + this->flags_.sent_ping = this->send_message(req, PingRequest::MESSAGE_TYPE); + if (!this->flags_.sent_ping) { + // If we can't send the ping request directly (tx_buffer full), + // schedule it at the front of the batch so it will be sent with priority + ESP_LOGW(TAG, "Buffer full, ping queued"); + this->schedule_message_front_(nullptr, PingRequest::MESSAGE_TYPE, PingRequest::ESTIMATED_SIZE); + this->flags_.sent_ping = true; // Mark as sent to avoid scheduling multiple pings + } + } +} + void APIConnection::process_active_iterator_() { // Caller ensures active_iterator_ != NONE if (this->active_iterator_ == ActiveIterator::LIST_ENTITIES) { diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index e34bed8ada..37855b2482 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -370,6 +370,10 @@ class APIConnection final : public APIServerConnectionBase { return this->client_supports_api_version(1, 14) ? MAX_INITIAL_PER_BATCH : MAX_INITIAL_PER_BATCH_LEGACY; } + // Send keepalive ping or disconnect unresponsive client. + // Cold path — extracted from loop() to reduce instruction cache pressure. + void __attribute__((noinline)) check_keepalive_(uint32_t now); + // Process active iterator (list_entities/initial_state) during connection setup. // Extracted from loop() — only runs during initial handshake, NONE in steady state. void __attribute__((noinline)) process_active_iterator_();