From e15782844e75ca15cb849647c6ab0a76f48213da Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 20 Mar 2026 18:34:55 -1000 Subject: [PATCH 1/3] [core] Inline after_loop_tasks_() into header Trivial one-liner (single assignment) called from the inlined loop() and setup(). Eliminates another unnecessary function call. --- esphome/core/application.cpp | 5 ----- esphome/core/application.h | 2 +- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/esphome/core/application.cpp b/esphome/core/application.cpp index 056a9bf85b5..fc865d8b93f 100644 --- a/esphome/core/application.cpp +++ b/esphome/core/application.cpp @@ -476,11 +476,6 @@ void Application::before_loop_tasks_(uint32_t loop_start_time) { this->in_loop_ = true; } -void Application::after_loop_tasks_() { - // Clear the in_loop_ flag to indicate we're done processing components - this->in_loop_ = false; -} - #ifdef USE_LWIP_FAST_SELECT bool Application::register_socket(struct lwip_sock *sock) { // It modifies monitored_sockets_ without locking — must only be called from the main loop. diff --git a/esphome/core/application.h b/esphome/core/application.h index 5cb5f32202b..5e1dd121863 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -618,7 +618,7 @@ class Application { void enable_pending_loops_(); void activate_looping_component_(uint16_t index); void before_loop_tasks_(uint32_t loop_start_time); - void after_loop_tasks_(); + inline void ESPHOME_ALWAYS_INLINE after_loop_tasks_() { this->in_loop_ = false; } /// Process dump_config output one component per loop iteration. /// Extracted from loop() to keep cold startup/reconnect logging out of the hot path. From 998d61f434f11822ccdafe58153609e1f45a1083 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 20 Mar 2026 18:42:55 -1000 Subject: [PATCH 2/3] [core] Inline yield_with_select_ fast path for LWIP_FAST_SELECT When USE_LWIP_FAST_SELECT is defined (ESP32/LibreTiny), move the yield_with_select_ implementation to application.h as always_inline. This eliminates another stack frame from the hot loop path on these platforms. The fallback select()/delay() paths for other platforms remain in application.cpp. --- esphome/core/application.cpp | 37 ++++------------------------------ esphome/core/application.h | 39 ++++++++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 33 deletions(-) diff --git a/esphome/core/application.cpp b/esphome/core/application.cpp index fc865d8b93f..8cf225fb747 100644 --- a/esphome/core/application.cpp +++ b/esphome/core/application.cpp @@ -12,13 +12,6 @@ #endif #ifdef USE_LWIP_FAST_SELECT #include "esphome/core/lwip_fast_select.h" -#ifdef USE_ESP32 -#include -#include -#else -#include -#include -#endif #endif // USE_LWIP_FAST_SELECT #include "esphome/core/version.h" #include "esphome/core/hal.h" @@ -557,34 +550,11 @@ void Application::unregister_socket_fd(int fd) { #endif +// When USE_LWIP_FAST_SELECT is defined, yield_with_select_ is inlined in application.h +#if !defined(USE_SOCKET_SELECT_SUPPORT) || !defined(USE_LWIP_FAST_SELECT) void Application::yield_with_select_(uint32_t delay_ms) { // Delay while monitoring sockets. When delay_ms is 0, always yield() to ensure other tasks run. -#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_LWIP_FAST_SELECT) - // Fast path (ESP32/LibreTiny): reads rcvevent directly from cached lwip_sock pointers. - // Safe because this runs on the main loop which owns socket lifetime (create, read, close). - if (delay_ms == 0) [[unlikely]] { - yield(); - return; - } - - // Check if any socket already has pending data before sleeping. - // If a socket still has unread data (rcvevent > 0) but the task notification was already - // consumed, ulTaskNotifyTake would block until timeout — adding up to delay_ms latency. - // This scan preserves select() semantics: return immediately when any fd is ready. - for (struct lwip_sock *sock : this->monitored_sockets_) { - if (esphome_lwip_socket_has_data(sock)) { - yield(); - return; - } - } - - // Sleep with instant wake via FreeRTOS task notification. - // Woken by: callback wrapper (socket data arrives), wake_loop_threadsafe() (other tasks), or timeout. - // Without USE_WAKE_LOOP_THREADSAFE, only hooked socket callbacks wake the task — - // background tasks won't call wake, so this degrades to a pure timeout (same as old select path). - ulTaskNotifyTake(pdTRUE, pdMS_TO_TICKS(delay_ms)); - -#elif defined(USE_SOCKET_SELECT_SUPPORT) +#if defined(USE_SOCKET_SELECT_SUPPORT) // Fallback select() path (host platform and any future platforms without fast select). // ESP32 and LibreTiny are excluded by the #if above — they use the fast path. if (!this->socket_fds_.empty()) [[likely]] { @@ -643,6 +613,7 @@ void Application::yield_with_select_(uint32_t delay_ms) { delay(delay_ms); #endif } +#endif // !defined(USE_SOCKET_SELECT_SUPPORT) || !defined(USE_LWIP_FAST_SELECT) // App storage — asm label shares the linker symbol with "extern Application App". // char[] is trivially destructible, so no __cxa_atexit or destructor chain is emitted. diff --git a/esphome/core/application.h b/esphome/core/application.h index 5e1dd121863..9cbae42e22c 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -27,6 +27,13 @@ #ifdef USE_SOCKET_SELECT_SUPPORT #ifdef USE_LWIP_FAST_SELECT #include "esphome/core/lwip_fast_select.h" +#ifdef USE_ESP32 +#include +#include +#else +#include +#include +#endif #else #include #ifdef USE_WAKE_LOOP_THREADSAFE @@ -628,7 +635,11 @@ class Application { void feed_wdt_arch_(); /// Perform a delay while also monitoring socket file descriptors for readiness +#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_LWIP_FAST_SELECT) + inline void ESPHOME_ALWAYS_INLINE yield_with_select_(uint32_t delay_ms); +#else void yield_with_select_(uint32_t delay_ms); +#endif #if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) && !defined(USE_LWIP_FAST_SELECT) void setup_wake_loop_threadsafe_(); // Create wake notification socket @@ -883,4 +894,32 @@ inline void ESPHOME_ALWAYS_INLINE Application::loop() { } } +#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_LWIP_FAST_SELECT) +inline void ESPHOME_ALWAYS_INLINE Application::yield_with_select_(uint32_t delay_ms) { + // Fast path (ESP32/LibreTiny): reads rcvevent directly from cached lwip_sock pointers. + // Safe because this runs on the main loop which owns socket lifetime (create, read, close). + if (delay_ms == 0) [[unlikely]] { + yield(); + return; + } + + // Check if any socket already has pending data before sleeping. + // If a socket still has unread data (rcvevent > 0) but the task notification was already + // consumed, ulTaskNotifyTake would block until timeout — adding up to delay_ms latency. + // This scan preserves select() semantics: return immediately when any fd is ready. + for (struct lwip_sock *sock : this->monitored_sockets_) { + if (esphome_lwip_socket_has_data(sock)) { + yield(); + return; + } + } + + // Sleep with instant wake via FreeRTOS task notification. + // Woken by: callback wrapper (socket data arrives), wake_loop_threadsafe() (other tasks), or timeout. + // Without USE_WAKE_LOOP_THREADSAFE, only hooked socket callbacks wake the task — + // background tasks won't call wake, so this degrades to a pure timeout (same as old select path). + ulTaskNotifyTake(pdTRUE, pdMS_TO_TICKS(delay_ms)); +} +#endif // defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_LWIP_FAST_SELECT) + } // namespace esphome From 1918e17c21600bf9381b40cc6c42e6e1d99d02e7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 20 Mar 2026 19:05:01 -1000 Subject: [PATCH 3/3] [core] Inline yield_with_select_ on all embedded platforms Inline yield_with_select_ for ESP8266/RP2040 (socket_delay) and no-socket (delay) paths in addition to the LWIP_FAST_SELECT path. Only the select() fallback (host platform) remains in the .cpp. This ensures yield_with_select_ is inlined into the loop on all embedded platforms, not just ESP32/LibreTiny. --- esphome/core/application.cpp | 18 +++--------------- esphome/core/application.h | 25 +++++++++++++++++++------ 2 files changed, 22 insertions(+), 21 deletions(-) diff --git a/esphome/core/application.cpp b/esphome/core/application.cpp index 8cf225fb747..064760915a1 100644 --- a/esphome/core/application.cpp +++ b/esphome/core/application.cpp @@ -550,13 +550,10 @@ void Application::unregister_socket_fd(int fd) { #endif -// When USE_LWIP_FAST_SELECT is defined, yield_with_select_ is inlined in application.h -#if !defined(USE_SOCKET_SELECT_SUPPORT) || !defined(USE_LWIP_FAST_SELECT) +// Only the select() fallback path remains in the .cpp — all other paths are inlined in application.h +#if defined(USE_SOCKET_SELECT_SUPPORT) && !defined(USE_LWIP_FAST_SELECT) void Application::yield_with_select_(uint32_t delay_ms) { - // Delay while monitoring sockets. When delay_ms is 0, always yield() to ensure other tasks run. -#if defined(USE_SOCKET_SELECT_SUPPORT) // Fallback select() path (host platform and any future platforms without fast select). - // ESP32 and LibreTiny are excluded by the #if above — they use the fast path. if (!this->socket_fds_.empty()) [[likely]] { // Update fd_set if socket list has changed if (this->socket_fds_changed_) [[unlikely]] { @@ -603,17 +600,8 @@ void Application::yield_with_select_(uint32_t delay_ms) { } // No sockets registered or select() failed - use regular delay delay(delay_ms); -#elif (defined(USE_ESP8266) || defined(USE_RP2040)) && defined(USE_SOCKET_IMPL_LWIP_TCP) - // No select support but can wake on socket activity - // ESP8266: via esp_schedule() - // RP2040: via __sev()/__wfe() hardware sleep/wake - socket::socket_delay(delay_ms); -#else - // No select support, use regular delay - delay(delay_ms); -#endif } -#endif // !defined(USE_SOCKET_SELECT_SUPPORT) || !defined(USE_LWIP_FAST_SELECT) +#endif // defined(USE_SOCKET_SELECT_SUPPORT) && !defined(USE_LWIP_FAST_SELECT) // App storage — asm label shares the linker symbol with "extern Application App". // char[] is trivially destructible, so no __cxa_atexit or destructor chain is emitted. diff --git a/esphome/core/application.h b/esphome/core/application.h index 9cbae42e22c..f916b6604bd 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -43,7 +43,8 @@ #endif // USE_SOCKET_SELECT_SUPPORT #if (defined(USE_ESP8266) || defined(USE_RP2040)) && defined(USE_SOCKET_IMPL_LWIP_TCP) namespace esphome::socket { -void socket_wake(); // NOLINT(readability-redundant-declaration) +void socket_wake(); // NOLINT(readability-redundant-declaration) +void socket_delay(uint32_t ms); // NOLINT(readability-redundant-declaration) } // namespace esphome::socket #endif #ifdef USE_BINARY_SENSOR @@ -635,10 +636,11 @@ class Application { void feed_wdt_arch_(); /// Perform a delay while also monitoring socket file descriptors for readiness -#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_LWIP_FAST_SELECT) - inline void ESPHOME_ALWAYS_INLINE yield_with_select_(uint32_t delay_ms); -#else +#if defined(USE_SOCKET_SELECT_SUPPORT) && !defined(USE_LWIP_FAST_SELECT) + // select() fallback path is too complex to inline (host platform) void yield_with_select_(uint32_t delay_ms); +#else + inline void ESPHOME_ALWAYS_INLINE yield_with_select_(uint32_t delay_ms); #endif #if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) && !defined(USE_LWIP_FAST_SELECT) @@ -894,8 +896,10 @@ inline void ESPHOME_ALWAYS_INLINE Application::loop() { } } -#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_LWIP_FAST_SELECT) +// Inline yield_with_select_ for all paths except the select() fallback +#if !defined(USE_SOCKET_SELECT_SUPPORT) || defined(USE_LWIP_FAST_SELECT) inline void ESPHOME_ALWAYS_INLINE Application::yield_with_select_(uint32_t delay_ms) { +#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_LWIP_FAST_SELECT) // Fast path (ESP32/LibreTiny): reads rcvevent directly from cached lwip_sock pointers. // Safe because this runs on the main loop which owns socket lifetime (create, read, close). if (delay_ms == 0) [[unlikely]] { @@ -919,7 +923,16 @@ inline void ESPHOME_ALWAYS_INLINE Application::yield_with_select_(uint32_t delay // Without USE_WAKE_LOOP_THREADSAFE, only hooked socket callbacks wake the task — // background tasks won't call wake, so this degrades to a pure timeout (same as old select path). ulTaskNotifyTake(pdTRUE, pdMS_TO_TICKS(delay_ms)); +#elif (defined(USE_ESP8266) || defined(USE_RP2040)) && defined(USE_SOCKET_IMPL_LWIP_TCP) + // No select support but can wake on socket activity + // ESP8266: via esp_schedule() + // RP2040: via __sev()/__wfe() hardware sleep/wake + socket::socket_delay(delay_ms); +#else + // No select support, use regular delay + delay(delay_ms); +#endif } -#endif // defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_LWIP_FAST_SELECT) +#endif // !defined(USE_SOCKET_SELECT_SUPPORT) || defined(USE_LWIP_FAST_SELECT) } // namespace esphome