Commit Graph

1360 Commits

Author SHA1 Message Date
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 a2b50d2463 Dropped bmoss/bsprout support completely
Similar to msprouts/mshrubs, this drops all of the logic necessary for
reading and maintaining bmoss/bshrubs, while clearing/reserving the
LFSR_RCOMPAT_BMOSS/BSPROUT flags in case we want to re-explore these in
the future.

---

Wait, wait, dropping bmosses? Inlined files? Aren't inlined files pretty
fundamental to littlefs?

  bmoss:         bsprout:       bshrub:        btree:
   .--------.     .--------.     .--------.     .--------.
  .| mdir   |    .| mdir   |    .| mdir   |    .| mdir   |
  || name   |    || name   |    || name   |    || config |
  || data   |    || bptr   |    || bshrub -.   || btree  |
  ||        |    ||   |    |    || data   <'   ||   |    |
  |'--------'    |'---|----'    |'--------'    |'---|----'
  '--------'     '----|---'     '--------'     '----|---'
                      v                             v
                  .--------.                    .--------.
                  | data   |                    | btree  |
                  |        |                    | data   |
                  |        |                    |        |
                  |        |                    |        |
                  '--------'                    '--------'

Yep! And that's why they stuck around for so long. I never really
expected something to replace the simplicity of inlined files.

But bmoss/inlined files' simplicity is deceptive. They're actually a big
pain-in-the-ass when you realize:

1. littlefs's file snapshot semantics means you somehow need to keep
   track of bmoss/inlined files that are no longer in the mdir tree.

2. You actually can't rely on bmoss/inlined files always fitting in RAM,
   in cases where the filesystem is shared between different drivers
   with different configurations.

   This has been a pretty big pain point upstream, and results in tricky
   edge cases that are difficult to test.

Bshrubs just sort of side-step these problems...

Well, we _do_ still need quite a bit of logic to keep track of detached
bshrubs across mdir compactions, but we'd need that logic for bshrubs
anyways. And why pay for two piles of logic if we can get away with one?

The only difference between inlined bmoss and inlined bshrubs is the
trunk pointer (<=9 bytes) and extra data tag (<=10 bytes), adding at
most 19 extra bytes per file.

But these extra 19 bytes per file save us a big chunk of code:

           code          stack          ctx
  before: 37652           2616          640
  after:  36668 (-2.6%)   2616 (+0.0%)  640 (+0.0%)

---

There _is_ an argument for keeping bmoss around: In cases where you have
a shitton of tiny files, these extra 19 bytes may add up.

But I think this is a micro-optimization for a very specific use case
that is out-of-scope for littlefs. We should always trade disk usage for
code size when possible.
2025-02-08 15:02:31 -06:00
Christopher Haster e580db20dd Dropped bmoss/inline file write strategy
This is the first step towards dropping bmoss/bsprout support
completely: Changing our write strategy to no longer emit bmosses.

Now, small files are converted directly to inlined bshrubs, which are
not _that_ much more overhead.

The biggest savings are in lfsr_file_truncate/fruncate, where we no
longer have to worry about the edge cases around converting from
bshrub/btree -> bmoss. We can just rely on btrees naturally folding into
bshrubs when they get small enough.

This saves a nice chunk of code, but keeping in mind we're still lugging
most of the bmoss circuitry around in order to support reading bmosses:

           code          stack          ctx
  before: 38284           2624          640
  after:  37652 (-1.7%)   2616 (-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 3f4984d33f Fixed truncated cksum tags reading past end-of-block
While we do check for out-of-bound tags in lfsr_bd_readtag, we were
ignoring the returned size in lfsr_rbyd_fetch when reading cksum tags.
This meant it was possible for lfsr_rbyd_fetch to try to read past the
end-of-block if:

1. The cksum tag was malformed with size < 4.

2. The malformed cksum tag was < 4 bytes from the end-of-block.

A pretty rare case! Considering we don't even bother writing cksum tags
when we're that close to the end-of-block. This can only happen in our
tests if existing garbage happens to look like a cksum tag.

While every cksum tag _should_ have at least 4 bytes for the cksum, we
can't guarantee that if we're parsing garbage.

Found by our test_ck_spam_dir_fuzz test.

---

I've also added a couple test_mtree_truncated_* tests to catch similar
truncation issues and prevent a regression in the future. We can't
really rely on test_ck_spam_* to always find nuanced errors like this,
but it's neat it found this one.

Code changes:

           code          stack          ctx
  before: 38340           2624          640
  after:  38344 (+0.0%)   2624 (+0.0%)  640 (+0.0%)
2025-02-08 15:02:31 -06:00
Christopher Haster 4a7e447051 Dropped msprouts/single mptr representation
I mean, what does the single mptr representation get us anyways?

A slightly smaller filesystem when metadata doesn't _quite_ fit in the
mroot, but fits in a single mdir? The only difference between the two is
the filesystem config, which really shouldn't be _that_ large:

  mmoss:         msprout:       mtree:
   .--------.     .--------.     .--------.
  .| mroot  |    .| mroot  |    .| mroot  |
  || config |    || config |    || config |
  || dir /  |    || mptr   |    || mptr   |
  |'--------'    |'---|----'    |'---|----'
  '--------'     '----|---'     '----|---'
                      v              v
                  .--------.     .--------.
                 .| mdir   |     | mtree  |
                 || mptr   |     | mptr   |
                 ||        |     |   |    |
                 |'--------'     '---|----'
                 '--------'          v
                                 .--------.
                                .| mdir   |
                                || mptr   |
                                ||        |
                                |'--------'
                                '--------'

Unlike the theoretical mshrub representation, which we've already given
up on, msprouts _don't_ require a significant code cost to implement.
But if they offer little value, why keep them around? Every code cost is
code cost.

---

This drops msprout support, saving a bit of code:

           code          stack          ctx
  before: 38508           2624          640
  after:  38340 (-0.4%)   2624 (+0.0%)  640 (+0.0%)

This also clears the LFSR_RCOMPAT_MSPROUT flag, allowing for msprouts to
be possibly reintroduced in the future.

But I don't really see any motivation for msprouts, even if you don't
care about code size, so I suspect this will just be a dead bit from now
on...
2025-02-08 15:02:26 -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 49a737b6a5 Added several more LFS_DEBUG* options
To help with debugging. These all seem useful, though the exact output
will probably be worth messing around with:

- LFS_DEBUGRBYDFETCHES - Debug every rbyd fetch
- LFS_DEBUGRBYDCOMMITS - Debug every rbyd commit
- LFS_DEBUGBTREEFETCHES - Debug every btree/bshrub fetch (though we
  currently don't fetch bshrubs...)
- LFS_DEBUGBTREECOMMITS - Debug every btree/bshrub commit
- LFS_DEBUGMDIRFETCHES - Debug every mdir fetch
- LFS_DEBUGMDIRCOMMITS - Debug every mdir commit
- LFS_DEBUGALLOCS - Debug every block allocation

Let's see if you can match these to each debug output:

  lfs.c:2942:debug: Fetched rbyd 0xe.d80 w77, eoff 3536, cksum 862283c6
  lfs.c:4233:debug: Committed rbyd 0xe.dd0 w78, eoff 3616, cksum 38ae1347
  lfs.c:4950:debug: Fetched btree 0x9f.806 w2048, cksum 7fb89b1b
  lfs.c:6609:debug: Committed btree 0x9f.806 w2048, cksum 7fb89b1b
  lfs.c:6603:debug: Committed bshrub 0x{0,1}.b06 w1747
  lfs.c:7290:debug: Fetched mdir -1 0x{1,0}.8f w0, cksum 7846be7a
  lfs.c:9022:debug: Committed mdir 0 0x{0,1}.a10 w2, cksum 4d2ccb29
  lfs.c:10083:debug: Allocated block 0x8f, lookahead 125/253/256

Also tweaked LFSR_DEBUGRBYDBALANCE to be a bit more readable when
LFS_DEBUGRBYDFETCHES is enabled, and tweaked the out-of-space error
message to show the same lookahead info as LFS_DEBUGALLOCS:

  lfs.c:10101:error: No more free space (lookahead 0/0/256)
                                                   ^ ^  ^
                             lookahead remaining --' |  |
                             ckpoint remaining ------'  |
                             block count ---------------'

No code changes.
2025-02-08 14:53:47 -06:00
Christopher Haster bf2be23f60 Added LFS_INFO, upgraded most LFS_DEBUG statements
This adds LFS_INFO and limits LFS_DEBUG to opt-in debug output.

Note that while LFS_DEBUG seems to be only used in LFS_DEBUGRBYDBALANCE,
it can also be useful for adding additional logging while debugging.

Having separate levels here is useful for filtering. Users should be
able to expect LFS_INFO output to not bog down a system, while LFS_DEBUG
can be a free-for-all dumping ground of debug info.

This also converges to the common 4 levels of logging found in other
systems, which was an intentional non-goal, but it's interesting to see
how each level serves a purpose.
2025-02-08 14:53:47 -06:00
Christopher Haster 98ecb5420d rbyd: Made LFS_DEBUGRBYDBALANCE output a bit more interesting
Now with both height (alt-height) and bheight (black-height):

  lfs.c:2970:debug: rbyd 0x9.c05: height 6-11, bheight 6-6

It's interesting to note this highlighted a minor mistake in our assumed
rbyd bounds.

Originally, our rbyd algorithm generated trees with height bounded by
2*bheight+1, but now that we have range removals, our height can get up
to 2*bheight+2 due to how diverged paths are stitched together.
2025-02-08 14:53:47 -06:00
Christopher Haster 558911693b rbyd: Renamed LFS_ASSERTRBYDBALANCE -> LFS_DEBUGRBYDBALANCE
This name helps avoid the mistake the that LFS_ASSERTRBYDBALANCE is
cheap/free like other asserts, which is very much not true.
LFS_DEBUGRBYDBALANCE is expensive and should only be used for testing.
2025-02-08 14:53:47 -06:00
Christopher Haster dbf0b5ebb1 rbyd: Cleaned up rbyd balance rework
Aside from cleaning up the mess of debug statements/commented code, this
also includes a bit of fiddling with the append logic to try to make
things a bit more readable and minimize code cost:

           code          stack          ctx
  before: 38784           2624          640
  after:  38548 (-0.6%)   2624 (+0.0%)  640 (+0.0%)

Now we can better compare before and after the balance rework:

                               code          stack          ctx
  before rbyd-balance-rework: 38440           2624          640
  after rbyd-balance-rework:  38548 (+0.3%)   2624 (+0.0%)  640 (+0.0%)

Though it's worth emphasizing that maintaining strictly balanced rbyds
is well worth the extra code cost, since it's sort of what the rest of
the filesystem is built on.
2025-02-08 14:53:47 -06:00
Christopher Haster b6b16099fc rbyd: Prioritized y-split prunes, fixed remaining balance issues!
The issue with relying solely on the jump > branch hack to disambiguate
recolorable prunes is that suddenly we're back in ambiguous territory
when we encounter post-split yellow nodes. We need to prioritize yellow
prunes or else post-split weights can become ambiguous.

This was the whole reason we added a 3rd color!

Fortunately we can still prioritize yellow nodes by trying both
recolorable prunes first, and only falling back to non-recolorable
prunes if we still have unreachable alts.

Ends up with a bit of code duplication, but gets things working again.

---

Turns out this was all we needed to get our rbyd operations perfectly
balanced! Now all test_rbyd tests are passing:

  test_rbyd+balance before: 1671/385878 failed
  test_rbyd+balance after:     0/385878 failed (-100.0%)

And after running the full test suite, can confirm _all_ tests are
passing with LFS_ASSERTRBYDBALANCE. So I think we have some pretty
decent confidence our rbyd algorithm maintains balance, even with range
removals:

  test+balance before: 20614/631541 failed
  test+balance after:      0/631541 failed (-100.0%)

It was an open question if this was even possible, so it's nice to see
some evidence balanced range removals are not a problem.

---

The current implementation is a bit hacky, so we do take a hit to code
size. Though we may be able to claw this back after cleaning things up:

           code          stack          ctx
  before: 38580           2624          640
  after:  38784 (+0.5%)   2624 (+0.0%)  640 (+0.0%)
2025-02-08 14:53:47 -06:00
Christopher Haster d3fe99ed98 rbyd: Fixed ambiguous stitching alt fix
What a funny bug.

I was wondering how jump ended up == branch, it turns out this was a
simple typo in the diverged-stitching rework.

What's extra humorous is I think the original code was trying to set
branch_ = jump, which wouldn't have worked. So the typo actually got
diverged-stitching to a more workable state than if there was no typo.

It just then broke the y-split logic.

Not randomly changing the branch when stitching is the correct fix.

---

This doesn't actually reduce any test failures since it's replacing the
previous jump >= branch "fix", which was really just a bandaid:

  test_rbyd+balance before: 1671/385878 failed
  test_rbyd+balance after:  1671/385878 failed (+0.0%)

Saves one ld/st though:

           code          stack          ctx
  before: 38584           2624          640
  after:  38580 (-0.0%)   2624 (+0.0%)  640 (+0.0%)
2025-02-08 14:53:47 -06:00
Christopher Haster 8e919df1b8 rbyd: Refined jump > branch pruning hack
Now that we have this jump > branch trick in our back pocket, we can
undo most of the y-pruning/diverged-pruning separation.

In theory all we need is to conditionally recolor based on if jump is >
branch.

---

Well, in theory at least. This seems to increase the number of test
failures, but it's unclear if this is due to jump > branch being an
incorrect assumption or some other bug exposed by the change in logic:

  test_rbyd+balance before:  300/385878 failed
  test_rbyd+balance after:  1671/385878 failed (+457.0%)

But it does save code:

           code          stack          ctx
  before: 38880           2624          640
  after:  38584 (-0.8%)   2624 (+0.0%)  640 (+0.0%)
2025-02-08 14:53:47 -06:00
Christopher Haster c89c846329 rbyd: Potential y-prune disambiguity with more jump >= branch trickery?
Idea!

The problem isn't so much that y-pruning recolors and diverged-pruning
doesn't recolor, but that we are unconditionally recoloring based on
the assumed yellow node direction.

Consider all possible yellow prune permutations:

          <y                      <r
  .-------'|              .-------'|
  |       <r  prune y     |        |
  |  .----'|    =>        |     .--'
  |  |    <b              |    <b
  |  |  .-'|              |  .-'|
  1  2  3  4           1  2  3  4  x

                                  <r
                       .----------'|
              prune r  |           |
                =>     |        .--'
                       |       <b
                       |     .-'|
                       1  2  3  4  x

                                  <b
                       .----------'|
              prune b  |           |
                =>     |  .--------'
                       |  |
                       |  |
                       1  2  3  4  x

Note these all maintain the color balance of our tree, but we only need
to recolor the yellow alt when it's the black alt we're pruning.

But how do we know if it's the black alt we're pruning? Well, we can use
the same trick we used in unflipping y-splits. The black alt is the only
alt where jump >= the current branch.

This is true because we always split yellow nodes as soon as we see
them.

---

This idea is promising, but needs a bit of work.

So, uh, please ignore the additional test failures:

  test_rbyd+balance before: 312/385878 failed
  test_rbyd+balance after:  300/385878 failed (-3.8%)

Curiously, these are now actual test failures and not just balance
failures. I can't decide if that's a good sign or not.

Code changes:

           code          stack          ctx
  before: 38860           2624          640
  after:  38880 (+0.1%)   2624 (+0.0%)  640 (+0.0%)
2025-02-08 14:53:47 -06:00
Christopher Haster 69adb1f033 rbyd: Trying to get separate r/b diverged trimming working
The conflicts between y-pruning and diverged-pruning are proving
difficult to resolve...

This is a step backwards but also a work in progress:

  test_rbyd+balance before: 156/385878 failed
  test_rbyd+balance after:  312/385878 failed (+100.0%)

Code changes:

           code          stack          ctx
  before: 38644           2624          640
  after:  38860 (+0.6%)   2624 (+0.0%)  640 (+0.0%)
2025-02-08 14:53:47 -06:00
Christopher Haster 3ba17cf2f9 rbyd: Fixed ambiguous stitching alt in y-split?
I'm not sure this is the correct solution, or why exactly we end up with
jump == branch... but the stitching alt was causing our branch-based
unflipping in the y-split logic to fail.

Changing this logic to jump >= branch seems to solve this issue, though
more investigation is needed into why...

But the good news is this small tweak solves half our current test
failures:

  test_rbyd+balance before: 300/385878 failed
  test_rbyd+balance after:  156/385878 failed (-48.0%)

Code changes:

           code          stack          ctx
  before: 38644           2624          640
  after:  38644 (+0.0%)   2624 (+0.0%)  640 (+0.0%)
2025-02-08 14:53:47 -06:00
Christopher Haster ee3a1374eb rbyd: Progress towards rbyd balance, separated y-pruning, hacky goto
The main idea here is we need to unconditionally descend down red/yellow
alts to check if they're post-split yellow nodes before pruning, since
yellow nodes should be recolored to maintain the color balance of our
tree.

To make this work:

- Added separate y-pruning logic before our diverged logic, this starts
  to look a bit like previous incarnations of this function.

- Our non-y-pruning logic is all now gated behind the if-diverging
  check, so we shouldn't be non-y-pruning at all unless we've diverged.

  It may be worth rewriting this to use the trimming logic directly,
  instead of reachability.

- Moving non-y-pruning logic behind the if-diverging check ended up
  causing problems for the stitching node, which is still a bit of a
  special case. A hacky goto solves this for now at the risk of
  velociraptors... https://xkcd.com/292

It's hacky, but the goal right now is to just get something working.

This may seem like a lot of changes for only a couple more tests
passing, but progress is progress:

  test_rbyd+balance before: 306/385878 failed
  test_rbyd+balance after:  300/385878 failed (-2.0%)

Code changes:

           code          stack          ctx
  before: 38528           2624          640
  after:  38644 (+0.3%)   2624 (+0.0%)  640 (+0.0%)
2025-02-08 14:53:47 -06:00
Christopher Haster 192779ae84 rbyd: Fixed eager red pruning ruining balance
In our pruning logic we eagerly prune root alts and red alts as soon as
we notice they're unreachable.

On paper this is fine, but it's hiding post-split yellow alts from being
recolored to maintain the color balance of the tree...

This is a work in progress...

Test changes:

  test_rbyd+balance before: 273/385878 failed
  test_rbyd+balance after:  306/385878 failed (+12.1%)

Code changes:

           code          stack          ctx
  before: 38536           2624          640
  after:  38528 (-0.0%)   2624 (+0.0%)  640 (+0.0%)
2025-02-08 14:53:47 -06:00
Christopher Haster ae27cdf56b rbyd: Preserved stitching color, a couple other balance tweaks
- Since the diverged stitching alt is no longer a special case, we
  should preserve its color in order to preserve the tree balance.

  The fact that this works (ignoring balance) is a great sign.

- It seems we need to collapse both-diverging alts outside of our
  diverging logic? I think this helps cover cases where all three alts
  in a yellow node diverge.

This got a few more tests passing:

  test_rbyd+balance before: 306/385878 failed
  test_rbyd+balance after:  273/385878 failed (-10.8%)

Code changes

           code          stack          ctx
  before: 38408           2624          640
  after:  38536 (+0.3%)   2624 (+0.0%)  640 (+0.0%)
2025-02-08 14:53:47 -06:00
Christopher Haster 1c0f5385a4 rbyd: Simplified diverged stitching, fixed unexpected balance issue
Found with LFS_ASSERTRBYDBALANCE, the way we were introducing a new alt
to stitch together diverged trunks was preventing unreachable diverged
trunks from being pruned.

This isn't a hard error, since we end up appending a null tag to
terminate the tree, but like our compaction balance issue, the
terminating null tag leads to balance issues down the road:

          .---> a                     .---> a
      .---r-b-> b                 .---r-b-> b
      |   .---> c                 |   .---> c
  .---r-b-r-b-> d             .---r-b-r-b-> d
  | .---b---b-> e         .---y-r---b---b-> e
  r-b---------> null  =>  | .-------------> f
                          | | .-----------> g
                          | | | .---------> h
                          b-y-r-b---------> i

                          '--------.--------'
                              unbalanced :(

Fortunately, after fiddling around with the algorithm a bit, it turns
out we don't really need a special case for the stitching alt as long as
we adjust the weight a bit.

This allows pruning of the stitching alt and avoids this unexpected
balance issue, while also simplifying how we stitch diverged trunks. Win
win.

Goes to show LFS_ASSERTRBYDBALANCE will probably be quite a valuable
assertion, assuming the remaining balance issues are solvable.

---

Test changes, this gets at least all non-weighted/non-range test_rbyd
tests passing with balance asserts:

  test_rbyd+balance before: 351/385878 failed
  test_rbyd+balance after:  306/385878 failed (-12.8%)

Ran with:

  $ DEBUG=1 \
        TESTS=tests/test_rbyd.toml \
        CFLAGS=-DLFS_ASSERTRBYDBALANCE \
        make test-runner -j \
        && ./scripts/test.py -j -B -k

While even saving code:

           code          stack          ctx
  before: 38440           2624          640
  after:  38408 (-0.1%)   2624 (+0.0%)  640 (+0.0%)

---

Also please excuse the mess, this is going to be a rough series of
commits...
2025-02-08 14:53:47 -06:00
Christopher Haster 94f493ec2e rbyd: Added LFS_ASSERTRBYDBALANCE for asserting rbyds are balanced
This is _not_ free, so an opt-in define is needed.

This is also expected to fail right now due to balance issues with
diverging yellow nodes in range-removals, but it's the first step
towards trying to fix said balance issues.

I've been trying to avoid adding debug machinery to lfs.c itself, since
it tends to hurt readability, but I really don't know how you could
assert rbyd balance out-of-tree short of trying to fetch every block in
emubd somehow...

Oh well, finding and asserting rbyd balance in lfsr_rbyd_fetch at least
doesn't require that much code.

No code changes, thanks to const-propagation.
2025-02-08 14:53:47 -06:00
Christopher Haster 68f0534dd0 rbyd: Dropped special altn/alta encoding
altas, and to a lesser extend altns, are just too problematic for our
rbyd-append algorithm.

Main issue is these break our "narrowing" invariant, where each alt only
ever decreases the bounds.

I wanted to use altas to simplify lfsr_rbyd_appendcompaction, but
decided it wasn't worth it. Handling them correctly would require adding
a number of special cases to lfsr_rbyd_appendrat, adding complexity to
an already incredibly complex function.

---

Fortunately, we don't really need altns/altas on-disk, but we _do_ need
a way to mark alts as unreachable internally in order to know when we
can collapse alts when recoloring (at this point bounds information is
lost).

I was originally going to use the alt's sign bit for this, but it turns
out we already have this information thanks to setting jump=0 to assert
that an alt is unreachable. So no explicit flag needed!

This ends up saving a surprising amount of code for what is only a
couple lines of changes:

           code          stack          ctx
  before: 38512           2624          640
  after:  38440 (-0.2%)   2624 (+0.0%)  640 (+0.0%)
2025-02-08 14:53:47 -06:00
Christopher Haster ab26437e46 Fixed issue where ckfetches hid cached btree nodes from ckmeta
In lfsr_mtree_traverse, we were explicitly not checking btree nodes when
ckfetches was enabled. This was an attempt to avoid double-checking btree
nodes when using both ckfetches and ckmeta.

Unfortunately this misses errors introduced _after_ fetch in btrees we
keep around in RAM. We still want to find these when doing an explicit
ckmeta scan!

---

Removing this special case _does_ mean we will be back to redundantly
checking non-cached btree nodes, but I think this is a case of better
safe than sorry.

In the future we may cache btree nodes more aggressively depending on
available RAM, and it would be a shame if that accidentally leads to a
weaker filesystem.

Worst case we can always revisit this ckfetches/ckmeta double-fetching
in the future if it become a bottleneck.

Found by our test_ck_spam_fwrite_fuzz test.

This actually saves a tiny bit of code, but at the cost of redundantly
checking non-cached btree nodes:

                     code          stack          ctx
  default before:   38512           2624          640
  default after:    38512 (+0.0%)   2624 (+0.0%)  640 (+0.0%)

  ckfetches before: 38766           2656          640
  ckfetches after:  38762 (-0.0%)   2656 (+0.0%)  640 (+0.0%)
2025-02-08 14:53:47 -06:00
Christopher Haster f90906458a rbyd: Terminate rbyd compaction layers with altgts
This fixes a minor balance issue with rbyd commits after compaction,
reducing the worst case rbyd height by ~1/2.

I've been noticing some strange balance issues in our rbyds for a while,
it turns out our algorithms for compaction and commit weren't playing
nicely with each other.

The problem is always terminating with altles, and subtleties around
incomplete trees (non-powers-of-two).

When we terminate with altles, we end up with a dangling null tag:

  rbyd log:             rbyd tree:
  data a   <.                   .---> a
  data b   <--.             .---r-b-> b
  data c   <----.           |   .---> c
  data d   <------.     .---r-b-r-b-> d
  data e   <--------.   | .---b---b-> e
  altrle a <. | | | |   r-b---------> null
  altble b -|-' | | |
  null      |   | | |
  altrle c <--.-' | |
  altble d -|-|---' |
  null      | |     |
  altble e <----.---'
  null      | | |
  altrle b <. | |
  altble d -|-' |
  null      |   |
  altble e <--.-'
  null      | |
  altrle d -' |
  altble e ---'
  null

Which is fine, we're allowed to terminate rbyds with a null tag.

The problem is that from the rbyd's view of the world, that null tag has
the same balance as the rest of the entire tree. If we continue to
append tags (which is a very common thing to do), we end up with a
lopsided tree:

  rbyd log:             rbyd tree:
  data a   <.                   .---> a
  data b   <--.             .---r-b-> b
  data c   <----.           |   .---> c
  data d   <------.     .---r-b-r-b-> d
  data e   <--------.   | .---b---b-> e
  altrle a <. | | | |   r-b---------> null
  altble b -|-' | | |          v
  null      |   | | |           .---> a
  altrle c <--.-' | |       .---r-b-> b
  altble d -|-|---' |       |   .---> c
  null      | |     |   .---r-b-r-b-> d
  altble e <----.---'   | .---b---b-> e
  null      | | |       r-b---------> f
  altrle b <. | |              v
  altble d -|-' |                 .---> a
  null      |   |             .---r-b-> b
  altble e <--.-'             |   .---> c
  null      | |         .-----r-b-r-b-> d
  altrle d  | |         | .-----b---b-> e
  altble e  | |         | | .---------> f
  null      | |         y-r-b---------> g
  altrle d  | |                v
  altble e  | |                   .---> a
  data f   <--.               .---r-b-> b
  altrle d <. |               |   .---> c
  altrle e -|-|           .---r-b-r-b-> d
  altble f  | |         .-y-r---b---b-> e
  data g   <----.       | .-----------> f
  altble e  | | |       | | .---------> g
  altrle f  | | |       b-r-b---------> h
  altble g  | | |              v
  data h   <------.                 .---> a
  altble e -' | | |             .---r-b-> b
  altrle f ---' | |             |   .---> c
  altrle g -----' |         .---r-b-r-b-> d
  altble h -------'     .---y-r---b---b-> e
  data i                | .-------------> f
                        | | .-----------> g
                        | | | .---------> h
                        b-y-r-b---------> i

                        '---- h=4 --------'

Fortunately the solution is relatively simple. If we terminate each
layer of the compaction with an altgt, instead of an altle, the null
tag becomes unreachable:

  rbyd log:             rbyd tree:
  data a   <.                   .---> a
  data b   <--.             .---r-b-> b
  data c   <----.           |   .---> c
  data d   <------.     .---r-b-r-b-> d
  data e   <--------.   r-b---b---b-> e
  altrle a <. | | | |
  altble b -|-' | | |
  null      |   | | |
  altrle c <--.-' | |
  altble d -|-|---' |
  null      | |     |
  altbgt d <----.---'
  null      | | |
  altrle b <. | |
  altble d -|-' |
  null      |   |
  altbgt d <--.-'
  null      | |
  altrle d -' |
  altbgt d ---'
  null

This preserves the balance of the tree:

  rbyd log:             rbyd tree:
  data a   <.                   .---> a
  data b   <--.             .---r-b-> b
  data c   <----.           |   .---> c
  data d   <------.     .---r-b-r-b-> d
  data e   <. | | |     r-b---b---b-> e
  altrle a -| | | |            v
  altble b -|-' | |           .---> a
  null      |   | |       .---r-b-> b
  altrle c <--.-' |       |   .---> c
  altble d -|-|---'     .-r-b-r-b-> d
  null      | |         |       .-> e
  altbgt d  | |         b---b---b-> f
  null      | |                v
  altrle b <. |               .---> a
  altble d -|-'           .---r-b-> b
  null      |             |   .---> c
  altbgt d  |           .-r-b-r-b-> d
  null      |           |     .---> e
  altrle d  |           |     | .-> f
  altbgt d  |           b---b-r-b-> g
  null      |                  v
  altble d  |                   .---> a
  altbn  d  |             .-----r-b-> b
  altble e  |             |     .---> c
  data f   <|-.         .-r-b---r-b-> d
  altble d  | |         |     .-----> e
  altbn     | |         |     | .---> f
  altrle e  | |         |     | | .-> g
  altble f  | |         b---b-y-r-b-> h
  data g   <----.              v
  altble d  | | |             .---> a
  altbn     | | |         .---r-b-> b
  altrle e <--. |         |   .---> c
  altrle f -|-| |       .-r-b-r-b-> d
  altble g  | | |       |     .---> e
  data h   <------.     |   .-y-r-> f
  altble d -' | | |     |   | .---> g
  altble f ---' | |     |   | | .-> h
  altrle g -----' |     b---b-r-b-> i
  altble h -------'
  data i                '---- h=3 --'

As a plus, this also makes it a bit easier to see compaction layers
without decoding jumps, which is nice for debugging.

---

It's worth noting the subtlety around when use use altgts vs altles
here. We need to use altles in all but the last tag, in order to know
what the largest tag was in each subtree when building the next layer.
Fortunately, the last tag is the only tag where we need an altgt in
order to make the null tag unreachable.

This adds a bit of code, but preserving rbyd balance after compaction is
well worth it:

           code          stack          ctx
  before: 38500           2624          640
  after:  38512 (+0.0%)   2624 (+0.0%)  640 (+0.0%)
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 a80694b9a7 Added lfs_crc32c_cube, replacing lfsr_gcksum_cube
This is a bit on the edge of what is a reasonable to shove in
lfs_util.h. But it's useful enough that we were giving it its own
function anyways, and moving it to lfs_util.h allows it to be
potentially overriden with cube-specific optimizations (expanded
terms?).

No code changes.
2025-02-08 14:53:47 -06:00
Christopher Haster bc959e4d0e Added lfs_memlen, replacing lfsr_gdelta_size
This drops the last lfsr_gdelta_* function, which were really just a
bunch of somewhat-quirky mem operations.

Moving this to lfs_util.h also allows users to override it with
hardware-specific tricks, though I think hardware tricks for lfs_memlen
will be quite rare. Reverse-order memory optimizations are pretty
uncommon...

This could also be done with a theoretical memrcchr, if one existed:

  p = memrcchr(buffer, 0, size);
  return (p) ? p - buffer : 0;

But I figured this use case is so niche we might as well just limit it
to c=0 (and avoid questions about memrchr).

No code changes.
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 c86f3bda00 Calculate grm deltas at the last minute
This finally gets rid of the annoying grm re-xoring we needed to do in
lfsr_mdir_commit.

If we're waiting until the last minute to append gdeltas, we might as
well wait to the last minute to encode/xor gdeltas. This avoids any
redundant xors because of failed mdir commits.

Note though, we still need our *_d copies to collect gdelta from any
dropped mdirs.

Also dropped lfsr_gdelta_xor and lfsr_is_zero. These were just small
wrappers over memxor and memcchr that add more noise than anything else.

This saves a nice bit of code/stack:

           code          stack          ctx
  before: 38588           2648          644
  after:  38560 (-0.1%)   2624 (-0.9%)  644 (+0.0%)
2025-02-08 14:53:47 -06:00
Christopher Haster 2ac9c0695b Delayed appending any gdelta until the last mdir commit
It's not the most intuitive, but we always know if an mdir commit will
be the last commit/atomic before attempting the commit. This means we
can wait to append/flush gdelta until the last commit, which simplifies
handling of gdelta state in lfsr_mdir_commit a bit.

We were already doing this for the gcksum, this just changes the grm to
match.

This also results in gdeltas being pushed upwards when relocating, which
is probably a good thing? On one hand this encourages deduplication of
gdeltas, on the other hand it does result in gdeltas getting pushed into
mroots that don't need them (grm in mrootanchor?).

This ends up saving a bit of code, which is nice:

           code          stack          ctx
  before: 38612           2648          644
  after:  38588 (-0.1%)   2648 (+0.0%)  644 (+0.0%)
2025-02-08 14:53:47 -06:00
Christopher Haster fde3c537ea Added lfsr_mdir_sync to help sync mdir state
Syncing mdirs got a bit awkward with the addition of the gcksumdelta, we
can't just copy the rbyd component. So added lfsr_mdir_sync to abstract
out the copying of any relevent mdir state.

Note this does _not_ include the mid, which may be different for
different files in the same mdir.

The code deduplication ended up saving a bit of code, which is nice:

           code          stack          ctx
  before: 38668           2648          644
  after:  38612 (-0.1%)   2648 (+0.0%)  644 (+0.0%)
2025-02-08 14:53:47 -06:00
Christopher Haster d93b24e998 Moved gcksumdelta append into lfsr_mdir_commit__
This tweaks lfsr_rbyd_appendcksum_ to take the canonical cksum as an
argument, allowing the caller to append any non-tree tags they'd like
before finalizing the commit.

This lets us calculate our gcksumdelta directly in lfsr_mdir_commit__,
which simplifies things a bit and avoids a redundant cksum copy.

The code cost mostly cancels out due to the extra gcksumdelta append
needed in lfsr_formatinited, but it's worth it for the code
simplification:

           code          stack          ctx
  before: 38664           2648          644
  after:  38668 (+0.0%)   2648 (+0.0%)  644 (+0.0%)
2025-02-08 14:53:47 -06:00
Christopher Haster 3983b5d1a1 Upgraded some LFS_DEBUG statements
These were mostly just overlooked. We should use LFS_WARN on unexpected
filesystem state and LFS_ERROR if we can no longer proceed:

- ckprog mismatch: LFS_DEBUG -> LFS_WARN
- stuck mdir: LFS_DEBUG -> LFS_ERROR
- mdir cksum mismatch: LFS_DEBUG -> LFS_ERROR
2025-02-08 14:53:47 -06:00
Christopher Haster 8414f18d1b t: Moved open mdir/mroot cksum checks into lfsr_mtree_traverse
This changes lfsr_mtree_traverse + CKMETA to check on-disk mdir
checksums against all open mdirs and the mroot (which is always "open").

This is basically free (a bit more CPU work, but no disk accesses), and
makes lfsr_mtree_traverse a bit more robust against rollback issues. It
also lets us drop the obshrub-specific mdir cksum check.

This adds a bit of code, but it's probably worth it to catch rollback
issues earlier:

           code          stack          ctx
  before: 38572           2640          644
  after:  38664 (+0.2%)   2648 (+0.3%)  644 (+0.0%)
2025-02-08 14:53:47 -06:00
Christopher Haster 624eb1ad52 Fixed gcksum mismatch in dirty/mutated traversal
It's an error to check the gcksum if our traversal ends up
dirty/mutated, since it may be contain cksums from different filesystem
states.

We already avoid clearing the CKMETA flag if this happens, so no change
needed there. lfsr_fs_gc will continue to traverse until this flag is
cleared. (Though running a ckmeta scan to completion before any mutation
_would_ be more robust, hmm...)

Found by our test_traversal_mutation test.

Code changes were zero after tweaking lfsr_mtree_traverse to better
deduplicate ckmeta/ckdata eot conditions:

           code          stack          ctx
  before: 38572           2640          644
  after:  38572 (+0.0%)   2640 (+0.0%)  644 (+0.0%)
2025-02-08 14:53:47 -06:00
Christopher Haster 47438a8c46 Fixed test_ck_spam*'s open file bshrub/btree issues
- In lfsr_mtree_traverse, we traverse open file bshrubs/btrees before
  we validate the gcksum, which means bugs/asserts can slip through
  before we have a chance to detect something is wrong.

  To work around this, I've added an explicit mdir cksum check right
  before we start traversing an open mdir's bshrubs/btrees. If an open
  mdir doesn't match the on-disk state, the on-disk state must contain
  an error (or the RAM, but that's a different story and wayyy out of
  scope).

  It might be better to rearrange lfsr_mtree_traverse to check gcksums
  first, but this will require another look at our traversal clobbering
  logic.

- For a similar reason, ckfetches can't detect open bshrub/btree
  corruption as is. As its name suggests, ckfetches only checks fetches,
  so any corruption after we've fetched bshrubs/btrees in lfsr_file_open
  will go undetected.

  Fortunately this just means we need a full ckmeta-scan in
  test_ck_spam* tests that keep open files.

  In real use, full ckmeta-scans should be preferred anyways. Limiting
  these scans to mtreeonly was just an attempt to better stress btree
  ckfetches.

  At least we're still testing ckmeta+mtreeonly+ckfetches in
  test_ck_spam_dir_fuzz and test_ck_spam_file_fuzz.

This gets the test_ck_spam* tests running under all of the current
interesting ck-modes.

Code changes:

           code          stack          ctx
  before: 38560           2640          644
  after   38572 (+0.0%)   2640 (+0.0%)  644 (+0.0%)
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 0eee57017d Minor DEBUG/WARN/ERROR cleanup
Indention has changed quite a bit for most of these, so I figured it's a
good time to go back through and clean things up.

Also tried to adopt indention rules a bit better, adding indention after
string continuation. This was adopted in the scripts but not in the
actual code.
2025-01-28 14:41:45 -06:00
Christopher Haster 7cd2d4dd11 Added LFSR_WCOMPAT_GCKSUM wcompat flag
The gcksum isn't actually implemented yet, I mostly just wanted to
measure this code cost separately:

           code          stack          ctx
  before: 37768           2608          620
  after:  37796 (+0.1%)   2608 (+0.0%)  620 (+0.0%)

I may be procrastinating a little bit...
2025-01-28 14:41:45 -06:00
Christopher Haster b6ab323eb1 Dropped the q-bit (previous-perturb) from cksum tags
Now that we perturb commit cksums with the odd-parity zero, the q-bit no
longer serves a purpose other than extra debug info. But this is a
double-edged sword, because redundant info just means another thing that
can go wrong.

For example, should we assert? If the q-bit doesn't reflect the
previous-perturb state it's a bug, but the only thing that would break
would be the q-bit itself. And if we don't assert what's the point of
keeping the q-bit around?

Dropping the q-bit avoids answering this question and saves a bit of
code:

           code          stack          ctx
  before: 37772           2608          620
  after:  37768 (-0.0%)   2608 (+0.0%)  620 (+0.0%)
2025-01-28 14:41:45 -06:00
Christopher Haster d08d254cd2 Switched to writing compat flags as le32s
Most of littlefs's metadata is encoded in leb128s now, with the
exception of tags (be16, sort of), revision counts (le32), cksums
(le32), and flags.

It makes sense for tags to be a special case, these are written and
rewritten _everywhere_, but less so for flags, which are only written to
the mroot and updated infrequently.

We might as well save a bit of code by reusing our le32 machinery.

---

This changes lfsr_format to just write out compat flags as le32s, saving
a tiny bit of code at the cost of a tiny bit of disk usage (the real
benefit being a tiny bit of code simplification):

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

Compat already need to handle trailing zeros gracefully, so this doesn't
change anything at mount time.

Also had to switch from enums to #defines thanks to C's broken enums.
Wooh. We already use #defines for the other flags for this reason.
2025-01-28 14:41:45 -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 0cab73730e Added LFS_WCOMPAT_RDONLY and LFS_RCOMPAT_WRONLY
LFS_WCOMPAT_RDONLY seems generally useful for tools that just want to
mark a filesystem is read-only. This is a common flag that exists in
other filesystems (RO_COMPAT_READONLY in ext4 for example).

LFS_RCOMPAT_WRONLY, on the other hand, is a bit more of a joke, but
there could be some niche use cases for it (preventing double mounts?).

Fortunately, these flags require no extra code, and fall out naturally
from our wcompat/rcompat handling.

---

Originally, the idea was to also add LFS_F_RDONLY, to match LFS_M_RDONLY
and set the LFS_WCOMPAT_RDONLY flag during format.

But this doesn't really work with the current API, since lfsr_format
would just give you an empty filesystem you can't write to. Which is a
bit silly.

Maybe we should add something like lfsr_fs_mkrdonly in the future? This
is probably low-priority.
2025-01-28 14:41:45 -06:00
Christopher Haster af6ea39cca Reworked rcompat flags
Mainly to add LFS_RCOMPAT_MSPROUT. It makes sense that a littlefs driver
may not want to support mroot-inlined mdirs, and this flag would be the
only way to indicate that. (Currently inlined mdir -> mtree is one way,
but this may not always be the case.)

This also makes space for a couple planned features:

  LFS_RCOMPAT_NONSTANDARD  0x00000001  Non-standard filesystem format
  LFS_RCOMPAT_WRONLY*      0x00000002  Reading is disallowed
  LFS_RCOMPAT_GRM          0x00000004  May use a global-remove
  LFS_RCOMPAT_MSPROUT      0x00000010  May use an inlined mdir
  LFS_RCOMPAT_MLEAF        0x00000020  May use a single mdir pointer
  LFS_RCOMPAT_MSHRUB       0x00000040  May use an inlined mtree
  LFS_RCOMPAT_MTREE        0x00000080  May use an mdir btree
  LFS_RCOMPAT_BSPROUT      0x00000100  Files may use inlined data
  LFS_RCOMPAT_BLEAF        0x00000200  Files may use single block pointers
  LFS_RCOMPAT_BSHRUB       0x00000400  Files may use inlined btrees
  LFS_RCOMPAT_BTREE        0x00000800  Files may use btrees

  *Planned

I've gone ahead and included rcompat flags we reserve but don't
currently use (LFS_RCOMPAT_MSHRUB). It seems like a good idea to make
these reservations explicit. Though we should still prohibit their use
until there is a good reason, in case we want to repurpose these flags
in the future.

Code changes minimal (larger literal? compiler noise?):

           code          stack          ctx
  before: 37788           2608          620
  after:  37792 (+0.0%)   2608 (+0.0%)  620 (+0.0%)
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