From 347501d895cb42d1a15633d4b2fd684ae6a2af6a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 12 Oct 2025 19:39:55 -1000 Subject: [PATCH 01/10] wifi fixed vector --- esphome/components/wifi/wifi_component.cpp | 2 +- esphome/components/wifi/wifi_component.h | 4 +- .../wifi/wifi_component_esp8266.cpp | 8 ++++ .../wifi/wifi_component_esp_idf.cpp | 5 +- .../wifi/wifi_component_libretiny.cpp | 2 +- esphome/core/helpers.h | 46 +++++++++++++++---- 6 files changed, 51 insertions(+), 16 deletions(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 2e083d4c68..1bb2674ad7 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -552,7 +552,7 @@ void WiFiComponent::start_scanning() { // Using insertion sort instead of std::stable_sort saves flash memory // by avoiding template instantiations (std::rotate, std::stable_sort, lambdas) // IMPORTANT: This sort is stable (preserves relative order of equal elements) -static void insertion_sort_scan_results(std::vector &results) { +static void insertion_sort_scan_results(FixedVector &results) { const size_t size = results.size(); for (size_t i = 1; i < size; i++) { // Make a copy to avoid issues with move semantics during comparison diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index ee62ec1a69..e1c3d6df88 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -278,7 +278,7 @@ class WiFiComponent : public Component { std::string get_use_address() const; void set_use_address(const std::string &use_address); - const std::vector &get_scan_result() const { return scan_result_; } + const FixedVector &get_scan_result() const { return scan_result_; } network::IPAddress wifi_soft_ap_ip(); @@ -385,7 +385,7 @@ class WiFiComponent : public Component { std::string use_address_; std::vector sta_; std::vector sta_priorities_; - std::vector scan_result_; + FixedVector scan_result_; WiFiAP selected_ap_; WiFiAP ap_; optional output_power_; diff --git a/esphome/components/wifi/wifi_component_esp8266.cpp b/esphome/components/wifi/wifi_component_esp8266.cpp index 3b3b4b139c..59909b2cb5 100644 --- a/esphome/components/wifi/wifi_component_esp8266.cpp +++ b/esphome/components/wifi/wifi_component_esp8266.cpp @@ -696,7 +696,15 @@ void WiFiComponent::wifi_scan_done_callback_(void *arg, STATUS status) { this->retry_connect(); return; } + + // Count the number of results first auto *head = reinterpret_cast(arg); + size_t count = 0; + for (bss_info *it = head; it != nullptr; it = STAILQ_NEXT(it, next)) { + count++; + } + + this->scan_result_.init(count); for (bss_info *it = head; it != nullptr; it = STAILQ_NEXT(it, next)) { WiFiScanResult res({it->bssid[0], it->bssid[1], it->bssid[2], it->bssid[3], it->bssid[4], it->bssid[5]}, std::string(reinterpret_cast(it->ssid), it->ssid_len), it->channel, it->rssi, diff --git a/esphome/components/wifi/wifi_component_esp_idf.cpp b/esphome/components/wifi/wifi_component_esp_idf.cpp index ccec800205..4c719ef4c3 100644 --- a/esphome/components/wifi/wifi_component_esp_idf.cpp +++ b/esphome/components/wifi/wifi_component_esp_idf.cpp @@ -763,8 +763,9 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) { const auto &it = data->data.sta_scan_done; ESP_LOGV(TAG, "Scan done: status=%" PRIu32 " number=%u scan_id=%u", it.status, it.number, it.scan_id); - scan_result_.clear(); this->scan_done_ = true; + scan_result_.clear(); + if (it.status != 0) { // scan error return; @@ -784,7 +785,7 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) { } records.resize(number); - scan_result_.reserve(number); + scan_result_.init(number); for (int i = 0; i < number; i++) { auto &record = records[i]; bssid_t bssid; diff --git a/esphome/components/wifi/wifi_component_libretiny.cpp b/esphome/components/wifi/wifi_component_libretiny.cpp index b15f710150..cb179d9022 100644 --- a/esphome/components/wifi/wifi_component_libretiny.cpp +++ b/esphome/components/wifi/wifi_component_libretiny.cpp @@ -411,7 +411,7 @@ void WiFiComponent::wifi_scan_done_callback_() { if (num < 0) return; - this->scan_result_.reserve(static_cast(num)); + this->scan_result_.init(static_cast(num)); for (int i = 0; i < num; i++) { String ssid = WiFi.SSID(i); wifi_auth_mode_t authmode = WiFi.encryptionType(i); diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index b5a0a1c8ac..12e921e3be 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -168,34 +168,54 @@ template class FixedVector { size_t size_{0}; size_t capacity_{0}; + // Helper to destroy elements and free memory + void cleanup_() { + if (data_ != nullptr) { + // Manually destroy all elements + for (size_t i = 0; i < size_; i++) { + data_[i].~T(); + } + // Free raw memory + ::operator delete(data_); + } + } + public: FixedVector() = default; - ~FixedVector() { - if (data_ != nullptr) { - delete[] data_; - } - } + ~FixedVector() { cleanup_(); } // Disable copy to avoid accidental copies FixedVector(const FixedVector &) = delete; FixedVector &operator=(const FixedVector &) = delete; - // Allocate capacity - can only be called once on empty vector + // Allocate capacity - can be called multiple times to reinit void init(size_t n) { - if (data_ == nullptr && n > 0) { - data_ = new T[n]; + cleanup_(); + data_ = nullptr; + capacity_ = 0; + size_ = 0; + if (n > 0) { + // Allocate raw memory without calling constructors + data_ = static_cast(::operator new(n * sizeof(T))); capacity_ = n; - size_ = 0; } } + // Clear the vector (reset size to 0, keep capacity) + void clear() { size_ = 0; } + + // Check if vector is empty + bool empty() const { return size_ == 0; } + /// Add element without bounds checking /// Caller must ensure sufficient capacity was allocated via init() /// Silently ignores pushes beyond capacity (no exception or assertion) void push_back(const T &value) { if (size_ < capacity_) { - data_[size_++] = value; + // Use placement new to construct the object in pre-allocated memory + new (&data_[size_]) T(value); + size_++; } } @@ -205,6 +225,12 @@ template class FixedVector { /// Caller must ensure index is valid (i < size()) T &operator[](size_t i) { return data_[i]; } const T &operator[](size_t i) const { return data_[i]; } + + // Iterator support for range-based for loops + T *begin() { return data_; } + T *end() { return data_ + size_; } + const T *begin() const { return data_; } + const T *end() const { return data_ + size_; } }; ///@} From fbef9b126403ca49999b589ba5670fbe980b7c9e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 13 Oct 2025 14:03:59 -1000 Subject: [PATCH 02/10] revert --- .../components/bluetooth_proxy/bluetooth_connection.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/esphome/components/bluetooth_proxy/bluetooth_connection.cpp b/esphome/components/bluetooth_proxy/bluetooth_connection.cpp index 6f172b0bcf..cde82fbfb0 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_connection.cpp +++ b/esphome/components/bluetooth_proxy/bluetooth_connection.cpp @@ -230,8 +230,8 @@ void BluetoothConnection::send_service_for_discovery_() { service_resp.handle = service_result.start_handle; if (total_char_count > 0) { - // Initialize FixedVector with exact count and process characteristics - service_resp.characteristics.init(total_char_count); + // Reserve space and process characteristics + service_resp.characteristics.reserve(total_char_count); uint16_t char_offset = 0; esp_gattc_char_elem_t char_result; while (true) { // characteristics @@ -275,8 +275,8 @@ void BluetoothConnection::send_service_for_discovery_() { continue; } - // Initialize FixedVector with exact count and process descriptors - characteristic_resp.descriptors.init(total_desc_count); + // Reserve space and process descriptors + characteristic_resp.descriptors.reserve(total_desc_count); uint16_t desc_offset = 0; esp_gattc_descr_elem_t desc_result; while (true) { // descriptors From ddf6e0a7b61ac09db7bfdafc17362cf4804b189f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 13 Oct 2025 14:04:15 -1000 Subject: [PATCH 03/10] revert --- esphome/components/api/api.proto | 4 ++-- esphome/components/api/api_options.proto | 6 ------ esphome/components/api/api_pb2.h | 4 ++-- esphome/components/api/proto.h | 26 +++++------------------- 4 files changed, 9 insertions(+), 31 deletions(-) diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index 9b714d00f1..87f477799d 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -1519,7 +1519,7 @@ message BluetoothGATTCharacteristic { repeated uint64 uuid = 1 [(fixed_array_size) = 2, (fixed_array_skip_zero) = true]; uint32 handle = 2; uint32 properties = 3; - repeated BluetoothGATTDescriptor descriptors = 4 [(fixed_vector) = true]; + repeated BluetoothGATTDescriptor descriptors = 4; // New field for efficient UUID (v1.12+) // Only one of uuid or short_uuid will be set. @@ -1531,7 +1531,7 @@ message BluetoothGATTCharacteristic { message BluetoothGATTService { repeated uint64 uuid = 1 [(fixed_array_size) = 2, (fixed_array_skip_zero) = true]; uint32 handle = 2; - repeated BluetoothGATTCharacteristic characteristics = 3 [(fixed_vector) = true]; + repeated BluetoothGATTCharacteristic characteristics = 3; // New field for efficient UUID (v1.12+) // Only one of uuid or short_uuid will be set. diff --git a/esphome/components/api/api_options.proto b/esphome/components/api/api_options.proto index ead8ac0bbc..633f39b552 100644 --- a/esphome/components/api/api_options.proto +++ b/esphome/components/api/api_options.proto @@ -64,10 +64,4 @@ extend google.protobuf.FieldOptions { // This is typically done through methods returning const T& or special accessor // methods like get_options() or supported_modes_for_api_(). optional string container_pointer = 50001; - - // fixed_vector: Use FixedVector instead of std::vector for repeated fields - // When set, the repeated field will use FixedVector which requires calling - // init(size) before adding elements. This eliminates std::vector template overhead - // and is ideal when the exact size is known before populating the array. - optional bool fixed_vector = 50013 [default=false]; } diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index 1798458393..d9e68ece9b 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -1923,7 +1923,7 @@ class BluetoothGATTCharacteristic final : public ProtoMessage { std::array uuid{}; uint32_t handle{0}; uint32_t properties{0}; - FixedVector descriptors{}; + std::vector descriptors{}; uint32_t short_uuid{0}; void encode(ProtoWriteBuffer buffer) const override; void calculate_size(ProtoSize &size) const override; @@ -1937,7 +1937,7 @@ class BluetoothGATTService final : public ProtoMessage { public: std::array uuid{}; uint32_t handle{0}; - FixedVector characteristics{}; + std::vector characteristics{}; uint32_t short_uuid{0}; void encode(ProtoWriteBuffer buffer) const override; void calculate_size(ProtoSize &size) const override; diff --git a/esphome/components/api/proto.h b/esphome/components/api/proto.h index a6a09bf7c5..9d780692ec 100644 --- a/esphome/components/api/proto.h +++ b/esphome/components/api/proto.h @@ -749,29 +749,13 @@ class ProtoSize { template inline void add_repeated_message(uint32_t field_id_size, const std::vector &messages) { // Skip if the vector is empty - if (!messages.empty()) { - // Use the force version for all messages in the repeated field - for (const auto &message : messages) { - add_message_object_force(field_id_size, message); - } + if (messages.empty()) { + return; } - } - /** - * @brief Calculates and adds the sizes of all messages in a repeated field to the total message size (FixedVector - * version) - * - * @tparam MessageType The type of the nested messages in the FixedVector - * @param messages FixedVector of message objects - */ - template - inline void add_repeated_message(uint32_t field_id_size, const FixedVector &messages) { - // Skip if the fixed vector is empty - if (!messages.empty()) { - // Use the force version for all messages in the repeated field - for (const auto &message : messages) { - add_message_object_force(field_id_size, message); - } + // Use the force version for all messages in the repeated field + for (const auto &message : messages) { + add_message_object_force(field_id_size, message); } } }; From d5234e335709e63eb022240c98c9c885994b247f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 13 Oct 2025 14:04:39 -1000 Subject: [PATCH 04/10] merge --- esphome/components/wifi/wifi_component_esp_idf.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/wifi/wifi_component_esp_idf.cpp b/esphome/components/wifi/wifi_component_esp_idf.cpp index 4c719ef4c3..e45b873e8d 100644 --- a/esphome/components/wifi/wifi_component_esp_idf.cpp +++ b/esphome/components/wifi/wifi_component_esp_idf.cpp @@ -763,8 +763,8 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) { const auto &it = data->data.sta_scan_done; ESP_LOGV(TAG, "Scan done: status=%" PRIu32 " number=%u scan_id=%u", it.status, it.number, it.scan_id); - this->scan_done_ = true; scan_result_.clear(); + this->scan_done_ = true; if (it.status != 0) { // scan error From ce46f1630859861534f91ed15e03a3ae5c335855 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 13 Oct 2025 14:05:19 -1000 Subject: [PATCH 05/10] merge --- esphome/components/wifi/wifi_component_esp_idf.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/esphome/components/wifi/wifi_component_esp_idf.cpp b/esphome/components/wifi/wifi_component_esp_idf.cpp index e45b873e8d..951f5803a6 100644 --- a/esphome/components/wifi/wifi_component_esp_idf.cpp +++ b/esphome/components/wifi/wifi_component_esp_idf.cpp @@ -765,7 +765,6 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) { scan_result_.clear(); this->scan_done_ = true; - if (it.status != 0) { // scan error return; From 7792a115c26d93b7d18cb112008457a30dfad2ea Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 13 Oct 2025 14:05:31 -1000 Subject: [PATCH 06/10] merge --- script/api_protobuf/api_protobuf.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/script/api_protobuf/api_protobuf.py b/script/api_protobuf/api_protobuf.py index 9a55f1d136..487c187372 100755 --- a/script/api_protobuf/api_protobuf.py +++ b/script/api_protobuf/api_protobuf.py @@ -1415,8 +1415,6 @@ class RepeatedTypeInfo(TypeInfo): # Check if this is a pointer field by looking for container_pointer option self._container_type = get_field_opt(field, pb.container_pointer, "") self._use_pointer = bool(self._container_type) - # Check if this should use FixedVector instead of std::vector - self._use_fixed_vector = get_field_opt(field, pb.fixed_vector, False) # For repeated fields, we need to get the base type info # but we can't call create_field_type_info as it would cause recursion @@ -1440,8 +1438,6 @@ class RepeatedTypeInfo(TypeInfo): if "<" in self._container_type and ">" in self._container_type: return f"const {self._container_type}*" return f"const {self._container_type}<{self._ti.cpp_type}>*" - if self._use_fixed_vector: - return f"FixedVector<{self._ti.cpp_type}>" return f"std::vector<{self._ti.cpp_type}>" @property From bb2f568f3d4971b536a6c5a59bb25bb59fc45365 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 13 Oct 2025 14:07:52 -1000 Subject: [PATCH 07/10] merge --- esphome/core/helpers.h | 3 --- 1 file changed, 3 deletions(-) diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index 084ad28882..6b04af3c7c 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -226,9 +226,6 @@ template class FixedVector { // Clear the vector (reset size to 0, keep capacity) void clear() { size_ = 0; } - // Check if vector is empty - bool empty() const { return size_ == 0; } - /// Add element without bounds checking /// Caller must ensure sufficient capacity was allocated via init() /// Silently ignores pushes beyond capacity (no exception or assertion) From b878aa0270c2faaedee25342d1a15b69c43dbad6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 13 Oct 2025 14:09:44 -1000 Subject: [PATCH 08/10] fix --- esphome/core/helpers.h | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index 6b04af3c7c..a3c0447a8d 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -226,6 +226,14 @@ template class FixedVector { // Clear the vector (reset size to 0, keep capacity) void clear() { size_ = 0; } + // Shrink capacity to fit current size (frees all memory) + void shrink_to_fit() { + cleanup_(); + data_ = nullptr; + capacity_ = 0; + size_ = 0; + } + /// Add element without bounds checking /// Caller must ensure sufficient capacity was allocated via init() /// Silently ignores pushes beyond capacity (no exception or assertion) From de10d781259fb73b605fed83d5c14812997234b8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 13 Oct 2025 14:10:41 -1000 Subject: [PATCH 09/10] dry --- esphome/core/helpers.h | 27 ++++++++++++--------------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index a3c0447a8d..e838c82f3e 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -180,6 +180,13 @@ template class FixedVector { } } + // Helper to reset pointers after cleanup + void reset_() { + data_ = nullptr; + capacity_ = 0; + size_ = 0; + } + public: FixedVector() = default; @@ -187,25 +194,19 @@ template class FixedVector { // Enable move semantics for use in containers FixedVector(FixedVector &&other) noexcept : data_(other.data_), size_(other.size_), capacity_(other.capacity_) { - other.data_ = nullptr; - other.size_ = 0; - other.capacity_ = 0; + other.reset_(); } FixedVector &operator=(FixedVector &&other) noexcept { if (this != &other) { // Delete our current data - if (data_ != nullptr) { - delete[] data_; - } + cleanup_(); // Take ownership of other's data data_ = other.data_; size_ = other.size_; capacity_ = other.capacity_; // Leave other in valid empty state - other.data_ = nullptr; - other.size_ = 0; - other.capacity_ = 0; + other.reset_(); } return *this; } @@ -213,9 +214,7 @@ template class FixedVector { // Allocate capacity - can be called multiple times to reinit void init(size_t n) { cleanup_(); - data_ = nullptr; - capacity_ = 0; - size_ = 0; + reset_(); if (n > 0) { // Allocate raw memory without calling constructors data_ = static_cast(::operator new(n * sizeof(T))); @@ -229,9 +228,7 @@ template class FixedVector { // Shrink capacity to fit current size (frees all memory) void shrink_to_fit() { cleanup_(); - data_ = nullptr; - capacity_ = 0; - size_ = 0; + reset_(); } /// Add element without bounds checking From 7b5a86e4df1a90f2521b283d7ec8a17c5c5e2359 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 13 Oct 2025 14:15:37 -1000 Subject: [PATCH 10/10] fixes --- esphome/components/wifi/wifi_component.cpp | 2 +- esphome/components/wifi/wifi_component.h | 12 ++++++++++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 3cdc034be6..8308421220 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -554,7 +554,7 @@ void WiFiComponent::start_scanning() { // Using insertion sort instead of std::stable_sort saves flash memory // by avoiding template instantiations (std::rotate, std::stable_sort, lambdas) // IMPORTANT: This sort is stable (preserves relative order of equal elements) -static void insertion_sort_scan_results(FixedVector &results) { +template static void insertion_sort_scan_results(VectorType &results) { const size_t size = results.size(); for (size_t i = 1; i < size; i++) { // Make a copy to avoid issues with move semantics during comparison diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index e1c3d6df88..bbb4649027 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -121,6 +121,14 @@ struct EAPAuth { using bssid_t = std::array; +// Use std::vector for RP2040 since scan count is unknown (callback-based) +// Use FixedVector for other platforms where count is queried first +#ifdef USE_RP2040 +template using wifi_scan_vector_t = std::vector; +#else +template using wifi_scan_vector_t = FixedVector; +#endif + class WiFiAP { public: void set_ssid(const std::string &ssid); @@ -278,7 +286,7 @@ class WiFiComponent : public Component { std::string get_use_address() const; void set_use_address(const std::string &use_address); - const FixedVector &get_scan_result() const { return scan_result_; } + const wifi_scan_vector_t &get_scan_result() const { return scan_result_; } network::IPAddress wifi_soft_ap_ip(); @@ -385,7 +393,7 @@ class WiFiComponent : public Component { std::string use_address_; std::vector sta_; std::vector sta_priorities_; - FixedVector scan_result_; + wifi_scan_vector_t scan_result_; WiFiAP selected_ap_; WiFiAP ap_; optional output_power_;