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.
- 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...
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.
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.
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.
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.
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.
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.