Commit Graph

388 Commits

Author SHA1 Message Date
Christopher Haster edc4cb2fa9 Changed TEST_PLS to track number of powerlosses seen by the current test
This turned out to have limited use for the tests themselves. I was
hoping to avoid the mount->format->mount fallback when powerloss
testing, but we still need it in case format was interrupted.

Still, TEST_PLS is very useful for debugging.

Previouly it was difficult to set a breakpoint at a specific location,
and after a specific powerloss event. Now all you need is this in gdb:

  b <line> if test_pls == <pls>
2023-10-14 00:11:20 -05:00
Christopher Haster 582dc5f1b2 Added some tests, quick seek impl, fixed bugs
Turns out it's hard to test file holes without seek.

It's interesting to note most of seek's buffer flush work actually
occurs lazily in lfsr_file_write, so lfsr_file_seek turns out to be a
relatively simple function.
2023-10-14 00:09:27 -05:00
Christopher Haster 02ae6050de Changed lfsr_data_t internals, added LFSR_DATA_CAT
The main purpose of this change is to introduce LFSR_DATA_CAT, a
generalized way to concatenated various data references internally.

As a side-effect lfsr_data_t has been completely restructured. Now,
lfsr_data_t can be in one of 4 modes:

If the size field's sign bit=0, the lfsr_data_t points in-device. A new,
count field, determines the encoding:

  sign(size)=0, count=0 => inlined:

    .---+---+---+---.
    |     size      |
    |---+---+---+---|
    |c=0| inlined d |  note inlined data is just enough to hold
    |---+           |  one encoded leb128
    | ata...        |
    '---------------'

  sign(size)=1, count=1 => direct:

    .---+---+---+---.   .---+---+---+---.
    |     size      | .>| data...       |
    |---+---+---+---| | |       .       |
    |c=1|           | | .       .       .
    |---+---+---+---| | .       .       .
    | direct ptr -----' .               .
    '---------------'

  sign(size)=1, count>=2 => indirect:

    .---+---+---+---.   .---+---+---+---.   .---+---+---+---.
    |     size      | .>|     size      | .>| data...       |
    |---+---+---+---| | |---+---+---+---| | |       .       |
    |c>1|           | | |c=1|           | | .       .       .
    |---+---+---+---| | |---+---+---+---| | .       .       .
    | indirect ptr ---' | direct ptr -----' .               .
    '---------------'   '---------------'   .---+---+---+---.
                        |     size      | .>| data...       |
                        |---+---+---+---| | |       .       |
                        |c=1|           | | .       .       .
                        |---+---+---+---| | .       .       .
                        | direct ptr -----' .               .
                        '---+---+---+---'
                        |       .       |
                        |       .       |
                        .       .       .
                        .               .
                        .               .

  note only one indirect layer is allowed due to no recursion

If the size field's sign bit=1, the lfsr_data_t points on-disk:

  sign(size)=0 => on-disk:

    .---+---+---+---.          .....
    |     size      |      ..''     ''..
    |---+---+---+---|     :    :        :
    |     block ------+->|            ..:|
    |---+---+---+---| |  |......( )::::::|
    |      off -------'  |:::'    :      |
    '---------------'     :'       :    :
                           ''..     :.''
                               '''''

My goal with this commit was to test the new implementation and see how
it would impact code/RAM size before adopting it in the actual file
handling code, and the results are... not great...

            code          stack
  before:  24668           1840
  after:   25552 (+3.5%)   1920 (+4.2%)

I think most of the new cost comes from the now correct handling of
read/cmp with concatentated datas, which previously would just assert.
This change gives us LFSR_DATA_CAT, so I will be working with it for
now, but this may be worth looking at again in the future. Maybe the
correct handling of read/cmp should just be reverted to an assert...
2023-10-13 23:45:41 -05:00
Christopher Haster da2a4b45f7 Dropped single-letter union variants, prefer descriptive names
This is in an effort to make the codebase a _bit_ more readable, but
these structs are ending up a complete mess.
2023-10-13 23:44:35 -05:00
Christopher Haster 93ee9d49cf Dropped lfs_t argument from lfsr_data_from* functions
Turns out access to lfs_t just isn't required, and perhaps for the same
reason these functions can never error.
2023-10-13 23:44:15 -05:00
Christopher Haster 1d92169e5b Tweaked cache size to temporarily avoid pathological shrub overflows
This will stop being a problem when we actually have btrees, but for now
the fragmentation caused by byte-level syncs was easily enough to
overflow an mdir when cache size is big.

A smaller cache size is also nicer for debugging, since smaller cache
sizes results in data getting flushed to disk earlier, which is easier
to inspect than in-device buffers. And a 16-byte cache still provides
decent test coverage over cache interactions.

---

Also dropped inline_size to block_size/8. I realized while debugging
that opened shrubs take up additional space until we sync, so we need to
expect up to 2 temporary copies of shrubs when writing files.
2023-10-13 23:35:24 -05:00
Christopher Haster 9f0160556f Made significant progress around inlined-file state during mdir commits
The main improvement is moving the special inlined-file compaction logic
up into lfsr_mdir_compact__. We only need this logic for files stored in
mdirs, and thanks to its recursive nature, we weren't getting any
benefit from handling this at a lower level anyways.

This is a nice logical restructuring that probably saves a bit of code
cost in the end.

Another significant improvement is moving the staging copy of the
inlined tree's state up into the file struct itself. This solves the
problem of needed N copies of temporary inlined state when you have N
open files.

It also provides a central place to stage changes when compacting
inlined trees, which happens across several different places in the mdir
commit logic. Though some may see this as more a hack than a feature.

Also note-worthy, but minor: these changes required an additional
opened-mdir linked-list to know when the mdir is a file and may contain
an inlined tree.
2023-10-13 23:19:24 -05:00
Christopher Haster 541fb07da4 Made progress torwards inlined files surviving compaction
Inlined files are unfortunately turning out to have more cost than
expected, mainly due to our strict no-recursion requirement.

It turns out recursively nesting (bounded) trees in a system without
recursion is a recipe for duplicating code. Though there may be other
ways to structure this.

One interesting hiccup during development is the need to have both NULL
tags and DEFERREDNULL tags in order to tell inlined trees apart from the
main tree during compaction.
2023-10-13 23:13:51 -05:00
Christopher Haster 2f38822820 Still missing quite a bit, but rudimentary inlined-trees are now working
And by working, I mean you can create inlined trees, just don't
compact/split/move/etc anything. But this does outline the path files
take when writing buffers into inlined trees.

"Inlined trees" in littlefs are entire small rbyd trees embedded as
secondary trees in an mdir's main rbyd tree. When fetching, we can
indicate if a given trunk belongs to the main tree or secondary tree by
setting one of the unused mode bits in the trunk's tag, now called the
"deferred" bit. This bit doesn't need to be included in the alt's "key"
field, so there's no issue with it conflicting with the alt's mode bits.

This requires a bit of tweaking lfsr_rbyd_fetch, since it needs to fall
back to the previous trunk if it discovers the most recent trunk belongs
to an inlined tree. But as a benefit we can leverage the full power of
rbyds in inlined files, including holes, partial updates, etc.

One downside is it looks like these inlined trees may involve more work
in maintining their state correctly, since they need to be sort of
"brought along" when mdirs are compacted, even if they don't actually
have a reference in the mdir yet. But the sheer amount of flexibility
this gives inlined files may make this overhead worth it.
2023-10-13 23:11:35 -05:00
Christopher Haster c3533ab816 Some progress, with deferred attributes taking shape
Ran into an interesting macro-related bug. Turns out the way we are
doing implicit prefixing in TAG/ATTR macros sort of breaks how C macros
work a bit. The following does not compile:

  lfsr_mdir_commit(lfs, &file->m.mdir, LFSR_ATTRS(
          LFSR_ATTR(file->m.mdir.mid, DEFER, 0, DEFER(
              (lfsr_rbyd_t*)&file->inlined,
              LFSR_ATTR(file->buffer_pos,
                  DEFERRED(INLINED), +file->buffer_size, BUF(
                      file->buffer, file->buffer_size))))));

Or to distill it down, this does not compile:

  #define LFSR_ATTR(_data)  (LFSR_##_data)
  #define LFSR_DEFER(_data) (LFSR_##_data)
  #define LFSR_DATA(_data)  (_data)

  int a = LFSR_ATTR(DEFER(ATTR(DATA(1))));

But this does:

  #define LFSR_ATTR(_data)  (_data)
  #define LFSR_DEFER(_data) (_data)
  #define LFSR_DATA(_data)  (_data)

  int a = LFSR_ATTR(LFSR_DEFER(LFSR_ATTR(LFSR_DATA(1))));

Why? Well it turns out the whole way nested C macro's work is a big
hack.

A very reasonable design decision in C is to disallow recursive macro
expansions. Unlike C++, we don't want our preprocessor to suddenly stack
overflow. This rule is enforced by stopping macro expansion when a macro
contains itself. For example:

  #define A() B()
  #define B() A()

  A()

Expands to:

  A()
      -> B()
      -> A() (stops, probably erroring with 'A' undeclared)

But it _is_ common to want to recursively expand macro arguments. Macros
are a part of C's syntax after all, and users usually expect
expressions, such as arguments, to be context-free:

  #define A(x) (x) + 1

  A(A(A(A(A(0)))))

Naively this would expand to:

  A(A(A(A(A(0)))))
      -> (A(A(A(A(0))))) + 1 (stops)

The big hack that makes this work in C's preprocessor is the "Argument
prescan". Instead of expanding the "called" macro first, we expand any macro
inside our argument list, _then_ expand the "called" macro, and _then_
expand any new macros produced as a result of the expansion again just
for good measure.

So the above actually expands to:

  A(A(A(A(A(0)))))
      -> A(A(A(A((0) + 1))))
      -> A(A(A(((0) + 1) + 1)))
      -> A(A((((0) + 1) + 1) + 1))
      -> A(((((0) + 1) + 1) + 1) + 1)
      -> (((((0) + 1) + 1) + 1) + 1) + 1

This is still recursive actually! But the recursion is limited to the
actual length of the source code, so the developers likely thought this
was a reasonable tradeoff.

But what does this mean for our implicit prefixing?

  #define P_A(x) P_##x
  #define P_B(x) P_##x
  #define P_C(x) (x)

  P_A(B(A(C(0))))

None of A, B, C are in scope without prefixes, so they get expanded
after the "called" macro's expansion:

  P_A(B(A(C)))
      -> P_B(A(C(0)))
      -> P_A(C(0)) (stops)

But this breaks when we hit the nested P_A macro.

---

For now I've gone with the temporary, and extra hacky, solution of
introducing a second LFSR_ATTR_ macro. This nesting of ATTR macros only
happens because of shrubs, and only ever goes 2 layers deep.

In the future maybe we should move away from implicit prefixing. They
have a few rough corners and may be a bit confusing for anyone new to
the code.
2023-10-13 22:00:55 -05:00
Christopher Haster c74ec1c133 Initial commit of basic file creation
Currently limited to inlined files and only simpler truncate-writes.

But still this lets us test file creation/deletion.

This is also enough logic to make it clear that, even though we have
some powerful high-level primitives, mapping file operations onto these
is still going to be non-trivial.
2023-09-17 11:04:44 -05:00
Christopher Haster 5f3994c83b Renamed mbits/mlimit to mleaf_bits/mleaf_limit
- mbits -> mleaf_bits
- mlimit -> mleaf_limit
- mweight -> mleaf_weight
- lfsr_mridmask -> lfsr_midrmask
- lfsr_mbidmask -> lfsr_midbmask

This is a bit tricky to name, since we want to clarify it's not the
mtree limit and not the mdir's actual rbyd weight. But this also risks
confusing around the difference between mdirs/mleaves (mdirs are
mtree's leaves).
2023-09-15 14:09:42 -05:00
Christopher Haster d44f9bdcd0 Prefer function-like macros when the result is a struct 2023-09-14 13:35:49 -05:00
Christopher Haster 518e9634e7 Tweaked gstate after mid changes
- Fixed LFSR_GRM_DSIZE upper bound, since our mids now fit in a single
  leb128.

- Renamed pgrm -> ggrm. To be honest I don't have a great name for this
  variable.
2023-09-14 13:27:31 -05:00
Christopher Haster d0c5bf1210 Adopted lfsr_data_from* pattern for internal data encoding
Taking advantage of the fact that these functions should never error,
changing the return type to lfsr_data_t allows all of the encoding
information to be passed around quite easily.

And, by giving each lfsr_data_from* function an LFSR_DATA_FROM* macro,
these functions can participate in our attr-list generating macros:

  LFSR_ATTR(-1, MTREE, 0, FROMBTREE(lfs, mtree, mtree_buf))

Though one thing to watch out for is the borrowed buffer that stores the
actual data. This might welcome use-after-free bugs since it's not super
clear the buffer remains borrowed. Will need to watch out for this.
2023-09-14 13:20:49 -05:00
Christopher Haster 7aa9280897 Added lfsr_rid/bid/mid/did_t types, tried using types more consistently
Adopted lfsr_rid/bid/mid/did_t where appropriate. This includes using
lfsr_rid_t for tag/rbyd weights. Although I am using lfsr_srid_t for
rbyd weights now, since it both captures the use of the sign bit and
reduces the number of casts a bit in the code.

I learned recently Zig has any-bit integers (e.g. uint31_t), and I'm
realizing how nice it would be to have those in this codebase.

Also tried to use lfs_size_t/lfs_off_t more correctly. In Linux/BSD,
only off_t is used for file-size-related operations and is usually much
larger than size_t. These were used interchangably in littlefs and their
original meaning kind of fell by the wayside. Getting their use right
will be important if littlefs ever supports different integer widths.
2023-09-14 11:31:28 -05:00
Christopher Haster b5c9b8eb49 Dropped lfsr_tag_next for tag+1
We were already using tag-1 several places anyways.
2023-09-14 00:37:42 -05:00
Christopher Haster 900ea807ae Changed to a shifted mid=bid.rid representation for debugging
This only matters for developers, not users, but it still helps a lot to
get debug representations right.

Since the exact mid encoding depends on the block_size in an unintuitive
manner, it's tricky to render in a debug-friendly way that is useful
both with and without tools.

Previously, I avoided shifting the bid representation, since this would
be closer to the value in the device, but this hides the actual
structure of the mtree. Now the bid is shifted, showing the underlying
mtree/mdir structure, at the cost of needing to know the number of mbits
to encode the mid back into an integer.

So for example, on a device with 4KiB blocks, or 8 mbits:

  mid=1
  mid=258
  mid=515

Becomes:

  mid=0.1
  mid=1.2
  mid=2.3

This continues to make the mbits a more fundamental part of littlefs,
but that's probably just how that's going to be.
2023-09-14 00:36:13 -05:00
Christopher Haster 2b98d62637 Tweaked mchildroot propagation again, adopted "mblocks" more consistently 2023-09-14 00:36:13 -05:00
Christopher Haster b06d48364d Switched to mid=-1 to detect removed mids, drops lfsr_mdir_isdropped
This is a simpler way to track dropped mids. Setting trunk=0 was more a
workaround that worked but added more purpose to the trunk field than
originally needed. The mdir's trunk usually still exists after all.

Using mid=-1 previously didn't work due to conflict with mid=-1 to
indicate an mdir is an mroot, but since removed mids only appear in the
opened-mdir list, and the opened-mdir list stores inlined mdirs as
mid=0, this is no longer a problem.

One downside of this change is we no longer get implicit NOENT behavior
from lfsr_rbyd_lookup when attempting to lookup a removed mid, but it
wasn't clear this behavior was going to be very useful...
2023-09-11 10:52:15 -05:00
Christopher Haster 441181d3d7 Added some more tests over reading dirs during fs mutation
These tests serve as a direct example of why we can't just return the
difference between the dir's bookmark mid and position mid, which is
unfortunate.
2023-09-05 10:51:46 -05:00
Christopher Haster c56124f90f Added handling of readonly grms to the mtree layer
This bit of code allows us to mount an "inconsistent" filesystem after
powerloss and behave as though we've fixed any pending grms without
actually fixing the grms. This lets the filesystem appear consistent
without needing to modify the disk, and allows truely readonly mounts
without sacrificing powerloss-resilience.

This works by just checking any readonly mid operations against pending
grms and returning NOENT if a fix would remove the mid. Fortunately the
more complex mid operations occur when mutating the filesystem, which we
can ignore as any mutation must be preceded by fixing pending grms.

This check has been added to lfsr_mtree_namelookup and lfsr_mtree_seek,
which should propagate the behavior to high-level functions with minimal
code impact.

This leaves only lfsr_mtree_lookup ignoring pending grms, which is useful
because we need it to actually fix the grms. I don't believe this
function will ever be called by a high-level function directly...

Coverage of readonly grms have also been added to the tests.
2023-09-05 10:10:33 -05:00
Christopher Haster cf90398197 Some small tweaks to mdir functions
- Added lfsr_mdir_lookupnext, for iteration through only a single mid.
  This is useful for MOVE attributes.

- Renamed LFSR_MDIR_MROOTANCHOR -> LFSR_MROOTANCHOR.

- Renamed functions that operate on mdir blocks lfsr_mdir_* ->
  lfsr_mblocks_*.

- Reordered arguments in lfsr_mdir_fetch.

- Renamed mrid_bits/mbid_weight -> mbits/mweight.
2023-09-05 10:10:33 -05:00
Christopher Haster f7900edc1c Updated dbg scripts with changes, adopted mbid.mrid in debug prints
This format for mids is a compromise in readability vs debugability.

For example, if our mbid weight is 256 (4KiB blocks), the 19th entry
in the second mdir would be the raw integer 275. With this mid format,
we would print it as 256.19.

The idea is to make it easy to see it's the 19th entry in the mdir while
still making it relatively easy to see that 256.19 and 275 are
equivalent when debugging.

---

The scripts also took some tweaking due to the mid change. Tried to keep
the names consistent, but I don't think it's worthwhile to change too
much of the scripts while they are working.
2023-09-05 10:10:30 -05:00
Christopher Haster a9b81820b0 Adopted rid-bound-dependent compressed mids.
This adopts a previously discarded idea for compressed mids with a few
tweaks to avoiding decoding the bid/rid portions as much as possible.

The idea of compressed mids is to shove both the mid bid and mid rid
into a single integer, saving RAM and potentially helping filesystem
integration where a unique per-file integer is useful.

Unfortunately this has proven tricky. littlefs fundamentally needs two
ids, one "bid" to lookup which mdir our entry resides on, and one "rid"
to lookup the entry in the mdir. It's tempting to use two half-sized
integers (16-bit for example), but this risks surprising limitations
around the number of files when blocks are either really large or
really small.

Optimally, we'd limit the number of bits reserved for the rid to the
upper bound of number of rids that can fit in a single mdir. This would
allows for more bids when the block size is small, and more rids when
the block size is large. This should roughly approximate the limits of
a per-file integer.

With a bit of math we can estimate the upper bound to be <=block_size/16
with our current compaction strategy.

This idea was previously discarded due to the overhead of extracting the
bids/rids when we need them, but the RAM savings and file-to-integer
mapping is too useful to give up. When it became clear half-width
integers wasn't really going to work, compressed mids became the new
plan:

  0bbbbbbb bbbbbbbb bbbbbbbb rrrrrrrr
  ^'-----------+-----------' '---+--'
  '------------|-----------------|---- sign-bit, reserved for driver
               |                 '---- nlog2(bs/16) bits for rid
               |                       (8-bits for 4KiB blocks)
               '---------------------- remaining bits for bid
                                       (23-bits for 4KiB blocks)

To reduce the overhead of encoding/decode bids/rids a few extra features
were added to the internal mdir APIs:

1. The mtree has been changed to store mids directly. Giving each mdir
   the upper bound as a weight. This allows direct lookup of mids
   without any sort of bid decoding, though does bake the upper bound
   estimate into the metadata of the filesystem, which isn't the
   cleanest design, but if it works it works.

   On the plus side, with this upper bound baked in to the filesystems,
   GRMs can be encoded in a single leb128, which is nice. This may have
   other savings if we ever store mids anywhere else in the filesystem.

2. rids are now mid relative in lfsr_mdir_lookup when non-negative. This
   is implemented with a simple condition that is hopefully optimized
   out when inlined, though there may be some room for improvement here.

3. rids are now mid relative in lfsr_mdir_commit. This was a bit tricky,
   but we can leverage the existing mechanisms for bid-relative rids
   used in the btree implementation.

The above changes make it so you can pass the mid around directly for
most of the mdir functions, hopefully reducing the mid decoding
overhead. This savings should only grow as more high-level filesystem
APIs are added.

Here is the resulting code/RAM changes for this entire change (from
before we adopted the mroot bit):

            code          stack          structs
  before:  20590           1784              908
  after:   20890 (+1.4%)   1744 (-2.3%)      864 (-5.1%)
2023-08-31 14:34:54 -05:00
Christopher Haster 2ea569c746 Fixed inlined mid -1/0 equivalence issue, made mid.bid left-leaning
We weren't comparing mid=-1/mid=0 correctly in lfsr_mdir_commit, which
can happend now thanks to inlining mids in our mroot. This went
unnoticed because we were just copying mroot.mid in our tests so we
never actually tested with mid=0. This is fixed now and the tests test
with a literal mid=0.

This also changes the mbids to be left-leaning, carving out an
mrid-sized number of bits from the mbid, making the route to compressed
mids easier.
2023-08-31 13:57:18 -05:00
Christopher Haster 19f2b24161 Dropped mroot bit, rely on context to determine mroots
This greatly simplifies mid handling at the cost of increased subtlety
around determining if a given mdir is an mroot.

Fortunately it turns out we can rely on context to determine if an mdir
is an mroot or not:

1. If an mdir's mid.bid == -1, it's an mroot. This is always true for
   fake mroots, since they can't hold any inlined mids.

2. If the mtree is inlined (mtree.weight == 0), all mdirs are mroots.
   This lets us use mid.bid=0 for inlined mids. We just need to check
   if the mtree is inlined before deciding if the mdir is an mroot or
   not.

The makes it so that for any non-mroot mdir, mid.bid=-1 is always a
reserved value. Which is very useful for compressed mids.
2023-08-31 13:45:24 -05:00
Christopher Haster 94941806c7 Changed mtree to be weighted by mdir upper bound
More on this when explaining compressed mids, but basically the idea is
instead of just storing all mdirs in our mtree as single element
entries, store each mdir in as a weighted entry, where the weight is a
known upper bound on the possible number of mid entries in a single
mdir.

With the current mid representation, this just complicates things
without much benefits. But with compressed mids it allows us to lookup
mdirs with the mid directly, and avoid decoding the bid from the mid in
some cases.

The mid-per-mdir upper bound is derived from the block size. We know:

1. Each tag needs <=2 alts+null with our current compaction strategy
2. Each tag/alt encodes to a minimum of 4 bytes

This gives us ~4*4 or ~16 bytes per mid at minimum. If we cram an mdir
with the smallest possible mids, this gives us at most ~block_size/16
mids in a single mdir before the mdir runs out of space.

Note we can't assume ~1/2 block utilization here, as an mdir may
temporarily fill with more mids before compaction occurs.
2023-08-31 13:32:21 -05:00
Christopher Haster 9f18b1fd50 Tweaked mid to use sign-bit to indicate mroots
This is an intermediate commit as a part of a tangent into compressed
mids.

The idea here, is instead of using bid=-1 as a special value for mroots,
use only the top bit to indicate mroots. This allows you to compare
against the grm/other uninlined mids by masking instead of signed
comparison.

This is valuable for compressed mids since extracting bids relies on
knowledge of the block size, and becomes quite a bit more expensive.

            mroot bid                             mroot cmp
  before:  0xffffffff  lfs_smax32(a, 0) == lfs_smax32(b, 0)
  after:   0x80000000  (a & 0x7fffffff) == (b & 0x7fffffff)

The implementation here is a bit clumsy. I think GCC may be not that
great at optimizing out copies of structs being passed around via
inlined functions. But this is only a proof-of-concept.
2023-08-31 13:15:58 -05:00
Christopher Haster 256430213d Dropped separate BTREE/BRANCH encodings
There is a bit of redundancy here, as we already know the weights of
btree's inner-branches from their parents. But in theory sharing the
same encoding for both the top level btree reference and inner-branches
should offer more chance for deduplication and hopefully less code.

This also moves some members around in the btree encoding so that the
redund blocks are at the beginning. This _might_ simplify decoding of
the variable-length redund blocks at some point.

Current btree encoding:

  .----+----+----+----.
  |       blocks    ...  redund leb128s (1-20 bytes)
  :                   :
  |----+----+----+----|
  |       trunk     ...  1 leb128 (1-5 bytes)
  |----+----+----+----|
  |       weight    ...  1 leb128 (1-5 bytes)
  |----+----+----+----|
  |       cksum       |  1 le32 (4 bytes)
  '----+----+----+----'

This also partially reverts some tag name changes:

- BNAME -> BRANCH
- DMARK -> BOOKMARK
2023-08-22 13:20:37 -05:00
Christopher Haster 20c036038a Some small tweaks to tests
- Adopted -1 as a cheap way to mark rbyds as unerased.

- Replaced literal references to alphas (alphas[0 % 26]) with their
  actual characters.
2023-08-22 00:15:23 -05:00
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 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 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 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 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 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 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 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 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 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