Commit Graph

144 Commits

Author SHA1 Message Date
Christopher Haster 321e33d5d5 data: Adopted more object-like lfs3_data_t operations
As much as I don't want to admit it, our 3-word lfs3_data_t struct is
just too large to be treated as pass-by-value with today's compilers.

It's a real shame, because I don't think there's a great technical
reason, just that compiler's pass-by-value optimizations generally stop
after 2 words.

If we could expect 16-bit block sizes (off and size), we could fit in
2 words, but this is already challenged by today's NAND chips
(bs>=128KiB).

---

So, as a compromise, this stops treating lfs3_data_t as pass-by-value,
with the exception of the lfs3_data_from* functions that still return
lfs3_data_t directly.

So instead of:

  lfs3_data_t data = lfs3_data_fromecksum(&ecksum, buffer);
  data = lfs3_data_slice(data, 8, -1);
  return lfs3_data_size(data);

Most operations take lfs3_data_t by pointer:

  lfs3_data_t data = lfs3_data_fromecksum(&ecksum, buffer);
  lfs3_data_slice(&data, 8, -1);
  return lfs3_data_size(&data);

One of the main consequences is there are now several ways to slice data
(internally these all redirect to lfs3_data_slice), and LFS3_DATA_SLICE
will likely see more use since we need temporary allocations to pass the
data slice by address:

- lfs3_data_slice(data, a, b) - Slices the data in place
- lfs3_data_fromslice(data, a, b) - Returns a new data slice
- LFS3_DATA_SLICE(data, a, b) - Creates a new compound-literal slice

---

As a pragmatic compromise, this saves a nice chunk of both code and
stack:

                 code          stack          ctx
  before:       35316           2176          660
  after:        35188 (-0.4%)   2136 (-1.8%)  660 (+0.0%)

                 code          stack          ctx
  gbmap before: 38172           2192          772
  gbmap after:  38048 (-0.3%)   2152 (-1.8%)  772 (+0.0%)
2025-12-02 01:14:44 -06:00
Christopher Haster dca915dd95 rattrs: Converted rattrs to full variable-length isa
It's funny to see what originally started as a simple list of rbyd attrs
slowly morph into a full isa. But it makes sense. What we really want is
an abstract description of operations that can be played and replayed as
necessary to atomically update the mtree.

Using a fixed lfs3_rattr_t struct to represent this in C is easy, and
avoids strict-aliasing issues, but ultimately limited when it comes to
the wide-range of data we want to attach to attributes.

Unlike a computer's isa, we want to be able to include full 12-24 byte
branch pointers directly in the instruction!

---

So here's a full variable-length isa organized by words (max(uintptr_t,
uint32_t)).

The first 32-bit word extends the 16-bit tag with an extra 16-bits of
control information:

  wwll llff ffcc cccc tttt tttt tttt tttt
   ^'-.-''-.-''--.--' :                 :
   '--|----|-----|----:-----------------:-- compressed weight
  ::  '----|-----|----:-----------------:-- total len
  ::       '-----|----:-----------------:-- from encoder
  ::             '----:-----------------:-- optional count
  ::                  rgmm kkkk -kkk kkkk
  11 => w=-1          ^^ ^ '-.' '---.---'
  00 => w=0           '|-|---|------|------ rm bit
  01 => w=+1           '-|---|------|------ grow bit
  10 => w=attached       '---|------|------ mask bits
                             '------|------ tag suptype
                                    '------ tag subtype

The 4-bit length field always encodes the full length of the
instruction, including the instruction itself and optional weight. The
4-bit from + 6-bit count fields operate independently and tell
lfs3_rbyd_appendrattr_ how to actually encode the data related to the
instruction.

To work around strict-aliasing issues, complex structs are expected to
be broken down into words and reconstructed in lfs3_rbyd_appendrattr_.
Most of our structs are organized into words anyways. For example:

  // new child
  *r++ = LFS3_RATTR(5, LFS3_TAG_BRANCH, -2, LFS3_FROM_BRANCH);
  *r++ = LFS3_RATTR_WEIGHT(+child_->weight);
  *r++ = LFS3_RATTR_ARG(child_->blocks[0]);
  *r++ = LFS3_RATTR_ARG(child_->trunk);
  *r++ = LFS3_RATTR_ARG(child_->cksum);

This also changes rattr-lists to be null-terminated, which makes a bit
more sense in a variable-length isa:

  *r++ = LFS3_RATTR_NULL; // all zeros, including length

One concern with null-terminated rattr-lists is how easy it is to
forget the null-terminator, but an assert that all non-null rattrs have
non-zero length seemed to catch the many many mistakes during adoption.

Alternatively, separate LFS3_FROM_NULL/LFS3_FROM_NIL from fields could
be used if encoding space gets tight.

I'm also quite happy with the 2-bit weight feild, which allows omitting
the optional weight word for -1,0,+1 weights. These should cover at
least all mdir operations.

Note the exact encoding of the rattr fields is less of a concern than
the tag fields, as it doesn't reside on-disk can be changed on whim.

---

Saves a nice chunk of code and stack:

                 code          stack          ctx
  before:       35920           2280          660
  after:        35324 (-1.7%)   2176 (-4.6%)  660 (+0.0%)

                 code          stack          ctx
  gbmap before: 38812           2296          772
  gbmap after:  38156 (-1.7%)   2192 (-4.5%)  772 (+0.0%)

The stack savings are obvious, but the code savings a bit less so. A
variable length isa _is_ more complicated, but by limiting most encoding
decisions to compile-time (2-bit weights vs 32-bit weights for example),
the savings from fewer word manipulations on the stack wins.
2025-12-02 01:14:31 -06:00
Christopher Haster ca678538d4 Adopted lowercase => internal pattern for LFS3_tag_* tags
This includes the mask/rm/grow bits:

- LFS3_tag_RM
- LFS3_tag_GROW
- LFS3_tag_MASK0/2/8/12

Our in-device only handle types:

- LFS3_tag_ORPHAN
- LFS3_tag_TRV
- LFS3_tag_UNKNOWN

And in-device only tags with special behavior:

- LFS3_tag_INTERNAL
- LFS3_tag_RATTRS
- LFS3_tag_SHRUBCOMMIT
- LFS3_tag_GRMPUSH
- LFS3_tag_MOVE
- LFS3_tag_ATTRS

Usually I'm not a big fan of case-sensitive naming patterns, but this
has been useful for self-documenting what compat flags are in-device
only. Might as well extend the idea to our tag definitions.
2025-11-18 00:56:13 -06:00
Christopher Haster 1cae72f419 tag-returning: Adopted tag-returns in rbyd lookupnext/lookup
This is the start of a big refactor to try to move tag out-pointers into
the return position of functions, muxing with error codes via the
sign-bit when necessary.

So instead of:

  lfs3_tag_t tag_;
  lfs3_data_t data_;
  int err = lfs3_rbyd_lookup(&lfs3, &rbyd, rid, tag,
          &tag_, &data_);
  if (err) {
      return err;
  }

We now do:

  lfs3_data_t data_;
  lfs3_stag_t tag_ = lfs3_rbyd_lookup(&lfs3, &rbyd, rid, tag,
          &data_);
  if (tag_ < 0) {
      return tag_;
  }

In theory, removing an out-pointer saves both code and stack, though it
will be interesting to actually see how much of an affect this has after
the dust has settled.

littlefs v2 used this technique heavily for its 32-bit tags, but we
never did a comparison with/without tags in the return position.

This is a big rewrite in the test code, so hopefully this ends up worth
it :)

Lots of regex.

Note this implicitly limits error codes to 16-bits, but supported error
codes are already a bit limited because we're using int everywhere
(instead of int32_t). If we need 32-bit error codes we can always add
another type to represent the mux in the future (lfs3_etag_t?).

---

So far the code savings look promising:

           code          stack          ctx
  before: 36828           2368          656
  after:  36576 (-0.7%)   2376 (+0.3%)  656 (+0.0%)

Stack usage is a big disappointing, but hopefully that is just a
temporary cost due to the internal scaffolding between different API
types while the refactor is ongoing.
2025-07-18 16:41:28 -05:00
Christopher Haster 6a2ecbac87 Replaced bool with lfs3->pcksum for prog-aligned cksums
This replaces the `bool align` parameter that goes through all the prog
layers with an optional prog-aligned cksum stored in the lfs3_t struct.
Normally ignored, this prog-aligned cksum can be requested by setting
cksum=&lfs3->pcksum in any prog call.

Does this work? Yes. Is it a great solution? Ehhhh...

I've been tinkering with other solutions that avoid the `bool align`
parameter, but with no luck.

- `bool align`, or previously two cksum arguments, work, but create a
  bit of a messy API. I'd like to find an alternative solution.

- Changing the cksum pointer to a richer lfs3_cksum_t struct with flags
  also works, but would be an even messier API.

- Adding an lfs3_t side-channel, lfs3->pcache could include a pointer to
  an optional prog-aligned cksum. But this would be the same/more cost
  as just storing the pcksum in lfs3_t. And then we'd need to worry
  about disentangling the cksum pointer on errors, etc.

- We could set a flag in lfs3->flags for alignment. This avoids the
  extra 4 bytes of ctx, but still suffers from the risk of entangled
  state on errors, etc.

- We could unconditionally calculate lfs3->pcksum. But then we'd be
  calculating a lot of cksums we don't use (every metadata commit), and
  still using the extra 4 bytes of ctx.

Lacking a good solution, using cksum=&lfs3->pcksum to indicate a
prog-aligned cksum is at least an ok solution.

I will happily change this if an alternative comes up in the future.

Another way of viewing this is that `&lfs3->pcksum` acts as a special
magic pointer value to tell the prog layers to calculate lfs3->pcksum.
A different non-NULL constant value could have worked just as well, but
those are a bit trickier to create in C.

---

Actually, there is a "better" cursed solution:

- Rely on pointer alignment to sneak a flag into the cksum pointer's
  lower bits.

But, while clever, this is is outside of C's machine model and would
limit portability.

---

This trades 4 bytes of ctx for 58 bytes of code and simpler (debatable)
internal prog APIs:

           code          stack          ctx
  before: 36860           2384          652
  after:  36832 (-0.1%)   2384 (+0.0%)  656 (+0.6%)

In theory this also saves stack in all the prog APIs, but none of prog
APIs end up on the stack hot-path. In our codebase the read APIs
dominate the stack thanks to block allocator traversals.
2025-07-16 17:50:06 -05:00
Christopher Haster 6eba1180c8 Big rename! Renamed lfs -> lfs3 and lfsr -> lfs3 2025-05-28 15:00:04 -05:00
Christopher Haster 7d45ca0892 tests: Big test cleanup!
Removing the vestiges of v2 tests.
2025-05-27 21:05:53 -05:00
Christopher Haster 677c078b50 Added LFSR_TAG_BNAME/MNAME, stop btree lookups at first tag
Now that we don't have to worry about name tag conflicts as much, we
can add name tags for things that aren't files.

This adds LFSR_TAG_BNAME for branch names, and LFSR_TAG_MNAME for mtree
names. Note that the upper 4 bits of the subtype match LFSR_TAG_BRANCH
and LFSR_TAG_MDIR respectively:

  LFSR_TAG_BNAME        0x0200  v--- --1- ---- ----
  LFSR_TAG_MNAME        0x0220  v--- --1- --1- ----

  LFSR_TAG_BRANCH       0x030r  v--- --11 ---- --rr
  LFSR_TAG_MDIR         0x0324  v--- --11 --1- -1rr

The encoding is somewhat arbitrary, but I figured reserving ~31 types
for files is probably going to be plenty for littlefs. POSIX seems to
do just fine with only ~7 all these years, and I think custom attributes
will be more enticing for "niche" file types (symlinks, compressed
files, etc), given the easy backwards compatibility.

---

In addition to the debugging benefits, the new name tags let us stop
btree lookups on the first non-bname/branch tag. Previously we always
had to fetch the first struct tag as well to check if it was a branch.

In theory this saves one rbyd lookup, but in practice it's a bit muddy.

The problem is that there's two ways to use named btrees:

1. As buckets: mtree -> mdir -> mid
2. As a table: ddtree -> ddid

The only named btree we _currently_ have is the mtree. And the mtree
operates in bucket mode, with each mdir acting more-or-less as an
extension to the btree. So we end up needing to do the second tag lookup
anyways, and all we've done is complicated up the code.

But we will _eventually_ need the table mode for the ddtree, where we
care if the ddname is an exact match.

And returning the first tag is arguably the more "correct" internal API,
vs arbitrarily the first struct tag.

But then again this change is pretty pricey...

           code          stack          ctx
  before: 35732           2440          640
  after:  35888 (+0.4%)   2480 (+1.6%)  640 (+0.0%)

---

It's worth noting the new BNAME/MNAME tags don't _require_ the btree
lookup changes (which is why we can get away with not touching the dbg
scripts). The previous algorithm of always checking for branch tags
still works.

Maybe there's an argument for conditionally using the previous API when
compiling without the ddtree, but that sounds horrendously messy...
2025-04-30 00:25:30 -05:00
Christopher Haster 200830aafe Adopted mask bits for tag lookup/append
This lets us cram in one more mask for potential redund bits:

  name                 tag    mask
  LFSR_TAG_MASK0    0x0000  0x0fff  ---- 1111 1111 1111
  LFSR_TAG_MASK2    0x1000  0x0ffc  ---- 1111 1111 11--
  LFSR_TAG_MASK8    0x2000  0x0f00  ---- 1111 ---- ----
  LFSR_TAG_MASK12   0x3000  0x0000  ---- ---- ---- ----
                                    '.-' '.-' '---.---'
                          mode bits -'    |       |   ^
                            suptype ------'       |   |
                            subtype --------------'   |
                        redund bits ------------------'

I toyed around with a bitwise alternative to the lookup table, but
couldn't come up with anything simpler than these:

- 0xfff & ~((((1<<((i>>1)*8))-1) << ((i&1)*4)) | ((1<<(i*2))-1))
- 0xfff & ~((1 << (((i>>1)*8)+((i&1)<<(1+(i>>1)))))-1)
- 0xfff & ~((1<<(2*i*i))-1) (requires multiply and 32-bit shift)

---

This also replaces the mdir/rbyd/btree/mtree lookup/sublookup/suplookup
functions with a single flexible lookup function that accepts tag masks.

This ended up adding a bit of code/stack (the extra NULL args are
surprisingly pricey), but will hopefully make the redund bits
easier/cheaper to use:

           code          stack          ctx
  before: 35548           2472          636
  after:  35584 (+0.1%)   2480 (+0.3%)  636 (+0.0%)
2025-04-20 15:53:18 -05:00
Christopher Haster 91341a4c48 Replaced rattr.u.etc with rattr relevant types
This does a couple things:

- Makes attr-lists a bit more self-documenting.

- Adds a bit more type-safety. The LFSR_RATTR_* macros should be able to
  reject types that don't match the expected encoding.

- Makes it easier to adjust dsize estimates at one location.

  Specifically, this makes it harder to forget bptr's LFSR_BPTR_DSIZE.

---

Surprisingly this did have a small impact on code size. I'm not entirely
sure why, but considering how much of the codebase this touches I'm just
going to chalk this up to compiler noise:

           code          stack          ctx
  before: 35488           2440          636
  after:  35536 (+0.1%)   2440 (+0.0%)  636 (+0.0%)

lfsr_file_carve seems the hardest hit:

  function (0 added, 0 removed)      osize    nsize    dsize
  lfsr_file_open                        16       20       +4 (+25.0%)
  lfsr_file_carve                     1316     1356      +40 (+3.0%)
  lfsr_remove                          408      412       +4 (+1.0%)
  TOTAL                              35488    35536      +48 (+0.1%)
2025-02-12 15:04:22 -06:00
Christopher Haster a017c230dc Reintroduced LFSR_RATTR_BUF
Mainly just for self-documentation reasons.

This may also make it easier to add LFSR_RATTR_BUF-specific asserts/
tweaks/etc, and helps future refactoring.

But functionally LFSR_RATTR_BUF is equivalent to LFSR_RATTR for now.

No code changes.
2025-02-12 02:07:27 -06:00
Christopher Haster 9a32379b8e Cleaned up LFSR_RATTR*__ -> LFSR_RATTR*
This finishes the eager -> lazy attr encoding rework.

Which makes it a good time to look at the total savings from adopting
lazy attr encoding, though there's still a bit of tinkering to do (eager
branches, cksum tags, etc):

                      code          stack          ctx
  before lazy-attrs: 36280           2576          636
  after lazy-attrs:  35592 (-1.9%)   2472 (-4.0%)  636 (+0.0%)

A ~free 688 byte savings in code and 104 bytes in stack is not bad.
2025-02-11 02:51:42 -06:00
Christopher Haster 919113f6c4 Fully adopted lazy attr encoding
This fully adopts LFSR_RATTR__ and friends:

- LFSR_RATTR      -> LFSR_RATTR__ or LFSR_RATTR_DATA__
- LFSR_RATTR_BUF  -> LFSR_RATTR__
- LFSR_RATTR_CAT  -> LFSR_RATTR_CAT__
- LFSR_RATTR_NOOP -> LFSR_RATTR_NOOP__
- LFSR_RATTR_NAME -> LFSR_RATTR_NAME__

Note the new LFSR_RATTR__ macro also lets us a drop the special rattr
macros, at the cost of a bit less type safety:

- LFSR_RATTR_RATTRS      -> LFSR_RATTR__
- LFSR_RATTR_MOVE        -> LFSR_RATTR__
- LFSR_RATTR_GRM         -> LFSR_RATTR__ (we weren't using this?)
- LFSR_RATTR_SHRUBCOMMIT -> LFSR_RATTR__

Curiously, this ended up adding ~88 bytes to lfsr_file_carve:

  function (0 added, 0 removed)      osize    nsize    dsize
  lfsr_file_carve                     1228     1316      +88 (+7.2%)
  lfsr_mdir_commit                    2144     2152       +8 (+0.4%)
  lfsr_mdir_commit__                  1192     1188       -4 (-0.3%)
  lfsr_file_truncate                   184      182       -2 (-1.1%)
  lfsr_mount                            98       96       -2 (-2.0%)
  TOTAL                              35508    35596      +88 (+0.2%)

I'm really not sure why, all I can think of is maybe the change from a
forced-inline function to a macro added a bunch of compiler noise?

Still, 80 bytes is not worth two competing LFSR_RATTR APIs. Though
it may be worth looking into this in the future.

Total code changes:

           code          stack          ctx
  before: 35508           2472          636
  after:  35596 (+0.2%)   2472 (+0.0%)  636 (+0.0%)
2025-02-11 02:51:42 -06:00
Christopher Haster 4303028c83 Adopted lazy encoding for name attrs
This one is interesting in that we don't just encode to a buffer, but
need to express the concatenation of did + name somehow. Fortunately we
can still leverage the cat circuitry by setting data_count=-2:

- LFSR_TAG_NAME       -+-> cat(fromleb128(did), name)
- LFSR_TAG_REG        -+
- LFSR_TAG_DIR        -+
- LFSR_TAG_STICKYNOTE -'

This does break our shrub estimate for name attrs (currently names have
no technical limit), which would be an issue, but we just happen to never
commit names to shrubs.

In theory you _could_ accurately estimate name attrs if you limited
names to <=(2^15)-5, but I figured this wouldn't be worth the extra code
cost in LFSR_RATTR_NAME__... Especially since it would just go unused...

Saves a nice bit of code, though no stack since we currently don't
allocate any names on the hot-path (lfsr_file_truncate):

           code          stack          ctx
  before: 35580           2440          636
  after:  35472 (-0.3%)   2440 (+0.0%)  636 (+0.0%)

Note this does _not_ include LFSR_TAG_BOOKMARK, which only contains the
did and can avoid a stack allocation if encoded as a single leb128 attr.

Though breaking up the LFSR_TAG_NAME types does risk a more complicated
switch-case-table...
2025-02-11 02:51:42 -06:00
Christopher Haster 76e0f8f73c Reverted lfsr_rat_t -> lfsr_rattr_t
This is the correct name for our rbyd attr type, even if it requires a
bit more typing.

lfsr_attr_t would be a better name, but that conflicts with our
user-facing attrs.
2025-02-11 02:50:38 -06:00
Christopher Haster 11115dbe81 Renamed lfsr_rattr_t -> lfsr_rat_t
We already have lfsr_cat_t so...

lfsr_rattr_t is a pretty fundamental type for littlefs, unfortunately
the name "rattr" is a mouthful. Shortening this to just "rat" hopefully
makes things easier to read at the cost of it being a bit less clear
what lfsr_rat_t actually is.

Though it's possible I've been staring at the dwarf spec (DW_AT_*) for
too long...
2025-01-28 14:41:45 -06:00
Christopher Haster bc587e7166 Renamed lfsr_attr_t -> lfsr_rattr_t
To avoid the obvious conflict with lfs_attr. Unlike lfsr_rattr_t,
lfs_attr is user facing, so it gets priority.

This name may change in the future if something better comes up, but in
the meantime we need to change the name to _something_.

Is this the reason Linux/BSD/etc call these xattrs?

(Note littlefs's attrs are much more limited than xattrs. We should
_not_ call these xattrs in case we want to add true xattrs in the
future.)
2024-08-23 12:54:27 -05:00
Christopher Haster 4d8bfeae71 attrs: Reduced UATTR/SATTR range down to 7-bits
It would be nice to have a full 8-bit range for both user attrs and
system attrs, for both backwards compatibility and maximizing the
available attr space, but I think it just doesn't make sense from an API
perspective.

Sure we could finagle the user/sys bit into a flags argument, or provide
separate lfsr_getuattr/getsattr functions, but asking users to use a
9-bit int for higher-level operations (dynamic attrs, iteration, etc) is
a bit much...

So this reduces the two attr ranges down to 7-bits, requiring 8-bits
total to store all possible attr types in the current system:

  TAG_ATTR      0x0400  v--- -1-a -aaa aaaa
  TAG_UATTR     0x04aa  v--- -1-- -aaa aaaa
  TAG_SATTR     0x05aa  v--- -1-1 -aaa aaaa

This really just affects scripts, since we haven't actually implemented
attributes yet.

Worst case we still have the 9-bit encoding space carved out, so we can
always add an additional set of attrs in the future if we start running
into attr pressure.

Or, you know, just turn on the subtype leb128 encoding the 8th subtype
bit is reserved for. Then you'd only be limited by internal driver
details, probably 24-bits per attr range if we make tags 32-bits
internally. Though this would probably come with quite a code cost...
2024-08-22 00:59:09 -05:00
Christopher Haster ccc073faed Rough implementation of ckreads
With the adoption of the odd-parity-zero rbyd perturb scheme, it's now
possible to validate individual tag's parity with neighboring valid
bits. This sparked an idea that I previously thought was intractable.

If we:

1. Validate all metadata reads by checking their on-disk parity bits.

2. Validate all data reads by checking their in-metadata checksums.

We end up with a closed system where all reads are checked by at least
a parity bit.

Being able to check all reads is a very valuable filesystem feature, but
difficult for littlefs:

- We need to keep relevant data in RAM while validating checksums.

  We can't just validate checksums and then perform a second read as
  that creates a hole where new bit-errors may be introduced.

- This is solved in other filesystems by loading and checking whole
  blocks in RAM. We just can't do that here.

- Without parity, we would need to check the rbyd's checksum on every
  tag read. This would lead to a crazy O(n^2 log n) rbyd compaction
  runtime.

  Which is why I original thought ckreads was just intractable.

Now, this isn't all sunshine and rainbows. ckreads, as implemented here,
has some deeply concerning flaws:

- A parity bit is, mathematically, the minimum possible error-detection
  possible. Is validating reads with only a parity bit sufficient for
  real world applications?

- Validating data checksums on every read may have severe performance
  implications. We need to read up to the entire block, which can lead
  to O(n^2) behavior when performing a lot of small reads in a file.

- In order to validate checksums/parity-bits, we need to know where the
  checksums/parity-bits actually are for each piece of data.

  Our lfsr_data_t struct provides a surprisingly nice abstraction for
  this, but oof is it expensive.

For the added code/stack cost alone, we probably want to eventually make
this an opt-in compile-time feature.

---

Implementation notes:

- This found an actual compiler bug! Turns out increasing lfsr_data_t
  from 3-words to 5-words confuses GCC:

  https://gcc.gnu.org/bugzilla/show_bug.cgi?id=101854

- Mid-commit, we may have not actually written the last tag's parity
  yet, which is a bit of a problem because we may read the last tag when
  building the next trunk!

  Fixing this required a whole separate tailck mechanism, which just
  tracks in-progress commit's parity bits.

  This doesn't help the code/stack cost situation...

- lfsr_bd_read/cmp/cpy all need to be extended to support calculating a
  checksum on the side, which is a bit of a mess.

- bptr's cksize/cksum is redundant now, which is going to make
  conditional compilation a mess.

- The extra parity byte we need to read makes hint calculation a pain.

Code cost wise... yeah, it's significant. Turns out almost doubling
lfsr_data_t has a significant impact on stack usage. Add in all the
extra code to track checksums/parity-bits and validate checksums/
parity-bits and you got yourself a pretty heavy feature:

           code          stack
  before: 36352           2672
  after:  38100 (+4.8%)   3032 (+13.5%)
2024-08-16 01:03:49 -05:00
Christopher Haster acfae9e072 Extended lfsr_mount to accept mount flags
This has been a long-time coming, mount flags are just too useful for
configuring a filesystem at runtime.

Currently this is limited to LFS_M_RDONLY and LFS_M_CKPROGS, but there
are a few more planned in the future:

  LFS_M_RDWR     = 0x0000, // Mount the filesystem as read and write
  LFS_M_RDONLY   = 0x0001, // Mount the filesystem as readonly
  LFS_M_STRICT*  = 0x0002, // Error if on-disk config does not match
  LFS_M_FORCE*   = 0x0004, // Ignore compat flags, mount readonly
  LFS_M_FORCEWITHRECKLESSABANDON*
                 = 0x0008, // Ignore compat flags, mount read write

  LFS_M_CKPROGS  = 0x0010, // Check progs by reading back progged data
  LFS_M_CKREADS* = 0x0020, // Check reads via checksums

  * Hypothetical

As a convenience, we also return mount flags in the struct lfs_fsinfo's
flags field as their relevant LFS_I_* variants. Though only to match
statvfs, and only because it's cheap, littlefs's API is low-level and we
should expect users to know what flags they passed to lfsr_mount.

As for the new mount flags:

- LFS_M_RDONLY - For consistency with existing APIs, this just asserts
  on write operations, which makes it a bit useless... But the info flag
  LFS_I_RDONLY may be useful for falling back to a readonly mode if
  we encounter on-disk compat issues.

  At least if implement the theoretical LFS_UNTRUSTED_USER mode
  LFS_M_RDONLY could become a runtime error.

- LFS_M_RDWR - This really just exists to compliment LFS_M_RDONLY and to
  match LFS_O_RDONLY/LFS_O_RDWR. It's just an alias for 0, and I don't
  think there will ever be a reason to make it non-0 (but I can always
  be wrong!).

- LFS_M_CKPROGS - This replaces the check_progs config option and avoids
  using a full byte to store a bool.

  We should probably also have a compile-time option to compile this out
  (LFS_NO_CKPROGS?), but that's a future thing to do.

This ended up adding a surprising bit of code, considering we're just
moving flags around, and noise in lfs_alloc added a bit of stack again:

           code          stack
  before: 35880           2672
  after:  35932 (+0.1%)   2680 (+0.3%)
2024-07-17 20:39:31 -05:00
Christopher Haster b4af52bc72 Implemented SOMEBITS/MOSTBITS emubd powerloss behavior
These emulate powerloss behavior where only some of the bits being
progged are actually progged if there is a powerloss. This behavior was
the original motivation for our ecksums/fcrcs, so it's good to have this
tested.

As a simplification, these only test the extremes:

- LFS_EMUBD_POWERLOSS_SOMEBITS => one bit progged
- LFS_EMUBD_POWERLOSS_MOSTBITS => all-but-one bit progged

Also they flips bits instead of preserving exact partial prog behavior,
but this is allowed (progs can have any intermediate value), has the
same effect as partial progs, and should encourage failed progs.

This required a number of tweaks in emubd: moved powerloss before prog,
moved mutate after powerloss, etc, but these shouldn't affect other
powerloss behaviors. Handling powerloss after prog was only to avoid
power_cycles=1 being useless, it's not strictly required.

Good news is testing so far suggests our ecksum design is sound.
2024-06-06 16:58:18 -05:00
Christopher Haster 50fc0ed680 Replaced lfsr_bd_unprog with an align flag in each prog function
This sort of reverts the addition of lfsr_bd_unprog, but with a slightly
better API. lfsr_bd_unprog was too much of a hack, and isn't really
generalizable. The align flag isn't necessarily any better, but at least
it's the simplest/least-confusing solution available.

And it's net savings, code-wise:

           code          stack          lfs_t
  before: 33690           2608            164
  after:  33678 (-0.0%)   2600 (-0.3%)    160 (-2.4%)
2024-05-30 01:46:58 -05:00
Christopher Haster 9c9a409524 Added fuzz test attribute
This acts as a marker to indicate a fuzz test. It should reference a
define, usually SEED, that can be randomized to get interesting test
permutations.

This is currently unused, but could lead to some interesting uses such
as time-based fuzz testing. It's also just useful for inspecting the
tests (make test-list).
2024-05-28 12:44:44 -05:00
Christopher Haster cd22c0d68b Aggressively cleaned up/reworked lfsr_attr_t, consumed lfsr_cat_t
This turned into a sort of system-wide refactor based on learned
knowledge of what we can do with lfsr_attr_t.

The big changes:

- Reverted LFSR_ATTR to mainly take lfsr_data_t again, keeping
  lfsr_data_t as the default data representation in the codebase.

  Now that we know

  LFSR_ATTR_CAT_ still provides concatenation mechanics, and LFSR_ATTR_
  provides a way to edit in-flight lfsr_attr_ts.

- Dropped lfsr_cat_t, replaced with explicit const void* + uint16_t,
  tried to limit to low-level operations and prefer passing aroud
  lfsr_attr_t and lfsr_data_t at a high-level.

  Note this cat + cat_count pair is quite similar to the common attrs +
  attr_count and buffer + size arguments.

- Adopted lfsr_attr_t more in mid-level functions, lfsr_rbyd_appendattr,
  lfsr_rbyd_appendcompactattr, lfsr_file_carve, etc. This is a bit more
  ergonomical, allows for use of LFSR_ATTR* macros, and in theory might
  even save a bit of stack.

Unfortunately this seems to have resulted in a net hit to code cost,
though I still think it's worth it for the internal ergonomics:

           code          stack
  before: 33652           2624
  after:  33780 (+0.4%)   2640 (+0.4%)

Investigating further suggests this may just be the result of compiler
noise and changes to argument placement. lfsr_attr_t does touch a lot of
code...

It's interesting to note the adoption of lfsr_attr_t in
lfsr_rbyd_appendattr* and friends prevents their transformation into
.isra functions, though this doesn't seem to impact code cost too much:

  function (5 added, 5 removed)          osize   nsize   dsize
  lfsr_cat_size                              -      48     +48 (+100.0%)
  lfsr_file_carve                            -    1600   +1600 (+100.0%)
  lfsr_rbyd_appendattr                       -    2120   +2120 (+100.0%)
  lfsr_rbyd_appendattr_                      -     244    +244 (+100.0%)
  lfsr_rbyd_appendcompactattr                -      68     +68 (+100.0%)
  lfsr_rbyd_appendcompactrbyd              144     152      +8 (+5.6%)
  lfsr_file_truncate                       298     314     +16 (+5.4%)
  lfsr_mdir_commit__                      1056    1112     +56 (+5.3%)
  lfsr_mdir_compact__                      502     526     +24 (+4.8%)
  lfsr_rbyd_appendattrs                    132     138      +6 (+4.5%)
  lfsr_file_fruncate                       386     402     +16 (+4.1%)
  lfsr_data_frombtree                       84      86      +2 (+2.4%)
  lfsr_rbyd_appendcksum                    512     520      +8 (+1.6%)
  lfsr_file_opencfg                        572     580      +8 (+1.4%)
  lfsr_rename                              608     616      +8 (+1.3%)
  lfsr_mkdir                               500     504      +4 (+0.8%)
  lfsr_bd_prog                             278     280      +2 (+0.7%)
  lfsr_mdir_commit                        2364    2360      -4 (-0.2%)
  lfsr_bshrub_commit                       716     712      -4 (-0.6%)
  lfsr_file_sync                           526     514     -12 (-2.3%)
  lfsr_file_flush_                        1868    1820     -48 (-2.6%)
  lfsr_remove                              456     436     -20 (-4.4%)
  lfsr_fs_fixgrm                           168     160      -8 (-4.8%)
  lfsr_cat_size.isra.0                      42       -     -42 (-100.0%)
  lfsr_file_carve.isra.0                  1596       -   -1596 (-100.0%)
  lfsr_rbyd_appendattr.isra.0             2088       -   -2088 (-100.0%)
  lfsr_rbyd_appendattr_.isra.0             232       -    -232 (-100.0%)
  lfsr_rbyd_appendcompactattr.isra.0        56       -     -56 (-100.0%)
  TOTAL                                  33652   33780    +128 (+0.4%)
2024-05-10 15:43:08 -05:00
Christopher Haster 88a098c616 Added lfsr_cat_t to represent concatenated data
So now, instead of one data type trying to do everything, we have two:

1. lfsr_data_t - Readable data, either in-RAM or on-disk

2. lfsr_cat_t - Concatenated data for progging, may be either a simple
   in-RAM buffer or an indirect list of lfsr_data_ts

This comes from an observation that most lfsr_attr_t datas were either
simple buffers, NULL, or required the indirect concatenated datas
anyways (concatendated file fragments). By separating lfsr_cat_t and
lfsr_data_t, maybe we can save RAM in lfsr_attr_t by not needing the
three words necessary for the less-common disk references.

Note the interesting tradeoff:

Simple in-RAM buffers/NULL decrease by 1 word (4 bytes):

  lfsr_data_t            lfsr_cat_t
  .---+---+---+---.      .---+---+---+---.
  |0|    size     |  =>  |0|    size     |
  +---+---+---+---+      +---+---+---+---+
  |      ptr      |      |      ptr      |
  +---+---+---+---+      '---+---+---+---'
  |    (unused)   |
  '---+---+---+---'
  '-------.-------'      '-------.-------'
      12 bytes                8 bytes

While on-disk references increase by 2 words (8 bytes):

  lfsr_data_t            lfsr_cat_t          lfsr_data_t
  .---+---+---+---.      .---+---+---+---.   .---+---+---+---.
  |1|    size     |  =>  |1|    size     | .>|1|    size     |
  +---+---+---+---+      +---+---+---+---+ | +---+---+---+---+
  |     block     |      |      ptr -------' |     block     |
  +---+---+---+---+      '---+---+---+---'   +---+---+---+---+
  |      off      |                          |      off      |
  '---+---+---+---'                          '---+---+---+---'
  '-------.-------'      '-----------------.-----------------'
      12 bytes                         20 bytes

Unless the on-disk references also need concatenation, in which case
this still saves 1 word (4 bytes).

Note I'm not sure this type split is generalizable to other systems. In
littlefs we can't use recursion, so progging concatenated datas already
required two nested functions, and we happen to never need to read
concatenated data, allowing us to completely omit that functionality. In
other systems, where maybe disk-reference attrs are more common, this
tradeoff may not make sense.

Some other things to note:

- We're also losing the inlined-data representation in this change.
  Unfortunately earlier lfsr_data_t measurements showed that this didn't
  really contribute much. It saved RAM in name attrs but added quite a
  bit of complexity to lfsr_data_t operations.

- By separating simple/cat and RAM/disk, we reduce the abused size bits
  from 2-bits down to 1-bit. This doesn't really matter for our current
  31/28-bit littlefs impl, but is nice in that it reenables the
  theoretical 31/31-bit littlefs impl without in-RAM data-structure
  changes.

There are a few temporary hacks that need to be figured out, but this is
already showing code/stack savings. Which is fascinating considering the
new lfsr_cat_* functions and increased temporary allocations:

           code          stack
  before: 33856           2824
  after:  33812 (-0.1%)   2800 (-0.8%)
2024-05-09 14:16:19 -05:00
Christopher Haster ab2a1cb571 Enabled erase=noop in test_rbyd, changed read* to error on leb128 overflow
Now that reproducibility issues with erase_value=-1 (erase=noop) are
fixed, this much more useful to test than erase_value=0x1b. Especially
since erase=noop is filled with so many sharp corners.

These tests already found that we were being too confident with our
leb128/lleb128/tag parsing. Since we need to partially parse unfinished/
old commits, lfsr_dir_read* can easily encounter invalid leb128s during
normal operation. If this happens we should not assert.

Doing things correctly has a bit of a cost:

           code          stack
  before: 33928           2824
  after:  33976 (+0.1%)   2824 (+0.0%)

At least we haven't seen any issues with our valid bit invalidating
logic yet.
2024-05-04 17:27:01 -05:00
Christopher Haster 8a75a68d8b Made rbyd cksums erased-state agnostic
Long story short, rbyd checksums are now fully reproducible. If you
write the same set of tags to any block, you will end up with the same
checksum.

This is actually a bit tricky with littlefs's constraints.

---

The main problem boils down to erased-state. littlefs has a fairly
flexible model for erased-state, and this brings some challenges. In
littlefs, storage goes through 2 states:

1. Erase - Prepare storage for progging. Reads after an erase may return
   arbitrary, but consistent, values.

2. Prog - Program storage with data. Storage must be erased and no progs
   attempted. Reads after a prog must return the new data.

Note in this model erased-state may not be all 0xffs, though it likely
will be for flash. This allows littlefs to support a wide range of
other storage devices: SD, RAM, NVRAM, encryption, ECC, etc.

But this model also means erased-state may be different from block to
block, and even different on later erases of the same block.

And if that wasn't enough of a challenge, _erased-state can contain
perfectly valid commits_. Usually you can expect arbitrary valid cksums
to be rare, but thanks to SD, RAM, etc, modeling erase as a noop, valid
cksums in erased-state is actually very common.

So how do we manage erased-state in our rbyds?

First we need some way to detect it, since we can't prog if we're not
erased. This is accomplished by the forward-looking erased-state cksum
(ecksum):

  .---+---+---+---.     \
  |     commit    |     |
  |               |     |
  |               |     |
  +---+---+---+---+     +-.
  |     ecksum -------. | | <-- ecksum - cksum of erased state
  +---+---+---+---+   | / |
  |     cksum --------|---' <-- cksum - cksum of commit,
  +---+---+---+---+   |                 including ecksum
  |    padding    |   |
  |               |   |
  +---+---+---+---+ \ |
  |     erased    | +-'
  |               | /
  .               .
  .               .

You may have already noticed the start of our problems. The ecksum
contains the erased-state, which is different per-block, and our rbyd
cksum contains the ecksum. We need to include the ecksum so we know if
it's valid, but this means our rbyd cksum changes block to block.

Solving this is simple enough: Stop the rbyd's canonical cksum before
the ecksum, but include the ecksum in the actual cksum we write to disk.

Future commits will need to start from the canonical cksum, so the old
ecksum won't be included in new commits, but this shouldn't be a
problem:

  .---+---+---+---. . . \ . \ . . . . .---+---+---+---.     \   \
  |     commit    |     |   |         |     commit    |     |   |
  |               |     |   +- rbyd   |               |     |   |
  |               |     |   |  cksum  |               |     |   |
  +---+---+---+---+     +-. /         +---+---+---+---+     |   |
  |     ecksum -------. | |           |     ecksum    |     .   .
  +---+---+---+---+   | / |           +---+---+---+---+     .   .
  |     cksum --------|---'           |     cksum     |     .   .
  +---+---+---+---+   |               +---+---+---+---+     .   .
  |    padding    |   |               |    padding    |     .   .
  |               |   |               |               |     .   .
  +---+---+---+---+ \ | . . . . . . . +---+---+---+---+     |   |
  |     erased    | +-'               |     commit    |     |   |
  |               | /                 |               |     |   +- rbyd
  .               .                   |               |     |   |  cksum
  .               .                   +---+---+---+---+     +-. /
                                      |     ecksum -------. | |
                                      +---+---+---+---+   | / |
                                      |     cksum ------------'
                                      +---+---+---+---+   |
                                      |    padding    |   |
                                      |               |   |
                                      +---+---+---+---+ \ |
                                      |     erased    | +-'
                                      |               | /
                                      .               .
                                      .               .

The second challenge is the pesky possibility of existing valid commits.
We need some way to ensure that erased-state following a commit does not
accidentally contain a valid old commit.

This is where are tag's valid bits come into play: The valid bit of each
tag must match the parity of all preceding tags (equivalent to the
parity of the crc32c), and we can use some perturb bits in the cksum tag
to make sure any tags in our erased-state do _not_ match:

  .---+---+---+---. \ . . . . . .---+---+---+---. \   \   \
  |v|    tag      | |           |v|    tag      | |   |   |
  +---+---+---+---+ |           +---+---+---+---+ |   |   |
  |     commit    | |           |     commit    | |   |   |
  |               | |           |               | |   |   |
  +---+---+---+---+ +-----.     +---+---+---+---+ +-. |   |
  |v|p|  tag      | |     |     |v|p|  tag      | | | |   |
  +---+---+---+---+ /     |     +---+---+---+---+ / | |   |
  |     cksum     |       |     |     cksum     |   | .   .
  +---+---+---+---+       |     +---+---+---+---+   | .   .
  |    padding    |       |     |    padding    |   | .   .
  |               |       |     |               |   | .   .
  +---+---+---+---+ . . . | . . +---+---+---+---+   | |   |
  |v---------------- != --'     |v------------------' |   |
  |     erased    |             +---+---+---+---+     |   |
  .               .             |     commit    |     |   |
  .               .             |               |     |   |
                                +---+---+---+---+     +-. +-.
                                |v|p|  tag      |     | | | |
                                +---+---+---+---+     / | / |
                                |     cksum ----------------'
                                +---+---+---+---+       |
                                |    padding    |       |
                                |               |       |
                                +---+---+---+---+       |
                                |v---------------- != --'
                                |     erased    |
                                .               .
                                .               .

New problem! The rbyd cksum contains the valid bits, which contain the
perturb bits, which depends on the erased-state!

And you can't just derive the valid bits from the rbyd's canonical
cksum. This avoids erased-state poisoning, sure, but then nothing in the
new commit depends on the perturb bits! The catch-22 here is that we
need the valid bits to both depend on, and ignore, the erased-state
poisoned perturb bits.

As far as I can tell, the only way around this is to make the rybd's
canonical cksum not include the parity bits. Which is annoying, masking
out bits is not great for bulk cksum calculation...

But this does solve our problem:

  .---+---+---+---. \ . . . . . .---+---+---+---. \   \   \   \
  |v|    tag      | |           |v|    tag      | |   |   o   o
  +---+---+---+---+ |           +---+---+---+---+ |   |   |   |
  |     commit    | |           |     commit    | |   |   |   |
  |               | |           |               | |   |   |   |
  +---+---+---+---+ +-----.     +---+---+---+---+ +-. |   |   |
  |v|p|  tag      | |     |     |v|p|  tag      | | | |   .   .
  +---+---+---+---+ /     |     +---+---+---+---+ / | |   .   .
  |     cksum     |       |     |     cksum     |   | .   .   .
  +---+---+---+---+       |     +---+---+---+---+   | .   .   .
  |    padding    |       |     |    padding    |   | .   .   .
  |               |       |     |               |   | .   .   .
  +---+---+---+---+ . . . | . . +---+---+---+---+   | |   |   |
  |v---------------- != --'     |v------------------' |   o   o
  |     erased    |             +---+---+---+---+     |   |   |
  .               .             |     commit    |     |   |   +- rbyd
  .               .             |               |     |   |   |  cksum
                                +---+---+---+---+     +-. +-. /
                                |v|p|  tag      |     | | o |
                                +---+---+---+---+     / | / |
                                |     cksum ----------------'
                                +---+---+---+---+       |
                                |    padding    |       |
                                |               |       |
                                +---+---+---+---+       |
                                |v---------------- != --'
                                |     erased    |
                                .               .
                                .               .

Note that because each commit's cksum derives from the canonical cksum,
the valid bits and commit cksums no longer contain the same data, so our
parity(m) = parity(crc32c(m)) trick no longer works.

However our crc32c still does tell us a bit about each tag's parity, so
with a couple well-placed xors we can at least avoid needing two
parallel calculations:

  cksum' = crc32c(cksum, m)
  valid' = parity(cksum' xor cksum) xor valid

This also means our commit cksums don't include any information about
the valid bits, since we mask these out before cksum calculation. Which
is a bit concerning, but as far as I can tell not a real problem.

---

An alternative design would be to just keep track of two cksums: A
commit cksum and a canonical cksum.

This would be much simpler, but would also require storing two cksums in
RAM in our lfsr_rbyd_t struct. A bit annoying for our 4-byte crc32cs,
and a bit more than a bit annoying for hypothetical 32-byte sha256s.

It's also not entirely clear how you would update both crc32cs
efficiently. There is a way to xor out the initial state before each
tag, but I think it would still require O(n) cycles of crc32c
calculation...

As it is, the extra bit needed to keep track of commit parity is easy
enough to sneak into some unused sign bits in our lfsr_rbyd_t struct.

---

I've also gone ahead and mixed in the current commit parity into our
cksum's perturb bits, so the commit cksum at least contains _some_
information about the previous parity.

But it's not entirely clear this actually adds anything. Our perturb
bits aren't _required_ to reflect the commit parity, so a very unlucky
power-loss could in theory still make a cksum valid for the wrong
parity.

At least this situation will be caught by later valid bits...

I've also carved out a tag encoding, LFSR_TAG_PERTURB, solely for adding
more perturb bits to commit cksums:

  LFSR_TAG_CKSUM          0x3cpp  v-11 cccc -ppp pppp

  LFSR_TAG_CKSUM          0x30pp  v-11 ---- -ppp pppp
  LFSR_TAG_PERTURB        0x3100  v-11 ---1 ---- ----
  LFSR_TAG_ECKSUM         0x3200  v-11 --1- ---- ----
  LFSR_TAG_GCKSUMDELTA+   0x3300  v-11 --11 ---- ----

  + Planned

This allows for more than 7 perturb bits, and could even mix in the
entire previous commit cksum, if we ever think that is worth the RAM
tradeoff.

LFSR_TAG_PERTURB also has the advantage that it is validated by the
cksum tag's valid bit before being included in the commit cksum, which
indirectly includes the current commit parity. We may eventually want to
use this instead of the cksum tag's perturb bits for this reason, but
right now I'm not sure this tiny bit of extra safety is worth the
minimum 5-byte per commit overhead...

Note if you want perturb bits that are also included in the rbyd's
canonical cksum, you can just use an LFSR_TAG_SHRUBDATA tag. Or any
unreferenced shrub tag really.

---

All of these changes required a decent amount of code, I think mostly
just to keep track of the parity bit. But the isolation of rbyd cksums
from erased-state is necessary for several future-planned features:

           code          stack
  before: 33564           2816
  after:  33916 (+1.0%)   2824 (+0.3%)
2024-05-04 17:25:01 -05:00
Christopher Haster 9b4e1b4cb7 Replace assert(!err) with assert(err == 0) in tests
This plays better with prettyasserts.py, which prints the err value on
failure.

We _could_ extend prettyasserts.py to print the contents of !err
patterns, but this risks making the error message more confusing when
the target is an actual boolean expression. Keep in mind
prettyasserts.py is purely syntactical and doesn't really know the
expression's type.
2024-03-23 16:27:19 -05:00
Christopher Haster 0a89d0c254 Fixed recoloring tail-recursion violations during range removals
I spoke too soon and made a mistake when reenabling color preservation
during range removals.

I assumed, that thanks to replacing the diverging alt with a new black
alt for stitching together diverging trunks, we would avoid the issue
where a deleted diverging alt violates our rbyd's tail-recursive
recoloring invariant.

Unfortunately, this is not the case. All the stitching alt did was make
this violation more difficult to reach, but still reachable. Arguable a
worse situation.

Now, for this violation to happen, in addition to all of the other
requirements, we need the lower-diverging trunk to become empty.

This is the only case where we have no stitching alt, because we don't
need to stitch an empty trunk. Which means if the upper-diverging trunk
has yellow nodes both before and after the diverging alt, our
tail-recursive recoloring invariant can break.

Here's an example:

     .-------------r-------------.
   .-o-.   .---+---y----.      .-o-.
  .o. .o. .o. .o. .o. .-y-+-. .o. .o.
  a a a a a a a a c c c e e e e e e e
                 '--+--'
                  remove

Again, this doesn't capture the alt-layout, which _is_ important, so
here's the dbgrbyd.py view:

                .-> aa                      .-> aa
              .-b-> a                     .-b-> a
              | .-> a                     | .-> a
  .-----------b-b-> a             .-------b-b-> a
  |             .-> a             |         .-> a
  |   .---------b-> a             |       .-b-> a
  |   |         .-> a             |       | .-> a
  |   | .-------b-> a             |     .-b-b-> a
  r-b-y-r-b-----b-> cc -.     =>  y-y-r-b-----> ee <- two yellows!
    |     |     '-> c   + rm        | '-----b-> e     different dirs!
    |     |     .-> c  -'           |       '-> e     should not happen!
    |     '-y-r-b-> ee              |       .-> e
    |       | '---> e               |     .-b-> e
    |       '-----> e               |     | .-> e
    |           .-> e               '-----b-b-> e
    |         .-b-> e
    |         | .-> e
    '---------b-b-> e

And the steps in our appendattr algorithm that led to this state, which
is insightful:

  read <r => [<r]
  read >b => [<r >b]
  read <r => [<r >b <r]
  read <r => [<r >b <r <r]
                     ^--^------ red + red implies yellow
  ysplit  => [<r >r <b]
  reorder => [<r <r >b]
              ^--^------------- yellow-same-dir invariant held
  read >b => [<r <r >b >b]
  diverge => [<r <r >b]
  read >r => [<r <r >b >r]
  read >r => <r [<r >b >r >r]
                ^-----------^-- our 4-alt fifo for flips/coloring
  ysplit  => <r [<r >r >b]
  reorder => <r [>r >r <b]
                 ^--^---------- yellow-same-dir invariant held
             ^---^------------- yellow-same-dir invariant NOT held
                                though 2 yellows is also a problem

The previous commit fixing this bug for the one-pass algorithm may also
be useful.

This tree is now tested in test_rbyd_delete_range_rydye and
test_rbyd_delete_range_rydye_backwards, though only
test_rbyd_delete_range_rydye_backwards reveals the bug, since the bug
requires _specifically_ the lower-diverging trunk to become empty (both
rydy and rydye now have in-order and backwards tests in case of other
chirality issues).

---

Taking a step back, and looking at this bug from a higher-level, the
core of the issue is that we are somewhat arbitrarily deleting nodes
after splitting nodes. This can break our tail-recursive recoloring
invariant.

What the heck is our tail-recursive recoloring invariant?

This is a property of 2-3-4 and greater B-trees, and transitively
red-black and red-black-yellow trees, that allows for tail-recursive,
self-balancing node insertion.

Basically, if you eagerly split any 4-nodes you encounter as you descend
down the tree, you will always be guaranteed to have an open slot in
your parent, so pushing up split nodes (or recoloring) only ever
propagates up a single level:

   .-----.        .-------.        .-------.
   |.a.h.|        |.a.c.h.|        |.a.c.h.|
   '|-|-|'        '|-|-|-|'        '|-|-|-|'
      |            .-' '-.          .-' '--.
      v            v     v          v      v
  .-------.      .---. .---.      .---. .-----.
  |.b.c.g.|  =>  |.b.| |.g.|  =>  |.b.| |.e.g.|
  '|-|-|-|'      '|-|' '|-|'      '|-|' '|-|-|'
       |                |              .-' '-.
       v                v              v     v
   .-------.        .-------.        .---. .---.
   |.d.e.f.|        |.d.e.f.|        |.d.| |.f.|
   '|-|-|-|'        '|-|-|-|'        '|-|' '|-|'

If you lazily split, you aren't guaranteed an open slot in your parent,
so you need recursion to solve splits. This is why 2-3 trees, though
self-balancing, are not tail-recursive:

   .-----.         .-----.
   |.a.h.|         |.a.h.|
   '|-|-|'         '|-|-|'
      |               |
      v               v
  .-------.      .'''''''''.
  |.b.c.g.|  =>  >.b.c.e.g.< 5!?
  '|-|-|-|'      '|.|.|.|.|'
       |            .-' '-.
       v            v     v
   .-------.      .---. .---.
   |.d.e.f.|      |.d.| |.f.|
   '|-|-|-|'      '|-|' '|-|'

But if you are eagerly splitting while also deleting nodes:

   .-----.        .-------.        .-------.              .'''''''''.
   |.a.h.|        |.a.c.h.|        |.a.c.h.|         5!?  >.a.c.e.h.<
   '|-|-|'        '|-|-|-|'        '|-|-|-|'              '|.|.|.|.|'
      |            .-' '-.          .-' '---.            .---' | '---.
      v            v     v          v       v            v     v     v
  .-------.      .---. .---.      .---. .-------.      .---. .---. .---.
  |.b.c.g.|  =>  |.b.| |.g.|  =>  |.b.| |.d.e.f.|  =>  |.b.| |.d.| |.g.|
  '|-|-|-|'      '|-|' '|-|'      '|-|' '|-|-|-|'      '|-|' '|-|' '|-|'
       | x              | x
       v                v
   .-------.        .-------.
   |.d.e.f.|        |.d.e.f.|
   '|-|-|-|'        '|-|-|-|'

Suddenly, recursion. This is a problem.

The workaround implemented here is to check during pruning if our parent
may risk recursion, and if so, recolor the last alt so nothing will
break.

This ends up equivalent to the following transformation:

   .-----.        .-------.        .-----.          .-----.
   |.a.h.|        |.a.c.h.|        |.a.c.|          |.a.c.|
   '|-|-|'        '|-|-|-|'        '|-|-|'          '|-|-|'
      |            .-' '-.          .-' '-.          .-' '--.
      v            v     v          v     v          v      v
  .-------.      .---. .---.      .---. .---.      .---. .-----.
  |.b.c.g.|  =>  |.b.| |.g.|  =>  |.b.| |.h.|  =>  |.b.| |.e.h.|
  '|-|-|-|'      '|-|' '|-|'      '|-|' '|-|'      '|-|' '|-|-|'
       | x              | x              |              .-' '-.
       v                v                v              v     v
   .-------.        .-------.        .-------.        .---. .---.
   |.d.e.f.|        |.d.e.f.|        |.d.e.f.|        |.d.| |.f.|
   '|-|-|-|'        '|-|-|-|'        '|-|-|-|'        '|-|' '|-|'

You may notice this isn't exactly optimal. The >h branch ends up one
level lower, making the balance of the tree off by one. But it at least
ends up with a functional tree.

I may try to find a better solution...

---

The test_rbyd_delete_range_rydy/rydye tests should cover the cases where
a diverging alt is deleted.

I also tried to write tests for the cases where an alt is pruned, the
closest I got is in test_rbyd_delete_range_dryy_backwards, but I
couldn't actually come up with a sequence that would break our rbyds.

In theory it's possible, but it would need this substructure:

      .-------> c      y-r-b-------> c
  y-r-b-y-r-b-> c  or  | | '-y-r-b-> c
  | |   | | |                | | |

Which, as far as I can tell, can't actually be created with our current
algorithm...

Note the inverse structure:

  .---------> c
  | .-y-r-b-> c
  y-r-  | |

Will be pruned before it has a chance to split. So there is no invariant
concerns there. We only have issues when it's the tail alts that get
pruned, because we decide to split before we know if we are pruning or
not. I don't think this can be avoided without additional read-ahead.

Also, even if we could create the above substructure, because we are on
a diverged trunk, and by definition all alts point the same direction,
we would never end up violating our same-dir yellow invariant/assert...

Code changes:

           code          stack
  before: 33880           2880
  after:  33912 (+0.1%)   2880 (+0.0%)
2024-03-15 00:30:43 -05:00
Christopher Haster 47416c1115 Switched to recoloring + red stitching removals due to diverged coloring bug
This was a nasty bug. I was initially concerned that this slipped
through our rbyd tests until I realized how excruciatingly rare it is.

If, during a range remove:

1. There is a pending yellow split immediately after the diverging alt
2. There is a pending yellow split immediately before the diverging alt
3. The diverging alt takes a black alt in the yellow split
4. There is a red node before the pending split before the diverging alt
5. The two alts in the red node point in different directions

We can end up violating our yellow node both-alts-point-same-direction
invariant.

The tree looks like this:

     .-------------r-------------.
   .-o-.      .----y---+---.   .-o-.
  .o. .o. .-+-y-. .o. .o. .o. .o. .o.
  a a a a a a a c e e e e e e e e e e
               '+'
              remove

Though this diagram doesn't capture the actual alt-layout, which does
matter here, so the dbgrbyd.py rendering may be more useful:

                .-> aa                      .-> aa
              .-b-> a                     .-b-> a
              | .-> a                     | .-> a
  .-----------b-b-> a               .-----b-b-> a
  |         .-----> a               |       .-> a
  |         | .---> a               | .-----b-> a
  |       .-y-r-b-> a               | |   .---> a
  |       |     '-> cc <- rm        | |   |
  r-b-y-r-b-----b-> ee        =>  y-y-r-b-r-b-> ee <- two yellows!
    | | |       '-> e             |     |   '-> e     different dirs!
    | | '-------b-> e             |     '-b-b-> e     should not happen!
    | |         '-> e             |       | '-> e
    | '---------b-> e             |       '-b-> e
    |           '-> e             |         '-> e
    |           .-> e             |         .-> e
    |         .-b-> e             |       .-b-> e
    |         | .-> e             |       | .-> e
    '---------b-b-> e             '-------+-b-> e

If all of these conditions are met, and we are preserving coloring, we
can end up with two yellow splits without an intermediate black alt,
implying recursion. But we're of course not recursive, so things just
break.

If we look at the trunk that is being built during our range removal:

  read <r => [<r]
  read >b => [<r >b]
  read >r => [<r >b >r]
  read >r => [<r >b >r >r]
                     ^--^------ red+red implies yellow
  ysplit  => [<r >r >b]
  reorder => [>r >r <b]
              ^--^------------- yellow-same-dir invariant held
  read <b => [>r >r <b <b]
  diverge => [>r >r <b]
  read <r => [>r >r <b <r]
  read <r => >r [>r <b <r <r]
                ^-----------^-- our 4-alt fifo for flips/coloring
  ysplit  => >r [>r <r <b]
  reorder => >r [<r <r >b]
                 ^--^---------- yellow-same-dir invariant held
             ^---^------------- yellow-same-dir invariant NOT held
                                though 2 yellows is also a problem

The important thing to note is that the diverging alt is effectively
deleted in both search paths. If the diverging alt is between two yellow
splits, that's not good.

If you think about the mapping to the underlying 2-3-4 tree, append is
only guaranteed to be tail-recursive because we eagerly split 4-nodes
into 2 2-nodes, ensuring that our parent always has a slot available for
a split (this is why 2-3 trees are not tail-recursive). But if we delete
one of the 2-nodes, and find another 4-node, the parent's slot has
already been taken. This is basically the problem we are running into
here.

A hypothetical 2-3-4-5 tree however...

Probably-isomorphic to a 2-3-4-5 tree, there are a couple of possible
solutions to this:

1. Increase the fifo to 5(?) alts and recursively propagate recolorings
   up 2 nodes.

   Note this would still be bounded and tail-recursive. Our current
   implementation is basically an isomorphism of recursively propagating
   recolorings up 1 node after all, if you want to think about it in
   about the most complicated way possible...

   Downsides: The increased fifo size means more RAM cost. And the
   implementation would be complicated as hell. Not to mention error
   prone. Imagine ~2x the current 15K lines of rbyd tests. It would be
   bad.

2. Discard split recolorings after a diverged alt.

   This would be quite a bit simpler, though would still require some
   annoying state to know if the previous alt diverged.

   If this state isn't perfect, the above checklist of conditions would
   just be incremented by 1, making this bug even harder to track down.

I'm starting to think that preserving color during range removals is a
bit complicated for its own good.

Considering that color-preserving range removals aren't even rigorous
and don't guarantee a balanced tree, I think this all just needs to be
scrapped until a more rigorous solution is found.

---

So this commit drops color-preserving range removals, and moves to a
simpler paint it black + stitch together alternating red alt strategy
when encountering a diverging range removal.

Thanks to the red-stitching, the resulting search path is at least
tried to be kept as small as possible.

This results in the following, not-broken tree:

                .-> aa                        .-> aa
              .-b-> a                       .-b-> a
              | .-> a                       | .-> a
  .-----------b-b-> a                 .-----b-b-> a
  |         .-----> a                 | .-------> a
  |         | .---> a                 | |   .---> a
  |       .-y-r-b-> a                 | |   | .-> a
  |       |     '-> cc <- rm          | |   | |
  r-b-y-r-b-----b-> ee        =>  y-r-b-r-b-r-b-> ee
    | | |       '-> e             | |     '-----> e
    | | '-------b-> e             | '-------b-b-> e
    | |         '-> e             |         | '-> e
    | '---------b-> e             |         '-b-> e
    |           '-> e             |           '-> e
    |           .-> e             |           .-> e
    |         .-b-> e             |         .-b-> e
    |         | .-> e             |         | .-> e
    '---------b-b-> e             '---------b-b-> e

It's interesting to note that this bug is so rare that it was only
caught by test_dirs_mv_fuzz after 2180 heuristic powerlosses. But it
was caught, so that's a good sign.

But it would have been better if this was caught in the rbyd tests. I've
gone ahead and added a specialized test, test_rbyd_delete_range_rry (and
a few other), to prevent a regression, which is very likely. It's more
likely than not we'll revisit range removals in the future.

On the plus side, since recoloring is simpler than color-preservation,
this means less code:

           code          stack
  before: 34072           2880
  after:  33992 (-0.2%)   2880 (+0.0%)
2024-03-05 15:02:56 -06:00
Christopher Haster 788a9d0129 Added lfsr_bd_unprog to replace flcksum args
Topologically, this isn't really much of a change. We just moved the
flcksum -> lfs.pcksum and made the internal API a bit better.

But hey, a better internal API at ~no cost is always a good thing:

           code          stack          lfs_t
  before: 33868           2880            212
  after:  33856 (-0.0%)   2880 (+0.0%)    216 (+1.9%)
2024-02-25 03:30:41 -06:00
Christopher Haster 4a66816d4f Renamed SUP/SUBMASK -> SUP/SUB
There wasn't really a collision with this, and I think it's clear what
these flags are doing.

Also fixed a missed renamed of lfsr_tag_issup/subwide ->
lfsr_tag_issup/sub
2024-02-24 14:41:39 -06:00
Christopher Haster 35a4934178 Switched to passing lfsr_data_t by value again
Thanks to poor compound literal optimization, it's actually cheaper to
pass lfsr_data_t by value everywhere, than to make all LFSR_DATA_*
macros lvalues:

  before: 34340           2896
  after:  34292 (-0.1%)   2896 (+0.0%)

Why are these two design choices linked? If lfsr_data_t is
pass-by-address, the rvalue/lvalue disinction is important because we
need to take the address of LFSR_DATA_* macros. If lfsr_data_t is
pass-by-value, rvalue/lvalue doesn't really matter because we, well,
pass by value.

To be honest, this is a bit of an excuse for better lfsr_data_t
ergonomics. It _is_ generally worse code-size wise to pass lfsr_data_t
by value, because most ABI optimizations stop at 2 words and
lfsr_data_t requires 3 words. But always passing lfsr_data_t by value
even if it is suboptimal makes for more consistent internal interfaces.

This also helps side-step a mistake I made earlier where I though
cat/fromimm/fromleb128 were the only LFSR_DATA_* macros that needed to
be lvalues to be consistent. THERE ARE MANY MORE LFSR_DATA_* macros,
every LFSR_DATA_FROMBLAH macro to be specific, and the resulting code
cost would be MUCH WORSE.

---

This also add lfsr_sprout_t to complement lfsr_bptr_t/lfsr_shrub_t/etc.
Unlike lfsr_data_t, lfsr_sprout_t _is_ pass-by-address

Actually that's the only difference, haha. lfsr_sprout_t is a typedef.

Though to be fair, by being pass-by-addres, lfsr_sprout_t keeps the
internal sprout/shrub/bptr/btree inferfaces consistent, and saves a bit
of code.
2024-02-24 00:52:20 -06:00
Christopher Haster 748bca0b61 Dropped LFSR_ATTR() prefix magic
Before:

  LFSR_ATTR(RM(SUBMASK(REG)), 0, BUF("hi", 2))

Now:

  LFSR_ATTR(
      LFSR_TAG_RM | LFSR_TAG_SUBMASK | LFSR_TAG_REG, 0,
      LFSR_DATA_BUF("hi", 2))

Yes, it's more verbose now.

But there were a couple reasons for dropping the idea:

- The implicit prefixing is a bit magical, and not really all that
  common in C code. It would likely confuse new users on first read.

- The implicitly prefixing macros did not play will with macro expansion
  rules.

  In particular, because the nested not-yet-prefixed macros aren't
  really macros, they aren't expanded as a part of argument prescan.
  This led to surprising compile-time errors, and prevented recursive
  attr-lists (which may be useful for shrubs).

- Implicit prefixes is not very C-like, and in particular it gets in the
  way of sed/grep operations on source files.

- RM(SUBMASK(REG)) for combining tags is (IMO) ugly, compared to
  LFSR_TAG_RM | LFSR_TAG_SUBMASK | LFSR_TAG_REG, even if the latter
  requires more typing.

- Sometimes you need runtime-dependent TAG/DATA values, which implicit
  prefixing gets in the way of. The LFSR_TAG_TAG(tag)/
  LFSR_DATA_DATA(tag) backdoors worked around this, but they are even
  more magical, and added noise to a not-actually-all-that-uncommon use
  case.

And it's really not _that_ much extra effort to write out the prefixes
everywhere.

lfs.c:

          lines           bytes
  before: 16894          537171
  after:  16907 (+0.1%)  538340 (+0.2%)

tests/*.toml:

          lines            bytes
  before: 53306          1811035
  after:  54517 (+2.3%)  1851006 (+2.2%)

qadte came in quite handy again for refactoring the tests without
completely losing my sanity.
2024-02-22 18:25:38 -06:00
Christopher Haster b21f4b81fa Cleaned/reworked bd/caching layer
We really had ~2 duplicate bd layers for a bit there.

This also involved a sort of rewrite of these low-level functions to see
if there were simplifications that could be made.

A couple tweaks:

- Added small low-level lfsr_bd_read/prog/erase/sync_ functions to
  only wrap the bd callbacks and apply any relevant asserts.

  These should be the only place we call the bd callbacks to make it
  easy to read/audit/insert hooks in the future.

- Changed pcache flush lazily, rather than eagerly flushing when full.

  This isn't for any real performance reason, it just makes the code
  simpler. It's not like we can shove more data into the pcache once
  full.

  It's _probably_ a good idea to flush eagerly, to avoid delay more work
  until sync, but I couldn't figure out how to make this work cleanly
  without code duplication...

- Deduplicated read pcache overwrites via lfsr_bd_read__.

  This logic is a bit annoying, but we need the pcache to take priority
  whenever we read from disk, which happens when we both fill our
  rcache, and bypass our rcache. Since these code paths go different
  places, another internal function was the only way I could think to
  deduplicate this.

  It may appear that our pcache/rcache prioritization loop will make
  this happen naturally, as it does in lfs_file_read for example, but
  this doesn't quite work as read-alignment requirements may force us to
  read past the pcache... Keep in mind read_size may be > prog_size.

- Dropped LFS_BLOCK_NULL, now using cache.size=0 to indicate a cache is
  unused.

  This avoids a special lfs_block_t value.

- Dropped lfsr_bd_readcksum, we never used this.

  We can always add it back if necessary.

In total, the caching bd prog/read functions now look quite a bit more
like our file read/write functions, so hopefully that's a good thing.

By the virtue of not have ~2 duplicate bd layers, this saves a bit of
code:

           code          stack
  before: 33700           2800
  after:  33560 (-0.4%)   2808 (+0.3%)
2024-02-20 12:33:41 -06:00
Christopher Haster 204f46a131 Reworked internal tests remove unnecessary shim functions
These shims, originally intended to remap the tests to new internal
APIs without a significant rewrite, are a long-outstanding piece of
technical debt. Now that the internal API is more stable, it's time for
that rewrite.

Reasons for not keeping the internal shims:

- They add more complexity to the test suites.
- They come with (out-of-date) constraints that limit what we can test.
- It's more difficult to debug test failures, with 2 layers and all.

I ended up writing a small tree editor out of tree to do most of this
rewrite.

Did it save time? Probably not. But it was quite a bit more fun than
manaully rewriting ~21K lines of code.
2024-02-03 18:39:13 -06:00
Christopher Haster 921fe2ba1b Tweaked documentation of implicit enums in test defines 2024-02-03 18:17:17 -06:00
Christopher Haster 66a557d19d Dropped all alpha lookup table for 'a'+mod 26 arithmetic
I'm not really sure why I thought this required a lookup table...
2024-02-03 18:17:13 -06:00
Christopher Haster 3a90d1046b Reverted insert tags appending, fixed insert issues in named btrees
Changing insert tags to append seems to have broken insertion into named
btrees in a subtle way.

Consider what happens when we insert immediately before a bid that
splits the btree:
1. namelookup returns the right rbyd, with rid=-1
2. converting this into a bid gives us the left rbyd, with rid=weight
3. the commit to insert the bid ends up inserting into the left rbyd

This doesn't initially seem like an issue, both entries are effectively
the same right? Well, not when you have names. The split name tells you
what _follows_, so this unintentional flipping causes the new name to
get placed in the wrong bucket.

It's not clear if it's possible to fix this, at least not without
inverting the split names to indicate what precedes, but that's a step
too far.

This was not detected earlier because I disabled the low-level
rbyd/btree/mtree tests temporarily due to high porting cost. Guess that
goes to show there's a cost to deferring test ports for too long.

---

This issue, along with being inconsistencies between rids/bids and mids,
and being a relatively unintuitive pattern, is the final nail in the
coffin for insert tags inserting after.

Now, insert tags insert before, like in most other systems, and insert
tags in attr-list just have an implicit +1 before them to allow splits
in attr-lists to work.

This is not a pure revert, as some of the changes with all the code
moving around revealed some better detail-level ideas.

And yes, rbyd/btree tests are up to date now. Unfortunately the mtree
tests require a bit more work.

---

One thing definitely worth noting, btree merges were broken! A mistake
in the has-parent condition meant we were never attempting to merge
btrees!

This hid some bugs in the actual btree merge code caused by mixing the
implicit swap of child rbyds to deduplicate code paths with btree commit
now needing to track bid/rid separately from the attr-list.

This should be fixed now. Interesting to note this bug has been in
lfsr_btree_commit_ for a while now! I think ever since we switched to
using trunks for the has-parent check. We just haven't been merging
btree nodes at all. But since not-merging isn't technically an error,
it's difficult to test for.

Code changes:

            code          stack
  before:  33808           2896
  after:   33964 (+0.5%)   2896 (+0.0%)
2024-02-03 18:17:07 -06:00
Christopher Haster 7868ec7122 Ported over most rbyd+btree tests to new attr-list format
Found a bug, and maybe a fundamental issue:

- The lfs_btree_lookupnext_ in lfsr_btree_commit_ no longer needs the
  min32, since we never commit with bid pointing past the end of the
  btree anymore.

  This was mixing the unsigned min32 with our now-signed bid type,
  causing the wrong btree leaf to be fetched when inserting at bid=-1 in
  a non-empty btree.

  Easy fix.

- lfsr_btree_commit_ with bid!=-1, rid=-1 (inserting at the beginning of
  not-the-first rbyd) now actually appends to the leaf to the left of
  the rbyd instead of inserting into the expected rbyd because of how
  lfs_btree_lookup_ works.

  Initially, this doesn't seem like it would be an issue, these should
  be more-or-less equivalent, but this doesn't match
  lfsr_btree_namelookup! This is a big problem!

  This wasn't noticed because it's rare for the high-level tests to
  trigger that many btree splits with names. Named btrees are only used
  for the mtree, and we need mdirs to split before the mtree even splits
  once.

  Not an easy fix.

On the upside, these low-level tests continue to prove themselves
valuable, if tedious to maintain...
2024-02-03 18:17:06 -06:00
Christopher Haster e04748dadd Renamed SUB/SUPWIDE -> SUB/SUPMASK
This name makes more sense to me given what these bits are doing. Though
that may just be from the embedded engineer side.
2024-02-03 18:16:54 -06:00
Christopher Haster 3c13afd5c2 Added explicit test over unreachable tag holes
Unreachable tag holes, null tags that _should_ be unreachable but
actually are reachable, are an unfortunate quirk to our alt tag
encoding. Because we only have an altgt, not altge, our "unreachable"
tag ends up encoded with an altgt 0, an alt, which you may notice, does
not guarantee unreachability.

Fortunately, tag 0, the null tag, should intentionally be unused. So as
long as we never lookup tag 0, nothing should break.

If you do lookup tag 0, you end up with spurious null tags, which can
complicate things.

The solution here is a tag_ = max(tag, 1) in lfsr_rbyd_lookupnext.

---

One interesting thing to note, as I was writing these tests I discovered
that setting tag=max(tag,1) in lfsr_rbyd_appendattr had no effect.
appendattr needs zip the rbyd tree to keep everything connected during
range removals, so tag=0/tag=1 both end up with the same tree.

So might as well drop the tag=max(tag,1) in lfsr_rbyd_appendattr.

A side effect of this, both before and after this commit, is that any
null tag holes created during range removals sort of stick around until
the next compaction.

---

Why altgt and not altge? altgt is the inverse of altle, requiring only
a single bit flip to flip between the two. And trust me, it would be
much more costly to make altle/altgt flips more complicated than a bit
flip.

---

Why altgt/altle and not altge/altlt? This is because our rbyds are
right-leaning, that is, lookups always find the requested rid+tag, or
the next smallest rid+tag.

Consider a simple tree:

       <5
  .----'|
 >=2    |
  |'-.  |
  1  2  5

What should lookup(3) return? If we are right-leaning, the answer
_should_ be 5. But we need to take the <5 branch to determine if there
is a hidden 3 or 4 in that subtree.

altgt/altle does not have that problem:

      <=2
  .----'|
  >1    |
  |'-.  |
  1  2  5

It might seem like you can workaround this by conservatively using the
neighbor +1 as the alt target, but this runs into tag overflow problems.
UATTR(0xff)+1 (0x057f+1) becomes UATTR(0x100) (0x0580) which is not
allowed due to reserving bit 7 for future subtype extensions.

Maybe you can workaround this workaround by using (tag+0x81)&~0x80
anywhere you need to increment (including lookupnext/iteration calls!),
but this becomes a bit of a mess. And there are still concerns about
overflows at the 0x77f boundary and 0xf7f boundary.
2024-02-03 18:16:52 -06:00
Christopher Haster 5f25f32ff1 Adopted SUPWIDE tag bit, parallel to the SUBWIDE (was WIDE) bit
Like SUBWIDE, SUPWIDE allows for "mask-like" operation during rbyd
commits, where you replace an entire subrange of tags with a single tag.

- SUBWIDE - Replace all subtypes of the given suptype - Useful for
  changing the subtype of an attr, for example replacing a BTREE with a
  BSHRUB.

- SUPWIDE - Replace all suptypes of the given rid - Useful for changing
  the suptype of an attr, for example replacing a REG file with an
  ORPHAN file.

These are effectively the same modifier, just with different ranges.

One benefit is this simplifies mid-level operations a bit, rename,
remove, etc, and decreases the stack cost of the related attr lists.
Though this isn't on the hot-path, so not measurable:

            code          stack
  before:  33956           2912
  after:   33928 (-0.1%)   2912 (+0.0%)

But the real motivation for this change is to remove cases where
lfsr_mdir_commit needs to operate on multiple mids. There may be an API
simplification here.
2024-02-03 18:16:50 -06:00
Christopher Haster 6fc040db1a Adopted paren-cond ternary operator style
So:

  x = (cond) ? yes : no;

Where there are always parentheses around the condition, even if not
required for disambiguity. Additional parentheses are always allowed,
but the parenthesized condition helps signal that a ternary operator is
coming earlier in the expression.

This style has grown on me as I think it helps code readability. It
reminds me of the required parentheses for if/while statements.

Might as well adopt codebase-wide.
2024-02-03 18:16:42 -06:00
Christopher Haster 006d656da2 Fixed unaligned data checksumming in two ways (uncrc32c, flcksum)
Checksumming unaligned data during block compaction is surprisingly
tricky. We don't know if our data will be aligned until after
a potentially unbounded number lookups, we need to write data into our
pcache as we go to avoid unnecessary lookups, but if we end up unaligned
we need to revert our checksum to the checksum of the aligned data.

The way I see it there are 4 options:

1. Calculate the checksum after writing data into the block.

   This is the most expensive option, requiring a full second read of
   the data to calculate the checksum. It is simple though.

2. Do a pass over the btree to figure out alignment before writing.

   This at least only reads metadata twice, so is more efficient than
   the 1st option.

3. Keep track of the aligned checksum on each flush, falling back to the
   last flushed checksum if we need to correct alignment.

   This solution is flexible though requires some extra state to track
   multiple checksums.

4. Leverage the math behind CRCs to run the CRC backwards when we
   truncate for alignment.

   This works, though a bit inefficiently, but is strictly tied to
   CRC-related checksums.

   By inefficient I mean that we would likely be limited to a bit-level
   "uncrc32c". It's possible to create nibble/byte tables for uncrc32c,
   but this adds significant code cost for a relatively uncritical
   function.

   I was hopeful that we could leverage the existing tables in both
   functions, but unfortunately it doesn't work out like that. You could
   scan the crc32c table to find the constant to reverse, but this
   requires ~16*2 or ~256 operations vs "naive" ~8 operations per byte.

This commit implements both 3 and 4, defaulting to 4 unless
LFS_NO_UNCRC32C is defined.

The current lfs_uncrc32c implementation is a simple bit-level
implementation, but does allow for crc32c truncation without any extra
state.

              code          stack
  before:    32044           2880
  uncrc32c:  32108 (+0.2%)   2880 (+0.0%)
  flcksum:   32132 (+0.3%)   2880 (+0.0%)
2023-12-17 15:18:10 -06:00
Christopher Haster 02d2919130 Adopted lfsr_rbyd_lookupwide, dropped wide bit in lookups
This trades a runtime check for a different function call. Enforcing
some minor semantics in the function's type/asserts.

This also makes it so there are no special tag bits used during rbyds
lookup, only rbyd commits.

In theory this saves a bit of code, we don't have a runtime check, but
in practice the extra function apparently outweighs the cost of the
runtime check:

            code          stack
  before:  31956           2880
  after:   32024 (+0.2%)   2880 (+0.0%)
2023-12-14 12:30:21 -06:00
Christopher Haster 51e39747c0 Reverting alternate redund block layout in lfsr_mdir_t
See the previous commit for the reason. The alternate redund block
layout is just inferior in terms of both code and RAM.
2023-12-06 22:23:16 -06:00
Christopher Haster 9d182c2055 Attempted alternate redund block layout in lfsr_mdir_t
The idea here is to revert moving redund blocks into lfsr_rbyd_t, and
instead just keep a redundant copy of the rbyd blocks in the redund
blocks in lfsr_mdir_t.

Surprisingly, extra overhead in lfsr_mdir_t ended up with worse stack
usage than extra overhead in lfsr_rbyd_t. I guess we end up allocated
more mdirs than rbyds, which makes a bit of sense given how complicated
lfsr_mdir_commit is:

                    code          stack          structs
  redund union:    30976           2496             1072
  redund in rbyd:  30948 (-0.1%)   2528 (+1.3%)     1100 (+2.6%)
  redund in mdir:  31000 (+0.1%)   2536 (+1.6%)     1092 (+1.8%)

The mdir option does seem to improve struct overhead, but this hasn't
been a reliable measurement since it doesn't take into account how many
of each struct is allocated.

Given that the mdir option is inferior in both code and stack cost, and
requires more care to keep the rbyd/redund blocks in sync, I think I'm
going to revert this for now but keep the commit in the commit history
since it's an interesting comparison.
2023-12-06 22:23:13 -06:00
Christopher Haster becbc0c2ad Moved redundant blocks into the lfsr_rbyd_t struct
This simplifies dependent structs with redundancy, mainly lfsr_mdir_t,
at a significant RAM cost:

            code          stack          structs
  before:  30976           2496             1072
  after:   30948 (-0.1%)   2528 (+1.3%)     1100 (+2.6%)

Which, to be honest, is not as bad as I thought it would be. Though it
is still pretty bad for no new features.

The motivation for this change:

1. The organization of the previous lfsr_mdir_t struct was a bit hacky
   and relied on exact padding so the redund block array and rbyd block
   lined up at the right offset.

2. The previous organization prevented theoretical "read-only rbyd
   structs" that could omit write-related fields, e.g. eoff and cksum.

   This idea is currently unused.

3. The current mdir=level-1, btree/data=level-0 redund design makes this
   RAM tradeoff pretty bad, but in theory higher btree redund levels
   would need the extra redund blocks in the rbyd struct anyways.

Still, the RAM impact to the current default configuration means this
should probably be reverted...
2023-12-06 22:23:11 -06:00
Christopher Haster a89b3e42ba Some cleanup items
- Adopted *_IS* naming convention for sign-bit macros.
- Made all struct initializing macros function-like, including the
  *_NULL() macros.
- Renamed ggrm/dgrm -> grm_g/grm_d.
- Renamed lfsr_mroot_commit_ -> lfsr_mroot_commit.
- Renamed LFSR_FILE_BSPROUT -> LFSR_FILE_ISDIRECT.
- Renamed LFSR_BSPROUT_NULL -> LFSR_FILE_BNULL().
- Dropped *_unerase functions for explicitly setting eoff=-1.
2023-12-06 22:23:06 -06:00