diff --git a/esphome/components/esp32/preferences.cpp b/esphome/components/esp32/preferences.cpp index 08439746b6..24c719c91c 100644 --- a/esphome/components/esp32/preferences.cpp +++ b/esphome/components/esp32/preferences.cpp @@ -19,16 +19,7 @@ static constexpr size_t KEY_BUFFER_SIZE = 12; struct NVSData { uint32_t key; - std::unique_ptr data; - size_t len; - - void set_data(const uint8_t *src, size_t size) { - if (!this->data || this->len != size) { - this->data = std::make_unique(size); - this->len = size; - } - memcpy(this->data.get(), src, size); - } + SmallInlineBuffer<8> data; // Most prefs fit in 8 bytes (covers fan, cover, select, etc.) }; static std::vector s_pending_save; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) @@ -41,14 +32,14 @@ class ESP32PreferenceBackend : public ESPPreferenceBackend { // try find in pending saves and update that for (auto &obj : s_pending_save) { if (obj.key == this->key) { - obj.set_data(data, len); + obj.data.set(data, len); return true; } } NVSData save{}; save.key = this->key; - save.set_data(data, len); - s_pending_save.emplace_back(std::move(save)); + save.data.set(data, len); + s_pending_save.push_back(std::move(save)); ESP_LOGVV(TAG, "s_pending_save: key: %" PRIu32 ", len: %zu", this->key, len); return true; } @@ -56,11 +47,11 @@ class ESP32PreferenceBackend : public ESPPreferenceBackend { // try find in pending saves and load from that for (auto &obj : s_pending_save) { if (obj.key == this->key) { - if (obj.len != len) { + if (obj.data.size() != len) { // size mismatch return false; } - memcpy(data, obj.data.get(), len); + memcpy(data, obj.data.data(), len); return true; } } @@ -136,10 +127,10 @@ class ESP32Preferences : public ESPPreferences { snprintf(key_str, sizeof(key_str), "%" PRIu32, save.key); ESP_LOGVV(TAG, "Checking if NVS data %s has changed", key_str); if (this->is_changed_(this->nvs_handle, save, key_str)) { - esp_err_t err = nvs_set_blob(this->nvs_handle, key_str, save.data.get(), save.len); - ESP_LOGV(TAG, "sync: key: %s, len: %zu", key_str, save.len); + esp_err_t err = nvs_set_blob(this->nvs_handle, key_str, save.data.data(), save.data.size()); + ESP_LOGV(TAG, "sync: key: %s, len: %zu", key_str, save.data.size()); if (err != 0) { - ESP_LOGV(TAG, "nvs_set_blob('%s', len=%zu) failed: %s", key_str, save.len, esp_err_to_name(err)); + ESP_LOGV(TAG, "nvs_set_blob('%s', len=%zu) failed: %s", key_str, save.data.size(), esp_err_to_name(err)); failed++; last_err = err; last_key = save.key; @@ -147,7 +138,7 @@ class ESP32Preferences : public ESPPreferences { } written++; } else { - ESP_LOGV(TAG, "NVS data not changed skipping %" PRIu32 " len=%zu", save.key, save.len); + ESP_LOGV(TAG, "NVS data not changed skipping %" PRIu32 " len=%zu", save.key, save.data.size()); cached++; } s_pending_save.erase(s_pending_save.begin() + i); @@ -178,7 +169,7 @@ class ESP32Preferences : public ESPPreferences { return true; } // Check size first before allocating memory - if (actual_len != to_save.len) { + if (actual_len != to_save.data.size()) { return true; } auto stored_data = std::make_unique(actual_len); @@ -187,7 +178,7 @@ class ESP32Preferences : public ESPPreferences { ESP_LOGV(TAG, "nvs_get_blob('%s') failed: %s", key_str, esp_err_to_name(err)); return true; } - return memcmp(to_save.data.get(), stored_data.get(), to_save.len) != 0; + return memcmp(to_save.data.data(), stored_data.get(), to_save.data.size()) != 0; } bool reset() override { diff --git a/esphome/components/libretiny/preferences.cpp b/esphome/components/libretiny/preferences.cpp index 68bc279767..287c635256 100644 --- a/esphome/components/libretiny/preferences.cpp +++ b/esphome/components/libretiny/preferences.cpp @@ -18,16 +18,7 @@ static constexpr size_t KEY_BUFFER_SIZE = 12; struct NVSData { uint32_t key; - std::unique_ptr data; - size_t len; - - void set_data(const uint8_t *src, size_t size) { - if (!this->data || this->len != size) { - this->data = std::make_unique(size); - this->len = size; - } - memcpy(this->data.get(), src, size); - } + SmallInlineBuffer<8> data; // Most prefs fit in 8 bytes (covers fan, cover, select, etc.) }; static std::vector s_pending_save; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) @@ -42,14 +33,14 @@ class LibreTinyPreferenceBackend : public ESPPreferenceBackend { // try find in pending saves and update that for (auto &obj : s_pending_save) { if (obj.key == this->key) { - obj.set_data(data, len); + obj.data.set(data, len); return true; } } NVSData save{}; save.key = this->key; - save.set_data(data, len); - s_pending_save.emplace_back(std::move(save)); + save.data.set(data, len); + s_pending_save.push_back(std::move(save)); ESP_LOGVV(TAG, "s_pending_save: key: %" PRIu32 ", len: %zu", this->key, len); return true; } @@ -58,11 +49,11 @@ class LibreTinyPreferenceBackend : public ESPPreferenceBackend { // try find in pending saves and load from that for (auto &obj : s_pending_save) { if (obj.key == this->key) { - if (obj.len != len) { + if (obj.data.size() != len) { // size mismatch return false; } - memcpy(data, obj.data.get(), len); + memcpy(data, obj.data.data(), len); return true; } } @@ -126,11 +117,11 @@ class LibreTinyPreferences : public ESPPreferences { snprintf(key_str, sizeof(key_str), "%" PRIu32, save.key); ESP_LOGVV(TAG, "Checking if FDB data %s has changed", key_str); if (this->is_changed_(&this->db, save, key_str)) { - ESP_LOGV(TAG, "sync: key: %s, len: %zu", key_str, save.len); - fdb_blob_make(&this->blob, save.data.get(), save.len); + ESP_LOGV(TAG, "sync: key: %s, len: %zu", key_str, save.data.size()); + fdb_blob_make(&this->blob, save.data.data(), save.data.size()); fdb_err_t err = fdb_kv_set_blob(&this->db, key_str, &this->blob); if (err != FDB_NO_ERR) { - ESP_LOGV(TAG, "fdb_kv_set_blob('%s', len=%zu) failed: %d", key_str, save.len, err); + ESP_LOGV(TAG, "fdb_kv_set_blob('%s', len=%zu) failed: %d", key_str, save.data.size(), err); failed++; last_err = err; last_key = save.key; @@ -138,7 +129,7 @@ class LibreTinyPreferences : public ESPPreferences { } written++; } else { - ESP_LOGD(TAG, "FDB data not changed; skipping %" PRIu32 " len=%zu", save.key, save.len); + ESP_LOGD(TAG, "FDB data not changed; skipping %" PRIu32 " len=%zu", save.key, save.data.size()); cached++; } s_pending_save.erase(s_pending_save.begin() + i); @@ -162,7 +153,7 @@ class LibreTinyPreferences : public ESPPreferences { } // Check size first - if different, data has changed - if (kv.value_len != to_save.len) { + if (kv.value_len != to_save.data.size()) { return true; } @@ -176,7 +167,7 @@ class LibreTinyPreferences : public ESPPreferences { } // Compare the actual data - return memcmp(to_save.data.get(), stored_data.get(), kv.value_len) != 0; + return memcmp(to_save.data.data(), stored_data.get(), kv.value_len) != 0; } bool reset() override { diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index 2e9c0e6b13..bbda75ebde 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -128,6 +128,67 @@ template class ConstVector { size_t size_; }; +/// Small buffer optimization - stores data inline when small, heap-allocates for large data +/// This avoids heap fragmentation for common small allocations while supporting arbitrary sizes. +/// Memory management is encapsulated - callers just use set() and data(). +template class SmallInlineBuffer { + public: + SmallInlineBuffer() = default; + ~SmallInlineBuffer() { + if (!this->is_inline_()) + delete[] this->heap_; + } + + // Move constructor - memcpy is safe because union is zero-initialized + SmallInlineBuffer(SmallInlineBuffer &&other) noexcept : len_(other.len_) { + memcpy(this->inline_, other.inline_, InlineSize); + other.len_ = 0; // Mark as empty so other's destructor is no-op + } + + // Move assignment - memcpy is safe because union is zero-initialized + SmallInlineBuffer &operator=(SmallInlineBuffer &&other) noexcept { + if (this != &other) { + if (!this->is_inline_()) + delete[] this->heap_; + this->len_ = other.len_; + memcpy(this->inline_, other.inline_, InlineSize); + other.len_ = 0; + } + return *this; + } + + // Disable copy (would need deep copy of heap data) + SmallInlineBuffer(const SmallInlineBuffer &) = delete; + SmallInlineBuffer &operator=(const SmallInlineBuffer &) = delete; + + /// Set buffer contents, allocating heap if needed + void set(const uint8_t *src, size_t size) { + // Free existing heap allocation if switching from heap to inline or different heap size + if (!this->is_inline_() && (size <= InlineSize || size != this->len_)) { + delete[] this->heap_; + } + // Allocate new heap buffer if needed + if (size > InlineSize && (this->is_inline_() || size != this->len_)) { + this->heap_ = new uint8_t[size]; // NOLINT(cppcoreguidelines-owning-memory) + } + this->len_ = size; + memcpy(this->data(), src, size); + } + + uint8_t *data() { return this->is_inline_() ? this->inline_ : this->heap_; } + const uint8_t *data() const { return this->is_inline_() ? this->inline_ : this->heap_; } + size_t size() const { return this->len_; } + + protected: + bool is_inline_() const { return this->len_ <= InlineSize; } + + size_t len_{0}; + union { + uint8_t inline_[InlineSize]{}; // Zero-init for safe memcpy in move ops + uint8_t *heap_; + }; +}; + /// Minimal static vector - saves memory by avoiding std::vector overhead template class StaticVector { public: