mirror of
https://github.com/esphome/esphome.git
synced 2025-07-29 14:46:40 +00:00
Split LockFreeQueue into base and notifying variants to reduce memory usage (#9330)
This commit is contained in:
parent
1368139f4d
commit
492580edc3
@ -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)
|
||||||
|
@ -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);
|
||||||
|
@ -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_;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user