Commit Graph

1203 Commits

Author SHA1 Message Date
Christopher Haster ac600ae35e Extended alloc tests to more disk sizes, fixed alloc ckpoint bug
I thought it was a bit funny we test various disk sizes in test_grow,
but no where else! test_grow actually found several bugs when reworking
the lookahead buffer related to small disks, so I figured we should have
some more intentional tests... And behold! A bug!

The issue is that we implicitly call lfs_alloc_ckpoint in
lfsr_mdir_commit. Originally the thinking was that this would be fine
since any in-flight blocks should be committed to a tracked btree/bshrub
first, but lfsr_bshrub_commit goes _through_ lfsr_mdir_commit. Bit of a
problem.

So if we call lfsr_bshrub_commit to add a recently allocated block, it
may end up calling lfsr_mdir_commit, erronously ckpointing the
allocator, and then clobbering the new block if the mdir needs to be
relocated, split, etc.

---

The fix here is to just move lfs_alloc_ckpoint out of lfsr_mdir_commit.
This adds a bit of noise, but it's probably a good thing for alloc
ckpoints to be explicit.

At least lfs_alloc_ckpoint is cheap:

           code          stack
  before: 36412           2680
  after:  36472 (+0.2%)   2680 (+0.0%)
2024-07-17 22:16:04 -05:00
Christopher Haster 4fc03f95a7 Reworked lookahead buffer (again) to avoid shifting bits
The main reason for this change is to allow keeping track of existing
known-free blocks while trying to find more free blocks. This makes it
so failed filesystem traversals don't result in negative progress, which
is nice.

This was difficult in the previous lookahead scheme, since we we'd need
to shift the lookahead buffer to keep off=0 rooted at the first bit.
Shifting bytes is relatively easily with memmove, but it gets tricky
when shifting bits:

  lookahead before: ???? ???? ???? ??00 1101 0101 00?? ????
                                     ^              ^
                                    off          off+size

  shift:            0011 0101 0100 ???? ???? ???? ???? ????
                    ^              ^
                   off          off+size

  traverse:         0011 0101 0100 0000 0000 0000 1100 0000
                    ^                                       ^
                   off                                   off+size

Instead, we now just let the lookahead buffer wrap around. No shifting
required:

  lookahead before: ???? ???? ???? ??00 1101 0101 00?? ????
                                     ^              ^
                                    off          off+size

  traverse:         0000 0000 1100 0000 1101 0101 0000 0000
                                     ^
                                    off
                                     ^
                                  off+size

This gets a bit confusing with the lookahead window also wrapping around
disk, but the math works out with enough modulos (if modulos are too
expensive, we should eventually be able to optimize these into simple
bit masks via compile-time config).

In the future, if we move away from the const config struct, it would
also be nice to try to reducing the number of modulos by storing the
lookahead buffer size in bits instead of bytes...

Note that if the lookahead buffer is larger than disk, the lookahead
window will sort of travel around the underlying buffer. This isn't
inherently a problem, but it did cause some bugs.

To avoid similar bit-related problems with zeroing, lfs_alloc_inc now
also zeros bits as we allocate/skip them, so bits should always be zero
when we start a lookahead traversal. Though note we still need to
manually memset the buffer when discarding lookahead state in init/grow.

---

The end result is surprisingly a net savings in terms of code size. I
guess mainly due to dropping all the lfs_alloc_shift calls:

           code          stack
  before: 36472           2680
  after:  36412 (-0.2%)   2680 (+0.0%)
2024-07-17 22:15:31 -05:00
Christopher Haster 4fe46a983f Added simple lfsr_fs_ckmeta/ckdata functions
These functions provide an easy API for checking all metadata/data
checksums in the filesystem:

  // Check the filesystem for metadata errors
  int lfsr_fs_ckmeta(lfs_t *lfs);

  // Check the filesystem for metadata + data errors
  int lfsr_fs_ckdata(lfs_t *lfs);

These are more-or-less the same as calling lfsr_fs_gc with
LFS_GC_CKMETA/CKDATA, but don't involve the gc/traversal-invalidation
machinery, and may be a bit easier for users to pick up.

---

Unfortunately, for simple wrappers, we're again hit with a somewhat
surprising code cost:

           code          stack
  before: 36288           2680
  after:  36472 (+0.5%)   2680 (+0.0%)

But I think we can again blame the high overhead of LFS_TRAVERSAL/
lfsr_mtree_gc. We should look into reducing/deduplicating this logic...
2024-07-17 22:15:08 -05:00
Christopher Haster 83f2a3c7fc t: Fixed missing lfs_alloc_ckpoint in manual btree compaction
Whoops! Turns out it's easy to forget to checkpoint the allocator when
most standalone operations involve lfsr_mdir_commit which checkpoints
the allocator automatically...

This is also the only case where we are doing btree modifications
outside of either file operations or mtree updates.

Easy fix, code changes minimal:

           code          stack
  before: 36280           2680
  after:  36288 (+0.0%)   2680 (+0.0%)
2024-07-17 21:55:21 -05:00
Christopher Haster 2f08662fb9 Added on-mount traversal flags: LFS_M_MKCONSISTENT/CKMETA/CKDATA/etc
These tell littlefs to do the relevant gc work during mount, which may
be more convenient than calling lfsr_mount and then lfsr_fs_gc.

It also implicitly tears down the filesystem on error, which you can
imagine would be quite useful for LFS_M_CKMETA/LFS_M_CKDATA.

Some flags are more useful here than other (is LFS_M_LOOKAHEAD/COMPACT
really useful?), but since we just pass these directly to our traversal
APIs, we might as well support all of them for consistency.

Also note that since these only change mount's behavior, and have no
effect on the rest of the filesystem, these LFS_M_* flags don't have
related LFS_I_* flags and are not returned by lfsr_fs_stat.

---

This added quite a chunk of code, considering that this is entirely for
convenience:

           code          stack
  before: 35932           2680
  after:  36280 (+1.0%)   2680 (+0.0%)

But I think this is mostly because our low-level traversal state is
relatively costly to manage. It may be possible to deduplicate this a
bit better...
2024-07-17 21:40:37 -05:00
Christopher Haster acfae9e072 Extended lfsr_mount to accept mount flags
This has been a long-time coming, mount flags are just too useful for
configuring a filesystem at runtime.

Currently this is limited to LFS_M_RDONLY and LFS_M_CKPROGS, but there
are a few more planned in the future:

  LFS_M_RDWR     = 0x0000, // Mount the filesystem as read and write
  LFS_M_RDONLY   = 0x0001, // Mount the filesystem as readonly
  LFS_M_STRICT*  = 0x0002, // Error if on-disk config does not match
  LFS_M_FORCE*   = 0x0004, // Ignore compat flags, mount readonly
  LFS_M_FORCEWITHRECKLESSABANDON*
                 = 0x0008, // Ignore compat flags, mount read write

  LFS_M_CKPROGS  = 0x0010, // Check progs by reading back progged data
  LFS_M_CKREADS* = 0x0020, // Check reads via checksums

  * Hypothetical

As a convenience, we also return mount flags in the struct lfs_fsinfo's
flags field as their relevant LFS_I_* variants. Though only to match
statvfs, and only because it's cheap, littlefs's API is low-level and we
should expect users to know what flags they passed to lfsr_mount.

As for the new mount flags:

- LFS_M_RDONLY - For consistency with existing APIs, this just asserts
  on write operations, which makes it a bit useless... But the info flag
  LFS_I_RDONLY may be useful for falling back to a readonly mode if
  we encounter on-disk compat issues.

  At least if implement the theoretical LFS_UNTRUSTED_USER mode
  LFS_M_RDONLY could become a runtime error.

- LFS_M_RDWR - This really just exists to compliment LFS_M_RDONLY and to
  match LFS_O_RDONLY/LFS_O_RDWR. It's just an alias for 0, and I don't
  think there will ever be a reason to make it non-0 (but I can always
  be wrong!).

- LFS_M_CKPROGS - This replaces the check_progs config option and avoids
  using a full byte to store a bool.

  We should probably also have a compile-time option to compile this out
  (LFS_NO_CKPROGS?), but that's a future thing to do.

This ended up adding a surprising bit of code, considering we're just
moving flags around, and noise in lfs_alloc added a bit of stack again:

           code          stack
  before: 35880           2672
  after:  35932 (+0.1%)   2680 (+0.3%)
2024-07-17 20:39:31 -05:00
Christopher Haster 0a3cb2dd3a Added filesystem-level info flags to lfsr_fs_stat
Thinking again of use cases, lfsr_fs_gc provides the perfect API to call
in the background to perform any pending filesystem work. But what if
there's no work to be done? Sure we could just spin forever, but that's
a waste. Especially on devices that can turn on sleep modes to save
power.

To help with this, this commit adds a set of flags to struct lfs_fsinfo
that signals when lfsr_fs_gc can accomplish work:

  LFS_I_INCONSISTENT     = 0x01, // Filesystem needs mkconsistent to write
  LFS_I_NEEDSUPGRADE*    = 0x02, // Filesystem needs an upgrade to write
  LFS_I_CANLOOKAHEAD     = 0x04, // Lookahead buffer is not full
  LFS_I_CANPREERASE+     = 0x08, // Pre-erase buffer is not full
  LFS_I_UNCOMPACTED      = 0x10, // Filesystem may have uncompacted metadata
  LFS_I_NEEDSREPAIRMETA+ = 0x20, // Filesystem contains damaged metadata
  LFS_I_NEEDSREPAIRDATA+ = 0x40, // Filesystem contains damaged data

  *Hypothetical
  +Planned

This flags field also provides a useful place internally to store other
filesystem-related flags, currently LFS_F_ORPHANS, though this may be
expanded in the future.

These flags allow users to know exactly what work can/needs to be done
for the filesystem to make progress:

- LFS_I_INCONSISTENT => LFS_GC_MKCONSISTENT or lfsr_fs_mkconsistent
- LFS_I_CANLOOKAHEAD => LFS_GC_LOOKAHEAD

- LFS_I_UNCOMPACTED => LFS_GC_COMPACT

  The one is new!

  If we complete a compaction-traversal without any mutation, we know
  all mdirs/btree nodes have been compacted and future traversals won't
  accomplish anything. Of course, we need to clear this bit on
  filesystem mutation.

  Right now we just pessimistically assume the filesystem is uncompacted
  during mount, but in theory we can also figure this out during our
  initial mount traversal.

- LFS_GC_CKMETA/CKDATA?

  LFS_GC_CKMETA and LFS_GC_CKDATA are a bit trickier. In theory,
  LFS_GC_CKMETA/CKDATA will always accomplish something, since time is
  the only ingredient necessary to introduce bit errors.

  So there isn't really a reasonable flag here. It's entirely up to the
  user to decide when to do an LFS_GC_CKMETA/CKDATA traversal.

Code changes:

           code          stack
  before: 35740           2672
  after:  35880 (+0.4%)   2672 (+0.0%)
2024-07-17 18:58:06 -05:00
Christopher Haster d18633e4e8 Tweaked lfsr_fs_gc to imply LFS_GC_MTREEONLY based on flags
LFS_GC_MTREEONLY is a rather niche/littlefs-specific flag, and we
probably shouldn't expect users to know when to use it. So now we
automatically switch to LFS_GC_MTREEONLY mode in lfsr_fs_gc if it is
sufficient for accomplishing all pending gc work.

Though currently the only traversal that can be LFS_GC_MTREEONLY is
LFS_GC_MKCONSISTENT...

Note that LFS_GC_MTREEONLY can still be explicitly provided, as it does
change the behavior of LFS_GC_COMPACT and LFS_GC_CKMETA (and combining
LFS_GC_MTREEONLY with LFS_GC_LOOKAHEAD/LFS_GC_CKDATA still asserts).

This adds a bit of code:

           code          stack
  before: 35728           2672
  after:  35740 (+0.0%)   2672 (+0.0%)
2024-07-17 18:49:08 -05:00
Christopher Haster 33804cee91 t: Moved eot state changes into lfsr_mtree_gc
This just deduplicates the post-traversal work (clearing orphan flags,
marking lookahead as free, etc) that every gc-esque function needs to do
on a succesful traversal, into the common lfsr_mtree_gc function.

This saves a bit of code:

           code          stack
  before: 35756           2672
  after:  35728 (-0.1%)   2672 (+0.0%)
2024-07-17 18:41:33 -05:00
Christopher Haster fc486ca4f7 Reworked lfsr_fs_gc to be incremental
Thinking about use case a bit, most lfsr_fs_gc will be to perform
background work, and can benefit from being incremental.

We already support incremental gc and all the mess associated with
traversal invalidation via the traversal API, so we might as well expose
this through lfsr_fs_gc.

The main downside is that we need to store an lfsr_traversal_t object
somewhere, which is not exactly a cheap struct. I was originally
considering limiting incremental gc to the traversal API for this
reason, but I think the value add of an incremental lfsr_fs_gc is too
compelling... Though we really should add a compile-time option
(LFS_NO_GC? LFS_NO_INCRGC?) to allow users to opt-out of this RAM cost
if they're never going to call this function.

Oh, and lfs_t also becomes self-referential, which might become a
problem for higher-level language users...

---

The incremental behavior of lfsr_fs_gc can be controlled by the new
gc_steps config option. This allows more than one step to be performed
at a time, which may allow for more progress when intermixed with
write-heavy filesystem operations. Setting gc_steps=-1 performs a full
traversal every call, which guarantees always making some amount of
progress.

This adds a bit of code, since we now need to check for/resume existing
traversals. But the real cost is the added RAM to lfs_t, which is
unfortunately wasted if you never call lfsr_fs_gc:

          code           stack          lfs_t
  before: 35708           2672            164
  after:  35756 (+0.1%)   2672 (+0.0%)    296 (+80.5%)
2024-07-17 18:08:32 -05:00
Christopher Haster a0e0ea2081 Switched to asserting only-known flags
So instead of asserting on explicitly disallowed flags, we assert that
all passed flags are in the relevant flag set.

This is a bit safer.
2024-07-17 18:08:11 -05:00
Christopher Haster 0ee6d73560 (Re)implemented lfsr_fs_gc
This just provides a simple, easy-to-call, wrapper over the new
traversal API:

  int lfsr_fs_gc(lfs_t *lfs, uint32_t flags);

The main difference from its previous incarnation, is that lfsr_fs_gc
now takes a flags argument to indicate exactly what gc operations to
perform. This gives the user more control, and may also make the API
more robust towards adding new features:

  LFS_GC_MTREEONLY    = 0x0010, // Only traverse the mtree
  LFS_GC_MKCONSISTENT = 0x0020, // Make the filesystem consistent
  LFS_GC_LOOKAHEAD    = 0x0040, // Populate lookahead buffer
  LFS_GC_COMPACT      = 0x0080, // Compact metadata logs
  LFS_GC_CKMETA       = 0x0100, // Check metadata checksums
  LFS_GC_CKDATA       = 0x0200, // Check metadata + data checksums
  LFS_GC_REPAIRMETA+  = 0x0400, // Repair metadata blocks
  LFS_GC_REPAIRDATA+  = 0x0800, // Repair metadata + data blocks

  + Planned

Alternatively, gc_flags could have been added as a config option. But
making gc_flags a function argument matches other flag APIs (open
mainly), and is slightly more flexible in that it allows a system to do
different gc operations in different system states (though this could
also be accomplished with the hypothetical lfsr_fs_gccfg, which would
probably be good to add anyways).

Worst case, defining a system-wide define that you always pass to
lfsr_fs_gc accomplishes roughly the same thing.

---

This adds a bit more code, mainly to check if we actually need to
traverse, and to make sure traversals accomplish all of the requested
work.

           code          stack
  before: 35448           2680
  after:  35708 (+0.7%)   2672 (-0.3%)

Curiously it also saved a bit of stack, which is a bit silly given this
commit is purely code addition. Apparently something in lfs_alloc and
lfsr_fs_gc is shared, getting uninlined, and messing with the stack
measurement. lfs_alloc is quite sensitive to stack changes after all.
2024-07-17 17:10:20 -05:00
Christopher Haster 0e34c46608 Dropped implicit multi-bit flags
- LFS_O_FLUSH   0x0040 -> 0x0040
- LFS_O_SYNC    0x00c0 -> 0x0080
- LFS_T_CKMETA  0x0100 -> 0x0100
- LFS_T_CKDATA  0x0300 -> 0x0200

This is just simpler and should avoid any surprises for both devs and
users.

This has no impact on code size:

           code          stack
  before: 35448           2680
  after:  35448 (+0.0%)   2680 (+0.0%)
2024-07-10 23:59:58 -05:00
Christopher Haster e4b6496e09 t: Merged mkdirty + clobber => lfsr_omdir_mkdirty
Saves a bit of typing and a bit of code:

           code          stack
  before: 35488           2680
  after:  35448 (-0.1%)   2680 (+0.0%)
2024-07-10 16:05:56 -05:00
Christopher Haster ee990938e1 t: Adopted bit swapping to save dirty/mutated state
The trick is realizing the dirty/mutated bits are redundant when
mutating. So instead of saving the current dirty bit on the stack, we
can just swap the mutated/dirty bits temporarily.

The bit swapping xor trick comes from Sean Eron Anderson's infamous bit
twiddling hacks collection, unfortunately it doesn't seem possible to
avoid the hardcoded bit locations...

Saves a bit of code:

           code          stack
  before: 35504           2680
  after:  35488 (-0.0%)   2680 (+0.0%)
2024-07-09 14:24:06 -05:00
Christopher Haster 86db4330d8 t: Rerouted lfsr_fs_mkconsistent through LFS_T_MKCONSISTENT
The hope here was that deduplicating the lfsr_fs_fixorphans/
lfsr_mtree_gc mtree traversal would result in code savings since we'd
end up with one shared code path.

Unfortunately in practice this didn't work out:

           code          stack
  before: 35484           2680
  after:  35504 (+0.1%)   2680 (+0.0%)

Still, it is nice to have one shared code path, because that means fewer
corner cases that could break.
2024-07-09 13:42:48 -05:00
Christopher Haster f191d25dcc t: Tweaked mutation flags, made sure zombie flag is cleared
lfsr_mdir_fixorphans always ends up setting the zombie flag, which is a
bit annoying. If we don't clear the zombie flag, lfsr_remove/lfsr_rename
may cause repeated mids during traversal, which probably won't break
anything, but isn't great...

Code changes:

           code          stack
  before: 35480           2680
  after:  35484 (+0.0%)   2680 (+0.0%)
2024-07-09 00:05:14 -05:00
Christopher Haster 0e2a909148 t: Reverted reverted most of LFS_T_MKCONSISTENT
After thinking about this for a bit, there are some compelling
motivations for including an incremental LFS_T_MKCONSISTENT:

- Being able to run incremental LFS_T_MKCONSISTENT traversals in
  parallel with read-only operations is actually quite enticing.

  The only complicated part is maintaining the invalidatable traversal
  state, which already exists with lfsr_traversal_t (except the
  annoying LFS_F_MUTATED bit).

- While it's not really effective to combine LFS_T_MKCONSISTENT and
  LFS_T_LOOKAHEAD traversals, it _is_ possible to combine
  LFS_T_MKCONSISTENT with LFS_T_COMPACT, LFS_T_CKMETA,
  LFS_T_REPAIRMETA (future), etc.

  Really, LFS_T_LOOKAHEAD is the odd one out.

- Making LFS_T_MKCONSISTENT incremental means all filesystem-level
  traversals (except lfsr_mount) can be run incrementally. Which is a
  nice feature to have when O(n = entire fs) risks being very long
  running.

The main downside of LFS_T_MKCONSISTENT (and LFS_T_COMPACT, etc) is that
attempting to run it immediately after mount will likely recursively
trigger a lookahead scan to satisfy block allocation requests -- which
will block the current thread for the duration of the lookahead scan.
But this seems to be more a problem of LFS_T_LOOKAHEAD interacting with
other traversals poorly.

Fortunately, long term, the current plan is to replace the lookahead
buffer with an on-disk block map on disks where the lookahead scan is a
bottleneck. If this gets implemented the problem goes away.

So re-reverting this for now. Worst case we can always re-re-revert this
again in the future. There is already a working implementation, so might
as well see where it goes...

Supporting incremental LFS_T_MKCONSISTENT does add a bit of a code
cost, but there is still some room for deduplicating lfsr_mtree_gc +
lfsr_fs_mkconsistent, which may be interesting:

           code          stack
  before: 35232           2680
  after:  35480 (+0.7%)   2680 (+0.0%)
2024-07-08 23:34:44 -05:00
Christopher Haster ffe8c1e820 t: Reverted most of LFS_T_MKCONSISTENT, just check for new grms/orphans
Checking for orphans + other traversal work turned out to mesh much
worse than originally thought:

- Adjusting mids and being able to drop mdirs mid-traversal complicates
  traversal quite a bit and has potential to hide difficult to reproduce
  bugs.

- Implementing incremental mkconsistent requires it's own separate state
  to detect mutation correctly since LFS_T_MKCONSISTENT and
  LFS_T_LOOKAHEAD are invalidated by slightly different things.

- If hasorphans=true, we're likely going to find orphans and clobber the
  traversal. So it's not really worth trying to opportunistically prove
  there are no orphans while doing other traversal operations.

- We don't really want to traverse the mroot/mtree during mkconsistent,
  which makes deduplicating these two functions a bit tricky. Doable,
  but annoying.

- grms don't involve traversals and are their own separate awkward step
  already.

Combine this with the fact that needing to scan for orphans should be
relatively rare in practice -- requiring either a powerloss or a
complicated set of file operations with at minimum 3 desynced files --
and parallel orphan checking starts to look like more trouble than it's
worth...

Instead, we now only check if the hasorphan bit has been set, and if it
has been we just call lfsr_fs_mkconsistent directly. This does a full
traversal in a single step, but at least makes it so traversal +
LFS_T_MKCONSISTENT in a background thread will do any necessary
janitorial work.

This saves a bit code:

           code          stack
  before: 35480           2680
  after:  35232 (-0.7%)   2680 (+0.0%)
2024-07-08 23:34:13 -05:00
Christopher Haster 24f5d05bfb Fixed lfsr_fs_fixorphans not fixing orphans
Turns out things get a bit tricky when mdirs are dropped while iterating
over the mtree.

This was actually broken quite a bit before traversal-related changes,
probably during some mtree refactor, but went unnoticed since no test
actually checked that lfsr_fs_fixorphans did what it said it did.

At least the new test_forphans_cleanup* tests should prevent this from
regressing again in the future.

Code changes:

           code          stack
  before: 35472           2680
  after:  35480 (+0.0%)   2680 (+0.0%)
2024-07-08 13:18:13 -05:00
Christopher Haster 12511468ef t: Implemented LFS_T_MKCONSISTENT
What seemed like a simple tweak to lfsr_fs_fixorphans, integration into
lfsr_mtree_gc, turned out to be surprisingly annoying.

- We need an additional traversal flag, LFS_F_MUTATED, in order to know
  if we intentionally modified the filesystem. This is different from
  LFS_F_DIRTY in that we don't invalidate orphan scans:

  - LFS_F_DIRTY   => invalidate lookahead + orphans
  - LFS_F_MUTATED => invalidate lookahead

- We need to break up lfsr_fs_fixorphans to expose lfsr_mdir_fixorphans,
  which is probably a good thing for readability.

  The interactions with each mdir being associated with a given mid is
  not great though, and requires a bit of awkward mid shuffling.

- Unlike LFS_T_COMPACT, LFS_T_MKCONSISTENT introduces more complicated
  mid changes, and makes it so mdirs can now be dropped in the middle of
  traversal.

  This messes with our internal lfsr_mtree_traverse -> lfsr_mtree_gc
  control flow, and means a single lfsr_traversal_read call may process
  an unbounded number of blocks in rare cases with lots of orphans.

But the good news is things are working, and lfsr_traversal_read with
LFS_T_MKCONSISTENT can scan for orphans in parallel with other traversal
operations.

Adds a bit of code:

           code          stack
  before: 35220           2680
  after:  35472 (+0.7%)   2680 (+0.0%)
2024-07-08 12:55:50 -05:00
Christopher Haster 4d86c90f1b t: Dropped LFS_T_EXCL/LFS_I_DIRTY
The tests highlighted that the LFS_I_DIRTY flag in lfsr_tinfo approach
is insufficient. Consider what happens if our filesystem is mutated
while traversing the last mdir:

1. Traversal traverses last mdir, populate blocks, return first block

2. Filesystem mutated, maybe mdir was compacted, clobbers traversal and
   sets LFS_I_DIRTY

3. Traversal return LFS_ERR_NOENT immediately, last block never
   returned (and out of date), LFS_I_DIRTY never returned

Not only do we miss the LFS_I_DIRTY flag, but we completely miss the
last block in the mdir pair without any warning.

This is _not_ a problem for the actual lookahead buffer, since we still
internally check the LFS_I_DIRTY flag before marking it as complete, but
it is an issue for any external logic that depends on the traversal
being complete...

---

We could revert to LFS_T_EXCL, but, to be honest, I just really don't
know a good name for this flag...

LFS_T_EXCL is a bad name because it conflicts with LFS_O_EXCL. These
flags have very different behaviors, which risks confusing users, and
risks potential name conflicts down the line if we ever want
LFS_T_EXCL-esque semantics for open dirs/files (not unreasonable, though
quite fancy).

My current best contender is LFS_T_WATCH, but while scratching my head
on this, I starting to wonder why we're even providing LFS_T_EXCL in the
first place...

We err on the side of forcing users to implement filesystem-external
features themselves when possible elsewhere, and LFS_T_EXCL technically
_can_ be implemented entirely outside of the filesystem. Though to be
fair it is quite annoying/tedious.

It's not like there's any equivalent feature for dir/file reads anyways.
And a background thread calling lfsr_traversal_read with LFS_T_LOOKAHEAD
will still _eventually_ make progress, even if it takes a bit longer.

Don't get me wrong, I understand it is significantly easier to implement
this inside the filesystem than outside. But it's also easier to
implement this later than right now. And if we implement this later,
hopefully we'll have a better idea what exactly will be useful for
users.

---

Removing LFS_T_EXCL/LFS_I_DIRTY has no real impact on code cost. We were
really just exposing internal logic that we need for lookahead
correctness anyways:

           code          stack
  before: 35224           2680
  after:  35220 (-0.0%)   2680 (+0.0%)
2024-07-08 08:59:26 -05:00
Christopher Haster 23c82bd7e5 t: Replaced LFS_T_EXCL with LFS_I_DIRTY flag in lfsr_tinfo
This just forwards the internal LFS_I_DIRTY flag to the user via the
lfsr_tinfo flags field.

Benefits of this approach:

- Gives the user more flexibility on what to do if the filesystem is
  modified, maybe you want to keep traversing depending on some other
  logic.

- Can eventually add other flags to tinfo.flags, such as
  LFS_I_COMPACTED, LFS_I_REPAIRED, LFS_I_INCONSISTENT, etc.

- Avoids confusion around the very different behaviors of LFS_O_EXCL and
  LFS_T_EXCL.

  I tried to come up with a better name (maybe LFS_T_WATCH?) but it was
  a bit of a struggle... Switching to a flags approach sidesteps the
  issue.

- Can drop the LFS_ERR_BUSY error code for now.

Code changes were fairly insignificant:

           code          stack
  before: 35244           2680
  after:  35224 (-0.1%)   2680 (+0.0%)

The only concern is that the tests highlighted it's possible for our
flag scheme to miss mutation if it happens after/during the last set of
blocks... Not sure how to handle this yet...
2024-07-08 08:59:10 -05:00
Christopher Haster 950124146c Adopted implied LFS_O_FLUSH bit pattern in LFS_O_SYNC
LFS_O_SYNC always implies LFS_O_FLUSH, otherwise what exactly are you
syncing? Making this explicit in the bit pattern should hopefully make
this clear for curious users, though lfsr_file_flush would be called
anyways because of how lfsr_file_sync is implemented.

This also moves the LFS_O_DESYNC bit pattern around so SYNC/FLUSH are
neighbors. SYNC/DESYNC may seem related, but in lfsr_file_open they
actually are quite different:

  LFS_O_FLUSH   0x0040  ---- ---- -1-- ----
  LFS_O_SYNC    0x00c0  ---- ---- 11-- ----
  LFS_O_DESYNC  0x0100  ---- ---1 ---- ----

Code changes, mostly just noise from moving bits around:

           code          stack
  before: 35228           2680
  after:  35244 (+0.0%)   2680 (+0.0%)
2024-07-05 16:20:54 -05:00
Christopher Haster b7165d51e6 t: Renamed LFS_T_CK -> LFS_T_CKDATA, kept implied LFS_T_CKMETA
It still doesn't make sense to check data without checking metadata, but
keeping this named LFS_T_CKDATA should hopefully clarify what it does
differently from LFS_T_CKMETA.

This implication is also now encoded in the bit pattern:

  LFS_T_CKMETA  0x0100  ---- ---1 ---- ----
  LFS_T_CKDATA  0x0300  ---- --11 ---- ----

In theory a clever user could force only the CKDATA bit to be set, and
such a configuration would _probably_ work fine, but it won't be
supported just to cut down on possible configurations to test.

No code changes:

           code          stack
  before: 35228           2680
  after:  35228 (+0.0%)   2680 (+0.0%)
2024-07-05 16:06:59 -05:00
Christopher Haster c258420dd0 t: Dropped mtraversal=traversal alias
We don't really need a second type anymore, and having one just risks
confusing new users.
2024-07-05 15:31:31 -05:00
Christopher Haster 2e6a5be4e3 t: Dropped mtinfo/btinfo, just use data/bptr for everything
It's probably a bad reason, but this avoids wasting too much time
figuring out how to name things.

Now most traversal functions return an lfsr_tag_t + lfsr_bptr_t pair,
which is enough to describe the current relevant traversal objects:

  tag=LFSR_TAG_MDIR   => (lfsr_mdir_t*)bptr.data.u.buffer
  tag=LFSR_TAG_BRANCH => (lfsr_rbyd_t*)bptr.data.u.buffer
  tag=LFSR_TAG_DATA   => bptr.data
  tag=LFSR_TAG_BPTR   => bptr

This would be a bit better if lfsr_data_t's buffer field was a void*,
but that would mess with byte-level arithmetic, which is more common
with lfsr_data_ts.

This also adopts the fragmented/optional out-params used elsewhere in
the codebase. I thought this would add quite a bit more stack cost,
since we need redundant tags/bptrs to make lfsr_mtree_traverse/
lfsr_mtree_gc work, but surprisingly not:

           code          stack
  before: 35256           2680
  after:  35228 (-0.1%)   2680 (+0.0%)

It seems we make up the extra stack cost of redundant tags/bptrs by
giving the compiler more stack-alloc flexibility, tighter per-function
return types, and opting-out of tags/bptrs in most low-level traversals:
lfs_alloc mainly.

But if the fragmented/optional out-params is net harmful for code/stack
size, we should reconsider the pattern system-wide. This does probably
deserve a second look in the future...
2024-07-05 15:31:29 -05:00
Christopher Haster 96834c2460 t: Renamed mtraversal states LFSR_MTRAVERSAL_* -> LFSR_MTSTATE_* 2024-07-05 15:31:26 -05:00
Christopher Haster f783e6f519 t: Dropped const from btree/bshrub/mtree traverse functions
This could go either way, it's a case of the classic C strchr type
conundrum.

But unlike iteration, we're more likely to mutate things when doing a
full traversal, so requiring everything to be mutable makes a bit more
sense.

Note that even readonly operations, fetchck for example, need access to
a mutable rbyd struct.

No code changes:

           code          stack
  before: 35256           2680
  after:  35256 (+0.0%)   2680 (+0.0%)
2024-07-05 15:31:24 -05:00
Christopher Haster 3c7b462659 t: Changed mtinfo/btinfo to refer to mdirs/rbyds by pointer
This solves the issue of multiple mdirs/rbyds in lfsr_mtree_gc, where
it's easy for traversal state to fall out of sync when mutating parts of
the filesystem.

Is it good design, with self-referential pointers making everything more
entangled? Not sure!

This saves a bit of stack, but adds a bit of code, which makes sense,
pointer chasing can be costly. But both of these changes are well below
the compiler noise floor:

           code          stack
  before: 35228           2688
  after:  35256 (+0.1%)   2680 (-0.3%)
2024-07-05 15:31:21 -05:00
Christopher Haster 181b08c723 Added Added lfsr_omdir_isbshrub to generalize obshrub checks
We probably want a common function to tell us if a given omdir is also
an obshrub.

Conveniently all types with bshrubs happen to share a common bit, and
this pattern will probably continue for a while:

  // user facing
  LFS_TYPE_REG          1  ---1 <-- bshrub
  LFS_TYPE_DIR          2  --1-
  LFS_TYPE_SYMLINK*     3  --11 <-- bshrub

  // internal
  LFS_TYPE_BOOKMARK     4  -1--
  LFS_TYPE_TRAVERSAL    5  -1-1 <-- bshrub

  * Hypothetical

Maybe this is overspecialized, but at least our tests will break quite
quickly if this ever turns out not to be true...

Code changes:

           code          stack
  before: 35256           2688
  after:  35228 (-0.1%)   2688 (+0.0%)
2024-07-05 15:31:17 -05:00
Christopher Haster c316270ebb Added lfsr_obshrub_t for generalized tracked bshrubs
So now files and traversals contain several nested structs:

  file     <-- lfsr_file_t
  file.o   <-- lfsr_obshrub_t
  file.o.o <-- lfsr_omdir_t

This gets a bit ugly, but it's really the only way to make the compiler
happy when also with C's annoying strict aliasing rules.

This also makes lfsr_traversal_t a simple alias of lfsr_mtraversal_t,
with lfsr_mtraversal_t now including all of the obshrub/omdir state.
This simplifies things internally, and allows lfsr_mtree_gc to assert on
opened-list enrollment, but risks increased stack cost for all of the
unused fields.

Fortunately this stack cost turned out to not be that significant:

           code          stack
  before: 35264           2680 (+0.0%)
  after:  35256 (-0.0%)   2688 (+0.3%)
2024-07-05 15:31:11 -05:00
Christopher Haster 8d7e71d961 t: Added lfsr_fs_mkdirty to mark all traversals as dirty
Does what it says on the tin.

This simplifies lfsr_omdir_clobber, and can be used in more places. It's
a bit more flexible than implicitly mkdirtying in lfs_alloc_ckpoint, but
does add another function call.

But thanks to better code reuse this ends up saving a bit of code:

           code          stack
  before: 35304           2688
  after:  35264 (-0.1%)   2680 (-0.3%)
2024-07-03 01:16:07 -05:00
Christopher Haster 8da3a06121 t: Clobber traversals at the omdir level, rely on state machine
This brings back clobbering individual omdirs, so modifying an unsynced
file should leave all other traversals intact. This is the most precise
level of clobbering that I think is reasonable to implement.

This means we should be able to, say, check all currently-committed
checksums while writing to unsynced files at the same time. Which might
be useful? Maybe?

To make this work, lfsr_traversal_clobber now relies on the current
traversal state to know how to clobber correctly. This is more verbose,
but likely safer/more flexible.

Curiously, this actually ended up saving a bit of code, which is a bit
surprising:

           code          stack
  before: 35356           2688
  after:  35304 (-0.1%)   2688 (+0.0%)

Maybe manipulating the state machine directly gives the compiler more
info to work with? Not sure.
2024-07-03 00:18:18 -05:00
Christopher Haster d8eedf052e t: Moved opened traversal clobbering into lfsr_file_close
Since we're clobbering at the mid-level now, our mtraversals can only
ever point to unsynced reg file handles.

This means we can limit traversal clobbering to lfsr_file_close, and
move it out of the common/simple lfsr_omdir_close.

Look like any code changes canceled out perfectly:

           code          stack
  before: 35356           2688
  after:  35356 (-0.0%)   2688 (+0.0%)
2024-07-02 21:01:48 -05:00
Christopher Haster 7b8667d7df t: Allow root mutation during bshrub traversals
This adds an indirect pointer to lfsr_btraversal_t, so references to the
btree/bshrub root point to the actual btree/bshrub root rbyd struct.
This means if our bshrub root is mutated due to, say, mdir compaction,
this doesn't necessarily invalidate our btraversal.

But note this is strictly limited to bshrub roots. If you modify any
other part of the bshrub/btree, expect the traversal to be broken.

This means we can do whatever we want with mdirs and not worry about
invaliding bshrub traversals, which is quite nice! It also fixes our
failing bshrub-traversal-mutation tests.

This adds a bit of stack cost, but because we are moving fewer rbyd
structs around in lfsr_btree_traverse_, actually ends up saving a bit of
code. Though we are well below the compiler noise floor:

           code          stack
  before: 35368           2680
  after:  35356 (-0.0%)   2688 (+0.3%)
2024-07-02 19:50:46 -05:00
Christopher Haster 7fdf0b7d23 t: Switched back to mid-based traversal clobbering
Implementing gc_compact_thresh over bshrubs highlighted that it's really
not that difficult, and probably required, for traversal bshrubs to be
tracked correctly during mdir commits/compacts/splits/etc. And if we
track bshrubs across mdir commits, we might as well clobber traversals
at the mid level, allowing traversals to always reach btrees/bshrubs not
under active mutation.

One key thing to note: we should never be traversing a bshrub that is
not referenced elsewhere, either on-disk in an mdir or in-ram via an
opened file. So any compacted traversal bshrubs are not wasted prog
cycles.

This moves most of the clobbering logic back up into the high-level
functions (lfsr_remove/rename mainly), where we know which mids may be
clobbered.

This has a code cost, but it's really not all that much for more
thorough/correct filesystem traversals under mutation:

           code          stack
  before: 35268           2680
  after:  35368 (+0.3%)   2680 (+0.0%)

Unfortunately, lingering rbyd references in our btraversal structs are
still an issue, and some bshrub tests are failing... Though I do have
some ideas on how to fix this.
2024-07-02 18:20:14 -05:00
Christopher Haster 7f4384fa27 Flipped btree/bshrub logic so commit__ implicitly finds rbyd
This does a few things:

- Deduplicates bshrub/btree rbyd lookups when rbyd is not explicitly
  provided -- note this is by far the most common case.

- Moves the mid-level rbyd allocation out of the stack-hot-path.

  Assuming lfsr_mtree_gc is never in the stack-hot-path, which seems
  unlikely.

- Reduces pointer chasing in lfsr_btree_commit__, giving the compiler
  more flexibility + assumptions and hopefully allowing it to optimize
  better.

  This may also be disentangling several cross-layer struct references,
  which is probably a good thing.

The result is rather significant stack savings for what is a minor
refactor:

           code          stack
  before: 35440           2800
  after:  35268 (-0.5%)   2680 (-4.3%)

This doesn't quite get us back to pre-commit_-rbyd levels, but it's
pretty close:

                                   code          stack
  before commit_-rbyd:            34652           2640
  commit_-rbyd-explicit (before): 35440 (+2.3%)   2800 (+6.1%)
  commit_-rbyd-implicit (after):  35268 (+1.8%)   2680 (+1.5%)
2024-07-01 16:36:41 -05:00
Christopher Haster e2ec25e511 Tweaked btree/bshrub rbyd commit_ to accept any bid
This gets a bit muddled now with traversals mutating inner btree nodes
directly.

Except for some asserts, we can accept any bid in the relevant rbyd
here, and accepting any bid is better than accepting only one bid
(left-leaning) inconsistent with the rest of the btree API
(right-leaning)...

Code changes minimal:

           code          stack
  before: 35448           2800
  after:  35440 (-0.0%)   2800 (+0.0%)
2024-07-01 16:36:37 -05:00
Christopher Haster f3446abfa7 t: Implemented gc_compact_thresh over bshrub nodes
These aren't really different than btree nodes, except bshrubs need to
be enrolled in our opened list for commits to work.

Fortunately this is already true for explicit traversals, which are
currently the only traversals where we need to simultaneously mutate the
filesystem. This mainly just required adding additional checks for
LFS_TYPE_TRAVERSAL bshrubs, tests, and making sure traversal.bshrub is
never in an invalid state.

This continues to add code/stack cost for what is ultimately a
relatively niche feature:

           code          stack
  before: 35268           2776
  after:  35448 (+0.5%)   2800 (+0.9%)

Maybe btree/bshrub compactions should be disabled by default?
2024-07-01 16:36:37 -05:00
Christopher Haster 61ecc135dc t: Implemented gc_compact_thresh over btree nodes
Note, gc_compact_thresh over bshrubs is not yet implemented... That's
_another_ can of worms since we need to be able to commit to non-tracked
bshrubs somehow...

But at least this proves gc_compact_thresh over btrees is possible.

Now, if LFS_T_COMPACT is provided, any btree nodes > gc_compact_thresh
will be compacted during traversal/gc operations.

To make this work required a rather deep modification to the
lfsr_btree_commit/lfsr_bshrub_commit code paths to expose direct-rbyd
commit functions that can commit to arbitrary btree nodes:

- lfsr_btree_commit   - bid, attrs, attr_count
- lfsr_bshrub_commit  - bid, attrs, attr_count
- lfsr_btree_commit_  - bid, rbyd, rid, attrs, attr_count
- lfsr_bshrub_commit_ - bid, rbyb, rid, attrs, attr_count
- lfsr_btree_commit__ - bscratch, bid, rbyd, rid, attrs, attr_count

These are good to have, and will also be useful for implementing
metadata redundancy in the future.

Unfortunately, all of this comes at a significant code/stack cost:

           code          stack
  before: 34652           2640
  after:  35268 (+1.8%)   2776 (+5.2%)
2024-07-01 16:36:24 -05:00
Christopher Haster 4d06fc2e0e t: (Re)implemented gc_compact_thresh, at least over mdirs
lfs_fs_gc is still not reimplemented, but this is accessible through the
traversal API with LFS_T_COMPACT.

This is also the first traversal operation that can mutate the
filesystem, which brings its own set of problems:

- We need to set LFS_F_DIRTY in lfsr_mtree_gc now, which really
  highlights how much of a mess having two flag fields is...

  We do _not_ clobber in this case, since we assume lfsr_mtree_gc knows
  what it's doing.

- We can now commit to an mroot in the mroot chain outside of the normal
  mroot chain update logic.

  This is a bit scary, but should just work.

  The only issue so far is that we need to allow mdirs to follow the
  mroot during mroot splits if mid=-1, even if they aren't lfs_t's mroot
  mdir.

  This should now be decently tested with the new
  test_traversal_compact_* tests.

- It's easy for mtraversal's mdir and mtinfo's mdir to fall out of sync
  when mutating... Why do we have two of these?

The actual compaction itself is pretty straightforward: just mark as
unerased, eoff=-1, and call lfsr_mdir_commit with an empty commit. This
is now wrapped up in lfsr_mdir_compact.

Code changes:

           code          stack
  before: 34528           2640
  after:  34652 (+0.4%)   2640 (+0.0%)

Though the real hard part will be implementing gc_compact_thresh over
btree nodes...
2024-06-24 21:09:54 -05:00
Christopher Haster ff0271ecbe t: Renamed LFS_T_CKDATA -> LFS_T_CK, implies LFS_T_CKMETA
It really doesn't make sense to check data and not check metadata. We're
already traversing the metadata, so validating it adds very little
overhead, and how can we trust our data if we can't trust our metadata?

This renames LFS_T_CKDATA -> LFS_T_CK, which now also implies
LFS_T_CKMETA. This implication is done explicitly in lfsr_mtree_traverse
instead of doing anything fancy with flags.

Implying LFS_T_CKMETA also means one less configuration to support.

Code changes:

           code          stack
  before: 34524           2640
  after:  34528 (+0.0%)   2640 (+0.0%)
2024-06-24 00:21:37 -05:00
Christopher Haster b665ee3a8d Dropped underscores from bd cksum arguments
To hopefully hint that these are not pure-output pointers, unlike
underscore arguments elsewhere (though this isn't really an intentional
convention).

Cksums need to be initialized with zero, so that multiple prog/read
operations can chain cksum updates.

This has already tripped me up a couple times.
2024-06-24 00:13:05 -05:00
Christopher Haster 2f1d711902 t: Changed lfsr_mtree_traverse to operate on mdir+mtraversal
Separated out omdir/mdir and mtraversal. You still need to allocate an
mdir for mtraversal to work, but this avoids the extra cost of omdir's
linked-list.

To avoid _too_ many pointers, I duplicated the flags field into both
lfsr_traversal_t and lfsr_mtraversal_t. This is basically free since we
end up with a bunch of padding for mtraversal's state field, but comes
with the risk of getting confused when the two flag fields don't match
in the future.

I also merged the intermediary btype field into flags to avoid yet
another single-byte field, where it fits comfortably in 3-bits.

Note that the mdir can be uninitialized in cases where we don't need to
worry about traversal clobbering.

---

This has the same problems as separating out mdirs/bshrubs in bshrub
functions: more stack/code to move the multiple pointers around, but is
necessary to avoid strict aliasing issues. There's no way to represent
overlapping omdir/mdir/mtraversal struct in standard C99 otherwise.

The end result saves a bit of code, but adds a bit of stack:

           code          stack
  before: 34576           2632
  after:  34524 (-0.2%)   2640 (+0.3%)

Though these numbers may be close enough to the compiler noise floor to
not really care about...
2024-06-23 23:47:00 -05:00
Christopher Haster b383821a22 Rearranged things so high-level fs functions come last
This is how littlefs used to be organized, and I found it a bit easier
to navigate: low-level => go to front, high-level => go to end.

This also moves lfs_alloc immediately after the mtree logic, which is
sort of related, and before anything high-level.

lfs_init/lfs_deinit are also now immediately before lfs_mount/
lfs_unmount/lfs_format, which are closely intertwined.

This did actually affect our code cost, which is interesting. No logic
was changed, only moved:

           code          stack
  before: 34566           2632
  after:  34576 (+0.0%)   2632 (+0.0%)
2024-06-23 23:46:58 -05:00
Christopher Haster c934357d60 Rearranged things to group bshrub/btree logic together
There is a cyclic dependency between the bshrub and mdir logic, so
there's no obvious order, but grouping up bshrubs and btrees makes a lot
of sense since they share many low-level operations (lfsr_btree_commit_,
lfsr_btree_traverse_, etc).

Bshrubs really are just inlined btrees after all.

lfs.c is now roughly organized into three large sections:

1. Raw on-disk data structures (rbyds, btrees, bshrubs, etc)
2. The mess that is metadata (mtree, mdirs, etc)
3. High-level types/functions (files, dirs, mount, traversals, etc)

No code changes.
2024-06-23 23:46:58 -05:00
Christopher Haster 55d9f5c76a Changed bshrub functions to operate on mdir+bshrub directly
This gets a bit messy, since lfsr_bshrub_commit really requires the
bshrub to be enrolled in the opened mdir list to stage correctly.

To make this work, our internal SHRUBCOMMIT and SHRUBTRUNK attrs now
take a pointer to the active shrub, and assume it is followed by a
staging shrub in memory. This is a big hack/assumption that leaks
through lfsr_bshrub_commit, but it at gets the job done in our current
system.

Note some functions were renamed instead, these didn't really make sense
as pure-bshrub functions:

- lfsr_bshrub_readnext -> lfsr_file_readnext
- lfsr_bshrub_read -> lfsr_file_read_

---

The main reason for this is to comply with C99's strict aliasing rules,
which can be a real PIA sometimes.

We need to track a bshrub in lfsr_mtraversal_t, but we really don't want
to pay the RAM cost for an entire lfsr_file_t. The best option I've
found is to pass around multiple pointers to the relevant internal
structs (mdir+bshrub), but this adds a stack+code cost.

So far, strict aliasing is a net downside:

                         code          stack
  before:               34478           2624
  -fno-strict-aliasing: 34502 (+0.1%)   2616 (-0.3%)
  after:                34566 (+0.3%)   2632 (+0.3%)

But it's baked into the standard and we can't always rely on
-fno-strict-aliasing being available.
2024-06-23 23:46:58 -05:00
Christopher Haster d155750f14 t: Renamed LFS_T_CKMETADATA -> LFS_T_CKMETA
Have you ever tried to type "metadata"? So much left hand motion while
the right hand sits there with nothing to do.
2024-06-23 23:46:54 -05:00
Christopher Haster e84d2afd60 Renamed lfsr_opened_* -> lfsr_omdir_*
Been leaning towards this naming scheme. Now lfsr_omdir_* functions
match the lfsr_omdir_t type they operate on.

- Renamed lfs.opened -> lfs.omdirs
- Renamed lfsr_opened_isopen -> lfsr_omdir_isopen
- Renamed lfsr_opened_add -> lfsr_omdir_open
- Renamed lfsr_opened_remove -> lfsr_omdir_close
- Renamed lfsr_mid_isopen -> lfsr_omdir_ismidopen
2024-06-21 13:02:57 -05:00