From f98e28a8a2e58f3bed1c074ff8751d52971821d7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 5 Jul 2025 08:57:04 -0500 Subject: [PATCH 1/4] Split LockFreeQueue into base and notifying variants to reduce memory usage --- esphome/components/esp32_ble/ble.h | 24 +++++--- esphome/components/mqtt/mqtt_backend_esp32.h | 2 +- esphome/core/lock_free_queue.h | 64 +++++++++++--------- 3 files changed, 54 insertions(+), 36 deletions(-) diff --git a/esphome/components/esp32_ble/ble.h b/esphome/components/esp32_ble/ble.h index ce452d65c4..81582eb09a 100644 --- a/esphome/components/esp32_ble/ble.h +++ b/esphome/components/esp32_ble/ble.h @@ -51,7 +51,7 @@ enum IoCapability { IO_CAP_KBDISP = ESP_IO_CAP_KBDISP, }; -enum BLEComponentState { +enum BLEComponentState : uint8_t { /** Nothing has been initialized yet. */ BLE_COMPONENT_STATE_OFF = 0, /** BLE should be disabled on next loop. */ @@ -141,21 +141,31 @@ class ESP32BLE : public Component { private: template friend void enqueue_ble_event(Args... args); + // Vectors (12 bytes each on 32-bit, naturally aligned to 4 bytes) std::vector gap_event_handlers_; std::vector gap_scan_event_handlers_; std::vector gattc_event_handlers_; std::vector gatts_event_handlers_; std::vector 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 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_{}; - bool enable_on_boot_{}; + + // optional (typically 16+ bytes on 32-bit, aligned to 4 bytes) optional 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) diff --git a/esphome/components/mqtt/mqtt_backend_esp32.h b/esphome/components/mqtt/mqtt_backend_esp32.h index 3611caf554..a24e75eaf9 100644 --- a/esphome/components/mqtt/mqtt_backend_esp32.h +++ b/esphome/components/mqtt/mqtt_backend_esp32.h @@ -252,7 +252,7 @@ class MQTTBackendESP32 final : public MQTTBackend { #if defined(USE_MQTT_IDF_ENQUEUE) static void esphome_mqtt_task(void *params); EventPool mqtt_event_pool_; - LockFreeQueue mqtt_queue_; + NotifyingLockFreeQueue mqtt_queue_; TaskHandle_t task_handle_{nullptr}; bool enqueue_(MqttQueueTypeT type, const char *topic, int qos = 0, bool retain = false, const char *payload = NULL, size_t len = 0); diff --git a/esphome/core/lock_free_queue.h b/esphome/core/lock_free_queue.h index 5460be0fae..7e29080f8e 100644 --- a/esphome/core/lock_free_queue.h +++ b/esphome/core/lock_free_queue.h @@ -31,11 +31,19 @@ namespace esphome { +// Base lock-free queue without task notification template class LockFreeQueue { 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 was_empty; + return push_internal_(element, was_empty); + } + + protected: + // Internal push that reports if queue was empty - for use by derived classes + bool push_internal_(T *element, bool &was_empty) { if (element == nullptr) return false; @@ -51,34 +59,15 @@ template class LockFreeQueue { return false; } - // Check if queue was empty before push - bool was_empty = (current_tail == head_before); + 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 - // 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; } + public: T *pop() { uint8_t current_head = head_.load(std::memory_order_relaxed); @@ -108,11 +97,6 @@ template class LockFreeQueue { 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: T *buffer_[SIZE]; // Atomic: written by producer (push/increment), read+reset by consumer (get_and_reset) @@ -123,7 +107,31 @@ 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) +}; + +// Extended queue with task notification support +template class NotifyingLockFreeQueue : public LockFreeQueue { + public: + NotifyingLockFreeQueue() : LockFreeQueue(), task_to_notify_(nullptr) {} + + bool push(T *element) { + bool was_empty; + bool result = this->push_internal_(element, was_empty); + + // Notify if push succeeded and queue was empty + if (result && task_to_notify_ != nullptr && was_empty) { + xTaskNotifyGive(task_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_; }; From e173b7f0c2806d47561887e7d695bda5ef09489e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 5 Jul 2025 08:58:41 -0500 Subject: [PATCH 2/4] Split LockFreeQueue into base and notifying variants to reduce memory usage --- esphome/core/lock_free_queue.h | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/esphome/core/lock_free_queue.h b/esphome/core/lock_free_queue.h index 7e29080f8e..e7c9ddb11f 100644 --- a/esphome/core/lock_free_queue.h +++ b/esphome/core/lock_free_queue.h @@ -118,9 +118,26 @@ template class NotifyingLockFreeQueue : public LockFreeQu bool was_empty; bool result = this->push_internal_(element, was_empty); - // Notify if push succeeded and queue was empty - if (result && task_to_notify_ != nullptr && was_empty) { - xTaskNotifyGive(task_to_notify_); + // Notify optimization: only notify if we need to + if (result && 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 current_tail = this->tail_.load(std::memory_order_relaxed); + uint8_t head_after = this->head_.load(std::memory_order_acquire); + // We just pushed, so go back one position to get the old tail + uint8_t previous_tail = (current_tail + SIZE - 1) % SIZE; + if (head_after == previous_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 result; From dfcc3206f724f872c6d0c6b504a5f4dd676e2763 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 5 Jul 2025 08:59:19 -0500 Subject: [PATCH 3/4] Split LockFreeQueue into base and notifying variants to reduce memory usage --- esphome/core/lock_free_queue.h | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/esphome/core/lock_free_queue.h b/esphome/core/lock_free_queue.h index e7c9ddb11f..80f9024910 100644 --- a/esphome/core/lock_free_queue.h +++ b/esphome/core/lock_free_queue.h @@ -38,12 +38,13 @@ template class LockFreeQueue { bool push(T *element) { bool was_empty; - return push_internal_(element, was_empty); + uint8_t old_tail; + return push_internal_(element, was_empty, old_tail); } protected: - // Internal push that reports if queue was empty - for use by derived classes - bool push_internal_(T *element, bool &was_empty) { + // 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) return false; @@ -60,6 +61,7 @@ template class LockFreeQueue { } was_empty = (current_tail == head_before); + old_tail = current_tail; buffer_[current_tail] = element; tail_.store(next_tail, std::memory_order_release); @@ -116,7 +118,8 @@ template class NotifyingLockFreeQueue : public LockFreeQu bool push(T *element) { bool was_empty; - bool result = this->push_internal_(element, 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) { @@ -125,11 +128,8 @@ template class NotifyingLockFreeQueue : public LockFreeQu xTaskNotifyGive(task_to_notify_); } else { // Queue wasn't empty - check if consumer has caught up to previous tail - uint8_t current_tail = this->tail_.load(std::memory_order_relaxed); uint8_t head_after = this->head_.load(std::memory_order_acquire); - // We just pushed, so go back one position to get the old tail - uint8_t previous_tail = (current_tail + SIZE - 1) % SIZE; - if (head_after == previous_tail) { + if (head_after == old_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 From 62088dfaedf8e7dca20e0d569181916dc843160f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 5 Jul 2025 09:02:33 -0500 Subject: [PATCH 4/4] Split LockFreeQueue into base and notifying variants to reduce memory usage --- esphome/core/lock_free_queue.h | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/esphome/core/lock_free_queue.h b/esphome/core/lock_free_queue.h index 80f9024910..df38ad9148 100644 --- a/esphome/core/lock_free_queue.h +++ b/esphome/core/lock_free_queue.h @@ -126,18 +126,14 @@ template class NotifyingLockFreeQueue : public LockFreeQu 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 = this->head_.load(std::memory_order_acquire); - if (head_after == old_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 + } else if (this->head_.load(std::memory_order_acquire) == old_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 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;