diff --git a/esphome/components/esp32_ble/ble.cpp b/esphome/components/esp32_ble/ble.cpp index 8adef79d2f..e3c9785078 100644 --- a/esphome/components/esp32_ble/ble.cpp +++ b/esphome/components/esp32_ble/ble.cpp @@ -1,6 +1,8 @@ #ifdef USE_ESP32 #include "ble.h" +#include "ble_event_pool.h" +#include "queue_index.h" #include "esphome/core/application.h" #include "esphome/core/log.h" @@ -23,8 +25,7 @@ namespace esp32_ble { static const char *const TAG = "esp32_ble"; -static RAMAllocator EVENT_ALLOCATOR( // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) - RAMAllocator::ALLOW_FAILURE | RAMAllocator::ALLOC_INTERNAL); +// No longer need static allocator - using pre-allocated pool instead void ESP32BLE::setup() { global_ble = this; @@ -301,8 +302,16 @@ void ESP32BLE::loop() { break; } - BLEEvent *ble_event = this->ble_events_.pop(); - while (ble_event != nullptr) { + size_t event_idx = this->ble_events_.pop(); + while (event_idx != LockFreeIndexQueue::INVALID_INDEX) { + BLEEvent *ble_event = this->ble_event_pool_.get(event_idx); + if (ble_event == nullptr) { + // This should not happen - log error and continue + ESP_LOGE(TAG, "Invalid event index: %zu", event_idx); + event_idx = this->ble_events_.pop(); + continue; + } + switch (ble_event->type_) { case BLEEvent::GATTS: { esp_gatts_cb_event_t event = ble_event->event_.gatts.gatts_event; @@ -349,10 +358,9 @@ void ESP32BLE::loop() { default: break; } - // Destructor will clean up external allocations for GATTC/GATTS - ble_event->~BLEEvent(); - EVENT_ALLOCATOR.deallocate(ble_event, 1); - ble_event = this->ble_events_.pop(); + // Return the event to the pool + this->ble_event_pool_.deallocate(event_idx); + event_idx = this->ble_events_.pop(); } if (this->advertising_ != nullptr) { this->advertising_->loop(); @@ -363,6 +371,31 @@ void ESP32BLE::loop() { if (dropped > 0) { ESP_LOGW(TAG, "Dropped %zu BLE events due to buffer overflow", dropped); } + + // Log pool usage periodically (every ~10 seconds) + static uint32_t last_pool_log = 0; + uint32_t now = millis(); + if (now - last_pool_log > 10000) { + size_t created = this->ble_event_pool_.get_total_created(); + if (created > 0) { + ESP_LOGD(TAG, "BLE event pool: %zu events created (peak usage), %zu currently allocated", created, + this->ble_event_pool_.get_allocated_count()); + } + last_pool_log = now; + } +} + +// Helper function to load new event data based on type +void load_ble_event(BLEEvent *event, esp_gap_ble_cb_event_t e, esp_ble_gap_cb_param_t *p) { + event->load_gap_event(e, p); +} + +void load_ble_event(BLEEvent *event, esp_gattc_cb_event_t e, esp_gatt_if_t i, esp_ble_gattc_cb_param_t *p) { + event->load_gattc_event(e, i, p); +} + +void load_ble_event(BLEEvent *event, esp_gatts_cb_event_t e, esp_gatt_if_t i, esp_ble_gatts_cb_param_t *p) { + event->load_gatts_event(e, i, p); } template void enqueue_ble_event(Args... args) { @@ -373,23 +406,35 @@ template void enqueue_ble_event(Args... args) { return; } - BLEEvent *new_event = EVENT_ALLOCATOR.allocate(1); - if (new_event == nullptr) { - // Memory too fragmented to allocate new event. Can only drop it until memory comes back + // Allocate an event from the pool + size_t event_idx = global_ble->ble_event_pool_.allocate(); + if (event_idx == BLEEventPool::INVALID_INDEX) { + // Pool is full, drop the event global_ble->ble_events_.increment_dropped_count(); return; } - new (new_event) BLEEvent(args...); - // Push the event - since we're the only producer and we checked full() above, - // this should always succeed unless we have a bug - if (!global_ble->ble_events_.push(new_event)) { + // Get the event object + BLEEvent *event = global_ble->ble_event_pool_.get(event_idx); + if (event == nullptr) { + // This should not happen + ESP_LOGE(TAG, "Failed to get event from pool at index %zu", event_idx); + global_ble->ble_event_pool_.deallocate(event_idx); + global_ble->ble_events_.increment_dropped_count(); + return; + } + + // Load new event data (replaces previous event) + load_ble_event(event, args...); + + // Push the event index to the queue + if (!global_ble->ble_events_.push(event_idx)) { // This should not happen in SPSC queue with single producer ESP_LOGE(TAG, "BLE queue push failed unexpectedly"); - new_event->~BLEEvent(); - EVENT_ALLOCATOR.deallocate(new_event, 1); + // Return to pool + global_ble->ble_event_pool_.deallocate(event_idx); } -} // NOLINT(clang-analyzer-unix.Malloc) +} // Explicit template instantiations for the friend function template void enqueue_ble_event(esp_gap_ble_cb_event_t, esp_ble_gap_cb_param_t *); diff --git a/esphome/components/esp32_ble/ble.h b/esphome/components/esp32_ble/ble.h index 58c064a2ef..36ca6073b7 100644 --- a/esphome/components/esp32_ble/ble.h +++ b/esphome/components/esp32_ble/ble.h @@ -12,7 +12,9 @@ #include "esphome/core/helpers.h" #include "ble_event.h" +#include "ble_event_pool.h" #include "queue.h" +#include "queue_index.h" #ifdef USE_ESP32 @@ -147,7 +149,8 @@ class ESP32BLE : public Component { std::vector ble_status_event_handlers_; BLEComponentState state_{BLE_COMPONENT_STATE_OFF}; - LockFreeQueue ble_events_; + LockFreeIndexQueue ble_events_; + BLEEventPool 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 f51095effd..f929c4662a 100644 --- a/esphome/components/esp32_ble/ble_event.h +++ b/esphome/components/esp32_ble/ble_event.h @@ -63,123 +63,66 @@ class BLEEvent { // Constructor for GAP events - no external allocations needed BLEEvent(esp_gap_ble_cb_event_t e, esp_ble_gap_cb_param_t *p) { this->type_ = GAP; - this->event_.gap.gap_event = e; - - if (p == nullptr) { - return; // Invalid event, but we can't log in header file - } - - // Only copy the data we actually use for each GAP event type - switch (e) { - case ESP_GAP_BLE_SCAN_RESULT_EVT: - // Copy only the fields we use from scan results - memcpy(this->event_.gap.scan_result.bda, p->scan_rst.bda, sizeof(esp_bd_addr_t)); - this->event_.gap.scan_result.ble_addr_type = p->scan_rst.ble_addr_type; - this->event_.gap.scan_result.rssi = p->scan_rst.rssi; - this->event_.gap.scan_result.adv_data_len = p->scan_rst.adv_data_len; - this->event_.gap.scan_result.scan_rsp_len = p->scan_rst.scan_rsp_len; - this->event_.gap.scan_result.search_evt = p->scan_rst.search_evt; - memcpy(this->event_.gap.scan_result.ble_adv, p->scan_rst.ble_adv, - ESP_BLE_ADV_DATA_LEN_MAX + ESP_BLE_SCAN_RSP_DATA_LEN_MAX); - break; - - case ESP_GAP_BLE_SCAN_PARAM_SET_COMPLETE_EVT: - this->event_.gap.scan_complete.status = p->scan_param_cmpl.status; - break; - - case ESP_GAP_BLE_SCAN_START_COMPLETE_EVT: - this->event_.gap.scan_complete.status = p->scan_start_cmpl.status; - break; - - case ESP_GAP_BLE_SCAN_STOP_COMPLETE_EVT: - this->event_.gap.scan_complete.status = p->scan_stop_cmpl.status; - break; - - default: - // We only handle 4 GAP event types, others are dropped - break; - } + this->init_gap_data(e, p); } // Constructor for GATTC events - uses heap allocation // Creates a copy of the param struct since the original is only valid during the callback BLEEvent(esp_gattc_cb_event_t e, esp_gatt_if_t i, esp_ble_gattc_cb_param_t *p) { this->type_ = GATTC; - this->event_.gattc.gattc_event = e; - this->event_.gattc.gattc_if = i; - - if (p == nullptr) { - this->event_.gattc.gattc_param = nullptr; - this->event_.gattc.data = nullptr; - return; // Invalid event, but we can't log in header file - } - - // Heap-allocate param and data - // Heap allocation is used because GATTC/GATTS events are rare (<1% of events) - // while GAP events (99%) are stored inline to minimize memory usage - this->event_.gattc.gattc_param = new esp_ble_gattc_cb_param_t(*p); - - // Copy data for events that need it - switch (e) { - case ESP_GATTC_NOTIFY_EVT: - this->event_.gattc.data = new std::vector(p->notify.value, p->notify.value + p->notify.value_len); - this->event_.gattc.gattc_param->notify.value = this->event_.gattc.data->data(); - break; - case ESP_GATTC_READ_CHAR_EVT: - case ESP_GATTC_READ_DESCR_EVT: - this->event_.gattc.data = new std::vector(p->read.value, p->read.value + p->read.value_len); - this->event_.gattc.gattc_param->read.value = this->event_.gattc.data->data(); - break; - default: - this->event_.gattc.data = nullptr; - break; - } + this->init_gattc_data(e, i, p); } // Constructor for GATTS events - uses heap allocation // Creates a copy of the param struct since the original is only valid during the callback BLEEvent(esp_gatts_cb_event_t e, esp_gatt_if_t i, esp_ble_gatts_cb_param_t *p) { this->type_ = GATTS; - this->event_.gatts.gatts_event = e; - this->event_.gatts.gatts_if = i; - - if (p == nullptr) { - this->event_.gatts.gatts_param = nullptr; - this->event_.gatts.data = nullptr; - return; // Invalid event, but we can't log in header file - } - - // Heap-allocate param and data - // Heap allocation is used because GATTC/GATTS events are rare (<1% of events) - // while GAP events (99%) are stored inline to minimize memory usage - this->event_.gatts.gatts_param = new esp_ble_gatts_cb_param_t(*p); - - // Copy data for events that need it - switch (e) { - case ESP_GATTS_WRITE_EVT: - this->event_.gatts.data = new std::vector(p->write.value, p->write.value + p->write.len); - this->event_.gatts.gatts_param->write.value = this->event_.gatts.data->data(); - break; - default: - this->event_.gatts.data = nullptr; - break; - } + this->init_gatts_data(e, i, p); } // Destructor to clean up heap allocations - ~BLEEvent() { - switch (this->type_) { - case GATTC: - delete this->event_.gattc.gattc_param; - delete this->event_.gattc.data; - break; - case GATTS: - delete this->event_.gatts.gatts_param; - delete this->event_.gatts.data; - break; - default: - break; + ~BLEEvent() { this->cleanup_heap_data(); } + + // Default constructor for pre-allocation in pool + BLEEvent() : type_(GAP) {} + + // Clean up any heap-allocated data + void cleanup_heap_data() { + if (this->type_ == GAP) { + return; } + if (this->type_ == GATTC) { + delete this->event_.gattc.gattc_param; + delete this->event_.gattc.data; + this->event_.gattc.gattc_param = nullptr; + this->event_.gattc.data = nullptr; + return; + } + if (this->type_ == GATTS) { + delete this->event_.gatts.gatts_param; + delete this->event_.gatts.data; + this->event_.gatts.gatts_param = nullptr; + this->event_.gatts.data = nullptr; + } + } + + // Load new event data for reuse (replaces previous event data) + void load_gap_event(esp_gap_ble_cb_event_t e, esp_ble_gap_cb_param_t *p) { + this->cleanup_heap_data(); + this->type_ = GAP; + this->init_gap_data(e, p); + } + + void load_gattc_event(esp_gattc_cb_event_t e, esp_gatt_if_t i, esp_ble_gattc_cb_param_t *p) { + this->cleanup_heap_data(); + this->type_ = GATTC; + this->init_gattc_data(e, i, p); + } + + void load_gatts_event(esp_gatts_cb_event_t e, esp_gatt_if_t i, esp_ble_gatts_cb_param_t *p) { + this->cleanup_heap_data(); + this->type_ = GATTS; + this->init_gatts_data(e, i, p); } // Disable copy to prevent double-delete @@ -224,6 +167,102 @@ class BLEEvent { esp_gap_ble_cb_event_t gap_event_type() const { return event_.gap.gap_event; } const BLEScanResult &scan_result() const { return event_.gap.scan_result; } esp_bt_status_t scan_complete_status() const { return event_.gap.scan_complete.status; } + + private: + // Initialize GAP event data + void init_gap_data(esp_gap_ble_cb_event_t e, esp_ble_gap_cb_param_t *p) { + this->event_.gap.gap_event = e; + + if (p == nullptr) { + return; + } + + // Copy data based on event type + switch (e) { + case ESP_GAP_BLE_SCAN_RESULT_EVT: + memcpy(this->event_.gap.scan_result.bda, p->scan_rst.bda, sizeof(esp_bd_addr_t)); + this->event_.gap.scan_result.ble_addr_type = p->scan_rst.ble_addr_type; + this->event_.gap.scan_result.rssi = p->scan_rst.rssi; + this->event_.gap.scan_result.adv_data_len = p->scan_rst.adv_data_len; + this->event_.gap.scan_result.scan_rsp_len = p->scan_rst.scan_rsp_len; + this->event_.gap.scan_result.search_evt = p->scan_rst.search_evt; + memcpy(this->event_.gap.scan_result.ble_adv, p->scan_rst.ble_adv, + ESP_BLE_ADV_DATA_LEN_MAX + ESP_BLE_SCAN_RSP_DATA_LEN_MAX); + break; + + case ESP_GAP_BLE_SCAN_PARAM_SET_COMPLETE_EVT: + this->event_.gap.scan_complete.status = p->scan_param_cmpl.status; + break; + + case ESP_GAP_BLE_SCAN_START_COMPLETE_EVT: + this->event_.gap.scan_complete.status = p->scan_start_cmpl.status; + break; + + case ESP_GAP_BLE_SCAN_STOP_COMPLETE_EVT: + this->event_.gap.scan_complete.status = p->scan_stop_cmpl.status; + break; + + default: + break; + } + } + + // Initialize GATTC event data + void init_gattc_data(esp_gattc_cb_event_t e, esp_gatt_if_t i, esp_ble_gattc_cb_param_t *p) { + this->event_.gattc.gattc_event = e; + this->event_.gattc.gattc_if = i; + + if (p == nullptr) { + this->event_.gattc.gattc_param = nullptr; + this->event_.gattc.data = nullptr; + return; + } + + // Heap-allocate param + this->event_.gattc.gattc_param = new esp_ble_gattc_cb_param_t(*p); + + // Copy data for events that need it + switch (e) { + case ESP_GATTC_NOTIFY_EVT: + this->event_.gattc.data = new std::vector(p->notify.value, p->notify.value + p->notify.value_len); + this->event_.gattc.gattc_param->notify.value = this->event_.gattc.data->data(); + break; + case ESP_GATTC_READ_CHAR_EVT: + case ESP_GATTC_READ_DESCR_EVT: + this->event_.gattc.data = new std::vector(p->read.value, p->read.value + p->read.value_len); + this->event_.gattc.gattc_param->read.value = this->event_.gattc.data->data(); + break; + default: + this->event_.gattc.data = nullptr; + break; + } + } + + // Initialize GATTS event data + void init_gatts_data(esp_gatts_cb_event_t e, esp_gatt_if_t i, esp_ble_gatts_cb_param_t *p) { + this->event_.gatts.gatts_event = e; + this->event_.gatts.gatts_if = i; + + if (p == nullptr) { + this->event_.gatts.gatts_param = nullptr; + this->event_.gatts.data = nullptr; + return; + } + + // Heap-allocate param + this->event_.gatts.gatts_param = new esp_ble_gatts_cb_param_t(*p); + + // Copy data for events that need it + switch (e) { + case ESP_GATTS_WRITE_EVT: + this->event_.gatts.data = new std::vector(p->write.value, p->write.value + p->write.len); + this->event_.gatts.gatts_param->write.value = this->event_.gatts.data->data(); + break; + default: + this->event_.gatts.data = nullptr; + break; + } + } }; // BLEEvent total size: 84 bytes (80 byte union + 1 byte type + 3 bytes padding) diff --git a/esphome/components/esp32_ble/ble_event_pool.h b/esphome/components/esp32_ble/ble_event_pool.h new file mode 100644 index 0000000000..f89a579efa --- /dev/null +++ b/esphome/components/esp32_ble/ble_event_pool.h @@ -0,0 +1,133 @@ +#pragma once + +#ifdef USE_ESP32 + +#include +#include +#include "ble_event.h" +#include "esphome/core/helpers.h" + +namespace esphome { +namespace esp32_ble { + +// BLE Event Pool - Pre-allocated pool of BLEEvent objects to avoid heap fragmentation +// This is a lock-free pool that allows the BLE task to allocate events without malloc +template class BLEEventPool { + public: + BLEEventPool() { + // Initialize all slots as unallocated + for (size_t i = 0; i < SIZE; i++) { + this->events_[i] = nullptr; + } + + // Initialize the free list - all indices are initially free + for (size_t i = 0; i < SIZE - 1; i++) { + this->next_free_[i] = i + 1; + } + this->next_free_[SIZE - 1] = INVALID_INDEX; + + this->free_head_.store(0, std::memory_order_relaxed); + this->allocated_count_.store(0, std::memory_order_relaxed); + this->total_created_.store(0, std::memory_order_relaxed); + } + + ~BLEEventPool() { + // Delete any events that were created + for (size_t i = 0; i < SIZE; i++) { + if (this->events_[i] != nullptr) { + delete this->events_[i]; + } + } + } + + // Allocate an event slot and return its index + // Returns INVALID_INDEX if pool is full + size_t allocate() { + while (true) { + size_t head = this->free_head_.load(std::memory_order_acquire); + + if (head == INVALID_INDEX) { + // Pool is full + return INVALID_INDEX; + } + + size_t next = this->next_free_[head]; + + // Try to update the free list head + if (this->free_head_.compare_exchange_weak(head, next, std::memory_order_release, std::memory_order_acquire)) { + this->allocated_count_.fetch_add(1, std::memory_order_relaxed); + return head; + } + // CAS failed, retry + } + } + + // Deallocate an event slot by index + void deallocate(size_t index) { + if (index >= SIZE) { + return; // Invalid index + } + + // No destructor call - events are reused + // The event's reset methods handle cleanup when switching types + + while (true) { + size_t head = this->free_head_.load(std::memory_order_acquire); + this->next_free_[index] = head; + + // Try to add this index back to the free list + if (this->free_head_.compare_exchange_weak(head, index, std::memory_order_release, std::memory_order_acquire)) { + this->allocated_count_.fetch_sub(1, std::memory_order_relaxed); + return; + } + // CAS failed, retry + } + } + + // Get event by index, creating it if needed + BLEEvent *get(size_t index) { + if (index >= SIZE) { + return nullptr; + } + + // Create event on first access (warm-up) + if (this->events_[index] == nullptr) { + // Use internal RAM for better performance + RAMAllocator allocator(RAMAllocator::ALLOC_INTERNAL); + BLEEvent *event = allocator.allocate(1); + + if (event == nullptr) { + // Fall back to regular allocation + event = new BLEEvent(); + } else { + // Placement new to construct the object + new (event) BLEEvent(); + } + + this->events_[index] = event; + this->total_created_.fetch_add(1, std::memory_order_relaxed); + } + + return this->events_[index]; + } + + // Get number of allocated events + size_t get_allocated_count() const { return this->allocated_count_.load(std::memory_order_relaxed); } + + // Get total number of events created (high water mark) + size_t get_total_created() const { return this->total_created_.load(std::memory_order_relaxed); } + + static constexpr size_t INVALID_INDEX = SIZE_MAX; + + private: + BLEEvent *events_[SIZE]; // Array of pointers, allocated on demand + size_t next_free_[SIZE]; // Next free index for each slot + std::atomic free_head_; // Head of the free list + std::atomic allocated_count_; // Number of currently allocated events + std::atomic total_created_; // Total events created (high water mark) +}; + +} // namespace esp32_ble +} // namespace esphome + +#endif \ No newline at end of file diff --git a/esphome/components/esp32_ble/queue_index.h b/esphome/components/esp32_ble/queue_index.h new file mode 100644 index 0000000000..3010310e5a --- /dev/null +++ b/esphome/components/esp32_ble/queue_index.h @@ -0,0 +1,81 @@ +#pragma once + +#ifdef USE_ESP32 + +#include +#include + +namespace esphome { +namespace esp32_ble { + +// Lock-free SPSC queue that stores indices instead of pointers +// This allows us to use a pre-allocated pool of objects +template class LockFreeIndexQueue { + public: + static constexpr size_t INVALID_INDEX = SIZE_MAX; + + LockFreeIndexQueue() : head_(0), tail_(0), dropped_count_(0) { + // Initialize all slots to invalid + for (size_t i = 0; i < SIZE; i++) { + buffer_[i] = INVALID_INDEX; + } + } + + bool push(size_t index) { + if (index == INVALID_INDEX) + return false; + + size_t current_tail = tail_.load(std::memory_order_relaxed); + size_t next_tail = (current_tail + 1) % SIZE; + + if (next_tail == head_.load(std::memory_order_acquire)) { + // Buffer full + dropped_count_.fetch_add(1, std::memory_order_relaxed); + return false; + } + + buffer_[current_tail] = index; + tail_.store(next_tail, std::memory_order_release); + return true; + } + + size_t pop() { + size_t current_head = head_.load(std::memory_order_relaxed); + + if (current_head == tail_.load(std::memory_order_acquire)) { + return INVALID_INDEX; // Empty + } + + size_t index = buffer_[current_head]; + head_.store((current_head + 1) % SIZE, std::memory_order_release); + return index; + } + + size_t size() const { + size_t tail = tail_.load(std::memory_order_acquire); + size_t head = head_.load(std::memory_order_acquire); + return (tail - head + SIZE) % SIZE; + } + + size_t get_and_reset_dropped_count() { return dropped_count_.exchange(0, std::memory_order_relaxed); } + + void increment_dropped_count() { dropped_count_.fetch_add(1, std::memory_order_relaxed); } + + bool empty() const { return head_.load(std::memory_order_acquire) == tail_.load(std::memory_order_acquire); } + + bool full() const { + size_t next_tail = (tail_.load(std::memory_order_relaxed) + 1) % SIZE; + return next_tail == head_.load(std::memory_order_acquire); + } + + protected: + size_t buffer_[SIZE]; + std::atomic head_; + std::atomic tail_; + std::atomic dropped_count_; +}; + +} // namespace esp32_ble +} // namespace esphome + +#endif \ No newline at end of file