Commit Graph

219 Commits

Author SHA1 Message Date
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 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 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 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
Christopher Haster 4efb55e0d7 In tests/benches, renamed cfg -> CFG
This is to better indicate this is a runner generated variable.
2023-08-04 14:05:07 -05:00
Christopher Haster 1c128afc90 Renamed internal runner field filter -> if_
This makes it more consistent with the actual test field, at the cost of
the symbol collision.
2023-08-04 13:54:10 -05:00
Christopher Haster 5be7bae518 Replaced tn/bn prefixes with an actual dependency system in tests/benches
The previous system of relying on test name prefixes for ordering was
simple, but organizing tests by dependencies and topologically sorting
during compilation is 1. more flexible and 2. simplifies test names,
which get typed a lot.

Note these are not "hard" dependencies, each test suite should work fine
in isolation. These "after" dependencies just hint an ordering when all
tests are ran.

As such, it's worth noting the tests should NOT error of a dependency is
missing. This unfortunately makes it a bit hard to catch typos, but
allows faster compilation of a subset of tests.

---

To make this work the way tests are linked has changed from using custom
linker section (fun linker magic!) to a weakly linked array appended to
every source file (also fun linker magic!).

At least with this method test.py has strict control over the test
ordering, and doesn't depend on 1. the order in which the linker merges
sections, and 2. the order tests are passed to test.py. I didn't realize
the previous system was so fragile.
2023-08-04 13:33:00 -05:00
Christopher Haster 56adc60a80 Added grms and reporting of missing/orphaned dstarts in dbglfs.py
With a bit of color, this is very useful for debugging and finding
incorrect dstart/grm situations.

This was used to find and fix the bugs in the previous commit.
2023-07-25 13:58:11 -05:00
Christopher Haster e4ba43dd5f Extended grm to support two atomics removes
Ugh. I overlooked a weird corner case in rename's behavior that requires
changes to the grm to support.

POSIX's rename, which lfsr_rename is trying to match, supports renaming
files over existing files, effectively removing the previous file during
the rename.

This is supported, even if the files are directories, but with the
additional requirement that the previous directory is empty (matching
the behavior of lfsr_remove).

This creates a weird situation for littlefs. In order to remove
directories in littlefs, we need to atomically remove both the dstart
entry that reserves the directory's did and the directories entry in its
parent. This is made possible by using the grm to mark one entry as
pending removed while removing the other.

But in order to rename atomically, we need to use the grm to mark the
source of the rename as removed while creating/replacing the destination
of the rename.

So we end up needing two grms simultaneously.

This is extra annoying because the niche case of renaming a directory
over another empty directory is the only case where we need two grms,
but this requirement almost doubles the grm size both in-ram and
reserved in every mdir, from 11 bytes to 21 bytes, and increases the
lfs_t size by 28 bytes.

---

Anyways, this commit extends the grm to support up to two pending removes.

Fortunately the implementation was simple since we already have a type
field that can be extended, and grm operations just needed to be
changed from if statements to for loops.
2023-07-25 13:30:04 -05:00
Christopher Haster c5e84e874f Changed how fuzz tests are iterated to allow powerloss-fuzz testing
Instead of iterating over a number of seeds in the test itself, the
seeds are now permuted as a part of normal test defines.

This lets each seed take advantage of other test features, mainly the
ability to test powerlosses heuristically.

This is probably how it should have been done in the first place, but
the permutation tests can't do this since the number of permutations
changes as the size of the test input changes. The test define system
can't handle that very well.

The tradeoffs here are:

- We can't do cross-fuzz checks, such as the balance checks in the rbyd
  tests, though those really should be moved to benchmarks anyways.

- The large number of cheap fuzz permutations skews the total
  permutation count, though I'm not sure this matters.

  before: 3083 permutations (-Gnor)
  after: 409893 permutations (-Gnor)
2023-07-18 21:40:44 -05:00
Christopher Haster c928ed131f Changed all dir tests to be reentrant
To help with this, added TEST_PL, which is set to true when powerloss
testing. This way tests can check for stronger conditions (no EEXIST)
when not powerloss testing.

With TEST_PL, there's really no reason every test in t5_dirs shouldn't
be reentrant, and this gives us a huge improvement of test coverage very
cheaply.

---

The increased test coverage caught a bug, which is that gstate wasn't
being consumed properly when mtree uninlining. Humorously, this went
unnoticed because the most common form of mtree uninlining, mdir splitting,
ended up incorrectly consuming the gstate twice, which canceled itself
out since the consume operation is basically just xor.

Also added support for printing dstarts to dbglfs.py, to help debugging.
2023-07-18 21:40:43 -05:00
Christopher Haster 97f867b28d Added powerloss testing over lfsr_mkdir, fixed grm bugs
The grm bugs were mostly issues with:

1. Not maintaining the on-disk grm state in RAM (lfs->grm) correctly,
   this needs to be updated correctly after every commit or littlefs
   gets a confused.

2. lfsr_fs_fixgrm got a bit confused when it was missed when changing
   the no-rm encoding from 0 to -2. Added some inline functions to help
   avoid this in the future.

3. Leaking information due to mixing fixed sized and variable sized
   encodings of the grm delta in places. This is a bit tricky to write
   an assert for as we don't parse the full grm when we see a no-rm grm.
2023-07-18 21:40:43 -05:00
Christopher Haster b98ac119c7 Added scripts/dbglfs.py for debugging the filesystem tree
Currently this can show:

- The filesystem tree:

    $ ./scripts/dbglfs.py disk -B4096
    littlefs v2.0 0x{0,1}.bd4, rev 1, weight 41
    mdir         ids      name                        type
    {00ce,00cf}:      0.1 dir0000                     dir 0x1070c73
    {0090,0091}:     2.30 |-> child0000               dir 0x8ec7fb2
    {0042,0043}:    24.35 |   |-> grandchild0000      dir 0x32d990b
    {0009,000a}:     25.0 |   |-> grandchild0001      dir 0x1461a08
                     25.1 |   |-> grandchild0002      dir 0x216e9fc
                     25.2 |   |-> grandchild0003      dir 0x7d6aff
                     25.3 |   |-> grandchild0004      dir 0x4b70e14
                     25.4 |   |-> grandchild0005      dir 0x6dc8d17
                     25.5 |   |-> grandchild0006      dir 0x58c7ee3
                     25.6 |   '-> grandchild0007      dir 0x7e7fde0
    {0090,0091}:     2.31 |-> child0001               dir 0xa87fcb1
    {0077,0078}:     29.1 |   |-> grandchild0000      dir 0x12194f5
                     29.2 |   |-> grandchild0001      dir 0x34a17f6
    ...

- The on-disk filesystem config:

    $ ./scripts/dbglfs.py disk -B4096 -c
    littlefs v2.0 0x{0,1}.bd4, rev 1, weight 41
    mdir         ids      tag                     data (truncated)
         config: major_version 2                  02                       .
                 minor_version 0                  00                       .
                 csum_type 2                      02                       .
                 flags 0                          00                       .
                 block_size 4096                  80 20                    .
                 block_count 256                  80 02                    ..
    ...

- Any global-state on-disk:

    $ ./scripts/dbglfs.py disk -B4096 -g -d
    littlefs v2.0 0x{0,1}.bd4, rev 1, weight 41
    mdir         ids      tag                     data (truncated)
         gstate: grm none                         00 00 00 cc 05 57 ff 7f .....W..
    {0000,0001}:       -1 grm 8                   01 03 24 cc 05 57 ff 7f ..$..W..
    {00ce,00cf}:        0 grm 3                   00 2f 1b                ./.
    {00d0,00d1}:        1 grm 3                   01 04 01                ...

  Note this already reveals a bug, since grm none should be all zeros.

Also made some other minor tweaks to dbg scripts for consistency.
2023-07-18 21:40:41 -05:00
Christopher Haster 61c51b699a In scripts, adopted aggresive width-finding for unbounded recursion
This makes it easier to read the output, at a cost of these scripts not
terminating if the underlying call sctucture contains loops.

Previously these scripts would not terminate, but at least output the
call tree as they visit each function. This was hard to read, and wasn't
really that useful? If you hit a case with infinite recursion, you can
limit the output size explicitly with -Z.

Note this also drops --tree in stack.py. Since we get more readable
output, this flag is less useful. This simplifies the script a bit.
2023-07-18 21:40:39 -05:00
Christopher Haster 3959545a9f Some dbg script work
- Changed how names are rendered in dbgbtree.py/dbgmtree.py to be
  consistent with non-names. The special rendering isn't really worth it
  now that names aren't just ascii/utf8.

- Changed the ordering of raw/device/human rendering of btree entries to
  be more consistent with rendering of other entries (don't attempt to
  group btree entries).

- Changed dbgmtree.py header to show information about the mtree.
2023-07-18 21:40:38 -05:00
Christopher Haster c2d9f1b047 Implemented, but untested, global-removes
This implementation is in theory correct, but of course, being untested,
who knows?

Though this does come with remounting added to all of the directory
tests. This effectively tests that all of the directory creation tests
we have so far maintain grm=0 after each unmount-mount cycle. Which is
valuable.
2023-07-18 21:40:36 -05:00
Christopher Haster cc0ac25b5e Implemented infrastructure necessary for global-removes
This has, in theory, global-removes (grm) being written out as a part of
of directory creation, but they aren't used in any form and so may not
be being written correctly.

But it did require quite a bit of problem solving to get to this point
(the interactions between mtree splitsand grms is really annoying), so
it's worth a commit.
2023-07-18 21:40:30 -05:00
Christopher Haster da810aca26 Implemented mtree path/dname lookup, rudimentary lfsr_mkdir/lfsr_dir_read
This makes it now possible to create directories in the new system.

The new system now uses a single global "mtree" to store all metadata
entries in the filesystem. In this system, a directory is simply a range
of metadata entries. This has a number of benefits, but does come with
its own problems:

1. We need to indicate which directory each file belongs to. To do this
   the file's name entry has been changed to a tuple of leb128-encoded
   directory-id + actual file name:

     01 66 69 6c 65 2e 74 78 74  .file.txt
      ^ '----------+----------'
      '------------|------------ leb128 directory-id
                   '------------ ascii/utf8 name

   If we include the directory-id as part of filename comparison, files
   should naturally be next to other files in the same directory.

2. We need a way allocate directory-ids for new directories. This turns
   out to be a bit more tricky than I expected.

   We can't use any mid/bid/rid inherent to the mtree, because these
   change on any file creation/deletion. And since we commit the did
   into the tree, that's not acceptable.

   Initially I though you could just find the largest did and increment,
   but this gives you no way to reclaim deleted dids. And sure, deleted
   dids have no storage consumption, but eventually you will overflow
   the did integer. Since this can suddenly happen in a filesystem
   that's been in a steady-state for years, that's pretty unnacceptable.

   One solution is to do a simple linear search over the mtree for an
   unused did. But with a runtime of O(n^2 log(n)), this raises
   performance concerns.

   Sidenote: It's interesting to note that the Linux kernel's allocation
   of process-ids, a very similar problem, is surprisingly complex and
   relies on a radix-tree of bitmaps (struct idr). This suggests I'm not
   missing an obvious solution somewhere.

   The solution I settled on here is to instead treat the set of dids as
   a sort of hash table:

   1. Hash the full directory path into a did.
   2. Perform a linear search until we have no collision.

     leb128(truncate28(crc32c("dir")))
          .--------'
          v
     9e cd c8 30 66 69 6c 65 2e 74 78 74  ...0file.txt
     '----+----' '----------+----------'
          '-----------------|------------ leb128 directory-id
                            '------------ ascii/utf8 name

   Worst case, this can still exhibit the worst case O(n^2 log(n))
   performance when we are close to full dids. However that seems
   unlikely to happen in practice, since we don't truncate our hashes,
   unlike normal hash tables. An additional 32-bit word for each file
   is a small price to pay for a low-chance of collisions.

   In the current implementation, I do truncate the hash to 28-bits.
   Since we encode the hash with leb128, and hashes are statistically
   random, this gives us better usage of the leb128 encoding. However
   it does limit a 32-bit littlefs to 256 Mi directories.

   Maybe this should be a configurable limit in the future.

   But that highlights another benefit of this scheme. It's easy to
   change in the future without disk changes.

3. We need a way to know if a directory-id is allocated, even if the
   directory is empty.

   For this we just introduce a new tag: LFSR_TAG_DSTART, which
   is an empty file entry that indicates the directory at the given did
   in the mtree is allocated.

   To create/delete these atomically with the reference in our parent
   directory, we can use the GRM system for atomic renames.

   Note this isn't implemented yet.

This is also the first time we finally get around to testing all of the
dname lookup functions, so this did find a few bugs, mostly around
reporting the root correctly.
2023-07-05 13:41:21 -05:00
Christopher Haster cf588ac3fa Dropped alt-always as an rbyd trunk terminator
Now that tree rebalancing is implemented and needed a null terminator
anyways, I think it's clear that the benefit of the alt-always pointers
as trunk terminator has pretty limited value.

Now a null or other tag is needed for every trunk, which simplifies
checks for end-of-trunk.

Alt-always tags are still emitted for deletes, etc, but there their
behavior is implicit, so no special checks are needed. Alt-always tags
are naturally cleaned up as a part of rbyd pruning.
2023-06-27 00:49:31 -05:00
Christopher Haster 43dc3a5c8d Implemented tree rebalancing during rbyd compaction
This isn't actually for performance reasons, but to reduce storage
overhead of the rbyd metadata tree, which was showing signs of being
problematic for small block sizes.

Originally, the plan for compaction was to rely on the self-balancing
rbyd append algorithm and simply append each tag to a new tree.
Unfortunately, since each append requires a rewrite of the trunk
(current search path), this introduces ~n*log(n) alts but only uses ~n alts
for the final tree. This really starts to put pressure on small blocks,
where the exponential-ness of the log doesn't kick in and overhead
limits are already tight.

Measuring lfsr_mdir_commit code size, this shows a ~556 byte cost on
thumb: 16416 -> 16972 (+3.4%). Though there are still some optimizations
on the table, this implementation needs a cleanup pass.

               alt overhead  code cost
  rebalance:        <= 28*n      16972
  append:    <= 24*n*log(n)      16416

Note these all assume worst case alt overhead, but we _need_ to assume
worst case for our rbyd estimations, or else the filesystem can get
stuck in unrecoverable compaction states.

Because of the code cost I'm not sure if rebalancing will stay, be
optional, or replace append-compaction completely yet.

Some implementation notes:

- Most tree balancing algorithms rely on true recursion, I suspect
  recursion may be a hard requirement in general, but it's hard to find
  bounded-ram algorithms.

  This solution gets around the ram requirement by leveraging the fact
  that our tags exist in a log to build up each layer in the tree
  tail-recursively. It's interesting to note that this is a special
  case of having little ram but lots of storage.

- Humorously this shouldn't result in a performance improvement. Rbyd
  trees result in a worst case 2*log(n) height, and rebalancing gives us
  a perfect worst case log(n) height, but, since we need an additional
  alt pointer for each node in our tree, things bump back up to 2*log(n).

- Originally the plan was to terminate each node with an alt-always tag,
  but during implementation I realized there was no easy way to get the
  key that splits the children with awkward tree lookups. As a
  workaround each node is terminated with an altle tag that contains the
  key followed by an unreachable null tag. This is redundant information,
  but makes the algorithm easier to implement.

  Fortunately null tags use the smallest tag encoding, which isn't that
  small, but that means this wastes at most 4*n bytes.

- Note this preserves the first-tag-always-ends-up-at-off=0x4 rule, which
  is necessary for the littlefs magic to end up in a consistent place.

- I've dropped dropping vestigial names for now, which means vestigial
  names can remain in btrees indefinitely. Need to revisit this.
2023-06-25 15:23:46 -05:00
Christopher Haster 799e7cfc81 Flipped btree encoding to allow variable redund blocks
This should have been done as a part of the earlier tag reencoding work,
since having the block at the end was what allowed us to move the
redund-count out of the tag encoding.

New encoding:

  [-- 32-bit csum   --]
  [-- leb128 weight --]
  [-- leb128 trunk  --]
  [-- leb128 block  --]

Note that since our tags have an explicit size, we can store a variable
number of blocks. The plan is to use this to eventually store redundant
copies for error correction:

  [-- 32-bit csum   --]
  [-- leb128 weight --]
  [-- leb128 trunk  --]
  [-- leb128 block  --] -.
  [-- leb128 block  --]  +- n redundant blocks
  [-- leb128 block  --]  |
           ...          -'

This does have a significant tradeoff, we need to know the checksum size
to access the btree structure. This doesn't seem like a big deal, but
with the possibility of different checksum types may be an annoying
issue.

Note that FCRC was also flipped for consistency.
2023-06-19 16:08:50 -05:00
Christopher Haster e79c15b026 Implemented wide tags for both rbyd commit and lookup
Wide tags are a happy accident that fell out of the realization that we
can view all subtypes of a given tag suptype as a range in our rbyd.
Combining this with how natural it is to operate on ranges in an rbyd
allows us to perform operations on an entire range of subtypes as though
it were a single tag.

- lookup wide tag => find the smallest tag with this tag's suptype, O(log(n))
- remove wide tag => remove all tags with this tag's suptype, O(log(n))
- append wide tag => remove all tags with this tag's suptype, and then
  append our tag, O(log(n))

This is very useful for littlefs, where we've already been using tag's
subtypes to hold extra type info, and have had to rely on awkward
alternatives such as deleting existing subtypes before writing our new
subtype.

For example, when committing file metadata (not yet implemented), we can
append a wide struct tag to update the metadata while also clearing out any
lingering struct tags from previous commits, all in one rbyd append
operation.

This uses another mode bit in-device to change the behavior of
lfsr_rbyd_commit, of which we have a couple:

  vwgrtttt 0TTTTTTT
  ^^^^---^--------^- valid bit (currently unused, maybe errors?)
   '||---|--------|- wide bit, ignores subtype (in-device)
    '|---|--------|- grow bit, don't create new id (in-device)
     '---|--------|- rm bit, remove this tag (in-device)
         '--------|- 4-bit suptype
                  '- leb128 subtype
2023-06-19 16:08:43 -05:00
Christopher Haster 2467d2e486 Added a separate tag encoding for the mtree
This helps with debugging and can avoid weird issues if a file btree
ever accidentally ends up attached to id -1 (due to fs bug).

Though a separate encoding isn't strictly necessary, maybe this should
be reverted at some point.
2023-06-18 15:12:43 -05:00
Christopher Haster 7180b70c9c Allowed "alta" (altbgt 0) to terminate rbyd trunks, dropped rm bit
This replaces unr with null on disk, though note both the rm bit and unr
are used in-device still, they just don't get written to disk.

This removes the need for the rm bit on disk. Since we no longer need to
figure out what's been removed during fetch, we can save this bit for both
internal and future on-disk use.

Special handling of alta allows us to avoid emitting an unr tag (now null) if
the current trunk is truly unreachable. This is minor now, but important
for a theoretical rbyd rebalance operation (planned), which brings the
rbyd overhead down from ~3x to ~2x.

These changes give us two ways to terminate trunks without a tag:

1. With an alta, if the current trunk is unreachable:

     altbgt 0x403 w0 0x7b
     altbgt 0x402 w0 0x29
     alta w0 0x4

2. With a null, if the current trunk is reachable, either for
   code convenience or because emitting an alta is impossible (an empty
   rbyd for example):

     altbgt 0x403 w0 0x7b
     altbgt 0x402 w0 0x29
     altbgt 0x401 w0 0x4
     null
2023-06-17 18:11:45 -05:00
Christopher Haster 2113d877d6 Moved bits around in tag encoding to allow leb128 custom attributes
Yet another tag encoding, but hopefully narrowing in on a good long term
design. This change trades a subtype bit for the ability to extend
subtypes indefinitely via leb128 in the future.

The immediate benefit is ~unlimited custom attributes, though I'm not
sure how to make this configurable yet. Extended custom attributes may
have a significant impact on alt tag sizes, so it may be worth
defaulting to only 8-bit custom attributes still.

Tag encoding:

   vmmmtttt 0TTTTTTT 0wwwwwww 0sssssss
   ^--^---^--------^--------^--------^- valid bit
      '---|--------|--------|--------|- 3-bit mode
          '--------|--------|--------|- 4-bit suptype
                   '--------|--------|- leb128 subtype
                            '--------|- leb128 weight
                                     '- leb128 size/jump

This limits subtypes to 7-bits, but this seems very reasonable at the
moment.

This also seems to limit custom attributes to 7-bits, but we can use two
separate suptypes to bring this back up to 8-bits. I was planning to do
this anyways to have separate "user-attributes" and "system-attributes",
so this actually fits in really well.
2023-06-16 01:51:33 -05:00
Christopher Haster b43d2d2d9d In watch.py, fixed --keep-open-paths typo, made --keep-open implicit sometimes
So now:

  ./scripts/watch.py -K lfs.c ./script.sh

Does the reasonable thing.
2023-06-16 01:51:25 -05:00
Christopher Haster b05db8e3d3 Added support for lists of conditional ifs in test/bench.py
Any conditions in both the suites and cases are anded together to
determine when the test/bench should run.

Accepting a list here makes it easier to compose multiple conditions,
since toml-level elements are a bit easier to modify than strings of
C expressions.
2023-06-01 17:40:51 -05:00
Christopher Haster 07244fb2d4 In test/bench.py, added "internal" flag
This marks internal tests/benches (case.in="lfs.c") with an otherwise-unused
flag that is printed during --summary/--list-*. This just helps identify which
tests/benches are internal.
2023-06-01 17:40:48 -05:00
Christopher Haster 82027f3d90 Changed bench/test.py to error if explicit suite/case can't be found
Previously no matches would noop, which, while consistent with an empty
test suite that contains no tests but shouldn't really error, this made
it easy to miss when a typo would cause tests to be missed.

Also added a bit of color to script-level errors in test/bench.py
2023-06-01 17:16:21 -05:00
Christopher Haster 2339e9865f Tweaked dbgmtree.py -Z flag to include mroots as depth
This helps debug a corrupted mtree with cycles, which has been a problem
in the past.

Also fixed a small rendering issue with dbgmtree.py not connecting inner
tree edges to mdir roots correctly during rendering.
2023-06-01 13:52:14 -05:00
Christopher Haster c60fa69ce1 Optimized dbg*.py tree generation/rendering by deduplicating edges
Optimizing a script? This might sound premature, but the tree rendering
was, uh, quite slow for any decently sized (>1024) btree.

The main reason is that tree generation is quite hacky in places, repeatedly
spitting out multiple copies of the inner node's rbyd trees for example.

Rather than rewrite the tree generation implementation to be smarter,
this just changes all edge representations to namedtuples (which may
reduce memory pressure a bit), and collects them into a Python set.

This has the effect of deduplicating generated edges efficiently, and
improved the rendering performance significantly.

---

I also considered memoizing rbyd tree, but dropped the idea since the
current renderer performs well enough.
2023-05-30 18:17:51 -05:00
Christopher Haster af0c3967b4 Adopted new tree renderer in dbgmtree, implemented mtree rendering
In addition to plugging in the rbyd and btree renderers in dbgbtree.py,
this required wiring in rbyd trees in the mdirs and mroots.

A bit tricky, but with a more-or-less straightforward implementation thanks
to the common edge description used for the tree renderer.

For example, a relatively small mtree:

  $ ./scripts/dbgmtree.py disk -B4096 -t -i
  mroot 0x{0,1}.45, rev 1, weight 0
  mdir                     ids   tag                     ...
  {0000,0001}: .--------->    -1 magic 8                 ...
               | .------->       config 21               ...
               +-+-+             btree 7                 ...
    0006.000a:     | .-+       0 mdir w1 2               ...
  {0002,0003}:     | | '->   0.0 inlined w1 1024         ...
    0006.000a:     '-+-+       1 mdir w1 2               ...
  {0004,0005}:         '->   1.0 inlined w1 1024         ...
2023-05-30 18:10:32 -05:00
Christopher Haster 9b803f9625 Reimplemented tree rendering in dbg*.py scripts
The goal here was to add the option to show the combined rbyd trees in
dbgbtree.py/dbgmtree.py.

This was quite tricky, (and not really helped by the hackiness of these
scripts), but was made a bit easier by adding a general purpose tree renderer
that can render a precomputed set of branches into the tag output.

For example, a 2-deep rendering of a simple btree with a block size of
1KiB, where you can see a bit of the emergent data-structure:

  $ ./scripts/dbgbtree.py disk -B1024 0x223 -t -Z2 -i
  btree 0x223.90, rev 46, weight 1024
  rbyd                       ids       tag                     ...
  0223.0090:     .-+             0-199 btree w200 9            ...
  00cb.0048:     | |     .->      0-39 btree w40 7             ...
                 | | .---+->     40-79 btree w40 7             ...
                 | | | .--->    80-119 btree w40 7             ...
                 | | | | .->   120-159 btree w40 7             ...
                 | '-+-+-+->   160-199 btree w40 7             ...
  0223.0090: .---+-+           200-399 btree w200 9            ...
  013e.004b: |     |     .->   200-239 btree w40 7             ...
             |     | .---+->   240-279 btree w40 8             ...
             |     | | .--->   280-319 btree w40 8             ...
             |     | | | .->   320-359 btree w40 8             ...
             |     '-+-+-+->   360-399 btree w40 8             ...
  0223.0090: | .---+           400-599 btree w200 9            ...
  01a7.004c: | |   |     .->   400-439 btree w40 8             ...
             | |   | .---+->   440-479 btree w40 8             ...
             | |   | | .--->   480-519 btree w40 8             ...
             | |   | | | .->   520-559 btree w40 8             ...
             | |   '-+-+-+->   560-599 btree w40 8             ...
  0223.0090: | | .-+           600-799 btree w200 9            ...
  021e.004c: | | | |     .->   600-639 btree w40 8             ...
             | | | | .---+->   640-679 btree w40 8             ...
             | | | | | .--->   680-719 btree w40 8             ...
             | | | | | | .->   720-759 btree w40 8             ...
             | | | '-+-+-+->   760-799 btree w40 8             ...
  0223.0090: +-+-+-+          800-1023 btree w224 10           ...
  021f.0298:       |     .->   800-839 btree w40 8             ...
                   |   .-+->   840-879 btree w40 8             ...
                   |   | .->   880-919 btree w40 8             ...
                   '---+-+->  920-1023 btree w104 9            ...

This tree renderer also replaces the adhoc tree rendere in dbgrbyd.py
for consistency.
2023-05-30 18:04:54 -05:00
Christopher Haster b67fcb0ee5 Added dbgmtree.py for debugging the littlefs metadata-tree
This builds on dbgrbyd.py and dbgbtree.py by allowing for quick
debugging of the littlefs mtree, which is a btree of rbyd pairs with a
few bells and whistles.

This also comes with a number of tweaks to dbgrbyd.py and dbgbtree.py,
mostly changing rbyd addresses to support some more mdir friendly
formats.

The syntax for rbyd addresses is starting to converge into a couple
common patterns, which is nice for quickly determining what type of
address you are looking at at a glance:

- 0x12         => An rbyd at block 0x12
- 0x12.34      => An rbyd at block 0x12 with trunk 0x34
- 0x{12,34}    => An rbyd at either block 0x12 or block 0x34 (an mdir)
- 0x{12,34}.56 => An rbyd at either block 0x12 or block 0x34 with trunk 0x56

These scripts have also been updated to support any number of blocks in
an rbyd address, for example 0x{12,34,56,78}. This is a bit of future
proofing. >2 blocks in mdirs may be explored in the future for the
increased redundancy.
2023-05-30 18:04:54 -05:00
Christopher Haster 975a98b099 Renamed a few superblock-related things
- supermdir -> mroot
- supermagic -> magic
- superconfig -> config
2023-05-30 14:46:56 -05:00
Christopher Haster 738eb52159 Tweaked tag encoding/naming for btrees/branches
LFSR_TAG_BNAME => LFSR_TAG_BRANCH
LFSR_TAG_BRANCH => LFSR_TAG_BTREE

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

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

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

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

---

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

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

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

This may be worth revisiting at some point to see if there's any other
unexpected tradeoffs.
2023-05-30 13:28:35 -05:00
Christopher Haster 85ebdd0881 Reintroduced Brent's algorithm for cycle detection in lfsr_mount 2023-05-30 13:28:07 -05:00
Christopher Haster 70a3a2b16e Rough implementation of lfsr_format/mount/unmount
This work already indicates we need more data-related helper
functions. We shouldn't need this many function calls to do "simple"
operations such as fetch the superconfig if it exists.
2023-05-30 13:16:03 -05:00
Christopher Haster a511696bad Added ability to bypass rbyd fetch during B-tree lookups
This is an absurd optimization that stems from the observation that the
branch encoding for the inner-rbyds in a B-tree is enough information to
jump directly to the trunk of the rbyd without needing an lfsr_rbyd_fetch.

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

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

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

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

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

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

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

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

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

It's also worth mentioning this will likely have a large impact on B-tree
traversal speed. Which is huge as I am expecting B-tree traversal to be
the main bottleneck once garbage-collection (or its replacement) is
involved.
2023-04-14 00:51:34 -05:00
Christopher Haster 7eb0c4763a Reversed LFSR_ATTR id/tag argument order
I've been wanting to make this change for a while now (tag,id => id,tag).
The id,tag order matches the common lexicographic order used for sorting
tuples. Sorting tag,id tuples by their id first is less common.

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

I may be thinking to much about this, but it does affect a significant
amount of the codebase.
2023-04-14 00:43:33 -05:00
Christopher Haster 2142b4a09d Reworked dbgrbyd.py's tree renderer to make more sense
While the previous renderer was "technically correct", the attempt to
map rotated alts to their nearest neighbor just made the resulting tree
an unreadable mess.

Now the renderer prunes alts with unreachable edges (like they would be
during lfsr_rbyd_append). And aligns all alts with their destination
trunk. This results in a much more readable, if slightly less accurate,
rendering of the tree.

Example:

  $ ./scripts/dbgrbyd.py -B4096 disk 0 -t
  rbyd 0x0, rev 1, size 1508, weight 40
  off                     ids   tag                     data (truncated)
  0000032a:         .-+->     0 reg w1 1                73                       s
  00000026:         | '->   1-5 reg w5 1                62                       b
  00000259: .-------+--->  6-11 reg w6 1                6f                       o
  00000224: |     .-+-+-> 12-17 reg w6 1                6e                       n
  0000028e: |     | | '->    18 reg w1 1                70                       p
  00000076: |     | '---> 19-20 reg w2 1                64                       d
  0000038f: |     |   .-> 21-22 reg w2 1                75                       u
  0000041d: | .---+---+->    23 reg w1 1                78                       x
  000001f3: | |       .-> 24-27 reg w4 1                6d                       m
  00000486: | | .-----+-> 28-29 reg w2 1                7a                       z
  000004f3: | | | .-----> 30-31 reg w2 1                62                       b
  000004ba: | | | | .---> 32-35 reg w4 1                61                       a
  0000058d: | | | | | .-> 36-37 reg w2 1                65                       e
  000005c6: +-+-+-+-+-+-> 38-39 reg w2 1                66                       f
2023-04-14 00:41:55 -05:00
Christopher Haster 0ccf283321 Changed in-tree tags to store their weights
Sorting weights instead of ids just had a number of benefits, suggesting
this is a better design:

- Calculating the id and delta of each rbyd trunk is surprisingly
  easier - id is now just lower+w-1, and no extra conditions are
  needed for unr tags, which just have a weight of zero.

- Removes ambiguity around which id unr tags should be assigned to,
  especially unrs that delete ids.

- No more +-1 weirdness when encoding/decoding tag ids - the weight
  can be written as-is and -1 ids are infered from their weight and
  position in the tree (lower+w-1 = 0+0-1 = -1).

- Weights compress better under leb128 encoding, since they are usually
  quite small.
2023-04-14 00:32:05 -05:00
Christopher Haster 13852df071 Switched back to altgt 0 for unreachable tags, made btree tests pass again
This fixed two notable bugs:

1. Using "altle 0xfff0" to terminate unreachable rbyd trunks threw off
   id calculations in lfsr_rbyd_fetch searches. We derive the tag's
   id+weight from the lower bound calculated as the sum of all "altle"s
   and an always-followed "altle 0xfff0" throws this off.

   We _could_ derive the tag's id+weight from the upper bound, inverting
   this relationship, but decided to revert back to using "altgt 0" to
   terminate unreachable rbyd trunks.

   Using the lower bound is more intuitive, and "altgt 0" has the
   benifit of supporting variable-length tags if we ever need to adopt
   those.

   To avoid the previous issues around 0-tag holes (which was the original
   motivation for altle 0xfff0), 0-tags are now automatically adjusted
   in lfsr_rbyd_lookup, and avoided in lfsr_rbyd_append.

   But note! if any implemention tries to look up 0-tags, this will
   eventually break! See previous commits for more info.

2. Unfortunately, we can't combine branch updates and weight updates in
   lfsr_btree_commit in the general case.

   If our btree contains bname tags, the weight is attached to the
   bname tag, separately from the branch tag.

   Branch updates in lfsr_btree_commit need two separate attrs for the
   weight and branch struct for this reason, which is unfortunate.

   The amount of extra conditions to make bname+branch pairs work makes
   me want to redesign the inner-nodes of the btrees, but I can't think
   of a better way to approach the problem.
2023-04-14 00:04:58 -05:00
Christopher Haster e5ad09b380 Some btree progress, implementing rbyd-tag-weight changes 2023-04-14 00:02:51 -05:00
Christopher Haster 85bd28951c Solved rbyd grow/insert ambiguity by adding a device-only "mk" bit
This "mk" bit must not be written to disk, it would conflict with the
other non-tree tag encodings. But we can use this bit in the context of
lfsr_tag_append to disambiguate tags changing weight from inserting new
tags.

Note that in the context of rbyd compactions, this will make things a bit
weird, since it's no longer just a direct one-to-one copy of each tag.

To make compactions a bit easier, this implementation allows the "mk"
bit to be set on any tag and ignores it when the weight delta is zero.

It turns out that this scheme greatly simplifies the awkward
leaf-split-alt calculation that previously had several if statements to
handle different corner cases, with the caveat that "mk" tags need their
ids adjusted by +1. Added this adjustment directly into lfsr_rbyd_append
for now, so the upper-level interface can be a bit more intuitive.
Though this may need to change later if it is more confusing than
helpful.
2023-04-13 19:00:39 -05:00
Christopher Haster 5a1c36f210 Attempting to add weight changes to every rbyd append
This does not work as is due to ambiguity with grows and insertions.

Before, these were disambiguated by seperate grow and attr tags. You
effectively grew the neighboring id before claiming its weight
as yours. But now that the attr itself creates the grow/insertion,
it's ambiguous which one is intended.
2023-04-13 18:58:56 -05:00