Skip to content

Commit 10a0d79

Browse files
Damian-Nordiccarlescufi
authored andcommitted
[nrf fromlist] nvs: implement NVS cache
The NVS data lookup time grows linearly with the number of allocation table entries to walk through, meaning that if some data pair in the NVS changes frequently, access to other data pairs that change rarely can take a lot of time. It is particularly visible when the NVS is used as the settings backend since the backend needs to perform multiple NVS reads to find a requested key. Implement a simple cache that stores an address of the most recent ATE for all NVS IDs that fall into the given cache position. CRC8/16 is used as a hash function used to distribute NVS IDs across the cache entries. The cache entries are only invalidated when an NVS sector is erased as part of the garbage collector task. PR: zephyrproject-rtos/zephyr#45769 Signed-off-by: Damian Krolik <[email protected]>
1 parent 25c0bb1 commit 10a0d79

File tree

7 files changed

+279
-5
lines changed

7 files changed

+279
-5
lines changed

include/zephyr/fs/nvs.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,9 @@ struct nvs_fs {
5555
struct k_mutex nvs_lock;
5656
const struct device *flash_device;
5757
const struct flash_parameters *flash_parameters;
58+
#if CONFIG_NVS_LOOKUP_CACHE
59+
uint32_t lookup_cache[CONFIG_NVS_LOOKUP_CACHE_SIZE];
60+
#endif
5861
};
5962

6063
/**

subsys/fs/nvs/Kconfig

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,22 @@ config NVS
1010

1111
if NVS
1212

13+
config NVS_LOOKUP_CACHE
14+
bool "Non-volatile Storage lookup cache"
15+
help
16+
Enable Non-volatile Storage cache, used to reduce the NVS data lookup
17+
time. Each cache entry holds an address of the most recent allocation
18+
table entry (ATE) for all NVS IDs that fall into that cache position.
19+
20+
config NVS_LOOKUP_CACHE_SIZE
21+
int "Non-volatile Storage lookup cache size"
22+
default 128
23+
range 1 65536
24+
depends on NVS_LOOKUP_CACHE
25+
help
26+
Number of entries in Non-volatile Storage lookup cache.
27+
It is recommended that it be a power of 2.
28+
1329
module = NVS
1430
module-str = nvs
1531
source "subsys/logging/Kconfig.template.log_config"

subsys/fs/nvs/nvs.c

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,78 @@
1616
#include <logging/log.h>
1717
LOG_MODULE_REGISTER(fs_nvs, CONFIG_NVS_LOG_LEVEL);
1818

19+
static int nvs_prev_ate(struct nvs_fs *fs, uint32_t *addr, struct nvs_ate *ate);
20+
static int nvs_ate_valid(struct nvs_fs *fs, const struct nvs_ate *entry);
21+
22+
#ifdef CONFIG_NVS_LOOKUP_CACHE
23+
24+
static inline size_t nvs_lookup_cache_pos(uint16_t id)
25+
{
26+
size_t pos;
27+
28+
#if CONFIG_NVS_LOOKUP_CACHE_SIZE <= UINT8_MAX
29+
/*
30+
* CRC8-CCITT is used for ATE checksums and it also acts well as a hash
31+
* function, so it can be a good choice from the code size perspective.
32+
* However, other hash functions can be used as well if proved better
33+
* performance.
34+
*/
35+
pos = crc8_ccitt(CRC8_CCITT_INITIAL_VALUE, &id, sizeof(id));
36+
#else
37+
pos = crc16_ccitt(0xffff, (const uint8_t *)&id, sizeof(id));
38+
#endif
39+
40+
return pos % CONFIG_NVS_LOOKUP_CACHE_SIZE;
41+
}
42+
43+
static int nvs_lookup_cache_rebuild(struct nvs_fs *fs)
44+
{
45+
int rc;
46+
uint32_t addr, ate_addr;
47+
uint32_t *cache_entry;
48+
struct nvs_ate ate;
49+
50+
memset(fs->lookup_cache, 0xff, sizeof(fs->lookup_cache));
51+
addr = fs->ate_wra;
52+
53+
while (true) {
54+
/* Make a copy of 'addr' as it will be advanced by nvs_pref_ate() */
55+
ate_addr = addr;
56+
rc = nvs_prev_ate(fs, &addr, &ate);
57+
58+
if (rc) {
59+
return rc;
60+
}
61+
62+
cache_entry = &fs->lookup_cache[nvs_lookup_cache_pos(ate.id)];
63+
64+
if (ate.id != 0xFFFF && *cache_entry == NVS_LOOKUP_CACHE_NO_ADDR &&
65+
nvs_ate_valid(fs, &ate)) {
66+
*cache_entry = ate_addr;
67+
}
68+
69+
if (addr == fs->ate_wra) {
70+
break;
71+
}
72+
}
73+
74+
return 0;
75+
}
76+
77+
static void nvs_lookup_cache_invalidate(struct nvs_fs *fs, uint32_t sector)
78+
{
79+
uint32_t *cache_entry = fs->lookup_cache;
80+
uint32_t *const cache_end = &fs->lookup_cache[CONFIG_NVS_LOOKUP_CACHE_SIZE];
81+
82+
for (; cache_entry < cache_end; ++cache_entry) {
83+
if ((*cache_entry >> ADDR_SECT_SHIFT) == sector) {
84+
*cache_entry = NVS_LOOKUP_CACHE_NO_ADDR;
85+
}
86+
}
87+
}
88+
89+
#endif /* CONFIG_NVS_LOOKUP_CACHE */
90+
1991
/* basic routines */
2092
/* nvs_al_size returns size aligned to fs->write_block_size */
2193
static inline size_t nvs_al_size(struct nvs_fs *fs, size_t len)
@@ -96,6 +168,12 @@ static int nvs_flash_ate_wrt(struct nvs_fs *fs, const struct nvs_ate *entry)
96168

97169
rc = nvs_flash_al_wrt(fs, fs->ate_wra, entry,
98170
sizeof(struct nvs_ate));
171+
#ifdef CONFIG_NVS_LOOKUP_CACHE
172+
/* 0xFFFF is a special-purpose identifier. Exclude it from the cache */
173+
if (entry->id != 0xFFFF) {
174+
fs->lookup_cache[nvs_lookup_cache_pos(entry->id)] = fs->ate_wra;
175+
}
176+
#endif
99177
fs->ate_wra -= nvs_al_size(fs, sizeof(struct nvs_ate));
100178

101179
return rc;
@@ -225,6 +303,10 @@ static int nvs_flash_erase_sector(struct nvs_fs *fs, uint32_t addr)
225303

226304
LOG_DBG("Erasing flash at %lx, len %d", (long int) offset,
227305
fs->sector_size);
306+
307+
#ifdef CONFIG_NVS_LOOKUP_CACHE
308+
nvs_lookup_cache_invalidate(fs, addr >> ADDR_SECT_SHIFT);
309+
#endif
228310
rc = flash_erase(fs->flash_device, offset, fs->sector_size);
229311

230312
if (rc) {
@@ -810,6 +892,10 @@ static int nvs_startup(struct nvs_fs *fs)
810892
fs->data_wra = fs->ate_wra & ADDR_SECT_MASK;
811893
}
812894

895+
#ifdef CONFIG_NVS_LOOKUP_CACHE
896+
rc = nvs_lookup_cache_rebuild(fs);
897+
#endif
898+
813899
end:
814900
/* If the sector is empty add a gc done ate to avoid having insufficient
815901
* space when doing gc.
@@ -1051,7 +1137,16 @@ ssize_t nvs_read_hist(struct nvs_fs *fs, uint16_t id, void *data, size_t len,
10511137

10521138
cnt_his = 0U;
10531139

1140+
#ifdef CONFIG_NVS_LOOKUP_CACHE
1141+
wlk_addr = fs->lookup_cache[nvs_lookup_cache_pos(id)];
1142+
1143+
if (wlk_addr == NVS_LOOKUP_CACHE_NO_ADDR) {
1144+
rc = -ENOENT;
1145+
goto err;
1146+
}
1147+
#else
10541148
wlk_addr = fs->ate_wra;
1149+
#endif
10551150
rd_addr = wlk_addr;
10561151

10571152
while (cnt_his <= cnt) {

subsys/fs/nvs/nvs_priv.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ extern "C" {
2828

2929
#define NVS_BLOCK_SIZE 32
3030

31+
#define NVS_LOOKUP_CACHE_NO_ADDR 0xFFFFFFFF
32+
3133
/* Allocation Table Entry */
3234
struct nvs_ate {
3335
uint16_t id; /* data id */
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
/*
2+
* Copyright (c) 2022 Nordic Semiconductor ASA
3+
*
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
&flash0 {
8+
erase-block-size = <0x400>;
9+
};

tests/subsys/fs/nvs/src/main.c

Lines changed: 151 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,11 @@
88
* This test is designed to be run using flash-simulator which provide
99
* functionality for flash property customization and emulating errors in
1010
* flash operation in parallel to regular flash API.
11-
* Test should be run on qemu_x86 target.
11+
* Test should be run on qemu_x86 or native_posix target.
1212
*/
1313

14-
#ifndef CONFIG_BOARD_QEMU_X86
15-
#error "Run on qemu_x86 only"
14+
#if !defined(CONFIG_BOARD_QEMU_X86) && !defined(CONFIG_BOARD_NATIVE_POSIX)
15+
#error "Run on qemu_x86 or native_posix only"
1616
#endif
1717

1818
#include <stdio.h>
@@ -41,7 +41,7 @@ void setup(void)
4141
sim_thresholds = stats_group_find("flash_sim_thresholds");
4242

4343
/* Verify if NVS is initialized. */
44-
if (fs.sector_count != 0) {
44+
if (fs.ready) {
4545
int err;
4646

4747
err = nvs_clear(&fs);
@@ -700,6 +700,146 @@ void test_nvs_gc_corrupt_ate(void)
700700
zassert_true(err == 0, "nvs_mount call failure: %d", err);
701701
}
702702

703+
#ifdef CONFIG_NVS_LOOKUP_CACHE
704+
static size_t num_matching_cache_entries(uint32_t addr, bool compare_sector_only)
705+
{
706+
size_t i, num = 0;
707+
uint32_t mask = compare_sector_only ? ADDR_SECT_MASK : UINT32_MAX;
708+
709+
for (i = 0; i < CONFIG_NVS_LOOKUP_CACHE_SIZE; i++) {
710+
if ((fs.lookup_cache[i] & mask) == addr) {
711+
num++;
712+
}
713+
}
714+
715+
return num;
716+
}
717+
#endif
718+
719+
/*
720+
* Test that NVS lookup cache is properly rebuilt on nvs_mount(), or initialized
721+
* to NVS_LOOKUP_CACHE_NO_ADDR if the store is empty.
722+
*/
723+
void test_nvs_cache_init(void)
724+
{
725+
#ifdef CONFIG_NVS_LOOKUP_CACHE
726+
int err;
727+
size_t num;
728+
uint32_t ate_addr;
729+
uint8_t data = 0;
730+
731+
/* Test cache initialization when the store is empty */
732+
733+
fs.sector_count = 3;
734+
err = nvs_mount(&fs);
735+
zassert_true(err == 0, "nvs_init call failure: %d", err);
736+
737+
num = num_matching_cache_entries(NVS_LOOKUP_CACHE_NO_ADDR, false);
738+
zassert_equal(num, CONFIG_NVS_LOOKUP_CACHE_SIZE, "uninitialized cache");
739+
740+
/* Test cache update after nvs_write() */
741+
742+
ate_addr = fs.ate_wra;
743+
err = nvs_write(&fs, 1, &data, sizeof(data));
744+
zassert_equal(err, sizeof(data), "nvs_write call failure: %d", err);
745+
746+
num = num_matching_cache_entries(NVS_LOOKUP_CACHE_NO_ADDR, false);
747+
zassert_equal(num, CONFIG_NVS_LOOKUP_CACHE_SIZE - 1, "cache not updated after write");
748+
749+
num = num_matching_cache_entries(ate_addr, false);
750+
zassert_equal(num, 1, "invalid cache entry after write");
751+
752+
/* Test cache initialization when the store is non-empty */
753+
754+
memset(fs.lookup_cache, 0xAA, sizeof(fs.lookup_cache));
755+
err = nvs_mount(&fs);
756+
zassert_true(err == 0, "nvs_init call failure: %d", err);
757+
758+
num = num_matching_cache_entries(NVS_LOOKUP_CACHE_NO_ADDR, false);
759+
zassert_equal(num, CONFIG_NVS_LOOKUP_CACHE_SIZE - 1, "uninitialized cache after restart");
760+
761+
num = num_matching_cache_entries(ate_addr, false);
762+
zassert_equal(num, 1, "invalid cache entry after restart");
763+
#endif
764+
}
765+
766+
/*
767+
* Test that even after writing more NVS IDs than the number of NVS lookup cache
768+
* entries they all can be read correctly.
769+
*/
770+
void test_nvs_cache_collission(void)
771+
{
772+
#ifdef CONFIG_NVS_LOOKUP_CACHE
773+
int err;
774+
uint16_t id;
775+
uint16_t data;
776+
777+
fs.sector_count = 3;
778+
err = nvs_mount(&fs);
779+
zassert_true(err == 0, "nvs_init call failure: %d", err);
780+
781+
for (id = 0; id < CONFIG_NVS_LOOKUP_CACHE_SIZE + 1; id++) {
782+
data = id;
783+
err = nvs_write(&fs, id, &data, sizeof(data));
784+
zassert_equal(err, sizeof(data), "nvs_write call failure: %d", err);
785+
}
786+
787+
for (id = 0; id < CONFIG_NVS_LOOKUP_CACHE_SIZE + 1; id++) {
788+
err = nvs_read(&fs, id, &data, sizeof(data));
789+
zassert_equal(err, sizeof(data), "nvs_read call failure: %d", err);
790+
zassert_equal(data, id, "incorrect data read");
791+
}
792+
#endif
793+
}
794+
795+
/*
796+
* Test that NVS lookup cache does not contain any address from gc-ed sector
797+
*/
798+
void test_nvs_cache_gc(void)
799+
{
800+
#ifdef CONFIG_NVS_LOOKUP_CACHE
801+
int err;
802+
size_t num;
803+
uint16_t data = 0;
804+
805+
fs.sector_count = 3;
806+
err = nvs_mount(&fs);
807+
zassert_true(err == 0, "nvs_init call failure: %d", err);
808+
809+
/* Fill the first sector with writes of ID 1 */
810+
811+
while (fs.data_wra + sizeof(data) <= fs.ate_wra) {
812+
++data;
813+
err = nvs_write(&fs, 1, &data, sizeof(data));
814+
zassert_equal(err, sizeof(data), "nvs_write call failure: %d", err);
815+
}
816+
817+
/* Verify that cache contains a single entry for sector 0 */
818+
819+
num = num_matching_cache_entries(0 << ADDR_SECT_SHIFT, true);
820+
zassert_equal(num, 1, "invalid cache content after filling sector 0");
821+
822+
/* Fill the second sector with writes of ID 2 */
823+
824+
while ((fs.ate_wra >> ADDR_SECT_SHIFT) != 2) {
825+
++data;
826+
err = nvs_write(&fs, 2, &data, sizeof(data));
827+
zassert_equal(err, sizeof(data), "nvs_write call failure: %d", err);
828+
}
829+
830+
/*
831+
* At this point sector 0 should have been gc-ed. Verify that action is
832+
* reflected by the cache content.
833+
*/
834+
835+
num = num_matching_cache_entries(0 << ADDR_SECT_SHIFT, true);
836+
zassert_equal(num, 0, "not invalidated cache entries aftetr gc");
837+
838+
num = num_matching_cache_entries(2 << ADDR_SECT_SHIFT, true);
839+
zassert_equal(num, 2, "invalid cache content after gc");
840+
#endif
841+
}
842+
703843
void test_main(void)
704844
{
705845
__ASSERT_NO_MSG(device_is_ready(flash_dev));
@@ -725,7 +865,13 @@ void test_main(void)
725865
ztest_unit_test_setup_teardown(
726866
test_nvs_gc_corrupt_close_ate, setup, teardown),
727867
ztest_unit_test_setup_teardown(
728-
test_nvs_gc_corrupt_ate, setup, teardown)
868+
test_nvs_gc_corrupt_ate, setup, teardown),
869+
ztest_unit_test_setup_teardown(
870+
test_nvs_cache_init, setup, teardown),
871+
ztest_unit_test_setup_teardown(
872+
test_nvs_cache_collission, setup, teardown),
873+
ztest_unit_test_setup_teardown(
874+
test_nvs_cache_gc, setup, teardown)
729875
);
730876

731877
ztest_run_test_suite(test_nvs);

tests/subsys/fs/nvs/testcase.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,6 @@ tests:
66
filesystem.nvs_0x00:
77
extra_args: DTC_OVERLAY_FILE=boards/qemu_x86_ev_0x00.overlay
88
platform_allow: qemu_x86
9+
filesystem.nvs_cache:
10+
extra_args: CONFIG_NVS_LOOKUP_CACHE=y CONFIG_NVS_LOOKUP_CACHE_SIZE=64
11+
platform_allow: native_posix

0 commit comments

Comments
 (0)