Commit Graph

546 Commits

Author SHA1 Message Date
Christopher Haster 36eabb1c68 Moved test_incompat into test_mount
These really are mount tests, and moving them into test_mount means less
confusion when adding future mount_somewhat_incompat tests.
2024-07-27 00:47:45 -05:00
Christopher Haster b37bff377b Added mount-time LFS_M_FLUSH/SYNC
These simply imply LFS_O_FLUSH/SYNC on all open writable files.
LFS_M_SYNC is equivalent to MS_SYNCHRONOUS in Linux/etc, while
LFS_M_FLUSH is just provided for consistency.

As pure conveniences, these may seem a bit out of scope for littlefs,
except they are _very_ cheap:

           code          stack
  before: 36356           2664
  after:  36356 (+0.0%)   2664 (+0.0%)

Ok, they're not _completely_ free! It just turns out they cost 8 bytes,
and a bit of simplification around flag checking in lfsr_mount saved
8 bytes:

                  code          stack
  before:        36356           2664
  m_flush/sync:  36364 (+0.0%)   2664 (+0.0%)
  mount-no-mask: 36356 (+0.0%)   2664 (+0.0%)
2024-07-27 00:47:45 -05:00
Christopher Haster d79e4ae455 Added LFS_O_CKMETA/CKDATA flags
These flags just call lfsr_file_ckmeta/ckdata under the hood, but make
it very easy to check metadata/data when opening a file. As an extra
plus they implicitly close the file on failure, so might make cleanup
easier.

Of course, everything has a cost:

           code          stack
  before: 36368           2664
  after:  36424 (+0.2%)   2664 (+0.0%)

These also ruin my previous "you don't pay for what you don't call"
assertion, since runtime flags unfortunately always pull in code.

We should add a compile-time switch for these evntually.
2024-07-27 00:47:45 -05:00
Christopher Haster e2c238c30d Added lfsr_file_ckmeta/ckdata
These are basically the same as lfsr_fs_ckmeta/ckdata but limited to a
single file. They may be useful when you need to validate a file but
don't want to bother validating the entire filesystem:

  // Check a file for metadata errors
  int lfsr_file_ckmeta(lfs_t *lfs, lfsr_file_t *file);

  // Check a file for metadata + data errors
  int lfsr_file_ckdata(lfs_t *lfs, lfsr_file_t *file);

I've also added test_ck to test these and added some more
lfsr_fs_ckmeta/ckdata tests there. These currently just test simple
full-block clobbering, but we should eventually test more interesting
error patterns.

Unfortunately lfsr_file_ckmeta/ckdata can't reuse the internal
lfsr_mtree_traverse in quite the same way lfsr_fs_ckmeta/ckdata can, so
they're actually a bit more expensive. Though keep in mind with
link-time gc you won't pay the cost unless you call these functions:

           code          stack
  before: 36024           2696
  after:  36368 (+1.0%)   2664 (-1.2%)

Oh, and the multiple calls to lfsr_btree/bshrub_traverse apparently
uninlined it out of lfsr_mtree_traverse, saving the stack cost in the
stack hot-path... Yay?
2024-07-27 00:47:45 -05:00
Christopher Haster 0893c1f6be Increased internal flags 16 bits -> 32 bits
If we add CKMETA/CKDATA and eventually REPAIRMETA/REPAIRDATA to the file
open flags, we'll end up with 17 flags total (13 user-facing,
4 internal), which is a bit (heh) too much for a 16-bit flags field!

There are a few ways to solve this, dropping features for one, instead
I've decided to expand the fields flag to 32-bits. Fortunately this was
already the field size for all user-facing fields.

To avoid a RAM increase, I've also shoved the opened-file types and
traversal tstates into the same field.

We have various flags in quite a few places now, here's how
everything fits together:

              8     8     8     8
            .----++----++----++----.
            .----..---..--..-------.
  o_flags:  |type|| f ||t ||   o   |
            |----||---|:--:'-------'
            |----||---|:--:--------.
  d_flags:  |type|| f |:  :        |
            |----||---|:--:--------'
            |----||---|:--'--..----.
  t_flags:  |type|| f ||  t  ||tstt|
            '----''---'|-----|'----'
            .----------|-----|-----.
  gc_flags: |          |  t  |     |
            '----------|-----|-----'
            .-----.---.|-----|.----.
  m_flags:  |     | m ||  t  || m  |
            '-----|---|'-----'|----|
            .----.|---|-------|----|
  i_flags:  | i  || m |       | m  |
            '----''---'-------'----'

Unfortunately, using the full 32-bit flag space highlights that C99's
enum types are kind of garbage...

In C99 enums are strictly signed ints, which means attempting to use
them for 32-bit bit fields overflows. There is no way around this so
I've switched our flag definitions to #defines.

I've kept types as enums for now but I'm keeping my eye on them...

---

The tradeoff of merging the type/btype/tstate/flags fields is that it
takes more code to extract/encode the various subfields. Since these
fields our heavily used in our codebase, this really adds up:

           code          stack
  before: 35888           2696
  after:  36048 (+0.4%)   2696 (+0.0%)

At least in theory the type fields can be optimized to a byte load, but
not btype/tstate. Also accessing bits in higher positions may be adding
cost.
2024-07-27 00:46:53 -05:00
Christopher Haster 35db3bc97f t: Dropped btree node compaction
After thinking about this for a while, btree node compaction is
subtlety different from mdir compaction, less valuable, and adds more
risk:

- Unlike mdirs, btree node compaction will always allocate a new
  block, leading to a higher chance of alloc failure.

- Btree node compaction also always requires additional writes to
  propagate btree changes, whereas mdir compaction is usually
  self-contained unless it triggers a relocation. If btree nodes are
  mostly full this risks being counter-productive.

- Btree node compaction requires a full tree traversal, whereas mdir
  compaction requires only traversing the mtree. Though you can always
  force mtree-only traversal manually with LFS_GC_MTREEONLY.

- Btrees/bshrubs are also more likely to be "cold storage", that is it
  probably won't be uncommon to create long-lived read-only btrees as a
  part of files. Compacting these btrees can actually be counter-
  productive as it can encourage splitting.

- Btrees/bshrubs are also more likely to be one use, and discarded as a
  file is truncated and rewritten. Compacting btree nodes in this case
  is a waste of erase cycles.

And since btree node compaction also introduces a lot of complexity/risk
of bugs, I'm going to drop this for now and limit LFS_GC_COMPACT to only
compacting mdirs. At least this tested implementation will live in the
history and can always be reintroduced in the future if it becomes a
wanted feature.

---

As is usually the case, doing less work ends up with less code:

           code          stack
  before: 36292           2704
  after:  35888 (-1.1%)   2696 (-0.3%)

Note this still keeps the rbyd-specific commit logic necessary for
committing to specific btree nodes, even though btree node compaction
was the only current use case. This should eventually be useful for
metadata repair. Hopefully const-propagation can minimize the cost, but
realistically this means we're probably leaving some code savings on the
table.
2024-07-24 13:58:26 -05:00
Christopher Haster 15090e5dcf gc: Also restart gc if lookahead + mutated/dirty
There is really no reason to continue lookahead traversals if our
filesystem has been mutated. Clearing the flag and restarting in this
case is more likely to make progress.

Note that it's worth continuing for all of the other current gc flags:

- LFS_GC_MKCONSISTENT - Except maybe for mkconsistent. We can't actually
  make progress, since we can't prove the filesystem is free of orphans,
  but it's beneficial to keep traversing and clearing orphans in case of
  other traversal flags that mutation would force a second traversal
  anyways.

  Continuing mkconsistent traversals also spreads out orphan cleanup a
  bit better, instead of just repeatedly cleaning up the first couple
  mdirs when under heavy contention.

  But to be honest, the chance of mutation that still leaves the
  filesystem with orphans is just so low that it's not worth doing
  anything. mkconsistent only needs to traverse the mtree anyways...

- LFS_GC_COMPACT - Like mkconsistent, compacting traversals are worth
  continuing for better mtree coverage under heavy contention.

  We will need a second pass to prove we compacted everything anyways,
  so might as well try to get as much mutation done as possible in the
  current traversal.

- LFS_GC_CKMETA/CKDATA - Continuing ckmeta/ckdata traversals provides
  better mtree coverage under heavy contention.

  This is much more important for CKMETA/CKDATA than the others, because
  _eventually_ checking every block for errors is more valuable than
  proving anything.

This adds some code, but the use of flags here is quite valuable for
expressing complex constraints like this cheaply:

           code          stack
  before: 36228           2680
  after:  36240 (+0.0%)   2680 (+0.0%)
2024-07-20 01:27:45 -05:00
Christopher Haster 6cf78527b4 Reverted gc-restart on flag change
Thinking about this more, we probably don't want to entangle
lfsr_fs_mkconsistent/ckmeta/etc and lfsr_fs_gc:

- lfsr_fs_ckmeta/ckdata are readonly and don't need to clobber
  traversals. The system can make more progress if these use separate
  states.

- We already need a bit of code to force traversals to restart for
  lfsr_fs_ckmeta/ckdata, so these already aren't simple wrappers.

- lfsr_fs_mkconsistent should also probably not invalidate gc traversals
  when the filesystem is already consistent. It is called by... checks
  notes... every function that writes to disk.

  This could be fixed in lfsr_fs_mkconsistent, but it'd be pretty close
  to just calling lfsr_mtree_gc...

- We don't really benefit from reusing the gc traversal state.
  lfsr_fs_mkconsistent/ckmeta/etc aren't on the stack hot-path, so the
  stack usage is more-or-less free (though I realize this depends on
  what functions are called in a given system).

- Calling lfsr_fs_gc can actually be a detriment for code size when
  considering link-time-gc (not related to fs-gc), since it will drag in
  the function when we don't need the traversal-invalidation features.

- Calling lfsr_fs_gc vs lfsr_mtree_gc shouldn't really be a significant
  code size difference. We should probably look into lfsr_mtree_gc,
  which is called from many places, instead of tangling everything
  together...

So this commit reverts gc-restarts and brings back gc masking on flag
change.

At the very least, moving all the code around led to a bit of code
savings:

                      code          stack
  before gc-restart: 36316           2680
  gc-restart:        36068 (-0.7%)   2680 (+0.0%)
  after gc-restart:  36240 (-0.2%)   2680 (+0.0%)
2024-07-20 01:27:45 -05:00
Christopher Haster 1cd6a6873a Restart gc on flag change, better dedup mkconsistent/ckmeta/etc
This simplifies lfsr_fs_gc a bit, and allows lfsr_fs_mkconsistent/
ckmeta/etc to call lfsr_fs_gc directly (it would be a bit strange for
these function to finish up unrelated gc traversals).

Unfortunately, this does risk gc getting stuck constantly restarting if
there is contention between two lfsr_fs_gc calls with different flags,
but you could argue this would be a system design mistake...

The deduplication of traversal state leads to some pretty nice code
savings:

           code          stack
  before: 36316           2680
  after:  36068 (-0.7%)   2680 (+0.0%)
2024-07-20 01:26:33 -05:00
Christopher Haster eced943685 Changed gc_steps into a runtime parameter, better dedup mount gc
So instead of configuring gc_steps at mount time (or eventually compile
time), lfsr_fs_gc now takes a steps parameter that controls how much gc
work to attempt:

  int lfsr_fs_gc(lfs_t *lfs, lfs_soff_t steps, uint32_t flags);

This API was needed internally to better deduplicate on-mount gc, and I
figured it might also be useful for users to be able to easily change
gc_steps per lfsr_fs_gc call.

I realize this could also be accomplished with the theoretical
lfsr_fs_gccfg, but it's a bit easier to not need a struct every call.

Most likely, depending on project/system, users will always call
lfsr_fs_gc with either 1 (minimal work) or -1 (maximal work), or, worst
case, can define a system-wide GC_STEPS somewhere.

---

Deduplicating on-mount gc work better saved some code, though it's worth
noting this could have been done internally and not exposed to users:

           code          stack
  before: 36476           2680 (+0.0%)
  after:  36316 (-0.4%)   2680 (+0.0%)
2024-07-18 20:46:58 -05:00
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 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 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 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 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 e04526f76d Renamed some test cases *_open -> *_opened for consistency
- *_open -> *_opened
- *_orphan -> *_orphaned
- *_desync -> *_desynced
- *_open_files -> *_files_opened
2024-07-08 13:18:16 -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 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 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 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 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 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 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 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
Christopher Haster ec44831381 Dropped lfsr_mtree_seek for explicit mtree lookups
lfsr_mtree_seek is a bit of an odd function, a hammer for too many
nails.

Using lfsr_mtree_lookup directly with manual mdir.mid manipulation gives
the internal layers more flexibility and room for optimizations.

Code changes:

           code          stack
  before: 34426           2624
  after:  34406 (-0.1%)   2624 (+0.0%)
2024-06-21 11:54:08 -05:00
Christopher Haster 4a5de70b00 Made mtree implied in lfsr_mtree_* functions, renamed a couple things
This makes mtree implicit in most of littlefs's core functions, which
simplifies things. It also makes lfsr_mtree_traverse naming consistent
with other mtree-esque operation.

Renames:

- Renamed lfsr_fs_weight -> lfsr_mtree_weight (implicit mtree)
- Renamed lfsr_mtree_weight -> lfsr_mtree_weight_ (explicit mtree)
- Renamed lfsr_fs_traverse* -> lfsr_mtree_traverse*
- Renamed LFSR_TSTATE_* -> LFSR_MTRAVERSAL_*

Implicit mtree functions, note these are pretty much the backbone of
littlefs:

- lfsr_mtree_weight
- lfsr_mtree_lookup
- lfsr_mtree_seek
- lfsr_mtree_namelookup
- lfsr_mtree_pathlookup
- lfsr_mtree_traverse

This makes the naming is a bit inconsistent with lfsr_btree_*,
lfsr_rbyd_*, etc, but sometimes rules needs to bend a bit.

Besides, most of these functions needed access to the mroot anyways, so
it's not like they were really ever able to operate on independent
mtrees correctly.

And you can't complain about the code savings:

           code          stack
  before: 34562           2624
  after:  34426 (-0.4%)   2624 (+0.0%)
2024-06-21 11:54:06 -05:00
Christopher Haster dd7b04564c t: Added a couple idempotent traversal mutation tests
Just to makes sure this works as expected. Though internally the logic
is pretty simple.
2024-06-20 14:46:02 -05:00
Christopher Haster 0502cb8f92 t: Restricted LFS_ERR_BUSY to only LFS_T_EXCL
So now lfsr_traversal_read will only return LFS_ERR_BUSY if LFS_T_EXCL
was provided to lfsr_traversal_open.

This means it's no longer possible to opportunistically traverse blocks,
_and_ detect mutation in the same traversal (though I suppose you could
open multiple traversals for this?), but on the flipside this
potentially frees up the implementation a bit.

This motivation for this is that LFS_ERR_BUSY is potentially confusing
and annoying to handle if you don't care about mutation.

           code          stack
  before: 34566           2624
  after:  34558 (-0.0%)   2624 (+0.0%)
2024-06-20 13:14:17 -05:00
Christopher Haster 5b72973f8f t: Filled out rest of test_traversal
There can always be more tests, but I think these give a nice set of
coverage over corner-cases in our traversal clobbering scheme.

These did find a couple bugs:

- If we clobber an inlined mroot, we need to adjust the mid by two
  mdirs, but only if there is no mtree/mdirs.

  To avoid this and other mid-related headaches, we just provide the new
  mid in lfsr_mdir_commit, since we always know it here.

- lfsr_mdir_commit compares mdirs by mptr, which means we need to
  clobber traversal's mdir's mptrs or else lfsr_mdir_commit will clobber
  already-clobbered traversals.

  There may be a better way to solve this, but it will probably get into
  the weeds with how lfsr_mdir_commit relies on mids vs mptrs...

Code changes:

           code          stack
  before: 34570           2624
  after:  34566 (-0.0%)   2624 (+0.0%)

Now that the dust has settled and we sort of know what the traversal
implementation will look like, we can look at the before and after to
get a rough idea of how much the traversal API actually costs:

                          code          stack
  no-traversal (before): 33886           2560
  yes-traversal (after): 34566 (+2.0%)   2624 (+2.5%)

Note this still includes the annoying lfsr_btree_traverse inlining stack
cost, which isn't really the traversal API's fault and may be avoidable
in the future.
2024-06-20 13:13:58 -05:00
Christopher Haster d36abd387d t: Tweaked traversal to use more states, less indirect pointers
This splits LFSR_TSTATE_BTREE into separate LFSR_TSTATE_MTREE/BTREE/
OBTREE states that indicate what to do next after traversing the btree.

This removes the need to point indirectly to file's o.next pointer,
since we can just point to the file struct itself.

I've also simplified opened-file clobbering to just move to the next
opened mdir, instead of searching for another unsynced file. This
simplifies things but does mean we now need to clobber traversals when
closing non-file objects. Implicitly calling lfsr_opened_clobber in
lfsr_opened_remove solves this with very little extra code cost,
deduplicated, and gives us a stronger invariant for traversal references
to closed objects. So win win?

Oh, and all the explicit open-file clobber checks are now deduplicated
into lfsr_opened_clobber again.

These tweaks save quite a bit of code:

           code          stack
  before: 34740           2624
  after:  34570 (-0.5%)   2624 (+0.0%)
2024-06-20 13:13:47 -05:00
Christopher Haster a8a214ec0b t: Adopted simpler blanket-mdir traversal clobbering scheme
Now, lfsr_mdir_commit just clobbers all traversals associated with the
current mdir, irrespective of mid.

This makes our traversal clobbering model quite a bit simpler, drops any
mess related to bshrub staging, and allows lfsr_mdir_commit to handle
most of the clobbering logic with the exception of opened file handles.

This also fits mtree/mroot clobbering a bit better, with mtree
clobbering behaving the same as a file btree in the mroot.

The downside is we will miss more blocks during clobbered traversals,
but clobbered traversals are best effort anyways. The saved code cost
and simpler/more robust clobbering model are probably worth it.

Traversal clobbering is already complicated enough...

Code/stack changes:

           code          stack
  before: 34716           2648
  after:  34740 (+0.1%)   2624 (-0.9%)
2024-06-20 13:13:44 -05:00
Christopher Haster 4cd1f84a89 t: Separated mtree traversal/iteration, bshrub staging
A number of traversal changes:

- Traversal now traverses the mtree's btree (the inner btree nodes)
  separately from iterating over mdirs in the mtree.

  This makes resuming clobbered traversals more robust as there's less
  state to worry about. It also reduces all btree traversals to a single
  state which simplifies the traversal logic and _in theory_ reduces
  code/RAM costs.

  This does add a second O(n logbn) pass through the mtree, but this
  takes the fast path since we already validated btree nodes. mtree
  traversal is probably dominated by mdir fetching anyways...

- lfsr_mdir_commit no longer clobbers mid-related traversals. This was a
  bit too complicated with attrs potentially inserting new mids.

  Instead, it's up to upper layers to explicitly clobber traversals.
  Most of these already need to update dir positions, so it's not that
  much extra code, but it does add cost.

  lfsr_mdir_commit still clobbers mroot/mtree related traversals.

- We now stage bshrubs in traversals during mdir compaction, so we
  shouldn't need to clobber traversals when the mdir compacts.

  In theory as long as we clobber traversals that reference opened
  files, we should never end up being the only reference to a bshrub. So
  we should be able to stage bshrubs without cost.

  This is _not_ working at the moment, because we aren't updating the
  actual btraversal state correctly... not sure how to fix this yet...

Code/stack changes:

           code          stack
  before: 34682           2544
  after:  34716 (+0.1%)   2648 (+4.1%)

The surprise stack cost is _very_ interesting. Where is this coming
from?

It turns out when we reduce all btree traversals to a single state, and a
single function call, GCC is happy to inline lfsr_btree_traverse
directly into lfsr_fs_traverse.

This is great for code cost, but now lfs_fs_traverse contains the entire
stack frame of lfsr_btree_traverse, which is quite large. When we called
lfsr_btree_traverse twice, this stack frame was never nested with
lfsr_mtree_lookup, but now our tools think it is...

I'm not sure how to fix this. Maybe improving our tooling to understand
shrinkwrap optimizations will find this doesn't actually cost as much?
Or maybe not since this is in a complicated switch case state machine?
We could use an explicit __attribute__((noinline)), but this sort of
heavy-handed optimization guidance has been out-of-scope for littlefs up
until now...

I'm leaving this as-is for now, but it may be worth looking this again
in the future.
2024-06-20 13:13:33 -05:00
Christopher Haster e744106f77 t: Implemented a simple traversal clobber scheme
This adds lfsr_opened_clobber which can be called to clobber any open
traversals related to an mid, or all traversals if mid=-1. Clobbering
here means throw away any in-progress btraversals and move to the next
mid. We need to do this in several places to avoid outdated references
to btrees.

The other option would be to treat traversals like additional unsynced
file handles, add them to the lookahead buffer, copy shrubs during
compaction, etc, but I don't think we want to pay this cost since the
underlying data is otherwise inaccessible. No reason to check/repair
blocks we're not using anymore...

To make this work, LFS_BTRAVERSAL(bid) now supports resuming from a
specific bid, in lfsr_mtraversal_t we use this to resume mtree traversal
from a specific mid when clobbered.

---

Other changes:

- lfsr_mdir_commit now marks all removed mdirs with LFS_F_ZOMBIE, and
  updating related dir positions is done in lfsr_remove/lfsr_rename.

  I was originally planning to use LFS_F_ZOMBIE to clobber traversals as
  well, but it didn't work out.

- Added lfsr_fs_weight, which returns the effective mdir/mtree weight,
  including inlined-in-mroot mdirs.

- Fixed did-mask miscalculation in lfsr_mkdir where fs/mtree weight
  wasn't shifted by mdir_bits. This probably just went unnoticed during
  some mid refactoring.

- Changed traversals to only traverse _unsynced_ opened files. No reason
  to traverse files we know match disk. This also makes is so only
  unsynced files need to worry about clobbering traversals.

  This has the catch that we need to point to the traversing file handle
  somehow so we can clobber correctly. The (hacky?) solution is to point
  to the next pointer itself, which tells us both where to go next, and
  what file handle we are currently traversing.

- Moved LFS_F_UNSYNC flags to before file operations, instead of after.
  This is needed for the above traverse-unsync-only logic in case we
  alloc in the middle of a file operation.

Code changes:

           code          stack
  before: 34454           2544
  after:  34682 (+0.7%)   2544 (+0.0%)

Also added some specific tests over corner cases caused by traversing
and mutating the filesystem at the same time.

Unfortunately these aren't passing yet. Our mid-clobbering logic doesn't
handle mid insertion correctly, so we end up clobbering more traversals
than we need to...
2024-06-20 13:12:59 -05:00
Christopher Haster ab9b975ac1 t: Tweaked lfsr_btree_traverse so btinfo/mtinfo alias
The traversal logic is a bit simpler if everything can pass around/
populate the same struct, so this reverts some changes made when
implementing lfsr_traversal_t, bringing back bid as a side-channel and
making btinfo/mtinfo typedef aliases.

btinfo/mtinfo are also required arguments for lfsr_btree_traverse/
lfsr_fs_traverse now, so it's even easier to forward these to lower
layers if they alias.

What return-pointers should/shouldn't be optional is still an open
question, but at least for btinfo/mtinfo matching lfs_stat makes sense.

This saves a bit of code/stack:

           code          stack
  before: 34474           2552
  after:  34454 (-0.0%)   2544 (-0.3%)
2024-06-20 13:11:46 -05:00
Christopher Haster 635e1fe8d4 t: Added lookahead to lfsr_traversal_t, adopted in lfs_alloc
This sort of turned into a complete refactor of lfs_alloc in order to
move/reuse the lookahead buffer filling logic into lfsr_fs_traverse.

lfs_alloc now calls lfsr_fs_traverse to fill the lookahead buffer when
no more blocks are available, but also you can too with lfsr_traversal_t
+ LFS_T_LOOKAHEAD.

The one big caveat being if any mutation happens to the filesystem, any
incomplete lookahead needs to be tossed out. To help with this,
lfsr_traversal_read now returns LFS_ERR_BUSY (-16) instead of
LFS_ERR_NOENT (-2) if the filesystem has been modified since the
traversal was opened.

Note that by default lfsr_traversal_t will still try to keep traversing
blocks, but can be told to terminate immediately with LFS_T_EXCL.
Continuing the traversal is probably desired for checking checksums,
debugging, etc, as otherwise you could end up looping over only the
first couple blocks in a write-heavy system, but if you are trying to
populate the lookahead buffer you probably want to just abort and start
over.

I considered adding a flags field to lfs_tinfo for this, but decided
against it since it would be the only place in the current API where we
don't use error codes to convey behavior-changing information. Though
this may be worth reconsidering at some point...

---

In reworking lfs_alloc, a lot of the internal logic was broken up into
specific functions:

- lfs_alloc_ckpoint - checkpoint the allocator
- lfs_alloc_discard - discard any lookahead
- lfs_alloc_shift - discard/shift lookahead if progress can be made
- lfs_alloc_markinuse - mark a block as in-use
- lfs_alloc_markfree - mark any remaining blocks as free
- lfs_alloc_findnext - find the next free block in lookahead

If anything this probably makes lfs_alloc more readable, though the
original motivation was to allow lfsr_traversal_t to only shift/zero the
lookahead buffer if there's a chance we can make progress.

This was based on upstream work by opilat and myself.

Code changes:

           code          stack
  before: 34226           2560
  after:  34474 (+0.7%)   2552 (-0.3%)
2024-06-20 13:11:41 -05:00
Christopher Haster 670b9fbf99 t: Implemented rudimentary lfsr_traversal_t and related functions
This adds the lfsr_traversal_t object, which encapsulates a traversal
over all blocks in the filesystem.

This replaces the earlier lfs_fs_traverse function, but is sort of
"inside-out" in that instead of taking a callback, an lfsr_traversal_t
object can be read from to return lfs_tinfo structs that describe the
blocks in our system:

  lfsr_traversal_open(&lfs, &t) => 0;
  lfsr_traversal_read(&lfs, &t, &tinfo) => 0;
  tinfo.btype => LFS_BTYPE_MDIR;
  tinfo.block => 0x0;
  lfsr_traversal_read(&lfs, &t, &tinfo) => 0;
  tinfo.btype => LFS_BTYPE_MDIR;
  tinfo.block => 0x1;
  lfsr_traversal_read(&lfs, &t, &tinfo) => 0;
  tinfo.btype => LFS_BTYPE_DATA;
  tinfo.block => 0x42;
  lfsr_traversal_read(&lfs, &t, &tinfo) => LFS_ERR_NOENT;
  lfsr_traversal_close(&lfs, &t) => 0;

This is more flexible, allowing for aborted traversals, yielding,
rewinding, etc, but also more complicated to implement, since it
requires all traversal state to be stored explicitly.

Fortunately, since we needed to reimplement filesystem traversals
anyways, I was able to build this into the new system from the start
using a small state machine to drive the traversal internally. So all
that was really needed was a bit of window dressing, adding
LFS_TYPE_TRAVERSAL to track open traversals, logic to handle
invalidating traversals on file close, mutation, etc...

Which, uh, that last one is not implemented yet. Interactions with other
filesystem operations gets messy, so I figured I'd go ahead and commit
what is currently working.

Ugh, and tests. The biggest downside of adding lfsr_traversal_t is how
many more corner-cases it adds to the system...

lfsr_traversal_t is going to be a work-in-progress for a bit...

---

lfsr_traversal_t also adds a really interesting path towards more access
to advanced low-level operations, such as checking metadata/data
checksums, incrementally progressing the garbage collector, even
repairing bad metadata/data blocks eventually.

Currently implemented is LFS_T_CKMETADATA and LFS_T_CKDATA to check
metadata and data checksums respectively. This is the first feature that
actually allows you to validate data checksums.

Code changes so far:

           code          stack
  before: 33886           2560
  after:  34226 (+1.0%)   2560 (+0.0%)
2024-06-20 13:09:24 -05:00