Compare commits

...

4 Commits

Author SHA1 Message Date
J. Nick Koston
42b9863cd3 bot concerns 2026-01-15 18:19:15 -10:00
J. Nick Koston
9bdefc98b1 bot concerns 2026-01-15 18:17:42 -10:00
J. Nick Koston
2db4e15452 Merge branch 'dev' into avoid_prefs_heap_churn 2026-01-15 18:09:37 -10:00
J. Nick Koston
7641c36c95 [preferences] Reduce heap churn with small inline buffer optimization 2026-01-15 18:03:44 -10:00
3 changed files with 96 additions and 42 deletions

View File

@@ -19,16 +19,7 @@ static constexpr size_t KEY_BUFFER_SIZE = 12;
struct NVSData {
uint32_t key;
std::unique_ptr<uint8_t[]> 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<uint8_t[]>(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<NVSData> 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<uint8_t[]>(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 {

View File

@@ -18,16 +18,7 @@ static constexpr size_t KEY_BUFFER_SIZE = 12;
struct NVSData {
uint32_t key;
std::unique_ptr<uint8_t[]> 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<uint8_t[]>(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<NVSData> 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 {

View File

@@ -132,6 +132,78 @@ template<typename T> 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<size_t InlineSize = 8> class SmallInlineBuffer {
public:
SmallInlineBuffer() = default;
~SmallInlineBuffer() {
if (!this->is_inline_())
delete[] this->heap_;
}
// Move constructor
SmallInlineBuffer(SmallInlineBuffer &&other) noexcept : len_(other.len_) {
if (other.is_inline_()) {
memcpy(this->inline_, other.inline_, this->len_);
} else {
this->heap_ = other.heap_;
other.heap_ = nullptr;
}
other.len_ = 0;
}
// Move assignment
SmallInlineBuffer &operator=(SmallInlineBuffer &&other) noexcept {
if (this != &other) {
if (!this->is_inline_())
delete[] this->heap_;
this->len_ = other.len_;
if (other.is_inline_()) {
memcpy(this->inline_, other.inline_, this->len_);
} else {
this->heap_ = other.heap_;
other.heap_ = nullptr;
}
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_;
this->heap_ = nullptr; // Defensive: prevent use-after-free if logic changes
}
// 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 ensures clean initial state
uint8_t *heap_;
};
};
/// Minimal static vector - saves memory by avoiding std::vector overhead
template<typename T, size_t N> class StaticVector {
public: