mirror of
https://github.com/esphome/esphome.git
synced 2025-08-10 12:27:46 +00:00
Extract lock-free queue and event pool to core helpers
This commit is contained in:
@@ -1,7 +1,6 @@
|
|||||||
#ifdef USE_ESP32
|
#ifdef USE_ESP32
|
||||||
|
|
||||||
#include "ble.h"
|
#include "ble.h"
|
||||||
#include "ble_event_pool.h"
|
|
||||||
|
|
||||||
#include "esphome/core/application.h"
|
#include "esphome/core/application.h"
|
||||||
#include "esphome/core/helpers.h"
|
#include "esphome/core/helpers.h"
|
||||||
|
@@ -12,8 +12,8 @@
|
|||||||
#include "esphome/core/helpers.h"
|
#include "esphome/core/helpers.h"
|
||||||
|
|
||||||
#include "ble_event.h"
|
#include "ble_event.h"
|
||||||
#include "ble_event_pool.h"
|
#include "esphome/core/lock_free_queue.h"
|
||||||
#include "queue.h"
|
#include "esphome/core/event_pool.h"
|
||||||
|
|
||||||
#ifdef USE_ESP32
|
#ifdef USE_ESP32
|
||||||
|
|
||||||
@@ -148,8 +148,8 @@ class ESP32BLE : public Component {
|
|||||||
std::vector<BLEStatusEventHandler *> ble_status_event_handlers_;
|
std::vector<BLEStatusEventHandler *> ble_status_event_handlers_;
|
||||||
BLEComponentState state_{BLE_COMPONENT_STATE_OFF};
|
BLEComponentState state_{BLE_COMPONENT_STATE_OFF};
|
||||||
|
|
||||||
LockFreeQueue<BLEEvent, MAX_BLE_QUEUE_SIZE> ble_events_;
|
esphome::LockFreeQueue<BLEEvent, MAX_BLE_QUEUE_SIZE> ble_events_;
|
||||||
BLEEventPool<MAX_BLE_QUEUE_SIZE> ble_event_pool_;
|
esphome::EventPool<BLEEvent, MAX_BLE_QUEUE_SIZE> ble_event_pool_;
|
||||||
BLEAdvertising *advertising_{};
|
BLEAdvertising *advertising_{};
|
||||||
esp_ble_io_cap_t io_cap_{ESP_IO_CAP_NONE};
|
esp_ble_io_cap_t io_cap_{ESP_IO_CAP_NONE};
|
||||||
uint32_t advertising_cycle_time_{};
|
uint32_t advertising_cycle_time_{};
|
||||||
|
@@ -139,6 +139,9 @@ class BLEEvent {
|
|||||||
// Default constructor for pre-allocation in pool
|
// Default constructor for pre-allocation in pool
|
||||||
BLEEvent() : type_(GAP) {}
|
BLEEvent() : type_(GAP) {}
|
||||||
|
|
||||||
|
// Invoked on return to EventPool
|
||||||
|
void clear() { this->cleanup_heap_data(); }
|
||||||
|
|
||||||
// Clean up any heap-allocated data
|
// Clean up any heap-allocated data
|
||||||
void cleanup_heap_data() {
|
void cleanup_heap_data() {
|
||||||
if (this->type_ == GAP) {
|
if (this->type_ == GAP) {
|
||||||
|
@@ -4,22 +4,20 @@
|
|||||||
|
|
||||||
#include <atomic>
|
#include <atomic>
|
||||||
#include <cstddef>
|
#include <cstddef>
|
||||||
#include "ble_event.h"
|
|
||||||
#include "queue.h"
|
|
||||||
#include "esphome/core/helpers.h"
|
#include "esphome/core/helpers.h"
|
||||||
|
#include "esphome/core/lock_free_queue.h"
|
||||||
|
|
||||||
namespace esphome {
|
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
|
// Events are allocated on first use and reused thereafter, growing to peak usage
|
||||||
template<uint8_t SIZE> class BLEEventPool {
|
template<class T, uint8_t SIZE> class EventPool {
|
||||||
public:
|
public:
|
||||||
BLEEventPool() : total_created_(0) {}
|
EventPool() : total_created_(0) {}
|
||||||
|
|
||||||
~BLEEventPool() {
|
~EventPool() {
|
||||||
// Clean up any remaining events in the free list
|
// Clean up any remaining events in the free list
|
||||||
BLEEvent *event;
|
T *event;
|
||||||
while ((event = this->free_list_.pop()) != nullptr) {
|
while ((event = this->free_list_.pop()) != nullptr) {
|
||||||
delete event;
|
delete event;
|
||||||
}
|
}
|
||||||
@@ -27,9 +25,9 @@ template<uint8_t SIZE> class BLEEventPool {
|
|||||||
|
|
||||||
// Allocate an event from the pool
|
// Allocate an event from the pool
|
||||||
// Returns nullptr if pool is full
|
// Returns nullptr if pool is full
|
||||||
BLEEvent *allocate() {
|
T *allocate() {
|
||||||
// Try to get from free list first
|
// Try to get from free list first
|
||||||
BLEEvent *event = this->free_list_.pop();
|
T *event = this->free_list_.pop();
|
||||||
if (event != nullptr)
|
if (event != nullptr)
|
||||||
return event;
|
return event;
|
||||||
|
|
||||||
@@ -40,7 +38,7 @@ template<uint8_t SIZE> class BLEEventPool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Use internal RAM for better performance
|
// Use internal RAM for better performance
|
||||||
RAMAllocator<BLEEvent> allocator(RAMAllocator<BLEEvent>::ALLOC_INTERNAL);
|
RAMAllocator<T> allocator(RAMAllocator<T>::ALLOC_INTERNAL);
|
||||||
event = allocator.allocate(1);
|
event = allocator.allocate(1);
|
||||||
|
|
||||||
if (event == nullptr) {
|
if (event == nullptr) {
|
||||||
@@ -49,24 +47,25 @@ template<uint8_t SIZE> class BLEEventPool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Placement new to construct the object
|
// Placement new to construct the object
|
||||||
new (event) BLEEvent();
|
new (event) T();
|
||||||
this->total_created_++;
|
this->total_created_++;
|
||||||
return event;
|
return event;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return an event to the pool for reuse
|
// Return an event to the pool for reuse
|
||||||
void release(BLEEvent *event) {
|
void release(T *event) {
|
||||||
if (event != nullptr) {
|
if (event != nullptr) {
|
||||||
|
// Clean up the event's allocated memory
|
||||||
|
event->clear();
|
||||||
this->free_list_.push(event);
|
this->free_list_.push(event);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
LockFreeQueue<BLEEvent, SIZE> free_list_; // Free events ready for reuse
|
LockFreeQueue<T, SIZE> free_list_; // Free events ready for reuse
|
||||||
uint8_t total_created_; // Total events created (high water mark)
|
uint8_t total_created_; // Total events created (high water mark)
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace esp32_ble
|
|
||||||
} // namespace esphome
|
} // namespace esphome
|
||||||
|
|
||||||
#endif
|
#endif
|
@@ -4,23 +4,25 @@
|
|||||||
|
|
||||||
#include <atomic>
|
#include <atomic>
|
||||||
#include <cstddef>
|
#include <cstddef>
|
||||||
|
#include <freertos/FreeRTOS.h>
|
||||||
|
#include <freertos/task.h>
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* BLE events come in from a separate Task (thread) in the ESP32 stack. Rather
|
* Lock-free queue for single-producer single-consumer scenarios.
|
||||||
* than using mutex-based locking, this lock-free queue allows the BLE
|
* This allows one thread to push items and another to pop them without
|
||||||
* task to enqueue events without blocking. The main loop() then processes
|
* blocking each other.
|
||||||
* these events at a safer time.
|
|
||||||
*
|
*
|
||||||
* This is a Single-Producer Single-Consumer (SPSC) lock-free ring buffer.
|
* 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 esphome {
|
||||||
namespace esp32_ble {
|
|
||||||
|
|
||||||
template<class T, uint8_t SIZE> class LockFreeQueue {
|
template<class T, uint8_t SIZE> class LockFreeQueue {
|
||||||
public:
|
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) {
|
bool push(T *element) {
|
||||||
if (element == nullptr)
|
if (element == nullptr)
|
||||||
@@ -29,14 +31,37 @@ template<class T, uint8_t SIZE> class LockFreeQueue {
|
|||||||
uint8_t current_tail = tail_.load(std::memory_order_relaxed);
|
uint8_t current_tail = tail_.load(std::memory_order_relaxed);
|
||||||
uint8_t next_tail = (current_tail + 1) % SIZE;
|
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
|
// Buffer full
|
||||||
dropped_count_.fetch_add(1, std::memory_order_relaxed);
|
dropped_count_.fetch_add(1, std::memory_order_relaxed);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if queue was empty before push
|
||||||
|
bool was_empty = (current_tail == head_before);
|
||||||
|
|
||||||
buffer_[current_tail] = element;
|
buffer_[current_tail] = element;
|
||||||
tail_.store(next_tail, std::memory_order_release);
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,6 +94,8 @@ template<class T, uint8_t SIZE> class LockFreeQueue {
|
|||||||
return next_tail == head_.load(std::memory_order_acquire);
|
return next_tail == head_.load(std::memory_order_acquire);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void set_task_to_notify(TaskHandle_t task) { task_to_notify_ = task; }
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
T *buffer_[SIZE];
|
T *buffer_[SIZE];
|
||||||
// Atomic: written by producer (push/increment), read+reset by consumer (get_and_reset)
|
// Atomic: written by producer (push/increment), read+reset by consumer (get_and_reset)
|
||||||
@@ -77,9 +104,10 @@ template<class T, uint8_t SIZE> class LockFreeQueue {
|
|||||||
std::atomic<uint8_t> head_;
|
std::atomic<uint8_t> head_;
|
||||||
// Atomic: written by producer (push), read by consumer (pop) to check if empty
|
// Atomic: written by producer (push), read by consumer (pop) to check if empty
|
||||||
std::atomic<uint8_t> tail_;
|
std::atomic<uint8_t> tail_;
|
||||||
|
// Task handle for notification (optional)
|
||||||
|
TaskHandle_t task_to_notify_;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace esp32_ble
|
|
||||||
} // namespace esphome
|
} // namespace esphome
|
||||||
|
|
||||||
#endif
|
#endif
|
Reference in New Issue
Block a user