Commit Graph

96 Commits

Author SHA1 Message Date
Christopher Haster ecd780a313 preerase: Inverted LFS3_PREERASE to be opt-in
The original motivation for making LFS3_PREERASE opt-out, is that it
makes sense for LFS3_GBMAP to bring in all gbmap-related features
(PREERASE, BADBLOCKS (future)). However, after a bit of use, I think
this just complicates our ifdef logic too much.

So instead, LFS3_PREERASE is now opt-in, with the intention of making
all ifdefs relative only to the default build. I think this will make it
easier to reason about ifdefs, at least internally.

Eventually, I want to look into alternative default builds (LFS3_BIGGER,
LFS3_BIGGERR, ..., LFS3_BIGGEST), which would provide an alternative way
to enable all gbmap-related features. Though these builds have a
high-risk of bikeshedding (LFS3_GC?), so we'll see.

---

That being said, the main ergonomic improvement was probably adding
a #error, so we don't have to check ifdef GBMAP everywhere.

Maybe this should be extended to LFS3_RDONLY? Or maybe not, LFS3_RDONLY
is a bit of a special case.

No code changes:

                    code          stack          ctx
  before:          35144           2136          660
  after:           35144 (+0.0%)   2136 (+0.0%)  660 (+0.0%)

                    code          stack          ctx
  gbmap before:    38380           2144          776
  gbmap after:     38380 (+0.0%)   2144 (+0.0%)  776 (+0.0%)

                    code          stack          ctx
  preerase before: 38920           2168          796
  preerase after:  38920 (+0.0%)   2168 (+0.0%)  796 (+0.0%)
2026-01-09 00:02:42 -06:00
Christopher Haster b3ab83d5b5 Added REVPERTURB, reworked how we handle revision counts
The main change is adding LFS3_M_REVPERTURB, which will be necessary for
preerase allocations, but I got distracted and ended up giving the
revision count subsystem a bit of a refactor.

Main changes:

- Added LFS3_M_REVPERTURB, which ensures the leading bit in the
  revision count changes after each allocation/relocation/compaction.

  This is generally optional, but will be required for preerase
  allocations. Our ecksum system is only reliable if we ensure at least
  one bit changes, otherwise the chance of ecksum collision is very
  high.

  The downside of LFS3_M_REVPERTURB is that we need to read the contents
  of the new block to figure out what the bit should change to. Probably
  a minimal cost in the system, but still a good reason to make the
  behavior optional.

  Does LFS3_M_REVPERTURB have any use outside of preerased allocation?
  I'm not sure. Maybe it has some niche use reducing the chance of bd
  ECC collisions?

- Dropped LFS3_M_REVDBG, but adding low-effort debug bits that are
  always enabled.

  Making LFS3_M_REVDBG conditional was probably overkill. The flag
  checks probably cost more than the actual debug bits when enabled.

  Instead, replaced with a simpler, low-effort debug bit system, where
  we only set the debug bits during mdir allocation/relocation. These
  bits shouldn't change during normal compaction, but we _don't_
  introduce debug bits if mounting a filesystem from a driver without
  these debug bits.

- Restricted recycle counter to at most 20-bits to make space for
  things. This ensures perturb/debug bits don't get overwritten (though
  we really only care about perturb bits).

  2^20 (~1M) recycles is probably enough for any device littlefs will
  run on, especially considering the recycle_count should probably be
  several orders of magnitude smaller than the device's expected erase
  cycles.

  Worst case this can always be increased in the future without
  backwards incompatible changes. The only hard requirement for revision
  counts is that the full 32-bits are comparable.

- Simplified lfs3_rev_inc and friends, and moved most of the
  disk-dependent revision count stuff down into lfs3_rbyd appendrev.

  This deduplicates the messy revision count handling in
  lfs3_btree_commit_.

  Though note the implicit lfs3_rbyd_appendrev now defaults to writing
  the btree debug bits ('b'). A bit of a hack, but works for littlefs.

Here's the resulting encoding:

  vvvv---- -------- -------- -ddddddd
  vvvvrrrr rrrrrr-- -------- -ddddddd
  vvvvrrrr rrrrrrnn nnnnnnnn pddddddd
  '-.''----.----''----.----' ^'--.--'
    '------|----------|------|---|---- 4-bit relocation revision
           '----------|------|---|---- recycle-bits recycle counter
                      '------|---|---- pseudorandom noise (if revnoise)
                             '---|---- perturb bit (if revperturb)
                                 '---- low-effort debug bits
                              11-1---  - h = mroot anchor
                              11-11-1  - m = mdir
                              11---1-  - b = btree node

Note we store revision counts as le32s, so the perturb bit should end up
as the leading bit in the first byte.

Costs a bit more code (mostly because the debug bits are now
unconditional, even if low-effort), but simplifies the codebase:

                        code          stack          ctx
  before:              35124           2136          660
  after:               35144 (+0.1%)   2136 (+0.0%)  660 (+0.0%)
  after+yesrevperturb: 35192 (+0.2%)   2136 (+0.0%)  660 (+0.0%)

                        code          stack          ctx
  gbmap+np before:     38252           2144          776
  gbmap+np after:      38272 (+0.1%)   2144 (+0.0%)  776 (+0.0%)
  gbmap+np after+yrp:  38328 (+0.2%)   2144 (+0.0%)  776 (+0.0%)

                        code          stack          ctx
  gbmap+yp before:     38832           2168          796
  gbmap+yp after:      38852 (+0.1%)   2168 (+0.0%)  796 (+0.0%)
  gbmap+yp after+yrp:  38908 (+0.2%)   2168 (+0.0%)  796 (+0.0%)
2026-01-09 00:02:05 -06:00
Christopher Haster 2d6707c002 gbmap: Renamed gbmap.free -> gbmap.next
This may not be the best name, but it's at least more clear than
gbmap.free, which risks confusion with total number of free blocks.
2026-01-09 00:01:54 -06:00
Christopher Haster 843412cc79 preerase: Implemented the gc side of preerase
Allocating pre-erased blocks gets quite complicated due to our
restricted flash model, but at least the actual pre-erasing is
relatively straightforward:

- We keep track of known preerased state in lfs3->gbmap.preeraser.

- If LFS3_GC_PREERASE is provided during gc work, we increment the
  preeraser's known window by scanning the gbmap.

- Any BMFREE ranges we find, we erase a block at a time, and store the
  resulting ecksum in a BMERASED range in the gbmap.

- We keep track of how many blocks we erased, and stop early if this
  exceeds cfg.gc_preerase_count. This just lets users tune how many
  blocks to preerase in case something (?) prevents preerased blocks
  from being used.

Some notes:

- We don't really do anything with ranges in lfs3_alloc_preerase. In
  theory we could bulk in erase to minimize the number of commits to the
  gbmap, but we expect erase to dominate, so this probably isn't worth
  it.

  And if erase doesn't dominate, why would you bother pre-erasing
  blocks?

- Preerasing isn't really a traversal operation, and is managed by a
  sort of secondary state machine in lfs3_fs_gc_.

  This also means lfs3_trv_read with LFS3_T_PREERASE does nothing, but I
  guess that is ok? It's tempting to try to make lfs3_trv_read also
  preerase, but it's unclear what block it should return -- it's
  probably the wrong API.

- Introducing ecksums actually went quite a bit smoother than I
  expected. Though it helps ecksums are the only optional payload, no
  type punning or anything.

  Ecksums do muddy the gbmap's design a bit, unfortunately. The main
  issue being that we can only merge BMERASED ranges with equal ecksums.
  This makes BMERASED ranges less compressable than the others, and may
  be one reason to limit cfg.gc_preerase_count.

  However:

  1. This is where I think it's useful to emphasize that the gbmap's
     responsibility is to track _free_ blocks, in-use blocks are
     secondary.

     When allocating, we're going to stop at the first BMFREE/BMERASED,
     but may need to skip over an unbounded number of BMINUSE/BMBAD
     blocks. So the compressability of BMFREE/BMERASED ranges should
     have less of an impact on block allocation.

  2. In practice, most flash uses consistent erase values, so the
     resulting ecksums will probably be compressable. The exceptions are
     noop-erases (SD/eMMC, RAM, NVRAM, etc), and encryption with block
     address permutation?

     Though noop-erases are a pretty big exception.

Code changes:

                    code          stack          ctx
  before:          35116           2136          660
  after:           35116 (+0.0%)   2136 (+0.0%)  660 (+0.0%)

                    code          stack          ctx
  gbmap+np before: 38040           2136          776
  gbmap+np after:  38188 (+0.4%)   2144 (+0.4%)  776 (+0.0%)

                    code          stack          ctx
  gbmap+yp before: 38040           2136          776
  gbmap+yp after:  38608 (+1.5%)   2144 (+0.4%)  796 (+2.6%)
2026-01-09 00:01:42 -06:00
Christopher Haster 07ba12fb20 Moved gstate struct definitions into lfs3_t
Despite programming in C for quite a while, I didn't expect this to
work. But it turns out you _can_ typedef nested structs, you just need
to put the typedef outside the struct.

IMO this makes it a bit easier to understand what state is exclusive to
the lfs3_t struct. We do use these typedefs internally, but only for
pointers into the core lfs3_t.

No code changes.
2026-01-09 00:01:36 -06:00
Christopher Haster 4f22d8c591 scripts: dbgflags.py: Added support for flag aliases
Also tweaked related flag comments a bit.

---

This adds another internal flag modifier ('a') to dbgflags.py to
indicate a flag is an alias for multiple other flags.

These still show up in -l/--list and name searches:

  $ ./scripts/dbgflags.py -l +ck
  LFS3_CK_MKCONSISTENT  0x00000100  Make the filesystem consistent
  LFS3_CK_LOOKAHEAD     0x00000200  Repopulate lookahead buffer
  LFS3_CK_COMPACT       0x00000800  Compact metadata logs
  LFS3_CK_CKMETA        0x00001000  Check metadata checksums
  LFS3_CK_CKDATA        0x00002000  Check metadata + data checksums
  LFS3_CK_CK            0x00003000  Alias for all check work
  LFS3_CK_GC            0x00003b00  Alias for all gc work

But are hidden from value searches, as they would be redundant and the
specific low-level flags are probably more useful:

  $ ./scripts/dbgflags.py +ck 0x00003000
  LFS3_CK_CKMETA  0x00001000  Check metadata checksums
  LFS3_CK_CKDATA  0x00002000  Check metadata + data checksums
2026-01-09 00:01:29 -06:00
Christopher Haster 124afb3034 Adopted LFS3_*_CK and LFS3_*_GC aliases, replacing LFS3_GC_ALL
This extends the hopefully useful LFS3_GC_ALL flag alias to the other
functions, without trying to figure out what the heck LFS3_M_ALL should
mean semantically.

Current definitions:

  // an alias for ck work
  *_CK              0x00003000  ---- ---- ---- ---- --11 ---- ---- ----

  // an alias for all possible gc work
  *_GC              0x00003b00  ---- ---- ---- ---- ++11 11+1 ---- ----

  *_MKCONSISTENT    0x00000100  ---- ---- ---- ---- ---- ---1 ---- ----
  *_LOOKAHEAD       0x00000200  ---- ---- ---- ---- ---- --1- ---- ----
  *_PREERASE+       0x00000400  ---- ---- ---- ---- ---- -+-- ---- ----
  *_COMPACT         0x00000800  ---- ---- ---- ---- ---- 1--- ---- ----
  *_CKMETA          0x00001000  ---- ---- ---- ---- ---1 ---- ---- ----
  *_CKDATA          0x00002000  ---- ---- ---- ---- --1- ---- ---- ----
  *_REPAIRMETA+     0x00004000  ---- ---- ---- ---- -+-- ---- ---- ----
  *_REPAIRDATA+     0x00008000  ---- ---- ---- ---- +--- ---- ---- ----

  + Planned

One weird artifact of this is that LFS3_GC_CK ~= LFS3_GC_CKDATA, but I'm
not sure that's a bad thing? Note this is very much not true for info
flags, LFS3_I_CK != LFS3_I_CKDATA.

No code changes.
2026-01-09 00:01:15 -06:00
Christopher Haster d54fef8099 Reorganized traversal flags again
One nice thing about merging LOOKAHEAD + LOOKGBMAP, is now our core
traversal flags fit in a single byte. This is useful for organizing
things, especially so as the traversal flags seem to permeate into
basically every flag set.

The main change was to actually group these flags into a byte, which
helps readability and in theory could make some bulk accesses cheaper
(in practice I don't think we currently leverage this):

  T_MODE             0x00000001  ---- ---- ---- ---- ---- ---- ---- ---1
  T_RDONLY           0x00000000  ---- ---- ---- ---- ---- ---- ---- ----
  T_RDWR             0x00000001  ---- ---- ---- ---- ---- ---- ---- ---1
  T_MTREEONLY        0x00000002  ---- ---- ---- ---- ---- ---- ---- --1-
  T_EXCL             0x00000008  ---- ---- ---- ---- ---- ---- ---- 1---
  T_MKCONSISTENT     0x00000100  ---- ---- ---- ---- ---- ---1 ---- ----
  T_LOOKAHEAD        0x00000200  ---- ---- ---- ---- ---- --1- ---- ----
  T_PREERASE*        0x00000400  ---- ---- ---- ---- ---- -1-- ---- ----
  T_COMPACT          0x00000800  ---- ---- ---- ---- ---- 1--- ---- ----
  T_CKMETA           0x00001000  ---- ---- ---- ---- ---1 ---- ---- ----
  T_CKDATA           0x00002000  ---- ---- ---- ---- --1- ---- ---- ----
  T_REPAIRMETA*      0x00004000  ---- ---- ---- ---- -1-- ---- ---- ----
  T_REPAIRDATA*      0x00008000  ---- ---- ---- ---- 1--- ---- ---- ----

  t_EVICT*           0x00000010  ---- ---- ---- ---- ---- ---- ---1 ----
  t_TYPE             0xf0000000  1111 ---- ---- ---- ---- ---- ---- ----
  t_ZOMBIE           0x08000000  ---- 1--- ---- ---- ---- ---- ---- ----
  t_CKPOINTED        0x04000000  ---- -1-- ---- ---- ---- ---- ---- ----
  t_DIRTY            0x02000000  ---- --1- ---- ---- ---- ---- ---- ----
  t_STALE            0x01000000  ---- ---1 ---- ---- ---- ---- ---- ----
  t_BTYPE            0x00ff0000  ---- ---- 1111 1111 ---- ---- ---- ----

  * Planned

This gives btype a full byte as well, which is a bit overkill, but can
be reduced in the future if we run into traversal flag pressure.

This also pushes some future planned flags (DEDUP, COMPR, etc) into
higher-order bits, but that's not the end of the world.

Code changes basically nothing:

                 code          stack          ctx
  before:       35152           2136          660
  after:        35152 (+0.0%)   2136 (+0.0%)  660 (+0.0%)

                 code          stack          ctx
  gbmap before: 38080           2136          776
  gbmap after:  38076 (-0.0%)   2136 (+0.0%)  776 (+0.0%)
2026-01-09 00:01:07 -06:00
Christopher Haster 7a57b1e2bd Renamed LFS3_T_COMPACTMETA -> LFS3_T_COMPACT (and gc_compact_thresh)
This effectively reverts 1f824a0:

- LFS3_T_COMPACTMETA -> LFS3_T_COMPACT
- gc_compactmeta_thresh -> gc_compact_thresh

And friends.

After using LFS3_T_COMPACTMETA for a bit, I think it just adds noise
without much value. Especially when next to LFS3_T_LOOKAHEAD,
LFS3_GC_PREERASE, LFS3_M_SYNC, etc.

It's interesting that we already have some very distinct verbs for this
sort of thing based on data type (compact => metadata, garbage-collect
=> disk, compress => data).
2026-01-09 00:01:05 -06:00
Christopher Haster ffc565508a alloc: Merged LOOKAHEAD+LOOKGBMAP -> single LOOKAHEAD flag
Our flag space is already really packed, and I'm not sure having these
as separate flags is meaningful or useful for users. They both indicate
to repopulate allocators, and most users probably won't care that there
are two subtly different allocators operating under the hood.

There's an argument that LOOKAHEAD not touching disk is a useful
distinction, but in practice you really only need LOOKAHEAD work when
mounted RDWR.

So, merged the behaviors of LOOKAHEAD + LOOKGBMAP such that
LFS3_*_LOOKAHEAD requests repopulation of all allocators based on
gc_lookahead_thresh and gc_lookgbmap_thresh.

In priority order (some notes below):

1. If max(lookahead, gbmap) < gc_lookahead_thresh => repop lookahead
2. If gbmap < gc_lookgbmap_thresh                 => repop gbmap

As a plus, this makes it easier to avoid LFS3_IFDEF_GBMAP mess.

---

It's interesting to note LFS3_*_LOOKAHEAD will still repopulate the
lookahead buffer when the gbmap is present, but only if this would gain
more knowledge than was is currently in the gbmap.

I considered disabling lookahead scans completely when we have a gbmap,
but repopulating the lookahead buffer is still useful if the gbmap is at
risk of exhaustion. This is what gc_lookahead_thresh is for anyways, and
users can set gc_lookahead_thresh=0 if they want to disable this
behavior.

Relatedly, lookahead scans are actually prioritized over gbmap scans
(when they would gain knowledge). In theory this minimizes gc latency,
as gbmap scans risk triggering a full lookahead scan when building the
new gbmap.

---

Code changes minimal:

                 code          stack          ctx
  before:       35152           2136          660
  after:        35152 (+0.0%)   2136 (+0.0%)  660 (+0.0%)

                 code          stack          ctx
  gbmap before: 38076           2136          776
  gbmap after:  38080 (+0.0%)   2136 (+0.0%)  776 (+0.0%)
2026-01-09 00:00:53 -06:00
Christopher Haster 7ccdee255b alloc: Reworked how lookahead/gbmap allocators interact
Before, the gbmap allocator worked by feeding the lookahead allocator,
so all allocation requests went through the lookahead buffer:

  alloc ---> lookahead ---> gbmap

This worked, and was easy to strap on to the existing system, but
limited what we could cache to what fits in our lookahead buffer. This
doesn't have a big effect on runtime analysis, since fragmentation is
always a concern, but it does mean we're not taking advantage of the
gbmap range representation and accessing disk more than we need to.

It also limits in-use range skipping to a lookahead buffer at a time,
which is problematic as the whole reason for the gbmap is to make the
lookahead buffer mostly irrelevant.

On top of the range issues, this design makes it difficult to add
preerase info, which is coming up on the TODO list.

---

To fix this, I reorganized the two allocators to run in parallel, with
the gbmap being queried first before falling back to the lookahead
buffer:

  alloc -+-> gbmap
         '-> lookahead

Instead of relying on the lookahead buffer, the gbmap now stores one
range in the gbmap.free field, using the sign to indicate if it's free
vs in-use (not the best name, but oh well).

This adds a word of storage to the gbmap, but as a tradeoff we can track
a full region in RAM and avoid repeated gbmap lookups.

The lookahead buffer can still be populated by gc work, which may be
useful if the gbmap is exhausted, but will also be cleared during gbmap
allocations to avoid out-of-date state.

---

While reworking this, I also tweaked a number of other allocator things:

- Adjusted lookahead.window to point to next block candidate
  (lookahead.window and gbmap.window should now always match)

- Renamed lfs3_alloc_zerogbmap -> lfs3_gbmap_zero

- Moved some alloc functions around

Code cost was mostly unaffected. Added an extra word to gbmap's ctx, but
as a tradeoff saved a chunk of stack by avoiding nested allocators:

                 code          stack          ctx
  before:       35160           2136          660
  after:        35152 (-0.0%)   2136 (+0.0%)  660 (+0.0%)

                 code          stack          ctx
  gbmap before: 38020           2152          772
  gbmap after:  38076 (+0.1%)   2136 (-0.7%)  776 (+0.5%)
2026-01-09 00:00:42 -06:00
Christopher Haster 465e9fbe9d Reverted best-effort fsinfo.known_free/inuse prototype
See previous commit for motivation.

I can't think of how you could easily find this information from the
gbmap during/after mount, short of a O(d log_b d) scan through the
gbmap. Maybe useful, but probably not a great tradeoff for what is only
debug/diagnostic information.

So reverting, but maybe interesting to explore in the future with other
debug APIs.

Code changes:

                 code          stack          ctx
  before:       35220           2136          660
  after:        35160 (-0.2%)   2136 (+0.0%)  660 (+0.0%)

                 code          stack          ctx
  gbmap before: 38116           2152          776
  gbmap after:  38020 (-0.3%)   2152 (+0.0%)  772 (-0.5%)
2025-12-02 21:36:38 -06:00
Christopher Haster 0b48faf898 Started prototyping best-effort fsinfo.known_free/inuse fields
The idea here was that we could provide best-effort known in-use/free
block info (and eventually pre-erased and bad block info) as a cheaper
alternative to lfs3_fs_usage. It's probably still not what users expect
from lfs3_fs_stat, but may be useful as debug/diagnostic info:

- fsinfo.known_free - Number of known free blocks
- fsinfo.known_inuse - Number of known in-use blocks
- fsinfo.known_preerased* - Number of pre-erased blocks
- fsinfo.known_bad* - Number of bad blocks
- fsinfo.block_count-(all of the above) - Number of unknown blocks

But while known_free/known_inuse is easy enough to find from the
lookahead buffer, it's surprisingly tricky from the gbmap. The best
option I can think of requires scanning the gbmap in O(d log_b d) either
(1) during mount, (2) during mkconsistent, or (3) during lookahead
scans. And that much extra work for debug/diagnostic info seems like a
poor tradeoff.

Note, though, that after scanning once, in theory the info would be
~free to maintain during gbmap rebuilds.

Will revert.

Code changes:

                 code          stack          ctx
  before:       35160           2136          660
  after:        35220 (+0.2%)   2136 (+0.0%)  660 (+0.0%)

                 code          stack          ctx
  gbmap before: 38020           2152          772
  gbmap after:  38116 (+0.3%)   2152 (+0.0%)  776 (+0.5%)
2025-12-02 21:24:08 -06:00
Christopher Haster 1abd9d732f Tried to use correct types for cfg/fsinfo things
In theory, lfs3_size_t should be used for in-block sizes (though this is
also mixed up with in-device sizes?), lfs3_off_t for file sizes, and
lfs3_block_t for block counts (I don't think lfs3_off_t/lfs3_block_t
will ever differ, but the notation is helpful).

Though I've not done a great job at keeping these types organized...

Changed:

- cfg.block_count:      lfs3_size_t  -> lfs3_block_t
- cfg.file_limit:       lfs3_size_t  -> lfs3_off_t
- fsinfo.block_count:   lfs3_size_t  -> lfs3_block_t
- fsinfo.file_limit:    lfs3_size_t  -> lfs3_off_t
- lfs3_fs_usage:        lfs3_ssize_t -> lfs3_sblock_t
- geometry.block_size:  lfs3_size_t  -> lfs3_off_t
- geometry.block_count: lfs3_size_t  -> lfs3_block_t
- and some internals

No code changes.
2025-12-02 14:37:08 -06:00
Christopher Haster 8db3ce342c rattrs: Replaced LFS3_tag_TAIL with single-recurse LFS3_tag_RATTRS
This originally started as an attempt to drop LFS3_tag_TAIL entirely,
but that didn't really go anywhere. Any attempt to work around the
double rattr-lists during mtree splits results in more mess than this
magic rattr.

But I did notice we only need to support "simple" rattrs during mtree
splits, which means we can just call lfs3_rbyd_appendrattrs to handle
these.

Maybe this will be problematic if we ever want to deduplicate
lfs3_mdir_commit___ and lfs3_rbyd_appendrattrs, but I don't see that
happening because of the different concerns (mdir-specific rattrs):

 function                code  stack  ctx
 lfs3_mdir_commit___     1052    744  396
 lfs3_rbyd_appendrattrs   142    584  388

The benefit of a single-recurse LFS3_tag_RATTRS:

- Simplifies lfs3_mdir_commit__, no more awkward loop recursion.

- May have other use cases for nesting simple rattrs?

Adds a bit of code:

                 code          stack          ctx
  before:       35316           2176          660
  after:        35320 (+0.0%)   2176 (+0.0%)  660 (+0.0%)

                 code          stack          ctx
  gbmap before: 38148           2192          772
  gbmap after:  38172 (+0.1%)   2192 (+0.0%)  772 (+0.0%)
2025-12-02 01:14:33 -06:00
Christopher Haster ce6cbc3c77 rattrs: Allowed LFS3_RATTR_TAIL as alternate rattr-list terminator
LFS3_tag_RATTRS, now LFS3_tag_TAIL, is a bit funny in that it only
supports tail-recursive rattrs. The whole point of littlefs is
bounded-RAM after all. And if we know LFS3_tag_TAIL will terminate an
rattr-list, why bother with an additional LFS3_tag_NULL?

Like LFS3_RATTR_NULL, LFS3_RATTR_TAIL sets length=0 to indicate the end
of the rattr-lists.

Saves a bit of code:

                 code          stack          ctx
  before:       35324           2176          660
  after:        35316 (-0.0%)   2176 (+0.0%)  660 (+0.0%)

                 code          stack          ctx
  gbmap before: 38156           2192          772
  gbmap after:  38148 (-0.0%)   2192 (+0.0%)  772 (+0.0%)
2025-12-02 01:14:32 -06:00
Christopher Haster dca915dd95 rattrs: Converted rattrs to full variable-length isa
It's funny to see what originally started as a simple list of rbyd attrs
slowly morph into a full isa. But it makes sense. What we really want is
an abstract description of operations that can be played and replayed as
necessary to atomically update the mtree.

Using a fixed lfs3_rattr_t struct to represent this in C is easy, and
avoids strict-aliasing issues, but ultimately limited when it comes to
the wide-range of data we want to attach to attributes.

Unlike a computer's isa, we want to be able to include full 12-24 byte
branch pointers directly in the instruction!

---

So here's a full variable-length isa organized by words (max(uintptr_t,
uint32_t)).

The first 32-bit word extends the 16-bit tag with an extra 16-bits of
control information:

  wwll llff ffcc cccc tttt tttt tttt tttt
   ^'-.-''-.-''--.--' :                 :
   '--|----|-----|----:-----------------:-- compressed weight
  ::  '----|-----|----:-----------------:-- total len
  ::       '-----|----:-----------------:-- from encoder
  ::             '----:-----------------:-- optional count
  ::                  rgmm kkkk -kkk kkkk
  11 => w=-1          ^^ ^ '-.' '---.---'
  00 => w=0           '|-|---|------|------ rm bit
  01 => w=+1           '-|---|------|------ grow bit
  10 => w=attached       '---|------|------ mask bits
                             '------|------ tag suptype
                                    '------ tag subtype

The 4-bit length field always encodes the full length of the
instruction, including the instruction itself and optional weight. The
4-bit from + 6-bit count fields operate independently and tell
lfs3_rbyd_appendrattr_ how to actually encode the data related to the
instruction.

To work around strict-aliasing issues, complex structs are expected to
be broken down into words and reconstructed in lfs3_rbyd_appendrattr_.
Most of our structs are organized into words anyways. For example:

  // new child
  *r++ = LFS3_RATTR(5, LFS3_TAG_BRANCH, -2, LFS3_FROM_BRANCH);
  *r++ = LFS3_RATTR_WEIGHT(+child_->weight);
  *r++ = LFS3_RATTR_ARG(child_->blocks[0]);
  *r++ = LFS3_RATTR_ARG(child_->trunk);
  *r++ = LFS3_RATTR_ARG(child_->cksum);

This also changes rattr-lists to be null-terminated, which makes a bit
more sense in a variable-length isa:

  *r++ = LFS3_RATTR_NULL; // all zeros, including length

One concern with null-terminated rattr-lists is how easy it is to
forget the null-terminator, but an assert that all non-null rattrs have
non-zero length seemed to catch the many many mistakes during adoption.

Alternatively, separate LFS3_FROM_NULL/LFS3_FROM_NIL from fields could
be used if encoding space gets tight.

I'm also quite happy with the 2-bit weight feild, which allows omitting
the optional weight word for -1,0,+1 weights. These should cover at
least all mdir operations.

Note the exact encoding of the rattr fields is less of a concern than
the tag fields, as it doesn't reside on-disk can be changed on whim.

---

Saves a nice chunk of code and stack:

                 code          stack          ctx
  before:       35920           2280          660
  after:        35324 (-1.7%)   2176 (-4.6%)  660 (+0.0%)

                 code          stack          ctx
  gbmap before: 38812           2296          772
  gbmap after:  38156 (-1.7%)   2192 (-4.5%)  772 (+0.0%)

The stack savings are obvious, but the code savings a bit less so. A
variable length isa _is_ more complicated, but by limiting most encoding
decisions to compile-time (2-bit weights vs 32-bit weights for example),
the savings from fewer word manipulations on the stack wins.
2025-12-02 01:14:31 -06:00
Christopher Haster cf34ba9aca Rearranged tag encodings, reserved suptype=0 for internal tags
This was motivated by a discussion with a gh user, in which it was noted
that not having a reserved suptype for internal tags risks potential
issues with long-term future tag compatibility.

I think the risk is low, but, without a reserved suptype, it _is_
possible for a future tag to conflict with an internal tag in an older
driver version, potentially and unintentionally breaking compatibility.
Note this is especially concerning during mdir compactions, where we
copy tags we may not understand otherwise.

In littlefs2 we reserved suptype=0x100, though this was mostly an
accident due to saturating the 3-bit suptype space. With the larger tag
space in littlefs3, the reserved suptype=0x100 was dropped.

---

Long story short, this reserves suptype=0 for internal flags (well, and
null, which is _mostly_ internal only, but does get written to disk as
unreachable tags).

Unfortunately, adding a new suptype _did_ require moving a bunch of
stuff around:

  LFS3_TAG_NULL           0x0000  v--- ---- +--- ----
  LFS3_TAG_INTERNAL       0x00tt  v--- ---- +ttt tttt

  LFS3_TAG_CONFIG         0x01tt  v--- ---1 +ttt tttt
  LFS3_TAG_MAGIC          0x0131  v--- ---1 +-11 --rr
  LFS3_TAG_VERSION        0x0134  v--- ---1 +-11 -1--
  LFS3_TAG_RCOMPAT        0x0135  v--- ---1 +-11 -1-1
  LFS3_TAG_WCOMPAT        0x0136  v--- ---1 +-11 -11-
  LFS3_TAG_OCOMPAT        0x0137  v--- ---1 +-11 -111
  LFS3_TAG_GEOMETRY       0x0138  v--- ---1 +-11 1---
  LFS3_TAG_NAMELIMIT      0x0139  v--- ---1 +-11 1--1
  LFS3_TAG_FILELIMIT      0x013a  v--- ---1 +-11 1-1-
  LFS3_TAG_ATTRLIMIT?     0x013b  v--- ---1 +-11 1-11

  LFS3_TAG_GDELTA         0x02tt  v--- --1- +ttt tttt
  LFS3_TAG_GRMDELTA       0x0230  v--- --1- +-11 ----
  LFS3_TAG_GBMAPDELTA     0x0234  v--- --1- +-11 -1rr
  LFS3_TAG_GDDTREEDELTA*  0x0238  v--- --1- +-11 1-rr
  LFS3_TAG_GPTREEDELTA*   0x023c  v--- --1- +-11 11rr

  LFS3_TAG_NAME           0x03tt  v--- --11 +ttt tttt
  LFS3_TAG_BNAME          0x0300  v--- --11 +--- ----
  LFS3_TAG_REG            0x0301  v--- --11 +--- ---1
  LFS3_TAG_DIR            0x0302  v--- --11 +--- --1-
  LFS3_TAG_STICKYNOTE     0x0303  v--- --11 +--- --11
  LFS3_TAG_BOOKMARK       0x0304  v--- --11 +--- -1--
  LFS3_TAG_SYMLINK?       0x0305  v--- --11 +--- -1-1
  LFS3_TAG_SNAPSHOT?      0x0306  v--- --11 +--- -11-
  LFS3_TAG_MNAME          0x0330  v--- --11 +-11 ----
  LFS3_TAG_DDNAME*        0x0350  v--- --11 +1-1 ----
  LFS3_TAG_DDTOMB*        0x0351  v--- --11 +1-1 ---1

  LFS3_TAG_STRUCT         0x04tt  v--- -1-- +ttt tttt
  LFS3_TAG_BRANCH         0x040r  v--- -1-- +--- --rr
  LFS3_TAG_DATA           0x0404  v--- -1-- +--- -1--
  LFS3_TAG_BLOCK          0x0408  v--- -1-- +--- 1err
  LFS3_TAG_DDKEY*         0x0410  v--- -1-- +--1 ----
  LFS3_TAG_DID            0x0420  v--- -1-- +-1- ----
  LFS3_TAG_BSHRUB         0x0428  v--- -1-- +-1- 1---
  LFS3_TAG_BTREE          0x042c  v--- -1-- +-1- 11rr
  LFS3_TAG_MROOT          0x0431  v--- -1-- +-11 --rr
  LFS3_TAG_MDIR           0x0435  v--- -1-- +-11 -1rr
  LFS3_TAG_MSHRUB+        0x0438  v--- -1-- +-11 1---
  LFS3_TAG_MTREE          0x043c  v--- -1-- +-11 11rr
  LFS3_TAG_BMRANGE        0x044u  v--- -1-- +1-- ++uu
  LFS3_TAG_BMFREE         0x0440  v--- -1-- +1-- ----
  LFS3_TAG_BMINUSE        0x0441  v--- -1-- +1-- ---1
  LFS3_TAG_BMERASED       0x0442  v--- -1-- +1-- --1-
  LFS3_TAG_BMBAD          0x0443  v--- -1-- +1-- --11
  LFS3_TAG_DDRC*          0x0450  v--- -1-- +1-1 ----
  LFS3_TAG_DDPCOEFF*      0x0451  v--- -1-- +1-1 ---1
  LFs3_TAG_PCOEFFMAP*     0x0460  v--- -1-- +11- ----

  LFS3_TAG_ATTR           0x06aa  v--- -11a +aaa aaaa
  LFS3_TAG_UATTR          0x06aa  v--- -11- +aaa aaaa
  LFS3_TAG_SATTR          0x07aa  v--- -111 +aaa aaaa

  LFS3_TAG_SHRUB          0x1kkk  v--1 kkkk +kkk kkkk
  LFS3_TAG_ALT            0x4kkk  v1cd kkkk +kkk kkkk

  LFS3_TAG_CKSUM          0x300p  v-11 ---- ++++ +pqq
  LFS3_TAG_NOTE           0x3100  v-11 ---1 ++++ ++++
  LFS3_TAG_ECKSUM         0x3200  v-11 --1- ++++ ++++
  LFS3_TAG_GCKSUMDELTA    0x3300  v-11 --11 ++++ ++++

  * Planned
  + Reserved
  ? Hypothetical

Some additional notes:

- I was on the fence on keeping the 0x30 prefix on config tags now that
  it is not longer needed to differentiate from null, but ultimately
  decided to keep it because: 1. it's fun, 2. it decreases the chance
  of false positives, 3. it keeps the redund bits readable in hexdumps,
  and 4. it reserves some tags < config, which is useful since order
  matters.

  Instead, I pushed the 0x30 prefix to _more_ tags, mainly gstate.

  As a coincidence, meta related tags (MNAME, MROOT, MRTREE) all shifted
  to also have the 0x30 prefix, which is a nice bit of unexpected
  consistency.

- I also considered reserving the redund bits across the config tags
  similarly to what we've done in struct/gstate tags, but decided
  against it as 1. it significantly reduces the config tag space
  available, and 2. makes alignment with VERSION + R/W/OCOMPAT a bit
  awkward.

  Instead I think would should relax the redund bit alignment in other
  suptypes, though in practice the intermixing of non-redund and redund
  tags makes this a bit difficult.

  Maybe we should consider including redund bits as a hint for things
  like DATA? DDKEY? BSHRUB? etc?

- I created a bit more space for file btree struct tags, allowing for
  both the future planned DDKEY, and BLOCK with optional erased-bit. We
  don't currently use this, but it may be useful for the future planned
  gddtree, which in-theory can track erased-state in partially written
  file blocks.

  Currently tracking erased-state in file blocks is difficult due to
  the potential of multiple references, and inability to prevent ecksum
  conflicts in raw data blocks.

- UATTR/SATTR bumped up to 0x600/0x700 to keep the 1-bit alignment,
  leaving the suptype 0x500 unused. Though this may be useful if we ever
  run out of struct tags (suptype=0x400), which is likely where most new
  tags will go.

---

Code changes were minimal, but with a bunch of noise:

                 code          stack          ctx
  before:       35912           2280          660
  after:        35920 (+0.0%)   2280 (+0.0%)  660 (+0.0%)

                 code          stack          ctx
  gbmap before: 38800           2296          772
  gbmap after:  38812 (+0.0%)   2296 (+0.0%)  772 (+0.0%)
2025-11-18 00:56:48 -06:00
Christopher Haster c16c4a00d3 ck: Merged FSCK+CK -> CK flag namespace
Unintentionally arriving at the infamous "fsck" name is a bit funny.

But it's probably something we don't want to conflict with if we can
help it, on the off chance we want a sort of lfs3_fsck function in the
future. (This is all hypothetical, but lfs3_fsck may expect an unmounted
filesystem, and have a much larger scope than lfs3_fs_ck. Though typing
this out now I'm realizing how confusing that might be...)

Since lfs3_file_ck and lfs3_fs_ck share a subset of flags, it's not
_entirely_ unreasonable for lfs3_file_ck and lfs3_fs_ck to share the
same namespace.

There's a risk of confusing users around what flags lfs3_file_ck
accepts, but we have asserts, and said flags (LFS3_CK_MKCONSISTENT,
LFS3_CK_LOOKAHEAD, etc) just don't really make sense in lfs3_file_ck:

  fs file
  y     LFS3_CK_MKCONSISTENT 0x00000800  Make the filesystem consistent
  y     LFS3_CK_LOOKAHEAD    0x00001000  Repopulate lookahead buffer
  y     LFS3_CK_LOOKGBMAP    0x00002000  Repopulate the gbmap
  y     LFS3_CK_PREERASE*    0x00004000  Pre-erase unused blocks
  y     LFS3_CK_COMPACTMETA  0x00008000  Compact metadata logs
  y  y  LFS3_CK_CKMETA       0x00010000  Check metadata checksums
  y  y  LFS3_CK_CKDATA       0x00020000  Check metadata + data checksums
  y  y  LFS3_CK_REPAIRMETA*  0x00040000  Repair data blocks
  y  y  LFS3_CK_REPAIRDATA*  0x00080000  Repair metadata + data blocks

  * Planned

Another option would be to document that lfs3_fs_ck accepts both
LFS3_CK_* _and_ LFS3_GC_* flags, but I worry that would be more
confusing. It would also lock us into supporting all LFs3_GC_* flags in
lfs3_fs_ck, which may not always be the case.

Though this is an argument for doing away with the whole
LFS3_M/F/CK/GC/I_* duplication... (tbh another reason for this is to
reduce the number of namespaces by at least one).

No code changes.
2025-11-18 00:56:39 -06:00
Christopher Haster 5c0cebb00b ck: Traded ckmeta/ckdata for flag-based ck functions
TLDR: Replaced lfs3_file_ckmeta/ckdata and lfs3_fs_ckmeta/ckdata with
flag based ck functions:

- lfs3_file_ckmeta -> lfs3_file_ck + LFS3_CK_CKMETA
- lfs3_file_ckdata -> lfs3_file_ck + LFS3_CK_CKDATA
- lfs3_fs_ckmeta -> lfs3_fs_ck + LFS3_FSCK_CKMETA
- lfs3_fs_ckdata -> lfs3_fs_ck + LFS3_FSCK_CKDATA

Note lfs3_fs_ck is equivalent to lfs3_fs_gc, but:

1. Performs the work in one call (equivalent to littlefs2's lfs2_fs_gc)
2. Takes flags at call time (like lfs3_mount) instead of cfg time (like
   lfs3_fs_gc)
3. Avoids the constant RAM necessary to track incremental GC state

---

Motivation:

I've been thinking: It's a bit weird that users are able to one-shot
janitorial work in lfs3_mount, but there's no equivalent function after
the filesystem is mounted.

Originally this is what lfs3_fs_gc was for, but after adding support for
incremental GC, it made sense to hide lfs3_fs_gc behind the opt-in
LFS3_GC ifdef due to the extra (ironically non-gc-able) state.

In theory lfs3_trv_t fills a bit of the gap, but, without the internal
i_flag handling and traversal restarts, it's a bit hard to use. And
basically requires duplicating said log, which we need anyways for
lfs3_mount!

So ideally we'd add an explicit one-shot GC function, but now lfs3_fs_gc
is taken.

While thinking about alternative names, I realized we can just call this
lfs3_fs_ck and completely replace lfs3_fs_ckmeta/ckdata.

This has some extra benefits:

- Avoids an explosion of ckmeta/ckdata/repairmeta/repairdata functions
- Discourages redundant traversals that could accomplish more work
- Makes it less confusing that ckdata implies ckmeta

---

I also tweaked lfs3_file_ck to match, but note that lfs3_file_ck is
internally very different from lfs3_fs_ck. For one, lfs3_file_ck only
supports "actual" check flags (LFS3_CK_*) vs all gc flags (LFS3_FSCK_*):

lfs3_file_ck:

  LFS3_CK_CKMETA          0x00010000  Check metadata checksums
  LFS3_CK_CKDATA          0x00020000  Check metadata + data checksums
  LFS3_CK_REPAIRMETA*     0x00040000  Repair metadata blocks
  LFS3_CK_REPAIRDATA*     0x00080000  Repair metadata + data blocks

  * Planned

lfs3_fs_ck:

  LFS3_FSCK_MKCONSISTENT  0x00000800  Make the filesystem consistent
  LFS3_FSCK_LOOKAHEAD     0x00001000  Repopulate lookahead buffer
  LFS3_FSCK_LOOKGBMAP     0x00002000  Repopulate the gbmap
  LFS3_FSCK_PREERASE*     0x00004000  Pre-erase unused blocks
  LFS3_FSCK_COMPACTMETA   0x00008000  Compact metadata logs
  LFS3_FSCK_CKMETA        0x00010000  Check metadata checksums
  LFS3_FSCK_CKDATA        0x00020000  Check metadata + data checksums
  LFS3_FSCK_REPAIRMETA*   0x00040000  Repair metadata blocks
  LFS3_FSCK_REPAIRDATA*   0x00080000  Repair metadata + data blocks

  * Planned

As a plus, this also saves a bit of code:

                 code          stack          ctx
  before:       35968           2280          660
  after:        35924 (-0.1%)   2280 (+0.0%)  660 (+0.0%)

                 code          stack          ctx
  gbmap before: 38828           2296          772
  gbmap after:  38812 (-0.0%)   2296 (+0.0%)  772 (+0.0%)
2025-11-18 00:56:32 -06:00
Christopher Haster 867d201bce Bumped seek whence up to uint32_t
I can't think of a reason this should be uint8_t. Bumping it up to
uint32_t matches the type used for other flags (even though whence is
arguably not flags in a strict sense).

No code changes.
2025-11-18 00:56:22 -06:00
Christopher Haster ca678538d4 Adopted lowercase => internal pattern for LFS3_tag_* tags
This includes the mask/rm/grow bits:

- LFS3_tag_RM
- LFS3_tag_GROW
- LFS3_tag_MASK0/2/8/12

Our in-device only handle types:

- LFS3_tag_ORPHAN
- LFS3_tag_TRV
- LFS3_tag_UNKNOWN

And in-device only tags with special behavior:

- LFS3_tag_INTERNAL
- LFS3_tag_RATTRS
- LFS3_tag_SHRUBCOMMIT
- LFS3_tag_GRMPUSH
- LFS3_tag_MOVE
- LFS3_tag_ATTRS

Usually I'm not a big fan of case-sensitive naming patterns, but this
has been useful for self-documenting what compat flags are in-device
only. Might as well extend the idea to our tag definitions.
2025-11-18 00:56:13 -06:00
Christopher Haster 0f30021a0d Moved most on-disk definitions into lfs3.h
Having on-disk definitions in one place is useful for referencing them
later, even if they aren't relevant for most API users.

.h files in C are already forced to expose a bunch of internal details
anyways, in order to provide struct size/alignment. Might as well
include on-disk information that would have even bigger consequences if
it changed.

Moved:

- Compat flag definitions
- Tag definitions
- DSIZEs and relevant encoding comments - Note some of these were
  already required to define lfs3_t
2025-11-18 00:56:03 -06:00
Christopher Haster 8233ac9dfe Renamed RELOOKAHEAD -> LOOKAHEAD, REGBMAP -> LOOKGBMAP
Yeah, after using these for a bit, the RE* names were not great.

Trying LOOK* now, as an alternative that hopefully still implies the
similar behavior without needing an additional prefix for LOOKAHEAD:

- LFS3_*_RELOOKAHEAD        -> LFS3_*_LOOKAHEAD
- LFS3_*_REGBMAP            -> LFS3_*_LOOKGBMAP
- cfg.regbmap_thresh        -> cfg.lookgbmap_thresh
- cfg.gc_relookahead_thresh -> cfg.gc_lookahead_thresh
- cfg.gc_regbmap_thresh     -> cfg.gc_lookgbmap_thresh
2025-11-13 16:14:56 -06:00
Christopher Haster 4ccc8dc120 Added support for all mount-traversal flags in lfs3_format
I mean, why not? These redirect to the same internal lfs3_fs_gc_
function anyways. Might as well keep things consistent.

Added:

  LFS3_F_MKCONSISTENT  0x00000800  Make the filesystem consistent
  LFS3_F_RELOOKAHEAD   0x00001000  Repopulate lookahead buffer

LFS3_F_MKCONSISTENT is guaranteed to be a noop, but LFS3_F_RELOOKAHEAD
forces a filesystem traversal, which may have some niche use case.

No code changes.
2025-11-13 16:14:56 -06:00
Christopher Haster b01a385bc9 Added LFS3_F_REGBMAP and LFS3_F_COMPACTMETA
These are unlikely to make much progress, but that doesn't seem like a
great reason to disallow these flags in lfs3_format:

  LFS3_F_REGBMAP      0x00002000  Repopulate the gbmap
  LFS3_F_COMPACTMETA  0x00008000  Compact metadata logs

These are actually guaranteed to do _no_ work when formatting _without_
the gbmap, but with the gbmap it's less clear. Looking forward to the
planned ckfactory feature, these may be useful for cleaning up any rbyd
commits created as a part of building the initial gbmap.

---

Also tweaked the formatting for LFS3_F_* flags a bit, including making
all ifdefs explicit (mainly ifdef LFS3_RDONLY). Mixed ifdefs are a real
pain to read.

No code changes.
2025-11-13 16:14:56 -06:00
Christopher Haster 9e75138f7a Rearranged O/M/F/GC/I flags
Now that we don't need to encode tstate info in our traversal flags, we
can move things around to be a bit more comfortable.

This is also after some tweaking to make space for planned features:

O flags:

  O_MODE             0x00000003  ---- ---- ---- ---- ---- ---- ---- --11
  O_RDONLY           0x00000000  ---- ---- ---- ---- ---- ---- ---- ----
  O_WRONLY           0x00000001  ---- ---- ---- ---- ---- ---- ---- ---1
  O_RDWR             0x00000002  ---- ---- ---- ---- ---- ---- ---- --1-
  O_CREAT            0x00000004  ---- ---- ---- ---- ---- ---- ---- -1--
  O_EXCL             0x00000008  ---- ---- ---- ---- ---- ---- ---- 1---
  O_TRUNC            0x00000010  ---- ---- ---- ---- ---- ---- ---1 ----
  O_APPEND           0x00000020  ---- ---- ---- ---- ---- ---- --1- ----
  O_FLUSH            0x00000040  ---- ---- ---- ---- ---- ---- -1-- ----
  O_SYNC             0x00000080  ---- ---- ---- ---- ---- ---- 1--- ----
  O_DESYNC           0x00100000  ---- ---- ---1 ---- ---- ---- ---- ----
  O_DEDAG*           0x00000100  ---- ---- ---- ---- ---- ---1 ---- ----
  O_DEDUP*           0x00000200  ---- ---- ---- ---- ---- --1- ---- ----
  O_COMPR?           0x00000400  ---- ---- ---- ---- ---- -1-- ---- ----

  O_CKMETA           0x00010000  ---- ---- ---- ---1 ---- ---- ---- ----
  O_CKDATA           0x00020000  ---- ---- ---- --1- ---- ---- ---- ----
  O_REPAIRMETA*      0x00040000  ---- ---- ---- -1-- ---- ---- ---- ----
  O_REPAIRDATA*      0x00080000  ---- ---- ---- 1--- ---- ---- ---- ----

  o_WRSET            0x00000003  ---- ---- ---- ---- ---- ---- ---- --11
  o_TYPE             0xf0000000  1111 ---- ---- ---- ---- ---- ---- ----
  o_ZOMBIE           0x08000000  ---- 1--- ---- ---- ---- ---- ---- ----
  o_UNCREAT          0x04000000  ---- -1-- ---- ---- ---- ---- ---- ----
  o_UNSYNC           0x02000000  ---- --1- ---- ---- ---- ---- ---- ----
  o_UNCRYST          0x01000000  ---- ---1 ---- ---- ---- ---- ---- ----
  o_UNGRAFT          0x00800000  ---- ---- 1--- ---- ---- ---- ---- ----
  o_UNFLUSH          0x00400000  ---- ---- -1-- ---- ---- ---- ---- ----

  * Planned
  ? Hypothetical

T flags:

  T_MODE             0x00000001  ---- ---- ---- ---- ---- ---- ---- ---1
  T_RDONLY           0x00000000  ---- ---- ---- ---- ---- ---- ---- ----
  T_RDWR             0x00000001  ---- ---- ---- ---- ---- ---- ---- ---1
  T_MTREEONLY        0x00000002  ---- ---- ---- ---- ---- ---- ---- --1-
  T_EXCL             0x00000008  ---- ---- ---- ---- ---- ---- ---- 1---
  T_MKCONSISTENT     0x00000800  ---- ---- ---- ---- ---- 1--- ---- ----
  T_RELOOKAHEAD      0x00001000  ---- ---- ---- ---- ---1 ---- ---- ----
  T_REGBMAP          0x00002000  ---- ---- ---- ---- --1- ---- ---- ----
  T_PREERASE*        0x00004000  ---- ---- ---- ---- -1-- ---- ---- ----
  T_COMPACTMETA      0x00008000  ---- ---- ---- ---- 1--- ---- ---- ----
  T_CKMETA           0x00010000  ---- ---- ---- ---1 ---- ---- ---- ----
  T_CKDATA           0x00020000  ---- ---- ---- --1- ---- ---- ---- ----
  T_REPAIRMETA*      0x00040000  ---- ---- ---- -1-- ---- ---- ---- ----
  T_REPAIRDATA*      0x00080000  ---- ---- ---- 1--- ---- ---- ---- ----

  t_EVICT*           0x00000010  ---- ---- ---- ---- ---- ---- ---1 ----
  t_TYPE             0xf0000000  1111 ---- ---- ---- ---- ---- ---- ----
  t_ZOMBIE           0x08000000  ---- 1--- ---- ---- ---- ---- ---- ----
  t_CKPOINTED        0x04000000  ---- -1-- ---- ---- ---- ---- ---- ----
  t_DIRTY            0x02000000  ---- --1- ---- ---- ---- ---- ---- ----
  t_STALE            0x01000000  ---- ---1 ---- ---- ---- ---- ---- ----
  t_BTYPE            0x00f00000  ---- ---- 1111 ---- ---- ---- ---- ----

  * Planned

M/F flags:

  M_MODE             0x00000001  ---- ---- ---- ---- ---- ---- ---- ---1
  M_RDWR             0x00000000  ---- ---- ---- ---- ---- ---- ---- ----
  M_RDONLY           0x00000001  ---- ---- ---- ---- ---- ---- ---- ---1
  M_STRICT?          0x00000002  ---- ---- ---- ---- ---- ---- ---- --1-
  M_FORCE?           0x00000004  ---- ---- ---- ---- ---- ---- ---- -1--
  M_FORCEWITHRECKLESSABANDON?
                     0x00000008  ---- ---- ---- ---- ---- ---- ---- 1---
  M_FLUSH            0x00000040  ---- ---- ---- ---- ---- ---- -1-- ----
  M_SYNC             0x00000080  ---- ---- ---- ---- ---- ---- 1--- ----
  M_DEDAG*           0x00000100  ---- ---- ---- ---- ---- ---1 ---- ----
  M_DEDUP*           0x00000200  ---- ---- ---- ---- ---- --1- ---- ----
  M_COMPR?           0x00000400  ---- ---- ---- ---- ---- -1-- ---- ----
  M_REVDBG           0x00000010  ---- ---- ---- ---- ---- ---- ---1 ----
  M_REVNOISE         0x00000020  ---- ---- ---- ---- ---- ---- --1- ----
  M_CKPROGS          0x00100000  ---- ---- ---1 ---- ---- ---- ---- ----
  M_CKFETCHES        0x00200000  ---- ---- --1- ---- ---- ---- ---- ----
  M_CKMETAPARITY     0x00400000  ---- ---- -1-- ---- ---- ---- ---- ----
  M_CKMETAREDUND*    0x00800000  ---- ---- 1--- ---- ---- ---- ---- ----
  M_CKDATACKSUMS     0x01000000  ---- ---1 ---- ---- ---- ---- ---- ----
  M_CKREADS*         0x01800000  ---- ---1 1--- ---- ---- ---- ---- ----

  M_MKCONSISTENT     0x00000800  ---- ---- ---- ---- ---- 1--- ---- ----
  M_RELOOKAHEAD      0x00001000  ---- ---- ---- ---- ---1 ---- ---- ----
  M_REGBMAP          0x00002000  ---- ---- ---- ---- --1- ---- ---- ----
  M_PREERASE*        0x00004000  ---- ---- ---- ---- -1-- ---- ---- ----
  M_COMPACTMETA      0x00008000  ---- ---- ---- ---- 1--- ---- ---- ----
  M_CKMETA           0x00010000  ---- ---- ---- ---1 ---- ---- ---- ----
  M_CKDATA           0x00020000  ---- ---- ---- --1- ---- ---- ---- ----
  M_REPAIRMETA*      0x00040000  ---- ---- ---- -1-- ---- ---- ---- ----
  M_REPAIRDATA*      0x00080000  ---- ---- ---- 1--- ---- ---- ---- ----

  F_CKFACTORY*       0x00000002  ---- ---- ---- ---- ---- ---- ---- --1-
  F_GBMAP            0x02000000  ---- --1- ---- ---- ---- ---- ---- ----
  F_GDDTREE*         0x04000000  ---- -1-- ---- ---- ---- ---- ---- ----
  F_GPTREE*          0x08000000  ---- 1--- ---- ---- ---- ---- ---- ----

  F_METAR1*          0x10000000  ---1 ---- ---- ---- ---- ---- ---- ----
  F_METAR2*          0x20000000  --1- ---- ---- ---- ---- ---- ---- ----
  F_METAR3*          0x30000000  --11 ---- ---- ---- ---- ---- ---- ----
  F_DATAR1*          0x40000000  -1-- ---- ---- ---- ---- ---- ---- ----
  F_DATAR2*          0x80000000  1--- ---- ---- ---- ---- ---- ---- ----
  F_DATAR3*          0xc0000000  11-- ---- ---- ---- ---- ---- ---- ----

  * Planned
  ? Hypothetical

It's a bit concerning that _all_ 32-bit mount flags end up used, but
what can you do...

Code changes minimal:

                 code          stack          ctx
  before:       35964           2280          660
  after:        35968 (+0.0%)   2280 (+0.0%)  660 (+0.0%)

                 code          stack          ctx
  gbmap before: 38828           2296          772
  gbmap after:  38828 (+0.0%)   2296 (+0.0%)  772 (+0.0%)
2025-11-13 16:13:24 -06:00
Christopher Haster 673fa7876f Reduced the scope of LFS3_REVDBG/REVNOISE
LFS3_REVDBG introduced a lot of overhead for something I'm not sure
anyone will actually use (I have enough tooling that the state of an
rbyd is rarely a mystery, see dbgbmap.py). That, and we're running out
of flags!

So this reduces LFS3_REVDBG to just store one of "himb" in the first
(lowest) byte of the revision count; information that is easily
available:

  vvvv---- -------- -------- --------
  vvvvrrrr rrrrrr-- -------- --------
  vvvvrrrr rrrrrrnn nnnnnnnn nnnnnnnn
  vvvvrrrr rrrrrrnn nnnnnnnn dddddddd
  '-.''----.----''----.- - - '---.--'
    '------|----------|----------|---- 4-bit relocation revision
           '----------|----------|---- recycle-bits recycle counter
                      '----------|---- pseudorandom noise (if revnoise)
                                 '---- h, i, m, or b (if revdbg)
                             -11-1---  - h = mroot anchor
                             -11-1--1  - i = mroot
                             -11-11-1  - m = mdir
                             -11---1-  - b = btree node

Some other notes:

- Enabled LFS3_REVDBG and LFS3_REVNOISE to work together, now that
  LFS3_REVDBG doesn't consume all unused rev bits.

  Note that LFS3_REVDBG has priority over LFS3_REVNOISE, but _not_
  recycle-bits, etc. Otherwise problems would happen for recycle-bits
  >2^20 (though do we care?).

- Fixed an issue where using the gcksum as a noise source results in
  noise=0 when there is only an mroot. This is due to how we xor out
  the current mdir cksum during an mdir commit.

  Fixed by using gcksum_p instead of gcksum.

- Added missing LFS3_I_REVDBG/REVNOISE flags in the tests, so now you
  can actually run the tests with LFS3_REVDBG/REVNOISE (this probably
  just fell out-of-date at some point).

---

Curiously, despite LFS3_REVDBG/REVNOISE being disabled by default, this
did save some code. I'm guessing the non-tail-call mtree/gbmap commit
functions prevented some level of inlining?:

                 code          stack          ctx
  before:       35964           2280          660
  after:        35964 (+0.0%)   2280 (+0.0%)  660 (+0.0%)

                 code          stack          ctx
  gbmap before: 38940           2296          772
  gbmap after:  38828 (-0.3%)   2296 (+0.0%)  772 (+0.0%)
2025-11-13 01:44:37 -06:00
Christopher Haster 4010afeafd trv: Reintroduced LFS3_T_EXCL
With the relaxation of traversal behavior under mutation, I think it
makes sense to bring back LFS3_T_EXCL. If only to allow traversals to
gaurantee termination under mutation. Now that traversals no longer
guarantee forward progress, it's possible to get stuck looping
indefinitely if the filesystem is constantly being mutated.

Non-excl traversals are probably still useful for GC work and debugging
threads, but LFS3_T_EXCL now allows traversals to terminate immediately
with LFS3_ERR_BUSY at the first sign of unrelated filesystem mutation:

  LFS3_T_EXCL  0x00000008  Error if filesystem modified

Internally, we already track unrelated mutation to avoid corrupt state
(LFS3_t_DIRTY), so this is a very low-cost feature:

                 code          stack          ctx
  before:       35944           2280          660
  after:        35964 (+0.1%)   2280 (+0.0%)  660 (+0.0%)

                 code          stack          ctx
  gbmap before: 38916           2296          772
  gbmap after:  38940 (+0.1%)   2296 (+0.0%)  772 (+0.0%)

                 code          stack          ctx
  gc before:    36016           2280          768
  gc after:     36036 (+0.1%)   2280 (+0.0%)  768 (+0.0%)
2025-11-12 13:30:11 -06:00
Christopher Haster e9f2944573 Renamed bshrub.shrub[_] -> bshrub.b[_]
Mostly for consistency with mtrv.b and gbmap.b, but also (1) this
hopefully reduces confusion around the fact that these can refer to both
bshrubs and btrees, and (2) saves a bit of typing with the messy struct
namespaces forced by C's strict aliasing.
2025-11-08 22:31:46 -06:00
Christopher Haster 14c369af93 trv: Adopted LFS3_t_STALE for marking block queue as stale
This solves the previous gc-needs-block-queue-so-we-can-clobber-block-
queue issue by adding an additional LFS3_t_STALE flag to indicate when
any block queues would be invalid.

So instead of clearing block queues in lfs3_alloc_ckpoint, we just set
LFS3_t_STALE, and any lfs3_trv_ts can clear their block queues in
lfs3_trv_read. This allows lfs3_mgc_ts to be allocated without a block
queue when doing any LFS3_M_*/LFS3_F_*/LFS3_GC_* work.

LFS3_t_STALE is set at the same time as LFS3_t_CKPOINT and LFS3_t_DIRTY,
but we need a separate bit so lfs3_trv_read can clear the flag after
flushing without losing ckpoint/dirty information.

---

Unfortunately, none of the stack-allocated lfs3_mgc_ts are on the stack
hot-path, so we don't immediate savings. But note the 2-words saved in
ctx when compiling in LFS3_GC mode:

                 code          stack          ctx
  before:       35940           2280          660
  after:        35944 (+0.0%)   2280 (+0.0%)  660 (+0.0%)

                 code          stack          ctx
  gbmap before: 38916           2296          772
  gbmap after:  38916 (+0.0%)   2296 (+0.0%)  772 (+0.0%)

                 code          stack          ctx
  gc before:    36012           2280          776
  gc after:     36016 (+0.0%)   2280 (+0.0%)  768 (-1.0%)
2025-11-08 22:31:42 -06:00
Christopher Haster d1d69c0a52 trv: Greatly simplified filesystem traversal
The main idea here is to drop the flag-encoded tstate state machine, and
replace it with a matrix controlled by special mid + bid values:

                    -- mid ->
             -5   -4   -3   -2 >=-1
  bid   -2    x    x              x  --> mdir
   v  >=-1         x  gbm  gbm    x  --> bshrub/btree

              '----|----|----|----|----> mroot anchor
                   '----|----|----|----> mroot chain + mtree
                        '----|----|----> gbmap   (in-ram gbmap)
                             '----|----> gbmap_p (on-disk gbmap)
                                  '----> file bshrubs/btrees

This was motivated by the observation that everything in our filesystem
can be modeled as mdir + bshrub/btree tuples, as long as some states are
noops. And we can cleanly encode these tuples in the unused negative
mid + bid ranges without needing an explicit state machine.

Well, that and the previous tstate state machine approach being an ugly
pile of switch cases and messy logic.

Note though that some mids may need to traverse multiple mdirs/bshrub/
btrees:

- The mroot chain + mtree (mid=-4) needs to traverse all mroots in the
  mroot chain, and detect any cycles.

- File mdirs (mid>=-1) need to traverse both the on-disk bshrub/btree
  and any opened file handles' bshrubs/btrees before moving onto the
  next mid.

  This grows O(n^2) because all file handles are in one big unsorted
  linked-list, but as usual we don't care.

In addition to the greatly simplified traversal logic, the new state
matrix simplifies traversal clobbering: Setting bid=-2 always forces a
bshrub/btree refetch.

This comes at the cost of traversal _precision_, i.e. we can now revisit
previously visited bshrub/btree nodes. But I think this is well worth it
for more robust traversal clobbering. Traversal clobbering is delicate
and difficult to get right.

Besides, we can already revisit blocks due to CoW references, so what's
the harm in revisiting blocks when under mutation?

---

The simpler traversal logic leads to a nice amount of code savings
across the board:

                 code          stack          ctx
  before:       36476           2304          660
  after:        35940 (-1.5%)   2280 (-1.0%)  660 (+0.0%)

                 code          stack          ctx
  gbmap before: 39524           2320          772
  gbmap after:  38916 (-1.5%)   2296 (-1.0%)  772 (+0.0%)

                 code          stack          ctx
  gc before:    36548           2304          804
  gc after:     36012 (-1.5%)   2280 (-1.0%)  776 (-3.5%)

Note the ctx savings in LFS3_GC mode. Most of the stack/ctx savings
comes from the smaller lfs3_mtrv_t struct, which no longer needs to
stage bshrubs (we no longer care about bshrubs across mdir commit as a
part of the above clobbering simplifications):

                before  after
  lfs3_mtrv_t:     128    100 (-21.9%)
  lfs3_mgc_t:      128    100 (-21.9%)
  lfs3_trv_t:      136    108 (-20.6%)

Unfortunately, the simpler clobbering means now any gc work needs the
block queue (i.e. lfs3_trv_t), solely so clobbering the block queue
doesn't clobber unallocated memory. Not great but hopefully fixable.

---

Some other notes:

- As a part of simplifying traversal clobbering, everything is triggered
  by lfs3_alloc_ckpoint (via lfs3_trv_ckpoint_).

  This may clobber traversals more than is strictly necessary, but
  that's kinda the idea. Better safe than sorry.

  And no more need to explicit lfs3_handle_clobber calls is nice.

- Opened file handle iteration is now tracked by the traversal handle's
  position in the handle linked-list, instead of a separate handle
  pointer. This means one less thing to disentangle and makes traversals
  no longer a special case for things like lfs3_handle_close.

  You may think this bumps traversals up to O(n^3) in-ram, but because
  we only ever visit each unique handle + mid once, we can keep the
  total O(n^2) if we're smart about linked-list updates!

- lfs3_mdir_commit needed to be tweaked to accept mids<=-1, instead of
  just mid=-1 for the mroot. Unfortunately I don't know how much this
  costs on its own.

- The reorganization of lfs3_mtrv_t means lfs3_mtortoise_t gets its own
  struct again!

- No more tstate state machine also frees up a big chunk of the
  traversal flag space, which was getting pretty cramped.
2025-11-08 19:46:22 -06:00
Christopher Haster a01b1b73b2 btree: Moved leaf caching behind LFS3_BLEAFCACHE ifdef
This is motivated by the observation that the O(n log_b n) btree
iteration really just hasn't been a bottleneck in our benchmarks.

Our write performance is mostly dominated by compaction costs, and while
filesystem _traversals_ are a concern, it's easy to explicitly track
rbyds in lfs3_btrv_t.

Additionally:

- We track mdirs during mtree iteration, which are the true mtree
  leaves.

- We already cache file leaves, i.e. bptrs and read-fragments.

On top of this, leaf caching adds complexity, both in terms of
code/stack costs, but also in terms of reliability. It introducing the
need for cache invalidation, which is infamously one of the two hard
problems in computer science!

This is the second(?) time btree leaf traversals have been reverted, so
see previous commit messages for even more arguments against.

---

Eventually, we should probably just delete the btree leaf cache logic to
avoid the maintenance headache (cache invalidation + opt+in/less
testing = ouch). But I want to do a bit more benchmarking comparing the
two modes, so just moving this behind an ifdef for now.

Saves code, and of course RAM:

                              code          stack          ctx
  before btrv:               37160           2352          688
  before:                    37088 (-0.2%)   2384 (+1.4%)  688 (+0.0%)
  after:                     36480 (-1.8%)   2304 (-2.0%)  660 (-4.1%)

But note while this keeps the performance implications of btree leaf
caching, it does not keep the code/stack optimizations that internally
reuse the leaf cache for things (btrv, lookupnext_ rbyd side-channel,
etc).

In _theory_ these could have been kept with enough ifdefs, but it would
have made the codebase quite a bit of a hell to maintain:

                              code          stack          ctx
  always-bleafcache:         37160           2352          688
  no-bleafcache:             36480 (-1.8%)   2304 (-2.0%)  660 (-4.1%)
  yes-bleafcache:            37044 (-0.3%)   2384 (+1.4%)  688 (+0.0%)

Gbmap mode has even more savings due to how many gbmap copies we have
flying around:

                              code          stack          ctx
  gbmap + always-bleafcache: 40132           2368          856
  gbmap + no-bleafcache:     39464 (-1.7%)   2320 (-2.0%)  772 (-9.8%)
  gbmap + yes-bleafcache:    40052 (-0.2%)   2400 (+1.4%)  856 (+0.0%)

---

In the future, _maybe_ we can revisit this. But I think a better design
would be to cache btree leaves globally, in lfs3_t, similarly to the
theoretical mdir cache. This would allow a user-configurable number of
cached btree nodes, and may make cache invalidation easier.

Note, however, that btree nodes don't need to be fetched (even for
commits now!), so the benefits would be much smaller than for the
theoretical mdir cache.

But hey, it would defend the lack of low-level rbyd tracking during
iteration/rattr queries!
2025-10-26 15:33:27 -05:00
Christopher Haster 39a265ce90 btree: Dropped reliance on leaf cache during traversals
Brings back lfs3_btrv_t, but keeps some of the btree internal changes.

I think the biggest one is dropping the internal branch pointer, now
instead of internally pointing to the root rbyd, we just unconditionally
sync the rbyd state anytime the rbyd matches the root's weight. This is
necessary to avoid out-of-sync state when traversing bshrubs under
mutation.

Also after refactoring I think the current btree traversal logic is
easier to read.

---

This is in preparation for removing the leaf cache, or at least making
it opt-in.

It adds a chunk of stack, but in theory we can reclaim this by allowing
leaf caches to be disabled:

           code          stack          ctx
  before: 37160           2352          688
  after:  37088 (-0.2%)   2384 (+1.4%)  688 (+0.0%)
2025-10-25 16:54:41 -05:00
Christopher Haster 5d905e6da4 Dropped LFS3_KVONLY and LFS3_2BONLY modes for now
I think these are good ideas to bring back when littlefs3 is more
mature, but at the moment the number of different builds is creating too
much friction.

LFS3_KVONLY and LFS3_2BONLY in particular _add_ significant chunks of
code (lfs3_file_readget_, lfs3_file_flushset_, and various extra logic
sprinkled throughout the codebase), and the current state of testing
means I have no idea if any of it still works.

These are also low-risk for introducing any disk related changes.

So, ripping out for now to keep the current experimental development
tractable. May reintroduce in the future (probably after littlefs3 is
stabilized) if there is sufficient user interest. But doing so will
probably also need to come with actual testing in CI.
2025-10-24 00:20:53 -05:00
Christopher Haster 207446223b rdonly: Fixed various LFS3_RDONLY compile errors
This just fell out-of-sync a bit during the gbmap work. Note we _do_
support LFS3_RDONLY + LFS3_GBMAP, as fetching the gbmap is necessary for
CKMETA to check all metadata. Fortunately this is relatively cheap:

                 code          stack          ctx
  rdonly:       10716            896          532
  rdonly+gbmap: 10988 (+2.5%)    896 (+0.0%)  680 (+27.8%)

Though this does highlight that a sort of LFS3_NO_TRV mode could remove
quite a bit of code.
2025-10-24 00:19:49 -05:00
Christopher Haster 3ab7ecb2b0 Renamed file_cache -> fcache and gbmap_re -> regbmap
This walks back some of the attempt at strict object namespacing in
struct lfs3_cfg:

- cfg.file_cache_size  -> cfg.fcache_size
- filecfg.cache_size   -> filecfg.fcache_size
- filecfg.cache_buffer -> filecfg.fcache_buffer
- cfg.gbmap_re_thresh  -> cfg.regbmap_thresh

Motivation:

- cfg.regbmap_thresh now matches cfg.gc_regbmap_thresh, instead of using
  awkwardly different namespacing patterns.

- Giving fcache a more unique name is useful for discussion. Having
  pcache, rcache, and then file_cache was a bit awkward.

  Hopefully it's also more clear that cfg.fcache_size and
  filecfg.fcache_size are related.

- Config in struct lfs3_cfg is named a bit more consistently, well, if
  you ignore gc_*_* options.

- Less typing.

Though this gets into pretty subjective naming territory. May revert
this if the new terms are uncomfortable after use.
2025-10-24 00:18:54 -05:00
Christopher Haster b49d9e9ece Renamed REPOP* -> RE*
So:

- cfg.gc_repoplookahead_thresh -> cfg.gc_relookahead_thresh
- cfg.gc_repopgbmap_thresh     -> cfg.gc_regbmap_thresh
- cfg.gbmap_repop_thresh       -> cfg.gbmap_re_thresh
- LFS3_*_REPOPLOOKAHEAD        -> LFS3_*_RELOOKAHEAD
- LFS3_*_REPOPGBMAP            -> LFS3_*_REGBMAP

Mainly trying to reduce the mouthful that is REPOPLOOKAHEAD and
REPOPGBMAP.

As a plus this also avoids potential confusion of "repop" as a push/pop
related operation.
2025-10-24 00:16:37 -05:00
Christopher Haster 8a58954828 trv: Reduced LFS3_t_CKPOINTED + LFS3_t_MUTATED -> LFS3_t_CKPOINTED
This drops LFS3_t_MUTATED in favor of just using LFS3_t_CKPOINTED
everywhere:

1. These meant roughly the same thing, with LFS3_t_MUTATED being a bit
   tighter at the cost of needing to be explicitly set.

2. The implicit setting of LFS3_t_CKPOINTED by lfs3_alloc_ckpoint -- a
   function that already needs to be called before mutation -- means we
   have one less thing to worry about.

   Implicit properties like LFS3_t_CKPOINTED are great for building a
   reliable system. Manual flags like LFS3_t_MUTATED, not so much.

3. Why use two flags when we can get away with one?

The only downside is we may unnecessarily clobber gc/traversal work when
we don't actually mutate the filesystem. Failed file open calls are a
good example.

However this tradeoff seems well worth it for an overall simpler +
more reliable system.

---

Saves a bit of code:

                 code          stack          ctx
  before:       37220           2352          688
  after:        37160 (-0.2%)   2352 (+0.0%)  688 (+0.0%)

                 code          stack          ctx
  gbmap before: 40184           2368          856
  gbmap after:  40132 (-0.1%)   2368 (+0.0%)  856 (+0.0%)
2025-10-24 00:12:32 -05:00
Christopher Haster 5d70e47708 trv: Reverted LFS3_t_NOSPC, forward gbmap repop errors
Note: This affects the blocking lfs3_alloc_repopgbmap as well as
incremental gc/traversal repopulations. Now all repop attempts return
LFS3_ERR_NOSPC when we don't have space for the gbmap, motivation below.

This reverts the previous LFS3_t_NOSPC soft error, in which traversals
were allowed to continue some gc/traversal work when encountering
LFS3_ERR_NOSPC. This results in a simpler implementation and fewer error
cases to worry about.

Observation/motivation:

- The main motivation is noticing that when we're in low-space
  conditions, we just start spamming gbmap repops even if they all fail.

  That's really not great! We might as well just mark the flash as dead
  if we're going to start spamming erases!

  At least with an error the user can call rmgbmap to try to make
  progress.

- If we're in a low-space condition, something else will probably return
  LFS3_ERR_NOSPC anyways. Might as well report this early and simplify
  our system.

- It's a simpler model, and littlefs3 is already much more complicated
  than littlefs2. Maybe we should lean more towards a simpler system
  at the cost of some niche optimizations.

---

This had the side-effect of causing more lfs3_alloc_ckpoints to return
errors during testing, which revealed a bug in our uz/uzd_fuzz tests:

- We weren't flushing after writes to the opened RDWR files, which could
  cause delayed errors to occur during the later read checks in the
  test.

  Fortunately LFS3_O_FLUSH provides a quick and easy fix!

  Note we _don't_ adopt this in all uz/uzd_fuzz tests, only those that
  error. It's good to test both with and without LFS3_O_FLUSH to test
  that read-flushing also works under stress.

Saves a bit of code:

                 code          stack          ctx
  before:       37260           2352          688
  after:        37220 (-0.1%)   2352 (+0.0%)  688 (+0.0%)

                 code          stack          ctx
  gbmap before: 40220           2368          856
  gbmap after:  40184 (-0.1%)   2368 (+0.0%)  856 (+0.0%)
2025-10-24 00:03:14 -05:00
Christopher Haster f892d299dd trv: Added LFS3_t_NOSPC, avoid ENOSPC errors in traversals
This relaxes error encountered during lfs3_mtree_gc to _not_ propagate,
but instead just log a warning and prevent the relevant work from being
checked off during EOT.

The idea is this allows other work to make progress in low-space
conditions.

I originally meant to limit this to gbmap repopulations, to match the
behavior of lfs3_alloc_repopgbmap, but I think extending the idea to all
filesystem mutating operations makes sense (LFS3_T_MKCONSISTENT +
LFS3_T_REPOPGBMAP + LFS3_T_COMPACTMETA).

---

To avoid incorrectly marking traversal work as completed, we need to
track if we hit any ENOSPC errors, thus the new LFS3_t_NOSPC flag:

  LFS3_t_NOSPC  0x00800000  Optional gc work ran out of space

Not the happiest just throwing flags at problems, but I can't think of a
better solution at the moment.

This doesn't differentiate between ENOSPC errors during the different
types of work, but in theory if we're hitting ENOSPC errors whatever
work returns the error is a toss-up anyways.

---

Adds a bit of code:

                 code          stack          ctx
  before:       37208           2352          688
  after:        37248 (+0.1%)   2352 (+0.0%)  688 (+0.0%)

                 code          stack          ctx
  gbmap before: 40120           2368          856
  gbmap after:  40204 (+0.2%)   2368 (+0.0%)  856 (+0.0%)
2025-10-24 00:00:39 -05:00
Christopher Haster 12874bff76 gbmap: Added gc_repoplookahead_thresh and gc_repopgbmap_thresh
To allow relaxing when LFS3_I_REPOPLOOKAHEAD and LFS3_I_REPOPGBMAP will
be set, potentially reducing gc workload after allocating only a couple
blocks.

The relevant cfg comments have quite a bit more info.

Note -1 (not the default, 0, maybe we should explicitly flip this?)
restores the previous functionality of setting these flags on the first
block allocation.

---

Also tweaked gbmap repops during gc/traversals to _not_ try to repop
unless LFS3_I_REPOPGBMAP is set. We probably should have done this from
the beginning since repopulating the gbmap writes to disk and is
potentially destructive.

Adds code, though hopefully we can claw this back with future config
rework:

                 code          stack          ctx
  before:       37176           2352          684
  after:        37208 (+0.1%)   2352 (+0.0%)  688 (+0.6%)

                 code          stack          ctx
  gbmap before: 40024           2368          848
  gbmap after:  40120 (+0.2%)   2368 (+0.0%)  856 (+0.9%)
2025-10-23 23:56:50 -05:00
Christopher Haster 1dc1a26f11 gc: Added LFS3_GC_ALL to make running all gc work easier
This is an alias for all possible gc work, which is a bit more
complicated than you might think due to compile-time features (example:
LFS3_GC_REPOPGBMAP).

The intention is to make loops like the following easy to write:

  struct lfs3_fsinfo fsinfo;
  lfs3_fs_stat(&lfs3, &fsinfo) => 0;

  lfs3_trv_t trv;
  lfs3_trv_open(&lfs3, &trv, fsinfo.flags & LFS3_GC_ALL) => 0;
  ...

It's possible to do this by explicitly setting all gc flags, but that
requires quite a bit of knowledge from the user.

Another option is allowing -1 for gc/traversal flags, but that loses
assert protection against unknown/misplaced flags.

---

This raises more questions about the prefix naming: it feels a bit weird
to take LFS3_I_* flags, mask with LFS3_GC_* flags, and pass them as
LFS3_T_* flags, but it gets the job done.

Limiting LFS3_GC_ALL to the LFS3_GC_* namespace avoids issues with
opt-out/mode flags such as LFS3_T_RDONLY, LFS3_T_MTREEONLY, etc. For
this reason it probably doesn't make sense to add something similar to
the other namespaces.
2025-10-23 23:55:54 -05:00
Christopher Haster 1f824a029b Renamed LFS3_T_COMPACT -> LFS3_T_COMPACTMETA (and gc_compactmeta_thresh)
- LFS3_T_COMPACT -> LFS3_T_COMPACTMETA
- gc_compact_thresh -> gc_compactmeta_thresh

And friends:

  LFS3_M_COMPACTMETA   0x00000800  Compact metadata logs
  LFS3_GC_COMPACTMETA  0x00000800  Compact metadata logs
  LFS3_I_COMPACTMETA   0x00000800  Filesystem may have uncompacted metadata
  LFS3_T_COMPACTMETA   0x00000800  Compact metadata logs

---

This does two things:

1. Highlights that LFS3_T_COMPACTMETA only interacts with metadata logs,
   and has no effect on data blocks.

2. Better matches the verb+noun names used for other gc/traversal flags
   (REPOPGBMAP, CKMETA, etc).

It is a bit more of a mouthful, but I'm not sure that's entirely a bad
thing. These are pretty low-level flags.
2025-10-23 23:54:57 -05:00
Christopher Haster 9bdfb25a09 Renamed LFS3_T_LOOKAHEAD -> LFS3_T_REPOPLOOKAHEAD
And friends:

  LFS3_M_REPOPLOOKAHEAD   0x00000200  Repopulate lookahead buffer
  LFS3_GC_REPOPLOOKAHEAD  0x00000200  Repopulate lookahead buffer
  LFS3_I_REPOPLOOKAHEAD   0x00000200  Lookahead buffer is not full
  LFS3_T_REPOPLOOKAHEAD   0x00000200  Repopulate lookahead buffer

To match LFS3_T_REPOPGBMAP, which is more-or-less the same operation.
Though this does turn into quite the mouthful...
2025-10-23 23:54:02 -05:00
Christopher Haster ced63a4c73 Renamed inline_size -> shrub_size
There's a strong argument for naming this inline_size as that's more
likely what users expect, but shrub_size is just the more correct name
and avoids confusion around having multiple names for the same thing.

It also highlights that shrubs in littlefs3 are a bit different than
inline files in littlefs2, and that this config also affects large files
with a shrubbed root.

May rerevert this in the future, but probably only if there is
significant user confusion.
2025-10-23 23:53:02 -05:00
Christopher Haster 3b4e1e9e0b gbmap: Renamed gbmap_rebuild_thresh -> gbmap_repop_thresh
And tweaked a few related comments.

I'm still on the fence with this name, I don't think it's great, but it
at least betters describes the "repopulation" operation than
"rebuilding". The important distinction is that we don't throw away
information. Bad/erased block info (future) is still carried over into
the new gbmap snapshot, and persists unless you explicitly call
rmgbmap + mkgbmap.

So, adopting gbmap_repop_thresh for now to see if it's just a habit
thing, but may adopt a different name in the future.

As a plus, gbmap_repop_thresh is two characters shorter.
2025-10-23 23:51:18 -05:00
Christopher Haster fb90bf976c trv: Split lfs3_trv_t -> lfs3_trv_t, lfs3_mgc_t, and lfs3_mtrv_t
A big downside of LFS3_T_REBUILDGBMAP is the addition of an lfs3_btree_t
struct to _every_ traversal object.

Unfortunately, I don't see a way around this. We need to track the new
gbmap snapshot _somewhere_, and other options (such as a global gbmap.b_
snapshot) just move the RAM around without actually saving anything.

To at least mitigate this internally, this splits lfs3_trv_t into
distinct lfs3_trv_t, lfs3_mgc_t, and lfs3_mtrv_t structs that capture
only the relevant state for internal traversal layers:

- lfs3_mtree_traverse <- lfs3_mtrv_t
- lfs3_mtree_gc       <- lfs3_mgc_t (contains lfs3_mtrv_t)
- lfs3_trv_read       <- lfs3_trv_t (contains lfs3_mgc_t)

This minimizes the impact of the gbmap rebuild snapshots, and saves a
big chunk of RAM. As a plus it also saves RAM in the default build by
limiting the 2-block block queue to the high-level lfs3_trv_read API:

                 code          stack          ctx
  before:       37176           2360          684
  after:        37176 (+0.0%)   2352 (-0.3%)  684 (+0.0%)

                 code          stack          ctx
  gbmap before: 40060           2432          848
  gbmap after:  40024 (-0.1%)   2368 (-2.6%)  848 (+0.0%)

The main downside? Our field names are continuing in their
ridiculousness:

  lfs3.gc.gc.t.b.h.flags // where else would the global gc flags be?
2025-10-23 23:49:58 -05:00
Christopher Haster 06bc4dff04 trv: Simplified MUTATED/DIRTY flags, no more swapping
A bit less simplified than I hoped, we don't _strictly_ need both
LFS3_t_DIRTY + LFS3_t_MUTATED if we're ok with either (1) making
multiple passes to confirm fixorphans succeeded or (2) clear the COMPACT
flag after one pass (which may introduce new uncompacted metadata). But
both of these have downsides, and we're not _that_ stressed for flag
space yet...

So keeping all three of:

  LFS3_t_DIRTY      0x04000000  Filesystem modified outside traversal
  LFS3_t_MUTATED    0x02000000  Filesystem modified during traversal
  LFS3_t_CKPOINTED  0x01000000  Filesystem ckpointed during traversal

But I did manage to get rid of the bit swapping by tweaking LFS3_t_DIRTY
to imply LFS3_t_MUTATED instead of being exclusive. This removes the
"failed" gotos in lfs3_mtree_gc and makes things a bit more readable.

---

I also split lfs3_fs/handle_clobber into separate lfs3_fs/handle_clobber
and lfs3_fs/handle_mutate functions. This added a bit of code, but I
think is worth it for a simpler internal API. A confusing internal API
is no good.

In total these simplifications saved a bit of code:

                 code          stack          ctx
  before:       37208           2360          684
  after:        37176 (-0.1%)   2360 (+0.0%)  684 (+0.0%)

                 code          stack          ctx
  gbmap before: 40100           2432          848
  gbmap after:  40060 (-0.1%)   2432 (+0.0%)  848 (+0.0%)
2025-10-23 23:41:43 -05:00
Christopher Haster f5508a1b6c gbmap: Added LFS3_T_REBUILDGBMAP and friends
This adds LFS3_T_REBUILDGBMAP and friends, and enables incremental gbmap
rebuilds as a part of gc/traversal work:

  LFS3_M_REBUILDGBMAP   0x00000400  Rebuild the gbmap
  LFS3_GC_REBUILDGBMAP  0x00000400  Rebuild the gbmap
  LFS3_I_REBUILDGBMAP   0x00000400  The gbmap is not full
  LFS3_T_REBUILDGBMAP   0x00000400  Rebuild the gbmap

On paper, this is more or less identical to repopulating the lookahead
buffer -- traverse the filesystem, mark blocks as in-use, adopt the new
gbmap/lookahead buffer on success -- but a couple nuances make
rebuilding the gbmap a bit trickier:

- Unlike the lookahead buffer, which eagerly zeros in allocation, we
  need an explicit zeroing pass before we start marking blocks as
  in-use. This means multiple traversals can potentially conflict with
  each other, risking the adoption of a clobbered gbmap.

- The gbmap, which stores information on disk, relies on block
  allocation and the temporary "in-flight window" defined by allocator
  ckpoints to avoid circular block states during gbmap rebuilds. This
  makes gbmap rebuilds sensitive to allocator ckpoints, which we
  consider more-or-less a noop in other parts of the system.

  Though now that I'm writing this, it might have been possible to
  instead include gbmap rebuild snapshots in fs traversals... but that
  would probably have been much more complicated.

- Rebuilding the gbmap requires writing to disk and is generally much
  more expensive/destructive. We want to avoid trying to rebuild the
  gbmap when it's not possible to actually make progress.

On top of this, the current trv-clobber system is a delicate,
error-prone mess.

---

To simplify everything related to gbmap rebuilds, I added a new
internal traversal flag: LFS3_t_CKPOINTED:

  LFS3_t_CKPOINTED  0x04000000  Filesystem ckpointed during traversal

LFS3_t_CKPOINTED is set, unconditionally, on all open traversals in
lfs3_alloc_ckpoint, and provides a simple, robust mechanism for checking
if _any_ allocator checkpoints have occured since a traversal was
started. Since lfs3_alloc_ckpoint is required before any block
allocation, this provides a strong guarantee that nothing funny happened
to any allocator state during a traversal.

This makes lfs3_alloc_ckpoint a bit less cheap, but the strong
guarantees that allocator state is unmodified during traversal are well
worth it.

This makes both lookahead and gbmap passes simpler, safer, and easier to
reason about.

I'd like to adopt something similar+stronger for LFs3_t_MUTATED, and
reduce this back to two flags, but that can be a future commit.

---

Unfortunately due to the potential for recursion, this ended up reusing
less logic between lfs3_alloc_rebuildgbmap and lfs3_mtree_gc than I had
hoped, but at like the main chunks (lfs3_alloc_remap,
lfs3_gbmap_setbptr, lfs3_alloc_adoptgbmap) could be split out into
common functions.

The result is a decent chunk of code and stack, but the value is high as
incremental gbmap rebuilds are the only option to reduce the latency
spikes introduced by the gbmap allocator (it's not significantly worse
than the lookahead buffer, but both do require traversing the entire
filesystem):

                 code          stack          ctx
  before:       37164           2352          684
  after:        37208 (+0.1%)   2360 (+0.3%)  684 (+0.0%)

                 code          stack          ctx
  gbmap before: 39708           2376          848
  gbmap after:  40100 (+1.0%)   2432 (+2.4%)  848 (+0.0%)

Note the gbmap build is now measured with LFS3_GBMAP=1, instead of
LFS3_YES_GBMAP=1 (maybe-gbmap) as before. This includes the cost of
mkgbmap, lfs3_f_isgbmap, etc.
2025-10-23 23:39:55 -05:00