Commit Graph

1044 Commits

Author SHA1 Message Date
Christopher Haster bacd09a673 Implicitly set the rm-bit in generic grow tags
Our rbyds support changing the weight of a tag without knowing the
actual tag. This is useful for btrees, which always make weight changes
without knowing if the leading tag is a name or a branch (it depends on
the type of btree).

But to make this work, it needs the rm-bit to be set. This is because
internally the rm-bit indicates we don't want to write-out a tag. Which
we don't for grow tags, because, well, they're not real tags.

Previously this was done by putting LFSR_TAG_GROW(RM) everywhere a
generic grow as needed, but since this is so common we might as well
just set the rm-bit in LFSR_TAG_GROW.

Note that LFSR_TAG_GROW(tag) (the macro) does not set the rm-bit.

This makes the code a bit more readable at the risk of an unintuitive
relationship between LFSR_TAG_GROW and LFSR_TAG_GROW(tag).
2023-08-19 14:49:18 -05:00
Christopher Haster 6088180076 Renamed several rbyd utility functions
- lfsr_rbyd_estimateall -> lfsr_rbyd_estimate
- lfsr_rbyd_appendall -> lfsr_rbyd_appendattrs
- lfsr_rybd_compact -> lfsr_rbyd_appendcompact
- lfsr_rybd_merge -> lfsr_rbyd_appendmerge
2023-08-19 14:48:23 -05:00
Christopher Haster 828e9b48a4 Dropped LFSR_NO_REBALANCE for now
This was not updated after changing btree merges to compact, and as more
of this codebase depends on our rebalanced code size, it's not worth
keeping this code around right now.

Maybe the option for not rebalancing during compaction should be looked
into again in the future. But I'll leave that up to then.
2023-08-19 14:44:44 -05:00
Christopher Haster dd2824d870 Leveraged lfsr_rbyd_appendall's rid range to avoid needing the bid
The start_rid already adjusts the attrs relative rid, and we don't
have a reason to store -1 rid tags in our btree yet, so we can drop the
extra bid parameters.

This is nice because the bid parameter was leaking across abstraction
layers a bit. Our rbyds don't need to understand bids anywhere else.

Unfortunately, this did require some tweaks to btree split, which was
expecting to be able to temporarily write out a -1 rid tag in the case
we're creating a new root. But this is a good change, the -1 rid tag
was sort of a hack that risked other similar bugs.

Funnily enough, these two changes canceled each other out exactly:

            code          stack
  before:  20626           1712
  after:   20626 (+0.0%)   1712 (+0.0%)
2023-08-19 14:42:06 -05:00
Christopher Haster c08350fd12 Small cleanup/tweaks of btree commit attr-lists
The goal here was to just be a bit more consistent/readable with how we
instantiate these attr-lists.
2023-08-19 14:40:52 -05:00
Christopher Haster de171e57f5 Changed btree merge to merge siblings via compaction
This extends lfsr_rbyd_compact to support compaction of any number of
rbyds (though we only ever compact 1 or 2), and leverages this to
compact both siblings during btree merges.

This should improve erased storage utilization for btree merges, help
maintain a better balance in the tree (since more merges can complete
successfully), and hopefully lessen the impact of repeated merge+splits.

Since merges are now compacted, we can also be sure the combined merge
fits in 1/2 our block (hand-waving the split name for now, though this
does need to be considered when determining btree commit limits). This
lets us move the merge code entirely before writing out the attr-list,
making this operation more in line with split/compact and offering more
chance as code deduplication.

            code          stack
  before:  20626           1728
  after:   20634 (+0.0%)   1712 (-0.9%)

This also ironically discards the previous work to find a simple
estimate of upper bound of uncompacted rbyds, though I'm sure that will
useful again at some point in the future.
2023-08-19 14:21:48 -05:00
Christopher Haster 5ecdc8b4f7 More btree tweaks, now with better handling of degenerate parents
Normally, in btrees, the height of the btree only decreases when nodes
are merged.

But not in our btree! Thanks again to lazy merging, btree nodes can be
dropped instead of merged.

We don't have enough information to decrease the height of the btree
exactly when we drop a btree node, since we don't know how many siblings
the original node had, but we can at least decrease the height of the
btree if we notice this condition during normal commits.
2023-08-19 14:16:47 -05:00
Christopher Haster 256488d4b4 Added tests for nasty btree drop conditions and fixed related bug
Thanks to lazy merging, our btree nodes can drop to zero weight at
pretty much any time. Unfortunately, we can't really represent non-root
zero weight btree nodes, so things break. (Though even if we could,
those nodes would become unreachable).

Previously we relied on fuzz testing to try to catch these cases, but
that turned out to be insufficient.

This adds explicit tests covering the cases where btree drops can occur,
thanks to the realy-big-attr trick used in similar mtree tests.

Sure enough this revealed a bug that can occur when we split a btree
node at the same time one of the siblings goes to zero weight. (Remember
splits carried out before playing attr-lists).

---

Fortunately this is pretty easy to fix. We can just reroute our split
code to the normal commit/compact recursion handling if one of our
siblings drops to zero, at the cost of some spaghetti.

xkcd.com/292 seems relevant here.
2023-08-19 14:07:44 -05:00
Christopher Haster df0ad072ea Rearranged btree commit to take advantage of better merge estimate
Now that we can predict if a merge will fit or not without needing to
write any attrs to disk, we can completely get rid of the merge_abort
code path.

            code          stack
  before:  20566           1728
  after:   20546 (-0.1%)   1728 (+0.0%)
2023-08-19 13:59:47 -05:00
Christopher Haster 51a874e584 Avoid aborted btree merges by deriving an upper bound on uncompacted size
Previously, we couldn't accurately predict if a sibling would fit in our
current rbyd because of the overhead of calculating how much space each
of our O(log(n)) alt trunks would take up.

The best we could do is make a rough estimate, and try to merge,
aborting and cleaning up any written tags if it turns out our merge
didn't end up fitting.

But I've recently found a way to calculate an upper bound without too
much overhead, relying only on the compacted estimate:

---

Consider a compacted estimate, e_c. When does our uncompacted estimate
deviate the most? When e_c is packed full of the smallest possible tag
encoding. Since, after compacting t tags, we need and additional 2 alts
and 1 null tag for our compacted rbyd, and since each tag encodes to 4
bytes at minimum, this gives us (1+2+1)*4 bytes, or 16 bytes per tag:

  e_c = 16*t

If we aren't compacting, we rely on rbyd's self-balancing properties,
which guarantees a height strictly less than 2*log2(n)+1. This gives us
a similar, but aymptotically different uncompacted estimate, e_u:

  e_u = 4*t*(1 + 2*log2(t) + 1)

Or, simplifying:

  e_u = 8*t*(log2(t) + 1)

If we know our compacted estimate, e_c, we can assume worst-case it's
full of small tags, and plug this into our uncompacted estimate e_u:

  e_u <= 8*(e_c/16)*(log2(e_c/16) + 1)

Or, simplifying:

  e_u <= (e_c/2)*(log2(e_c/16) + 1)

Since we're dealing with integers, log2(e_c/16) is strictly >= 1. We can
substitute this in for a slightly simpler equation:

  e_u <= (e_c/2)*(log2(e_c/16) + log2(e_c/16))

Or, simplifying:

  e_u <= e_c * log2(e_c/16)

This gives us a simple upper bound calculation we can do to convert any
compacted estimate into a rough, uncompacted one:

  e_u <= e_c * log2(e_c/16)

---

We can use this estimate in our btree merge code to be sure we won't
overflow our current rbyd before we even try merging.

            code          stack
  before:  20638           1744
  after:   20566 (-0.4%)   1728 (-0.9%)

It's worth noting these numbers are purely from the removal of the merge
abort code. There are likely still opportunities to save code/RAM thanks
to predicting merges more accurately.
2023-08-19 13:32:18 -05:00
Christopher Haster 732998cf77 More btree tweaks, renamed a few things
- prid -> rid, this is the rid of our current rbyd after all
- s* -> sibling_*, prefer more descriptive names
- s* -> split_*, prefer more descriptive names
2023-08-19 13:29:17 -05:00
Christopher Haster 842b143c91 Reverted multiple fetches of parents, other btree tweaks
While it may make more logical sense to fetch the parent after our rbyd
commit completes, fetching the parent first just works out better in
terms of code deduplication.

            code          stack
  before:  20750           1752
  after:   20634 (-0.6%)   1744 (-0.5%)
2023-08-19 13:28:12 -05:00
Christopher Haster a2b8f81fc5 Some more btree cleanup
Trying to deduplicate the attr-list constructions before tail recursion
as much as possible.

Also tried rearranging the fetch of our parent to after we commit to our
current rbyd. This makes more logical sense, in terms of the order of
operations, but does mean duplicate parent fetches for the different
code paths...

            code          stack
  before:  20830           1752
  after:   20750 (-0.4%)   1752 (+0.0%)
2023-08-19 13:27:24 -05:00
Christopher Haster a5260aa290 Tried to deduplicate tail-end of rbyd commits in btree commit
This gets a bit ugly with all of the gotos (which is always a great
thing to hear in a C codebase), but with both our normal commit and
compact code paths obviously sharing the same commit logic when we
tail-recurse to our parent, it is really nice to deduplicate these
two paths.

merge_abort is also still there, annoyingly it needs a slightly
different label since merge_abort still needs to append the cksum that
finalizes the commit.

            code          stack
  before:  20874           1752
  after:   20830 (-0.2%)   1752 (+0.0%)

In theory, both commit/compact/merge _could_ share the cksum append,
because compact and merge can't error with LFS_ERR_RANGE (which
might risk an infinite loop?), but that's a level of spaghetti code I'm
not ready to take on yet.
2023-08-19 13:25:37 -05:00
Christopher Haster d039c58acd Tweaked lfsr_btree_commit a bit
Trying to avoid copying rbyd structs as much as possible, by having a
before (rbyd) and after (rbyd_) copy up until we tail-recurse. This is
similar to how we handle before/after states in lfsr_mdir_commit.

           code          stack
  before: 20890           1744
  after:  20874 (-0.1%)   1752 (+0.5%)

Not sure this is worth the change...

Also renamed pid/sid -> prid/srid to keep with the strict rid naming
convention.
2023-08-19 13:24:03 -05:00
Christopher Haster 670f7bf207 Changed lfsr_rbyd_commit to not recover on error
It turns out we just don't need this functionality.

The only caller of lfsr_rbyd_commit now is lfsr_format, where there's no
filesystem yet, so recovering after an error doesn't make sense.

Leaving it up to higher-level layers to deal with recovery from rbyd
errors means one less rbyd to allocate.
2023-08-19 12:33:42 -05:00
Christopher Haster b710769dda Moved *_get functions into tests
With the introduction of lfsr_data_t, these stopped being useful
functions for littlefs internally.

Maybe these tests should be rewritten to use the *_lookup functions
directly? Unfortunately with the quantity of tests we have now this adds
non-trivial amount of work with questionable benefit.
2023-08-19 12:33:16 -05:00
Christopher Haster d09a3646aa Moved lfsr_btree_push/set/pop/split into the tests
These functions are no longer needed in lfs.c. They are still needed for
the tests as they are written, but that's not a reason to pollute the
littlefs source code.

Maybe these tests should be rewritten to use lfsr_btree_commit directly?
Unfortunately with the quantity of tests we have now this adds
non-trivial amount of work with questionable benefit.
2023-08-19 12:33:03 -05:00
Christopher Haster 528f104cb4 Enabled internal test code at the suite-level
Test suites already had the ability to provide suite-level code via the
"code" attribute, but this was placed in the suite's generated source
file, making it inaccessbile to internal tests.

This change allows suite code to be placed in the same place as internal
tests, via the "in" attribute, though this has some caveats:

1. Suite-level code generally declares helper functions in global scope.
   We don't parse this code or anything, so name collisions between
   helper functions across different test suites is up to the developer
   to resolve.

2. Internal suite-level code has access to internal functions/variables/
   etc, this means we can't place a copy in our suite's generate source
   and expect it to compile. For this reason, internal suite-level code
   is unavailable for non-internal tests in the suite.

   This also means you only get to place internal suite-level code in a
   single source file. Though this is not really an issue since littlefs
   is basically a single file...
2023-08-19 12:20:13 -05:00
Christopher Haster fb2fdb536c Adopted lfsr_btree_commit, replacing all other btree operations.
Note this required changing the INLINED tag to REG in most of the tests,
because our mtree now explicitly requires some sort of NAME tag.

We can also finally see the impact on code and RAM from this restructure:

                                 code          stack
  before (push/set/pop/split):  21750           1928
  after (commit):               20970 (-3.7%)   1744 (-10.6%)

Not too shabby if I say so myself.
2023-08-19 12:17:11 -05:00
Christopher Haster a4c3a12f68 Merged lfsr_btree_commit/lfsr_btree_commit__, related cleanup
This was a temporary hack to make refactoring easier. These are really
the same function.
2023-08-19 12:16:06 -05:00
Christopher Haster 2abc61c49c Made btree commit track bids correctly during recursion
This doesn't have that big an impact at the moment, but limiting the
bids/rids to well intentioned values helps development and debugging.

As we tail-recurse up the btree, the current bid always indicates the
left-most/least id in the current rbyd. This contrasts with pid, which
is the right-most id in the current rbyd. Before this the bid was
somewhat arbitrary after the first leaf, which risks confusion later.

This also implies bid=0 when we reach the root, which is a useful debug
assertion.
2023-08-19 12:15:50 -05:00
Christopher Haster f5436caf24 Extended appendall to adjust bid-relative attrs, made attr-lists const again
This adds an extra bid parameter to lfsr_rbyd_appendall so that attrs
relative to a bid can be adjusted correctly.

This allows us to make attr-lists const again, which is generally a good
things. Passing around complex mutable state is just asking for bugs.

Though since these attr-lists are generally just passed as temporary
arguments, maybe it's not that bad?
2023-08-19 12:10:17 -05:00
Christopher Haster 9b2f3cd5bb Rerouted all btree mutation through attr-list parser
The idea here: Instead of having unique functionality for each
individual btree operation (push/set/pop/split), we treat btrees sort of
like rbyds, with a single commit entry point that operates on attr-lists.

This adds code cost, due to needing to parse the attr-list for properties
that can affect inlined btrees (tag changes mostly), but, in theory, comes
with some advantages:

1. A single btree commit entry point with all of the inlined/uninlining
   logic should offer better chances for code deduplication, vs
   spreading this logic out in each btree operation.

2. Higher-levels should know what the current weight of the branch is,
   so we may be able to avoid the implicit math needed to calculate
   deltas.

3. Higher-levels have more knowledge about the state of the btree in
   general, so there may be other shortcuts. The mtree, for example,
   only operates on weight=1 entries, which greatly simplifies a lot of
   the related math.

Note that btrees still have strict limits in what's possible in an
attr-list. Btree operations can't cross leaf-rbyd boundaries for
example.

---

A notable omission in this change is the loss of reinlining btrees.

This wase dropped for a couple reasons. It may be worth adding back at a
later time, maybe after we actually have files implemented, but for now
does not seem worth it:

1. Reinlining adds code cost. Reinlining is more complex than you might
   expect because we only reinline on compaction. And because we compact
   before playing out our attr-list, we need to know if a commit makes
   the btree inlinable before committing to the btree.

   This is still doable with our attr-lists. We already derive the
   change in tags, since we need this to know when to uninline. But it
   adds a kind of complex bailing out of btree commits.

2. The benefits of reinlining may not be that great. In most systems, a
   tree that is uninlined once is likely to be uninlined again. It's
   only if there is a bigger state change in a system that it makes
   sense to reinline.

   Though, to be fair, waiting for compaction to reinline handled this
   quite well. Only reinlining when all erased storage is used up...

3. Thanks to our roots did entry, our mtree can never reinline.

   It would be nice to change this, but this would require explicit
   handling in lfsr_mdir_commit. Future work?

4. Files are another can of worms, with more complex interactions with
   inlinability thanks to (at least on paper right now) always having
   inlined data even when uninlined.

   If reinlining is valuable for files this can change during that work.

5. Even if files never support reinlinability, truncating files (via
   either lfsr_file_truncate or LFSR_O_TRUNC) should give the file a
   blank slate, effectively reinlining the file in that case.

---

The current implementation also changes the attr-list to be mutable so
we can adjust attr-list based on the current btree node. This is a
temporary hack! We should add the appropriate functionality to our rbyd
utilities to revert this eventually.
2023-08-19 11:41:36 -05:00
Christopher Haster 3dbc986752 Added explicit tests over btree reinlining
These tests, and this feature really, is a bit tricky since our btrees
reinline "lazily". That is, our btrees only check if they can inline
during compaction, allowing potentially inlinable btrees to remain
uninlined.

This better utilizes any erased storage in the btree's rbyd, but adds
some corner cases we need to be concerned about.

Added because of some ongoing btree rewrite work, where it did catch
incorrect behavior.
2023-08-19 11:40:10 -05:00
Christopher Haster 5541ad0c00 Fixed missed opportunity for wide tags in btree set
This code was written before we had wide tags, which were introduced for
this exact, and common, use case of needing to replace a range of tag
subtypes. Must have just been missed.
2023-08-19 11:39:32 -05:00
Christopher Haster e5a4b3e50d Dropped btree attr-list hijacking hackery
In an effort to better utilize RAM in the tail-recursive btree commit
implementation, we were previously hijacking the attr-list passed to
lfsr_btree_commit and reusing that memory for our own tail-recursive
attr-lists.

I decided to remove this for now for code smell reasons, since it is
a big hack, but it turns out removing the attr-list hijack actually
saved RAM?

            code          stack
  before:  21754           1968
  after:   21782 (+0.1%)   1944 (-1.2%)

This was a nice surprise. Maybe the RAM savings come from better
compiler optimizations thanks to simpler variable lifetimes? Or maybe
we're just below the compiler's noise floor...
2023-08-19 11:38:16 -05:00
Christopher Haster 314c832588 Adopted new struct encoding scheme with redund tag bits
Struct tags, in littlefs, generally encode pointers to different on-disk
data structures. At this point, they've gotten a bit complex, with the
btree struct, for example, containing 1. a block address, 2. the trunk
offset, 3. the weight of the trunk, and 4. a checksum.

Also some future plans:

1. Block redundancy will make it so these pointers may have a variable
   number of block addresses to contend with.

2. Different checksum types may make the checksum field itself variable
   length, at least on larger builds of littlefs.

   This may also happen if we support truncated checksums in littlefs
   for storage saving reasons.

Having two variable sized fields becomes a bit of a pain. We can use the
encoded tag size to figure out the size of one of these fields, but not
both.

The change here makes it so the tag size now determines the checksum
size, requiring the redundancy amount to go somewhere else. This makes
it so checksums can be variably sized, and the explicit redundancy
amount avoids the need to parse the leb128s fully to know how many
blocks we're expecting.

But where to put the redundancy amount?

This commit carves out 2-bits from the struct tag to store the amount of
redundancy to allow up to 3 blocks of redundancy:

  v0000011 0TTTTTrr
  ^--^---^-^----^-^- valid bit
     '---|-|----|-|- 3-bit mode (0x0 for structs)
         '-|----|-|- 4-bit suptype (0x3 for structs)
           '----|-|- 0 bit (reserved for leb128)
                '-|- 5-bit subtype
                  '- 2-bit redund

3 blocks may sound extremely limiting, but it's a common limit for
filesystems, 1. because you have to keep in mind each redundant block
adds that much more writing/reading overhead and 2. the fact
that 2^(2^n)-1 is always divisible by 3 makes >3 parity blocks much more
complicated mathematically.

Worst case, if we ever have >3 redundant blocks, we can create new
struct subtypes. Maybe adding extended struct types that prefix the
block addresses with a leb128 encoding the redundancy amount.

---

As a part of this, reorganized the on-disk btree and ecksum encodings to
put the checksum last.

Also split out the btree and inner btree branches as separate struct
types. The btree includes the weight, whereas the weight is implicit in
inner btree branches. This came about after realizing context-specific
prefixes are relatively easy to add thanks to the composability of our
parsers.

This led to some name collisions though:

- BRANCH   -> BNAME
- BOOKMARK -> DMARK
2023-08-11 12:55:48 -05:00
Christopher Haster d069fed3ed Adopted more macro concatenation in tag defines
This cleans up the code a bit, and means we no longer need to define all
of the LFSR_TAG_RMGROWWIDEREG permutations.
2023-08-11 01:35:07 -05:00
Christopher Haster 99b83a4ef1 More bikeshedding around mdir mids
- Use mroot address to determine if we follow mroot during splits
- Prefer bid == -1/bid != -1 for now
- Use u.m when copying mdir internals

I also looked at dropping the bid == -1 representation of inlined mroots,
but it's just too convenient for now. We can leverage address
comparisons to see if we are committing to the actual mroot, and we can
(expensively) compare the mdir blocks for other mroot checks, but we
also use bid == -1 to indicate if we're on the mroot chain in both
lfsr_mdir_commit and lfsr_mtree_traverse...

This is a bit of a shame, since reserving -1 either limits these bids to
15-bits, which is concerning, or requires special handling to cut off
bids at 2^16-1.
2023-08-11 01:29:38 -05:00
Christopher Haster e34665723c Added lfsr_rbyd_appendcksum as an alternative to lfsr_rbyd_commit
The main benefit, aside from a bit better code organization, is that
functions calling lfsr_rbyd_appendcksum don't incur the cost of copying
the lfsr_rbyd_t struct to allow safe rollback in the event of failure.
lfsr_rbyd_commit provides this guarantee, but for situations where
lfsr_rbyd_appendcksum are appropriate, this guarantee is useless since
there are usually other lfsr_rbyd_append* calls involved.

---

Also during restructuring I realized the checksum validation step after
a commit is nearly useless. It only checks the checksum since the last
lfsr_rbyd_append* function, so when building rbyds incrementally it
doesn't really validate any metadata.

This is a shame, since the checksum validation was very useful for
finding bugs, but it's not strictly necessary. Humorously, now is
probably the best time to have found this, since the rbyd stuff is
relatively stable at this point.

The validation has been removed as it's incompatible with this
restructure. It might be possible to add back into lfsr_mdir_commit to
at least validate mdir commits, but it's unclear if that's useful.

Validation in general needs to be looked at anyways.
2023-08-11 01:29:38 -05:00
Christopher Haster f1f1a5aaf8 Unionized mtree traversal's tortoise state and btree traversal state
Realistically, because our btree is protected by CoW checksums, the only
place we can end up with a cycle is in our mroot chain.

This is convenient, as we don't need our btree traversal state when
traversing the mroot chain, so we can put both the tortoise state and
btree traversal state into a union, theoretically saving some RAM.

Unfortunately stack measurements show no change, even though our mtree
traversal in on the hot path. I'm not sure why this is. My best guess is
that the RAM savings is beneath the compilation noise floor, since we
currently only ever create one of these structs.
2023-08-11 01:29:33 -05:00
Christopher Haster 0449b06506 Some small tweaks to mdir comparison functions
- lfsr_mid_cmp no longer uses a union. This was undefined behavior and
  the lfsr_mid_t type isn't word aligned, so this could break pretty badly
  on machine/compiler change.

  Also dropped ordering based on endianness, since we need to marshal
  these into an int for the comparison anyways.

- Changed lfsr_mdir_cmp to use min/max functions as part of the
  comparison. The result is also "ordered" now, though the ordering
  is nonsensical. I guess the mrootanchor is less than all other mdirs?

  Also considered only comparing a single min/max block, since it would
  be an error for mdirs to share blocks, but note we rely on
  lfsr_mdir_cmp to check for relocations in lfsr_mdir_commit. These
  relocations can end up being partial in the case of bad block
  detection.
2023-08-10 12:34:38 -05:00
Christopher Haster 571be807dc Reverted lfsr_data_t in low-level rbyd functions
Two reasons:

- The lfsr_data_t API is a bit too high-level for our rbyd functions,
  which need to jump around inside the block, keep track of several
  offsets simultaneously, check for boundary conditions, etc.

- Stack measurements showed a +1.6% stack increase, likely due to extra
  lfsr_data_t copies.

Though there were some cases where adopting lfsr_data_t made sense,
mainly the parsing of the ecksum struct, and along the way some code was
cleaned up in rbyd fetch and rbyd compact, so after reverting we
actually ended up with less code/stack than when we started:

                 code          stack
  before:       21702           1992
  lfsr_data_t:  21626 (-0.4%)   2024 (+1.6%)
  after:        21666 (-0.2%)   1976 (-0.8%)
2023-08-10 11:48:48 -05:00
Christopher Haster 7614cf29c1 Adopted lfsr_data_t in low-level rbyd functions
The low-level rbyd functions need to parse things (mostly tags), so why
not use our parsers? In theory this offers a bit more code reuse.

In theory we can also rely on lfsr_data_t to do bounds checking of
offsets in the block, in practice we need to setup those bounds
correctly for lfsr_data_t, so not so much...

Code/stack cost:

            code          stack
  before:  21702           1992
  after:   21626 (-0.4%)   2024 (+1.6%)
2023-08-10 11:47:29 -05:00
Christopher Haster 1d39c5dd68 Reworked how branch/btree disk functions interact with upper layers
This is kind of messy. The fact that btrees encode any inlined
entry's types directly in the tag, and that btree have multiple tags
themselves (btree (future), mtree, ptree (future), gftree (future)),
means we need several extra parameters to make the btree to/from disk
functions work.

This is going to get more complex with file btrees having their own
inline system.

So for now I've moved the inlined to/from disk logic up into upper
layers, limiting btree to/from disk functions to only parse actual
btrees.

Since btree/branch to/from disk functions are basically the same thing
now, the two have been merged into the btree to/from disk functions.
2023-08-10 11:36:57 -05:00
Christopher Haster d8f988a8fc Made data read functions "consume" their data pointers
Composable parsing functions always feel a bit weird to me in C. I don't
know if this is because of something C lacks, such as multiple return
values, or if composable parsers are just inherently awkward to describe
in procedural languages because of the different levels of state.

But I think the API here is pretty ok. The main idea is that data
parsers can be added as functions in the lfsr_data_* namespace that take
lfsr_data_t as a mutable reference, updating the lfsr_data_t's internal
state as data is parsed.

In practice you only need a couple of primitives, bytes, le32s, leb128s,
that touch the internals of lfsr_data_t, and the other parsers can be
built using these.

This leverages the pointer-like abstraction of lfsr_data_t, and avoids
needing to keep track of offsets. And thanks to lfsr_data_t being
relatively cheap to make copies, this API is relatively flexible.

Some other tweaks:

- Signed leb128 overflow detection is moved up into lfs_fromleb128.
  littlefs now assumes _all_ leb128s are 31-bits, which is useful for
  leveraging the sign bit internally.

  This also fixes the an issue in overflow detection in lfs_fromleb128
  which wouldn't catch overflows in the last byte of a >32-bit leb128.

- Most lfsr_data_t functions now take a pointer. This offered a small
  bit of code savings and feels more natural in C. Though most functions
  that accept lfsr_data_t still take a copy. Most of these functions
  would need to make a copy anyways now that the parsers are consuming,
  and these copies avoid concerns about shared state.

  At 3-words, lfsr_data_t is right at that boundary of questionable
  reasonableness for copying, but copying is a very useful feature of
  this struct.

This ends up with some decent code/stack savings:

            code          stack
  before:  22118           2048
  after:   21722 (-1.8%)   1992 (-2.7%)
2023-08-10 11:13:14 -05:00
Christopher Haster 77de73e39c Mostly reverted support for 2 leb128 encodings in lfsr_data_t
As much as it was a nice way to utilize all 96-bits of lfsr_data_t, the
added code and RAM cost just made this not worth it.

The main problem with inlining leb128s into lfsr_data_t is that
lfsr_data_t reads have to contend with a number of awkward corner cases
involving offsets into the not-yet-encoded leb128s.

Adding to this the extra overhead encoding the number of leb128s in
lfsr_data_t, and the fact that 2 leb128s is mostly useless when metadata
redundancy > 2, I'm reverting this back to only a single optional leb128
in lfsr_data_ts limited to lfsr_bd_progdata:

   0 = in-device buffer     1 = on-disk data
  .----+----+----+----.  .----+----+----+----.
  |0|      size       |.>|1|      size       |
  |----+----+----+----|  |----+----+----+----|
  | (optional leb128) |  |        off        |
  |----+----+----+----|  |----+----+----+----|
  |       buffer      |  |       buffer      |
  '----+----+----+----'  '----+----+----+----'

It's also worth noting that since this leb128 is limited to
lfsr_bd_progdata, it shouldn't add any code cost to readonly variants of
littlefs.

Another interesting thing is that, while this single-injected-leb128-
for-lfsr_bd_progdata sounds limited on paper, it covers a number of
convenient use cases:

- injecting directory-ids into name attributes
- programming single leb128s, such as bookmarks
- prefixing B-tree branches with weights (not-yet implemented)
2023-08-09 02:54:08 -05:00
Christopher Haster cc991396c2 Extended lfsr_data_t to support 1 and 2 leb128 encodings
The idea of this is:

1. Aside from the encoded size, our lfsr_data_t has space for 2 integers.
2. Our mdir addresses are exactly 2 leb128s.
3. We already need to be able to inject 1 leb128 for did entries.

So if we can cram our 2 leb128s inline into the lfsr_data_t, we should
be able to avoid the indirection, wasted space in lfsr_data_t, and
duplicate encoding costs for the mdir addresses.

Conveniently for us, there are exactly 2 unused bits in various fields,
thanks to our common 31-bit limits.

It's a bit awkward since we must assume our buffer pointer uses all
32-bits, but here are the current encodings:

  00 = in-device buffer    10 = on-disk data
       no leb128s               no leb128s
  .----+----+----+----.  .----+----+----+----.
  |0|      size       |  |1|      size       |
  |----+----+----+----|  |----+----+----+----|
  |0000000000000000000|  |0|     offset      |
  |----+----+----+----|  |----+----+----+----|
  |       buffer      |  |       block       |
  '----+----+----+----'  '----+----+----+----'

  01 = in-device buffer      11 = 2 leb128s
       1 leb128
  .----+----+----+----.  .----+----+----+----.
  |0|      size       |  |1|      size       |
  |----+----+----+----|  |----+----+----+----|
  |1|     leb128      |  |1|     leb128      |
  |----+----+----+----|  |----+----+----+----|
  |       buffer      |  |       leb128      |
  '----+----+----+----'  '----+----+----+----'

This encoding also presents a relatively nice code-path, since we can
treat the 2 leb128 case as an on-disk data reference with no size.

Unfortunately the initial measurements look, uh, really bad:

            code          stack
  before:  22194           2048
  after:   22426 (+1.0%)   2088 (+2.0%)

This needs more investigation, but from what I can tell so far the RAM
cost comes from the leb128 encoding buffer moving into the "hot path",
aka the deepest call stack in littlefs, which involves lfsr_data_read
as a part of mtree traversal as a part of block allocation.

I have no idea about the code cost though...
2023-08-09 02:48:51 -05:00
Christopher Haster 78a199b59b Reworked LFSR_ATTR macros to support extensions better
Generally the more creative you get with C macros, the more
unmaintainable your codebase becomes, but in this case I think a small
bit of macro sugar for the attribute lists in littlefs goes a long way
for making the internals flexible and readable.

Attribute lists generally look like this:

  LFSR_ATTRS(
      LFSR_ATTR(id, TAG, delta, DATA(data)),
      LFSR_ATTR(id, TAG, delta, DATA(data)),
        ...
      LFSR_ATTR(id, TAG, delta, DATA(data)))

Which more-or-less gets expanded to this:

  ((const lfsr_attr_t[]){
      ((lfsr_attr_t){id, LFSR_TAG_TAG, delta, LFSR_DATA_DATA(data)}),
      ((lfsr_attr_t){id, LFSR_TAG_TAG, delta, LFSR_DATA_DATA(data)}),
        ...
      ((lfsr_attr_t){id, LFSR_TAG_TAG, delta, LFSR_DATA_DATA(data)}),}),
  attr_count

Note the use of preprocessor concatenation to put the TAG and DATA
identifiers in their respective namespaces. These can end up invoking
other macros, which allows attrs to be rather extensible.

Previously there were also LFSR_ATTR_ (note the trailing underscore)
macros to allow passing of variable tags/datas. This is replaced with
redundant macros which sort of "unwrap" themselves as a part of macro
expansion. This avoids a bunch of duplicate macro definitions.

  #define LFSR_TAG_TAG(tag) (tag)
  #define LFSR_DATA_DATA(data) (data)

So:

  LFSR_ATTR(id, TAG(tag), delta, DATA(data))

Becomes:

  ((lfsr_attr_t){id, LFSR_TAG_TAG(tag), delta, LFSR_DATA_DATA(data)})

Becomes:

  ((lfsr_attr_t){id, tag, delta, data})
2023-08-08 22:23:10 -05:00
Christopher Haster d2f2b53262 Renamed fcksum -> ecksum
This checksum is used to keep track of if we have erased, and not yet
touched, the unused bytes trailing our current commit in the rbyd.

The working theory is that if any prog attempt is made, it will, most
likely, change the checksum of the contents, allowing littlefs to
determine if trailing erased-state is safe to use, even under powerloss.
littlefs can also perturb future data by a single bit, to force this
checksum to always be invalidated during normal operation.

The original name, "forward erased-state checksums (fcksum)", came from the
idea that the checksum "looks forward" into the next commit.

But after using them for a bit, I think the name is unnecessarily
confusing. It, uh, also looks a lot like a swear word. I think
shortening the name to just "erased-state checksums (ecksum)", even
though the previous name is already in use in  a release, is reasonable.

---

It's probably hard to believe but the name change from fcrc -> ecrc
really was unrelated to the crc -> cksum change. But boy is it
convenient for avoiding an awkward name. A lot of these name changes
involved sed scripts, so I didn't notice how awkward fcksum would be to
use until writing this commit message.
2023-08-07 14:34:47 -05:00
Christopher Haster 7031d6e1b3 Changed most references to crc/csum -> cksum
The reason for this is to move away from the idea that littlefs is
strictly bound to CRCs and make the code more welcoming to other
checksum types, such as SHA256, etc.

Of course, changing the name doesn't really do anything. littlefs
actually _is_ strictly bound to CRCs in a couple ways that other
filesystems aren't. These would need to have workarounds for other
checksum types:

- We leverage the parity-preserving nature of (some) CRCs to not have
  to also calculate the parity of metadata in rbyd commits.

- We leverage the linearity of CRCs to retroactively flip the
  perturb bit in the cksum tag without needing to recalculate the
  checksum. Though the fact we need to do this is because of how we
  use parity above, so this may just not be needed for non-CRC
  checksums.

- The plans for global-CRCs (not yet implemented) rely heavily on the
  mathematical properties of CRC polynomials. This doesn't mean
  global-CRCs can't work with other checksums, you would just need to
  find a different type of polynomial.
2023-08-07 14:18:37 -05:00
Christopher Haster dc3b7d435e Tried to better name *_buf/d/w variables
Unless very obvious, all buf variables should be prefixed with the
related variable they are being used to encode. Unlike other common
variables, bufs need to be sized correctly for what they are encoding.
Sharing bufs between variables is most likely a coding mistake.

Also tried to move away from the single letter 'w' variables, at least
in the C source.
2023-08-07 14:18:36 -05:00
Christopher Haster d77a173d5c Changed source to consistently use rid for rbyd ids
Originally it made sense to name the rbyd ids, well, ids, at least in
the internals of the rbyd functions. But this doesn't work well outside
of the rbyd code, where littlefs has to juggle several different id
types with different purposes:

- rid => rbyd-id, 31-bit index into an rbyd
- bid => btree-id, 31-bit index into a btree
- mid => mdir-id, 15-bit+15-bit index into the mtree
- did => directory-id, 31-bit unique identifier for directories

Even though context makes it clear which id the id refers to in the rbyd
internals, updating the name to rid makes it clearer that these are the
same type of id when looking at code both inside and outside the rbyd
functions.
2023-08-07 14:10:09 -05:00
Christopher Haster 64a1b46ea2 Renamed a couple directory related things
- dstart -> bookmark
- *dnamelookup -> *namelookup
2023-08-07 14:00:44 -05:00
Christopher Haster c37bab6040 Reworked rbyd/btree/mdir structs again so redund blocks are at the end
For a couple reasons:

1. Organizing the overlaps this way avoid potential undefined behavior.
   It turns out C does define the overlap the "initial sequence" of
   union members, as long as the types are the same. But when we
   overlapped the block with the size/tag fields in lfsr_btree_t, it was
   probably undefined behavior.

   At the very least, it would introduce a need for quite a bit of
   preprocessing to make it work with different integer sizes and
   redundancy levels.

2. Overlapping the blocks at the end of the rbyd struct means our block
   array is natural ordered such that the first block is the "active"
   block, i.e. the block with the most recent revision count that passes
   checksums.

   This has been useful as a debugging tool, so I would like to continue
   the pattern. It is possible to mostly preserve this order with the
   previous method by intentional reversing the block array when
   logging or writing to disk, but it's a bit cumbersome.

2. It's unlikely we'll be able to use readonly variants of the rbyd/mdir
   structs for RAM savings. Unfortunately C makes this too cumbersome.
   Though if we do this should be revisited.

Here are the new overlaps. Note it's no longer possible to truncate the
types when readonly. If readonly struct are useful this will need to be
revisited again:

   lfsr_rbyd_t            lfsr_btree_t           lfsr_mdir_t
                                                  8b   8b   8b   8b
                                                .----+----+----+----.
    8b   8b   8b   8b      8b   8b   8b   8b    | mid.bid | mid.rid |
  .----+----+----+----.  .----+----+----+----.  |----+----+----+----|
  |       weight      |.>|       weight      |  |       weight      |
  |----+----+----+----|  |----+----+----+----|  |----+----+----+----|
  |       trunk       |  |   tag   |   size  |  |       trunk       |
  |----+----+----+----|  |----+----+----+----|  |----+----+----+----|
  |        off        |  |    inlined data   |  |        off        |
  |----+----+----+----|  |         |         |  |----+----+----+----|
  |        crc        |  |         v         |  |        crc        |
  |----+----+----+----|  |                   |  |----+----+----+----|
  |       block       |..|                   |.>|       blocks      |
  '----+----+----+----'  '----+----+----+----'  |                   |
                                                |                   |
                                                '----+----+----+----'
2023-08-06 23:40:32 -05:00
Christopher Haster fe941ef443 Reworked rbyd/btree/mdir structs to allow better access to subcomponents
This turned out to be tricky.

At littlefs's core, we have the lfsr_rbyd_t struct. It is really
important this is as small as possible since littlefs creates many rbyd
copies in order to track state of metadata on disk.

Wrapping rbyd, we have the lfsr_btree_t struct, which can alternatively
contain a single inlined entry, accomplished by overlapping the width
field in both cases. And the lfsr_mdir_t struct, which tracks any redundant
blocks, and would be nice if the blocks lined up as neighbors so all blocks
involved in the mdir could be passed around as an array. Both of these
wrappers attempt to overlap fields of the lfsr_rbyd_t struct, which presents
a bit of a problem.

The solution here is to put the rbyd block field at the beginning of the
lfsr_rbyd_t struct, and use exactly 32-bits of padding in lfsr_btree_t
to overlap the width field even though it is not at the beginning of the
struct. To avoid inflating the lfsr_btree_t size, we sneak the inlined
size and tag into the overlapping padding. This will need special
handling if the size of these fields change, but saves a decent amount
of RAM:

   lfsr_rbyd_t            lfsr_btree_t           lfsr_mdir_t
                                                  8b   8b   8b   8b
                                                .----+----+----+----.
                                                | mid.bid | mid.rid |
                                                |----+----+----+----|
    8b   8b   8b   8b      8b   8b   8b   8b    |       blocks      |
  .----+----+----+----.  .----+----+----+----.  |                   |
  |       block       |..|   tag   |size|padd|.>|                   |
  |----+----+----+----|  |----+----+----+----|  |----+----+----+----|
  |       weight      |.>|       weight      |  |       weight      |
  |----+----+----+----|  |----+----+----+----|  |----+----+----+----|
  |       trunk       |  |    inlined data   |  |       trunk       |
  |----+----+----+----|  |         |         |  |----+----+----+----|
  |        off        |  |         v         |  |        off        |
  |----+----+----+----|  |                   |  |----+----+----+----|
  |        crc        |  |                   |  |        crc        |
  '----+----+----+----'  '----+----+----+----'  '----+----+----+----'

Also tried to reduce the amount of mdir usage in lfsr_mdir_commit by
better using only the arrays of relevant mdir blocks, to limited success.
2023-08-06 23:40:28 -05:00
Christopher Haster db514f20f2 Fixed structs.py when structs contain substructs
The previous state machine would happily pick up random names if the
struct had no name of its own. This was picking up typedefs of random
structs and making things really confusing.

Now the rule is that unnamed structs are not printed. Unnamed structs
are usually implementation details so their size is not really useful.

Also made the parsing state machine for objdump outputs more resilient
to these sort of issues.

Also changed structs.py to also report unions if they have a name.
2023-08-06 00:40:40 -05:00
Christopher Haster 3c42ed98a4 Some small tweaks
- Updated LFSR_BTREE_INLINESIZE to properly include the overhead for
  mdir pointers, which need 2 block addresses instead of 1. This adds
  4 bytes to the lfsr_btree_t struct.

- Changed code that marks rbyds as "needing compaction" to use -1
  instead of block_size. This can use a cheaper constant and helps
  debugging.

- Changed the mid representation of root to 0.0 from ?.-1. The mid 0.0
  is always reserved for the roots dstart, so it shouldn't be used for
  any actual file. This disambiguates root vs special metadata mids and
  is a step towards making mids unsigned.

  It also saves a tiny bit of code since 0 comparisons are generally
  cheaper and we can leverage the order-preserving conversion of mid
  to an integer.
2023-08-05 12:06:45 -05:00
Christopher Haster da4e86abac Split test_dirs into test_dtree and test_dseek
- test_dtree - Pure directory creation/deletion/move functionality
  testing. This ends up testing the core of littlefs file entry
  manipulation, since directories is all we need for that.

- test_dseek - Tests more of the corner cases specific to directory
  iteration and seeking. This involves an annoying amount of
  interactions with concurrent updates to the filesystem that are
  complicated to test for.

Also generally renaming the "fstree" concept to "dtree". This only
changes dbglfs.py as far as I'm aware. It's useful to have a name for
this thing and "directory tree" fits a bit better than "filesystem tree"
which could be ambiguous when we also have the "metadata tree" as a
different concept.
2023-08-04 14:17:42 -05:00