Commit Graph

562 Commits

Author SHA1 Message Date
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 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 bac740a90e Reverted "source rbyd optional" and moved reloc/alloc into lfsr_mdir_compact_
This consolidates all mdir relocation and allocation logic into
lfsr_mdir_compact by using an extra mid hint to indicate if the mdir is
new and unallocated. When mid == -4, the mdir is NOT new, and the mid
should be taken from the mdir.

This deduplicates quite a bit of logic between mdir compaction and
unlining, though it is incompatible with the optional rbyd scheme
previously used to compact mroot 0x{0,1} during mroot extensions.

Though I'm not sure this is the best approach. We should probably look
at this again after things have more of a shape.
2023-05-30 16:36:37 -05:00
Christopher Haster cd2d54855e Added a number of tests over mdir relocations, fixed minor bugs
- lfsr_btree_isnull still used tag and not only weight for null trees
- relocation forgot the mid
- missed relocation when uninlining, though this fix should be cleaned up
- made revision count behavior a bit more consistent

Note that the new tests may be -Gnor exclusive, they rely quite a bit on
exactly when compaction happens...
2023-05-30 16:36:37 -05:00
Christopher Haster 7b1c35a99b Tweaked lfsr_mdir_compact_ to make source rbyd optional
This allows lfsr_mdir_compact_ to also cover the mroot extension commit.

The mroot located at 0x{0,1} is a bit unique in that it can never
relocated. Instead we "extend" the mroot chain by an additional mroot
that can relocate.

One nice thing is we can implement this by letting lfsr_mdir_commit_
perform a normal relocation, and then rewrite the 0x{0,1} mroot with
a pointer to the "relocated" mroot.

Though we have to take extra care to make sure this write doesn't
recursively trigger an additional relocate, which would never terminate.

Fortunately, this situation only happens when we are compacting the
0x{0,1} mroot. Which simplifies things a bit.
2023-05-30 16:36:37 -05:00
Christopher Haster 395eff49ad Changed mdir weight->0 to drop caches instead of introducing a temporary commit
This was actually a bug, and would have eventually been caught when we resume
power-loss testing.

In our current implementation of the mtree, we immediately drop mdirs
when their weight goes to zero, since at this point there's no route to
write new commits to the mdir. This was implemented by 1. writing out the
commit, and then 2. removing the mdir from the mtree if its weight is zero.

But this has a problem analogous to why we can't salvage failed compacts
during mdir split: If we allow a valid commit to be written to an mdir
before we update its position in the mtree, that commit becomes immediately
visible in the case of a power-loss.

This is a bit tricky to fix since we rely entirely on appending tags to
the on-disk rbyd to determine weight changes. We tried simulating weight
changes previously, but that was a mistake that created complexity.

The solution here is separate the appending of tags from the commit
finalization:

1. Append any pending tags.
2. If weight->0, drop caches, abort the commit.
3. Otherwise, write the checksum, finalizing the commit.

This has the new side-effect of intentionally leaving unfinalized commits
on-disk, but since we have no way to reclaim the erased bytes in these
mdirs, that is probably ok.
2023-05-30 16:36:37 -05:00
Christopher Haster 7877eeaa9d Restructured lfsr_mdir_commit into separate high/low-level implementations
lfsr_mdir_commit => lfsr_mdir_commit
                    |-> lfsr_mdir_commit_
                    '-> lfsr_mdir_compact_

The mess that was lfsr_mdir_commit was a growing problem. Flattening all
possible mdir operations into a single loop may have resulted in a
smaller code size, but at a significant cost to implementation
difficult, readability, bugs, etc.

This restructure splits the mdir commit logic into three components:

1. lfsr_mdir_compact_

   This handles the swapping of mdir blocks, revision counts, erasing, etc.

   lfsr_mdir_compact_ also accepts a range of ids, allowing it to be
   called directly for mdir splitting/uninlining.

   Actually, the biggest feature in lfsr_mdir_compact_, which is easy to
   overlook, is that is accepts two attr lists. This seems like a weird
   feature for an API, but keep in mind we have strict RAM limitations,
   so we can't really concatenate attr lists easily.

   There is only a single case we need two attr lists: When uninlining
   an mroot we need to include 1. any pending mroot attrs, and 2. the
   new mtree. But one case is enough to make attempted workarounds
   excessively complicated.

   Simply accepting two attr lists here resolves this.

2. lfsr_mdir_commit_

   This handles the low-level mdir commit logic: It tries to do a simple
   rbyd commit, and if that fails falls back to a compact/relocate loop.

   Perhaps surprisingly, lfsr_mdir_commit_ does not handle mdir splits.
   The exact behavior of mdir splits is context specific, so
   lfsr_mdir_commit_ simple errors if lfsr_rbyd_estimate indicates
   compaction will be unsuccessful.

   Less surprisingly, lfsr_mdir_commit_ does not handle any
   mtree/internal state updates. lfsr_mdir_commit_ is only concerned
   with the specific mdir struct provided.

3. lfsr_mdir_commit

   This ties together all of the mdir commit logic and provides the main
   mechanism by which the rest of the filesystem interacts with mdirs.

   lfsr_mdir_commit is mainly responsible for handling the side-effects
   of the low-level operations:

   - Propagating mtree/mroot updates caused by relocations/splits/drops
   - Updating the provided mdir struct correctly if it splits/relocates
     based on a rid hint
   - Updating the internally tracked mroot/mtree state on success
   - Updating any open mdirs on success (TODO)

   This is a complicated function, but most of that complexity can be
   captured in a large, but relatively simple, tree of if statements.
   Not great for code cost, but this may just be a necessity of the new
   mtree data-structure.

   This also includes the tail-recursive mroot propagation loop, which
   is an excellent example of how splitting the high/low-level logic
   helps separate context-specific logic.

This still needs work, but the significantly improved readability of
lfsr_mdir_commit provides much more confidence in this design.

This already has the strong advantage that the extra mdir copies make it
clear when exactly the higher-level mdir copies are updated. This gives
us much better confidence that errors will not render the mdir state
unusable, though may be coming with a RAM cost.
2023-05-30 16:33:20 -05:00
Christopher Haster ef4fb9d3d3 Added specific tests to cover complex mdir split/drop corner cases
Dropped the high-level "large entry" tests in exchange for these low-level
tests. The high-level tests accomplished the same thing, but worse and
less reliably.

Added some rough fixes (this whole code path needs to be rewritten).

Also made lfsr_rbyd_bisect a bit better behaved when dealing with a
small number of large entries. This was necessary for the split/drop
corner case tests since these rely on precise control of when mdirs
split.
2023-05-30 14:57:45 -05:00
Christopher Haster 6bc85375ea Added a very rough implementation of mdir drops
mdirs behave a bit differently than btree nodes here. When an mdir's
weight drops to zero, we eagerly drop the mdir. Unfortunately this
introduce a large number of conditions into lfsr_mdir_commit. Maybe
there's some different way to structure to code to avoid this...

Also expanded mtree tests to cover more corner cases, these are
desperately for any confidence that mdir drops work.
2023-05-30 14:57:19 -05:00
Christopher Haster ea28413eb2 Added a bit of fuzz testing over mtree splits
This isn't the greatest coverage as we don't have a verifiable simulation.
Simulating the splitting-bucket-tree that is the mtree is tricky.

So right now this mostly just checks there's no internal assert failures and
if we have the expected number of entries afterwards.
2023-05-30 14:55:56 -05:00
Christopher Haster 3625882343 Added separate lfsr_btree_lookupnext_/lfsr_btree_lookupnext
- lfsr_btree_lookupnext_ => gives you the underlying rbyd/rid, intended
  for btree-internal use.

- lfsr_btree_lookupnext => does not give you underlying rbyd/rid, used
  for general purpose lookups/iteration.

This is for consistency with other *_lookupnext functions, and
discourages use of the leaf rbyd/rid. These are sensitive to internal
btree state.
2023-05-30 14:54:33 -05:00
Christopher Haster 755789701f Adopted lfsr_data_t in btree push/update/split functions
This is mostly for consistency. It's unclear if we'll ever actually
use the on-disk lfsr_data_t representation here, since current thoughts
expect most btrees to only store pointers with in-device
representations.

It may be worth reverting this in the future.
2023-05-30 14:52:56 -05:00
Christopher Haster 99e9e18baa Moved heavy work of copying/filtering tags into compact/appendall
After implementing lfsr_btree_commit and lfsr_mdir_commit, a common
pattern emerged for all compact/split operations:

1. Copy over subrange of tags.
2. Apply pending tags in that subrange.

lfsr_rbyd_appendall and lfsr_rbyd_compact now provide these operations,
allowing for better code sharing across these two algorithms.

The only hiccup is vestigial names in btree commit, which require a flag
and some special handling.

Note that btree merge is a bit of a special case for now.
2023-05-30 14:52:05 -05:00
Christopher Haster abbcf58d07 Simplified btree merge estimate at the risk of failed merges
This trades off simpler estimation (and more flexibility for
experimenting with better compaction strategies) for the possibility of
failed merges that require cleanup.

Note, we still estimate if our merge fits post-compaction, this just
isn't reliable for knowing if the merge fits appended to the current
compaction, with in-flight attrs for both the in-flight commit and merge name.

Fortunately, since our estimate is conservative, we shouldn't see any
split<->merge oscillation.

---

It's also worth noting since the merge name isn't accounted for in our
sibling, it wasn't clear if our estimation was correct in the first
place.

This change avoids any issues that may cause.

One downside of using our compaction estimate as a heuristic to avoid
failed merges: The merge abort code path may be difficult to cover in tests.

We should make sure merge aborts don't go untested.
2023-05-30 14:47:29 -05:00
Christopher Haster a57e79bc68 Some cleanup, reverted merge of rbyd_estimate/isdegenerate
Unfortunately due to different early-exit conditions,
estimate/isdegenerate isn't trivially compatible. The previous, merged
implementation missed the opportunity to inline btrees with two large
entries undergoing compaction.

It's unlikely to hit this, but splitting these back into two separate
passes simplifies the code and avoids the potential for other bugs from
this combination of unrelated pieces of logic.

Keep in mind lfsr_rbyd_isdegenerate is cheap:
1. Only ran when compacting the root of a btree.
2. Cutoff is usually small, at the moment requires at most 2 ids, or
   2*2 rbyd lookups with the current btree implementation.
2023-05-30 14:47:01 -05:00
Christopher Haster 975a98b099 Renamed a few superblock-related things
- supermdir -> mroot
- supermagic -> magic
- superconfig -> config
2023-05-30 14:46:56 -05:00
Christopher Haster 7925f9f019 Some more mtree split/uninlining tests and fixes
Currently relying on lfsr_rbyd_append/appendattrs to inject extra
attributes during lfsr_mdir_commit, need to consider if this is really
the best solution. This probably results in more function calls than we
really need.
2023-05-30 14:44:18 -05:00
Christopher Haster 9b72406632 Implemented mtree uninlining and splitting
This is the first step towards a working mtree, though raises more
questions than it resolves.
2023-05-30 13:55:21 -05:00
Christopher Haster a3bfa3488f Adopted a different strategy for mdir split threshold estimation
This approach is simpler: fall back to using two passes if we split a
supermdir.

This trades off code complexity with runtime, but I think we really don't
care about the runtime here, since this operation should really only happen
once in a filesystem's entire lifetime.
2023-05-30 13:55:16 -05:00
Christopher Haster 06b04bda6b Working toward supermdir split, consolidated more logic into lfsr_rbyd_inthresh
This is tricky because of the number of corner-cases that can occur:

1. Our supermdir fits as is => compact normally.

2. Our supermdir does not fit, but it does if we separate the superattrs
   from file attrs => uninline, but don't split.

3. Our supermdir does not fit, and does not fit after separating the
   superattrs => uninline and split.
2023-05-30 13:53:59 -05:00
Christopher Haster f15add4374 Simplified rbyd compaction estimate
This trades of a simpler compaction estimate for a looser upper bound.
We now only lower the bound for:

- The number of alts per tag.
- The worst-case leb128 encoding assuming current block_size.

Since this worst case encoding only depends on the block_size, it can
also be precalculated and stored somewhere, though we're currently not
doing that.

On the plus side, this no longer varies depending on the rbyd's weight,
which could cause hard-to-detect issues for very large B-trees.
2023-05-30 13:44:08 -05:00
Christopher Haster beba584501 Implemented and adopted rbyd compaction estimates
Still needs work, but at least adopted optionally in the btree.

Ignoring the mdirs for now, which is a bit ironic, because the mdir
compaction is really what this feature is for. But this at least proves
the concept.

---

Unlike btrees, mdirs simply cannot perform the attempt-then-delete-half
strategy current performed by the btrees during compaction with a single
pcache. This is because the moment we finish the commit with the delete,
it becomes visible to the filesystem. We can't abort the commit temporarily
to deal with the other half of the split, because our pcache is in use.

So, instead, the idea is to estimate the compacted rbyd size before
compacting, using conservative (but tight!) estimates for various leb128
encoded parts of the metadata.

And if we adopt this strategy for mdirs, we should probably adopt it in
the btrees for better code sharing.

A couple benefits:

- Major reduction in progs during split, since we don't write out tags
  just to delete them.

- btree merge can actually consider both siblings now.

- Not needing to weave the split/merge logic around compact offers a
  better route for code deduplication.

- mdir compact will actually work, that's generally a good thing.

And a couple downsides:

- This estimate is complex, meaning more code-cost and a bigger surface
  area for bugs.

- This results in a minor performance hit for the common compact case,
  since we need to read the rbyd being compacted twice instead of once.
2023-05-30 13:41:41 -05:00
Christopher Haster 738eb52159 Tweaked tag encoding/naming for btrees/branches
LFSR_TAG_BNAME => LFSR_TAG_BRANCH
LFSR_TAG_BRANCH => LFSR_TAG_BTREE

Maybe this will be a problem in the future if our branch structure is
not the same as a standalone btree, but I don't really see that
happening.
2023-05-30 13:41:28 -05:00
Christopher Haster 4e3dca0b81 Partial implementation of a rudimentary mtree
This became surprisingly tricky.

The main issue is knowing when to split mdirs, and how to determine
this without wasting erase cycles.

Unlike splitting btree nodes, we can't salvage failed compacts here. As
soon as the salvage commit is written to disk, the commit becomes immediately
visibile to the filesystem because it still exists in the mtree. This is
a problem if we lose power.

We're likely going to need to implement rbyd estimates. This is
something I hoped to avoid because it brings in quite a bit of
complexity and might lead to an annoying amount of storage waste since
our estimates will need to be conservative to avoid unrecoverable
situations.

---

Also changed the on-disk btree/branch struct to store a copy of the weight.

This was already required for the root of the btree, requiring the
weight to be stored in every btree pointer allows better code
deduplication at the cost of some redundancy on btree branches, where
the weight is already implied by the rbyd structure.

This weight is usually a single byte for most branches anyways.

This may be worth revisiting at some point to see if there's any other
unexpected tradeoffs.
2023-05-30 13:28:35 -05:00
Christopher Haster 85ebdd0881 Reintroduced Brent's algorithm for cycle detection in lfsr_mount 2023-05-30 13:28:07 -05:00
Christopher Haster 6236f460a4 Added rough draft of the rest of superblock parsing 2023-05-30 13:27:05 -05:00
Christopher Haster eacf5895c6 Reorganized lfsr_mount/lfsr_format to better reuse code
Added lfsr_mountinited/lfsr_formatinited, mostly so lfsr_formatinited
can just call lfsr_mountinited for its mount-check.

This also leads to a nice consolidation of the cleanup-on-error part of
lfsr_mount/lfsr_format.
2023-05-30 13:26:14 -05:00
Christopher Haster 038f6b4c4b Adopted more pedantic names for lookupnext/lookup
The exact behavior of lfsr_rbyd_lookup is a bit unusual, and has already
resulted in a few mistakes. To make this more clear at a glance, names
have been changed and a few more helper functions added.

The new names and expected behavior:

- *_lookupnext - lookup the smallest id/tag less than or equal to the
  requested id/tag, returns LFS_ERR_NOENT if id/tag is greater than all
  ids/tags in the data structure.

- *_lookup - lookup the exact id/tag, returns LFS_ERR_NOENT if id/tag
  is not in the data structure.

These have been adopted in all current data structures: rbyd/btree/mdir

- lfsr_rbyd_lookup => lfsr_rbyd_lookupnext
- lfsr_btree_lookup => lfsr_btree_lookupnext
- lfsr_btree_namelookup => lfsr_btree_namelookupnext
- lfsr_mdir_lookup => lfsr_mdir_lookupnext

Note no lfsr_btree_namelookup is added, this is a more complicated
than lfsr_btree_lookup (we need to cmp the name on-disk for equality)
and also probably not needed.
2023-05-30 13:25:40 -05:00
Christopher Haster c83d8b7abc Added lfsr_data_add for more lfsr_data_t manipulation
Also fixed an internal (currently unreachable) bug in lfs_bd_cmp where
the hint could underflow if zero.
2023-05-30 13:25:29 -05:00
Christopher Haster 59552e8f1e Merged lfsr_data_progcsum into lfsr_data_prog
Considering that _most_ progs in littlefs needs to be checksummed for
future consistency checks, it's not really worth it to have a separate
non-checksumming function. Especially when you consider the fanout of
the prog extensions for different types.

A similar problem, sort of unresolved, is what to do with all the
validating functions. I guess we'll cross that bridge when we need to.
2023-05-30 13:24:40 -05:00
Christopher Haster 21bd43fa0c Implemented a set of convenience lfsr_data_read* functions
This makes lfsr_data_t a more powerful primitive in littlefs.

- Implemented a set of convenience lfsr_data_read* functions for
  easier reading from muxed on-disk/in-ram data.

- Adoped these functions and lfsr_data_t in *_fromdisk functions
  as well as low-level rbyd operations.

- Changed leb128 parsing to rely on returned limits instead of
  pre-initialized 0xffs to detect truncated leb128s.

- Prefer incremental reading+parsing in more places as a side-effect,
  though we don't really have a measurement if this is a net benefit or
  cost code/ram wise.
2023-05-30 13:23:48 -05:00
Christopher Haster 283b8e84c4 Some more experimental lfsr_bd_ functions
These functions offer more than previous internal bd functions, the idea being
that the more functionality we can move into this layer, the less
functionality gets duplicated across dependent functions.

- lfsr_bd_read - caching read with hint
- lfsr_bd_readcsum - read with checksum
- lfsr_bd_csum - calculate checksum, don't read data
- lfsr_bd_cmp - compare data against a buffer
- lfsr_bd_prog - caching prog
- lfsr_bd_progcsum - prog with checksum
- lfsr_bd_sync - complete an in-flight prog
- lfsr_bd_progvalidate - prog with read-back validation
- lfsr_bd_progcsumvalidate - prog with checksum and read-back validation
- lfsr_bd_syncvalidate - complete an in-flight prog with read-back validation
- lfsr_bd_erase - erase a block

- lfsr_bd_readtag - read a tag with optional checksum
- lfsr_bd_progtag - prog a tag with checksum

Of course these are all susceptible to change.
2023-05-30 13:17:01 -05:00
Christopher Haster 70a3a2b16e Rough implementation of lfsr_format/mount/unmount
This work already indicates we need more data-related helper
functions. We shouldn't need this many function calls to do "simple"
operations such as fetch the superconfig if it exists.
2023-05-30 13:16:03 -05:00
Christopher Haster 3a470f9d73 Enabled btree tests on all geometries, fixed some revealed bugs
The main issue with the btree tests cross-geometry is the number of ways
large btrees can run out of memory without garbage collection.

- Large progs => A lot of padding on non-compacting commits
- Small blocks => Deeper trees and more compacts

Rather than figure out every precondition, I've just added code that
ignores out-of-space errors.

As a plus this is now also testing that errors don't corrupt the btree
being modified.

Bugs found:

- Thanks to lazy merges, it's possible for an in-btree rbyd weight to
  equal the total btree weight even when it's not the child of the root
  of the btree.

  The behavior is the same (collapse all degenerate parent), but the
  assert that we were the root's child is incorrect.

- Thanks again to lazy merges, it's possible to merge siblings where one
  of the blocks has no entries. Attempting to reintroduce the split name
  in this case can lead to LFS_ERR_NOENT issues.

  Fortunately we can simply skip the reintroduction of the split name in
  this case.

Also added more asserts for LFS_ERR_RANGE in lfsr_btree_commit. This is
still a rather fragile part of the algorithm so the asserts here help
identify when the pending attribute size is the problem.
2023-04-16 01:43:45 -05:00
Christopher Haster da8fa4b133 Made name entries in lfsr_btree_split optional, more consistent attrs
This was a correct-but-inefficient bug where lfsr_btree_split
unconditionally added name entries, but if we don't have a name writing
those entries just wastes storage/lookup cost.

Also cleaned up lfsr_btree_commit attr usage to be a bit more
consistent.

Unfortunately some rough measurements around the ternary selection of
attributes shows it's a bit costly, perhaps because gcc isn't that smart
about optimizing compound literals. It may be worth seeing if there's a
more efficient way to implement these in the future, but hey, at least
this implementation leads to concise source code.
2023-04-16 01:30:29 -05:00
Christopher Haster bdfe66aab2 Cleanup around lfsr_btree_commit
- Consistent handling of missing branches - now asserts

- Consistent short-circuiting of name-less branches - we can always pull
  these off in one lookup

- Skip validating already-fetched rbyd - this only affects the root
  rbyd, but as the most heavily accessed rbyd in the tree this is a nice
  optimization. In practice root rbyds should be validated exactly once.

- Dropped accidental redundant check of some btree merge conditions
2023-04-14 02:44:36 -05:00
Christopher Haster 4662e93c29 Implemented a slightly improved bisect algorithm for btree split
Emphasis on slightly.

Preliminary benchmarking already shows btree split as a significant spike
and main read cost of lfsr_btree_commit, so any savings here are
valuable.

Unfortunately the problem of evenly bisecting an rbyd can be reduced to
finding the mid-point in an array of arbitrary weights, which is O(m)
best case (and O(m log(m)) over our rbyds).

But at the time we realize compact will fail, we have already traversed
at least 1/2 of the tags in the rbyd. If we also keep track of
cumulative dsize, we can in theory bisect the rbyd by traversing only
another 1/2 of the tags in the rbyd.

The implementation here does this by:

1. Keep track of the lower_dsize as we compact.
2. If we split, first traverse backwards through ids keeping track
   of the upper_dsize.
3. Steal dsize from lower_dsize in the case it's over-committed.
4. Stop when both upper_dsize and lower_dsize are more-or-less equal.

So for example:

  an rbyd needing compaction:
  [a b c d e f g h i j k l _ _ _ _]

  compact to 1/2 the rbyd, oh no it doesn't fit, we need to split:
  [a b c d e f g h i j k l _ _ _ _]
   -------------->

  traverse from the end to find the mid-point:
  [a b c d e f g h i j k l _ _ _ _]
   -------------->
               <----------

Best case, a barely overflowing rbyd, we end up traversing m*3/4 tags:

  [a b c d e f g h _ _ _ _ _ _ _ _]
   -------------->
           <------

Worst case, a full rbyd, we end up traversing m*1 tags:

  [a b c d e f g h i j k l m n o p]
   -------------->
                   <--------------
2023-04-14 02:24:21 -05:00
Christopher Haster f878f3f03c Investigated weights in rbyd's fetch, ended up with just cleanup
I was hopeful it would be possible to remove the weight lookup in
lfsr_btree_namelookup. We do a linear search during fetch to find the
name and tag, so finding the weight as well for free looked promising.

Unfortunately, it seems to be impossible to reliably find the weight.

Consider what happens when we match an id that is later deleted. We know
the new id should be id-1, but we don't have enough information to
determine the new weight.

So just ended up adding a comment explaining the limitation and cleaning
up the logic in lfsr_rbyd_fetch a bit.
2023-04-14 02:24:14 -05:00
Christopher Haster cfaeeaa690 Adopted lfsr_data_t in more places, mainly the low-level lookup functions
lfsr_data_t is proving itself to be a powerful abstraction.

As a plus, the reduction from two out-pointers to one out-pointer in
lookup functions (off+size vs lfsr_data_t) may actually save some code
size in places.

Also adopted the ones-complement sort of conditional size field similar
to the weight field in lfsr_btree_t.
2023-04-14 02:21:46 -05:00
Christopher Haster f35061c7eb Implemented deferred btree inlining via cutoff parameter
This finally provides a solution for deferred B-tree inlining without
needing to evaluate attrs.

Deferred inlining is the idea that instead of inlining B-trees as soon
as the number of entries drops to either 1 or 0, we wait until a
compaction occurs to inline a B-tree. This accomplishes a few things:

1. Limits any extra reads for conditions to compaction time.

2. Avoids wasting erased bytes if we drop to 1 or 0 entries only
   temporarily.

3. Avoids excessive erase costs if we oscillate between ~1 and ~2
   entries.

Unfortunately after moving away from evaluating attrs, deferred inlining
became deceptively tricky.

In the current, non-evaluating-attr implementation, our btree commits
always lag one commit behind. When we compact, we first compact
everything currently in the rbyd, and then append any pending attr.
Never needing to evaluate the attrs removes a big chunk of logic as long
as we can assert that the largest attr set fits after compaction.

But this lagging of commits presents a problem for deferred inlining, if
we detect an inlinable tree during compaction, we can't be sure it's
_actually_ inlinable until we evaluate our attr. Which we really don't
want to do.

The solution here is to move the problem up a level. Instead of trying
to determine when to inline purely from the provided attr, we require
higher-level functions to provide this info in the form of a "cutoff".
Where, if compaction results in fewer entries than this cutoff, the
higher-level function can instead inline.

This effectively allows the higher-level functions to intercept
unnecessary compactions that can be inlined.

So far this solution seems to work quite well, with the added plus of
consolidating the corner cases around inlined/inlining btrees in these
higher-level functions.

---

Note that this has the peculiar side-effect of allowing zero-weight,
non-inlined B-trees. Our previous internal B-tree struct using the sign
of an integer to determine inline-ness, this was changed to use just the
sign-bit for the condition as a sort of ones-complement width field.

I think this sort of encoding may actually bit a tiny bit more
efficient. I was poking around with thumb code and noticed there is no
actual "abs" instruction, with gcc outputing an "it" sequence. But there
is a cheap bit-clear "bic" instruction.
2023-04-14 01:42:04 -05:00
Christopher Haster 5a5598930e Some cleanup of the compact route in lfsr_btree_commit
Mostly just moving the rbyd commit/compact operations into the same code
path so they can share the same tail-recursive propagation of their
branch encoding.

Also tried to make variable names in lfsr_btree_commit a bit more consistent.
2023-04-14 01:38:11 -05:00
Christopher Haster 47e4f719f5 Cleanup, fixed inconsistent names, moved btree attr allocation up
- len => size - these all refer to byte-arrays
- buf => buffer - this doesn't matter but buffer is currently used more
- delta => d - we use delta for weight deltas, gstate deltas, using a
  slightly different name (if somehow even less descriptive) for byte
  offset-offsets helps avoid name collisions a little bit

The storage changes in btree operations should've probably been a
separate commit but got wrapped up in these changes. Now the high-level
btree operations are responsible to the attr storage for all internal
btree commits, as defined by LFSR_BTREE_SCRATCHATTRS.

This leads to slightly less total RAM usage, since it allows the
low-level btree operations to cannibilize the attrs of the high-level
btree operations as a part of its unrolled-tail-recursive
implementation.

This also includes some other cleanup such as removing old commented out
parts.
2023-04-14 01:37:56 -05:00
Christopher Haster 774ae676e4 Flipped layout of fcrcs to match B-tree branches
This may seem more complicated to decode, we can't assume crcs start
at the beginning of the data, but this layout of putting the crcs at the
end has the benefit of allowing the size of the crc to be unknown in
certain cases.

The is a bit of optimistic future proofing for the case where we may
support different crc widths.
2023-04-14 01:37:47 -05:00
Christopher Haster a511696bad Added ability to bypass rbyd fetch during B-tree lookups
This is an absurd optimization that stems from the observation that the
branch encoding for the inner-rbyds in a B-tree is enough information to
jump directly to the trunk of the rbyd without needing an lfsr_rbyd_fetch.

This results in a pretty ridiculous performance jump from O(m log_m(n/m))
to O(log(m) log_m(n/m)).

If the complexity analysis isn't impressive enough, look at some rough
benchmarking of read operations for 4KiB-block, 1K-entry B-trees:

   12KiB ^     ::  :. :: .: .: :. : .: :. : : .. : : . : .: : : :
         |    .:: .::.::.:: ::.::::::::::::.::::::::.::::::::::::.
         |    : :::':: ::'::'::':: :' :':: :'::::::::': ::::::': :
before   |  ::: ::' :' :' :: :' '' '  ' '' : : : '' ' ' '
         | :::            ''
         |:
      0B :'------------------------------------------------------>

  .17KiB ^               ............:::::::::::::::::::::::::::::
         |   .   .....:::::'''''''''  '         '          '
         |  .::::::::::::
after    |  :':''
         |.::
         .:'
      0B :------------------------------------------------------->
         0                                                      1K

In order for this to work, the branch encoding did need to be tweaked
slightly. Before it stored block+off, now it stores block+trunk where
"trunk" is the offset of the entry point into the rbyd tree. Both off
and trunk are enough info to know when to stop fetching, if necessary,
but trunk allows lookups to jump directly into the branches rbyd tree
without a fetch.

With the change to trunk, lfsr_rbyd_fetch has also be extended to allow
fetching of any internal trunks, not just the last trunk in the commit.
This is very useful for dbgrbyd.py, but doesn't currently have a use in
littlefs itself. But it's at least valuable to have the feature available
in case it does become useful.

Note that two cases still requires the slower O(m log_m(n/m)) lookup
with lfsr_rbyd_fetch:

1. Name lookups, since we currently use a linear-search O(m) to find names.

2. Validating B-tree rbyd's, which requires a linear fetch O(m) to
   validate the checksums. We will need to do this at least once
   after mount.

It's also worth mentioning this will likely have a large impact on B-tree
traversal speed. Which is huge as I am expecting B-tree traversal to be
the main bottleneck once garbage-collection (or its replacement) is
involved.
2023-04-14 00:51:34 -05:00
Christopher Haster ed8d8c0c24 Folded rbyd.erased into rbyd.off=block_size, some rbyd cleanup
- The erased flag in lfsr_rbyd_t uses only a single bit, which is
  wasteful for a heavily used struct in littlefs. We can use
  rbyd.off=block_size to indicate the same state for free. Note that
  when rbyd.off=block_size, we must treat rbyd as unerased anyways.

- Improved state handling in rbyd_append/commit when an error occurs.
  I will be trying to make better use of cleanup gotos to make these
  functions less unpredictable when an error occurs. Hopefully the state
  of littlefs after an error can be well-defined in the future.

- Fixed sign-mismatch warnings in asserts when compiled outside of the
  test runner.
2023-04-14 00:51:19 -05:00
Christopher Haster 7eb0c4763a Reversed LFSR_ATTR id/tag argument order
I've been wanting to make this change for a while now (tag,id => id,tag).
The id,tag order matches the common lexicographic order used for sorting
tuples. Sorting tag,id tuples by their id first is less common.

The reason for this order in the codebase is because all attrs on disk
start with their tag first, since its decoding determines the purpose of
the id field (keep in mind this includes other non-tree tags such as
crcs, alts, etc). But with the move to storing weights instead of tags
on disk, this gives us a clear point to switch from tag,w to id,tag
ordering.

I may be thinking to much about this, but it does affect a significant
amount of the codebase.
2023-04-14 00:43:33 -05:00
Christopher Haster a463d6f106 Changed attr list implementation back to an array
I keep wanting this to use a linked-list, since I think there's
potentially some interesting use with lower layers cheaply prepending
attributes to attribute lists from upper layers. (terminating at the
user-provided custom attributes, for example). But this never really
works out.

In this case, the amount of in-place editing in B-trees just makes
maintaining the next pointers just not worth the extra code cost. And
it's likely measurements will show what was found in the original
version of v2: the RAM/code cost of next pointers outweighs any benefits
potentially gained from prepending attributes for free.

In practice, we can't really just prepend custom attributes, as this
would expose the internal lfs_attr_t struct and tag encoding to the
public API.

And you can always have in-device-only tags that are handled specially
to enable a limited form of this attribute list extension. This is how
custom attributes are currently implemented.
2023-04-14 00:42:38 -05:00