From c04335d21d39145510767c33d0e1916981368aa7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 12 Mar 2026 16:48:07 -1000 Subject: [PATCH 1/2] [api] Increase log Nagle coalescing on ESP32 and RP2040 Increase LOG_NAGLE_COUNT from 2 to 3 on ESP32 and RP2040, which have larger TCP send buffers (64KB and 11.7KB respectively). This coalesces 4 log messages per Nagle cycle instead of 3, reducing the number of individual write() calls and significantly reducing EWOULDBLOCK hits on the log subscriber connection. Tested on ESP32 and RP2040: buffered writes dropped from ~15% to near zero with this change. ESP8266 and LibreTiny remain at LOG_NAGLE_COUNT=2 due to tighter buffer constraints. --- esphome/components/api/api_frame_helper.h | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/esphome/components/api/api_frame_helper.h b/esphome/components/api/api_frame_helper.h index 98de24501e..bec726b2a1 100644 --- a/esphome/components/api/api_frame_helper.h +++ b/esphome/components/api/api_frame_helper.h @@ -134,12 +134,16 @@ class APIFrameHelper { // // For log messages: Use Nagle to coalesce multiple small log packets into // fewer larger packets, reducing WiFi overhead. However, we limit batching - // to 3 messages to avoid excessive LWIP buffer pressure on memory-constrained - // devices like ESP8266. LWIP's TCP_OVERSIZE option coalesces the data into - // shared pbufs, but holding data too long waiting for Nagle's timer causes - // buffer exhaustion and dropped messages. + // to avoid excessive LWIP buffer pressure on memory-constrained devices. + // LWIP's TCP_OVERSIZE option coalesces the data into shared pbufs, but + // holding data too long waiting for Nagle's timer causes buffer exhaustion + // and dropped messages. // - // Flow: Log 1 (Nagle on) -> Log 2 (Nagle on) -> Log 3 (NODELAY, flush all) + // ESP32 (TCP_SND_BUF=64KB) / RP2040 (8×MSS): 4 logs per cycle + // ESP8266 / LibreTiny: 3 logs per cycle (tighter buffers) + // + // Flow (ESP32/RP2040): Log 1 (Nagle on) -> Log 2 -> Log 3 -> Log 4 (NODELAY, flush) + // Flow (other): Log 1 (Nagle on) -> Log 2 -> Log 3 (NODELAY, flush all) // void set_nodelay_for_message(bool is_log_message) { if (!is_log_message) { @@ -255,10 +259,16 @@ class APIFrameHelper { uint8_t tx_buf_tail_{0}; uint8_t tx_buf_count_{0}; // Nagle batching state for log messages. NODELAY_ON (-1) means NODELAY is enabled - // (immediate send). Values 1-2 count log messages in the current Nagle batch. + // (immediate send). Values 1..LOG_NAGLE_COUNT count log messages in the current Nagle batch. // After LOG_NAGLE_COUNT logs, we switch to NODELAY to flush and reset. + // ESP32 and RP2040 have larger TCP send buffers and can coalesce more; + // ESP8266 and LibreTiny have tighter buffers. static constexpr int8_t NODELAY_ON = -1; +#if defined(USE_ESP32) || defined(USE_RP2040) + static constexpr int8_t LOG_NAGLE_COUNT = 3; +#else static constexpr int8_t LOG_NAGLE_COUNT = 2; +#endif int8_t nodelay_state_{NODELAY_ON}; // Internal helper to set TCP_NODELAY socket option From c3fee4a21193aaf8a99a4258ff6bfd9f55d8d386 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 12 Mar 2026 16:54:03 -1000 Subject: [PATCH 2/2] Include LibreTiny in LOG_NAGLE_COUNT=3 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit LibreTiny's TCP_SND_BUF is 4×MSS (5840 bytes), matching ESP32's default. Only ESP8266 at 2×MSS needs the conservative count=2. --- esphome/components/api/api_frame_helper.h | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/esphome/components/api/api_frame_helper.h b/esphome/components/api/api_frame_helper.h index bec726b2a1..28784fc056 100644 --- a/esphome/components/api/api_frame_helper.h +++ b/esphome/components/api/api_frame_helper.h @@ -139,11 +139,11 @@ class APIFrameHelper { // holding data too long waiting for Nagle's timer causes buffer exhaustion // and dropped messages. // - // ESP32 (TCP_SND_BUF=64KB) / RP2040 (8×MSS): 4 logs per cycle - // ESP8266 / LibreTiny: 3 logs per cycle (tighter buffers) + // ESP32 (TCP_SND_BUF=4×MSS+) / RP2040 (8×MSS) / LibreTiny (4×MSS): 4 logs per cycle + // ESP8266 (2×MSS): 3 logs per cycle (tightest buffers) // - // Flow (ESP32/RP2040): Log 1 (Nagle on) -> Log 2 -> Log 3 -> Log 4 (NODELAY, flush) - // Flow (other): Log 1 (Nagle on) -> Log 2 -> Log 3 (NODELAY, flush all) + // Flow (ESP32/RP2040/LT): Log 1 (Nagle on) -> Log 2 -> Log 3 -> Log 4 (NODELAY, flush) + // Flow (ESP8266): Log 1 (Nagle on) -> Log 2 -> Log 3 (NODELAY, flush all) // void set_nodelay_for_message(bool is_log_message) { if (!is_log_message) { @@ -261,13 +261,13 @@ class APIFrameHelper { // Nagle batching state for log messages. NODELAY_ON (-1) means NODELAY is enabled // (immediate send). Values 1..LOG_NAGLE_COUNT count log messages in the current Nagle batch. // After LOG_NAGLE_COUNT logs, we switch to NODELAY to flush and reset. - // ESP32 and RP2040 have larger TCP send buffers and can coalesce more; - // ESP8266 and LibreTiny have tighter buffers. + // ESP8266 has the tightest TCP send buffer (2×MSS) and needs conservative batching. + // ESP32 (4×MSS+), RP2040 (8×MSS), and LibreTiny (4×MSS) can coalesce more. static constexpr int8_t NODELAY_ON = -1; -#if defined(USE_ESP32) || defined(USE_RP2040) - static constexpr int8_t LOG_NAGLE_COUNT = 3; -#else +#ifdef USE_ESP8266 static constexpr int8_t LOG_NAGLE_COUNT = 2; +#else + static constexpr int8_t LOG_NAGLE_COUNT = 3; #endif int8_t nodelay_state_{NODELAY_ON};