From d0a402f20163b662271d9c83d7c5007217083988 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 27 Jun 2025 12:49:44 -0500 Subject: [PATCH] Extract lock-free queue and event pool to core helpers --- esphome/components/esp32_ble/ble.cpp | 1 - esphome/components/esp32_ble/ble.h | 8 ++-- esphome/components/esp32_ble/ble_event.h | 3 ++ .../ble_event_pool.h => core/event_pool.h} | 31 ++++++------- .../queue.h => core/lock_free_queue.h} | 46 +++++++++++++++---- 5 files changed, 59 insertions(+), 30 deletions(-) rename esphome/{components/esp32_ble/ble_event_pool.h => core/event_pool.h} (61%) rename esphome/{components/esp32_ble/queue.h => core/lock_free_queue.h} (58%) diff --git a/esphome/components/esp32_ble/ble.cpp b/esphome/components/esp32_ble/ble.cpp index b10d1fe10a..8b0cf4da98 100644 --- a/esphome/components/esp32_ble/ble.cpp +++ b/esphome/components/esp32_ble/ble.cpp @@ -1,7 +1,6 @@ #ifdef USE_ESP32 #include "ble.h" -#include "ble_event_pool.h" #include "esphome/core/application.h" #include "esphome/core/helpers.h" diff --git a/esphome/components/esp32_ble/ble.h b/esphome/components/esp32_ble/ble.h index 9fe996086e..ce452d65c4 100644 --- a/esphome/components/esp32_ble/ble.h +++ b/esphome/components/esp32_ble/ble.h @@ -12,8 +12,8 @@ #include "esphome/core/helpers.h" #include "ble_event.h" -#include "ble_event_pool.h" -#include "queue.h" +#include "esphome/core/lock_free_queue.h" +#include "esphome/core/event_pool.h" #ifdef USE_ESP32 @@ -148,8 +148,8 @@ class ESP32BLE : public Component { std::vector ble_status_event_handlers_; BLEComponentState state_{BLE_COMPONENT_STATE_OFF}; - LockFreeQueue ble_events_; - BLEEventPool ble_event_pool_; + esphome::LockFreeQueue ble_events_; + esphome::EventPool ble_event_pool_; BLEAdvertising *advertising_{}; esp_ble_io_cap_t io_cap_{ESP_IO_CAP_NONE}; uint32_t advertising_cycle_time_{}; diff --git a/esphome/components/esp32_ble/ble_event.h b/esphome/components/esp32_ble/ble_event.h index dd3ec3da42..bbb4984b9c 100644 --- a/esphome/components/esp32_ble/ble_event.h +++ b/esphome/components/esp32_ble/ble_event.h @@ -139,6 +139,9 @@ class BLEEvent { // Default constructor for pre-allocation in pool BLEEvent() : type_(GAP) {} + // Invoked on return to EventPool + void clear() { this->cleanup_heap_data(); } + // Clean up any heap-allocated data void cleanup_heap_data() { if (this->type_ == GAP) { diff --git a/esphome/components/esp32_ble/ble_event_pool.h b/esphome/core/event_pool.h similarity index 61% rename from esphome/components/esp32_ble/ble_event_pool.h rename to esphome/core/event_pool.h index ef123b1325..39537267ca 100644 --- a/esphome/components/esp32_ble/ble_event_pool.h +++ b/esphome/core/event_pool.h @@ -4,22 +4,20 @@ #include #include -#include "ble_event.h" -#include "queue.h" #include "esphome/core/helpers.h" +#include "esphome/core/lock_free_queue.h" namespace esphome { -namespace esp32_ble { -// BLE Event Pool - On-demand pool of BLEEvent objects to avoid heap fragmentation +// Event Pool - On-demand pool of objects to avoid heap fragmentation // Events are allocated on first use and reused thereafter, growing to peak usage -template class BLEEventPool { +template class EventPool { public: - BLEEventPool() : total_created_(0) {} + EventPool() : total_created_(0) {} - ~BLEEventPool() { + ~EventPool() { // Clean up any remaining events in the free list - BLEEvent *event; + T *event; while ((event = this->free_list_.pop()) != nullptr) { delete event; } @@ -27,9 +25,9 @@ template class BLEEventPool { // Allocate an event from the pool // Returns nullptr if pool is full - BLEEvent *allocate() { + T *allocate() { // Try to get from free list first - BLEEvent *event = this->free_list_.pop(); + T *event = this->free_list_.pop(); if (event != nullptr) return event; @@ -40,7 +38,7 @@ template class BLEEventPool { } // Use internal RAM for better performance - RAMAllocator allocator(RAMAllocator::ALLOC_INTERNAL); + RAMAllocator allocator(RAMAllocator::ALLOC_INTERNAL); event = allocator.allocate(1); if (event == nullptr) { @@ -49,24 +47,25 @@ template class BLEEventPool { } // Placement new to construct the object - new (event) BLEEvent(); + new (event) T(); this->total_created_++; return event; } // Return an event to the pool for reuse - void release(BLEEvent *event) { + void release(T *event) { if (event != nullptr) { + // Clean up the event's allocated memory + event->clear(); this->free_list_.push(event); } } private: - LockFreeQueue free_list_; // Free events ready for reuse - uint8_t total_created_; // Total events created (high water mark) + LockFreeQueue free_list_; // Free events ready for reuse + uint8_t total_created_; // Total events created (high water mark) }; -} // namespace esp32_ble } // namespace esphome #endif diff --git a/esphome/components/esp32_ble/queue.h b/esphome/core/lock_free_queue.h similarity index 58% rename from esphome/components/esp32_ble/queue.h rename to esphome/core/lock_free_queue.h index 75bf1eef25..ec26d268a0 100644 --- a/esphome/components/esp32_ble/queue.h +++ b/esphome/core/lock_free_queue.h @@ -4,23 +4,25 @@ #include #include +#include +#include /* - * BLE events come in from a separate Task (thread) in the ESP32 stack. Rather - * than using mutex-based locking, this lock-free queue allows the BLE - * task to enqueue events without blocking. The main loop() then processes - * these events at a safer time. + * Lock-free queue for single-producer single-consumer scenarios. + * This allows one thread to push items and another to pop them without + * blocking each other. * * This is a Single-Producer Single-Consumer (SPSC) lock-free ring buffer. - * The BLE task is the only producer, and the main loop() is the only consumer. + * Common use cases: + * - BLE events: BLE task produces, main loop consumes + * - MQTT messages: main task produces, MQTT thread consumes */ namespace esphome { -namespace esp32_ble { template class LockFreeQueue { public: - LockFreeQueue() : head_(0), tail_(0), dropped_count_(0) {} + LockFreeQueue() : head_(0), tail_(0), dropped_count_(0), task_to_notify_(nullptr) {} bool push(T *element) { if (element == nullptr) @@ -29,14 +31,37 @@ template class LockFreeQueue { uint8_t current_tail = tail_.load(std::memory_order_relaxed); uint8_t next_tail = (current_tail + 1) % SIZE; - if (next_tail == head_.load(std::memory_order_acquire)) { + // Read head before incrementing tail + uint8_t head_before = head_.load(std::memory_order_acquire); + + if (next_tail == head_before) { // Buffer full dropped_count_.fetch_add(1, std::memory_order_relaxed); return false; } + // Check if queue was empty before push + bool was_empty = (current_tail == head_before); + buffer_[current_tail] = element; tail_.store(next_tail, std::memory_order_release); + + // Notify optimization: only notify if we need to + if (task_to_notify_ != nullptr) { + if (was_empty) { + // Queue was empty - consumer might be going to sleep, must notify + xTaskNotifyGive(task_to_notify_); + } else { + // Queue wasn't empty - check if consumer has caught up to previous tail + uint8_t head_after = head_.load(std::memory_order_acquire); + if (head_after == current_tail) { + // Consumer just caught up to where tail was - might go to sleep, must notify + xTaskNotifyGive(task_to_notify_); + } + // Otherwise: consumer is still behind, no need to notify + } + } + return true; } @@ -69,6 +94,8 @@ template class LockFreeQueue { return next_tail == head_.load(std::memory_order_acquire); } + void set_task_to_notify(TaskHandle_t task) { task_to_notify_ = task; } + protected: T *buffer_[SIZE]; // Atomic: written by producer (push/increment), read+reset by consumer (get_and_reset) @@ -77,9 +104,10 @@ template class LockFreeQueue { std::atomic head_; // Atomic: written by producer (push), read by consumer (pop) to check if empty std::atomic tail_; + // Task handle for notification (optional) + TaskHandle_t task_to_notify_; }; -} // namespace esp32_ble } // namespace esphome #endif