Commit Graph

503 Commits

Author SHA1 Message Date
e5cd2904ee Tweaked always-follow alts to follow even for 0 tags
Changed always-follow alts that we use to terminated grow/shrink/remove
operations to use `altle 0xfff0` instead of `altgt 0`.

`altgt 0` gets the job done as long as you make sure tag 0 never ends up
in an rbyd query. But this kept showing up as a problem, and recent
debugging revealed some erronous 0 tag lookups created vestigial alt
pointers (not necessarily a problem, but space-wasting).

Since we moved to a strict 16-bit tag, making these `altle 0xfff0`
doesn't really have a downside, and means we can expect rbyd lookups
around 0 to behave how one would normally expect.

As a (very minor) plus, the value zero usually has special encodings in
instruction sets, so being able to use it for rbyd_lookups offers a
(very minor) code size saving.

---

Sidenote: The reasons altle/altgt is how it is and asymmetric:

1. Flipping these alts is a single bit-flip, which only happens if they
   are asymmetric (only one includes the equal case).

2. Our branches are biased to prefer the larger tag. This makes
   traversal trivial. It might be possible to make this still work with
   altlt/altge, but would require some increments/decrements, which
   might cause problems with boundary conditions around the 16-bit tag
   limit.
2023-03-27 02:32:08 -05:00
8f26b68af2 Derived grows/shrinks from rbyd trunk, no longer needing explicit tags
I only recently noticed there is enough information in each rbyd trunk
to infer the effective grow/shrinks. This has a number of benefits:

- Cleans up the tag encoding a bit, no longer expecting tag size to
  sometimes contain a weight (though this could've been fixed other
  ways).

  0x6 in the lower nibble now reserved exclusively for in-device tags.

- grow/shrinks can be implicit to any tag. Will attempt to leverage this
  in the future.

- The weight of an rbyd can no longer go out-of-sync with itself. While
  this _shouldn't_ happen normally, if it does I imagine it'd be very
  hard to debug.

  Now, there is only one source of knowledge about the weight of the
  rbyd: The most recent set of alt-pointers.

Note that remove/unreachable tags now behave _very_ differently when it
comes to weight calculation, remove tags require the tree to make the
tag unreachable. This is a tradeoff for the above.
2023-03-27 01:45:34 -05:00
546fff77fb Adopted full le16 tags instead of 14-bit leb128 tags
The main motivation for this was issues fitting a good tag encoding into
14-bits. The extra 2-bits (though really only 1 bit was needed) from
making this not a leb encoding opens up the space from 3 suptypes to
15 suptypes, which is nothing to shake a stick at.

The main downsides:
1. We can't rely on leb encoding for effectively-infinite extensions.
2. We can't shorten small tags (crcs, grows, shrinks) to one byte.

For 1., extending the leb encoding beyond 14-bits is already
unpalatable, because it would increase RAM costs in the tag
encoder/decoder,` which must assume a worst-case tag size, and would likely
add storage cost to every alt pointer, more on this in the next section.

The current encoding is quite generous, so I think it is unlikely we
will exceed the 16-bit encoding space. But even if we do, it's possible
to use a spare bit for an "extended" set of tags in the future.

As for 2., the lack of compression is a downside, but I've realized the
only tags that really matter storage-wise are the alt pointers. In any
rbyds there will be roughly O(m log m) alt pointers, but at most O(m) of
any other tags. What this means is that the encoding of any other tag is
in the noise of the encoding of our alt pointers.

Our alt pointers are already pretty densely packed. But because the
sparse key part of alt-pointers are stored as-is, the worst-case
encoding of in-tree tags likely ends up as the encoding of our
alt-pointers. So going up to 3-byte tags adds a surprisingly large
storage cost.

As a minor plus, le16s should be slightly cheaper to encode/decode. It
should also be slightly easier to debug tags on-disk.

  tag encoding:
                     TTTTtttt ttttTTTv
                        ^--------^--^^- 4+3-bit suptype
                                 '---|- 8-bit subtype
                                     '- valid bit
  iiii iiiiiii iiiiiii iiiiiii iiiiiii
                                     ^- m-bit id/weight
  llll lllllll lllllll lllllll lllllll
                                     ^- m-bit length/jump

Also renamed the "mk" tags, since they no longer have special behavior
outside of providing names for entries:
- LFSR_TAG_MK       => LFSR_TAG_NAME
- LFSR_TAG_MKBRANCH => LFSR_TAG_BNAME
- LFSR_TAG_MKREG    => LFSR_TAG_REG
- LFSR_TAG_MKDIR    => LFSR_TAG_DIR
2023-03-25 14:36:29 -05:00
9ceaca372a In B-tree commit, moved pending attrs before merge attempts
I'm still not sure this is the right place for this, but it does
simplify pending attr calculations during merge and deduplicates
two instances of writing pending attrs, at the cost of needing to
track an additional rbyd weight during merge.

Going to roll with this for now, the B-tree merge code needs to be
cleaned up anyways, maybe it's possible to simplify the state we need to
track.

Another side-effect is this makes our B-trees slightly less aggressive
at merging. I have no idea if this is a good or bad thing.
2023-03-21 14:01:33 -05:00
a43d7c4249 Implemented a number of minor B-tree optimizations
- Added cleanup of vestigial names on inner branches.

- Avoided extra struct lookups when there is no name on a branch.

- Simplified merge name lookup a little bit, probably at some runtime
  cost but merge is an exceptional operation.

- Moved commit before split lookup, in theory this should help stack
  shrink-wrapping slightly, in practice it's probably a premature
  optimization.

- Removed debugging asserts/printfs.
2023-03-21 13:30:02 -05:00
23956cc25b Added sparse B-tree name tests, fixed weight-calculation bug during merge
More proof the tests are working.

This bug was in the code that does an extra lookup for the was-split entry
during merge, so we make sure we have the right id to attach the split
name to. Humorously, this code was already set up correctly, the
"split_id" just wasn't actually used. Unfortunately since
lfsr_rbyd_lookup uses out-pointers to return multiple things the
compiler couldn't detect the unused variable.
2023-03-21 13:26:58 -05:00
89d5a5ef80 Working implementation of B-tree name split/lookup with vestigial names
B-trees with names are now working, though this required a number of
changes to the B-tree layout:

1. B-tree no-longer require name entries (LFSR_TAG_MK) on each branch.
   This is a nice optimization to the design, since these name entries
   just waste space in purely weight-based B-trees, which are probably
   going to be most B-trees in the filesystem.

   If a name entry is missing, the struct entry, which is required,
   should have the effective weight of the entry.

   The first entry in every rbyd block is expected to be have no name
   entry, since this is the default path for B-tree lookups.

2. The first entry in every rbyd block _may_ have a name entry, which
   is ignored. I'm calling these "vestigial names" to make them sound
   cooler than they actually are.

   These vestigial names show up in a couple complicated B-tree
   operations:

   - During B-tree split, since pending attributes are calculated before
     the split, we need to play out pending attributes into the rbyd
     before deciding what name becomes the name of entry in the parent.
     This creates a vestigial name which we _could_ immediately remove,
     but the remove adds additional size to the must-fit split operation

   - During B-tree pop/merge, if we remove the leading no-name entry,
     the second, named entry becomes the leading entry. This creates a
     vestigial name that _looks_ easy enough to remove when making the
     pending attributes for pop/merge, but turns out the be surprisingly
     tricky if the parent undergoes a split/merge at the same time.

   It may be possible to remove all these vestigial names proactively,
   but this adds additional rbyd lookups to figure out the exact tag to
   remove, complicates things in a fragile way, and doesn't actually
   reduce storage costs until the rbyd is compacted.

   The main downside is that these B-trees may be a bit more confusing
   to debug.
2023-03-21 12:59:46 -05:00
7a0842295c Partial implementation of B-tree name split/lookup
Name lookup brings back the O(m') scan-during-fetch approach of the
previous metadata layout. Since our rbyd trees map id+attr pairs and not
actual names, this beats the alternative O(m log(m)) scan of the tree.

Though tree searching does only include the current attributes, where as
scanning during fetch needs to also look at outdated attributes. Which
may make the winner less obvious depending on how we find the rbyd. But
being able to do the search in the same pass as fetch is an extra plus.

---

What turned out to be surprisingly complicated was the propagation of
names during B-tree splits and merges. The on-disk reference,
lfsr_data_t, does most of the heavy lifting here, but there's just a lot
of corner cases to consider.

At the moment this isn't working due to outdated names on the leading
entries of the rbyds, but to fix this bigger changes to the B-tree
layout may be needed.
2023-03-21 12:50:38 -05:00
0756c0acf2 Cleanup of code that is no longer going to be used
- lfsr_rbyd_predictedlookup, the new B-tree approach means we hopefully
  won't need this anymore. Worst case this remove can be reverted.

- LFSR_TAG_FROM - this will likely come back, but needs to be
  rewrittern.
2023-03-19 01:21:31 -05:00
15e27f92af Implemented lfsr_btree_split, a shortcut for pop+push+push
Another straightforward exercise of making sure the pending attributes
are setup correctly.

If you think this isn't worth its own function, consider how much
overhead the 3x commits for pop+push+push would add, especially for
large-prog devices.

Worst case this can be dropped in the future.
2023-03-17 15:04:21 -05:00
dd5af724fd Added generalize fuzz testing for B-trees, fixed single-child bug
A single child is just another condition to watch out for during B-tree
merge, since a single-child obviously can't have a sibling.

This is a good safety to have, but I was surprised this can happen. But
it turns out to be quite easy since our rbyds defer the B-tree
operations until compaction. A merge down to a single child won't
propagate the merge until the parent compacts.
2023-03-17 14:54:59 -05:00
8732904ef6 Implemented lfsr_btree_pop and btree merges
B-tree remove/merge is the most annoying part of B-trees.

The implementation here follows the same ideas implemented in push/split:
1. Defer splits/merges until compaction.
2. Assume our split/merge will succeed and play it out into the rbyd.
3. On the first sign of failure, revert any unnecessary changes by
   appending deletes.
4. Do all of this in a single commit to avoid issues with single-prog
   blocks.

Mapping this onto B-tree merge, the condition that triggers merge is
when our rbyd is <1/4 the block_size after compaction, and the condition
that aborts a merge is when our rbyd is >1/2 the block_size, since that
would trigger a split on a later compact.

Weaving this into lfsr_btree_commit is a bit subtle, but relatively
straightforward all things considered.

One downside is it's not physically possible to try merging with both
siblings, so we have to choose just one to attempt a merge. We handle
the corner case of merging the last sibling in a block explicitly, and
in theory the other sibling will eventually trigger a merge during its
own compaction.

Extra annoying are the corner cases with merges in the root rbyd that
make the root rbyd degenerate. We really should avoid a compaction in
this case, as otherwise we would erase a block that we immediately
inline at a significant cost. However determining if our root rbyd is
degenerate is tricky. We can determine a degenerate root with children
by checking if our rbyd's weight matches the B-tree's weight when we
merge. But determining a degenerate root that is a leaf requires
manually looking up both children in lfsr_btree_pop to see if they will
result in a degenerate root. Ugh.

On the bright side, this does all seem to be working now. Which
completes the last of the core B-tree algorithms.
2023-03-17 14:29:02 -05:00
a897b875d3 Implemented lfsr_btree_update and added more tests
This was a rather simple exercise. lfsr_btree_commit does most of the
work already, so all this needed was setting up the pending attributes
correctly.

Also:
- Tweaked dbgrbyd.py's tree rendering to match dbgbtree.py's.
- Added a print to each B-tree test to help find the resulting B-tree
  when debugging.
2023-03-17 14:20:40 -05:00
eb6b5332a0 Fixed a nasty bug in rbyd where shrinking the last id can leave bad alts
This was particularly nasty to track down, the bad alts left in this way
are zero-weight, zero-tag alts that point out of the bounds of the rbyd.
This creates an immovable-object/unstoppable-force situation since the
alt that will never be followed should always be followed. This ended up
creating a confusing issue later since grows can follow this alt and
cause the alt state to fall apart.

The solution is to check for shrink leaves that drop to weight zero and
prune them. This has a side-effect of nicely handling over-sized
shrinks, though these shouldn't happen anyways and are being asserted
on.

Because I really, really don't want a regression, I've added a specific
test for this, though the minimal reproducible case is a bit complex.
The state of the rbyd is rather sensitive and it's not fully clear to me
what ultimately triggers the breakdown of the rbyd tree.

Also added a slightly better check for grow/shrink tags on altle leaves.
I don't know if this is strictly required but I know it keeps me sane.
2023-03-17 14:20:40 -05:00
ce18bec66d Added sparse btree tests, fixed found bugs (grow/shrink bisect)
- After a B-tree split, when we're append pending attributes, it's
  possible for the id chosen for bisection to be itself modified by
  pending grows/shrinks. This needs to be accounted for in the two
  passes for the two children.

But this means our tests are working.
2023-03-17 14:20:09 -05:00
0a3c6b39c1 Implement generalized btree push, note the boundary conditions when id=weight
This really just required care around calculating the expected B-tree id
and rbyd id (which are different!).

B-tree append, aka B-tree push with id=weight, is actually the outlier.
We need a B-tree id that can identify the rbyd we're appending to, but
this id itself doesn't exist in the tree yet, which can be a bit tricky.
2023-03-17 14:20:09 -05:00
d6a2666614 I'm dumb and btree split w/o predicted actually doesn't need two pcaches
In B-tree split we turn one rbyd into two by comparing each tag to an id we
as a mid-point.

I first implemented this by writing both children in parallel, which is
efficient, but requires two pcaches for low-level page alignment issues.
However we really don't have to write these in parallel. We can just write
each child sequentially by making two passes of the original rbyd.

---

With this fix, the rewrite of B-tree splitting without predicted rbyd
sizes now works.

The idea is, instead of predicting the rbyd size to decide whether or not
to split, assume we always fit, perform a normal compaction, and if it
turns out we don't fit, make a split, writing rm tags as necessary to revert
any ids that don't belong in the first child.

The neat thing about this is we can use low-level, uncommitting rbyd appends
to do all of this in a single commit, avoiding issues with single-prog
blocks.

This can waste some progs, up to 1/4 of a block during a B-tree split.
However, it removes the main need for the rbyd prediction operations,
which are complicated, error prone, and concerning. B-tree
removes/merges still need an implementation, but this may mean that we
can let the on-disk rbyd data-structure be the only source of knowledge
about tags, which is great for ensuring consistent behavior.

This does mean we don't predict rbyd changes during compaction. Any pending
attributes just get appended to the rbyd after compaction, so size-changing
operations such as removes can lead to splits that could be avoided. But
I think these cases can lead to unnecessary splits anyways depending on
when compaction occurs, so I'm not sure it's really an issue. But I can
always be wrong about that.
2023-03-17 14:20:09 -05:00
0b619df6ca Partial rewrite of btree split without prediction, needs two pcaches
I didn't realize until testing, this approach requires two pcaches. This
completely breaks assumptions in the caching layer and requiring an
additional cache is probably too much of a cost to be acceptable. So
back to the drawing board.
2023-03-17 14:20:09 -05:00
88e3db98a9 Rough implementation of btree append
This involves many, many hacks, but is enough to test the concept
and start looking at how it interacts with different block sizes.

Note only append (lfsr_btree_push on the end) is implemented, and it
makes some assumption about how the ids can interact when splitting
rbyds.
2023-03-17 14:20:09 -05:00
361dfb0625 Added more rbyd fuzz testing
- Added test_rbyd_fuzz_mixed/test_rbyd_fuzz_sparse
- Added test_rbyd_unwritten_mixed_fuzz/test_rbyd_unwritten_sparse_fuzz
- Also renamed "random" tests to "fuzz", this describes their purpose a
  bit better

These were a bit tricky to add since they need to simulate rbyd weights,
but they should give significant coverage over complicated rbyd corner
cases I may have not thought about.

Also fixed a miscalculation in lfsr_rbyd_pendinglookup when finding a
id that grew. Finding this bug is a good sign these tests are working.
2023-03-17 14:20:09 -05:00
7f16c6e473 Implementation of lfsr_rbyd_pendinglookup in one pass
This ends up surprisingly tricky with sparse ids. I feel like I'm missing
a simpler solution, but this at least proves an implementation is possible.

The implementation here does a single pass through the attributes
backwards (which should probably be changed from a linked-list), keeping
track of the best matching tag/id while updating everything based on
grows/shrinks. Once we find the source of the best id we adjust things
back to the pending id space.

The implementation here only works with some significant caveats:

1. This solution might be able to find the id weights by keeping track
   of a lower bound, but it would be difficult and add complexity, so we
   don't do it. Really lfsr_rbyd_pendinglookup is only going to be used
   in full traversals as a part of compaction/splitting, so weight can
   be derived trivially from neighboring ids.

2. We don't know the difference between grows/shrinks used to change a
   branch's weight and used to create/delete ids. This is a bit of a
   problem here, but we can work around it by assuming that
   non-destructive grows/shrinks are always on the lower edge of a
   weighted id.

   Fortunately this assumption is only needed for in-flight attrs in
   lfsr_rbyd_pendinglookup, so this is not a requirement on-disk or in
   future implemenations.
2023-03-17 14:20:09 -05:00
c0ee405cf2 Attempted impl of lfsr_rbyd_pendinglookup with two passes
1. Search backwards through our tags to find the most recent,
   best matching id.

2. Replay tags after the found id to adjust for any pending changes.

In theory this should work in controlled cases, but there are a lot of
corner cases around grows and shrinks. Tests are written, and failing,
but I think it may be simpler and more efficient to implement this in a
single pass, with tighter assumptions about what grow/shrinks are
allowed.
2023-03-17 14:20:09 -05:00
6f4704474b Changed GROW/SHRINK to always be explicit, dropped LFSR_TAG_RM
Generally, less implicit behavior => simpler systems, which is the goal
here.
2023-03-17 14:20:09 -05:00
1709aec95b Rough draft of general btree implementation, needs work
This implements a common B-tree using rbyd's as inner nodes.

Since our rbyds actually map to sorted arrays, this fits together quite
well.

The main caveat/concern is that we can't rely on strict knowledge on the
on-disk size of these things. This first shows up with B-tree insertion,
we can't split in preparation to insert as we descend down the tree.

Normally, this means our B-tree would require recursion in order to keep
track of each parent as we descend down our tree. However, we can
avoid this by not storing our parent, but by looking it up again on each
step of the splitting operation.

This brute-force-ish approach makes our algorithm tail-recursive, so
bounded RAM, but raises our runtime from O(logB(n)) to O(logB(n)^2)

That being said, O(logB(n)^2) is still sublinear, and, thanks to
B-tree's extremely high branching factor, may be insignificant.
2023-03-17 14:20:09 -05:00
b887365fcb Some more rbyd cleanup/tweaks
Mostly just deduplicating redundant conditionals for tags with special
handling in fetch/append.
2023-03-17 14:20:09 -05:00
98532f3287 Adding sparse ids to rbyd trees
The way sparse ids interact with our flat id+attr tree is a bit wonky.

Normally, with weighted trees, one entry is associated with one weight.
But since our rbyd trees use id+attr pairs as keys, in theory each set of
id+attr pairs should share a single weight.

  +-+-+-+-> id0,attr0   -.
  | | | '-> id0,attr1    +- weight 5
  | | '-+-> id0,attr2   -'
  | |   |
  | |   '-> id5,attr0   -.
  | '-+-+-> id5,attr1    +- weight 5
  |   | '-> id5,attr2   -'
  |   |
  |   '-+-> id10,attr0  -.
  |     '-> id10,attr1   +- weight 5
  '-------> id10,attr2  -'

To make this representable, we could give a single id+attr pair the
weight, and make the other attrs have a weight of zero. In our current
scheme, attr0 (actually LFSR_TAG_MK) is the only attr required for every
id, and it has the benefit of being the first attr found during
traversal. So it is the obvious choice for storing the id's effective weight.

But there's still some trickiness. Keep in mind our ids are derived from
the weights in the rbyd tree. So if follow intuition and implement this naively:

  +-+-+-+-> id0,attr0   weight 5
  | | | '-> id5,attr1   weight 0
  | | '-+-> id5,attr2   weight 0
  | |   |
  | |   '-> id5,attr0   weight 5
  | '-+-+-> id10,attr1  weight 0
  |   | '-> id10,attr2  weight 0
  |   |
  |   '-+-> id10,attr0  weight 5
  |     '-> id15,attr1  weight 0
  '-------> id15,attr2  weight 0

Suddenly the ids in the attr sets don't match!

It may be possible to work around this with special cases for attr0, but
this would complicate the code and make the presence of attr0 a strict
requirement.

Instead, if we associate each attr set with not the smallest id in the
weight but the largest id in the weight, so id' = id+(weight-1), then
our requirements work out while still keeping each attr set on the same
low-level id:

  +-+-+-+-> id4,attr0   weight 5
  | | | '-> id4,attr1   weight 0
  | | '-+-> id4,attr2   weight 0
  | |   |
  | |   '-> id9,attr0   weight 5
  | '-+-+-> id9,attr1   weight 0
  |   | '-> id9,attr2   weight 0
  |   |
  |   '-+-> id14,attr0  weight 5
  |     '-> id14,attr1  weight 0
  '-------> id14,attr2  weight 0

To be blunt, this is unintuitive, and I'm worried it may be its own
source of complexity/bugs. But this representation does solve the problem
at hand, so I'm just going to see how it works out.
2023-03-17 14:19:49 -05:00
a812cfa70b Moved conditional id/weight nudging into readtag/progtag
So readtag/progtag take care of all tag-encoding idiosyncrasies and this
id-1/0 thing doesn't leak between abstraction layers.
2023-02-12 17:15:05 -06:00
c7c0e013db Fiddled around with how diverged state is tracked
Moving the main path flipping code to the end of the loop helped
organize things a bit better. Still, thanks to needing to track multiple
diverged paths, the state tracking ended up quite complicated. This
implementation uses 3-bits to store the current diverged state:

  diverged=0 => not diverged
  diverged=4 => diverged, on lower path
  diverged=5 => diverged, on upper path
  diverged=2 => diverged, found one tag, on lower path
  diverged=3 => diverged, found one tag, on upper path

I also explored the early design using two variables (lt weight/gt weight)
instead of three (lower bound/upper bound/key), but it still has
problems:

- Keeping track of the found key in lfsr_rbyd_append requires an additional
  variable, so the actual savings are unclear.

- Knowing when to diverge is a bit of a problem, before we only
  needed one set of bounds and two different target keys, but with lt/gt
  weights we'd need two sets of lt/gt weights.

  We technically already pay the RAM cost for this, since we end up
  needing two copies of the bounds after diverging, but deciding when
  to update which lt/gt weights is complicated

There is a risk this whole thing is a premature optimization, but oh well,
I've probably been staring at this function for too long.
2023-02-12 17:14:58 -06:00
9b414e448c Upped attribute size to the full 32-bits
This increases the leb128 size from 4 bytes to 5 bytes for very little
gain, but 1 byte of RAM is not worth sweating over and this means fewer
surprises for a "32-bit" littlefs implementation.

If that 1 byte is worth saving, this should be configurable in the
future.
2023-02-12 17:14:57 -06:00
86bafaee27 Dropped lfsr_sid_t for lfs_ssize_t 2023-02-12 17:14:57 -06:00
8628178631 Attempted to simplify the core rbyd_append algorithm as much as possible 2023-02-12 17:14:57 -06:00
739a6e6d98 Rearranged lfsr_rbyd_append to have a single sprial loop with black recoloring during deletes
Preliminary comparisons show a minor improvement to code size at the cost
of stack usage. Really this boils down to a toss up, I'm currently
leaning towards this implementation of lfsr_rbyd_append as it has the
fewest moving parts, reusing the core rbyd loop for all mutating
operations.

Note these numbers are _very_ rough, there are likely some low-hanging
optimizations/cleanup and the rbyd size measurements are simply found
from fuzzing 1000 random permutations:

               commit size                       rbyd size
               code  stack  append/removes  create/deletes
spiralrb:      3178    664            1609            1621
spiralb:       3058    664            1609            1606 (current)
2stepb:        3284    632            1609            1606
2023-02-12 17:14:57 -06:00
d3d340c426 Did some rearranging of lfsr_rbyd_append to perform deletes in two-stages
This simplifies things, but at a code cost. This likely deserves a
different approach.
2023-02-12 17:14:57 -06:00
745b89d02b Fixed issue where looking up tag 0 fails after a delete id0
Well not really fixed, more just added an assert to make sure
lfsr_rbyd_lookup is not called with tag 0. Because our alt tags only
encode less-than-or-equal and greater-than, which can be flipped
trivially, it's not possible to encode removal of tag 0 during deletes.

Fortunately, this tag should already not exist for other pragmatic
reasons, it was just used as the initial value for traversals, where it
could cause this bug.
2023-02-12 17:14:57 -06:00
7af2e722a8 Some minor rbyd cleanup 2023-02-12 17:14:57 -06:00
2a4b6fcad9 Rbyd tests are now passing again, however range removal needs a review 2023-02-12 17:14:57 -06:00
56fbf4155b Reenabling more tests, tracking down another difficult bug 2023-02-12 17:14:57 -06:00
2f3c0129d6 An ugly lfsr_rbyd_append implementation, but a working one 2023-02-12 17:14:57 -06:00
1c64ccbde7 Steady state before attempting a different rewrite of rbyd pruning 2023-02-12 17:14:57 -06:00
588a103db7 Working through 3-leb range deletes, proving to be problematic
The seperate interactions between ids and keys is new and confusing.
This was something that the previous combined weights hid.
2023-02-12 17:14:57 -06:00
5e0418029d Added mixed attr+id testing, tweaks to 3-leb tag utility functions 2023-02-12 17:14:57 -06:00
08f5d9ddf4 Middle of a rewrite for 3-leb encoding, but rbyd appends and creates both work
If we combine rbyd ids and B-tree weights, we need 32-bit ids since this
will eventually need to cover the full range of a file. This simply
doesn't fit into a single word anymore, unless littlefs uses 64-bit tags.
Generally not a great idea for a filesystem targeting even 8-bit
microcontrollers.

So here is a tag encoding that uses 3 leb128 words. This will likely
have more code cost and slightly more disk usage (we can no longer fit
tags into 2 bytes), though with most tags being alt pointers (O(m log m)
vs O(m)), this may not be that significant.

Note that we try to keep tags limited to 14-bits to avoid an extra leb128 byte,
which would likely affect all alt pointers. To pull this off we do away
with the subtype/suptype distinction, limiting in-tree tag types to
10-bits encoded on a per-suptype basis:

  in-tree tags:
                       ttttttt ttt00rv
                                 ^--^^- 10-bit type
                                    '|- removed bit
                                     '- valid bit
  iiii iiiiiii iiiiiii iiiiiii iiiiiii
                                     ^- n-bit id
       lllllll lllllll lllllll lllllll
                                     ^- m-bit length

  out-of-tree tags:
                       ttttttt ttt010v
                                 ^---^- 10-bit type
                                     '- valid bit
                               0000000
       lllllll lllllll lllllll lllllll
                                     ^- m-bit length

  alt tags:
                       kkkkkkk kkk1dcv
                                 ^-^^^- 10-bit key
                                   '||- direction bit
                                    '|- color bit
                                     '- valid bit
  wwww wwwwwww wwwwwww wwwwwww wwwwwww
                                     ^- n-bit weight
       jjjjjjj jjjjjjj jjjjjjj jjjjjjj
                                     ^- m-bit jump

The real pain is that with separate integers for id and tag, it no
longer makes sense to combine these into one big weight field. This
requires a significant rewrite.
2023-02-12 17:14:44 -06:00
cdc3a486d6 Initial exploration of B-trees, but ran into issues composing with rbyds
The original idea was weighted B-trees composed out of weighted rbyds,
with the two weight systems being independent. Descent down the B-tree
uses the same technique in the current metadata data-structure of
searching for which branch to take during fetch, basically getting the
search for free (well, on top of the already required O(m) fetch
operation).

But this is fundamentally flawed. While file names provide an absolute
reference for finding matches, weights are relative references. So we
don't have enough information to do weight-based lookup during fetch.

This smells just like the relative-vs-absolute key issues that led to
rbyd vs rbd trees in the first place...

One option is to do rbyd traversals at each B-tree node to build the
necessary information to figure out the weights. But with rbyd
traversals taking O(m log m), this makes B-tree lookups O(log n * m log m),
and B-tree traversals a messy O(n log n * m log m), which is acceptable, but
disapointing for what will likely be the most common operation in the
filesystem.

But the rbyd trees _are_ already weighted. A better solution might be to
go back and rethink the seperation of B-tree weights and rbyd ids.
Unfortunately, with only 16-bits available for rbyd ids, this would
likely require a rewrite of how rbyd tags are encoded...
2023-02-12 17:14:42 -06:00
22f00649a6 Reduced caching during rbyd lookup based on preliminary benchmarking
This deserves more scrutiny in the future, but early benchmarking shows
caching here to add significant cost, since we often load branches we
don't take.

There may be smarter things we can do, but ideas quickly get into
branch-prediction territory, which is very, very out of scope.
2023-02-12 17:14:42 -06:00
27e4fbd3ad Re-upped on-disk leb128 limit for tags to 5-bytes
This gives us the full 16-bit range of ids (65536) instead of the much
smaller 12-bit range (4096) when limited to truncated 4-byte leb128s.

The real motivation for the truncated 4-byte leb128s is to keep the
wasted space in crc padding down, which this doesn't matter for.

In the future this may be configurable. Or maybe not. Only if truncated
leb128 tags prove to have value, at the moment it looks more like a
premature optimization if anything...

Either way we do need to test for overflowing this.
2023-02-12 17:14:42 -06:00
d08497c299 Rearranged type encoding for crcs so they mostly fit in a single byte
I'm still not sure this is the best decision, since it may add some
complexity to tag parsing, but making most crcs one byte may be valuable
since these exist in every single commit.

This gives tags three high-level encodings:

  in-tree tags:
  iiiiiii iiiiitt ttTTTTT TTT00rv
              ^----^--------^--^^- 16-bit id
                   '--------|--||- 4-bit suptype
                            '--||- 8-bit subtype
                               '|- removed bit
                                '- valid bit
  lllllll lllllll lllllll lllllll
                                ^- n-bit length

  out-of-tree tags:
  ------- -----TT TTTTTTt ttt01pv
                       ^----^--^^- 8-bit subtype
                            '--||- 4-bit suptype
                               '|- perturb bit
                                '- valid bit
  lllllll lllllll lllllll lllllll
                                ^- n-bit length

  alt tags:
  wwwwwww wwwwwww wwwwwww www1dcv
                            ^-^^^- 28-bit weight
                              '||- direction bit
                               '|- color bit
                                '- valid bit
  jjjjjjj jjjjjjj jjjjjjj jjjjjjj
                                ^- n-bit jump

Having the location of the subtype flipped for crc tags vs tree tags is
unintuitive, but it makes more crc tags fit in a single byte, while
preserving expected tag ordering for tree tags.

The only case where crc tags don't fit in a single byte if is non-crc
checksums (sha256?) are added, at which point I expect the subtype to
indicate which checksum algorithm is in use.
2023-02-12 17:14:14 -06:00
55b072e761 Opened up rbyd testing for all geometries, and fixed related bugs
- Caching is still presenting issues with the new requirements for
  rbyd trees, in this case the default bd, with 64 byte progs, revealed
  and issue where rcache could become outdated when reading from disk
  while ignoring what's in the pcache.

  It assumes the pcache will always override the rcache, but this is not
  true after pcache is flushed.

  This didn't happen before as the rcache and pcache don't
  interact while writing in the previous implementation. Because of
  these new requirements the caching system probably deserves a
  rework...

- The quick tests for sublinear space utilization don't work when
  prog_size is > a byte, fortunately we should always have NOR-like
  geometry under test, so we can limit these asserts to NOR-like
  geometry.

- Lots of problems fitting these tests into 512-byte block_size
  geometries, which is a bit concerning. This may be a larger change
  from the previous implementation than expected. This may deserve more
  scrutiny at small block sizes to see how things fit, since the
  sublinear space utilization doesn't really kick in at this scale...

  On the other hand it may just be that these tests are too aggressive
  for 512-byte block sizes, since they don't yet do compaction, which
  should help with padding/crc overhead...
2023-02-12 17:14:12 -06:00
8581eec433 Added lfs_rbyd_rangesize (untested), some cleanup
Toying around with the idea that since rbyd trees have strict height
gaurantees after compaction (2*log2(n)+1), we can proactively calculate
the maximum on-disk space required for a worst case tree+leb128
encoding.

This would _greatly_ simplify things such as metadata compaction and
splitting, and allow unstorable file metadata (too many custom
attributes) to error early.

One issue is that this calculated worst case will likely be ~4-5x worst
than the actual encoding due to leb128 compression. Though this may be an
acceptable tradeoff for the simplification and more reliable behavior.
2023-02-12 17:14:12 -06:00
4aabb8f631 Reworked tag representation so that sup/sub types have expected order
Previously the subtype was encoded above the suptype. This was an issue
if you wanted to, say, traverse all tags in a given suptype.

I'm not sure yet if this sort of functionality is needed, it may be
useful for cleaning up/replacing classes of tags, such as file struct
tags, but not sure yet. At the very least is avoids unintuitive tag
ordering in the tree, which could potential cause problems for
create/deletes.

New encoding:

  tags:
  iiiiiii iiiiitt ttTTTTT TTT0trv
              ^----^--------^-^^^- 16-bit id
                   '--------|-'||- 5-bit suptype (split)
                            '--||- 8-bit subtype
                               '|- perturb/remove bit
                                '- valid bit
  lllllll lllllll lllllll lllllll
                                ^- n-bit length

  alts:
  wwwwwww wwwwwww wwwwwww www1dcv
                            ^^^-^- 28-bit weight
                             '|-|- color bit
                              '-|- direction bit
                                '- valid bit
  jjjjjjj jjjjjjj jjjjjjj jjjjjjj
                                ^- n-bit jump

Also a large amount of name changes and other cleanup.
2023-02-12 17:13:57 -06:00
1d037f4ec9 Symbol cleanup as weight representation becomes more concrete 2023-02-12 15:24:04 -06:00