Split LockFreeQueue into base and notifying variants to reduce memory usage (#9330)

This commit is contained in:
J. Nick Koston 2025-07-06 21:50:14 -05:00 committed by GitHub
parent 1368139f4d
commit 492580edc3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 65 additions and 36 deletions

View File

@ -51,7 +51,7 @@ enum IoCapability {
IO_CAP_KBDISP = ESP_IO_CAP_KBDISP, IO_CAP_KBDISP = ESP_IO_CAP_KBDISP,
}; };
enum BLEComponentState { enum BLEComponentState : uint8_t {
/** Nothing has been initialized yet. */ /** Nothing has been initialized yet. */
BLE_COMPONENT_STATE_OFF = 0, BLE_COMPONENT_STATE_OFF = 0,
/** BLE should be disabled on next loop. */ /** BLE should be disabled on next loop. */
@ -141,21 +141,31 @@ class ESP32BLE : public Component {
private: private:
template<typename... Args> friend void enqueue_ble_event(Args... args); template<typename... Args> friend void enqueue_ble_event(Args... args);
// Vectors (12 bytes each on 32-bit, naturally aligned to 4 bytes)
std::vector<GAPEventHandler *> gap_event_handlers_; std::vector<GAPEventHandler *> gap_event_handlers_;
std::vector<GAPScanEventHandler *> gap_scan_event_handlers_; std::vector<GAPScanEventHandler *> gap_scan_event_handlers_;
std::vector<GATTcEventHandler *> gattc_event_handlers_; std::vector<GATTcEventHandler *> gattc_event_handlers_;
std::vector<GATTsEventHandler *> gatts_event_handlers_; std::vector<GATTsEventHandler *> gatts_event_handlers_;
std::vector<BLEStatusEventHandler *> ble_status_event_handlers_; std::vector<BLEStatusEventHandler *> ble_status_event_handlers_;
BLEComponentState state_{BLE_COMPONENT_STATE_OFF};
// Large objects (size depends on template parameters, but typically aligned to 4 bytes)
esphome::LockFreeQueue<BLEEvent, MAX_BLE_QUEUE_SIZE> ble_events_; esphome::LockFreeQueue<BLEEvent, MAX_BLE_QUEUE_SIZE> ble_events_;
esphome::EventPool<BLEEvent, MAX_BLE_QUEUE_SIZE> ble_event_pool_; esphome::EventPool<BLEEvent, MAX_BLE_QUEUE_SIZE> ble_event_pool_;
BLEAdvertising *advertising_{};
esp_ble_io_cap_t io_cap_{ESP_IO_CAP_NONE}; // optional<string> (typically 16+ bytes on 32-bit, aligned to 4 bytes)
uint32_t advertising_cycle_time_{};
bool enable_on_boot_{};
optional<std::string> name_; optional<std::string> name_;
uint16_t appearance_{0};
// 4-byte aligned members
BLEAdvertising *advertising_{}; // 4 bytes (pointer)
esp_ble_io_cap_t io_cap_{ESP_IO_CAP_NONE}; // 4 bytes (enum)
uint32_t advertising_cycle_time_{}; // 4 bytes
// 2-byte aligned members
uint16_t appearance_{0}; // 2 bytes
// 1-byte aligned members (grouped together to minimize padding)
BLEComponentState state_{BLE_COMPONENT_STATE_OFF}; // 1 byte (uint8_t enum)
bool enable_on_boot_{}; // 1 byte
}; };
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)

View File

@ -252,7 +252,7 @@ class MQTTBackendESP32 final : public MQTTBackend {
#if defined(USE_MQTT_IDF_ENQUEUE) #if defined(USE_MQTT_IDF_ENQUEUE)
static void esphome_mqtt_task(void *params); static void esphome_mqtt_task(void *params);
EventPool<struct QueueElement, MQTT_QUEUE_LENGTH> mqtt_event_pool_; EventPool<struct QueueElement, MQTT_QUEUE_LENGTH> mqtt_event_pool_;
LockFreeQueue<struct QueueElement, MQTT_QUEUE_LENGTH> mqtt_queue_; NotifyingLockFreeQueue<struct QueueElement, MQTT_QUEUE_LENGTH> mqtt_queue_;
TaskHandle_t task_handle_{nullptr}; TaskHandle_t task_handle_{nullptr};
bool enqueue_(MqttQueueTypeT type, const char *topic, int qos = 0, bool retain = false, const char *payload = NULL, bool enqueue_(MqttQueueTypeT type, const char *topic, int qos = 0, bool retain = false, const char *payload = NULL,
size_t len = 0); size_t len = 0);

View File

@ -31,11 +31,20 @@
namespace esphome { namespace esphome {
// Base lock-free queue without task notification
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), task_to_notify_(nullptr) {} LockFreeQueue() : head_(0), tail_(0), dropped_count_(0) {}
bool push(T *element) { bool push(T *element) {
bool was_empty;
uint8_t old_tail;
return push_internal_(element, was_empty, old_tail);
}
protected:
// Internal push that reports queue state - for use by derived classes
bool push_internal_(T *element, bool &was_empty, uint8_t &old_tail) {
if (element == nullptr) if (element == nullptr)
return false; return false;
@ -51,34 +60,16 @@ template<class T, uint8_t SIZE> class LockFreeQueue {
return false; return false;
} }
// Check if queue was empty before push was_empty = (current_tail == head_before);
bool was_empty = (current_tail == head_before); old_tail = current_tail;
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
// Note: There's a benign race here - between reading head_after and calling
// xTaskNotifyGive(), the consumer could advance further. This would result
// in an unnecessary wake-up, but is harmless and extremely rare in practice.
xTaskNotifyGive(task_to_notify_);
}
// Otherwise: consumer is still behind, no need to notify
}
}
return true; return true;
} }
public:
T *pop() { T *pop() {
uint8_t current_head = head_.load(std::memory_order_relaxed); uint8_t current_head = head_.load(std::memory_order_relaxed);
@ -108,11 +99,6 @@ 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);
} }
// Set the FreeRTOS task handle to notify when items are pushed to the queue
// This enables efficient wake-up of a consumer task that's waiting for data
// @param task The FreeRTOS task handle to notify, or nullptr to disable notifications
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)
@ -123,7 +109,40 @@ 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) };
// Extended queue with task notification support
template<class T, uint8_t SIZE> class NotifyingLockFreeQueue : public LockFreeQueue<T, SIZE> {
public:
NotifyingLockFreeQueue() : LockFreeQueue<T, SIZE>(), task_to_notify_(nullptr) {}
bool push(T *element) {
bool was_empty;
uint8_t old_tail;
bool result = this->push_internal_(element, was_empty, old_tail);
// Notify optimization: only notify if we need to
if (result && task_to_notify_ != nullptr &&
(was_empty || this->head_.load(std::memory_order_acquire) == old_tail)) {
// Notify in two cases:
// 1. Queue was empty - consumer might be going to sleep
// 2. Consumer just caught up to where tail was - might go to sleep
// Note: There's a benign race in case 2 - between reading head and calling
// xTaskNotifyGive(), the consumer could advance further. This would result
// in an unnecessary wake-up, but is harmless and extremely rare in practice.
xTaskNotifyGive(task_to_notify_);
}
// Otherwise: consumer is still behind, no need to notify
return result;
}
// Set the FreeRTOS task handle to notify when items are pushed to the queue
// This enables efficient wake-up of a consumer task that's waiting for data
// @param task The FreeRTOS task handle to notify, or nullptr to disable notifications
void set_task_to_notify(TaskHandle_t task) { task_to_notify_ = task; }
private:
TaskHandle_t task_to_notify_; TaskHandle_t task_to_notify_;
}; };