From 8921a9dd60c3e00d4ba676cdaad4e5141f9d7d04 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 10 Apr 2026 20:11:34 -1000 Subject: [PATCH] [core] fast_select stats: log details on load-bearing hit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a non-inline helper note_fast_select_load_bearing_() invoked only when the scan found data and the task notification counter was 0. Logs: hit sequence number, lwip_sock pointer, index in monitored_sockets_, raw rcvevent value, delay_ms, and how many other sockets also had data at that instant. Hot path is unchanged — the helper is out-of-line so yield_with_select_ still inlines to the same code as before for the zero-hit path. --- esphome/core/application.cpp | 28 ++++++++++++++++++++++++++++ esphome/core/application.h | 5 ++++- 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/esphome/core/application.cpp b/esphome/core/application.cpp index 8d51ddf0fd0..37aac76740b 100644 --- a/esphome/core/application.cpp +++ b/esphome/core/application.cpp @@ -462,6 +462,34 @@ void Application::log_fast_select_scan_stats_() { load_bearing); } +void Application::note_fast_select_load_bearing_(struct lwip_sock *sock, uint32_t delay_ms) { + uint32_t load_bearing = fast_select_scan_load_bearing_.fetch_add(1, std::memory_order_relaxed) + 1; + // Find the socket's index in monitored_sockets_ for easier correlation with registration order. + int index = -1; + for (size_t i = 0; i < this->monitored_sockets_.size(); i++) { + if (this->monitored_sockets_[i] == sock) { + index = static_cast(i); + break; + } + } + // Read the rcvevent value directly. This is the same offset-based read used by + // esphome_lwip_socket_has_data(); value > 0 means unread data is queued. + int16_t rcvevent = + *reinterpret_cast(reinterpret_cast(sock) + ESPHOME_LWIP_SOCK_RCVEVENT_OFFSET); + // Count how many other sockets also had data at this scan (could reveal whether it's always + // the same socket or a burst across multiple). + size_t sockets_with_data = 0; + for (struct lwip_sock *s : this->monitored_sockets_) { + if (esphome_lwip_socket_has_data(s)) + sockets_with_data++; + } + ESP_LOGW(TAG, + "fast_select LOAD-BEARING hit #%" PRIu32 ": sock=%p idx=%d/%u rcvevent=%d delay_ms=%" PRIu32 + " sockets_with_data=%u", + load_bearing, sock, index, static_cast(this->monitored_sockets_.size()), rcvevent, delay_ms, + static_cast(sockets_with_data)); +} + bool Application::register_socket(struct lwip_sock *sock) { // It modifies monitored_sockets_ without locking — must only be called from the main loop. if (sock == nullptr) diff --git a/esphome/core/application.h b/esphome/core/application.h index f0ce013224c..7072b6b4a24 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -664,6 +664,8 @@ class Application { static std::atomic fast_select_scan_load_bearing_; uint32_t fast_select_scan_stats_last_log_{0}; void log_fast_select_scan_stats_(); + // Non-inline, called only on the rare load-bearing event so the hot path stays unchanged. + void note_fast_select_load_bearing_(struct lwip_sock *sock, uint32_t delay_ms); #elif defined(USE_HOST) std::vector socket_fds_; // Vector of all monitored socket file descriptors #endif @@ -947,7 +949,8 @@ inline void ESPHOME_ALWAYS_INLINE Application::yield_with_select_(uint32_t delay fast_select_scan_found_data_.fetch_add(1, std::memory_order_relaxed); if (fast_select_notify_value_before_scan == 0) { // Scan was load-bearing: no notification pending, so Take would have stalled. - fast_select_scan_load_bearing_.fetch_add(1, std::memory_order_relaxed); + // Delegate to a non-inline helper so the hot path stays the same size. + this->note_fast_select_load_bearing_(sock, delay_ms); } yield(); return;