Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
193 changes: 121 additions & 72 deletions lib/ss_cache.c
Original file line number Diff line number Diff line change
Expand Up @@ -7,119 +7,168 @@

#include <zephyr/logging/log.h>
#include <zephyr/sys/printk.h>
#include <zephyr/toolchain.h>

#include "ss_cache.h"
#include <onomondo/softsim/mem.h>

LOG_MODULE_DECLARE(softsim, CONFIG_SOFTSIM_NRF_LOG_LEVEL);

#define SS_MAX_ENTRIES 10

/* Fixed header preceding the variable-length name in each DIR record:
* a 1-byte name length followed by a 2-byte (big-endian) NVS key. */
#define DIR_RECORD_HEADER_LEN 3

/* The 8-byte entry is the point of this table; catch a padding surprise. */
BUILD_ASSERT(sizeof(struct ss_dir_entry) == 8, "struct ss_dir_entry must stay 8 bytes");

/* FNV-1a. Case-sensitive on purpose, matching the strcmp lookup it replaced;
* the submodule's ss_profile_crc32() lowercases its input and would not. */
static uint32_t fnv1a(const uint8_t *data, size_t len)
{
uint32_t hash = 2166136261u;

for (size_t i = 0; i < len; i++) {
hash = (hash ^ data[i]) * 16777619u;
}

return hash;
}

/* See in ss_cache.h */
struct cache_entry *f_cache_find_buffer(struct cache_entry *entry, struct ss_list *cache)
int ss_dir_table_from_blob(const uint8_t *blob, size_t size, struct ss_dir_entry **out)
{
struct cache_entry *cursor;
struct cache_entry *no_hits_no_write_existing_buff =
NULL; /* Best case: no write needed, existing buffer with size >= min_buf_size */
struct cache_entry *no_hits_no_write =
NULL; /* No write needed, existing buffer with size < min_buf_size */
struct cache_entry *no_hits = NULL; /* Write needed but low hit count */

size_t cached_entries = 0;
size_t min_buf_size = entry->_l;
size_t min_hits_1 = 100, min_hits_2 = 100, min_hits_3 = 100;

SS_LIST_FOR_EACH(cache, cursor, struct cache_entry, list)
{
if (cursor->buf) {
if (!cursor->_b_dirty && cursor->_b_size >= min_buf_size &&
cursor->_cache_hits < min_hits_1) {
min_hits_1 = cursor->_cache_hits;
no_hits_no_write_existing_buff = cursor;
}
if (!cursor->_b_dirty && cursor->_cache_hits < min_hits_2) {
min_hits_2 = cursor->_cache_hits;
no_hits_no_write = cursor;
}
if (cursor->_cache_hits < min_hits_3) {
min_hits_3 = cursor->_cache_hits;
no_hits = cursor;
}
cached_entries++;
size_t cursor = 0;
size_t count = 0;

*out = NULL;

/* First pass: count the well-formed records, applying the same
* truncation rule as the fill below (a declared name that runs past
* the end of the blob must not be read). */
while (cursor < size) {
uint8_t len = blob[cursor]; /* peek the name length */

if (cursor + DIR_RECORD_HEADER_LEN + len > size) {
LOG_WRN("DIR blob truncated; ignoring trailing %u byte(s)",
(unsigned)(size - cursor));
break;
}
cursor += DIR_RECORD_HEADER_LEN + len;
count++;
}

/* Let cache grow to SS_MAX_ENTRIES */
if (cached_entries < SS_MAX_ENTRIES) {
return NULL;
if (count == 0) {
return 0;
}

if (no_hits_no_write_existing_buff) {
return no_hits_no_write_existing_buff;
struct ss_dir_entry *dir = SS_ALLOC_N(count * sizeof(struct ss_dir_entry));

if (!dir) {
LOG_ERR("Failed to allocate the directory table (%u entries)", (unsigned)count);
return -1;
}

if (no_hits_no_write) {
return no_hits_no_write;
cursor = 0;
for (size_t i = 0; i < count; i++) {
uint8_t len = blob[cursor];
uint16_t id = (blob[cursor + 1] << 8) | blob[cursor + 2];

dir[i].hash = fnv1a(&blob[cursor + DIR_RECORD_HEADER_LEN], len);
dir[i].key = id;
dir[i].flags = (id & 0xFF00) >> 8;
dir[i].hits = 0;
cursor += DIR_RECORD_HEADER_LEN + len;
}

if (no_hits) {
return no_hits;
/* Two paths with the same hash would make lookups serve the wrong
* file; refuse the whole table instead. Quadratic, but only at init
* and only over a few hundred entries at most. */
for (size_t i = 1; i < count; i++) {
for (size_t j = 0; j < i; j++) {
if (dir[i].hash == dir[j].hash) {
LOG_ERR("DIR entries with NVS keys 0x%04x and 0x%04x share hash "
"0x%08x; refusing the table",
dir[j].key, dir[i].key, dir[i].hash);
SS_FREE(dir);
return -1;
}
}
}

return NULL;
*out = dir;
return (int)count;
}

/* See in ss_cache.h */
struct cache_entry *f_cache_find_by_name(const char *name, struct ss_list *cache)
int ss_dir_find(const struct ss_dir_entry *dir, size_t count, const char *name)
{
struct cache_entry *cursor;
uint32_t hash = fnv1a((const uint8_t *)name, strlen(name));

SS_LIST_FOR_EACH(cache, cursor, struct cache_entry, list)
{
if (strcmp(cursor->name, name) == 0) {
return cursor;
for (size_t i = 0; i < count; i++) {
if (dir[i].hash == hash) {
return (int)i;
}
}

return NULL;
return -1;
}

/* See in ss_cache.h */
void generate_dir_table_from_blob(struct ss_list *dirs, uint8_t *blob, size_t size)
int ss_slot_find(const struct ss_cache_slot *slots, size_t count, uint16_t dir_idx)
{
size_t cursor = 0;

while (cursor < size) {
uint8_t len = blob[cursor]; /* peek the name length */
/* Check if the record header and name fit in the remaining blob */
if (cursor + DIR_RECORD_HEADER_LEN + len > size) {
LOG_WRN("DIR blob truncated; ignoring trailing %u byte(s)",
(unsigned)(size - cursor));
break;
for (size_t i = 0; i < count; i++) {
if (slots[i].buf && slots[i].dir_idx == dir_idx) {
return (int)i;
}
cursor++;
}

uint16_t id = (blob[cursor] << 8) | blob[cursor + 1];
return -1;
}

cursor += 2;
/* See in ss_cache.h */
int ss_slot_acquire(const struct ss_dir_entry *dir, const struct ss_cache_slot *slots, size_t count,
size_t want_len)
{
int no_hits_no_write_existing_buff =
-1; /* Best case: no write needed, buffer size >= want_len */
int no_hits_no_write = -1; /* No write needed, buffer too small */
int no_hits = -1; /* Write needed but low hit count */

/* Above the hit counter's ceiling, so a fully-hit cache still yields a
* victim: the slot table is the capacity, there is no growing past it. */
size_t min_hits_1 = 0x100, min_hits_2 = 0x100, min_hits_3 = 0x100;

/* Let the cache grow to capacity before evicting anything. */
for (size_t i = 0; i < count; i++) {
if (!slots[i].buf) {
return (int)i;
}
}

char *name = SS_ALLOC_N(len + 1);
memcpy(name, &blob[cursor], len);
name[len] = '\0';
cursor += len;
for (size_t i = 0; i < count; i++) {
uint8_t hits = dir[slots[i].dir_idx].hits;

struct cache_entry *entry = SS_ALLOC(struct cache_entry);
memset(entry, 0, sizeof(struct cache_entry));
if (!slots[i]._b_dirty && slots[i]._b_size >= want_len && hits < min_hits_1) {
min_hits_1 = hits;
no_hits_no_write_existing_buff = (int)i;
}
if (!slots[i]._b_dirty && hits < min_hits_2) {
min_hits_2 = hits;
no_hits_no_write = (int)i;
}
if (hits < min_hits_3) {
min_hits_3 = hits;
no_hits = (int)i;
}
}

entry->key = id;
entry->name = name;
entry->_flags = (id & 0xFF00) >> 8;
entry->buf = NULL;
if (no_hits_no_write_existing_buff >= 0) {
return no_hits_no_write_existing_buff;
}

ss_list_put(dirs, &entry->list);
if (no_hits_no_write >= 0) {
return no_hits_no_write;
}

return no_hits;
}
91 changes: 65 additions & 26 deletions lib/ss_cache.h
Original file line number Diff line number Diff line change
Expand Up @@ -9,54 +9,93 @@
#include <stddef.h>
#include <stdint.h>

#include <onomondo/softsim/list.h>

#define FS_READ_ONLY (1UL << 8)
#define FS_COMMIT_ON_CLOSE (1UL << 7) /* Commit changes to NVS on close */

struct cache_entry {
struct ss_list list;
uint16_t key; /* NVS key */
uint8_t _flags; /* Part of ID is used for flags */
/* How many files may hold a content buffer at once. */
#define SS_MAX_ENTRIES 10

/* One directory record per file. The path itself is not kept: lookups compare
* a 32-bit FNV-1a hash of the path instead, which is what makes the entry 8
* bytes rather than 28-plus-a-string. ss_dir_table_from_blob() refuses a table
* with colliding hashes, so within a table a hash identifies exactly one file;
* the residual risk is a path that is NOT in the table hashing onto one that
* is (~n/2^32 per lookup), which would be served instead of failing. */
struct ss_dir_entry {
uint32_t hash; /* FNV-1a of the path string */
uint16_t key; /* NVS key (all 16 bits, including the flag byte) */
uint8_t flags; /* Derived from (key >> 8); mutable at runtime */
uint8_t hits; /* Open count, saturating; biases eviction */
};

/* One buffered file. A slot with buf == NULL is free and its other fields are
* meaningless. A file handle (ss_FILE) is a pointer to its slot and does not
* survive eviction of that file; the storage backend opens one file at a time
* and closes it before the next open, so an open file is never evicted. */
struct ss_cache_slot {
uint8_t *buf; /* Cached content; NULL = slot free */
uint16_t dir_idx; /* Owning entry in the directory table */
uint16_t _p; /* Local 'file' pointer (ftell, fseek, etc.) */
uint16_t _l; /* Local 'file' length */
uint8_t *buf; /* In case content is cached */
uint16_t _b_size; /* Memory allocated for buf */
uint8_t _b_dirty; /* Buf is divergent from NVS */
uint8_t _cache_hits;
char *name; /* Path/key for lookup */
};

/**
* @brief Find a suitable cache entry with a buffer that can be re-used
* @brief Build the directory table from the "DIR" file content.
*
* @param entry Pointer to a cache entry
* @param cache Pointer to a cache
* Each blob record is [name_len | id_hi | id_lo | name[name_len]]. A record
* that runs past the end of the blob ends the parse (truncated flash content
* must not be read past).
*
* @return Pointer to a suitable cache entry, or NULL if none found
* @param blob Pointer to blob of data
* @param size Size of blob
* @param out Receives the allocated table (NULL when the return is <= 0)
*
* @return Number of entries, or -1 on allocation failure or when two paths
* hash identically (the table would serve the wrong file; fail loudly)
*/
struct cache_entry *f_cache_find_buffer(struct cache_entry *entry, struct ss_list *cache);
int ss_dir_table_from_blob(const uint8_t *blob, size_t size, struct ss_dir_entry **out);

/**
* @brief Find a cache entry by name
* @brief Find a directory entry by path.
*
* @param name Name of the cache entry to find
* @param cache Pointer to a cache
* @param dir Directory table
* @param count Number of entries in the table
* @param name Path to look up
*
* @return Pointer to the cache entry with the given name, or NULL if not found
* @return Index of the entry, or -1 if not found
*/
struct cache_entry *f_cache_find_by_name(const char *name, struct ss_list *cache);
int ss_dir_find(const struct ss_dir_entry *dir, size_t count, const char *name);

/**
* @brief Generate the directory structure based on the content in the "DIR" file.
* @brief Find the slot buffering a given file, if any.
*
* The DIR file encodes ID (used to locate the actual file in flash) and the name
* of the file.
* @param slots Slot table
* @param count Number of slots
* @param dir_idx Directory index of the file
*
* @param dirs Linked list to populate
* @param blob Pointer to blob of data
* @param size Size of blob
* @return Index of the slot, or -1 if the file is not buffered
*/
int ss_slot_find(const struct ss_cache_slot *slots, size_t count, uint16_t dir_idx);

/**
* @brief Pick the slot to load a file into.
*
* A free slot is returned first (the cache grows to its capacity before
* anything is evicted). Once full, the victim preference is: clean with a
* buffer already big enough for want_len and fewest hits, then clean with
* fewest hits, then simply fewest hits. The caller writes a dirty victim
* back and reuses or frees its buffer.
*
* @param dir Directory table (source of the per-file hit counts)
* @param slots Slot table
* @param count Number of slots
* @param want_len Length of the file about to be loaded
*
* @return Index of the slot to use, or -1 when count is 0
*/
void generate_dir_table_from_blob(struct ss_list *dirs, uint8_t *blob, size_t size);
int ss_slot_acquire(const struct ss_dir_entry *dir, const struct ss_cache_slot *slots, size_t count,
size_t want_len);

#endif /* _F_CACHE_H_ */
Loading
Loading