Commit Graph

953 Commits

Author SHA1 Message Date
Christopher Haster c2d9f1b047 Implemented, but untested, global-removes
This implementation is in theory correct, but of course, being untested,
who knows?

Though this does come with remounting added to all of the directory
tests. This effectively tests that all of the directory creation tests
we have so far maintain grm=0 after each unmount-mount cycle. Which is
valuable.
2023-07-18 21:40:36 -05:00
Christopher Haster cc0ac25b5e Implemented infrastructure necessary for global-removes
This has, in theory, global-removes (grm) being written out as a part of
of directory creation, but they aren't used in any form and so may not
be being written correctly.

But it did require quite a bit of problem solving to get to this point
(the interactions between mtree splitsand grms is really annoying), so
it's worth a commit.
2023-07-18 21:40:30 -05:00
Christopher Haster cb1319c9e6 Expanded fuzz testing a bit, found/fixed an mid neighbor update bug
This bug was just overlooked in testing the mtree, fortunately dir
fuzzing found it. Though since this depends on neighboring mdirs, it
probably would have been found quicker with smaller block sizes. At the
moment I am only testing on NOR-liked geometry (4KiB blocks).

The fix is easy, we can use the difference in the mtree size to
determine if a split or drop happened in mdir commit, since at most one
of these can happen on any mdir commit.

Also added an explicit test for mid updates when splitting and dropping.
2023-07-07 16:31:52 -05:00
Christopher Haster 039bdf91b4 Added lfsr_stat and integrated into dir tests
lfsr_stat is really a directory operation underneath, so it's good to
add to our testing while we are building up the dir tests.

It's interesting to note lfsr_stat and lfsr_dir_read are less
deduplicatable than their previous versions, since lfsr_stat can get
most of it's info from lfsr_mtree_pathlookup. Though there will probably
need to be some code sharing when we get to files with sizes.
2023-07-07 13:53:39 -05:00
Christopher Haster f472327f74 Added tests over potential directory-id checksum issues
- Checksum collisions
- Collisions with root did
- Collisions needing wraparound
- Possible leb128 encoding issues

Sure enough the last one caught an off-by-one error in our calculation
of the leb128 encoded size. I sort of expected a bug there, since it's
rather nuanced math, so it's good to have test coverage now.
2023-07-06 15:44:53 -05:00
Christopher Haster 21f7fd1032 Added more testing over mkdir, fixed issues dname changes introduced
The main issues:

- The addition of the root's dstart entry during lfsr_format throws off
  our mtree tests. It's a bit of a hack, but for now I am just manually
  deleting the root's dstart entry at the beginning of each tests.

  It might be possible to make the mtree tests work around the root's
  dstart, but it seems to cause problems for when exactly the mtree
  splits.

- btree dnamelookup and mdir dnamelookup need different things from
  the rbyd dnamelookup when the dname is not found. The btree lookup
  needs the largest branch smaller than the dname, since this is the
  "bucket" containing our dname, while the mdir dnamelookup needs
  the id that _follows_ the id smaller than the dname, since insertion
  causes all ids >= the inserting id to shift up.

  The solution here is to make rbyd dnamelookup behave as expected by
  btree dnamelookup. btree needs more info about the branch (weight
  mostly), so this avoids more issues. mdir dnamelookup adjusts the
  id as needed, which costs a bit of code, but makes things work.

  Fortunately, mdir dnamelookup can assume the weight is 1, which
  simplifies things a bit.
2023-07-06 00:55:28 -05:00
Christopher Haster da810aca26 Implemented mtree path/dname lookup, rudimentary lfsr_mkdir/lfsr_dir_read
This makes it now possible to create directories in the new system.

The new system now uses a single global "mtree" to store all metadata
entries in the filesystem. In this system, a directory is simply a range
of metadata entries. This has a number of benefits, but does come with
its own problems:

1. We need to indicate which directory each file belongs to. To do this
   the file's name entry has been changed to a tuple of leb128-encoded
   directory-id + actual file name:

     01 66 69 6c 65 2e 74 78 74  .file.txt
      ^ '----------+----------'
      '------------|------------ leb128 directory-id
                   '------------ ascii/utf8 name

   If we include the directory-id as part of filename comparison, files
   should naturally be next to other files in the same directory.

2. We need a way allocate directory-ids for new directories. This turns
   out to be a bit more tricky than I expected.

   We can't use any mid/bid/rid inherent to the mtree, because these
   change on any file creation/deletion. And since we commit the did
   into the tree, that's not acceptable.

   Initially I though you could just find the largest did and increment,
   but this gives you no way to reclaim deleted dids. And sure, deleted
   dids have no storage consumption, but eventually you will overflow
   the did integer. Since this can suddenly happen in a filesystem
   that's been in a steady-state for years, that's pretty unnacceptable.

   One solution is to do a simple linear search over the mtree for an
   unused did. But with a runtime of O(n^2 log(n)), this raises
   performance concerns.

   Sidenote: It's interesting to note that the Linux kernel's allocation
   of process-ids, a very similar problem, is surprisingly complex and
   relies on a radix-tree of bitmaps (struct idr). This suggests I'm not
   missing an obvious solution somewhere.

   The solution I settled on here is to instead treat the set of dids as
   a sort of hash table:

   1. Hash the full directory path into a did.
   2. Perform a linear search until we have no collision.

     leb128(truncate28(crc32c("dir")))
          .--------'
          v
     9e cd c8 30 66 69 6c 65 2e 74 78 74  ...0file.txt
     '----+----' '----------+----------'
          '-----------------|------------ leb128 directory-id
                            '------------ ascii/utf8 name

   Worst case, this can still exhibit the worst case O(n^2 log(n))
   performance when we are close to full dids. However that seems
   unlikely to happen in practice, since we don't truncate our hashes,
   unlike normal hash tables. An additional 32-bit word for each file
   is a small price to pay for a low-chance of collisions.

   In the current implementation, I do truncate the hash to 28-bits.
   Since we encode the hash with leb128, and hashes are statistically
   random, this gives us better usage of the leb128 encoding. However
   it does limit a 32-bit littlefs to 256 Mi directories.

   Maybe this should be a configurable limit in the future.

   But that highlights another benefit of this scheme. It's easy to
   change in the future without disk changes.

3. We need a way to know if a directory-id is allocated, even if the
   directory is empty.

   For this we just introduce a new tag: LFSR_TAG_DSTART, which
   is an empty file entry that indicates the directory at the given did
   in the mtree is allocated.

   To create/delete these atomically with the reference in our parent
   directory, we can use the GRM system for atomic renames.

   Note this isn't implemented yet.

This is also the first time we finally get around to testing all of the
dname lookup functions, so this did find a few bugs, mostly around
reporting the root correctly.
2023-07-05 13:41:21 -05:00
Christopher Haster 0bb1e0b8b5 Changed namelookup functions to include a directory-id
The plan is that names in littlefs now include a directory-id prefixed
as a single leb128.

  01 66 69 6c 65 2e 74 78 74  .file.txt
   ^ '----------+----------'
   '------------|------------ leb128 directory-id
                '------------ ascii/utf8 name

Unfortunately, while this is easy for read/compare operations to implement,
it creates a bit of a problem for writes. We can't allocate a new buffer
for each name, so we need some sort of extra mechanism.

The solution here is to just add a did member to lfsr_data_t that is
written when non-negative. This works, though it does introduce some
complexity.

Fortunately, did in lfsr_data_t is somewhat free when
sizeof(void*) == sizeof(lfs_size_t), due to the union with disk
references.
2023-07-03 23:28:14 -05:00
Christopher Haster 2fe2078f50 Renamed tests/benches such that order is logical
It doesn't make sense to test more complex logic, such as t2_btree.toml,
when the logic it is built on, t1_rbyd.toml, does not past testing. The
test runner already guarantees a consistent lexicographic order, so all
we need to do is renamed these from test_* -> tn_*.

Note, if we every have more than 10 tests, we will need to bump up the
number of digits for all tests, so t1_rbyd.toml -> t01_rbyd.toml. This
is the main downside of lexicographic ordering. But we'll cross that
bridge when we get to it.
2023-06-30 16:37:23 -05:00
Christopher Haster 938cee1640 Updated benches based on changes, commented out outdated benchmarks 2023-06-30 03:00:07 -05:00
Christopher Haster eee0e6cfa1 Reimplemented the block-allocator over mtree traversal
Took the opportunity to make some allocator tweaks:

- Renamed lfs.free -> lfs.lookahead, it's previous name did cause some
  confusion.

- Renamed lfs.free.off -> lfs.lookahead.start
- Renamed lfs.free.i   -> lfs.lookahead.next
- Renamed lfs.free.ack -> lfs.lookahead.acked

- Changed bitmap from using 32-bit words to using 8-bit bytes, dropping
  the alignment requirement. One of the reasons for 32-bit alignment was
  an attempt at future proofing for some sort of free-list.

  This never landed, and if it did, it could have been provided without
  breaking backwards compatiblity via an additional config option, at a
  minor RAM cost.

  We never used ffs/clz instructions for this bitmap, so I don't think
  using 32-bit words offers much advantage. It just creates another
  potential issue for users if their lookahead buffer is unaligned.

These changes should probably also be upstreamed to the current version.
They don't depend on anything rbyd specific.

Note, at some point lfs_alloc will need to be extended to mark block tags,
etc, as in-use during traversal.
2023-06-30 02:32:36 -05:00
Christopher Haster 91d90b7eef Some minor tweaks to internal ptr types
- Renamed mpair -> mptr, may have >2 blocks in the future.

- Renamed branch -> bptr for consistency.

- Renamed other_block -> redund_rbyd.

- Changed comparison functions to use -1, 0, +1, even for unordered
  types.

- Added lfs_cmp function for unioning comparisons with signed errors.
2023-06-27 13:21:22 -05:00
Christopher Haster cf588ac3fa Dropped alt-always as an rbyd trunk terminator
Now that tree rebalancing is implemented and needed a null terminator
anyways, I think it's clear that the benefit of the alt-always pointers
as trunk terminator has pretty limited value.

Now a null or other tag is needed for every trunk, which simplifies
checks for end-of-trunk.

Alt-always tags are still emitted for deletes, etc, but there their
behavior is implicit, so no special checks are needed. Alt-always tags
are naturally cleaned up as a part of rbyd pruning.
2023-06-27 00:49:31 -05:00
Christopher Haster f311d1102c Cleaned up rbyd rebalance implementation, optimized things
- Since both trunks emit altle tags now, reworked the trunk merging to
  reuse more code.

- Changed lfsr_mdir_fetch to rely on trunk=0 to detect the no-commit
  state. This is purely for consistency.

  This actually broke some tests that committed nothing, resulting in
  trunk-less rbyds, which is a bit concerning, but I don't think
  trunk-less rbyds will ever be valid in our system?

- Simplified lfsr_rbyd_estimate calculation, merged lfsr_rbyd_bisect
  since this is almost always needed after an estimated failure, and
  that way dependent function have to call fewer things to implement
  rbyd splitting.

- Dropped vestigial names for now, though need to revisit this later.

After these changes the code size difference between rebalancing and
appending is a bit smaller at ~392 bytes: 16208 -> 16600 (+2.4%). It's
interesting to note this is mostly because the conservative overhead
calculation is easier with rebalancing.

In theory this also saves some stack usage, but since I'm measuring
maximum stack usage it doesn't show up since it's not on the deepest
path.
2023-06-27 00:49:25 -05:00
Christopher Haster 43dc3a5c8d Implemented tree rebalancing during rbyd compaction
This isn't actually for performance reasons, but to reduce storage
overhead of the rbyd metadata tree, which was showing signs of being
problematic for small block sizes.

Originally, the plan for compaction was to rely on the self-balancing
rbyd append algorithm and simply append each tag to a new tree.
Unfortunately, since each append requires a rewrite of the trunk
(current search path), this introduces ~n*log(n) alts but only uses ~n alts
for the final tree. This really starts to put pressure on small blocks,
where the exponential-ness of the log doesn't kick in and overhead
limits are already tight.

Measuring lfsr_mdir_commit code size, this shows a ~556 byte cost on
thumb: 16416 -> 16972 (+3.4%). Though there are still some optimizations
on the table, this implementation needs a cleanup pass.

               alt overhead  code cost
  rebalance:        <= 28*n      16972
  append:    <= 24*n*log(n)      16416

Note these all assume worst case alt overhead, but we _need_ to assume
worst case for our rbyd estimations, or else the filesystem can get
stuck in unrecoverable compaction states.

Because of the code cost I'm not sure if rebalancing will stay, be
optional, or replace append-compaction completely yet.

Some implementation notes:

- Most tree balancing algorithms rely on true recursion, I suspect
  recursion may be a hard requirement in general, but it's hard to find
  bounded-ram algorithms.

  This solution gets around the ram requirement by leveraging the fact
  that our tags exist in a log to build up each layer in the tree
  tail-recursively. It's interesting to note that this is a special
  case of having little ram but lots of storage.

- Humorously this shouldn't result in a performance improvement. Rbyd
  trees result in a worst case 2*log(n) height, and rebalancing gives us
  a perfect worst case log(n) height, but, since we need an additional
  alt pointer for each node in our tree, things bump back up to 2*log(n).

- Originally the plan was to terminate each node with an alt-always tag,
  but during implementation I realized there was no easy way to get the
  key that splits the children with awkward tree lookups. As a
  workaround each node is terminated with an altle tag that contains the
  key followed by an unreachable null tag. This is redundant information,
  but makes the algorithm easier to implement.

  Fortunately null tags use the smallest tag encoding, which isn't that
  small, but that means this wastes at most 4*n bytes.

- Note this preserves the first-tag-always-ends-up-at-off=0x4 rule, which
  is necessary for the littlefs magic to end up in a consistent place.

- I've dropped dropping vestigial names for now, which means vestigial
  names can remain in btrees indefinitely. Need to revisit this.
2023-06-25 15:23:46 -05:00
Christopher Haster fd43534b0e Renamed lfsr_btree_update -> lfsr_btree_set 2023-06-20 02:56:41 -05:00
Christopher Haster 85f2aa4f65 Deduplicated mtree split logic by moving it into new lfsr_mtree_split_
This really helps just make the mess that is lfsr_mdir_commit readable,
though seems to only save ~200 bytes. The number of arguments that need
to be set up in order to call lfsr_mdir_commit seem to be offsetting
code savings.

It's interesting to note more code could probably be saved if
lfsr_mtree_split_ was inlined into lfsr_mdir_commit, with one of the
two invocations code using a goto both to jump in and jump out of the
common split logic. But I'm not about to go down that sort of hellish
path.
2023-06-20 02:56:41 -05:00
Christopher Haster 854e1e68f0 Added some more mtree tests, fixed mroot extension bug
- Finally figured out how to test multiple mroot extensions without an
  allocator, though hopefully forcing PROG_SIZE doesn't break test
  framework things at some point...

- Added tests that magic string is always in the same place. This isn't
  strictly required for littlefs to work, but is a nice feature to have.

Of course, the new tests found a bug, but it was in a surprisingly
place. Accidentally allowed the revision count to be uninitialized when
compacting the mroot. At least there's a test that covers this now.
2023-06-20 02:56:41 -05:00
Christopher Haster 0690a86f1d Removed revision count from lfsr_rbyd_t
Now we read the revision count on-demand, trading off some extra reads
for a smaller lfsr_rbyd_t struct.

I believe this is worth it because:

1. We're created a lot of lfsr_rbyd_t structs as a part of the relatively
   complicated mdir/btree commit logic in order to safely fallback on errors.

2. We don't really need the revision count for our Cow btrees, so we
   only need to read the revision count on mdir fetch (which we were
   already reading too many times), on mdir compact, and on rbyd fetch
   as a part of checksum calculation.

   This really only adds a O(1) cost when we are compacting, which is rather
   small.

Current measurements:

  code:  8980 -> 9036 (+0.6%)
  stack: 1024 -> 1000 (-2.3%)

Though note this is currently without any mdir/btree commit code being
dragged in.
2023-06-20 02:56:35 -05:00
Christopher Haster 98ef342942 Reworked manchor compact to be able to use lfsr_mdir_compact_
More code reuse => smaller code size generally. Though this adds another
special mid value.

Note that the range in this compact excludes all tags, we really only
want the revision count. It's tempting to implicitly copy the
magic/config via the compact range, but this risks included user
attributes and other things that we really don't want cluttering up our
mroot chain.
2023-06-19 16:08:50 -05:00
Christopher Haster 5a88eaccbc Reserved bit 7 for leb128 subtypes in the future
This should have also been done previouly as a part of the tag
reencoding work. Oh well.
2023-06-19 16:08:50 -05:00
Christopher Haster 799e7cfc81 Flipped btree encoding to allow variable redund blocks
This should have been done as a part of the earlier tag reencoding work,
since having the block at the end was what allowed us to move the
redund-count out of the tag encoding.

New encoding:

  [-- 32-bit csum   --]
  [-- leb128 weight --]
  [-- leb128 trunk  --]
  [-- leb128 block  --]

Note that since our tags have an explicit size, we can store a variable
number of blocks. The plan is to use this to eventually store redundant
copies for error correction:

  [-- 32-bit csum   --]
  [-- leb128 weight --]
  [-- leb128 trunk  --]
  [-- leb128 block  --] -.
  [-- leb128 block  --]  +- n redundant blocks
  [-- leb128 block  --]  |
           ...          -'

This does have a significant tradeoff, we need to know the checksum size
to access the btree structure. This doesn't seem like a big deal, but
with the possibility of different checksum types may be an annoying
issue.

Note that FCRC was also flipped for consistency.
2023-06-19 16:08:50 -05:00
Christopher Haster a5e7ff7be0 Adopted wide tags where possible and some general cleanup 2023-06-19 16:08:50 -05:00
Christopher Haster e79c15b026 Implemented wide tags for both rbyd commit and lookup
Wide tags are a happy accident that fell out of the realization that we
can view all subtypes of a given tag suptype as a range in our rbyd.
Combining this with how natural it is to operate on ranges in an rbyd
allows us to perform operations on an entire range of subtypes as though
it were a single tag.

- lookup wide tag => find the smallest tag with this tag's suptype, O(log(n))
- remove wide tag => remove all tags with this tag's suptype, O(log(n))
- append wide tag => remove all tags with this tag's suptype, and then
  append our tag, O(log(n))

This is very useful for littlefs, where we've already been using tag's
subtypes to hold extra type info, and have had to rely on awkward
alternatives such as deleting existing subtypes before writing our new
subtype.

For example, when committing file metadata (not yet implemented), we can
append a wide struct tag to update the metadata while also clearing out any
lingering struct tags from previous commits, all in one rbyd append
operation.

This uses another mode bit in-device to change the behavior of
lfsr_rbyd_commit, of which we have a couple:

  vwgrtttt 0TTTTTTT
  ^^^^---^--------^- valid bit (currently unused, maybe errors?)
   '||---|--------|- wide bit, ignores subtype (in-device)
    '|---|--------|- grow bit, don't create new id (in-device)
     '---|--------|- rm bit, remove this tag (in-device)
         '--------|- 4-bit suptype
                  '- leb128 subtype
2023-06-19 16:08:43 -05:00
Christopher Haster 2467d2e486 Added a separate tag encoding for the mtree
This helps with debugging and can avoid weird issues if a file btree
ever accidentally ends up attached to id -1 (due to fs bug).

Though a separate encoding isn't strictly necessary, maybe this should
be reverted at some point.
2023-06-18 15:12:43 -05:00
Christopher Haster f2c36efdb3 Inverted mk-bit logic, renamed to grow-bit
This only affects the in-device tags, not the on-disk tags.

The mk variant of tags was seeing much more use than the grow variant,
since the grow variant is really only used by the btree internals. But
since the default encoding of tags cleared the mk-bit, this led to a
bunch of extra lfsr_tag_setmk calls just to reserialize things correctly
during compact, split, etc.

Flipping the logic so the bit needs to be set to grow tags simplified
things quite a bit.

Note that mk tags do nothing when their delta is zero, so zero-delta
tags are the same in both mk/grow mode.
2023-06-18 15:12:36 -05:00
Christopher Haster 7180b70c9c Allowed "alta" (altbgt 0) to terminate rbyd trunks, dropped rm bit
This replaces unr with null on disk, though note both the rm bit and unr
are used in-device still, they just don't get written to disk.

This removes the need for the rm bit on disk. Since we no longer need to
figure out what's been removed during fetch, we can save this bit for both
internal and future on-disk use.

Special handling of alta allows us to avoid emitting an unr tag (now null) if
the current trunk is truly unreachable. This is minor now, but important
for a theoretical rbyd rebalance operation (planned), which brings the
rbyd overhead down from ~3x to ~2x.

These changes give us two ways to terminate trunks without a tag:

1. With an alta, if the current trunk is unreachable:

     altbgt 0x403 w0 0x7b
     altbgt 0x402 w0 0x29
     alta w0 0x4

2. With a null, if the current trunk is reachable, either for
   code convenience or because emitting an alta is impossible (an empty
   rbyd for example):

     altbgt 0x403 w0 0x7b
     altbgt 0x402 w0 0x29
     altbgt 0x401 w0 0x4
     null
2023-06-17 18:11:45 -05:00
Christopher Haster aa559d30b0 Implemented lfsr_rbyd_namelookup as a binary search, dropped search in fetch
Originally I thought doing a linear search during fetch was going to be
the best route for name lookups, since we already needed a O(b) fetch,
which set a hard ceiling for name lookup performance.

But it turns out we don't need to fetch during btree name lookups
unless we're also validating! Now that validation and lookups are
disentangled, we can do a binary search over the rbyd to drop our
name lookup down to O(log(b)^2).

Two other motivations for this change:

1. This removes the name search from lfsr_rbyd_fetch, which has been
   surprisingly tricky to get right.

2. Now that lfsr_rbyd_fetch doesn't need to follow the create/delete
   history to find and reconstruct the state of names, we only need to
   know the create/delete state of tags post-fetch, freeing up the rbyd
   encoding to be more flexible.

   The next thing I plan to do is drop on-disk remove tags, for example.

This drops the total btree namelookup cost from O(b log_b(n)) to
O(log(b)^2 log_b(n)).
2023-06-16 14:43:47 -05:00
Christopher Haster 2113d877d6 Moved bits around in tag encoding to allow leb128 custom attributes
Yet another tag encoding, but hopefully narrowing in on a good long term
design. This change trades a subtype bit for the ability to extend
subtypes indefinitely via leb128 in the future.

The immediate benefit is ~unlimited custom attributes, though I'm not
sure how to make this configurable yet. Extended custom attributes may
have a significant impact on alt tag sizes, so it may be worth
defaulting to only 8-bit custom attributes still.

Tag encoding:

   vmmmtttt 0TTTTTTT 0wwwwwww 0sssssss
   ^--^---^--------^--------^--------^- valid bit
      '---|--------|--------|--------|- 3-bit mode
          '--------|--------|--------|- 4-bit suptype
                   '--------|--------|- leb128 subtype
                            '--------|- leb128 weight
                                     '- leb128 size/jump

This limits subtypes to 7-bits, but this seems very reasonable at the
moment.

This also seems to limit custom attributes to 7-bits, but we can use two
separate suptypes to bring this back up to 8-bits. I was planning to do
this anyways to have separate "user-attributes" and "system-attributes",
so this actually fits in really well.
2023-06-16 01:51:33 -05:00
Christopher Haster 4ff7c1f771 Commenting out outdated functions for now
This makes it easier to evaluate the code/stack/etc sizes and run tests
without bringing in all of the outdated code.

I guess this officially makes this branch more-or-less a full rewrite,
though the benefit of commenting vs deleting this code is that it can be
easily pulled back in when useful.
2023-06-16 01:51:29 -05:00
Christopher Haster b43d2d2d9d In watch.py, fixed --keep-open-paths typo, made --keep-open implicit sometimes
So now:

  ./scripts/watch.py -K lfs.c ./script.sh

Does the reasonable thing.
2023-06-16 01:51:25 -05:00
Christopher Haster b05db8e3d3 Added support for lists of conditional ifs in test/bench.py
Any conditions in both the suites and cases are anded together to
determine when the test/bench should run.

Accepting a list here makes it easier to compose multiple conditions,
since toml-level elements are a bit easier to modify than strings of
C expressions.
2023-06-01 17:40:51 -05:00
Christopher Haster 07244fb2d4 In test/bench.py, added "internal" flag
This marks internal tests/benches (case.in="lfs.c") with an otherwise-unused
flag that is printed during --summary/--list-*. This just helps identify which
tests/benches are internal.
2023-06-01 17:40:48 -05:00
Christopher Haster 82027f3d90 Changed bench/test.py to error if explicit suite/case can't be found
Previously no matches would noop, which, while consistent with an empty
test suite that contains no tests but shouldn't really error, this made
it easy to miss when a typo would cause tests to be missed.

Also added a bit of color to script-level errors in test/bench.py
2023-06-01 17:16:21 -05:00
Christopher Haster 2339e9865f Tweaked dbgmtree.py -Z flag to include mroots as depth
This helps debug a corrupted mtree with cycles, which has been a problem
in the past.

Also fixed a small rendering issue with dbgmtree.py not connecting inner
tree edges to mdir roots correctly during rendering.
2023-06-01 13:52:14 -05:00
Christopher Haster 49ec7a12b9 Tweaked btree traversal to visit each leaf at most once
We are already paying the memory cost of a fetched lfsr_rbyd_t
during btree so we can traverse inner btree nodes. But we are currently
just wasting this memory when we traverse leaf entries.

Instead, we can use this memory to cache the btree's current leaf node,
avoiding a btree walk until we've iterated over all rids in the current
leaf.

Think about this for a second:

1. We cache the root rbyd in lfsr_btree_t because we share the memory with
   a union and we always need to read the root during lookups.

2. We cache the leaf rbyds in lfsr_btree_traversal_t because we need the
   memory for traversing inner btree nodes.

The only btree nodes we don't cache during traversal are inner nodes
when the height of the btree >= 3.

If you're familiar with how btrees behave on storage, you know the
height because _exponentially_ less likely to grow as the tree gets
larger. It's entirely possible for btree traversal to simply never
traverse a non-cached btree node once your block size gets large
enough.

---

There may be a way to instead reclaim this memory, such as sharing this
memory with mtree traversal and higher layers, but the current API design
and hierarchy of C structs make this difficult. Maybe this is worth looking
into in the future, but at most it would save one lfsr_rbyd_t.

This also reverts some rid changes in btree lookups to share less code
but be a bit easier to reason about.
2023-05-30 19:49:15 -05:00
Christopher Haster 30bcb6947b Added a test for mtree cycle detection, limited cycle detection to mdirs
I intended to also add a test for cycles in the btree that backs the
mtree (and eventually other btrees), but something really curious
happened.

It turns out it's actually really hard to create a btree cycle, even
intentionally.

This is because each CoW btree pointer includes the expected CRC of
the branch's rbyd. To succesfully create a cycle that isn't trivially
detected in a validating mtree traversal, you would somehow need to
solve for a cyclic set of dependent CRCs that are still valid.

I suspect this is slightly easier than a hash-based construction, due to
the linear nature of CRCs, but still I think it's unreasonable to expect
these sort of cycles to occur in the wild. Even with filesystem bugs.

---

Note this isn't true for the mdirs, which are mutable so storing a
checksum in the pointer isn't possible. For this reason, cycle detection
is kept for mdirs during mtree traversal. This may not be strictly
necessary for the mtree, but it needed for the mroot chain.

Nonetheless, this does simplify things. Specifically it reduces the
cycle detection's tortoise state to only mdir pairs.
2023-05-30 19:41:39 -05:00
Christopher Haster 773278eb26 Adopted mtree traversal in lfsr_mountinited
This is a nice bit of deduplication as long as the mtree traversal can
handle both:

1. Cycle detection
2. Btree node validation

Eventually we'll also collect gstate here, which mtree traversal should
make quite easy.

The only catch is if we eventually need a non-fetching way to read the
mroot config, such as if we need to infer the csum type or block-size,
but that's a future problem.
2023-05-30 19:38:04 -05:00
Christopher Haster 1631ca8d78 Reimplemented Brent's cycle detection on top of mtree traversal
This is a bit tricky because our tortoise state is now quite large
thanks to how we are nesting traversals:

- Current mdir pair
- Current mtree block+trunk
- Current btree block+trunk? (TODO)
- Others? (TODO)

This also raises some questions about what constitutes a cycle in our
btrees. Since they are strictly CoW, they should be strictly DAGs worst
case. But is that still true when considering that btree nodes can
contain multiple trunk versions?

To be safe, I'm currently including the trunks in our tortoise state,
but it may be possible to relax this in the future.
2023-05-30 19:37:36 -05:00
Christopher Haster 6a96866737 Added an mtree traversal benchmark
Note that because we amortize the traversal cost over the number of
entries, mtree traversal may have some strange looking results when
compared to mtree lookup.

Though it's interesting to note this is a valid result. In mtree lookups
we need to fetch the mdir for each entry, which is expensive. However
mtree traversal can strictly avoid fetching each mdir more than once.
This does make mdir traversal faster when iterating over all mdirs in
order.

This can be represented in big O notation if we treat the number of
entries (n) and block size (b) as variables:

- mtree traversal via lookup    = O(nb+nlog(b)logb(n))
- mtree traversal via traversal = O(nlog(b)logb(n))
2023-05-30 19:21:34 -05:00
Christopher Haster 09b3d24036 Moved btree rbyd validation into mtree traversal
Validating btree nodes during lfsr_btree_lookup was useful as a
proof-of-concept, but it's not really needed if we validate btree nodes
during mtree traversal.

mtree traversal provides the first reads into the filesystem. It's how
we find the real mroot, and (in theory at the moment) it provides the core
operation for error detection in correction. With this in mind,
implementing btree node validation in mtree traversal makes a lot of
sense, with lfsr_btree_lookup leveraging an assumed successful
validation for faster/smaller btree walks.

Note that btree node validation during traversal is still optional. We
really don't want to pay this cost during block allocation for example.

---

It may look concerning that there's no related validation in btree traversal
layer itself.

It turns out that a quirk of btree traversal returning inner btree nodes on
first visit, before actually traversing the btree node, is that it's
safe for us to validte the btree node in only the mtree traversal layer.
As long as we don't continue traversing on finding a corrupted btree,
the btree traversal layer will never traverse an unvalidated btree node.

This keeps all the validation logic in the same place, mtree traversal.
I don't know if this will stay this way if/when more error correction
features are added, but it's convenient in the meantime.
2023-05-30 18:52:02 -05:00
Christopher Haster 34bcb62a9e Implemented incremental mtree traversal
Just like lfsr_btree_traversal_t, lfsr_mtree_traversal_t provides a
mechanism for traversing the mtree incrementally, including any inner
btree nodes.

This is one level more complex than btree traversal because we also need
to handle the mroot chain and traversal of rids in each mdir.

Again, mtree traversal returns temporary decoded rbyd structs for inner
nodes. Actually, mtree traversal only returns inner nodes... so maybe
using lfsr_data_t here is the wrong choice:

- tag=LFSR_TAG_BTREE => lfsr_rbyd_t
- tag=LFSR_TAG_MDIR  => lfsr_mdir_t
2023-05-30 18:47:30 -05:00
Christopher Haster 93bf68c84b Added lfsr_btree_traversal_t, incremental traversal of btree nodes
The main thing to note is that traversal here != iteration.

Thanks to the right-leaning nature of our btrees, iteration is already
provided by lfsr_btree_lookupnext, using the bid as the current
iteration state.

What btree traversal provides is traversal over every rbyd + entries
used in the btree, include the inner btree nodes. This is useful for
things like garbage collection and error detection that need to operate
on the raw rbyds.

Note that both btree traversal and iteration are still O(n log_b(n)). We
can't do any better than that without recursion.

One non-intuitive implementation detail, we return a tag describing each
entry, but instead of returning an on-disk data reference for inner
btree nodes, we return a pointer to a temporarily decoded rbyd struct.
This simplifies root handling, and we probably want the decoded version
anyways:

- tag=LFSR_TAG_BTREE => lfsr_rbyd_t
- tag=anything else  => lfsr_data_t

The reason for making btree traversal incremental, and not just use a
callback like we've done previously, is to eventually use this as a part
of high-level incremental garbage-collection/error-correction. For this
to work, all of the lower-levels also need to be incremental.
2023-05-30 18:32:51 -05:00
Christopher Haster 565c8cb9c7 Reimplemented the internal opened-mdir linked-list
littlefs uses an invasive linked-list in open mdirs to keep any open
files/dirs (and some special mdirs) in sync during filesystem
operations. The main benefit of this is that the filesystem doesn't need
to know the number of open files at compile time.

The implementation here introduces a new type, lfsr_openedmdir_t, for
mdirs that want to participate in the opened-mdir linked-list. This
saves a couple words of memory in the cases where the mdir does not need
to participate in the opend-mdir linked-list.

Since we are creating quite a few more mdir structs in lfsr_mdir_commit now,
the size of this struct is valuable.

The implementation of lfsr_mdir_commit knew this was coming, so aside
from the new type, adding this feature was straightforward:

1. Update opened-mdirs based on in-flight attrs.
2. Update opened-mdirs rbyd state.
3. Mark any deleted opened-mdirs with the reserved mid -2.
4. Test.
2023-05-30 18:24:36 -05:00
Christopher Haster c60fa69ce1 Optimized dbg*.py tree generation/rendering by deduplicating edges
Optimizing a script? This might sound premature, but the tree rendering
was, uh, quite slow for any decently sized (>1024) btree.

The main reason is that tree generation is quite hacky in places, repeatedly
spitting out multiple copies of the inner node's rbyd trees for example.

Rather than rewrite the tree generation implementation to be smarter,
this just changes all edge representations to namedtuples (which may
reduce memory pressure a bit), and collects them into a Python set.

This has the effect of deduplicating generated edges efficiently, and
improved the rendering performance significantly.

---

I also considered memoizing rbyd tree, but dropped the idea since the
current renderer performs well enough.
2023-05-30 18:17:51 -05:00
Christopher Haster af0c3967b4 Adopted new tree renderer in dbgmtree, implemented mtree rendering
In addition to plugging in the rbyd and btree renderers in dbgbtree.py,
this required wiring in rbyd trees in the mdirs and mroots.

A bit tricky, but with a more-or-less straightforward implementation thanks
to the common edge description used for the tree renderer.

For example, a relatively small mtree:

  $ ./scripts/dbgmtree.py disk -B4096 -t -i
  mroot 0x{0,1}.45, rev 1, weight 0
  mdir                     ids   tag                     ...
  {0000,0001}: .--------->    -1 magic 8                 ...
               | .------->       config 21               ...
               +-+-+             btree 7                 ...
    0006.000a:     | .-+       0 mdir w1 2               ...
  {0002,0003}:     | | '->   0.0 inlined w1 1024         ...
    0006.000a:     '-+-+       1 mdir w1 2               ...
  {0004,0005}:         '->   1.0 inlined w1 1024         ...
2023-05-30 18:10:32 -05:00
Christopher Haster 9b803f9625 Reimplemented tree rendering in dbg*.py scripts
The goal here was to add the option to show the combined rbyd trees in
dbgbtree.py/dbgmtree.py.

This was quite tricky, (and not really helped by the hackiness of these
scripts), but was made a bit easier by adding a general purpose tree renderer
that can render a precomputed set of branches into the tag output.

For example, a 2-deep rendering of a simple btree with a block size of
1KiB, where you can see a bit of the emergent data-structure:

  $ ./scripts/dbgbtree.py disk -B1024 0x223 -t -Z2 -i
  btree 0x223.90, rev 46, weight 1024
  rbyd                       ids       tag                     ...
  0223.0090:     .-+             0-199 btree w200 9            ...
  00cb.0048:     | |     .->      0-39 btree w40 7             ...
                 | | .---+->     40-79 btree w40 7             ...
                 | | | .--->    80-119 btree w40 7             ...
                 | | | | .->   120-159 btree w40 7             ...
                 | '-+-+-+->   160-199 btree w40 7             ...
  0223.0090: .---+-+           200-399 btree w200 9            ...
  013e.004b: |     |     .->   200-239 btree w40 7             ...
             |     | .---+->   240-279 btree w40 8             ...
             |     | | .--->   280-319 btree w40 8             ...
             |     | | | .->   320-359 btree w40 8             ...
             |     '-+-+-+->   360-399 btree w40 8             ...
  0223.0090: | .---+           400-599 btree w200 9            ...
  01a7.004c: | |   |     .->   400-439 btree w40 8             ...
             | |   | .---+->   440-479 btree w40 8             ...
             | |   | | .--->   480-519 btree w40 8             ...
             | |   | | | .->   520-559 btree w40 8             ...
             | |   '-+-+-+->   560-599 btree w40 8             ...
  0223.0090: | | .-+           600-799 btree w200 9            ...
  021e.004c: | | | |     .->   600-639 btree w40 8             ...
             | | | | .---+->   640-679 btree w40 8             ...
             | | | | | .--->   680-719 btree w40 8             ...
             | | | | | | .->   720-759 btree w40 8             ...
             | | | '-+-+-+->   760-799 btree w40 8             ...
  0223.0090: +-+-+-+          800-1023 btree w224 10           ...
  021f.0298:       |     .->   800-839 btree w40 8             ...
                   |   .-+->   840-879 btree w40 8             ...
                   |   | .->   880-919 btree w40 8             ...
                   '---+-+->  920-1023 btree w104 9            ...

This tree renderer also replaces the adhoc tree rendere in dbgrbyd.py
for consistency.
2023-05-30 18:04:54 -05:00
Christopher Haster b67fcb0ee5 Added dbgmtree.py for debugging the littlefs metadata-tree
This builds on dbgrbyd.py and dbgbtree.py by allowing for quick
debugging of the littlefs mtree, which is a btree of rbyd pairs with a
few bells and whistles.

This also comes with a number of tweaks to dbgrbyd.py and dbgbtree.py,
mostly changing rbyd addresses to support some more mdir friendly
formats.

The syntax for rbyd addresses is starting to converge into a couple
common patterns, which is nice for quickly determining what type of
address you are looking at at a glance:

- 0x12         => An rbyd at block 0x12
- 0x12.34      => An rbyd at block 0x12 with trunk 0x34
- 0x{12,34}    => An rbyd at either block 0x12 or block 0x34 (an mdir)
- 0x{12,34}.56 => An rbyd at either block 0x12 or block 0x34 with trunk 0x56

These scripts have also been updated to support any number of blocks in
an rbyd address, for example 0x{12,34,56,78}. This is a bit of future
proofing. >2 blocks in mdirs may be explored in the future for the
increased redundancy.
2023-05-30 18:04:54 -05:00
Christopher Haster f7d4497b80 Added some simple mtree benchmarks
It's interesting to note the different performance characteristics of
purely CoW btrees vs our mutable mtree.

The main downside of our mtree is the need to fetch leaf mdirs. This
fetch is expensive, and can be avoided in CoW btrees by storing the
trunk in each branch's parent.

On the other hand, btrees need to propagate all changes upwards to the
root.

An interesting takeaway is that a sort of mdir-trunk cache may be a very
interesting optimization for relatively little RAM cost. This may be
something to explore in the future.
2023-05-30 18:04:48 -05:00
Christopher Haster 41c28952c5 Updated benches/bench_btree.toml with btree changes 2023-05-30 16:36:50 -05:00