From 63a0581440c45150d9a0910523a355047094f5c4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 1 Apr 2026 12:05:21 -1000 Subject: [PATCH 1/2] Add empty_relaxed() fast path to wifi_loop_ queue drain On Xtensa (ESP32), even relaxed atomic loads emit memw instructions, but they avoid the more expensive acquire fences. The pop() path required acquire loads + release stores just to discover the queue was empty. By checking empty_relaxed() first, the steady-state connected path (queue always empty) returns in ~7 instructions instead of falling through to get_and_reset_dropped_count() and pop(). --- esphome/components/wifi/wifi_component_esp_idf.cpp | 5 +++++ esphome/components/wifi/wifi_component_libretiny.cpp | 7 ++++++- esphome/core/freertos_queue.h | 4 ++++ esphome/core/lock_free_queue.h | 6 ++++++ 4 files changed, 21 insertions(+), 1 deletion(-) diff --git a/esphome/components/wifi/wifi_component_esp_idf.cpp b/esphome/components/wifi/wifi_component_esp_idf.cpp index 4097df80af..23dd81c866 100644 --- a/esphome/components/wifi/wifi_component_esp_idf.cpp +++ b/esphome/components/wifi/wifi_component_esp_idf.cpp @@ -716,6 +716,11 @@ const char *get_disconnect_reason_str(uint8_t reason) { } void WiFiComponent::wifi_loop_() { + // Fast path: relaxed check avoids acquire fences (4x memw on Xtensa) when queue is empty. + // Safe from consumer side — worst case we process events one loop iteration late. + if (this->event_queue_.empty_relaxed()) + return; + uint16_t dropped = this->event_queue_.get_and_reset_dropped_count(); if (dropped > 0) { ESP_LOGW(TAG, "Dropped %u WiFi events due to buffer overflow", dropped); diff --git a/esphome/components/wifi/wifi_component_libretiny.cpp b/esphome/components/wifi/wifi_component_libretiny.cpp index 479030d442..1adfa57332 100644 --- a/esphome/components/wifi/wifi_component_libretiny.cpp +++ b/esphome/components/wifi/wifi_component_libretiny.cpp @@ -778,7 +778,12 @@ network::IPAddress WiFiComponent::wifi_subnet_mask_() { return {WiFi.subnetMask( network::IPAddress WiFiComponent::wifi_gateway_ip_() { return {WiFi.gatewayIP()}; } network::IPAddress WiFiComponent::wifi_dns_ip_(int num) { return {WiFi.dnsIP(num)}; } void WiFiComponent::wifi_loop_() { - // Check for dropped events due to queue overflow + // Fast path: skip queue drain when empty. + // On LockFreeQueue platforms, relaxed loads avoid memory fences. + // On FreeRTOSQueue platforms, this is a lightweight uxQueueMessagesWaiting check. + if (this->event_queue_.empty_relaxed()) + return; + uint16_t dropped = this->event_queue_.get_and_reset_dropped_count(); if (dropped > 0) { ESP_LOGW(TAG, "Dropped %" PRIu16 " WiFi events due to buffer overflow", dropped); diff --git a/esphome/core/freertos_queue.h b/esphome/core/freertos_queue.h index 2f3faf818a..a67718ea12 100644 --- a/esphome/core/freertos_queue.h +++ b/esphome/core/freertos_queue.h @@ -82,6 +82,10 @@ template class FreeRTOSQueue { bool empty() const { return uxQueueMessagesWaiting(this->handle_) == 0; } + /// Fast empty check — same as empty() for FreeRTOS queues since + /// uxQueueMessagesWaiting is already a lightweight read. + bool empty_relaxed() const { return this->empty(); } + bool full() const { return uxQueueSpacesAvailable(this->handle_) == 0; } size_t size() const { return uxQueueMessagesWaiting(this->handle_); } diff --git a/esphome/core/lock_free_queue.h b/esphome/core/lock_free_queue.h index 316186ea54..62c3b21fae 100644 --- a/esphome/core/lock_free_queue.h +++ b/esphome/core/lock_free_queue.h @@ -118,6 +118,12 @@ template class LockFreeQueue { bool empty() const { return head_.load(std::memory_order_acquire) == tail_.load(std::memory_order_acquire); } + /// Fast empty check using relaxed loads — no memory fences on Xtensa. + /// Safe when called only from the consumer side: head_ is only written by the + /// consumer, and tail_ can only advance. Worst case we miss a just-pushed item + /// and catch it next loop iteration. + bool empty_relaxed() const { return head_.load(std::memory_order_relaxed) == tail_.load(std::memory_order_relaxed); } + bool full() const { uint8_t next_tail = next_index(tail_.load(std::memory_order_relaxed)); return next_tail == head_.load(std::memory_order_acquire); From 20884d5844f67b92a99923bab12d0b60d1286709 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 1 Apr 2026 12:08:39 -1000 Subject: [PATCH 2/2] Use empty() instead of empty_relaxed() for wifi_loop_ fast path MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit GCC on Xtensa emits memw for relaxed atomic loads too, so the savings from relaxed vs acquire are only 2 memw (~4 cycles) — not worth the weaker correctness guarantees. The real optimization is the early return that skips get_and_reset_dropped_count() and the pop() loop entirely. --- esphome/components/wifi/wifi_component_esp_idf.cpp | 5 ++--- esphome/components/wifi/wifi_component_libretiny.cpp | 6 ++---- esphome/core/freertos_queue.h | 4 ---- esphome/core/lock_free_queue.h | 6 ------ 4 files changed, 4 insertions(+), 17 deletions(-) diff --git a/esphome/components/wifi/wifi_component_esp_idf.cpp b/esphome/components/wifi/wifi_component_esp_idf.cpp index 23dd81c866..e079f3cda2 100644 --- a/esphome/components/wifi/wifi_component_esp_idf.cpp +++ b/esphome/components/wifi/wifi_component_esp_idf.cpp @@ -716,9 +716,8 @@ const char *get_disconnect_reason_str(uint8_t reason) { } void WiFiComponent::wifi_loop_() { - // Fast path: relaxed check avoids acquire fences (4x memw on Xtensa) when queue is empty. - // Safe from consumer side — worst case we process events one loop iteration late. - if (this->event_queue_.empty_relaxed()) + // Fast path: skip dropped count check and pop loop when queue is empty + if (this->event_queue_.empty()) return; uint16_t dropped = this->event_queue_.get_and_reset_dropped_count(); diff --git a/esphome/components/wifi/wifi_component_libretiny.cpp b/esphome/components/wifi/wifi_component_libretiny.cpp index 1adfa57332..946d1ca50d 100644 --- a/esphome/components/wifi/wifi_component_libretiny.cpp +++ b/esphome/components/wifi/wifi_component_libretiny.cpp @@ -778,10 +778,8 @@ network::IPAddress WiFiComponent::wifi_subnet_mask_() { return {WiFi.subnetMask( network::IPAddress WiFiComponent::wifi_gateway_ip_() { return {WiFi.gatewayIP()}; } network::IPAddress WiFiComponent::wifi_dns_ip_(int num) { return {WiFi.dnsIP(num)}; } void WiFiComponent::wifi_loop_() { - // Fast path: skip queue drain when empty. - // On LockFreeQueue platforms, relaxed loads avoid memory fences. - // On FreeRTOSQueue platforms, this is a lightweight uxQueueMessagesWaiting check. - if (this->event_queue_.empty_relaxed()) + // Fast path: skip dropped count check and pop loop when queue is empty + if (this->event_queue_.empty()) return; uint16_t dropped = this->event_queue_.get_and_reset_dropped_count(); diff --git a/esphome/core/freertos_queue.h b/esphome/core/freertos_queue.h index a67718ea12..2f3faf818a 100644 --- a/esphome/core/freertos_queue.h +++ b/esphome/core/freertos_queue.h @@ -82,10 +82,6 @@ template class FreeRTOSQueue { bool empty() const { return uxQueueMessagesWaiting(this->handle_) == 0; } - /// Fast empty check — same as empty() for FreeRTOS queues since - /// uxQueueMessagesWaiting is already a lightweight read. - bool empty_relaxed() const { return this->empty(); } - bool full() const { return uxQueueSpacesAvailable(this->handle_) == 0; } size_t size() const { return uxQueueMessagesWaiting(this->handle_); } diff --git a/esphome/core/lock_free_queue.h b/esphome/core/lock_free_queue.h index 62c3b21fae..316186ea54 100644 --- a/esphome/core/lock_free_queue.h +++ b/esphome/core/lock_free_queue.h @@ -118,12 +118,6 @@ template class LockFreeQueue { bool empty() const { return head_.load(std::memory_order_acquire) == tail_.load(std::memory_order_acquire); } - /// Fast empty check using relaxed loads — no memory fences on Xtensa. - /// Safe when called only from the consumer side: head_ is only written by the - /// consumer, and tail_ can only advance. Worst case we miss a just-pushed item - /// and catch it next loop iteration. - bool empty_relaxed() const { return head_.load(std::memory_order_relaxed) == tail_.load(std::memory_order_relaxed); } - bool full() const { uint8_t next_tail = next_index(tail_.load(std::memory_order_relaxed)); return next_tail == head_.load(std::memory_order_acquire);