Commit Graph

480 Commits

Author SHA1 Message Date
Christopher Haster 588a103db7 Working through 3-leb range deletes, proving to be problematic
The seperate interactions between ids and keys is new and confusing.
This was something that the previous combined weights hid.
2023-02-12 17:14:57 -06:00
Christopher Haster 5e0418029d Added mixed attr+id testing, tweaks to 3-leb tag utility functions 2023-02-12 17:14:57 -06:00
Christopher Haster 08f5d9ddf4 Middle of a rewrite for 3-leb encoding, but rbyd appends and creates both work
If we combine rbyd ids and B-tree weights, we need 32-bit ids since this
will eventually need to cover the full range of a file. This simply
doesn't fit into a single word anymore, unless littlefs uses 64-bit tags.
Generally not a great idea for a filesystem targeting even 8-bit
microcontrollers.

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

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

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

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

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

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

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

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

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

But the rbyd trees _are_ already weighted. A better solution might be to
go back and rethink the seperation of B-tree weights and rbyd ids.
Unfortunately, with only 16-bits available for rbyd ids, this would
likely require a rewrite of how rbyd tags are encoded...
2023-02-12 17:14:42 -06:00
Christopher Haster 55b072e761 Opened up rbyd testing for all geometries, and fixed related bugs
- Caching is still presenting issues with the new requirements for
  rbyd trees, in this case the default bd, with 64 byte progs, revealed
  and issue where rcache could become outdated when reading from disk
  while ignoring what's in the pcache.

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

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

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

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

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

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

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

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

New encoding:

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

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

Also a large amount of name changes and other cleanup.
2023-02-12 17:13:57 -06:00
Christopher Haster 3d008b793d This method of leaf splitting works, but I want to explore another option
Committing current progress.
2023-02-12 15:18:15 -06:00
Christopher Haster d8540974d4 Significant cleanup of lfs_rbyd_append, simplified pruning rules 2023-02-12 15:17:54 -06:00
Christopher Haster 179a1df3d3 Changed rbyd removes to remove from the tree, similar to deletes
Tag removal is basically a range-delete of one that doesn't change
the tree weights. This deduplicates the two methods of deleting tags and
completely gets rid of tombstoning.

Note we still need a "removed tag" encoding so that we can invalidate
tags that may be found during fetch operations. Fortunately this
encoding is basically free due to overlap with alt encoding.
2023-02-12 15:14:06 -06:00
Christopher Haster 92ce5df949 Added tests for theoretical bound on tree height and recovery from delete-all
- Unless there is a bug, rbyd trees should be strictly <= (2*log2(n)+1)
  in height. The extra +1 from traditional red-black trees is due to the
  introduced to-be-pruned alt, but since we clean those up as soon as we
  can, only one will ever exist in any search path.

  This also holds true with range deletion, however the definition of n
  changes to the number of tree operations. This is the same for tombstoning.

- Delete-all recovery is a bit tricky because we have no tree at that
  point, which is weird for an append-only data-structure.
2023-02-12 15:14:01 -06:00
Christopher Haster 788e98a989 Made delete remove the path from the tree completely, fixed failing tests 2023-02-12 15:13:52 -06:00
Christopher Haster 045a1098fd Added similar delete permutation tests, note that some are failing 2023-02-12 15:02:45 -06:00
Christopher Haster b14851b657 Added more tests, mostly around remove permutations 2023-02-12 15:02:27 -06:00
Christopher Haster dc5e74ac41 Added range deletion testing 2023-02-12 15:02:11 -06:00
Christopher Haster b621774759 Adopted upper/lower bounds in lfs_rbyd_append
There are two ways to represent the bounds in the search down the rbyd
tree:

1. Using lower/upper bounds and the id we are searching for:

     lower bound    id    upper bound
     |              |     |
     v              v     v
   <-a--b--c--d--e--f--g--h->

2. Using the lower/upper weights, which implicitly encodes the id,
   saving a word:

       lower weight     upper weight
       |                |
     .-'-----------. .--'-.
   <-a--b--c--d--e--f--g--h->

Now that I am diving deep into the rbyd algorithm again, the lower/upper
weight based approach just isn't worth the extra mental steps required
to understand what the algorithm is doing. Besides, we likely pay for
the implicit id anyways since we need enough state to remove the
ambiguity of sparse tags.
2023-02-12 14:52:46 -06:00
Christopher Haster d78c4412b6 Half-adopted upper/lower bounds in lookup and append 2023-02-12 14:51:34 -06:00
Christopher Haster 55e9df571b Minor test_rbyd improvements
Attempted to implement TEST_PERMUTATIONS but that went nowhere useful.
Unfortunately test defines don't have a way to represent arrays, and
adding such a representation would be unreasonably complicated.

For practical purposes it's better to just do the permutation generation
inside the tests. Maybe at some point it would be nice to integrate
permutation testing so the state gets hashed somehow as a part of the
test failure, but now is not the time.
2023-02-12 14:49:04 -06:00
Christopher Haster ef7ee6eb7d Added delete permutation testing (failing as expected) and some minor tweaks 2023-02-12 14:48:33 -06:00
Christopher Haster 22879c1cbb Avoided issue with tail deletions by tossing out-of-bound lookups early 2023-02-12 14:47:38 -06:00
Christopher Haster 1b6e1fdd33 Fixed delete issue with cut starting in a red edge
Though this is a bit of an ugly fix...

Red edges mess everything up during deletion since we always need to
go down red edges in case we need to do a red flip. This puts our
algorithm into an awkward state.

It would be easier to do away with colors completely when we start
deleting (after all this branch of the tree _will_ be shrinking), but
this conflicts with the ultimate goal of deduplicate rbyd append.
2023-02-12 14:41:47 -06:00
Christopher Haster ca710b5a29 Initial, very, very rough implementation of rbyd range deletion
Tree deletion is such a pain. It always seems like an easy addition to
the core algorithm but always comes with problems.

The initial plan for deletes was to iterate through all tags, tombstone,
and then adjust weights as needed. This accomplishes deletes with little
change to the rbyd algorithm, but adds a complex traversal inside the
commit logic. Doable in one commit, but complex. It also risks weird
unintuitive corner cases since the cost of deletion grows with the number
of tags being deleted (O(m log n)).

But this rbyd data structure is a tree, so in theory it's possible to
delete a whole range of tags in a single O(log n) operation.

---

This is a proof-of-concept range deletion algorithm for rbyd trees.

Note, this does not preserve rbyd's balancing properties! But it is no
worse than tombstoning. This is acceptable for littlefs as any
unbalanced trees will be rebalanced during compaction.

The idea is to follow the same underlying dhara algorithm, where we
follow a search path and save any alt pointers not taken, but we follow
both search paths that form the outside of the range, and only keep
outside edges.

For example, a tree:

        .-------o-------.
        |               |
    .---o---.       .---o---.
    |       |       |       |
  .-o-.   .-o-.   .-o-.   .-o-.
  |   |   |   |   |   |   |   |
  a   b   c   d   e   f   g   h

To delete the range d-e, we would search for d, and search for e:

        ********o********
        *               *
    .---*****       *****---.
    |       *       *       |
  .-o-.   .-***   ***-.   .-o-.
  |   |   |   *   *   |   |   |
  a   b   c   d   e   f   g   h

And keep the outside edges:

    .---                 ---.
    |                       |
  .-o-.   .-         -.   .-o-.
  |   |   |           |   |   |
  a   b   c           f   g   h

But how do we combine the outside edges? The simpler option is to do
both searches seperately, one after the other. This would end up with a
tree like this:

    .---------o
    |         |
  .-o-.   .---o
  |   |   |   |
  a   b   c   o---------.
              |         |
              o---.   .-o-.
              |   |   |   |
              _   f   g   h

But this horribly throws off the balance of our tree! It's worse than
tombstoning, and gets worse with more tags.

An alternative strategy, which is used here, is to alternate edges as we
descend down the tree. This unfortunately is more complex, and requires
~2x the RAM, but better preserves the balance of our tree. It isn't
perfect, because we lose color information, but we can leave that up to
compaction:

  .---------o
  |         |
.-o-.       o---------.
|   |       |         |
a   b   .---o       .-o-.
        |   |       |   |
        c   o---.   g   h
            |   |
            _   f

I also hope this can be merged into lfs_rbyd_append, deduplicating the
entire core rbyd append algorithm.
2023-02-12 13:29:06 -06:00
Christopher Haster 2979e6273a Added weight-adjusting insert operations, though it is currently very hacky 2023-02-12 13:22:45 -06:00
Christopher Haster 6c66e20349 Added lfs_rbyd_get, bumped hint back up to full during lookup
The idea behind the full hint is that we may stay on the current search
path, benefiting from a loaded cache. Though this should probably be
benchmarked eventually.
2023-02-12 13:21:41 -06:00
Christopher Haster 3c17c94b94 Added test_rbyd_large with better boundary conditions near end-of-block
test_rbyd_large also doubles as a decent fuzz test, since it involves
many more tags than the permutation testing can ever hit.
2023-02-12 13:20:12 -06:00
Christopher Haster 5cdda57373 Added the ability to remove rbyd tags via tombstoning
It's quite lucky a spare bit is free in the tag encoding, this means we
don't need a reserved length value as originally planned. We end up using
all of the bits that overlap the alt pointer encoding, which is nice and
unexpected.
2023-02-12 13:16:55 -06:00
Christopher Haster 2b475662e9 Rbyd test tweak: limited 0-tag lookups to traversal testing 2023-02-12 13:16:13 -06:00
Christopher Haster 1108191184 Added permutation testing of rbyd lookups over multiple commits
In theory spreading rbyd trees across multiple commits shouldn't change
anything, but the whole point of testing is to catch things that are not
in the theory.
2023-02-12 13:15:15 -06:00
Christopher Haster 084094116f Got all <=7 lookup permutations working, required a recoloring fix
The alt pointers change position, but the colors are supposed to stay
the same. If you randomly mix up the colors it sure does make things
confusing.
2023-02-12 13:11:18 -06:00
Christopher Haster e73f1c0e8d A number of small rbyd tweaks and cleanup 2023-02-12 13:11:06 -06:00
Christopher Haster d9b419d36a Rbyd prune now working 2023-02-12 13:09:25 -06:00
Christopher Haster 471f12c79a Fixed issue with rcache going out-of-date when overlapping pcache
This was a inter-abstraction-layer assumption that won't hold true with
the rbyd blocks.

The whole caching layer probably deserves a rewrite at this point.
2023-02-12 13:07:29 -06:00
Christopher Haster b03420ccfc Got rbyd yellow splitting working 2023-02-12 13:06:58 -06:00
Christopher Haster ac021f623c Added rbyd traverse testing 2023-02-12 13:02:31 -06:00
Christopher Haster e95ca03ff1 Minor rbyd tweaks and cleanup 2023-02-12 13:01:52 -06:00
Christopher Haster 53efaac243 Ok, now fixed rflip, branch/jump needed tweaking 2023-02-12 13:01:29 -06:00
Christopher Haster fe28837861 Rbyd trees with 4-leaves now working, fixed lfs_rtag_flip bug
- This is when flips starts happening during lfs_rbyd_append
- lfs_rtag_flip had an off-by-one math mistake
2023-02-12 12:58:55 -06:00
Christopher Haster c5fec90465 Rbyd rflips are now working, quite nicely actually
It turns out statefulness works quite well with this algorithm (The
prototype was in Haskell, which created some artificial problems. I
think it may have just been too high-level a language for this
near-instruction-level algorithm).
2023-02-12 12:58:29 -06:00
Christopher Haster 05276cef9a Added a bias to alt weights so in-between tags prefer larger tags
This bias makes it so that tag lookups always find a tag strictly >= the
requested tag, unless we are at the end of the tree.

This makes tree traversal trivial, which is quite nice.

Need to remove ntag now, it's no longer needed.
2023-02-12 12:49:19 -06:00
Christopher Haster 168977ffad Implemented lfs_rbyd_lookup
Though maybe this should not require exact matches?
2023-02-12 12:48:06 -06:00
Christopher Haster 024aaeba56 Some small tweaks
- Moved alt encoding 0x1 => 0x4, which can lead to slightly better
  lookup tables, the perturb bit takes the same place as the color bit,
  which means both can be ignored in readonly operations.

- Dropped lfs_rbyd_fetchmatch, asking each lfs_rbyd_fetch to include NULL
  isn't that bad.

New encoding:

  tags:
  iiii iiiiiii iiiiiTT TTTTTTt ttt0tpv
                   ^--------^------^^^- 16-bit id
                            '------|||- 8-bit type2
                                   '||- 5-bit type1
                                    '|- perturb bit
                                     '- valid bit
  llll lllllll lllllll lllllll lllllll
                                     ^- n-bit length

  alts:
  wwww wwwwwww wwwwwww wwwwwww www1dcv
                                 ^^^-^- 28-bit weight
                                  '|-|- color bit
                                   '-|- direction bit
                                     '- valid bit
  jjjj jjjjjjj jjjjjjj jjjjjjj jjjjjjj
                                     ^- n-bit jump
2023-02-12 12:40:19 -06:00
Christopher Haster 9a0e3fc749 More rbyd tests, multi-commit now working 2023-02-12 12:39:17 -06:00
Christopher Haster 2802880eaa Initial groundwork for rbyd trees
- primitive lfs_rbyd_fetch
- primitive lfs_rbyd_commit
- tag reading/progging and encoding machinery

The tag encoding scheme here uses pairs of leb128s, encoding either
a normal tag:

  iiii iiiiiii iiiiiTT TTTTTTt ttttt0v
                   ^--------^------^-^- 16-bit id
                            '------|-|- 8-bit type2
                                   '-|- 6-bit type1
                                     '- valid bit
  llll lllllll lllllll lllllll lllllll
                                     ^- n-bit length

Or an alt pointer:

  wwww wwwwwww wwwwwww wwwwwww wwwcd1v
                                 ^^^-^- 28-bit weight
                                  '|-|- color bit
                                   '-|- direction bit
                                     '- valid bit
  jjjj jjjjjjj jjjjjjj jjjjjjj jjjjjjj
                                     ^- n-bit jump

Note that two bits overlap the alt pointer dir/color encoding, this
is actually not a problem at all since some tags (crcs/fcrcs) don't
participate in the rbyd tree and can use these bits.

There's a number of benefits to using leb128s, which should probably
be written about, most notably is the abstraction of the device's
word-size. The "n-bits" above can be whatever word size works on the
device, trading off code-size for storage capabilities without breaking
compatibility with other devices. This will eventually be negotiated via
the superblock.
2023-02-12 12:37:59 -06:00
Christopher Haster 91ad673c45 Cleaned up a few additional commit corner cases
- General cleanup from integration, including cleaning up some older
  commit code
- Partial-prog tests do not make sense when prog_size == block_size
  (there can't be partial-progs!)
- Fixed signed-comparison issue in modified filebd
2022-12-17 12:42:05 -06:00
Christopher Haster 52dd83096b Initial implementation of forward-looking erase-state CRCs
This change is necessary to handle out-of-order writes found by pjsg's
fuzzing work.

The problem is that it is possible for (non-NOR) block devices to write
pages in any order, or to even write random data in the case of a
power-loss. This breaks littlefs's use of the first bit in a page to
indicate the erase-state.

pjsg notes this behavior is documented in the W25Q here:
https://community.cypress.com/docs/DOC-10507

---

The basic idea here is to CRC the next page, and use this "erase-state CRC" to
check if the next page is erased and ready to accept programs.

.------------------. \   commit
|     metadata     | |
|                  | +---.
|                  | |   |
|------------------| |   |
| erase-state CRC -----. |
|------------------| | | |
|   commit CRC    ---|-|-'
|------------------| / |
|     padding      |   | padding (doesn't need CRC)
|                  |   |
|------------------| \ | next prog
|     erased?      | +-'
|        |         | |
|        v         | /
|                  |
|                  |
'------------------'

This is made a bit annoying since littlefs doesn't actually store the
page (prog_size) in the superblock, since it doesn't need to know the
size for any other operation. We can work around this by storing both
the CRC and size of the next page when necessary.

Another interesting note is that we don't need to any bit tweaking
information, since we read the next page every time we would need to
know how to clobber the erase-state CRC. And since we only read
prog_size, this works really well with our caching, since the caches
must be a multiple of prog_size.

This also brings back the internal lfs_bd_crc function, in which we can
use some optimizations added to lfs_bd_cmp.

Needs some cleanup but the idea is passing most relevant tests.
2022-12-17 12:42:05 -06:00
Christopher Haster b0382fa891 Added BENCH/TEST_PRNG, replacing other ad-hoc sources of randomness
When you add a function to every benchmark suite, you know if should
probably be provided by the benchmark runner itself. That being said,
randomness in tests/benchmarks is a bit tricky because it needs to be
strictly controlled and reproducible.

No global state is used, allowing tests/benches to maintain multiple
randomness stream which can be useful for checking results during a run.

There's an argument for having global prng state in that the prng could
be preserved across power-loss, but I have yet to see a use for this,
and it would add a significant requirement to any future test/bench runner.
2022-12-06 23:09:07 -06:00
Christopher Haster d8e7ffb7fd Changed lfs_emubd_get* -> lfs_emubd_*
lfs_emubd_getreaded      -> lfs_emubd_readed
lfs_emubd_getproged      -> lfs_emubd_proged
lfs_emubd_geterased      -> lfs_emubd_erased
lfs_emubd_getwear        -> lfs_emubd_wear
lfs_emubd_getpowercycles -> lfs_emubd_powercycles
2022-12-06 23:09:07 -06:00
Christopher Haster f89d758444 Fixed test out-of-space issues with powerloss testing
These are just incorrect limits in the tests that can be triggered by
powerloss testing, which can end up with more metadata-pairs than
without powerloss testing due to orphans.
2022-11-28 12:51:18 -06:00
Christopher Haster 1a07c2ce0d A number of small script fixes/tweaks from usage
- Fixed prettyasserts.py parsing when '->' is in expr

- Made prettyasserts.py failures not crash (yay dynamic typing)

- Fixed the initial state of the emubd disk file to match the internal
  state in RAM

- Fixed true/false getting changed to True/False in test.py/bench.py
  defines

- Fixed accidental substring matching in plot.py's --by comparison

- Fixed a missed LFS_BLOCk_CYCLES in test_superblocks.toml that was
  missed

- Changed test.py/bench.py -v to only show commands being run

  Including the test output is still possible with test.py -v -O-, making
  the implicit inclusion redundant and noisy.

- Added license comments to bench_runner/test_runner
2022-11-15 13:42:07 -06:00
Christopher Haster 4fe0738ff4 Added bench.py and bench_runner.c for benchmarking
These are really just different flavors of test.py and test_runner.c
without support for power-loss testing, but with support for measuring
the cumulative number of bytes read, programmed, and erased.

Note that the existing define parameterization should work perfectly
fine for running benchmarks across various dimensions:

./scripts/bench.py \
    runners/bench_runner \
    bench_file_read \
    -gnor \
    -DSIZE='range(0,131072,1024)'

Also added a couple basic benchmarks as a starting point.
2022-11-15 13:33:34 -06:00