475 Commits

Author SHA1 Message Date
Christopher Haster 3413f125d8 Set LFS_VERSION to v0.0
Like the LFS_DISK_VERSION, the intention is to mark the current driver
as experimental.

Just in case...
2025-05-27 21:39:55 -05:00
Christopher Haster 31aafe0f99 Big cleanup!
Removing the final vestiges of v2.
2025-05-27 21:05:59 -05:00
Christopher Haster f7e17c8aad Added LFS_T_RDONLY, LFS_T_RDWR, etc
These mimic the relevant LFS_O_* flags, and allow users to assert
whether or not a traversal will mutate the filesystem:

  LFS_T_MODE          0x00000001  The traversal's access mode
  LFS_T_RDWR          0x00000000  Open traversal as read and write
  LFS_T_RDONLY        0x00000001  Open traversal as read only

In theory, these could also change internal allocations, but littlefs
doesn't really work that way.

Note we _don't_ add related LFS_GC_RDONLY, LFS_GC_RDWR, etc flags. These
are sort of implied by the relevant LFS_M_* flags.

Adds a bit more code, probably because of the slightly more complicated
internal constants for the internal traversals. But I think the
self-documentingness is worth it:

           code          stack          ctx
  before: 37200           2288          636
  after:  37220 (+0.1%)   2288 (+0.0%)  636 (+0.0%)
2025-05-24 23:27:10 -05:00
Christopher Haster 5b74aafa17 Reworked the flag encoding again
This time to account for the new LFS_o_UNCRYST and LFS_o_UNGRAFT flags.

This required moving the T flags out of the way, which of course
conflicted with TSTATE, so that had to move...

One thing that helped was shoving LFS_O_DESYNC up with the internal
state flags. It's definitely more a state flag than the other public
flags, it just also happens to be user toggleable.

Here's the new jenga:

              8     8     8     8
            .----++----++----++----.
            .-..----..-..-..-------.
  o_flags:  |t|| f  ||o||t||   o   |
            |-||-.--':-:|-|'--.-.--'
            |-||-|.----.|-'--------.
  t_flags:  |t||f||tstt||    t     |
            '-''-''----'|----.-----'
            .----..-.:-:|----|:-:.-.
  m_flags:  | m  ||c||o|| t  ||o||m|
            |----||-|'-'|-.--''-''-'
            |----||-|---|-|.-------.
  f_flags:  | m  ||c|   |t||   f   |
            '----''-'---'-''-------'

This adds a bit of code, but that's not the end of the world:

           code          stack          ctx
  before: 37172           2288          636
  after:  37200 (+0.1%)   2288 (+0.0%)  636 (+0.0%)
2025-05-24 22:21:39 -05:00
Christopher Haster f5dd6f69e8 Renamed LFS_CKMETAPARITY and LFS_CKDATACKSUMREADS
- LFS_CKPARITY -> LFS_CKMETAPARITY
- LFS_CKDATACKSUMS -> LFS_CKDATACKSUMREADS

The goal here is to provide hints for 1. what is being checked (META,
DATA, etc), and 2. on what operation (FETCHES, PROGS, READS, etc).

Note that LFS_CKDATACKSUMREADS is intended to eventually be a part of a
set of flags that can pull off closed fully-checked reads:

- LFS_CKMETAREDUNDREADS - Check data checksums on reads
- LFS_CKDATACKSUMREADS - Check metadata redund blocks on reads
- LFS_CKREADS - LFS_CKMETAREDUNDREADS + LFS_CKDATACKSUMREADS

Also it's probably not a bad idea for LFS_CKMETAPARITY to be harder to
use. It's really not worth enabling unless you understand its
limitations (<1 bit of error detection, yay).

No code changes.
2025-05-24 21:55:45 -05:00
Christopher Haster a1c90d2624 Reverted attempted per-btree leaf caches
See the relevant commit for why. These just added surprisingly little
performance benefit for the code/stack cost.

Maybe in a future performance-preferring littlefs driver.
2025-05-24 18:49:38 -05:00
Christopher Haster a49e13b992 Attempted to implement per-btree leaf caches
The idea here, is we give each lfsr_btree_t an optional leaf rbyd, in
addition to the root rbyd. This leaf rbyd acts as a cache for the most
recent leaf, allowing nearby btree lookups to skip the full btree walk.

Unfortunately, this failed on pretty much every measurable metric...

---

The motivation for this is that we often do a bunch of nearby btree
lookups:

- Btree iteration via lfsr_btree_lookupnext is a bit naive, walking from
  the root every step.

- Our crystallization algorithm requires a bunch of nearby lookups to
  figure out our crystallization heuristic. Currently at most 4, when
  you need to lookup both crystal neighbors and then _also_ both
  fragment neighbors for coalescing.

- Checksum collision resolution for dids and (FUTURE) ddkeys can require
  an unbounded number of sequential lookups.

  Though to be fair, this is an exceptional case if our checksum is any
  good.

- Bids with multiple rattrs require nearby lookups to resolve.

  Though currently this can be explicitly avoided via
  lfsr_btree_lookupleaf + lfsr_rbyd_lookup.

The theory was that cases like these could explicitly keep track of the
leaf rbyd to avoid full btree walks, but in practice this never really
worked out. Tracking if we're still in the relevant leaf rbyd just adds
too much logic/code cost.

But if this leaf tracking logic was implemented once in the btree
layer...

The other theoretical benefit was being able to move more rbyds off the
stack. Sure our btrees take up more RAM, but if that results in stack
savings, that may be a win.

Oh, and this would let our btree API and rbyd API converge without
performance concerns. Internal users could in theory call
lfsr_btree_lookupnext + lfsr_btree_lookup with the same performance as
explicitly tracking the rbyd.

---

But this was a complete failure!

First the good news: There was a modest speedup of around ~2x to linear
reads.

And that's the good news.

Now the bad news:

1. There was no noticeable performance gain in any other benchmarks.

   To be fair, we're at the early stages of benchmarking, so the
   benchmarks may not be the most thorough, but thinking about it, there
   are some explanations:

   - In any benchmark that writes, fetch + erase + prog dominates. Being
     able to skip fetches during lookups makes our btree lookups
     surprisingly cheap!

   - Any random read heavy benchmark is likely thrashing this cache,
     which is to be expected.

   - For small 1-block btrees, the leaf cache is useless because the
     entire btree is cache in the root rbyd.

     And keep in mind, our blocks are BIG. "Small" here could be on
     the order of ~128KiB-1MiB for NAND flash.

   - For the mtree, fetched mdirs actually already act as a sort of leaf
     cache.

     The extra btree leaf cache isn't doing _nothing_, but each layer of
     the mtree has diminishing returns due to btree's ridiculous
     branching factor.

   - For file btrees, we're explicitly caching the leaf fragments/
     blocks, so the extra btree leaf cache has diminishing returns for
     the same reason.

2. Code cost was bad, stack cost was worse:

              code          stack          ctx
     before: 37172           2288          636
     after:  38068 (+2.4%)   2416 (+5.6%)  664 (+4.4%)

   Tracking the leaf required more code, that's expected. And, to be
   fair, the current code has had a lot more time to congeal.

   What wasn't expected was the stack cost.

   Unfortunately these caches didn't really take any rbyds off the stack
   hot-path:

   - We _can_ get rid of the rbyd in lfsr_btree_lookup/namelookup, but
     we were already hacking our way around the critical one in
     lfsr_mtree_lookup/namelookup by reusing the mdir's rbyd!

   - We can't even abuse the leaf rbyd in the commit logic, since the
     target btree can end up iterated/traversed by lfs_alloc.

     That was a fun bug.

   And the addition of a second rbyd to lfsr_btree_t increases both ctx
   and stack anywhere btrees are allocated.

Maybe this will make more sense when we add the auxiliary btrees, or
after more benchmarking, but for now the theoretical performance
improvements just aren't worth it.

Will probably revert this, but I wanted to commit it in case the idea is
worth resurrecting in the future, if in the future nearby btree lookups
are a bigger penalty than they are now.
2025-05-24 18:37:37 -05:00
Christopher Haster aaefad11e3 Fixed outdated LFS_type_* values
This should match the internal LFSR_TAG_* values, but was probably
missed during a refactor.
2025-05-24 09:58:52 -05:00
Christopher Haster 1cce0dab5c Reverted limiting file->leaf to reads + erased-state caching
Still on the fence about this, but in hindsight the code/stack
difference is not _that_ much:

           code          stack          ctx
  before: 36460           2280          636
  after:  37092 (+1.7%)   2304 (+1.1%)  636 (+0.0%)

Especially with the potential to significantly speed up linear file
writes/rewrites, which are usually the most common file operation. You
ever just, you know, write a whole file at once?

Note we can still add the previous behavior as an opt-in write strategy
to save code/stack when preferred over linear write/rewrite speed.

This is actually the main reason I think we should prefer
lazy-crystallization by default. Of the theoretical/future write
strategies, lazy-crystallization was the only one trading performance
for code/stack and not vice versa (global-alignment, linear-only,
fully-fragmented, etc).

If we default to a small, but less performant filesystem, it risks users
thinking littlefs is slow when they just haven't turned on the right
flags.

That being said there's a balance here. Users will probably judge
littlefs based on its default code size for the same reason.

---

Note this includes the generalized lfsr_file_crystallize_ API, which
adds a bit of code:

                     code          stack          ctx
  before gen-cryst: 37084           2304          636
  after gen-cryst:  37092 (+0.0%)   2304 (+0.0%)  636 (+0.0%)
2025-05-23 19:48:56 -05:00
Christopher Haster 22c43124de Limited file->leaf to reads + erased-state caching
This reverts most of the lazy-grafting/crystallization logic, but keeps
the general crystallization algorithm rewrite and file->leaf for caching
read operations and erased-state.

Unfortunately lazy-grafting/crystallization is both a code and stack
heavy feature for a relatively specific write pattern. It doesn't even
help if we're forced to write fragments due to prog alignment.

Dropping lazy-grafting/crystallization trades off linear write/rewrite
performance for code and stack savings:

                           code          stack          ctx
  before:                 37084           2304          636
  after:                  36428 (-1.8%)   2248 (-2.4%)  636 (+0.0%)

But with file->leaf we still keep the improvements to linear read
performance!

Compared to pre-file->leaf:

                           code          stack          ctx
  before file->leaf:      36016           2296          636
  after lazy file->leaf:  37084 (+3.0%)   2304 (+0.3%)  636 (+0.0%)
  after eager file->leaf: 36428 (+1.1%)   2248 (-2.1%)  636 (+0.0%)

I'm still on the fence about this, but lazy-grafting/crystallization is
just a lot of code... And the first 6 letters of littlefs don't spell
"speedy" last time I checked...

At the very least we can always add lazy-grafting/crystallization as an
opt-in write strategy later.
2025-05-23 15:22:33 -05:00
Christopher Haster 9c3a866508 Reworked crystallization to better use erased-state on rewrites
This adopts lazy crystallization in _addition_ to lazy grafting, managed
by separate LFS_o_UNCRYST and LFS_o_UNGRAFT flags:

  LFS_o_UNCRYST  0x00400000  File's leaf not fully crystallized
  LFS_o_UNGRAFT  0x00800000  File's leaf does not match bshrub/btree

This lets us graft not-fully-crystallized blocks into the tree without
needing to fully crystallize, avoiding repeated recrystallizations when
linearly rewriting a file.

Long story short, this gives file rewrites roughly the same performance
as linear file writes.

---

In theory you could also have fully crystallized but ungrafted blocks
(UNGRAFT + ~UNCRYST), but this doesn't happen with the current logic.
lfsr_file_crystallize eagerly grafts blocks once they're crystallized.

Internally, lfsr_file_crystallize replaces lfsr_file_graft for the
"don't care, gimme file->leaf" operation. This is analogous to
lfsr_file_flush for file->cache.

Note we do _not_ use LFS_o_UNCRYST to track erased-state! If we did,
erased-state wouldn't survive lfsr_file_flush!

---

Of course, this adds even more code. Fortunately not _that_ much
considering how many lines of code changed:

           code          stack          ctx
  before: 37012           2304          636
  after   37084 (+0.2%)   2304 (+0.0%)  636 (+0.0%)

There is another downside however, and that's that our benchmarked disk
usage is slightly worse during random writes.

I haven't fully investigated this, but I think it's due to more
temporary fragments/blocks in the B-tree before flushing. This can cause
B-tree inner nodes to split earlier than when eagerly recrystallizing.

This also leads to higher disk usage pre-flush since we keep both the
old and new blocks around while uncrystallized, but since most rewrites
are probably going to be CoW on top of committed files, I don't think
this will be a big deal.

Note the disk usage ends up the same after lfsr_file_flush.
2025-05-23 15:13:56 -05:00
Christopher Haster 9ed326f3d3 Adopted file->leaf, reworked how we track crystallization
TLDR: Added file->leaf, which can track file fragments (read only) and
blocks independently from file->b.shrub. This speeds up linear
read/write performance at a heavy code/stack cost.

The jury is still out on if this ends up reverted.

---

This is another change motivated by benchmarking, specifically the
significant regression in linear reads.

The problem is that CTZ skip-lists are actually _really_ good at
appending blocks! (but only appending blocks) The entire state of the
file is contained in the last block, so file writes can resume without
any reads. With B-trees, we need at least 1 B-tree lookup to resume
appending, and this really adds up when writing extremely blocks.

To try to mitigate this, I added file->leaf, a single in-RAM bptr for
tracking the most recent leaf we've operated on. This avoids B-tree
lookups during linear reads, and allowing the leaf to fall out-of-sync
with the B-tree avoids both B-tree lookups and commits during writes.

Unfortunately this isn't a complete win for writes. If we write
fragments, i.e. cache_size < prog_size, we still need to incrementally
commit to the B-tree. Fragments are a bit annoying for caching as any
B-tree commit can discard the block they reside on.

For reading, however, this brings read performance back to roughly the
same as CTZ skip-lists.

---

This also turned into more-or-less a full rewrite of the lfsr_file_flush
-> lfsr_file_crystallize code path, which is probably a good thing. This
code needed some TLC.

file->leaf also replaces the previous eblock/eoff mechanism for
erased-state tracking via the new LFSR_BPTR_ISERASED flag. This should
be useful when exploring more erased-state tracking mechanisms (ddtree).

Unfortunately, all of this additional in-RAM state is very costly. I
think there's some cleanup that can be done (the current impl is a bit
of a mess/proof-of-concept), but this does add a significant chunk of
both code and stack:

           code          stack          ctx
  before: 36016           2296          636
  after:  37228 (+3.4%)   2328 (+1.4%)  636 (+0.0%)

file->leaf also increases the size of lfsr_file_t, but this doesn't show
up in ctx because struct lfs_info dominates:

  lfsr_file_t before: 116
  lfsr_file_t after:  136 (+17.2%)

Hm... Maybe ctx measurements should use a lower LFS_NAME_MAX?
2025-05-23 12:15:13 -05:00
Christopher Haster 7c8c7e662a Tightened mdir compaction estimate, added mattr_estimate
This adds mattr_estimate, which is basically the same as rattr_estimate,
but assumes weight <= 1:

  rattr tag:
  .---+---+---+- -+- -+- -+- -+---+- -+- -+- -.  worst case: <=11 bytes
  |  tag  | weight            | size          |  rattr est:  <=3t + 4
  '---+---+---+- -+- -+- -+- -+---+- -+- -+- -'              <=37 bytes

  mattr tag:
  .---+---+---+---+- -+- -+- -.                  worst case: <=7 bytes
  |  tag  | w | size          |                  mattr est:  <=3t + 4
  '---+---+---+---+- -+- -+- -'                              <=25 bytes

This may seem like only a minor improvement, but with 3 tags for every
attr, this really adds up. And with our compaction estimate overheads we
need every byte of shaving we can get.

---

This ended up necessary to get littlefs running with 512 byte blocks
again. Now that our compaction overheads are so high, littlefs is having
a hard time fitting even just the filesystem config in a single block:

  mroot estimate 512B before: 246/256
  mroot estimate 512B after:  162/256 (-34.1%)

Whether or not it makes sense to run littlefs with 512 byte blocks is
still an open question, even after this tweak.

Note that even if 512 byte blocks ends up intractable, this doesn't mean
littlefs won't be able to run on SD/eMMC! The configured block_size can
always be a multiple, >=, of the physical block_size, and choosing a
larger block_size completely side-steps this problem.

The new design of littlefs is primarily focused on devices with very
large block sizes, so you may want to use larger block sizes on SD/eMMC
for performance reasons anyways.

---

Code changes were pretty minimal. This does add an additional field to
lfs_t, but it's just a byte and fits into padding with the other small
precomputed constants:

           code          stack          ctx
  before: 35824           2368          636
  after:  35836 (+0.0%)   2368 (+0.0%)  636 (+0.0%)
2025-05-15 18:16:35 -05:00
Christopher Haster 9f2f0b92e9 Renamed lfsr_fs_size -> lfsr_fs_usage
This better matches how other filesystems refer to the number of in-use
blocks.

Which makes sense when you consider that "size" could also refer to the
configured block_count. The term "usage" avoids this ambiguity.
2025-05-01 00:37:07 -05:00
Christopher Haster 97f8eeb9e9 Tweaked more functions to operate on lfs_t directly
Mainly the grm and ptail subsystems. This matches the internal mtree
API.

Unfortunately this _did_ add a little bit of code, I guess due to the
larger struct offsets. But since this simplifies the internal API I'm
going to chalk it up to compiler noise:

           code          stack          ctx
  before: 35768           2368          636
  after:  35792 (+0.1%)   2368 (+0.0%)  636 (+0.0%)
2025-04-30 00:55:54 -05:00
Christopher Haster f2e6b60f36 Reworked grm encoding a bit
This drops the leading count/mode byte, and instead uses mid=0 to
terminate grms. This shaves off 1 bytes from grmdeltas.

Previously, we needed the count/mode byte for a couple reasons:

- We needed to know the number of grm entries somehow, and there wasn't
  always an obvious sentinel value. mid=-1, for example, is
  unrepresentable with our unsigned leb128 encoding.

  But now that development has settled, we can use mid=0.0 to figure out
  the end-of-queue. mid=0.0 should always map to the root bookmark,
  which doesn't make sense to delete, so it makes for a reasonable null
  terminator here.

- It provided a route for future grm extensions, which could use the >2
  count/mode encodings.

  But I think we can use additional grm tag encodings for this.

  There's only one gdelta tag so far, but the current plan for future
  gdelta tags is to carve out the bottom 2 bits for redund like we do
  with the struct tags:

    LFSR_TAG_GDELTA        0x01tt  v--- ---1 -ttt ttrr
    LFSR_TAG_GRMDELTA      0x0100  v--- ---1 ---- ----
    LFSR_TAG_GBMAPDELTA    0x0104  v--- ---1 ---- -1rr
    LFSR_TAG_GDDTREEDELTA  0x0108  v--- ---1 ---- 1-rr
    LFSR_TAG_GPTREEDELTA   0x010c  v--- ---1 ---- 11rr
    ...

  Decoding is a bit more complicated for gstate, since we will need to
  xor those bits if mutable, but this avoids needing a full byte just
  for redund in every auxiliary tree.

  Long story short, we can leverage the lower 2 bits of the grm tag for
  future extensions using the same mechanism.

This may seem like a lot of effort for only a handful of bytes, but keep
in mind each gdelta lives in more-or-less every mdir in the filesystem.

Also saves a bit of code/ctx:

           code          stack          ctx
  before: 35772           2368          640
  after:  35768 (-0.0%)   2368 (+0.0%)  636 (-0.6%)
2025-04-30 00:53:33 -05:00
Christopher Haster f3cd9802b8 Adopted LFSR_TAG_ORPHAN, simplified internal stickynote handling
This adds LFSR_TAG_ORPHAN, which simplifies quite a bit of the internal
stickynote handling.

Now that we don't have to worry about conflicts with future unknown
types, we can add whatever types we want internally. One useful one
is LFSR_TAG_ORPHAN, which lets us determine stickynote's orphan status
early (in lfsr_mdir_lookupnext and lfsr_mdir_namelookup):

- non-orphan stickynotes -> LFSR_TAG_STICKYNOTE
- orphan stickynotes     -> LFSR_TAG_ORPHAN

This simplifies all the places where we need to check if a stickynote
really exists, which is most of the high-level functions.

One downside is that this makes stickynote _manipulation_ a bit more
delicate. lfsr_mdir_lookup(LFSR_TAG_ORPHAN) no longer works as expected,
for example.

Fortunately we can sidestep this issue by dropping down to
lfsr_rbyd_lookup when we need to interact with stickynotes directly,
skipping the is-orphan checks.

---

Saves a nice bit of code:

           code          stack          ctx
  before: 35984           2440          640
  after:  35832 (-0.4%)   2440 (+0.0%)  640 (+0.0%)

It got a little muddy since this now include the unknown-type changes,
but here's the code diff from before we exposed LFSR_TYPE_STICKYNOTE to
users:

           code          stack          ctx
  before: 35740           2440          640
  after:  35832 (+0.3%)   2440 (+0.0%)  640 (+0.0%)
2025-04-24 16:33:24 -05:00
Christopher Haster 09c3749d7a Reworked how unknown file types are handled
This changes how we approach unknown file types.

Before:

> Unknown file types are allowed and may leak resources if modified,
> so attempted modification (rename/remove) will error with
> LFS_ERR_NOTSUP.

Now:

> Unknown file types are only allowed in RDONLY mode. This avoids the
> whole leaking resources headache.

Additionally, unknown types are now mapped to LFS_TYPE_UNKNOWN, instead
of just being forwarded to the user. This allows us to add internal
types/tags to the LFSR_TAG_NAME type space without worrying about
conflicts with future types:

- reg             -> LFS_TYPE_REG
- dir             -> LFS_TYPE_DIR
- stickynote      -> LFS_TYPE_STICKYNOTE
- everything else -> LFS_TYPE_UNKNOWN

Thinking about potential future types, it seems most (symlinks,
compressed files, etc) can be better implemented via custom attributes.
Using custom attributes doesn't mean the filesystem _can't_ inject
special behavior, and custom attributes allow for perfect backwards
compatibility.

So with future types less likely, forwarding type info to users is less
important (and potentially error prone). Instead, allowing on-disk +
internal types to be represented densely is much more useful.

And it avoids setting an upper bound on future types prematurely.

---

This also includes a minor rcompat/wcompat rework. Since we're probably
going to end up with 32-bit rcompat flags anyways, might as well make
them more human-readable (nibble-aligned):

  LFS_RCOMPAT_NONSTANDARD  0x00000001  Non-standard filesystem format
  LFS_RCOMPAT_WRONLY       0x00000002  Reading is disallowed
  LFS_RCOMPAT_BMOSS        0x00000010  Files may use inlined data
  LFS_RCOMPAT_BSPROUT      0x00000020  Files may use block pointers
  LFS_RCOMPAT_BSHRUB       0x00000040  Files may use inlined btrees
  LFS_RCOMPAT_BTREE        0x00000080  Files may use btrees
  LFS_RCOMPAT_MMOSS        0x00000100  May use an inlined mdir
  LFS_RCOMPAT_MSPROUT      0x00000200  May use an mdir pointer
  LFS_RCOMPAT_MSHRUB       0x00000400  May use an inlined mtree
  LFS_RCOMPAT_MTREE        0x00000800  May use an mdir btree
  LFS_RCOMPAT_GRM          0x00001000  Global-remove in use

  LFS_WCOMPAT_NONSTANDARD  0x00000001  Non-standard filesystem format
  LFS_WCOMPAT_RDONLY       0x00000002  Writing is disallowed
  LFS_WCOMPAT_REG          0x00000010  Regular file types in use
  LFS_WCOMPAT_DIR          0x00000020  Directory file types in use
  LFS_WCOMPAT_STICKYNOTE   0x00000040  Stickynote file types in use
  LFS_WCOMPAT_GCKSUM       0x00001000  Global-checksum in use

---

Code changes:

           code          stack          ctx
  before: 35928           2440          640
  after:  35924 (-0.0%)   2440 (+0.0%)  640 (+0.0%)
2025-04-24 16:29:19 -05:00
Christopher Haster 7dd473df82 Tweaked LFSR_TAG_STICKYNOTE encoding 0x205 -> 0x203
Now that LFS_TYPE_STICKYNOTE is a real type users can interact with, it
makes sense to group it with REG/DIR. This also has the side-effect of
making these contiguous.

---

LFSR_TAG_BOOKMARKs, however, are still hidden from the user. This
unfortunately means there will be a bit of a jump if we ever add
LFS_TYPE_SYMLINK in the future, but I'm starting to wonder if that's the
best way to approach symlinks in littlefs...

If instead LFS_TYPE_SYMLINKS were implied via custom attribute, you
could avoid the headache that comes with adding a new tag encoding, and
allow perfect compatibility with non-symlink drivers. Win win.

This seems like a better approach for _all_ of the theoretical future
types (compressed files, device files, etc), and avoids the risk of
oversaturating the type space.

---

This had a surprising impact on code for just a minor encoding tweak. I
guess the contiguousness pushed the compiler to use tables/ranges for
more things? Or maybe 3 vs 5 is just an easier constant to encode?

           code          stack          ctx
  before: 35952           2440          640
  after:  35928 (-0.1%)   2440 (+0.0%)  640 (+0.0%)
2025-04-24 14:35:52 -05:00
Christopher Haster 76493142e7 Reworked stickynote API, exposed LFS_TYPE_STICKYNOTE to users
This adds the LFS_TYPE_STICKYNOTE type, allowing users to interact with
stickynotes as long as they aren't orphaned.

This hopefully solves the long-standing mess that was the LFS_O_EXCL
API.

---

As for what I mean by orphaned vs non-orphaned stickynotes:

Non-orphaned stickynotes represent files that have been "created" (via
LFS_O_CREAT), but not "committed" (via sync/close). You can still close
and convert the stickynote to a reg file, so these aren't orphans. These
are also called "uncreated" files in some parts of the codebase:

- open+O_CREAT -> non-orphaned stickynote (uncreated file)

Orphaned stickynotes are possible by either removing an open file, or
desyncing a file before sync/close. These are still invisible to the
user and will be eventually cleaned up after the last file handle is
closed:

- open+remove               -> orphaned stickynote (zombied file)
- open+O_CREAT+desync+close -> orphaned stickynote (orphaned file)

Desynced files are a bit special. Even though they technically aren't
orphaned, they also behave like orphaned file handles:

- open+O_CREAT+close -> orphaned stickynote (desynced file)

The idea is this mimics the state of files post-close, and allows for
some tricks like using a desync file as a temporary file with no
observable effects on the filesystem.

---

The motivation for this comes from staring at the LFS_O_EXCL API for too
long and realizing the problem is that littlefs's API contradicts itself
when it comes to whether or not uncreated files exist.

This solution is to consistently treat uncreated files as though they
exist (the alternative would make LFS_O_EXCL pretty much useless), but I
really didn't want to do this as having what appears to be normal files
disappear after powerloss risks confusion.

The compromise here is to give these files a special type, repurposing
the internal LFS_TAG_STICKYNOTE, which hopefully hints to the user these
won't behave like normal files.

If the user is more interested in POSIX compatibility, they can always
map these to either LFS_TYPE_REG or LFS_ERR_NOENT, whichever they think
is the least confusing.

As a quirk of littlefs's API, stickynotes should never actually contain
any data, and will always have size 0.

However they can have custom attributes assigned now (which is I guess
ok? also TODO should probably test this).

---

The implementation right now is a bit naive, I mostly just wanted to get
the tests working again in this new model. It may be possible to claw
back some of this code cost:

           code          stack          ctx
  before: 35740           2440          640
  after:  35952 (+0.6%)   2440 (+0.0%)  640 (+0.0%)
2025-04-23 23:22:09 -05:00
Christopher Haster b3669f02c2 Dropped __cplusplus guards
littlefs is not a C++ project, and it's important to make sure users are
aware of that in case the header file ever breaks C++ (C++ is _not_
compatible with C99).

So dropping these guards.

C++ users should wrap the relevant includes with extern "C":

  extern "C" {
  #include "lfs.h"
  }
2025-04-23 23:21:18 -05:00
Christopher Haster 96eb38c8c2 Added LFS_REVDBG, tweaked LFS_REVNOISE
This tweaks a number of extended revision count things:

- Added LFS_REVDBG, which adds debug info to revision counts.

  This initializes the bottom 12 bits of every revision count with a
  hint based on rbyd type, which may be useful when debugging:

  - 68 69 21 v0 (hi!.) => mroot anchor
  - 6d 72 7e v0 (mr~.) => mroot
  - 6d 64 7e v0 (md~.) => mdir
  - 62 74 7e v0 (bt~.) => file btree node
  - 62 6d 7e v0 (bm~.) => mtree node

  This may be overwritten by the recycle counter if it overlaps, worst
  case the recycle counter takes up the entire revision count, but these
  have been chosen to at least keep some info if partially overwritten.

  To make this work required the LFS_i_INMTREE hack (yay global state),
  but a hack for debug info isn't the end of the world.

  Note we don't have control over data blocks, so there's always a
  chance they end up containing what looks like one of the above
  revision counts.

- Renamed LFS_NOISY -> LFS_REVNOISE

- LFS_REVDBG and LFS_REVNOISE are incompatible, so using both asserts.

  This also frees up the theoretical 0x00000030 state for an additional
  rev mode in the future.

- Adopted LFS_REVNOISE (and LFS_REVDBG) in btree nodes as well.

  If you need rev noise, you probably want it in all rbyds/metadata
  blocks, not just mdirs.

---

This had no effect on the default code size, but did affect
LFS_REVNOISE:

                    code          stack          ctx
  before:          35688           2440          640
  after:           35688 (+0.0%)   2440 (+0.0%)  640 (+0.0%)

  revnoise before: 35744           2440          640
  revnoise after:  35880 (+0.4%)   2440 (+0.0%)  640 (+0.0%)

  default:         35688           2440          640
  revdbg:          35912 (+0.6%)   2448 (+0.3%)  640 (+0.0%)
  revnoise:        35880 (+0.5%)   2440 (+0.0%)  640 (+0.0%)
2025-04-23 23:20:49 -05:00
Christopher Haster 385199d4b5 Tweaked some crystallization comments
- Trying to prefer crystal over compact verbiage to try to avoid
  confusion with metadata/rbyd compaction

- crystal_thresh >= block_size implying a fully-fragmented file was a
  mistake, it should be crystal_thresh > block_size.

  crystal_thresh == block_size has the behavior of waiting until the
  last moment to crystallize a block, but this still breaks the
  fully-fragmented random-write guarantee.

  This changed during development, so the comment was probably just
  outdated.
2025-04-23 23:19:50 -05:00
Christopher Haster c5efe35ab2 Split crystal_thresh into crystal_thresh + fragment_thresh
So now crystal_thresh only controls when fragments are compacted into
blocks, while fragment_thresh controls when blocks are broken into
fragments. Setting fragment_thresh=-1 will follow crystal_thresh and
keeps the previous behavior.

These were already two separate pieces of logic, so it makes sense to
provide two separate knobs for tuning.

Setting fragment_thresh lower than crystal_thresh has some potential to
reduce hysteresis in cases where random writes push blocks close to
crystal_thresh. It will be interesting to explore this more when
benchmarking.

---

The additional config option adds a bit of code/ctx, but hopefully that
will go away in the future config rework:

           code          stack          ctx
  before: 35584           2480          636
  after:  35600 (+0.0%)   2480 (+0.0%)  640 (+0.6%)
2025-04-20 15:53:18 -05:00
Christopher Haster 89356fc697 Renamed a couple mbit related things
- mdir_bits -> mbits
- lfsr_mid_bid -> lfsr_mbid
- lfsr_mid_rid -> lfsr_mrid

These now match the naming in the dbg scripts.

I feel like this is more terse in a way that is also more readable, but
maybe that's just me.
2025-04-20 15:53:18 -05:00
Christopher Haster 2b3fdffe4c Renamed tailp -> ptail
This is mainly just to match pcache.

Which is where I would put ptail if these two didn't have annoyingly
different flush semantics.
2025-02-13 16:07:02 -06:00
Christopher Haster 19a23c7788 Renamed/reverted file->buffer -> file->cache
And the related config options:

- cfg->file_buffer_size -> cfg->file_cache_size
- file->cfg->buffer_size -> file->cfg->cache_size
- file->cfg->buffer -> file->cfg->cache_buffer

The original motivation to rename this to file->buffer was to better
align with what other filesystems call this, but I think this is a case
where internal consistency is more important than external consistency.

file->cache better matches lfs->pcache and lfs->rcache, and makes it
easier to read code involving both file->cache and other user-provided
buffers.

Keeping the upstream name also helps with continuity.
2025-02-13 16:02:46 -06:00
Christopher Haster 76e0f8f73c Reverted lfsr_rat_t -> lfsr_rattr_t
This is the correct name for our rbyd attr type, even if it requires a
bit more typing.

lfsr_attr_t would be a better name, but that conflicts with our
user-facing attrs.
2025-02-11 02:50:38 -06:00
Christopher Haster bac2464b8f Renamed lfs->cfg->shrub_size -> lfs->cfg->inline_size
While I think shrub_size is probably the more correct name at a
technical level, inline_size is probably more what users expect and
doesn't require a deeper understanding of filesystem details.

The only risk is that users may think inline_size has no effect on large
files, when in fact it still controls how much of the btree root can be
inlined.

There's also the point that sticking with inline_size maintains
compatibility with both the upstream version and any future version that
has other file representations.

May revisit this, but renaming to lfs->cfg->inline_size for now.
2025-02-11 02:50:38 -06:00
Christopher Haster 6cd29bede2 Dropped lfs->cfg->inline_size
Now that we no longer have bmoss files, inline_size and shrub_size are
effectively the same thing.

We weren't using this, so no code change, but it does save a word of
ctx:

           code          stack          ctx
  before: 36280           2576          640
  after:  36280 (+0.0%)   2576 (+0.0%)  636 (-0.6%)
2025-02-11 02:50:38 -06:00
Christopher Haster d248f70e6a Adopted LFSR_DATA_ISBPTR flag in lfsr_data_t/lfsr_bptr_t
This takes advantage of another bit in lfsr_data_t's size field to
differentiate between normal lfsr_data_ts, and lfsr_data_ts in a bptr:

  in-RAM buffer:       on-disk data:        on-disk bptr:
  .---+---+---+---. .. .---+---+---+---. .. .---+---+---+---.
  |00|   size     |    |10|   size     |    |11|   size     |
  +---+---+---+---+ .. +---+---+---+---+    +---+---+---+---+
  |      ptr -------.  |     block     |    |     block     |
  +---+---+---+---+ |  +---+---+---+---+    +---+---+---+---+
  |    (unused)   | |  |      off      |    |      off      |
  '---+---+---+---' |  '---+---+---+---' .. +---+---+---+---+
  .---+---+---+---. |                       |     cksize    |
  |     data      |<'                       +---+---+---+---+
  :       :       :                         |     cksum     |
                                            '---+---+---+---'

Note this bit is unused even in a theoretical 16/14-bit littlefs mode.
This also leaves space for one more encoding (0b01), but I don't have
any good use for this yet. Previous ideas around an inlined
representation failed to improve anything.

This accomplishes a couple things:

1. We no longer need to return the tag in lfsr_file_lookupnext, since
   these can only be blocks or fragments.

2. We no longer need to rely on cksize=0 to determine checksummed data
   from non-checksummed data when running with LFS_CKDATACKSUMS.

This was supposed to be a relatively free optimization, but our
lfsr_data_fromslice implementation is being a bit... funky... It seems
we're right on the edge of some inline heuristic, where adding this flag
prevents lfsr_data_fromslice from being inlined, missing a number of
contextual optimizations and causing things to explode.

This can be worked around with __attribute__((always_inline)), but we
should probably revisit our data slicing macros to see if this can be
solved without a compiler specific hack. Relying on such a sensitive
function is not great:

                        code          stack          ctx
  always_inline:       36320           2584          640
  inline:              36424 (+0.3%)   2664 (+3.1%)  640 (+0.0%)

Weird inlining noise aside, this was an overall improvement. Not needing
to fetch tags in lfsr_file_lookupnext saves a bit of stack in our
hot-path, which is nice:

                        code          stack          ctx
  default before:      36460           2608          640
  default after:       36320 (-0.4%)   2584 (-0.9%)  640 (+0.0%)

Hmmm, though maybe not for ckdatacksums:

                        code          stack          ctx
  ckdatacksums before: 37628           3048          640
  ckdatacksums after:  38096 (+1.2%)   3072 (+0.8%)  640 (+0.0%)
2025-02-11 02:50:38 -06:00
Christopher Haster bc639b03f2 Reworked lfsr_bshrub_t, renamed file.o -> file.b
This moves all of the shrub tracking logic from lfsr_obshrub_t into
lfsr_bshrub_t, completely drops the lfsr_obshrub_t type, and changes all
lfsr_bshrub_* functions to take lfsr_bshrub_t instead of the mdir+shrub
pair.

This makes the lfsr_bshrub_* functions <-> lfsr_bshrub_t relationship
more consistent with other APIs, such as lfsr_btree_t:

  - lfsr_bshrub_lookupnext(lfs, &file->o.o.mdir, &file->o.bshrub, ...)
  + lfsr_bshrub_lookupnext(lfs, &file->b, ...)

I think the reason why this design wasn't obvious before is because, at
least conceptually, having the lfsr_mdir_t live inside the lfsr_bshrub_t
is a bit weird. It's only thanks to lfsr_file_t invasively using the
internal lfsr_mdir_t that we can avoid duplicate lfsr_mdir_t objects.

This also reorganizes the structs in lfs.h a bit, and renames the
related file.o -> file.b fields (much needed because lfs->gc.t.o.o.mdir.
rbyd.blocks was starting to get _real_ confusing).

---

Unfortunately, reducing the number of arguments to lfsr_bshrub_*
functions did not save nearly as much code as I thought it would. It
even ended up with a net _increase_ of code, apparently due to needing
to recalculate the bshrub->shrub offset more often:

           code          stack          ctx
  before: 36476           2608          640
  after:  36484 (+0.0%)   2608 (+0.0%)  640 (+0.0%)

Strange, but this rework is still worthwhile if only for the code
readability.
2025-02-11 02:50:28 -06:00
Christopher Haster 7c17be4dbe Typedefed lfsr_shrub_t -> lfsr_rbyd_t, replacing lfsr_bshrub_t union
The lfsr_shrub_t/lfsr_btree_t union was _technically_ not undefined
behavior, because the relevant fields were all a part of the "common
initial sequence", but collapsing these to the same type certainly does
simplify things.

The only weirdness is that we now store shrub.estimate in shrub.eoff.

We could add a union here, but the extra noise is just not worth the
slighty better name. The shrub.estimate is a sort of "simulated
shrub.eoff" anyways.

---

This makes it so all of these types alias to the same core lfsr_rbyd_t
type, which I suppose actually reflects the on-disk format quite well:

  lfsr_shrub_t  => lfsr_rbyd_t
  lfsr_bshrub_t
  lfsr_btree_t

Code cost more-or-less unaffected:

           code          stack          ctx
  before: 36432           2608          640
  after:  36436 (+0.0%)   2608 (+0.0%)  640 (+0.0%)
2025-02-08 15:02:31 -06:00
Christopher Haster 77a9ce3418 Tweaked lfsr_bshrub_t representation to rely on LFSR_RBYD_ISSHRUB
D'oh, here I am trying to rely solely on our shrub.block == mdir.block
condition to tell bshrubs and btrees apart, when we already have
LFSR_RBYD_ISSHRUB as an explicit flag in the rbyd code!

Long story short, these are equivalent:

  bshrub.trunk & LFSR_RBYD_ISSHRUB => bshrub is shrub
  bshrub.block == mdir.block       => bshrub is shrub

But in theory flag checks are cheaper and require less things being
in-sync (i.e. fewer things can go wrong).

This also means we only need to look at bshrub.trunk to determine if
it's a bshrub, btree, or neither (trunk=0):

  bnull:               bshrub:              btree:
  .---+---+---+---. .. .---+---+---+---. .. .---+---+---+---.
  |    weight=0   |    |    weight>0   |    |    weight>0   |
  +---+---+---+---+    +---+---+---+---+    +---+---+---+---+
  |   block=mdir  |    |   block=mdir  |    |  block!=mdir  |
  +---+---+---+---+ .. +---+---+---+---+    +---+---+---+---+
  |    (unused)   |    |    (unused)   |    |    (unused)   |
  +---+---+---+---+    +---+---+---+---+    +---+---+---+---+
  |0|  trunk=0    |    |1|   trunk     |    |0|   trunk     |
  +---+---+---+---+    +---+---+---+---+ .. +---+---+---+---+
  |    (unused)   |    |    estimate   |    |p|   eoff      |
  +               +    +---+---+---+---+    +---+---+---+---+
  |               |    |    (unused)   |    |     cksum     |
  '---+---+---+---'    '---+---+---+---'    '---+---+---+---'

As a side-effect, lfsr_file_truncate/fruncate are back to dropping
zero-weight btrees even if they have erased-state (now handled in
lfsr_file_carve). On reflection this is the simpler approach, consistent
with LFS_O_TRUNC, uses fewer blocks, and if keeping erased-state turns
out to be more valuable we can always change this in the future.

Though we should at least add a test that we can read existing
zero-weight btrees and bshrubs...

---

This ended up highlighting that we were leaving dangling bshrub
references in lfsr_mtree_traverse_!

You may think these dangling references would've been fine with the
previous logic, but they could've created problems when the block
allocator makes a full circle. Not great!

Fortunately, relying on LFSR_RBYD_ISSHRUB is a lot safer, and lets us
catch issues like this with asserts in lfsr_mdir_commit.

---

Code savings were a bit disappointing, but any change that reduces
assumptions in the code is a good change:

           code          stack          ctx
  before: 36460           2608          640
  after:  36424 (-0.1%)   2608 (+0.0%)  640 (+0.0%)
2025-02-08 15:02:31 -06:00
Christopher Haster 475ca76cdf Simplified lfsr_bshrub_t representation
Now that we don't use bmoss or bsprouts anymore, we can drop the
LFSR_BSHRUB_ISNULLORBMOSSORBPTR flag and simplify our lfsr_bshrub_t
struct quite a bit.

However, we do still need a bnull representation, which is surprisingly
tricky... And annoying...

Current solution: Bnulls are bshrubs with weight=0. This works, but
unfortunately does mean we need to update bnull blocks on mdir
relocation/compaction, and risks bnull blocks falling out-of-sync, which
is a really weird thing to worry about:

  bnull:               bshrub:              btree:
  .---+---+---+---. .. .---+---+---+---. .. .---+---+---+---.
  |    weight=0   |    |    weight>0   |    |    weight     |
  +---+---+---+---+    +---+---+---+---+    +---+---+---+---+
  |   block=mdir  |    |   block=mdir  |    |  block!=mdir  |
  +---+---+---+---+ .. +---+---+---+---+    +---+---+---+---+
  |    (unused)   |    |    (unused)   |    |    (unused)   |
  +               +    +---+---+---+---+    +---+---+---+---+
  |               |    |     trunk     |    |     trunk     |
  +               +    +---+---+---+---+ .. +---+---+---+---+
  |               |    |    estimate   |    |     eoff      |
  +               +    +---+---+---+---+    +---+---+---+---+
  |               |    |    (unused)   |    |     cksum     |
  '---+---+---+---'    '---+---+---+---'    '---+---+---+---'

Note we can't just assume all weight=0 files are bnulls, or else we
won't use erased-state in empty btree roots. This risks thrashing in
files oscillating around weight=0.

Technically, weight=0 bshrubs _are_ slightly different than bnulls (
bshrubs point to a null tag, while bnulls simply have no tree), but
unlike btrees, there's no reason to keep weight=0 bshrubs around. Any
bshrub erased-state can still be used by the mdir.

This change also makes LFS_O_TRUNC and lfsr_file_truncate/fruncate
behave slightly differently, with LFS_O_TRUNC unconditionally reverting
to a bnull, while lfsr_file_truncate/fruncate tries to keep the btree
root around. This may be worth revisiting...

---

Despite the awkward encoding, this simplification still ends up saving a
nice bit of code and stack:

           code          stack          ctx
  before: 36668           2616          640
  after:  36460 (-0.6%)   2608 (-0.3%)  640 (+0.0%)
2025-02-08 15:02:31 -06:00
Christopher Haster 01f2d613bd Simplified lfsr_mtree_t now that we don't need to represent msprouts
We had to be a bit clever with our lfsr_mtree_t representation to
support msprouts. Now that we don't support msprouts, we can simplify
this and drop the lfsr_mtree_t type completely! which is nice for both
code cost and readability.

Saves a bit more code:

           code          stack          ctx
  before: 38344           2624          640
  after:  38284 (-0.2%)   2624 (+0.0%)  640 (+0.0%)

Which increases the total savings of dropping msprouts:

                 code          stack          ctx
  yes msprouts: 38508           2624          640
  no msprouts:  38284 (-0.6%)   2624 (+0.0%)  640 (+0.0%)
2025-02-08 15:02:31 -06:00
Christopher Haster 415e6325d1 Moved revision count noise behind ifdef LFS_NOISY
littlefs is intentionally designed to not rely on noise, even with cksum
collisions (hello, perturb bit!). So it makes sense for this to be an
optional feature, even if it's a small one.

Disabling revision count noise by default also helps with testing. The
whole point of revision count noise is to make cksum collisions less
likely, which is a bit counterproductive when that's something we want
to test!

This doesn't really change the revision count encoding:

  vvvvrrrr rrrrrrnn nnnnnnnn nnnnnnnn
  '-.''----.----''---------.--------'
    '------|---------------|---------- 4-bit relocation revision
           '---------------|---------- recycle-bits recycle counter
                           '---------- pseudorandom noise (optional)

I considered moving the recycle-bits down when we're not adding noise,
but the extra logic just isn't worth making the revision count a bit
more human-readable.

---

This saves a small bit of code in the default build, at the cost of some
code for the runtime checks in the LFS_NOISY build. Though I'm hoping
future config work will let users opt-out of these runtime checks:

                    code          stack          ctx
  before:          38548           2624          640
  default after:   38508 (-0.1%)   2624 (+0.0%)  640 (+0.0%)
  LFS_NOISY after: 38568 (+0.1%)   2624 (+0.0%)  640 (+0.0%)

Honestly the thing I'm more worried about is using one of our precious
mount flags for this... There's not that many bits left!
2025-02-08 14:53:47 -06:00
Christopher Haster 109bd4e0ab Added lfsr_fs_cksum
This just exposes the gcksum to the user, but exposing the gcksum allows
the user to store it externally for an extra layer of protection against
filesystem corruption.

As far as I'm aware this is the only real way to protect against global
rollback issues, which is a problem for any filesystem with logs (aka
any powerloss-resilient filesystem).

This required a comically small amount of code:

           code          stack          ctx
  before: 38492           2624          640
  after:  38500 (+0.0%)   2624 (+0.0%)  640 (+0.0%)
2025-02-08 14:53:47 -06:00
Christopher Haster a63b8e1527 Dropped internal LFS_i_UNTIDY pseudo-alias flag
We really shouldn't have two names for the same thing, it just makes
things more confusing, even if the public name doesn't quite match the
internal usage. Especially now that we internally rely on these being
the same flag.

This renames LFS_i_UNTIDY -> LFS_I_MKCONSISTENT and drops the untidy/
mktidy naming internally.

No code changes.
2025-02-08 14:53:47 -06:00
Christopher Haster 8cfaacbfb6 Replaced lfs->seed with lfs->gcksum for pseudorandom noise
lfs->seed was already just the rough xor of all mdir cksums, so
replacing it with our gcksum doesn't really do anything but make its
definition more rigorous.

That and save both code and ctx:

           code          stack          ctx
  before: 38560           2624          644
  after:  38492 (-0.2%)   2624 (+0.0%)  640 (-0.6%)
2025-02-08 14:53:47 -06:00
Christopher Haster 57e9c3b706 Check gcksum during traversals, harder ckmeta/ckdata tests
This adds a check that the on-disk gcksum matches the in-RAM gcksum
in lfsr_mtree_traverse, so ckmeta/ckdata scans should now be able to
at least detect global-rollback issues that occur while mounted.

This also moves the LFS_I_CKMETA/CKDATA flag clearing logic from
lfsr_mtree_gc -> lfsr_mtree_traverse. There's no reason to not clear
these flags if we've made a successful traversal. We weren't actually
calling lfsr_mtree_traverse with the right flags for this to matter, but
it does let us drop an explicit flag clear in lfsr_fs_ck.

---

These changes were a part of adding the harder versions of our ckmeta/
ckdata tests, where we flip individual bits instead of clobbering the
entire block. These are more realistic errors and stress our gcksum
system.

Recalculating the gcksum required another gcksum copy in
lfsr_traversal_t, which adds a bit of code and ctx to our incremental-gc
build:

                   code          stack          ctx
  default before: 38428           2640          644
  default after:  38560 (+0.3%)   2640 (+0.0%)  644 (+0.0%)

  gc before:      38484           2640          788
  gc after:       38616 (+0.3%)   2640 (+0.0%)  792 (+0.5%)

Unfortunately we can't easily abuse the copies in lfs_t since
multiple traversals may be open at once.
2025-02-08 14:53:47 -06:00
Christopher Haster 1c5adf71b3 Implemented self-validating global-checksums (gcksums)
This was quite a puzzle.

The problem: How do we detect corrupt mdirs?

Seems like a simple question, but we can't just rely on mdir cksums. Our
mdirs are independently updateable logs, and logs have this annoying
tendency to "rollback" to previously valid states when corrupted.

Rollback issues aren't littlefs-specific, but what _is_ littlefs-
specific is that when one mdir rolls back, it can disagree with other
mdirs, resulting in wildly incorrect filesystem state.

To solve this, or at least protect against disagreeable mdirs, we need
to somehow include the state of all other mdirs in each mdir commit.

---

The first thought: Why not use gstate?

We already have a system for storing distributed state. If we add the
xor of all of our mdir cksums, we can rebuild it during mount and verify
that nothing changed:

   .--------.   .--------.   .--------.   .--------.
  .| mdir 0 |  .| mdir 1 |  .| mdir 2 |  .| mdir 3 |
  ||        |  ||        |  ||        |  ||        |
  || gdelta |  || gdelta |  || gdelta |  || gdelta |
  |'-----|--'  |'-----|--'  |'-----|--'  |'-----|--'
  '------|-'   '------|-'   '------|-'   '------|-'
  '--.------'  '--.------'  '--.------'  '--.------'
   cksum |      cksum |      cksum |      cksum |
     |   |        v   |        v   |        v   |
     '---------> xor -------> xor -------> xor -------> gcksum
         |            v            v            v         =?
         '---------> xor -------> xor -------> xor ---> gcksum

Unfortunately it's not that easy. Consider what this looks like
mathematically (g is our gcksum, c_i is an mdir cksum, d_i is a
gcksumdelta, and +/-/sum is xor):

  g = sum(c_i) = sum(d_i)

If we solve for a new gcksumdelta, d_i:

  d_i = g' - g
  d_i = g + c_i - g
  d_i = c_i

The gcksum cancels itself out! We're left with an equation that depends
only on the current mdir, which doesn't help us at all.

Next thought: What if we permute the gcksum with a function t before
distributing it over our gcksumdeltas?

   .--------.   .--------.   .--------.   .--------.
  .| mdir 0 |  .| mdir 1 |  .| mdir 2 |  .| mdir 3 |
  ||        |  ||        |  ||        |  ||        |
  || gdelta |  || gdelta |  || gdelta |  || gdelta |
  |'-----|--'  |'-----|--'  |'-----|--'  |'-----|--'
  '------|-'   '------|-'   '------|-'   '------|-'
  '--.------'  '--.------'  '--.------'  '--.------'
   cksum |      cksum |      cksum |      cksum |
     |   |        v   |        v   |        v   |
     '---------> xor -------> xor -------> xor -------> gcksum
         |            |            |            |   .--t--'
         |            |            |            |   '-> t(gcksum)
         |            v            v            v          =?
         '---------> xor -------> xor -------> xor ---> t(gcksum)

In math terms:

  t(g) = t(sum(c_i)) = sum(d_i)

In order for this to work, t needs to be non-linear. If t is linear, the
same thing happens:

  d_i = t(g') - t(g)
  d_i = t(g + c_i) - t(g)
  d_i = t(g) + t(c_i) - t(g)
  d_i = t(c_i)

This was quite funny/frustrating (funnistrating?) during development,
because it means a lot of seemingly obvious functions don't work!

- t(g) = g              - Doesn't work
- t(g) = crc32c(g)      - Doesn't work because crc32cs are linear
- t(g) = g^2 in GF(2^n) - g^2 is linear in GF(2^n)!?

Fortunately, powers coprime with 2 finally give us a non-linear function
in GF(2^n), so t(g) = g^3 works:

  d_i = g'^3 - g^3
  d_i = (g + c_i)^3 - g^3
  d_i = (g^2 + gc_i + gc_i + c_i^2)(g + c_i) - g^3
  d_i = (g^2 + c_i^2)(g + c_i) - g^3
  d_i = g^3 + gc_i^2 + g^2c_i + c_i^3 - g^3
  d_i = gc_i^2 + g^2c_i + c_i^3

---

Bleh, now we need to implement finite-field operations? Well, not
entirely!

Note that our algorithm never uses division. This means we don't need a
full finite-field (+, -, *, /), but can get away with a finite-ring (+,
-, *). And conveniently for us, our crc32c polynomial defines a ring
epimorphic to a 31-bit finite-field.

All we need to do is define crc32c multiplication as polynomial
multiplication mod our crc32c polynomial:

  crc32cmul(a, b) = pmod(pmul(a, b), P)

And since crc32c is more-or-less just pmod(x, P), this lets us take
advantage of any crc32c hardware/tables that may be available.

---

Bunch of notes:

- Our 2^n-bit crc-ring maps to a 2^n-1-bit finite-field because our crc
  polynomial is defined as P(x) = Q(x)(x + 1), where Q(x) is a 2^n-1-bit
  irreducible polynomial.

  This is a common crc construction as it provides optimal odd-bit/2-bit
  error detection, so it shouldn't be too difficult to adapt to other
  crc sizes.

- t(g) = g^3 is not the only function that works, but it turns out to be
  a pretty good one:

  - 3 and 2^(2^n-1)-1 are coprime, which means our function t(g) = g^3
    provides a one-to-one mapping in the underlying fields of all crc
    rings of size 2^(2^n).

    We know 3 and 2^(2^n-1)-1 are coprime because 2^(2^n-1)-1 =
    2^(2^n)-1 (a Fermat number) - 2^(2^n-1) (a power-of-2), and 3
    divides Fermat numbers >=3 (A023394) and is not 2.

  - Our delta, when viewed as a polynomial in g: d(g) = gc^2 + g^2c +
    c^3, has degree 2, which implies there are at most 2 solutions or
    1-bit of information loss in the underlying field.

    This is optimal since the original definition already had 2
    solutions before we even chose a function:

      d(g) = t(g + c) - t(g)
      d(g) = t(g + c) - t((g + c) - c)
      d(g) = t((g + c) + c) - t(g + c)
      d(g) = d(g + c)

  Though note the mapping of our crc-ring to the underlying field
  already represents 1-bit of information loss.

- If you're using a cryptographic hash or other non-crc, you should
  probably just use an equal sized finite-field.

  Though note changing from a 2^n-1-bit field to a 2^n-bit field does
  change the math a bit, with t(g) = g^7 being a better non-linear
  function:

  - 7 is the smallest odd-number coprime with 2^n-1, a Fermat number,
    which makes t(g) = g^7 a one-to-one mapping.

    3 humorously divides all 2^n-1 Fermat numbers.

  - Expanding delta with t(g) = g^7 gives us a 6 degree polynomial,
    which implies at most 6 solutions or ~3-bits of information loss.

    This isn't actually the best you can do, some exhaustive searching
    over small fields (<=2^16) suggests t(g) = g^(2^(n-1)-1) _might_ be
    optimal, but that's a heck of a lot more multiplications.

- Because our crc32cs preserve parity/are epimorphic to parity bits,
  addition (xor) and multiplication (crc32cmul) also preserve parity,
  which can be used to show our entire gcksum system preserves parity.

  This is quite neat, and means we are guaranteed to detect any odd
  number of bit-errors across the entire filesystem.

- Another idea was to use two different addition operations: xor and
  overflowing addition (or mod a prime).

  This probably would have worked, but lacks the rigor of the above
  solution.

- You might think an RS-like construction would help here, where g =
  sum(c_ia^i), but this suffers from the same problem:

    d_i = g' - g
    d_i = g + c_ia^i - g
    d_i = c_ia^i

  Nothing here depends on anything outside of the current mdir.

- Another question is should we be using an RS-like construction anyways
  to include location information in our gcksum?

  Maybe in another system, but I don't think it's necessary in littlefs.

  While our mdir are independently updateable, they aren't _entirely_
  independent. The location of each mdir is stored in either the mtree
  or a parent mdir, so it always gets mixed into the gcksum somewhere.

  The only exception being the mrootanchor which is always at the fixed
  blocks 0x{0,1}.

- This does _not_ catch "global-rollback" issues, where the most recent
  commit in the entire filesystem is corrupted, revealing an older, but
  still valid, filesystem state.

  But as far as I am aware this is just a fundamental limitation of
  powerloss-resilient filesystems, short of doing destructive
  operations.

  At the very least, exposing the gcksum would allow the user to store
  it externally and prevent this issue.

---

Implementation details:

- Our gcksumdelta depends on the rbyd's cksum, so there's a catch-22 if
  we include it in the rbyd itself.

  We can avoid this by including it in the commit tags (actually the
  separate canonical cksum makes this easier than it would have been
  earlier), but this does mean LFSR_TAG_GCKSUMDELTA is not an
  LFSR_TAG_GDELTA subtype. Unfortunate but not a dealbreaker.

- Reading/writing the gcksumdelta gets a bit annoying with it not being
  in the rbyd. For now I've extended the low-level lfsr_rbyd_fetch_/
  lfsr_rbyd_appendcksum_ to accept an optional gcksumdelta pointer,
  which is a bit awkward, but I don't know of a better solution.

- Unlike the grm, _every_ mdir commit involves the gcksum, which means
  we either need to propagate the gcksumdelta up the mroot chain
  correctly, or somehow keep track of partially flushed gcksumdeltas.

  To make this work I modified the low-level lfsr_mdir_commit__
  functions to accept start_rid=-2 to indicate when gcksumdeltas should
  be flushed.

  It's a bit of a hack, but I think it might make sense to extend this
  to all gdeltas eventually.

The gcksum cost both code and RAM, but I think it's well worth it for
removing an entire category of filesystem corruption:

           code          stack          ctx
  before: 37796           2608          620
  after:  38428 (+1.7%)   2640 (+1.2%)  644 (+3.9%)
2025-02-08 14:53:30 -06:00
Christopher Haster e5609c98ec Renamed bsprout -> bmoss, bleaf -> bsprout
I just really don't like saying bleaf. Also I think the term moss
describes inlined data a bit better.
2025-01-28 14:41:45 -06:00
Christopher Haster 5f6dbdcb14 Reworked o/f/m/gc/i/t flags
This is mainly to free up space for flags, we're pretty close to running
out of 32-bits with future planned features:

1. Reduced file type info from 8 -> 4 bits

   We don't really need more than this, but it does mean type info is
   no longer a simple byte load.

2. Moved most internal file-state flags into the next 4 bits

   These are mostly file-type specific (except LFS_o_ZOMBIE), so we
   don't need to worry too much about overlap.

3. Compacted ck-flags into 5 bits:

     LFS_M_CKPROGS       0x00000800
     LFS_M_CKFETCHES     0x00001000
     LFS_M_CKPARITY      0x00002000
     LFS_M_CKMETAREDUND* 0x00004000
     LFS_M_CKDATACKSUMS  0x00008000

     *Planned

   Now that ck-flags are a bit more mature, it's pretty clear we'll
   probably never have CKMETACKSUMS (ckcksums + small tag reads is
   crazy expensive) or CKDATAREDUND (non-trivial parity fanout makes
   this crazy expensives. So reserving bits for these just wastes bits.

This also moves things around so ck-flags no longer overlap with open
flags.

It's a tight fit, and I still think file-specific ck-flags are out-of-
scope, but this at least decreases flag ambiguity.

New jenga:

              8     8     8     8
            .----++----++----++----.
            .-..-..-.-------.------.
  o_flags:  |t||f||t|       |  o   |
            |-||-||-|-------:--.---'
            |-||-||-'--.----.------.
  t_flags:  |t||f|| t  |    | tstt |
            '-''-'|----|----'------'
            .----.|----|.--.:--:.--.
  m_flags:  | f  || t  ||c ||o ||m |
            |----||-.--'|--|'--''--'
            |----||-|---|--|.------.
  f_flags:  | f  ||t|   |c ||  f   |
            '----''-'---'--''------'

Fortunately no major code costs:

           code          stack          ctx
  before: 37792           2608          620
  after:  37788 (-0.0%)   2608 (+0.0%)  620 (+0.0%)
2025-01-28 14:41:45 -06:00
Christopher Haster 726bf86d21 Added dbgflags.py for easier flag debugging
dbgerr.py and dbgtag.py have proven to be incredibly useful for quick
debugging/introspection, so I figured why not have more of that.

My favorite part is being able to quickly see all flags set on an open
file handle:

  (gdb) p file.o.o.flags
  $2 = 24117517
  (gdb) !./scripts/dbgflags.py o 24117517
  LFS_O_WRONLY   0x00000001  Open a file as write only
  LFS_O_CREAT    0x00000004  Create a file if it does not exist
  LFS_O_EXCL     0x00000008  Fail if a file already exists
  LFS_O_DESYNC   0x00000100  Do not sync or recieve file updates
  LFS_o_REG      0x01000000  Type = regular-file
  LFS_o_UNFLUSH  0x00100000  File's data does not match disk
  LFS_o_UNSYNC   0x00200000  File's metadata does not match disk
  LFS_o_UNCREAT  0x00400000  File does not exist yet

The only concern is if dbgflags.py falls out-of-sync often, I suspect
flag encoding will have quite a bit more churn than flags/tags. But we
can always drop this script in the future if this turns into a problem.

---

While poking around this also ended up with a bunch of other small
changes:

- Added LFS_*_MODE masks for consistency with other "type<->flag
  embeddings"

- Added compat flag comments

- Adopted lowercase prefix for internal flags (LFS_o_ZOMBIE), though
  not sure if I'll keep this yet...

- Tweaked dbgerr.py to also match ERR_ prefixes and to ignore case
2025-01-28 14:41:45 -06:00
Christopher Haster 9ed9cf0ccd gc: Added more tests over info flags, dropped gc_flags default
Since we dropped lfsr_gc_setflags/setsteps, it was no longer possible to
set gc_flags to zero (perfectly valid and useful for system bringup/
testing things). Supporting gc_flags=0 means it's not possible to
provide a default, but this is probably ok as users need to opt-in to
LFS_GC anyways.

Note that at least gc_steps=0 doesn't make sense, so the default there
is reasonable.

Fixing this also highlighted that gc_flags/steps are no longer mutable,
making the comment in lfs_init out-of-date. Dropping these saves a bit
of lfs_t size, so that's nice.

And then testing also revealed that LFS_GC_CKDATA implying LFS_GC_CKDATA
means it should probably clear the LFS_I_CKMETA flag as well.

---

And here I thought this was going to be just a simple test-writing
exercise!

Code changes:

                   code          stack          ctx
  default before: 37792           2608          620
  default after:  37792 (-0.0%)   2608 (+0.0%)  620 (+0.0%)

  gc before:      37896           2608          768
  gc after:       37848 (-0.1%)   2608 (+0.0%)  760 (-1.0%)
2025-01-28 14:41:45 -06:00
Christopher Haster 1965593644 Dropped LFS_F_COMPACT flags from lfsr_format
The argument for this flag is pretty brittle. Yes it's _technically_
possible to end up with a compactable filesystem during lfsr_format, but
it's pretty unlikely. And keeping LFS_F_COMPACT around means we'd always
need the lfsr_mtree_gc circuitry in lfsr_format, for such a niche
situation, that can be easily cleaned up in lfsr_mount.

So dropping for now.

No code changes, but this does mean one less feature to support:

           code          stack          ctx
  before: 37804           2608          620
  after:  37804 (+0.0%)   2608 (+0.0%)  620 (+0.0%)
2025-01-28 14:41:45 -06:00
Christopher Haster 94e9cb5081 Dropped LFS_T_MTREEONLY from all APIs except lfsr_traversal_t
Looking at future planned features, we're running into some real issues
fitting all these flags into 32 bits.

I think the only real use case for LFS_T_MTREEONLY is in
lfsr_traversal_t, where the depth of traversal can't be infered. So no
reason to keep this flag around in the other APIs.

No code changes:

                   code          stack          ctx
  default before: 37804           2608          620
  default after:  37804 (+0.0%)   2608 (+0.0%)  620 (+0.0%)

  gc before:      37940           2608          768
  gc after:       37940 (+0.0%)   2608 (+0.0%)  768 (+0.0%)
2025-01-28 14:41:45 -06:00
Christopher Haster a4c74967ec Renamed LFS_I_* flags to match LFS_GC_*
- LFS_I_INCONSISTENT -> LFS_I_MKCONSISTENT
- LFS_I_CANLOOKAHEAD -> LFS_I_LOOKAHEAD
- LFS_I_UNCOMPACTED  -> LFS_I_COMPACT
- LFS_I_CANCKMETA    -> LFS_I_CKMETA
- LFS_I_CANCKDATA    -> LFS_I_CKDATA

This just makes everything easier to read/pattern match, even if it's
a bit inaccurate english-wise. The imperative transformations were also
wildly inconsistent...
2025-01-28 14:41:45 -06:00
Christopher Haster 9c9a23e27b gc: Renamed lfsr_gc -> lfsr_fs_gc, keep lfsr_fs_unck in non-gc
- lfsr_gc -> lfsr_fs_gc
- lfsr_gc_unck -> lfsr_fs_unck

lfsr_fs_unck is surprisingly still useful in non-gc builds, since we
still have ckmeta/ckdata state. These flags can still be queried with
lfsr_fs_stat and cleared with lfsr_fs_ckmeta/ckdata/lfsr_traversal_t, so
it seems useful to keep this function around.

It's also a relatively cheap function.

Though this does mean it deserves a rename. Dropping the gc prefix
hopefully makes it clearer this function is not entirely gc-specific.

And since we no longer have lfsr_gc_setflags/setsteps, it makes sense to
rename lfsr_gc back to lfsr_fs_gc, to be consistent with the other
filesystem-wide utilities.

Code changes, apparently lfsr_fs_unck costs 12 bytes:

                   code          stack          ctx
  default before: 37792           2608          620
  default after:  37804 (+0.0%)   2608 (+0.0%)  620 (+0.0%)

  gc before:      37938           2608          768
  gc after:       37940 (+0.0%)   2608 (+0.0%)  768 (+0.0%)
2025-01-28 14:41:45 -06:00