mirror of
https://github.com/esphome/esphome.git
synced 2026-09-03 19:46:02 +00:00
[esphome.ota] Filter fast-select wake hook to OTA listener netconn only
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.
This commit is contained in:
@@ -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 <cerrno>
|
||||
#include <cstdio>
|
||||
@@ -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() {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user