From 01beb56899202b4608f5cd6935d285c538842c8b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 10 Apr 2026 17:14:00 -1000 Subject: [PATCH] [esphome.ota] Filter fast-select wake hook to OTA listener netconn only MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The inline OTA wake hook was firing on every NETCONN_EVT_RCVPLUS across every monitored socket (API client data packets, mDNS queries, web server, etc.). Each false fire paid two volatile stores + memw barriers to mark OTA pending-enable, only for OTA::loop() to run a wake-up tick and re-disable itself because there was no actual listener activity. Add a compare-against-listener filter in esphome_socket_event_callback so the wake hook only fires when `conn` matches the OTA listen socket's netconn. Non-match sockets now cost only a pointer load + one branch (~3 instructions) instead of the full ~10-instruction hook body. Plumbing: - lwip_fast_select.[ch]: new s_ota_listener_conn global + esphome_fast_select_set_ota_listener_sock() setter, used in the callback. - BSDSocketImpl / LwIPSocketImpl: new public get_cached_sock() accessor (only under USE_LWIP_FAST_SELECT) mirroring the existing get_fd() pattern. - ESPHomeOTAComponent::setup(): after registering the wake component, install the listener filter with this->server_->get_cached_sock(). Raw TCP (ESP8266/RP2040) is unaffected — that path wakes from LWIPRawListenImpl::accept_fn_, which only fires for the specific listener pcb it was registered on, so the filtering is implicit there. --- .../components/esphome/ota/ota_esphome.cpp | 11 +++++ esphome/components/socket/bsd_sockets_impl.h | 9 ++++ esphome/components/socket/lwip_sockets_impl.h | 7 ++++ esphome/core/lwip_fast_select.c | 42 ++++++++++++------- esphome/core/lwip_fast_select.h | 8 ++++ 5 files changed, 63 insertions(+), 14 deletions(-) diff --git a/esphome/components/esphome/ota/ota_esphome.cpp b/esphome/components/esphome/ota/ota_esphome.cpp index e2b12e1208..af2a609642 100644 --- a/esphome/components/esphome/ota/ota_esphome.cpp +++ b/esphome/components/esphome/ota/ota_esphome.cpp @@ -15,6 +15,9 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" #include "esphome/core/util.h" +#ifdef USE_LWIP_FAST_SELECT +#include "esphome/core/lwip_fast_select.h" +#endif #include #include @@ -69,6 +72,14 @@ void ESPHomeOTAComponent::setup() { // Register for socket wake notifications. loop() disables itself on its first // idle tick — no need to disable_loop() here explicitly. App.set_ota_wake_component(this); +#ifdef USE_LWIP_FAST_SELECT + // Install the listener filter so the fast-select RCVPLUS wake hook only fires for + // events on this listener's netconn (i.e. new incoming connections). Without this, + // every RCVPLUS across all monitored sockets (API client data, mDNS, etc.) would + // pay the inline hook's two volatile stores + memw barriers to mark OTA + // pending-enable, even though OTA would just re-disable itself on the next tick. + esphome_fast_select_set_ota_listener_sock(this->server_->get_cached_sock()); +#endif } void ESPHomeOTAComponent::dump_config() { diff --git a/esphome/components/socket/bsd_sockets_impl.h b/esphome/components/socket/bsd_sockets_impl.h index e520784702..ceb7556d9f 100644 --- a/esphome/components/socket/bsd_sockets_impl.h +++ b/esphome/components/socket/bsd_sockets_impl.h @@ -118,6 +118,15 @@ class BSDSocketImpl { int get_fd() const { return this->fd_; } +#ifdef USE_LWIP_FAST_SELECT + // Cached lwip_sock pointer captured at construction. Used by OTA to register its + // listener netconn with the fast-select wake hook filter so the hook only fires on + // OTA-relevant events. Returns nullptr for non-monitored sockets. + struct lwip_sock *get_cached_sock() const { + return this->cached_sock_; + } +#endif + protected: int fd_{-1}; #ifdef USE_LWIP_FAST_SELECT diff --git a/esphome/components/socket/lwip_sockets_impl.h b/esphome/components/socket/lwip_sockets_impl.h index 942d0ccf85..cda37b5dc6 100644 --- a/esphome/components/socket/lwip_sockets_impl.h +++ b/esphome/components/socket/lwip_sockets_impl.h @@ -84,6 +84,13 @@ class LwIPSocketImpl { int get_fd() const { return this->fd_; } +#ifdef USE_LWIP_FAST_SELECT + // See BSDSocketImpl::get_cached_sock() — same purpose, same semantics. + struct lwip_sock *get_cached_sock() const { + return this->cached_sock_; + } +#endif + protected: int fd_{-1}; #ifdef USE_LWIP_FAST_SELECT diff --git a/esphome/core/lwip_fast_select.c b/esphome/core/lwip_fast_select.c index 061fafbc51..60a1cf60f1 100644 --- a/esphome/core/lwip_fast_select.c +++ b/esphome/core/lwip_fast_select.c @@ -161,16 +161,24 @@ _Static_assert(offsetof(struct lwip_sock, rcvevent) == ESPHOME_LWIP_SOCK_RCVEVEN static netconn_callback s_original_callback = NULL; #ifdef ESPHOME_USE_OTA -// Extern wake hook for the OTA component (implemented in application.cpp). Called from the -// TCP/IP task on every NETCONN_EVT_RCVPLUS — not just OTA's listener, so this can be a false -// wake from an unrelated monitored socket. OTA::loop() handles that by disabling itself again -// when there is no pending work. The hook only marks the OTA component as pending loop-enable; -// it does not itself wake the main task (the caller below already does that). -// NOTE: ESPHOME_USE_OTA (not USE_OTA) because USE_OTA only lives in defines.h, and this .c -// file cannot include defines.h — macros.h → Arduino.h would break the C compile under -// Arduino builds. ota/__init__.py emits -DESPHOME_USE_OTA as a build flag specifically so -// this file can see it without a name collision with the defines.h USE_OTA entry. -extern void esphome_wake_ota_component_any_context(void); +// OTA listener netconn, captured via esphome_fast_select_set_ota_listener_sock() at OTA +// setup(). The wake hook only fires when the callback's `conn` argument matches this +// pointer — i.e. only for RCVPLUS events on the OTA listen socket (new accepts). Avoids +// paying the inline wake hook's cost (two volatile stores + memw barriers) on every API +// client data packet, mDNS query, etc. +static struct netconn *s_ota_listener_conn = NULL; +// Inline wake hook defined in esphome/core/wake.h. ESPHOME_USE_OTA (not USE_OTA) because +// USE_OTA only lives in defines.h, and this .c file cannot include defines.h — macros.h → +// Arduino.h would break the C compile under Arduino builds. ota/__init__.py emits +// -DESPHOME_USE_OTA as a build flag so this file can see it without a name collision with +// the defines.h USE_OTA entry. +#include "esphome/core/wake.h" + +void esphome_fast_select_set_ota_listener_sock(struct lwip_sock *sock) { + s_ota_listener_conn = (sock != NULL) ? sock->conn : NULL; +} +#else +void esphome_fast_select_set_ota_listener_sock(struct lwip_sock *sock) { (void) sock; } #endif // Wrapper callback: calls original event_callback + notifies main loop task. @@ -188,10 +196,16 @@ static void esphome_socket_event_callback(struct netconn *conn, enum netconn_evt // already wake the main loop through the RCVPLUS path. if (evt == NETCONN_EVT_RCVPLUS) { #ifdef ESPHOME_USE_OTA - // Mark the OTA component pending-enable BEFORE xTaskNotifyGive — the flags must be - // visible before we wake the main task, otherwise the main loop could run a full - // iteration without seeing the pending-enable request. - esphome_wake_ota_component_any_context(); + // Filter: only mark OTA pending-enable when the event is for OTA's listen socket. + // Without this, every RCVPLUS (API client data, mDNS, etc.) would pay the inline + // wake hook's two volatile stores + memw barriers. The setter that installs + // s_ota_listener_conn is called from OTA setup(); until then the pointer is NULL + // and the filter skips the wake work entirely, which is the correct idle behavior. + // MUST happen before xTaskNotifyGive below — the flags have to be visible before + // the main task wakes, or the main loop could run a full iteration and miss them. + if (conn == s_ota_listener_conn) { + esphome_wake_ota_component_any_context(); + } #endif TaskHandle_t task = esphome_main_task_handle; if (task != NULL) { diff --git a/esphome/core/lwip_fast_select.h b/esphome/core/lwip_fast_select.h index 20ac191673..9e1438ecdd 100644 --- a/esphome/core/lwip_fast_select.h +++ b/esphome/core/lwip_fast_select.h @@ -53,6 +53,14 @@ static inline bool esphome_lwip_socket_has_data(struct lwip_sock *sock) { /// The sock pointer must have been obtained from esphome_lwip_get_sock(). void esphome_lwip_hook_socket(struct lwip_sock *sock); +/// Filter the inline OTA wake hook in the fast-select callback so it only fires for +/// RCVPLUS events on this specific listener's netconn. Without this, every monitored +/// socket's RCVPLUS (API client data, web server, mDNS, etc.) would mark OTA +/// pending-enable and force its loop to run a wake-up tick only to re-disable itself. +/// Captured at OTA setup(); stays pointing at the listener for the device's lifetime. +/// Pass NULL to clear the filter (wake fires on every RCVPLUS, pre-filter behavior). +void esphome_fast_select_set_ota_listener_sock(struct lwip_sock *sock); + /// Set or clear TCP_NODELAY on a socket's tcp_pcb directly. /// Must be called with the TCPIP core lock held (LwIPLock in C++). /// This bypasses lwip_setsockopt() overhead (socket lookups, switch cascade,