Commit Graph

1776 Commits

Author SHA1 Message Date
Christopher Haster 3b1b571d5c Added btree/branch fetch functions
These weren't really necessary when btree/branch fetch was nothing more
than tag decoding, but now that we have ckfetches it makes sense to
deduplicate things for a bit of code savings.

One reason I was punting on this was I wasn't really sure if btree/
branch fetch should take decoded fields or the raw lfsr_data_t. We don't
get any code savings with the former, but it's the only API that's
consistent with lfsr_mdir_fetch/lfsr_rbyd_fetch/etc. I was going to go
with lfsr_btree_fetch/fetch_, but fortunately shoving the latter into
the lfsr_data_* namespace solved this dilemma:

- lfsr_branch_fetch     - fetches from decoded fields
- lfsr_data_fetchbranch - fetches from raw data + weight
- lfsr_btree_fetch      - fetches from decoded fields
- lfsr_data_fetchbtree  - fetches from raw data

Unfortunately, lfsr_btree_parent creates a bit of a wrinkle. We don't
want to redundantly fetch the child we're looking for, so we need to
decode and fetch in separate steps. This prevents inlining between these
small functions that could otherwise take place.

And, while they do save a bit of code, the position of these fetch
functions in the stack hot-path end up increasing our total stack
usage when ckfetches are enabled:

                     code          stack
  default before:   36428           2680
  default after:    36456 (+0.1%)   2680 (+0.0%)

  ckfetches before: 36848           2680
  ckfetches after:  36686 (-0.4%)   2712 (+1.2%)

But maybe this is just indicative of us not accounting for
shrinkwrapping?
2024-08-20 00:28:55 -05:00
Christopher Haster a53151df1f Renamed high-level spam tests to include *_spam_*
These are our current set of general-purpose high-level tests that can
be turned to when needing to test a wide range of filesystem operations.

They were getting a bit hard to keep track of without a consistent
prefix, especially since no individual test suite can actually use all
of them at the same time.

Now, finding these tests is as simple as: ./scripts/test.py -L *_spam_*

I also renamed a couple because their names were starting to get
ridiculous. I mean just look at
test_badblocks_alternating_spam_orphanzombiedir_fuzz...

- *_spam_orphanzombie_fuzz    -> *_spam_oz_fuzz
- *_spam_orphanzombiedir_fuzz -> *_spam_ozd_fuzz
- *_spam_file_pl_fuzz         -> *_spam_f_pl_fuzz
- *_spam_filedir_pl_fuzz      -> *_spam_fd_pl_fuzz

Here are all of the current spam tests and contexts we use them in:

                traversal               badblocks   relocations
                |     gc    ck    grow  |     powerloss   exhaustion
  dir_many      y     y           y     y     y     y
  dir_fuzz      y     y     y     y     y           y     y
  file_many     y     y           y     y     y     y
  file_fuzz     y     y     y     y     y           y     y
  fwrite_fuzz   y     y     y           y                 y
  oz_fuzz       y     y     y     y     y           y     y
  ozd_fuzz      y     y     y     y     y           y     y
  f_pl_fuzz                       y           y     y
  fd_pl_fuzz                      y           y     y
2024-08-20 00:28:55 -05:00
Christopher Haster 4fa2864f30 Replaced test_ck_every_* with more interesting error-spam tests
Instead of testing every block (which test_badblocks_every already
does) with a single random bit-error, the new test_ck_spam tests
continuously throw bit-errors at the filesystem until it fails.

This should reveal much more interesting failures than flipping a single
bit in the entire device, while also taking less testing time. And we
still have test_badblocks_every to make sure no specific problem blocks
(except the mrootanchor) are missed.

This makes test_ck_spam more similar to test_exhaustion than
test_badblocks_every.

All this being said, these tests are still sort of in stasis until
rollback protection gets sorted out. So we're not actually testing
anything interesting yet...

I've also reverted the test_badblocks -> test_ck dependency, since we
want to keep the longer-running tests near the end of the queue.
2024-08-20 00:28:55 -05:00
Christopher Haster 73015909a1 Added high-level every-block error tests to test_ck
These are basically the same as our test_badblock tests, except we
accept LFS_ERR_CORRUPT. This lets us test more checking modes that may
not enable recovery (ckreads, ckfetches, etc).

Well, in theory, at least. The lack of rollback protection gets in the
way of both ckreads and ckfetches, so we're currently only testing
ckprogs, which isn't much of an improvement. At least this gets the
scaffolding in place...

This also inverts the test_ck -> test_badblocks dependency. Now that
these both have exhaustive tests, we might as well limit test_badblocks
to simple erroring erases/progs and let test_ck check the ck checks.
2024-08-20 00:28:55 -05:00
Christopher Haster e7a150ea36 Rearranged jellyfish in tests to better match ck-modes 2024-08-20 00:28:52 -05:00
Christopher Haster 5502fe55ab Implemented ckfetches
Ckfetches implements what might be your first idea on how to check
checksums in a filesystem: Check each block/mdir on first access
(fetch) to make sure the data is sound.

Unfortunately, there are two problems with this approach, both which
come from the fact that blocks are big and can't fit in RAM:

1. We still have a checksum-read hole.

   We can't keep a whole block around in RAM, so reads after a fetch may
   need to reread from disk, at which point new bit-errors may slip in
   undetected.

   This is especially problematic for traversing our rbyds, which
   involves a lot of small reads in a block.

2. Ckfetches may have a surprisingly negative performance impact.

   Consider the case of reading a large file with a bunch of small
   reads. Because we don't cache blocks, each read may need a btree
   lookup, and a full block fetch. On paper this can quickly end up
   O(b^2), which is not great.

   Though this is helped by the file buffer. It will be interesting to
   benchmark and see if this theoretical O(b^2) translates to poor
   performance in practice.

   Note ckreads has this same performance issue.

Still, despite these problems, ckfetches may be useful for cases where
you just want an extra layer of safety, or don't care about the tiny
chance an error is introduced between a fetch an subsequent read.

---

Like ckprogs/ckreads, ckfetches is an opt-in feature, and requires both
1. defining LFS_CKFETCHES, and 2. passing LFS_M_CKFETCHES during mount.

This is a bit of a quick implementation to get testing in place, so the
code cost is probably higher than strictly necessary. If we can refactor
the code internally to avoid all the duplicate lfsr_rbyd_fetchck/
lfsr_bptr_ck calls, we can probably bring this down a bit:

                  code          stack
  before:        36428           2680
  yes-ckfetches: 36848 (+1.2%)   2680 (+0.0%)
  no-ckfetches:  36428 (+0.0%)   2680 (+0.0%)

Oh, and also added lfs_emubd_flipbit to allow tests to manually flip
bits themselves. LFS_EMUBD_BADBLOCK_PROGFLIP is quick to find the above
mentioned checksum-read hole.

This could be done manually with read+erase+prog, but no reason to make
it harder than it needs to be.
2024-08-16 01:04:26 -05:00
Christopher Haster 10feccf18c Moved ckprogs behind LFS_CKPROGS ifdef
So just like ckreads, ckprogs is now opt-in, requiring both 1. defining
LFS_CKPROGS at compile-time, and 2. passing the LFS_M_CKPROGS flag
during lfsr_mount.

_Unlike_ ckreads, ckprogs is actually a very lightweight feature. So the
difference between compiling with/without ckprogs is really quite small:

                code          stack
  before:      36480           2680
  yes-ckprogs: 36480 (+0.0%)   2680 (+0.0%)
  no-ckprogs:  36428 (-0.1%)   2680 (+0.0%)

It's almost not worth putting behind an ifdef if not for consistency
with ckreads.
2024-08-16 01:04:24 -05:00
Christopher Haster e536300606 Rearranged flags a bit
Mainly to make space for more shared open/mount flags that are future
planned.

The nice thing about our flags is they don't live on-disk, so we can
always change them whenever we need to.

It gets a bit messy, but this is what the current layout looks like:

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

The main downside of this layout is that some of the traversal flags,
LFS_T_DIRTY/MUTATED, now risk ambiguity with open/mount flags. But I
don't think this is really avoidable as traversals are already using
almost the entire 32-bit encoding space...

No code changes:

           code          stack
  before: 36480           2680
  after:  36480 (+0.0%)   2680 (+0.0%)
2024-08-16 01:04:21 -05:00
Christopher Haster 80ef963bec Renamed LFS_I_ORPHANS -> LFS_I_HASORPHANS
This better matches how other flags sometimes include the relevant verb,
LFS_RBYD_ISSHRUB, LFSR_DATA_ONDISK, etc, and feels a bit more
consistent.
2024-08-16 01:04:19 -05:00
Christopher Haster 6d0b05da6c Extended lfsr_format with some gc flags
These fall out quite naturally when you consider that we call
lfsr_mountinited internally to check that our format was successful.

That being said... they don't really do anything right now since we only
write a single mdir:

- LFS_F_COMPACT - The only gc operation that _might_ actually do
  something is LFS_F_COMPACT, but only if our fs config exceeds >1/2 the
  block size. But I'm not sure littlefs will even be able to write file
  metadata if this happens...

- LFS_F_CKMETA - We already check the only mdir by calling
  lfsr_mountinited, which implicitly fetches the mrootanchor.

- LFS_F_CKDATA - We uh, don't have any data immediately after
  lfsr_format. But I guess it doesn't hurt to keep this around for
  consistency, it at least implies CKMETA.

Hopefully these flags will be more interesting if/when we start adding
auxiliary trees to the filesystem, otherwise they may be worth reverting
in the future...

Until then, they at least provide some consistency, and I guess a way to
triply check that format was successful.

---

This could probably be better deduplicated, but calling lfsr_fs_gc from
both lfsr_mount and lfsr_format provides a bit better code organization:

           code          stack
  before: 36448           2680
  after:  36480 (+0.1%)   2680 (+0.0%)
2024-08-16 01:04:16 -05:00
Christopher Haster acad3a3143 Added format flags to lfsr_format
This is mainly to solve the weird check-hole where passing CKPROGS/
CKREADS as mount flags has no effect on lfsr_format (I mean, it'd be a
bit silly if it did somehow):

  LFS_F_RDWR              0  // Format the filesystem as read and write
  LFS_F_CKPROGS  0x00000010  // Check progs by reading back progged data
  LFS_F_CKREADS  0x00000020  // Check reads via parity bits/checksums

This makes lfsr_format a more cumbersome interface, but I don't know if
this is necessarily a bad thing. There's always risk of data loss when
calling lfsr_format, so maybe it should be a pain to call.

At the very least, format flags may be useful in the future for
enabling/disabling format-time things such as the planned block-map,
parity-tree, etc. Though it's unclear if such significant settings
should be format flags or somehow encoded as fields in our config
struct.

---

The LFS_F_* format flags of course ended up conflicting with our
internal LFS_F_* flags, so I renamed most of the internal flags to match
the closest flag set they participate in:

- LFS_F_TYPE        -> LFS_O_TYPE
- LFS_F_UNFLUSH     -> LFS_O_UNFLUSH
- LFS_F_UNSYNC      -> LFS_O_UNSYNC
- LFS_F_ORPHAN      -> LFS_O_ORPHAN
- LFS_F_ZOMBIE      -> LFS_O_ZOMBIE

- LFS_F_ORPHANS     -> LFS_I_ORPHANS
- LFS_F_UNCOMPACTED -> LFS_I_UNCOMPACTED

- LFS_F_TSTATE      -> LFS_T_TSTATE
- LFS_F_BTYPE       -> LFS_T_BTYPE
- LFS_F_DIRTY       -> LFS_T_DIRTY
- LFS_F_MUTATED     -> LFS_T_MUTATED

This may make it a bit less clear which flags are a part of the public
API, vs intended only for internal use, but at the very least our asserts
in format/mount/open/etc should catch most of these mistakes.

---

Code cost ended up being pretty minimal. Actually negative. This is the
second time we're _adding_ a feature that somehow saves code, though the
reality for this one is we're really just pushing constants up into the
user's stack frame. Still, it's a good indication the cost of format
flags is small:

           code          stack
  before: 36452           2680
  after:  36448 (-0.0%)   2680 (+0.0%)
2024-08-16 01:04:13 -05:00
Christopher Haster dffd8fa0fa Fixed writing of unaligned fragments to new files
This was only noticed when forcing btrees for other unrelated tests
(INLINED_SIZE=0, CRYSTAL_THRESH=-1), where even simple file writes would
end up with some unaligned fragments the size of our file buffer.

It was hard to notice without forcing btrees, since our crystallization
algorithm has a tendency to fix alignment issues.

The problem was that we weren't bypassing the file buffer correctly when
buffer.size == 0. We relied on the LFS_F_UNFLUSH flag to know if we
could do a bypassing write, but inlined files set the LFS_F_UNFLUSH flag
even for empty files. This led to blocked bypassing writes, attempts
to merge with empty buffers, and unaligned fragments.

To avoid this, lfsr_file_write now checks for buffer.size == 0
explicitly. There may be a better solution, but for now this gets the
job done.

---

To make sure we don't end up with unaligned fragments again in the
future, I've extend the fwrite litmus tests to check for well-aligned
fragments in addition to blocks:

- test_fwrite_simple_litmus_fragments
- test_fwrite_incr_litmus_fragments

These fixes end up adding a bit of code, as checking for both the
unflushed flag and buffer.size == 0 has a cost:

           code          stack
  before: 36424           2680
  after:  36452 (+0.1%)   2680 (+0.0%)

But hey, file aren't stuck with unaligned fragments anymore.
2024-08-16 01:04:11 -05:00
Christopher Haster 9d4b4d2557 (Re?)adopted read hints in lfsr_bd_cmp/cmpck
I'm not entirely sure what I was thinking when I thought we couldn't use
read hints in lfsr_bd_cmp. It's true read hints will just be clobbered
when ckprogs are enabled, but if ckprogs aren't enabled, and, perhaps
more rarely, rcache_size > pcache_size, we should still be able to
benefit from read hints in lfsr_bd_cmp.

This has a bigger effect on ckreads, where we likely need to read
trailing data to validate checksums/parity bits and can benefit from
earlier reads keeping more data in the rcache.

Curiously this actually saves a bit a code, not sure why that is:

           code          stack
  before: 36428           2680
  after:  36424 (-0.0%)   2680 (+0.0%)
2024-08-16 01:04:08 -05:00
Christopher Haster a59d7c9954 emubd: Dropped lfs_emubd_cksum/bdcksum
One of these was missed during the crc -> cksum rename, so it wouldn't
have even linked correctly. Rather than fixing it I'm just going to drop
these functions.

At some point they were useful for debugging, but with emubd's disk
mirroring and dbgblock.py, it's both easier and more reliable to
find checksums with external scripts.
2024-08-16 01:04:05 -05:00
Christopher Haster 6e2af5bf80 Carved out ckreads, disabled at compile-time by default
This moves all ckread-related logic behind the new opt-in compile-time
LFS_CKREADS flag. So in order to use ckreads you need to 1. define
LFS_CKREADS at compile time, and 2. pass LFS_M_CKREADS during
lfsr_mount.

This was always the plan since, even if ckreads worked perfectly, it
adds a significant amount of baggage (stack mostly) to track the
ck context of all reads.

---

This is the first non-trivial opt-in define in littlefs, so more test
framework features!

test.py and build.py now support the optional ifdef attribute, which
makes it easy to indicate a test suite/case should not be compiled when
a feature is missing.

Also interesting to note is the addition of LFS_IFDEF_CKREADS, which
solves several issues (and general ugliness) related to #ifdefs in
expression. For example:

  // does not compile :( (can't embed ifdefs in macros)
  LFS_ASSERT(flags == (
          LFS_M_CKPROGS
              #ifdef LFS_CKREADS
              | LFS_M_CKREADS
              #endif
              ))

  // does compile :)
  LFS_ASSERT(flags == (
          LFS_M_CKPROGS
              | LFS_IFDEF_CKREADS(LFS_M_CKREADS, 0)));

---

This brings us way back down to our pre-ckread levels of code/stack:

                   code          stack
  before-ckreads: 36352           2672
  ckreads:        38060 (+4.7%)   3056 (+14.4%)
  after-ckreads:  36428 (+0.2%)   2680 (+0.3%)

Unfortunately, we do end up with a bit more code cost than where we
started. Mainly due to code moving around to support the ckread
infrastructure:

                   code          stack
  lfsr_bd_readtag:  +52 (+23.2%)    +8 (+10.0%)
  lfsr_rbyd_fetch:  +36 (+5.0%)     +8 (+6.2%, cold)
  lfs_toleb128:     -12 (-25.0%)    -4 (-20.0%, cold)
  total:            +76 (+0.2%)     +8 (+0.3%)

But oh well. Note that some of these changes are good even without
ckreads, such as only parsing the last ecksum tag.
2024-08-16 01:04:03 -05:00
Christopher Haster 185f209dbf Moved ckreads behind the LFS_M_CKREADS flag
Added some code, though we don't _really_ care:

           code          stack
  before: 37872           3048
  after:  38060 (+0.5%)   3056 (+0.3%)

Also interesting to note the difference in testing time, this highlights
_some_ of the performance cost of ckreads:

  with ckreads:    1135.92s
  without ckreads:  821.24s
2024-08-16 01:04:00 -05:00
Christopher Haster 458fe16f38 Extended emubd to test metastability, added ckprog/ckread tests
Metastability is a rather nasty error condition where successive reads
to a memory location may return different values, either due to bus
issues or a failed prog. It's a tricky error condition to detect, and
one that ckreads was, in theory, supposed to help with.

To help test metastability (and other single-bit errors), emubd gained
several new features:

- LFS_EMUBD_BADBLOCK_PROGFLIP    - Prog flips a bit
- LFS_EMUBD_BADBLOCK_READFLIP    - Read flips a bit sometimes
- LFS_EMUBD_POWERLOSS_METASTABLE - Reads may flip a bit

These only affect a single bit in a given block, but by randomizing
which bit during every erase (and exhaustive bit testing in test_ck) we
should still see some fairly interesting bit-error patterns over time.

It's a bit difficult to test with more than a single bit error because
you can quickly find checksum/parity collisions when fuzz testing. But
there may be other interesting error patterns to look at in the future?

Also the erase_cycles implementation got a bit of a rework since it was
lopsided previously (progs/reads would always error before erases). And
since I was messing with emubd's internals I added lfs_emubd_markbad/
markgood and a few other convenience functions that seem useful:

- lfs_emubd_seed - Manually set the prng, needed in test_ck actually
- lfs_emubd_markbad - Mark block as bad, same as wear=-1
- lfs_emubd_markgood - Mark block as good, same as wear=0
- lfs_emubd_badbit - Get which big failed
- lfs_emubd_setbadbit - Set which bit will fail
- lfs_emubd_randomizebadbit - Randomize bad bit on erase
- lfs_emubd_markbadbit - Mark bit as bad, same as setbadbit+markbad

---

The intention of this new metastability emulation was to extend test_ck
to test ckreads/ckprogs. This went... interestingly.

The good news, the new emulation and tests worked quite well. They were
able to quite quickly show that ckreads is fundamentally not able to
detect all single-bit errors in our current design.

The problem boils down to the fact that the location of our parity bits
depends on the tag's leb128-encoded size. If a bit flip changes this
size field, we end up with a new parity bit, which 50/50 may or may not
detect the error.

For example, one bit flip:

  40 0c 00 12 80 0d ff ff
  '----.----' ^--------------------.
       '- altble 0xc w0 -18 parity=1

  40 0c 80 12 80 0d ff ff
  '-------.-------' ^----------------------.
          '- altble 0xc w2304 -1664 parity=1

This doesn't make ckreads _completely_ useless, just mostly useless. We
can still use it to check parity bits, but without a systematic proof.

But there's enough problems with ckreads: performance, RAM, code, etc,
that I think it may just be an interesting proof-of-concept and not
something users should actually use. Checking reads in the bd-layer
solves all of these problems...

---

At the very least ckprogs gets better testing, thanks to new tests in
test_ck and the addition of LFS_EMUBD_BADBLOCK_PROGFLIP in
test_badblocks.

The extra testing also found a ckprog/ckread hole in that we don't
ckprog/ckread during lfsr_format! I fixed this by making lfsr_format
always use ckprogs/ckreads if available, but maybe lfsr_format should
take its own set of flags?

Funnily enough this had no impact on code size since it probably just
changed the constant in a constant pool:

          code           stack
  before: 37872           3048
  after:  37872 (+0.0%)   3048 (+0.0%)
2024-08-16 01:03:57 -05:00
Christopher Haster 89565ec513 Changed lfs_toleb128 to assert on buffer overflow
We should always know the worst-case leb128 size when calling
lfs_toleb128, so it's really a developer-error if lfs_toleb128 results
in a buffer overflow.

This is different from lfs_fromleb128, since we use lfs_fromleb128 to
parse on-disk leb128s, which may be corrupted, incomplete, malformed,
etc.

Changing this to an assert (unreachable really) saves a bit of code
since the compile can eliminate the LFS_ERR_CORRUPT that was only
reachable via developer-error:

           code          stack
  before: 37884           3048
  after:  37872 (-0.0%)   3048 (+0.0%)
2024-08-16 01:03:55 -05:00
Christopher Haster 6e57318194 Better deduplicated ckprefix/cksuffix
These pieces of logic were common across the lfsr_bd_readck/cmpck/cpyck/
readtag functions and made sense to break out into their own functions.

It was just a bit tricky to figure out what the internal API should look
like.

This saves a bit of code at the cost of some stack. But it also makes
the code cleaner so this tradeoff is worth it to me:

           code          stack
  before: 38100           3032
  after:  37884 (-0.6%)   3048 (+0.5%)
2024-08-16 01:03:51 -05:00
Christopher Haster ccc073faed Rough implementation of ckreads
With the adoption of the odd-parity-zero rbyd perturb scheme, it's now
possible to validate individual tag's parity with neighboring valid
bits. This sparked an idea that I previously thought was intractable.

If we:

1. Validate all metadata reads by checking their on-disk parity bits.

2. Validate all data reads by checking their in-metadata checksums.

We end up with a closed system where all reads are checked by at least
a parity bit.

Being able to check all reads is a very valuable filesystem feature, but
difficult for littlefs:

- We need to keep relevant data in RAM while validating checksums.

  We can't just validate checksums and then perform a second read as
  that creates a hole where new bit-errors may be introduced.

- This is solved in other filesystems by loading and checking whole
  blocks in RAM. We just can't do that here.

- Without parity, we would need to check the rbyd's checksum on every
  tag read. This would lead to a crazy O(n^2 log n) rbyd compaction
  runtime.

  Which is why I original thought ckreads was just intractable.

Now, this isn't all sunshine and rainbows. ckreads, as implemented here,
has some deeply concerning flaws:

- A parity bit is, mathematically, the minimum possible error-detection
  possible. Is validating reads with only a parity bit sufficient for
  real world applications?

- Validating data checksums on every read may have severe performance
  implications. We need to read up to the entire block, which can lead
  to O(n^2) behavior when performing a lot of small reads in a file.

- In order to validate checksums/parity-bits, we need to know where the
  checksums/parity-bits actually are for each piece of data.

  Our lfsr_data_t struct provides a surprisingly nice abstraction for
  this, but oof is it expensive.

For the added code/stack cost alone, we probably want to eventually make
this an opt-in compile-time feature.

---

Implementation notes:

- This found an actual compiler bug! Turns out increasing lfsr_data_t
  from 3-words to 5-words confuses GCC:

  https://gcc.gnu.org/bugzilla/show_bug.cgi?id=101854

- Mid-commit, we may have not actually written the last tag's parity
  yet, which is a bit of a problem because we may read the last tag when
  building the next trunk!

  Fixing this required a whole separate tailck mechanism, which just
  tracks in-progress commit's parity bits.

  This doesn't help the code/stack cost situation...

- lfsr_bd_read/cmp/cpy all need to be extended to support calculating a
  checksum on the side, which is a bit of a mess.

- bptr's cksize/cksum is redundant now, which is going to make
  conditional compilation a mess.

- The extra parity byte we need to read makes hint calculation a pain.

Code cost wise... yeah, it's significant. Turns out almost doubling
lfsr_data_t has a significant impact on stack usage. Add in all the
extra code to track checksums/parity-bits and validate checksums/
parity-bits and you got yourself a pretty heavy feature:

           code          stack
  before: 36352           2672
  after:  38100 (+4.8%)   3032 (+13.5%)
2024-08-16 01:03:49 -05:00
Christopher Haster 1044c9d2b7 Adopted odd-parity-zero rbyd perturb scheme
I've been scratching my head over our rbyd perturb scheme. It's gotten
rather clunky with needing to xor valid bits and whatnot.

But it's tricky with needing erased-state to be included in parity bits,
while at the same time excluded from our canonical checksum. If only
there was some way to flip the checksums parity without changing its
value...

Enter the crc32c odd-parity zero: 0xfca42daf!

This bends the definition of zero a bit, but it is one of two numbers in
our crc32c-ring with a very interesting property:

  crc32c(m) == crc32c(m xor 0xfca42daf) xor 0xfca42daf  // odd-p zero
  crc32c(m) == crc32c(m xor 0x00000000) xor 0x00000000  // even-p zero

Recall that crc32c's polynomial, 0x11edc6f41, is composed of two
polynomials: 0x3, the parity polynomial, and 0xf5b4253f, a maximally
sized irreducible polynomial. Because our polynomial breaks down into
two smaller polynomials, our crc32c space turns out to not be a field,
but rather a ring containing two smaller sub-fields. Because these
sub-fields are defined by their polynomials, one is the 31-bit crc
defined by the polynomial 0xf5b4253f, while the other is the current
parity.

We can move in the parity sub-field without changing our position in the
31-bit crc sub-field by xoring with a number that is one in the parity
sub-field, but zero in the 31-bit crc sub-field.

This number happens to be 0xf5b4253f (0xfca42daf bit-reversed)!

(crcs being bit-reversed will never not be annoying)

So long story short, xoring any crc32c with 0xfca42daf will change its
parity but not its value.

---

An that's basically our new perturb scheme. If we need to perturb, xor
with 0xfca42daf to change the parity, and after calculating/validating
the checksum, xor with 0xfca42daf to get our canonical checksum.

Isn't that neat!

There was one small hiccup: At first I assumed you could continue
including the valid bits in the checksum, which would have been nice for
bulk checksumming. But this doesn't work because while valid bits cancel
out so the parity doesn't change, changing valid bits _does_ change the
underlying 31-bit crc, poisoning our checksum and making everything a
mess.

So we still need to mask out valid bits, which is a bit annoying.

But then I stumbled on the funny realization that by masking our valid
bits, we accidentally end up with a fully functional parity scheme.
Because valid bits _don't_ include the previous valid bit, we can figure
out the parity for not only the entire commit, but also each individual
tag:

  80 03 00 08 6c 69 74 74 6c 65 66 73 80
  ^'----------------.---------------' ^
  |                 |                 |
  v       +       parity      =       v'

Or more simply:

  80 03 00 08 6c 69 74 74 6c 65 66 73 80
  '----------------.----------------' ^
                   |                  |
                 parity       =       v'

Double neat!

Some other notes:

- By keeping the commit checksum perturbed, but not the canonical
  checksum, the perturb state is self-validating. We no longer need to
  explicitly check the previous-perturb-bit (q) to avoid the perturb
  hole we ran into previously.

  I'm still keeping the previous-perturb-bit (q) around, since it's
  useful for debugging. We still need to know the perturb state
  internally at all times in order to xor out the canonical checksum
  correctly anyways.

- Thanks to all of our perturb iterations, we now know how to remove the
  valid bits from the checksum easily:

    cksum ^= 0x00000080 & (tag >> 8)

  This makes the whole omitting-valid-bits thing less of a pain point.

- It wasn't actually worth it to perturb the checksum when building
  commits, vs manually flipping each valid bit, as this would have made
  our internal appendattr API really weird.

  At least the perturbed checksum made fetch a bit simpler.

Not sure exactly how to draw this with our perturb scheme diagrams,
maybe something like this?

  .---+---+---+---. \   \   \   \
  |v|    tag      | |   |   |   |
  +---+---+---+---+ |   |   |   |
  |     commit    | |   |   |   |
  |               | +-. |   |   |
  +---+---+---+---+ / | |   |   |
  |v|qp-------------->p>p-->p   .
  +---+---+---+---+   | .   .   .
  |     cksum     |   | .   .   .
  +---+---+---+---+   | .   .   .
  |    padding    |   | .   .   .
  |               |   | .   .   .
  +---+---+---+---+   | |   |   |
  |v------------------' |   |   |
  +---+---+---+---+     |   |   |
  |     commit    |     +-. |   +- rbyd
  |               |     | | |   |  cksum
  +---+---+---+---+     / | +-. /
  |v----------------------' | |
  +-------+---+---+         / |
  |     cksum ----------------'
  +---+---+---+---+
  |    padding    |
  |               |
  +---+---+---+---+
  |     erased    |
  |               |
  .               .
  .               .

---

Code changes were minimal, saving a tiny bit of code:

           code          stack
  before: 36368           2664
  after:  36352 (-0.0%)   2672 (+0.3%)

There was a stack bump in lfsr_bd_readtag, but as far as I can tell it's
just compiler noise? I poked around a bit but couldn't figure out why it
changed...
2024-08-16 01:03:43 -05:00
Christopher Haster dd339e3090 scripts: Added parity.py for quick parity calculation
This is probably overkill, but who really wants to be calculating parity
by hand...
2024-08-01 16:15:34 -05:00
Christopher Haster 26572bb369 Reverted to simpler ecksum calculation
So simply calculating the ecksum over the whole prog size, instead of
manually CRCing the leading byte that we need to separately read to
check if we need to perturb.

Yes, this risks two reads instead of the one we need, but it's simpler,
less error prone, and less code. Our caching layer should prevent double
reads like this, so we might as well rely on it.

Saves some code:

           code          stack
  before: 36396           2664
  after:  36368 (-0.1%)   2664 (+0.0%)
2024-07-31 13:13:46 -05:00
Christopher Haster fb73f78c91 Updated comments to prefer "canonical checksum" for rbyd checksums
I think this describes the goal of the non-perturbed rbyd checksums
decently. At the very least it's less wrong that "data checksum", and
calling it the "metadata checksum" would just be confusing. (Would our
commit checksum be the "metametadata checksum" then?)
2024-07-31 12:29:13 -05:00
Christopher Haster 9a9b3fc161 scripts: Reverted crc32c.py to naive CRC implementation
The naive implementation is simpler, less code, and more likely to be
correct, each of these are more valuable than speed in our debug
scripts.

We're in Python anyways (no offense Python!).

Plus I think it's good to show that the underlying logic of CRCs aren't
really that complex, at least until we throw optimizations into the mix.
2024-07-31 12:17:13 -05:00
Christopher Haster 7fe6e2ce45 Fixed block crystallization not triggering on boundary underflow
It's expected for our crystal boundary calculation to underflow, but
when checking for holes we were using the wrong signed/unsigned
comparison, so lfsr_file_carve thought there was a hole when there
wasn't:

  -crs   pos                              pos    -crs
  .-------|       <-- this lookup    ------|       .--
  '---.   | +crs                     --.   | +crs  '--
  .   |---|---.       ended up         |---|---.   .
  .   v   v   v       looking   -->    v   v   v   .
  .   .---.           like this        .---.       .
  .   |dat|                            |dat|       .
  .   '---'                            '---'       .
  .   0   .   n                        0   .   n   .

  '---.---'                                '---.---'
   no hole                               clearly a hole

This led to unoptimal block compaction and weird block alignment for
even relatively simple files.

The crystallization threshold is only a heuristic so this didn't exactly
break anything, but it was causing block-aligned files to waste a bit of
of space which wasn't great.

---

To hopefully protect against this in the future, I've added a couple
*_litmus tests to check that at least some simple block-aligned files
end up with the correct number of branches/blocks. This should at least
give us some confidence our crystallization algorithm is working as
intended.

We don't have all that many tests (any?) over the exact topology of
files, mainly because of how many heuristics are involved. Maybe we
should look into adding a couple more.

No code changes:

           code          stack
  before: 36396           2664
  after:  36396 (+0.0%)   2664 (+0.0%)
2024-07-29 13:12:58 -05:00
Christopher Haster 0ab0406d53 Added useful handling of LFS_M_RDONLY
It was a bit tricky to figure out what this should look like.
Traditionally, filesystems tend to fallback to readonly if they detect
unsupported wcompat (ro_compat) flags or similar config mismatch.

We could do something similar in littlefs, but since we default to
asserting on writes to readonly objects for smaller code size, this
would be really weird and hard to use from a users perspective...

Instead, lfsr_mount returns LFS_ERR_NOTSUP on encountering wcompat-
mismatch in RDWR mode, but _not_ RDONLY mode. This allows the common
rdonly-fallback pattern to be implemented on the user's side of things,
similar to the common format-fallback pattern:

  int err = lfsr_mount(&lfs, LFS_M_RDWR, &cfg);
  if (err && err != LFS_ERR_NOTSUP) {
      return err;
  }
  if (err == LFS_ERR_NOTSUP) {
      err = lfsr_mount(&lfs, LFS_M_RDONLY, &cfg);
      if (err) {
          return err;
      }
  }

Note that lfsr_mount may still return LFS_ERR_NOTSUP if it encounters
rcompat-flags, even with RDONLY. Detecting this state will likely need
two lfsr_mount calls with the current API, but I don't think that will
be a big deal.

The main benefit of this scheme is that it is quite cheap thanks to
pushing the fallback logic on the user:

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

One missing puzzle piece here is how do you upgrade the filesystem? But I
think the lesson from the on-disk v2.0 -> v2.1 version bump is that this
should really be an explicit function (lfsr_fs_upgrade?). If explicit
and stand-alone, like lfsr_format, we shouldn't need a weird pseudo-
rdonly mode at all.
2024-07-27 00:47:45 -05:00
Christopher Haster 36eabb1c68 Moved test_incompat into test_mount
These really are mount tests, and moving them into test_mount means less
confusion when adding future mount_somewhat_incompat tests.
2024-07-27 00:47:45 -05:00
Christopher Haster e66170308e Renamed public API params to match internal names
- traversal -> t
- config -> cfg

This is just to make things consistent in case users want to peek behind
the curtain.
2024-07-27 00:47:45 -05:00
Christopher Haster b37bff377b Added mount-time LFS_M_FLUSH/SYNC
These simply imply LFS_O_FLUSH/SYNC on all open writable files.
LFS_M_SYNC is equivalent to MS_SYNCHRONOUS in Linux/etc, while
LFS_M_FLUSH is just provided for consistency.

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

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

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

                  code          stack
  before:        36356           2664
  m_flush/sync:  36364 (+0.0%)   2664 (+0.0%)
  mount-no-mask: 36356 (+0.0%)   2664 (+0.0%)
2024-07-27 00:47:45 -05:00
Christopher Haster e676bb225c Deduplicate lfsr_fs_ckmeta/data -> lfsr_fs_ck internally
This just saves a bit of code:

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

Of course, everything has a cost:

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

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

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

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

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

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

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

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

Oh, and the multiple calls to lfsr_btree/bshrub_traverse apparently
uninlined it out of lfsr_mtree_traverse, saving the stack cost in the
stack hot-path... Yay?
2024-07-27 00:47:45 -05:00
Christopher Haster e812ac4a8c Reverting most of internal LFS_F_CANLOOKAHEAD
It's really not that much code (36 bytes, and only if you call
lfsr_fs_gc), and implicit state is better the explicit state (less
things that can fall out of sync).

I'm keeping the fancy F/GC flag masking in lfsr_fs_gc though.

Code changes:

           code          stack
  before: 35988           2696
  after:  36024 (+0.1%)   2696 (+0.0%)
2024-07-27 00:47:45 -05:00
Christopher Haster b7e7313ef0 Added internal LFS_F_CANLOOKAHEAD flag
This is equivalent to the user-facing LFS_I_CANLOOKAHEAD flag, but
explicitly set in lfs_alloc/lfs_alloc_markfree, rather than being
implied.

Usually, I prefer implicit state, as this means less things that can
fall out-of-sync if there is a filesystem bug, but for
LFS_F_CANLOOKAHEAD explicit state might be warranted.

The main benefit is we can take advantage of the matching F/GC bit
patterns to simplify lfsr_fs_gc's progress checks.

This ends up saving a bit of code:

           code          stack
  before: 36048           2696
  after:  35988 (-0.2%)   2696 (+0.0%)
2024-07-27 00:47:45 -05:00
Christopher Haster 0893c1f6be Increased internal flags 16 bits -> 32 bits
If we add CKMETA/CKDATA and eventually REPAIRMETA/REPAIRDATA to the file
open flags, we'll end up with 17 flags total (13 user-facing,
4 internal), which is a bit (heh) too much for a 16-bit flags field!

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

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

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

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

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

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

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

---

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

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

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

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

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

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

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

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

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

---

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

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

Note this still keeps the rbyd-specific commit logic necessary for
committing to specific btree nodes, even though btree node compaction
was the only current use case. This should eventually be useful for
metadata repair. Hopefully const-propagation can minimize the cost, but
realistically this means we're probably leaving some code savings on the
table.
2024-07-24 13:58:26 -05:00
Christopher Haster ff4cc52ebb Switched lfsr_fs_ckmeta/ckdata to use lfsr_mtree_traverse
These should never need to mutate the filesystem, so calling
lfsr_mtree_gc doesn't really make sense. This mainly matters for
link-time gc in case we never need lfsr_mtree_gc (readonly mode?).

Unfortunately this adds a code cost because the optional pointers to
lfsr_mtree_traverse can no longer be const-propagated:

           code          stack
  before: 36284           2704
  after:  36292 (+0.0%)   2704 (+0.0%)
2024-07-24 13:49:53 -05:00
Christopher Haster fb3c0daa0a t: Moved some stuff around in lfsr_mtree_gc
Nothing consequential.

One interesting question is if we should swap our dirty bits during the
call to lfsr_mtree_traverse. At the moment I think limiting this to just
our lfsr_mtree_gc logic will create the least surprise in the future.

Code changes minimal:

           code          stack
  before: 36288           2704
  after:  36284 (-0.0%)   2704 (+0.0%)
2024-07-20 01:27:45 -05:00
Christopher Haster 46488ebc7f t: Moved lookahead population into lfsr_mtree_gc/lfs_alloc
It was a bit weird to have this in lfsr_mtree_traverse, which doesn't
change any filesystem state otherwise.

At the very least we should call lfs_alloc_markinuse and
lfs_alloc_markfree in the same function, and rerouting
lfsr_mtree_traverse eot to handle this would have added code cost
anyways.

The main cost is stack:

           code          stack
  before: 36256           2680
  after:  36288 (+0.1%)   2704 (+0.9%)

Unfortunately this reveals one of the bigger issues with our optional
return parameters: if a function with optional return parameters needs
the structs to perform work, in this case lfsr_mtree_traverse needs
lfsr_bptr_t in case ckmeta/ckdata is requested, it requires an
additional stack allocation.

In theory, these stack allocations could be elided if the return structs
are provided, but you can't really express this in standard C.

Combine this with the fact that lfs_alloc is sensitive to stack changes,
and lives at the bottom at every hot-path, and the end result is more
stack usage.

Note that the additional stack cost, 24 bytes, is exactly equal to one
tag + one bptr, 4 bytes + 20 bytes.
2024-07-20 01:27:45 -05:00
Christopher Haster 51fa5b9831 gc: Reverted to only consider ckmeta/ckdata done if not mutated
Two main reasons:

1. If we mount without ckprogs, we do actually have a pretty decent hole
   here where data can be written with errors and go unchecked during
   lfsr_fs_gc.

2. If we're traversing a btree that gets mutated mid-traversal, we're
   kicked entirely off the btree. This means we could miss large ranges
   of btree nodes/data blocks that may not have themselves been mutated.
   Not great.

Worst case, it doesn't hurt to check things again if the filesystem
changes. If this is too much of a bottleneck, you should probably be
running gc in incremental mode anyways, which always starts a new
traversal on ckmeta/ckdata.

Checking for dirty/mutated doesn't really add that much code:

           code          stack
  before: 36240           2680
  after:  36256 (+0.0%)   2680 (+0.0%)
2024-07-20 01:27:45 -05:00
Christopher Haster 15090e5dcf gc: Also restart gc if lookahead + mutated/dirty
There is really no reason to continue lookahead traversals if our
filesystem has been mutated. Clearing the flag and restarting in this
case is more likely to make progress.

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

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

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

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

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

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

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

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

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

           code          stack
  before: 36228           2680
  after:  36240 (+0.0%)   2680 (+0.0%)
2024-07-20 01:27:45 -05:00
Christopher Haster cb94fa4256 Tried to make flag-anding a bit more readable 2024-07-20 01:27:45 -05:00
Christopher Haster 631bfbc1e8 gc: Made lfsr_fs_gc a bit smarter when flags change
Now we consider if it's still possible for the current traversal to make
progress. If it can, we continue with the relevant masked flags,
otherwise we restart. This should prevent us from traversing the
filesystem for no reason.

I also reverted the ckedmeta/ckeddata flags, these ended up just adding
code cost. We're not in the stack hot-path anyways...

Code changes:

           code          stack
  before: 36244           2680
  after:  36228 (-0.0%)   2680 (+0.0%)
2024-07-20 01:27:45 -05:00
Christopher Haster c58a48c02e gc: Consider ckmeta/ckdata successful even if we mutated the filesystem
Also moved ckmeta/ckdata progress into lfs->flags. We have the bits
available so we might as well use them instead of allocating bools on
the stack...

Whether or not to consider ckmeta/ckdata successful when the filesystem
has been mutated is a bit nuanced.

Initially, I thought we trigger a re-traversal, since we may have
introduced new blocks that haven't been checked. But think about it,
where did those blocks come from?

Any new blocks introduced by filesystem mutation will have just been
written. And if a write introduces corruption you probably have bigger
problems...

... Actually as I write this I realized mounting without ckprogs makes
this even more nuanced, but since ckmeta/ckdata is more intended for
data-at-rest error detection I'm going to keep the change for now.

If you want to catch write errors, you really should enable ckprogs.
This is only a problem for lfsr_fs_gc, and the use cases for
ckmeta/ckdata in lfsr_fs_gc will probably catch any write errors on the
next cycle anyways...

Code changes:

           code          stack
  before: 36208           2680
  after:  36244 (+0.1%)   2680 (+0.0%)
2024-07-20 01:27:45 -05:00
Christopher Haster ad4051b5a2 Don't bother closing lfs->gc in lfsr_unmount
It makes the the are-any-files-still-open assert a bit uglier, but we
really don't need to bother calling lfsr_omdir_close here. We're done
with this struct anyways...

Not doing something saves a bit of code:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

---

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

           code          stack
  before: 36476           2680 (+0.0%)
  after:  36316 (-0.4%)   2680 (+0.0%)
2024-07-18 20:46:58 -05:00
Christopher Haster 54ecc94702 Moved lfs_alloc_ckpoint out of lfsr_mtree_gc
Matching lfsr_mdir_commit, it's probably safer if lfs_alloc_ckpoint
calls are always explicit.

This is doubly true for traversals since we absolutely must not call
lfs_alloc_ckpoint in lfsr_mtree_traverse, or else lfs_alloc will break
in a really comical fashion.

It's also a bit silly how little an impact on code size this had:

  before: 36472           2680
  after:  36476 (-0.0%)   2680 (+0.0%)
2024-07-17 22:16:54 -05:00