Skip to content

Commit 27fe57f

Browse files
jplexergmarullclaude
committed
fw/services/settings: add growable settings files
settings_file_open pre-allocates the PFS file at ~120% of max_used_space, so a large per-app cap forces every persist file to claim that much flash up front, even when most apps only use a few hundred bytes. To make raising the cap cheap, decouple the enforcement cap from the physical allocation. Add an alloc_used_space field to SettingsFile that tracks the physical allocation budget separately from max_used_space. A new settings_file_open_growable() API sets alloc_used_space to a small initial value. When a write needs more physical space but fits within the enforcement cap, the file is automatically grown via rewrite with doubling allocation (e.g. 4K -> 8K -> 16K -> ... up to max_used_space). To prevent grown files from holding flash hostage indefinitely after the data is no longer needed, settings_file_compact picks the smallest doubling step that leaves ~2x headroom over live data and lowers alloc_used_space toward the floor before rewriting. The new min_alloc_used_space field tracks the per-file floor (the initial_alloc_size requested at open) so files never shrink below the caller's stated minimum. Existing callers of settings_file_open() are unchanged: alloc_used_space and min_alloc_used_space both equal max_used_space, preserving the old pre-allocation behavior with no shrink. The persist service now uses settings_file_open_growable() with a 4 KiB initial allocation (~5 KiB physical), close to the existing 6 KiB cap. Co-Authored-By: Gerard Marull-Paretas <gerard@teslabs.com> Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Signed-off-by: Gerard Marull-Paretas <gerard@teslabs.com> Signed-off-by: Joshua Jun <lets@throw.rocks>
1 parent 72ba25f commit 27fe57f

6 files changed

Lines changed: 388 additions & 20 deletions

File tree

include/pbl/services/settings/settings_file.h

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,16 @@ typedef struct SettingsFile {
4141
//! fail.
4242
int max_used_space;
4343

44+
//! The current allocation budget for physical file size. For growable files,
45+
//! this starts at a small initial value and grows toward max_used_space on
46+
//! demand. For non-growable files, this equals max_used_space.
47+
int alloc_used_space;
48+
49+
//! The floor for alloc_used_space. Compact will not shrink below this. For
50+
//! growable files this is the initial_alloc_size requested at open time;
51+
//! for non-growable files this equals max_used_space (no shrink).
52+
int min_alloc_used_space;
53+
4454
//! Amount of space in the settings_file that is currently dead, i.e.
4555
//! has been written to with some data, but that data is no longer valid.
4656
//! (overwritten records get added to this)
@@ -69,6 +79,8 @@ typedef struct SettingsFile {
6979
//! ignored. We could change this if the need arises.
7080
status_t settings_file_open(SettingsFile *file, const char *name,
7181
int max_used_space);
82+
status_t settings_file_open_growable(SettingsFile *file, const char *name,
83+
int max_used_space, int initial_alloc_size);
7284
void settings_file_close(SettingsFile *file);
7385

7486
bool settings_file_exists(SettingsFile *file, const void *key, size_t key_len);

src/fw/services/persist/service.c

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
#include "util/units.h"
2121

2222
#define PERSIST_STORAGE_MAX_SPACE KiBYTES(6)
23+
#define PERSIST_STORAGE_INITIAL_ALLOC KiBYTES(4)
2324

2425
typedef struct PersistStore {
2526
ListNode list_node;
@@ -124,7 +125,9 @@ SettingsFile * persist_service_lock_and_get_store(const Uuid *uuid) {
124125
if (!store->file_open) {
125126
char filename[PERSIST_FILE_NAME_MAX_LENGTH];
126127
PBL_ASSERTN(PASSED(prv_get_file_name(filename, sizeof(filename), uuid)));
127-
PBL_ASSERTN(PASSED(settings_file_open(&store->file, filename, PERSIST_STORAGE_MAX_SPACE)));
128+
PBL_ASSERTN(PASSED(settings_file_open_growable(&store->file, filename,
129+
PERSIST_STORAGE_MAX_SPACE,
130+
PERSIST_STORAGE_INITIAL_ALLOC)));
128131
store->file_open = true;
129132
}
130133
return &store->file;

src/fw/services/settings/settings_file.c

Lines changed: 94 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -25,16 +25,17 @@ static bool file_hdr_is_uninitialized(SettingsFileHeader *file_hdr) {
2525
&& (file_hdr->flags == 0xffff);
2626
}
2727

28-
static status_t prv_open(SettingsFile *file, const char *name, uint8_t flags, int max_used_space) {
28+
static status_t prv_open(SettingsFile *file, const char *name, uint8_t flags,
29+
int max_used_space, int alloc_used_space,
30+
int min_alloc_used_space) {
2931
// Making the max_space_total at least a little bit larger than the
30-
// max_used_space allows us to avoid thrashing. Without it, if
31-
// max_space_total == max_used_space, then if the file is full, changing a
32+
// alloc_used_space allows us to avoid thrashing. Without it, if
33+
// max_space_total == alloc_used_space, then if the file is full, changing a
3234
// single value would force the whole file to be rewritten- every single
3335
// time! It's probably worth it to "waste" a bit of flash space to avoid
3436
// this pathalogical case.
35-
int max_space_total = pfs_sector_optimal_size(max_used_space * 12 / 10, strlen(name));
37+
int max_space_total = pfs_sector_optimal_size(alloc_used_space * 12 / 10, strlen(name));
3638

37-
// TODO: Dynamically sized files?
3839
int fd = pfs_open(name, flags, FILE_TYPE_STATIC, max_space_total);
3940
if (fd < 0) {
4041
PBL_LOG_ERR("Could not open settings file '%s', %d", name, fd);
@@ -50,6 +51,8 @@ static status_t prv_open(SettingsFile *file, const char *name, uint8_t flags, in
5051
*file = (SettingsFile) {
5152
.name = kernel_strdup_check(name),
5253
.max_used_space = max_used_space,
54+
.alloc_used_space = alloc_used_space,
55+
.min_alloc_used_space = min_alloc_used_space,
5356
.max_space_total = max_space_total,
5457
};
5558

@@ -73,32 +76,37 @@ static status_t prv_open(SettingsFile *file, const char *name, uint8_t flags, in
7376
PBL_LOG_WRN("Unrecognized version %d for file %s, removing...",
7477
file_hdr.version, name);
7578
pfs_close_and_remove(fd);
76-
return prv_open(file, name, flags, max_used_space);
79+
return prv_open(file, name, flags, max_used_space, alloc_used_space, min_alloc_used_space);
80+
}
81+
82+
// For growable files, adopt the actual file size before bootup_check so that
83+
// any compaction during recovery uses the correct (grown) allocation size.
84+
int actual_size = pfs_get_file_size(file->iter.fd);
85+
if (alloc_used_space < max_used_space && actual_size > max_space_total) {
86+
file->alloc_used_space = actual_size * 10 / 12;
87+
file->max_space_total = actual_size;
7788
}
7889

7990
status_t status = bootup_check(file);
8091
if (status < 0) {
8192
PBL_LOG_ERR("Bootup check failed (%"PRId32"), not good. "
8293
"Attempting to recover by deleting %s...", status, name);
8394
pfs_close_and_remove(fd);
84-
return prv_open(file, name, flags, max_used_space);
95+
return prv_open(file, name, flags, max_used_space, alloc_used_space, min_alloc_used_space);
8596
}
8697

8798
// There's a chance that the caller increased the desired size of the settings file since
8899
// the file was originally created (i.e. the file was created in an earlier version of the
89100
// firmware). If we detect that situation, let's re-write the file to the new larger requested
90101
// size.
91-
int actual_size = pfs_get_file_size(file->iter.fd);
92-
if (actual_size < max_space_total) {
102+
if (alloc_used_space >= max_used_space && actual_size < max_space_total) {
93103
PBL_LOG_INFO("Re-writing settings file %s to increase its size from %d to %d.",
94104
name, actual_size, max_space_total);
95-
// The settings_file_rewrite_filtered call creates a new file based on file->max_used_space
96-
// and copies the contents of the existing file into it.
97105
status = settings_file_rewrite_filtered(file, NULL, NULL);
98106
if (status < 0) {
99107
PBL_LOG_ERR("Could not resize file %s (error %"PRId32"). Creating new one",
100108
name, status);
101-
return prv_open(file, name, flags, max_used_space);
109+
return prv_open(file, name, flags, max_used_space, alloc_used_space, min_alloc_used_space);
102110
}
103111
}
104112

@@ -109,7 +117,16 @@ static status_t prv_open(SettingsFile *file, const char *name, uint8_t flags, in
109117

110118
status_t settings_file_open(SettingsFile *file, const char *name,
111119
int max_used_space) {
112-
return prv_open(file, name, OP_FLAG_READ | OP_FLAG_WRITE, max_used_space);
120+
return prv_open(file, name, OP_FLAG_READ | OP_FLAG_WRITE,
121+
max_used_space, max_used_space, max_used_space);
122+
}
123+
124+
status_t settings_file_open_growable(SettingsFile *file, const char *name,
125+
int max_used_space, int initial_alloc_size) {
126+
// prv_grow doubles alloc_used_space; a zero seed would loop forever.
127+
PBL_ASSERTN(initial_alloc_size > 0);
128+
return prv_open(file, name, OP_FLAG_READ | OP_FLAG_WRITE,
129+
max_used_space, initial_alloc_size, initial_alloc_size);
113130
}
114131

115132
void settings_file_close(SettingsFile *file) {
@@ -199,7 +216,8 @@ status_t settings_file_rewrite_filtered(
199216

200217
SettingsFile new_file;
201218
status_t status = prv_open(&new_file, file->name, OP_FLAG_OVERWRITE | OP_FLAG_READ,
202-
file->max_used_space);
219+
file->max_used_space, file->alloc_used_space,
220+
file->min_alloc_used_space);
203221
if (status < 0) {
204222
PBL_LOG_ERR("Could not open temporary file to compact settings file. Error %"PRIi32".",
205223
status);
@@ -253,8 +271,11 @@ status_t settings_file_rewrite_filtered(
253271
// old file. After the close suceeds, we will end up reading the new
254272
// (compacted) file.
255273
char *name = kernel_strdup(new_file.name);
274+
int alloc_used_space = new_file.alloc_used_space;
275+
int min_alloc_used_space = new_file.min_alloc_used_space;
256276
settings_file_close(&new_file);
257-
status = prv_open(file, name, OP_FLAG_READ | OP_FLAG_WRITE, file->max_used_space);
277+
status = prv_open(file, name, OP_FLAG_READ | OP_FLAG_WRITE,
278+
file->max_used_space, alloc_used_space, min_alloc_used_space);
258279
kernel_free(name);
259280

260281
// FIRM-1649: instrumentation. See note at the top of this function.
@@ -272,7 +293,29 @@ void settings_file_set_change_callback(SettingsFileChangeCallback callback) {
272293
}
273294

274295
T_STATIC status_t settings_file_compact(SettingsFile *file) {
275-
return settings_file_rewrite_filtered(file, NULL, NULL);
296+
// For growable files, drop alloc_used_space toward the floor when live data
297+
// is much smaller than the current allocation. Without this, a file that
298+
// burst to e.g. 256 KiB and then idled would hold that flash forever even
299+
// after the records were deleted, slowly bleeding free PFS space across
300+
// device lifetime. Aim for the smallest doubling step that leaves ~2x
301+
// headroom over used_space; the next grow cycle will re-expand if needed.
302+
const int old_alloc = file->alloc_used_space;
303+
if (file->alloc_used_space > file->min_alloc_used_space) {
304+
int target = file->min_alloc_used_space;
305+
while (target < file->used_space * 2 && target < file->alloc_used_space) {
306+
target *= 2;
307+
}
308+
if (target < file->alloc_used_space) {
309+
file->alloc_used_space = target;
310+
}
311+
}
312+
status_t status = settings_file_rewrite_filtered(file, NULL, NULL);
313+
if (status < 0) {
314+
// rewrite_filtered fails before the swap if it fails at all; the on-disk
315+
// file is still at old_alloc, so put the in-memory book-keeping back.
316+
file->alloc_used_space = old_alloc;
317+
}
318+
return status;
276319
}
277320

278321
static bool key_matches(SettingsRawIter *iter, const uint8_t *key, int key_len) {
@@ -420,6 +463,27 @@ status_t settings_file_set_byte(SettingsFile *file, const void *key,
420463
return S_SUCCESS;
421464
}
422465

466+
static status_t prv_grow(SettingsFile *file, int needed_used_space) {
467+
int new_alloc = file->alloc_used_space;
468+
while (new_alloc < needed_used_space && new_alloc < file->max_used_space) {
469+
new_alloc *= 2;
470+
}
471+
if (new_alloc > file->max_used_space) {
472+
new_alloc = file->max_used_space;
473+
}
474+
if (new_alloc < needed_used_space) {
475+
return E_OUT_OF_STORAGE;
476+
}
477+
478+
int old_alloc = file->alloc_used_space;
479+
file->alloc_used_space = new_alloc;
480+
status_t status = settings_file_rewrite_filtered(file, NULL, NULL);
481+
if (status < 0) {
482+
file->alloc_used_space = old_alloc;
483+
}
484+
return status;
485+
}
486+
423487
// Internal implementation that takes a timestamp parameter
424488
// Note that this operation is designed to be atomic from the perspective of
425489
// an outside observer. That is, either the new value will be completely
@@ -443,7 +507,14 @@ static status_t prv_settings_file_set_internal(SettingsFile *file, const void *k
443507
return E_OUT_OF_STORAGE;
444508
}
445509
if (file->used_space + file->dead_space + rec_size > file->max_space_total) {
446-
status_t status = settings_file_compact(file);
510+
bool needs_growth = (file->used_space + rec_size > file->max_space_total) &&
511+
(file->alloc_used_space < file->max_used_space);
512+
status_t status;
513+
if (needs_growth) {
514+
status = prv_grow(file, file->used_space + rec_size);
515+
} else {
516+
status = settings_file_compact(file);
517+
}
447518
if (status < 0) {
448519
return status;
449520
}
@@ -612,7 +683,8 @@ status_t settings_file_rewrite(SettingsFile *file,
612683
SettingsFile new_file;
613684
status_t status = prv_open(&new_file, file->name,
614685
OP_FLAG_OVERWRITE | OP_FLAG_READ,
615-
file->max_used_space);
686+
file->max_used_space, file->alloc_used_space,
687+
file->min_alloc_used_space);
616688
if (status < 0) {
617689
return status;
618690
}
@@ -628,8 +700,11 @@ status_t settings_file_rewrite(SettingsFile *file,
628700
// old file. After the close suceeds, we will end up reading the new
629701
// (compacted) file.
630702
char *name = kernel_strdup(new_file.name);
703+
int alloc_used_space = new_file.alloc_used_space;
704+
int min_alloc_used_space = new_file.min_alloc_used_space;
631705
settings_file_close(&new_file);
632-
status = prv_open(file, name, OP_FLAG_READ | OP_FLAG_WRITE, file->max_used_space);
706+
status = prv_open(file, name, OP_FLAG_READ | OP_FLAG_WRITE,
707+
file->max_used_space, alloc_used_space, min_alloc_used_space);
633708
kernel_free(name);
634709

635710
return status;

tests/fakes/fake_settings_file.c

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,11 @@ status_t settings_file_open(SettingsFile *file, const char *name,
103103
}
104104
}
105105

106+
status_t settings_file_open_growable(SettingsFile *file, const char *name,
107+
int max_used_space, int initial_alloc_size) {
108+
return settings_file_open(file, name, max_used_space);
109+
}
110+
106111
void settings_file_close(SettingsFile *file) {
107112
cl_assert(s_settings_file.open);
108113
s_settings_file.open = false;

0 commit comments

Comments
 (0)