docs(nvs_flash): improved description of NVS space consumption

- Improved description of space required to store data types into NVS partition
  - Example showing nvs statistics extended with a code demonstrating the fragmentation effect
This commit is contained in:
radek.tandler
2026-08-03 11:39:47 +08:00
committed by Zhang Shuxian
parent 5371ee669b
commit 56a01ca822
7 changed files with 397 additions and 85 deletions
+83 -67
View File
@@ -18,6 +18,42 @@ Statistics obtained via [nvs_get_stats()](https://docs.espressif.com/projects/es
Detailed functional description of NVS and API is provided in [documentation](https://docs.espressif.com/projects/esp-idf/en/latest/api-reference/storage/nvs_flash.html).
## Blob Storage-Overhead Measurement
In addition to the basic statistics demonstration, the example can measure how much usable storage a blob actually consumes, taking the NVS metadata and free-space fragmentation into account. This part is enabled by default and can be turned off via `idf.py menuconfig` → *Example Configuration* → *Run NVS blob storage-overhead measurement*.
The measurement sweeps a matrix of **partition sizes** × **blob sizes** and, for each cell, fills the partition to capacity and reports the heap demand, NVS entry usage and the resulting storage overhead.
### Variable partition size
The example uses a custom partition table (`partitions.csv`) that defines several NVS partitions of different sizes (`nvs_16k`, `nvs_32k`, `nvs_64k`). The measurement iterates over them using `nvs_flash_init_partition()` / `nvs_get_stats()`, so the influence of the partition size on the relative overhead becomes directly visible.
### Variable blob size
For each partition, blobs of increasing size (128, 256, 512, 1024, 2048 and 4096 bytes) are stored with unique keys until `nvs_set_blob()` returns `ESP_ERR_NVS_NOT_ENOUGH_SPACE`. The number of stored blobs is then compared against the theoretical (ideal) count derived from the documented per-blob entry cost:
```
entries_per_blob = 1 (BLOB_INDEX) + k (per-page BLOB_DATA chunk headers) + ceil(blob_size / 32)
```
where `k` is the number of pages the blob data is split across.
### Worst-case fragmentation
By design, each NVS page is a 4096-byte flash sector holding 126 usable 32-byte entries. A string occupies `1 + ceil((len + 1) / 32)` entries and must fit contiguously within a single page, while a blob may split its data into per-page chunks.
To demonstrate the worst case, the example optionally pre-populates a partition so that **every page is filled up to its last 2 entries** (enabled via *Add a worst-case (pre-fragmented) measurement pass*). This is achieved by writing large strings:
* the first string is sized to leave 2 free entries on page 0 while accounting for the namespace entry,
* every following string fills a fresh page to 124 entries, leaving exactly 2 free.
After this step the partition reports a large amount of `free_entries`, but the largest contiguous run of free entries on any page is only 2. The consequences are then measured by filling the partition with blobs:
* each blob chunk can only use 1 chunk-header + 1 data entry per page, so roughly half of the consumed space becomes metadata overhead,
* because the number of chunks per blob is bounded, large blobs may become unstoreable even though many `free_entries` remain.
This makes the relationship between fragmentation, remaining free space and resulting overhead measurable, instead of presenting a single (best-case) number that could create false expectations.
## How to use example
### Hardware required
@@ -38,6 +74,8 @@ See the Getting Started Guide for full steps to configure and use ESP-IDF to bui
## Example Output
The first part of the output shows the basic statistics demonstration:
```
...
I (265) nvs_statistics_example: Erasing the contents of the default NVS partition...
@@ -49,71 +87,49 @@ I (485) nvs_statistics_example: Free NVS entries: 755
I (495) nvs_statistics_example: Available NVS entries: 629
I (495) nvs_statistics_example: Total NVS entries: 756
I (505) nvs_statistics_example: Namespace count: 1
I (505) nvs_statistics_example: Writing mock data key-value pairs to NVS namespace '_mock_data'...
I (525) nvs_statistics_example: Committing data in NVS namespace '_mock_data'...
I (525) nvs_statistics_example: Getting post-commit NVS statistics...
I (525) nvs_statistics_example: NVS statistics:
I (535) nvs_statistics_example: Used NVS entries: 30
I (535) nvs_statistics_example: Free NVS entries: 726
I (545) nvs_statistics_example: Available NVS entries: 600
I (545) nvs_statistics_example: Total NVS entries: 756
I (555) nvs_statistics_example: Namespace count: 1
I (555) nvs_statistics_example: Newly used entries match expectation.
I (565) nvs_statistics_example: Newly used entries: 29, expected: 29.
I (575) nvs_statistics_example: NVS handle for namespace '_mock_data' closed.
I (575) nvs_statistics_example: Opening Non-Volatile Storage (NVS) handle for namespace '_mock_data'...
I (585) nvs_statistics_example: Reading stored data from NVS namespace '_mock_data'...
I (595) nvs_statistics_example: Read key-value pair from NVS: 'wifi_ssid':'HomeNetwork'
I (605) nvs_statistics_example: Read key-value pair from NVS: 'wifi_pass':'MySecretPass'
I (605) nvs_statistics_example: Read key-value pair from NVS: 'dev_name':'LivingRoomThermostat'
I (615) nvs_statistics_example: Read key-value pair from NVS: 'temp_unit':'Celsius'
I (625) nvs_statistics_example: Read key-value pair from NVS: 'target_temp':'22'
I (635) nvs_statistics_example: Read key-value pair from NVS: 'eco_mode':'false'
I (635) nvs_statistics_example: Read key-value pair from NVS: 'fw_version':'1.2.3'
I (645) nvs_statistics_example: Read key-value pair from NVS: 'led_bright':'80'
I (655) nvs_statistics_example: Read key-value pair from NVS: 'auto_update':'true'
I (665) nvs_statistics_example: Read key-value pair from NVS: 'last_sync':'2025-01-01T08:00:00Z'
I (665) nvs_statistics_example: Read key-value pair from NVS: 'user_lang':'en'
I (675) nvs_statistics_example: Read key-value pair from NVS: 'long_token':'2f8c1e7b5a4d9c6e3b0f1a8e5d7c2b6f4e1a9c7b'
I (685) nvs_statistics_example: Read key-value pair from NVS: 'very_long_token':'7e2b1c9f5a4d8e3b0f1a6c7e2d9b5a4c8e1f7b2d6c3a9e5b0f1a8c7e2d9b5a4c8e1f7b2d6c3a9e5b'
I (705) nvs_statistics_example: NVS handle for namespace '_mock_data' closed.
I (705) nvs_statistics_example: Opening Non-Volatile Storage (NVS) handle for namespace '_mock_backup'...
I (715) nvs_statistics_example: Getting NVS statistics...
I (725) nvs_statistics_example: NVS statistics:
I (725) nvs_statistics_example: Used NVS entries: 31
I (735) nvs_statistics_example: Free NVS entries: 725
I (735) nvs_statistics_example: Available NVS entries: 599
I (745) nvs_statistics_example: Total NVS entries: 756
I (745) nvs_statistics_example: Namespace count: 2
I (755) nvs_statistics_example: Writing mock data key-value pairs to NVS namespace '_mock_backup'...
I (765) nvs_statistics_example: Committing data in NVS namespace '_mock_backup'...
I (765) nvs_statistics_example: Getting post-commit NVS statistics...
I (775) nvs_statistics_example: NVS statistics:
I (775) nvs_statistics_example: Used NVS entries: 60
I (785) nvs_statistics_example: Free NVS entries: 696
I (785) nvs_statistics_example: Available NVS entries: 570
I (795) nvs_statistics_example: Total NVS entries: 756
I (795) nvs_statistics_example: Namespace count: 2
I (805) nvs_statistics_example: Newly used entries match expectation.
I (805) nvs_statistics_example: Newly used entries: 29, expected: 29.
I (815) nvs_statistics_example: NVS handle for namespace '_mock_backup' closed.
I (825) nvs_statistics_example: Opening Non-Volatile Storage (NVS) handle for namespace '_mock_backup'...
I (835) nvs_statistics_example: Reading stored data from NVS namespace '_mock_backup'...
I (835) nvs_statistics_example: Read key-value pair from NVS: 'wifi_ssid':'HomeNetwork'
I (845) nvs_statistics_example: Read key-value pair from NVS: 'wifi_pass':'MySecretPass'
I (855) nvs_statistics_example: Read key-value pair from NVS: 'dev_name':'LivingRoomThermostat'
I (865) nvs_statistics_example: Read key-value pair from NVS: 'temp_unit':'Celsius'
I (865) nvs_statistics_example: Read key-value pair from NVS: 'target_temp':'22'
I (875) nvs_statistics_example: Read key-value pair from NVS: 'eco_mode':'false'
I (885) nvs_statistics_example: Read key-value pair from NVS: 'fw_version':'1.2.3'
I (895) nvs_statistics_example: Read key-value pair from NVS: 'led_bright':'80'
I (895) nvs_statistics_example: Read key-value pair from NVS: 'auto_update':'true'
I (905) nvs_statistics_example: Read key-value pair from NVS: 'last_sync':'2025-01-01T08:00:00Z'
I (915) nvs_statistics_example: Read key-value pair from NVS: 'user_lang':'en'
I (925) nvs_statistics_example: Read key-value pair from NVS: 'long_token':'2f8c1e7b5a4d9c6e3b0f1a8e5d7c2b6f4e1a9c7b'
I (935) nvs_statistics_example: Read key-value pair from NVS: 'very_long_token':'7e2b1c9f5a4d8e3b0f1a6c7e2d9b5a4c8e1f7b2d6c3a9e5b0f1a8c7e2d9b5a4c8e1f7b2d6c3a9e5b'
I (945) nvs_statistics_example: NVS handle for namespace '_mock_backup' closed.
I (955) nvs_statistics_example: Returning from app_main().
I (955) main_task: Returned from app_main()
...
```
I (565) nvs_statistics_example: Newly used entries match expectation.
...
```
The second part reports the blob storage overhead for each partition / blob size, in both a pristine and a pre-fragmented partition:
```
...
I (1265) nvs_statistics_example: Starting NVS blob storage-overhead measurement...
NVS BLOB TEST - 128 B (partition 'nvs_16k', pristine):
======================
heap before NVS init: 299220 B
heap after NVS init: 255472 B (diff 43748 B)
available heap after fill: 254100 B (diff 1372 B)
expected blobs count: 21
stored blobs count: 20
used_entries: 120 (3840 B)
free_entries: 6 (192 B)
total_entries: 126 (4032 B)
STORAGE OVERHEAD: 36%
NVS BLOB TEST - 128 B (partition 'nvs_16k', fragmented):
======================
heap before NVS init: 299220 B
heap after NVS init: 255472 B (diff 43748 B)
fragmentation strings written: 3
available heap after fill: 254100 B (diff 1372 B)
expected blobs count: 21
stored blobs count: 2
used_entries: 124 (3968 B)
free_entries: 2 (64 B)
total_entries: 126 (4032 B)
STORAGE OVERHEAD: 98%
...
I (9000) nvs_statistics_example: NVS blob storage-overhead measurement done.
I (9010) nvs_statistics_example: Returning from app_main().
...
```
> The exact numbers depend on the target, partition size and blob size; the values above are illustrative. The key takeaway is the difference in `STORAGE OVERHEAD` and `stored blobs count` between the pristine and fragmented passes.
To reset NVS data, erase the contents of flash memory using `idf.py erase-flash`, then upload the program again as described above.
@@ -0,0 +1,21 @@
menu "Example Configuration"
config EXAMPLE_RUN_OVERHEAD_MEASUREMENT
bool "Run NVS blob storage-overhead measurement"
default y
help
When enabled, the example sweeps several NVS partition sizes and blob
sizes, fills each partition to capacity with blobs and reports the
resulting heap demand, entry usage and storage overhead.
config EXAMPLE_INDUCE_FRAGMENTATION
bool "Add a worst-case (pre-fragmented) measurement pass"
depends on EXAMPLE_RUN_OVERHEAD_MEASUREMENT
default y
help
When enabled, every measured partition is first pre-populated with
large strings that leave only two free entries per NVS page. This
scatters the free space into non-coalescable gaps and demonstrates
the worst-case blob storage overhead caused by fragmentation.
endmenu
@@ -1,5 +1,5 @@
/*
* SPDX-FileCopyrightText: 2025 Espressif Systems (Shanghai) CO LTD
* SPDX-FileCopyrightText: 2025-2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Unlicense OR CC0-1.0
*/
@@ -15,7 +15,10 @@
CONDITIONS OF ANY KIND, either express or implied.
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <inttypes.h>
#include "sdkconfig.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_check.h"
@@ -27,6 +30,26 @@
#define MOCK_DATA_NAMESPACE "_mock_data"
#define MOCK_DATA_BACKUP_NAMESPACE "_mock_backup"
/* NVS on-flash geometry. These are by-design constants of the NVS format (one
* 4096-byte flash sector per page, 32-byte entries, 126 usable entries per
* page). They are not exposed through the public API, so they are mirrored here
* to allow precise control over page-level fragmentation below. */
#define NVS_ENTRY_SIZE 32
#define NVS_ENTRIES_PER_PAGE 126
#define NVS_PAGE_CHUNK_MAX_SIZE (NVS_ENTRY_SIZE * (NVS_ENTRIES_PER_PAGE - 1)) // 4000 B
/* String lengths (strlen, excluding the NUL terminator) used to fragment a
* partition. A string of length L occupies 1 header entry + ceil((L+1)/32) data
* entries and must fit contiguously within a single page.
* - FRAG_STR_LEN fills a fresh page to 124 entries, leaving exactly 2 free.
* - FRAG_STR_FIRST_LEN is one entry shorter to account for the namespace entry
* that is written on the first page, so that page also keeps 2 free entries. */
#define FRAG_STR_LEN (123 * NVS_ENTRY_SIZE - 1) // 3935 -> 124 entries
#define FRAG_STR_FIRST_LEN (122 * NVS_ENTRY_SIZE - 1) // 3903 -> 123 entries
#define FRAG_NAMESPACE "_frag"
#define BLOB_NAMESPACE "_blobs"
static const char *TAG = "nvs_statistics_example";
// Maximum key character length is 15 (NVS_KEY_NAME_MAX_SIZE-1)
@@ -195,6 +218,200 @@ static esp_err_t read_mock_data_from_namespace(const char* namespace_name)
return ESP_OK;
}
#if CONFIG_EXAMPLE_RUN_OVERHEAD_MEASUREMENT
// Partitions of various sizes (declared in partitions.csv) swept by the measurement.
static const char* measured_partitions[] = {
"nvs_16k",
"nvs_32k",
"nvs_64k",
};
// Blob sizes (in bytes) measured for each partition.
static const size_t blob_sizes[] = {128, 256, 512, 1024, 2048, 4096};
// Theoretical entries consumed by a single blob in a non-fragmented partition:
// 1 BLOB_INDEX entry + 'chunks' chunk-header entries + ceil(size/32) data entries.
static size_t entries_per_blob_ideal(size_t blob_size)
{
size_t chunks = (blob_size + NVS_PAGE_CHUNK_MAX_SIZE - 1) / NVS_PAGE_CHUNK_MAX_SIZE;
if (chunks == 0) {
chunks = 1;
}
size_t data_entries = (blob_size + NVS_ENTRY_SIZE - 1) / NVS_ENTRY_SIZE;
return 1 + chunks + data_entries;
}
// Pre-populate a partition with large strings so that every page is filled up to
// its last 2 entries. Returns the number of strings written.
static size_t fragment_partition(const char* partition_name)
{
nvs_handle_t handle;
esp_err_t err = nvs_open_from_partition(partition_name, FRAG_NAMESPACE, NVS_READWRITE, &handle);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Error (%s) opening fragmentation handle on '%s'!", esp_err_to_name(err), partition_name);
return 0;
}
char* buffer = malloc(FRAG_STR_LEN + 1);
if (buffer == NULL) {
ESP_LOGE(TAG, "Failed to allocate fragmentation buffer!");
nvs_close(handle);
return 0;
}
memset(buffer, 'A', FRAG_STR_LEN);
buffer[FRAG_STR_LEN] = '\0';
size_t count = 0;
char key[16];
// First string is one entry shorter to compensate for the namespace entry
// written on page 0, so that page also retains exactly 2 free entries.
buffer[FRAG_STR_FIRST_LEN] = '\0';
snprintf(key, sizeof(key), "f%05u", (unsigned)count);
err = nvs_set_str(handle, key, buffer);
buffer[FRAG_STR_FIRST_LEN] = 'A';
if (err == ESP_OK && nvs_commit(handle) == ESP_OK) {
count++;
}
// Remaining full-page strings until the partition cannot hold another one.
while (true) {
snprintf(key, sizeof(key), "f%05u", (unsigned)count);
err = nvs_set_str(handle, key, buffer);
if (err == ESP_ERR_NVS_NOT_ENOUGH_SPACE) {
break;
}
if (err != ESP_OK) {
ESP_LOGE(TAG, "Error (%s) writing fragmentation string!", esp_err_to_name(err));
break;
}
if (nvs_commit(handle) != ESP_OK) {
break;
}
count++;
}
free(buffer);
nvs_close(handle);
return count;
}
// Fill a partition with same-sized blobs until it runs out of space.
// Returns the number of blobs successfully stored.
static size_t fill_with_blobs(const char* partition_name, size_t blob_size)
{
nvs_handle_t handle;
esp_err_t err = nvs_open_from_partition(partition_name, BLOB_NAMESPACE, NVS_READWRITE, &handle);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Error (%s) opening blob handle on '%s'!", esp_err_to_name(err), partition_name);
return 0;
}
uint8_t* blob = malloc(blob_size);
if (blob == NULL) {
ESP_LOGE(TAG, "Failed to allocate %u B blob buffer!", (unsigned)blob_size);
nvs_close(handle);
return 0;
}
memset(blob, 0x5A, blob_size);
size_t count = 0;
char key[16];
while (true) {
snprintf(key, sizeof(key), "b%05u", (unsigned)count);
err = nvs_set_blob(handle, key, blob, blob_size);
if (err == ESP_ERR_NVS_NOT_ENOUGH_SPACE) {
break;
}
if (err != ESP_OK) {
ESP_LOGE(TAG, "Error (%s) writing blob!", esp_err_to_name(err));
break;
}
err = nvs_commit(handle);
if (err == ESP_ERR_NVS_NOT_ENOUGH_SPACE) {
break;
}
count++;
}
free(blob);
nvs_close(handle);
return count;
}
// Run one measurement cell: erase + init a partition, optionally fragment it,
// fill it with blobs of the given size and report heap/entry/overhead statistics.
static void measure_blob_overhead(const char* partition_name, size_t blob_size, bool fragment)
{
ESP_ERROR_CHECK(nvs_flash_erase_partition(partition_name));
uint32_t heap_before_init = esp_get_free_heap_size();
ESP_ERROR_CHECK(nvs_flash_init_partition(partition_name));
uint32_t heap_after_init = esp_get_free_heap_size();
nvs_stats_t stats;
ESP_ERROR_CHECK(nvs_get_stats(partition_name, &stats));
size_t total_entries = stats.total_entries;
size_t frag_strings = 0;
if (fragment) {
frag_strings = fragment_partition(partition_name);
}
size_t stored = fill_with_blobs(partition_name, blob_size);
uint32_t heap_after_fill = esp_get_free_heap_size();
ESP_ERROR_CHECK(nvs_get_stats(partition_name, &stats));
size_t per_blob = entries_per_blob_ideal(blob_size);
size_t expected = (per_blob != 0) ? (total_entries / per_blob) : 0;
size_t payload = stored * blob_size;
size_t capacity = total_entries * NVS_ENTRY_SIZE;
int overhead_pct = (capacity != 0) ? (int)(100 - (100ULL * payload) / capacity) : 0;
printf("\n");
printf("NVS BLOB TEST - %u B (partition '%s', %s):\n",
(unsigned)blob_size, partition_name, fragment ? "fragmented" : "pristine");
printf("======================\n");
printf("heap before NVS init: %" PRIu32 " B\n", heap_before_init);
printf("heap after NVS init: %" PRIu32 " B (diff %" PRId32 " B)\n",
heap_after_init, (int32_t)(heap_before_init - heap_after_init));
if (fragment) {
printf("fragmentation strings written: %u\n", (unsigned)frag_strings);
}
printf("\n");
printf("available heap after fill: %" PRIu32 " B (diff %" PRId32 " B)\n",
heap_after_fill, (int32_t)(heap_after_init - heap_after_fill));
printf("expected blobs count: %u\n", (unsigned)expected);
printf("stored blobs count: %u\n", (unsigned)stored);
printf("used_entries: %u (%u B)\n", stats.used_entries, (unsigned)(stats.used_entries * NVS_ENTRY_SIZE));
printf("free_entries: %u (%u B)\n", stats.free_entries, (unsigned)(stats.free_entries * NVS_ENTRY_SIZE));
printf("total_entries: %u (%u B)\n", stats.total_entries, (unsigned)(stats.total_entries * NVS_ENTRY_SIZE));
printf("STORAGE OVERHEAD: %d%%\n", overhead_pct);
ESP_ERROR_CHECK(nvs_flash_deinit_partition(partition_name));
}
static void run_overhead_measurement(void)
{
const size_t partition_count = sizeof(measured_partitions) / sizeof(measured_partitions[0]);
const size_t blob_size_count = sizeof(blob_sizes) / sizeof(blob_sizes[0]);
ESP_LOGI(TAG, "Starting NVS blob storage-overhead measurement...");
for (size_t p = 0; p < partition_count; p++) {
for (size_t b = 0; b < blob_size_count; b++) {
measure_blob_overhead(measured_partitions[p], blob_sizes[b], false);
#if CONFIG_EXAMPLE_INDUCE_FRAGMENTATION
measure_blob_overhead(measured_partitions[p], blob_sizes[b], true);
#endif
}
}
ESP_LOGI(TAG, "NVS blob storage-overhead measurement done.");
}
#endif // CONFIG_EXAMPLE_RUN_OVERHEAD_MEASUREMENT
void app_main(void)
{
// Erase the contents of the default NVS partition for clean run of this example
@@ -230,5 +447,9 @@ void app_main(void)
ESP_LOGE(TAG, "Error (%s) reading back stored data from namespace '%s'!", esp_err_to_name(ret), MOCK_DATA_BACKUP_NAMESPACE);
}
#if CONFIG_EXAMPLE_RUN_OVERHEAD_MEASUREMENT
run_overhead_measurement();
#endif
ESP_LOGI(TAG, "Returning from app_main().");
}
@@ -0,0 +1,8 @@
# Name, Type, SubType, Offset, Size
factory, app, factory, 0x10000, 1M
nvs, data, nvs, , 0x6000
phy_init, data, phy, , 0x1000
# Partitions of different sizes used by the storage-overhead measurement.
nvs_16k, data, nvs, , 0x4000
nvs_32k, data, nvs, , 0x8000
nvs_64k, data, nvs, , 0x10000
1 # Name, Type, SubType, Offset, Size
2 factory, app, factory, 0x10000, 1M
3 nvs, data, nvs, , 0x6000
4 phy_init, data, phy, , 0x1000
5 # Partitions of different sizes used by the storage-overhead measurement.
6 nvs_16k, data, nvs, , 0x4000
7 nvs_32k, data, nvs, , 0x8000
8 nvs_64k, data, nvs, , 0x10000
@@ -1,4 +1,4 @@
# SPDX-FileCopyrightText: 2025 Espressif Systems (Shanghai) CO LTD
# SPDX-FileCopyrightText: 2025-2026 Espressif Systems (Shanghai) CO LTD
# SPDX-License-Identifier: Unlicense OR CC0-1.0
import pytest
from pytest_embedded import Dut
@@ -14,4 +14,9 @@ def test_examples_nvs_statistics(dut: Dut) -> None:
dut.expect('Getting post-commit NVS statistics...', timeout=5)
dut.expect('Newly used entries match expectation.', timeout=5)
# Blob storage-overhead measurement (sweeps partition and blob sizes).
dut.expect('Starting NVS blob storage-overhead measurement...', timeout=10)
dut.expect('STORAGE OVERHEAD:', timeout=60)
dut.expect('NVS blob storage-overhead measurement done.', timeout=120)
dut.expect('Returning from app_main().', timeout=5)
@@ -0,0 +1,3 @@
CONFIG_PARTITION_TABLE_CUSTOM=y
CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions.csv"
CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y