Commit Graph

26 Commits

Author SHA1 Message Date
Christopher Haster 6c38677661 trv: Reverted dropped fixgrm call
See previous commit for motivation. This was an attempt to simplify
lfs3_trv_read that wasn't worth it.

Code changes:

                 code          stack          ctx
  before:       35112           2136          660
  after:        35164 (+0.1%)   2136 (+0.0%)  660 (+0.0%)

                 code          stack          ctx
  gbmap before: 38040           2136          776
  gbmap after:  38088 (+0.1%)   2136 (+0.0%)  776 (+0.0%)
2026-01-09 00:01:20 -06:00
Christopher Haster d56cd2c140 trv: Attempted to drop fixgrm calls from lfs3_trv_read
Will revert.

The idea here is that fixgrm isn't really a traversal operation. It's
convenient, but in an effort to simplify things, dropping fixgrm from
lfs3_trv_read makes sense.

But dropping fixgrm seems to cause more problems than it's worth.

---

Note test_trvs is currently failing because attempting to remove an
orphaned stickynote in the grm queue without calling fixgrm breaks
things.

It's probably fixable, but why? If we keep the implied fixgrm it's not
possible to trigger a remove without a clean grm queue. And we want to
keep our grm queue clean anyways to prevent a full fixorphan scan.

Code changes:

                 code          stack          ctx
  before:       35164           2136          660
  after:        35112 (-0.1%)   2136 (+0.0%)  660 (+0.0%)

                 code          stack          ctx
  gbmap before: 38088           2136          776
  gbmap after:  38040 (-0.1%)   2136 (+0.0%)  776 (+0.0%)
2026-01-09 00:01:18 -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 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 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 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 9e006fd7dc trv: Reordered gbmap traversal before mdir iteration
This is in preparation for some traversal simplification ideas, which
rely on all auxiliary/non-file btrees being visitable before file
btrees.

In theory the order of file vs auxiliary btrees doesn't really matter,
other than the number of different routes from mtree/mroot -> gbmap/file
btrees being a bit of a pain.

Note this is not true for the mtree, which must come first for
lfs3_mount to work.

---

Adds a bit of code when building with the gbmap:

                 code          stack          ctx
  before:       36480           2304          660
  after:        36476 (-0.0%)   2304 (+0.0%)  660 (+0.0%)

                 code          stack          ctx
  gbmap before: 39464           2320          772
  gbmap after:  39524 (+0.2%)   2320 (+0.0%)  772 (+0.0%)

                 code          stack          ctx
  gc before:    36552           2304          804
  gc after:     36548 (-0.0%)   2304 (+0.0%)  804 (+0.0%)
2025-11-08 19:46:20 -06: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 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 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
Christopher Haster 9b4ee982bc gbmap: Tried to adopt the gbmap name more consistently
Having gbmap/bmap used in different places for the same thing was
confusing. Preferring gbmap as it is consistent with other gstate (grm
queue, gcksums), even if it is a bit noisy.

It's interesting to note what didn't change:

- The BM* range tags: LFS3_TAG_BMFREE, etc. These already differs from
  the GBMAP* prefix enough, and adopting GBM* would risk confusion for
  actual gstate.

- The gbmap revdbg string: "bb~r". We don't have enough characters for
  anything else!

- dbgbmap.py/dbgbmapsvg.py. These aren't actually related to the gbmap,
  so the name difference is a good thing.
2025-10-09 14:33:27 -05:00
Christopher Haster 9d322741ca bmap: Simplified bmap configs, reduced to one LFS3_F_GBMAP flag
TLDR: This drops the idea of different bmap strategies/modes, and sorts
out most of the compile-time/runtime conditional bmap interactions.

---

Motivation: Benchmarking (at least up to the 32-bit word limit) has
shown the bmap will unlikely be a significant bottleneck, even on large
disks. The largest disks tend to be NAND, and NAND's ridiculous block
size limits pressure on block allocation.

There are still concerns for areas I haven't measured yet:

- SD/eMMC/FTL - Small blocks, so more pressure on block allocation. In
  theory the logical block size can be artificially increased, but this
  comes with a granularity tradeoff.

- I've only measured throughput, latency is a whole other story.

  However, users have reported lfs3_fs_gc is useful for mitigating this,
  so maybe latency is less of a concern now?

But while there may still be room for improvement via alternative bmap
strategies, the risk a concerning amount of complexity. Yes,
configuration gets more complicated, but the real issue is any bmap
strategies that try to track _deallocations_ (the original idea being
treediffing) risk falling leaking blocks if all cases aren't covered.

The current "bmap cache" strategy strikes a really nice balance where it
reduces _amortized_ block allocation -> ~O(log n) without RAM, while
retaining the safe, bug-resistant, single-source-of-truth properties
that come with lookahead-based allocation.

---

So, long story short, dropping other strategies, and now the presence of
the bmap is a boolean flag.

This is also the first format-specific flag:

- Define LFS3_BMAP to enable the bmap logic, but note by default the
  bmap will still not be used.

- Define LFS3_YES_BMAP to force the bmap to be used.

- With LFS3_BMAP, passing LFS3_F_GBMAP to lfs3_format will include the
  on-disk block-map.

- No flag is needed during mount, the presence of the bmap is determined
  by the on-disk wcompat flags (LFS3_WCOMPAT_GBMAP). This also prevents
  rw mounting if the bmap is not supported, but rdonly mounting is
  allowed.

- Users can check if the bmap is in use via lfs3_fs_stat, which reports
  LFS3_I_GBMAP in the flags field.

There's still some missing pieces, but these will be a bit more
involved:

- lfs3_fs_grow needs to be made bmap aware!

- We probably want something like lfs3_fs_mkgbmap and lfs3_fs_rmgbmap to
  allow converting between bmap backed/not-backed filesystem images.

Code changes minimal:

                code          stack          ctx
  before:      37172           2352          684
  after:       37172 (+0.0%)   2352 (+0.0%)  684 (+0.0%)

                code          stack          ctx
  bmap before: 38844           2456          800
  bmap after:  38852 (+0.0%)   2456 (+0.0%)  800 (+0.0%)
2025-10-09 14:33:27 -05:00
Christopher Haster 7289619859 Tweaked lfs3_mdir_commit to imply lfs3_alloc_ckpoint
Now that lfs3_alloc_ckpoint is more complicated, and can error, it makes
sense for lfs3_alloc_ckpoint to be implied by lfs3_mdir_commit.

Most lfs3_mdir_commit calls represent an atomic transaction from one
state -> another, so this saves a bit of code:

                code          stack          ctx
  before:      36912           2368          684
  after:       36836 (-0.2%)   2368 (+0.0%)  684 (+0.0%)

                code          stack          ctx
  bmap before: 38512           2400          812
  bmap after:  38436 (-0.2%)   2400 (+0.0%)  812 (+0.0%)

The notable exception being bshrub-related commits in
lfs3_bshrub_commitroot_. Bshrub commits are trying to resolve an
in-flight btree, so the relevant blocks are very much _not_ at rest.

---

I've been hesitant to adopt this mostly just because it makes the
lfs3_mdir_commit* names even more of a mess:

- lfs3_mdir_commit__   -> lfs3_mdir_commit___
- lfs3_mdir_commit_    -> lfs3_mdir_commit__
- lfs3_mdir_commit     -> lfs3_mdir_commit_
- added lfs3_mdir_commit
- lfs3_mdir_compact    -> lfs3_mdir_compact_
- add lfs3_mdir_compact
- lfs3_mdir_alloc__    -> lfs3_mdir_alloc___
- lfs3_mdir_estimate__ -> lfs3_mdir_estimate___
- lfs3_mdir_swap__     -> lfs3_mdir_swap___
2025-10-01 17:56:24 -05:00
Christopher Haster 27e3e10634 bmap: Added error propagation to ckpoints and cleaned up test TODOs
The main change is error propagation in lfs3_alloc_ckpoint. Since
lfs3_alloc_ckpoint writes to disk during bmap rebuilds, it can now fail
in all sorts of ways. Fortunately lfs3_alloc_ckpoint should only ever be
called by write operations, where these errors are be expected.

With bmap rebuild errors now reported correctly, this unblocks most of
the remaining test TODOs:

- Passing test_badblocks
- Passing test_ck
- Passing test_trvs

With this, LFS3_YES_BMAP is now passing all but two tests, which are
still ifndef-disabled as a temporary measure:

- test_btree - We make some low-level assumptions about the lookahead
  allocator when testing btrees. It's probably not worth trying to get
  this passing with the bmap allocator.

- test_grow - This one does need fixing! We currently don't update
  on-disk bmaps correctly when growing the filesystem.

Code changes minimal:

                code          stack          ctx
  before:      36912           2368          684
  after:       36912 (+0.0%)   2368 (+0.0%)  684 (+0.0%)

                code          stack          ctx
  bmap before: 38456           2400          812
  bmap after:  38512 (+0.1%)   2400 (+0.0%)  812 (+0.0%)
2025-10-01 17:56:22 -05:00
Christopher Haster 316ca1cc05 bmap: The initial bmapcache algorithm seems to be working
At least at a proof-of-concept level, there's still a lot of cleanup
needed.

To make things work, lfs3_alloc_ckpoint now takes an mdir, which
provides the target for gbmap gstate updates.

When the bmap is close to empty (configurable via bmap_scan_thresh), we
opportunistically rebuild it during lfs3_alloc_ckpoints. The nice thing
about lfs3_alloc_ckpoint is we know the state of all in-flight blocks,
so rebuilding the bmap just requires traversing the filesystem + in-RAM
state.

We might still fall back to the lookahead buffer, but in theory a well
tuned bmap_scan_thresh can prevent this from becoming a bottleneck (at
the cost of more frequent bmap rebuilds).

---

This is also probably a good time to resume measuring code/ram costs,
though it's worth repeating the above note about the bmap work still
needing cleanup:

             code          stack          ctx
  before:   36840           2368          684
  after:    36920 (+0.2%)   2368 (+0.0%)  684 (+0.0%)

Haha, no, the bmap isn't basically free, it's just an opt-in features.
With -DLFS3_YES_BMAP=1:

             code          stack          ctx
  no bmap:  36920           2368          684
  yes bmap: 38552 (+4.4%)   2472 (+4.4%)  812 (+18.7%)
2025-10-01 17:56:14 -05:00
Christopher Haster cd9f93d859 btree: Resurrected btree leaf caching
This is an indulgence to simplify the upcoming auxiliary btree work.

Brings back the previously-reverted per-btree leaf caches, where each
lfs3_btree_t keeps track of two rbyds: The root and the most recently
accessed leaf.

At the surface level, this optimizes repeated access to the same btree
leaf. A common pattern for a number of littlefs's operations that has
proven tricky to manually optimize:

- Btree iteration
- Pokes for our crystalization heuristic
- Checksum collision resolution for dids and (FUTURE) ddkeys
- Related rattrs attached to a single bid

But the real motivation is to drop lfs3_btree_*lookupleaf and simplify
the internal APIs. If repeated lfs3_btree_lookup*s are already
efficient, there's no reason for extra leaf-level APIs, and in theory
any logic that interacts with btrees will be simpler.

---

This comes at a cost (humorously about the same amount as the
tag-returning refactor, if you ignore the extra 28 bytes of ctx).
Unsurprisingly, increasing the size of lfs3_btree_t has the biggest
impact on stack and ctx:

           code          stack          ctx
  before: 36084           2336          656
  after:  36784 (+1.9%)   2400 (+2.7%)  684 (+4.3%)

Also note from the previous commit messages: Btree leaf caching has
resulted in surprisingly little performance improvement for our current
benchmarks + implementation. It turns out if you're dominated by write
cost, optimizing btree lookups -- which already skip rbyd fetches, has
barely noticeable impact.

---

A note on reverting!

Eventually (after the auxiliary btree work) it will probably make sense
to revert this -- or at least provide a non-leaf-caching build for
code/RAM sensitive users.

I don't think this should be reverted as-is. Instead, I think we should
allow the option to just disable the leaf cache, while keeping the
simpler internal API. This would give us the best of all three worlds:

- A small code/RAM option
- Optimal btree iteration/nearby-lookup performance
- Simpler internal APIs

The only reason this isn't already implemented is because I want to
avoid fragmenting the codebase further while we're still in development
mode.
2025-07-20 13:57:50 -05:00
Christopher Haster 7b330d67eb Renamed config -> cfg
Note this includes both the lfs3_config -> lfs3_cfg structs as well as
the LFS3_CONFIG -> LFS3_CFG include define:

- LFS3_CONFIG -> LFS3_CFG
- struct lfs3_config -> struct lfs3_cfg
- struct lfs3_file_config -> struct lfs3_file_cfg
- struct lfs3_*bd_config -> struct lfs3_*bd_cfg
- cfg -> cfg

We were already using cfg as the variable name everywhere. The fact that
these names were different was an inconsistency that should be fixed
since we're committing to an API break.

LFS3_CFG is already out-of-date from upstream, and there's plans for a
config rework, but I figured I'd go ahead and change it as well to lower
the chances it gets overlooked.

---

Note this does _not_ affect LFS3_TAG_CONFIG. Having the on-disk vs
driver-level config take slightly different names is not a bad thing.
2025-07-18 18:29:41 -05:00
Christopher Haster 2586fe68a2 Renamed traversal -> trv
- test_traversal -> test_trvs
- lfs3_traversal_t -> lfs3_trv_t
- lfs3_btraversal_t -> lfs3_btrv_t
- t -> trv
- bt -> btrv
- lfs3_traversal_* -> lfs3_trv_*
- lfs3_btraversal_* -> lfs3_btrv_*

The traversal type is becoming one of the more fundamental types in
littlefs, and if DIR and REG both get shortened names, it makes sense
for TRV to have one as well.

This also removes the temptation to use t for traversals, which is
probably an even worse name.

---

Note that lfs3_btree_traverse, lfs3_mtree_traverse, etc, remain
unaffected. This may change in the future, but it's interesting to note
that verbs seem to need much less typing than nouns.
2025-07-18 18:28:57 -05:00