Skip to content

Add support for shrinking a filesystem #1094

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 5 commits into
base: master
Choose a base branch
from

Conversation

sosthene-nitrokey
Copy link
Contributor

This PR adds a new lfs_fs_shrink, which functions similarly to lfs_fs_grow, but supports reducing the block count.

This functions first checks that none of the removed block are in use. If it is the case, it will fail.

Closes #1093

@sosthene-nitrokey sosthene-nitrokey force-pushed the shrink-fs branch 3 times, most recently from 7b8aaa0 to a1533aa Compare April 17, 2025 08:04
This PR adds a new `lfs_fs_shrink`, which functions similarly to
`lfs_fs_grow`, but supports reducing the block count.

This functions first checks that none of the removed block are in use.
If it is the case, it will fail.
@geky-bot
Copy link
Collaborator

Tests passed ✓, Code: 17220 B (+0.6%), Stack: 1448 B (+0.0%), Structs: 812 B (+0.0%)
Code Stack Structs Coverage
Default 17220 B (+0.6%) 1448 B (+0.0%) 812 B (+0.0%) Lines 2452/2613 lines (+0.0%)
Readonly 6230 B (+0.0%) 448 B (+0.0%) 812 B (+0.0%) Branches 1285/1614 branches (+0.2%)
Threadsafe 18088 B (+0.7%) 1448 B (+0.0%) 820 B (+0.0%) Benchmarks
Multiversion 17292 B (+0.6%) 1448 B (+0.0%) 816 B (+0.0%) Readed 29369693876 B (+0.0%)
Migrate 18880 B (+0.5%) 1752 B (+0.0%) 816 B (+0.0%) Proged 1482874766 B (+0.0%)
Error-asserts 18000 B (+0.6%) 1440 B (+0.0%) 812 B (+0.0%) Erased 1568888832 B (+0.0%)

@geky
Copy link
Member

geky commented May 2, 2025

Hi @sosthene-nitrokey, thanks for the PR.

I'm going to contradict past me a bit, but I think this would be better implemented as an extension to lfs_fs_grow.

I've come around on the idea of add lfs_fs_shrink to littlefs, since it is also valuable for allowing users to allocate fixed storage for application specific uses (XIP mainly, #692). That being said, it is still a hard problem.

But in obnoxiously POSIX fashion I think we want to avoid adding a new function and instead allow lfs_fs_grow to specify block_counts both larger and smaller than the origin block_count. This would match the behavior of lfs_file_truncate. It's unintuitive but there is a bit of reason behind the madness as it allows you to specify the block_count without caring about the current state. (It may also lead to better code reuse as you may not need the lfs_fs_rewrite_block_count function.)

To avoid unnecessary code cost, shrinking behavior would be opt-in behind ifdef LFS_SHRINK. This opens the door for using a different define, maybe ifdef LFS_SHRINKIFCHEAP (open to better names), for the erroring behavior proposed here.


So, instead of adding lfs_fs_shrink, add ifdef LFS_SHRINKIFCHEAP which enables the shrink-if-not-in-use behavior in lfs_fs_grow. I think this should be a relatively easy change to the proposed PR. But let me know if you have concerns.

Also I appreciate the relevant test cases. Usually I have to beg for those :)

Copy link
Member

@geky geky left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added some nits to consider. But this is some pretty great code. It would just be nice to move under ifdef LFS_SHRINKIFCHEAP to fit into above future plans.

lfs.c Outdated
{tag, &superblock}));
lfs_block_t threshold = block_count;

int err = lfs_fs_traverse_(lfs, lfs_shrink_check_block, &threshold, true);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Please limit to 80 chars/columns (I should really add a CI job for this)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hi OverLength ctermbg=darkblue
match OverLength /\%81v./     

is a decent way to highlight >80 char lines if you're in vim.

lfs.c Outdated
@@ -5233,38 +5233,67 @@ static int lfs_fs_gc_(lfs_t *lfs) {
#endif

#ifndef LFS_READONLY
static int lfs_fs_rewrite_block_count(lfs_t *lfs, lfs_size_t block_count) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Naming is a bit weird, but useful, in littlefs. Underscores indicate namespaces/subsystems for the most part. So this should be lfs_fs_rewriteblockcount.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense. This function will be removed anyway following shrink being part of lfs_fs_grow.

lfs.c Outdated
return tag;
}
lfs_superblock_fromle32(&superblock);
static int lfs_shrink_check_block(void * data, lfs_block_t block) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: see above, namespace quirkyness: This should be lfs_fs_shrink_checkblock.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

lfs.h Outdated
// Shrinks the filesystem to a new size, updating the superblock with the new
// block count.
//
// Note: This first checks that none of the blocks that are being removed are in use
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: 80 cols

lfs.c Outdated
}
lfs_superblock_fromle32(&superblock);
static int lfs_shrink_check_block(void * data, lfs_block_t block) {
lfs_size_t threshold = *((lfs_size_t *) data);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: spacing: *((lfs_size_t*)data);

@bmcdonnell-fb
Copy link

@geky

... I think this would be better implemented as an extension to lfs_fs_grow.

Maybe rename it to lfs_fs_resize?

You could #define lfs_fs_grow lfs_fs_resize, and deprecate lfs_fs_grow.

@geky
Copy link
Member

geky commented May 2, 2025

Ah, but the name abuse is part of the charm of POSIX, see lfs_file_truncate :)

Users might also think lfs_fs_resize and lfs_fs_size are somehow related (though I do want to rename lfs_fs_size -> lfs_fs_usage at some point, but it's low priority).

@geky geky added enhancement needs work nothing broken but not ready yet needs minor version new functionality only allowed in minor versions labels May 2, 2025
@sosthene-nitrokey
Copy link
Contributor Author

Thank you for the review!

I made a fixup commit that applies your suggestions. I'm just not sure how to make it so that LFS_FS_SHRINKCHEAP is defined in the relevant tests (the tests pass if I add it to runners/test_runners.h so it's only about where I should put the define).

Regarding merging this functionality into lfs_fs_grow, I initially made it into its own function because I think shrinking is a more "dangerous" operation and should be explicit (as any usage would require careful review, more so than grow). I don't care much about being "POSIX-like", but if that's what you prefer no issue from me.

@sosthene-nitrokey
Copy link
Contributor Author

For the name LFS_FS_SHRINKCHEAP what about LFS_FS_SHRINK_NONMOVING ?

@bmcdonnell-fb
Copy link

Regarding merging this functionality into lfs_fs_grow, I initially made it into its own function because I think shrinking is a more "dangerous" operation and should be explicit (as any usage would require careful review, more so than grow).

This makes sense to me.

@geky maybe consider prioritizing clarity over POSIX-likeness, in cases where POSIX-likeness is quirky, unexpected, or unclear? Just my 2¢; apologies if you've already decided otherwise for the project.

@sosthene-nitrokey sosthene-nitrokey force-pushed the shrink-fs branch 3 times, most recently from 9a728cd to 9b8f802 Compare May 5, 2025 16:00
@geky
Copy link
Member

geky commented May 5, 2025

I made a fixup commit that applies your suggestions.

Thanks! It's looking good to me codewise now.

I'm just not sure how to make it so that LFS_FS_SHRINKCHEAP is defined in the relevant tests (the tests pass if I add it to runners/test_runners.h so it's only about where I should put the define).

Ah, this is a bit messy at the moment because most ifdefs are large in scope.

You can probably copy the test-no-intrinsics job and add -DLFS_SHRINKIFCHEAP to the CFLAGS.

You could limit the test suites via:

CFLAGS="$CFLAGS -DLFS_NO_INTRINSICS" \
    TESTFLAGS="test_superblocks test_grow test_shrink" \
    make test

But this is definitely not going to be a bottleneck vs powerloss/emulated/valgrind testing, so we may as well just test everything.

There's also a minor CI rework, and an umbrella LFS_BIGGEST define, in the future, so I wouldn't worry too much about fragmentation at the moment.

@geky
Copy link
Member

geky commented May 5, 2025

For the name LFS_FS_SHRINKCHEAP what about LFS_FS_SHRINK_NONMOVING ?

That is a better name, but what if I raise you LFS_SHRINKIFTRIVIAL?

@geky
Copy link
Member

geky commented May 5, 2025

Regarding merging this functionality into lfs_fs_grow, I initially made it into its own function because I think shrinking is a more "dangerous" operation and should be explicit (as any usage would require careful review, more so than grow).

That's a fair argument. Though I'm not sure if "dangerous" is the right word. Shrinking would still use copy-on-write updates, so worst case lfs_fs_grow errors and you end up with an unmodified filesystem (ignoring current issues with error recovery).

However it is a much more expensive operation.

And there is also the potential benefit of compile-time gc with the separate lfs_fs_shrink, though not sure if this is useful with it already being an opt-in ifdef...

Hmm. May have to think on this. Sorry if this ends up going the other way.

The main reason for merging is to prefer fewer, more powerful (not more complicated!), functions to make it easier for users the keep the API in their head. The more fragmented your API becomes, the harder it is to know whether or not a relevant function exists as a user.

Though maybe the benefits of separating outweigh the benefits of minimizing the API surface here...

@geky maybe consider prioritizing clarity over POSIX-likeness, in cases where POSIX-likeness is quirky, unexpected, or unclear? Just my 2¢; apologies if you've already decided otherwise for the project.

At the risk of getting into controversial opinion territory, my opinion is consistency > clarity when it comes to API design. A clear name doesn't exactly help you if you can't find the function, and usually the name won't be able to capture the full idiosyncrasies of the function anyways.

@geky-bot
Copy link
Collaborator

geky-bot commented May 6, 2025

Tests passed ✓, Code: 17116 B (+0.0%), Stack: 1448 B (+0.0%), Structs: 812 B (+0.0%)
Code Stack Structs Coverage
Default 17116 B (+0.0%) 1448 B (+0.0%) 812 B (+0.0%) Lines 2433/2594 lines (+0.0%)
Readonly 6230 B (+0.0%) 448 B (+0.0%) 812 B (+0.0%) Branches 1279/1610 branches (-0.0%)
Threadsafe 17964 B (-0.0%) 1448 B (+0.0%) 820 B (+0.0%) Benchmarks
Multiversion 17188 B (+0.0%) 1448 B (+0.0%) 816 B (+0.0%) Readed 29369693876 B (+0.0%)
Migrate 18780 B (+0.0%) 1752 B (+0.0%) 816 B (+0.0%) Proged 1482874766 B (+0.0%)
Error-asserts 17896 B (+0.0%) 1440 B (+0.0%) 812 B (+0.0%) Erased 1568888832 B (+0.0%)

@sosthene-nitrokey
Copy link
Contributor Author

For the name LFS_FS_SHRINKCHEAP what about LFS_FS_SHRINK_NONMOVING ?

That is a better name, but what if I raise you LFS_SHRINKIFTRIVIAL?

I still prefer SHRINKNONMOVING. It's not that important to me.

@geky
Copy link
Member

geky commented May 7, 2025

Having slept on it, I still think this should be one function (lfs_fs_grow):

  1. It simplifies maintaining a variable sized fixed region of storage after the filesystem (for XIP, etc).
  2. It better shares code internally.
  3. It minimizes the API surface.
  4. It matches lfs_fs_truncate, for better or worse.

It's subjective, but it feels like the best option to me. Thanks for the feedback both of you, and for adopting the unified function @sosthene-nitrokey.


I still prefer SHRINKNONMOVING. It's not that important to me.

We can go with LFS_SHRINKNONMOVING. Though thoughts on LFS_SHRINKNONRELOCATING or LFS_SHRINKNORELOCATE? "Relocate" is the term used elsewhere in the codebase specifically for block -> block migrations.

lfs.c Outdated
#endif
#ifdef LFS_SHRINKIFCHEAP
lfs_block_t threshold = block_count;
err = lfs_fs_traverse_(lfs, lfs_shrink_checkblock, &threshold, true);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, sorry I should have caught this earlier.

Even with LFS_SHRINKNONRELOCATING enabled, we shouldn't traverse if unless block_count < lfs->block_count. lfs_fs_traverse_ involves a lot of io, and users probably expect lfs_fs_grow to be cheap if it is a simple grow operation.

Also I think this can just be &block_count? It doesn't look like threshold is used after this.

lfs.c Outdated
@@ -5233,40 +5233,63 @@ static int lfs_fs_gc_(lfs_t *lfs) {
#endif

#ifndef LFS_READONLY
#ifdef LFS_SHRINKIFCHEAP
static int lfs_shrink_checkblock(void * data, lfs_block_t block) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: spacing: void *data

lfs.c Outdated
@@ -5233,40 +5233,63 @@ static int lfs_fs_gc_(lfs_t *lfs) {
#endif

#ifndef LFS_READONLY
#ifdef LFS_SHRINKIFCHEAP
static int lfs_shrink_checkblock(void * data, lfs_block_t block) {
lfs_size_t threshold = *((lfs_size_t *) data);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: spacing: *((lfs_size_t*)data)


if = "AFTER_BLOCK_COUNT <= BLOCK_COUNT"
code = '''
#ifdef LFS_SHRINKIFCHEAP
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, yes. This is the best way to ifdef right now. I just noticed the earlier test failures.

I have a branch that adds ifdef attrs to the test case itself, but it hasn't been upstreamed yet unfortunately...

@geky-bot
Copy link
Collaborator

geky-bot commented May 7, 2025

Tests passed ✓, Code: 17116 B (+0.0%), Stack: 1448 B (+0.0%), Structs: 812 B (+0.0%)
Code Stack Structs Coverage
Default 17116 B (+0.0%) 1448 B (+0.0%) 812 B (+0.0%) Lines 2433/2594 lines (+0.0%)
Readonly 6230 B (+0.0%) 448 B (+0.0%) 812 B (+0.0%) Branches 1279/1610 branches (-0.0%)
Threadsafe 17964 B (-0.0%) 1448 B (+0.0%) 820 B (+0.0%) Benchmarks
Multiversion 17188 B (+0.0%) 1448 B (+0.0%) 816 B (+0.0%) Readed 29369693876 B (+0.0%)
Migrate 18780 B (+0.0%) 1752 B (+0.0%) 816 B (+0.0%) Proged 1482874766 B (+0.0%)
Error-asserts 17896 B (+0.0%) 1440 B (+0.0%) 812 B (+0.0%) Erased 1568888832 B (+0.0%)

@geky
Copy link
Member

geky commented May 8, 2025

Looks good to me, thanks for the tweaks 👍 Will bring this into the next minor release.

@geky geky added next minor and removed needs work nothing broken but not ready yet labels May 8, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement needs minor version new functionality only allowed in minor versions next minor
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Ability to downsize a filesystem
4 participants