Commit Graph

697 Commits

Author SHA1 Message Date
Christopher Haster 71b9ad2412 bmap: Enabled at least opportunistic bmap allocations
This doesn't fully replace the lookahead buffer, but at least augments
it with known bmap state when available.

To be honest, this is a minimal effort hack to try to get something
benchmarkable without dealing with all the catch-22 issues that a
self-support bmap allocator would encounter (allocating blocks for the
bmap requires a bmap, oh no).

Though now that I'm writing this, maybe this is a reasonable long-term
solution? Having the lookahead buffer to fall back on solves a lot of
problems, and, realistically, it's unlikely to be a performance
bottleneck unless the user has extreme write requests (>available
storage?).

---

Also tweaked field naming to be consistent between the bmap and
lookahead buffer.
2025-10-01 17:56:12 -05:00
Christopher Haster 838a4beee1 bmap: Moved gbmap traversal to the end
This avoids issues with the different traversal paths with an mtree vs
inline-mtree. Previously this was broken when the mtree was inlined.

This order also makes more sense if we want to check mdirs before we
consider the gstate to be trustworthy enough for gbmap traversal.
2025-10-01 17:56:10 -05:00
Christopher Haster 732d6079e3 bmap: Added low-level bmap set algorithm and related tests
The neat thing about the on-disk bmap is that it's a range tree. We can
leverage order-statistic properties to compactly represent ranges of
similar blocks.

However, this does make updating the bmap slightly more complicated...
2025-10-01 17:55:39 -05:00
Christopher Haster a871e02354 btree: Reworked btree traversal to leverage leaf caches
This comes from an observation that we never actually use the leaf cache
during traversals, and there is surprisingly little risk of a lookup
creating a conflict in the future.

Btree traversal fall into two categories:

1. Full traversals, where we traverse a full btree all at once. These
   are unlikely to have lookup conflicts because everything is
   usually self-contained in one chunk of logic.

2. Incremental traversals. These _are_ at risk, but in our current
   design limited to lfs3_trv_t, which already creates a fully
   bshrub/btree copy for tracking purposes.

   This copy unintentionally, but conveniently, protects against lookup
   conflicts.

So, why not reuse the btree leaf cache to hold the rbyd state during
traversals? In theory this makes lfs3_btree_traverse the same cost and
lfs3_btree_lookupnext, drops the need for lfs3_btrv_t, and simplifies
the internal API.

The only extra bit of state we need is the current target bid, which is
now expected as a caller-incremented argument similar to
lfs3_btree_lookupnext iteration.

There was a bit of futzing around with bid=-1 being necessary to
initialize traversal (to avoid conflicts with bid=-1 => 0 caused by
empty btrees). But the end result is a btree traversal that only needs
one extra word of state.

---

Unfortunately, in practice, the savings were not as great as expected:

           code          stack          ctx
  before: 36792           2400          684
  after:  36876 (+0.2%)   2384 (-0.7%)  684 (+0.0%)

This does claw back some stack, but less than a full rbyd due to the
union with the mtortoise in lfs3_trv_t. The mtortoise now dominates. It
might be possible to union the mtortoise and the bshrub/btree state
better (both are not needed at the same time), but strict aliasing rules
in C make this tricky.

The new lfs3_btree_traverse is also a bit more complicated in terms of
code cost. In theory this would be offset by the simpler traversal setup
logic, but we only actually call lfs3_btree_traverse twice:

1. In lfs3_mtree_traverse
2. In lfs3_file_ck

Still, some stack savings + a simpler internal API makes this worthwhile
for now. lfs3_trv_t is also due for a revisit, and hopefully it's
possible to better union things with btree leaf caches somehow.
2025-07-21 16:36:50 -05:00
Christopher Haster cd9f93d859 btree: Resurrected btree leaf caching
This is an indulgence to simplify the upcoming auxiliary btree work.

Brings back the previously-reverted per-btree leaf caches, where each
lfs3_btree_t keeps track of two rbyds: The root and the most recently
accessed leaf.

At the surface level, this optimizes repeated access to the same btree
leaf. A common pattern for a number of littlefs's operations that has
proven tricky to manually optimize:

- Btree iteration
- Pokes for our crystalization heuristic
- Checksum collision resolution for dids and (FUTURE) ddkeys
- Related rattrs attached to a single bid

But the real motivation is to drop lfs3_btree_*lookupleaf and simplify
the internal APIs. If repeated lfs3_btree_lookup*s are already
efficient, there's no reason for extra leaf-level APIs, and in theory
any logic that interacts with btrees will be simpler.

---

This comes at a cost (humorously about the same amount as the
tag-returning refactor, if you ignore the extra 28 bytes of ctx).
Unsurprisingly, increasing the size of lfs3_btree_t has the biggest
impact on stack and ctx:

           code          stack          ctx
  before: 36084           2336          656
  after:  36784 (+1.9%)   2400 (+2.7%)  684 (+4.3%)

Also note from the previous commit messages: Btree leaf caching has
resulted in surprisingly little performance improvement for our current
benchmarks + implementation. It turns out if you're dominated by write
cost, optimizing btree lookups -- which already skip rbyd fetches, has
barely noticeable impact.

---

A note on reverting!

Eventually (after the auxiliary btree work) it will probably make sense
to revert this -- or at least provide a non-leaf-caching build for
code/RAM sensitive users.

I don't think this should be reverted as-is. Instead, I think we should
allow the option to just disable the leaf cache, while keeping the
simpler internal API. This would give us the best of all three worlds:

- A small code/RAM option
- Optimal btree iteration/nearby-lookup performance
- Simpler internal APIs

The only reason this isn't already implemented is because I want to
avoid fragmenting the codebase further while we're still in development
mode.
2025-07-20 13:57:50 -05:00
Christopher Haster 7b330d67eb Renamed config -> cfg
Note this includes both the lfs3_config -> lfs3_cfg structs as well as
the LFS3_CONFIG -> LFS3_CFG include define:

- LFS3_CONFIG -> LFS3_CFG
- struct lfs3_config -> struct lfs3_cfg
- struct lfs3_file_config -> struct lfs3_file_cfg
- struct lfs3_*bd_config -> struct lfs3_*bd_cfg
- cfg -> cfg

We were already using cfg as the variable name everywhere. The fact that
these names were different was an inconsistency that should be fixed
since we're committing to an API break.

LFS3_CFG is already out-of-date from upstream, and there's plans for a
config rework, but I figured I'd go ahead and change it as well to lower
the chances it gets overlooked.

---

Note this does _not_ affect LFS3_TAG_CONFIG. Having the on-disk vs
driver-level config take slightly different names is not a bad thing.
2025-07-18 18:29:41 -05:00
Christopher Haster 2586fe68a2 Renamed traversal -> trv
- test_traversal -> test_trvs
- lfs3_traversal_t -> lfs3_trv_t
- lfs3_btraversal_t -> lfs3_btrv_t
- t -> trv
- bt -> btrv
- lfs3_traversal_* -> lfs3_trv_*
- lfs3_btraversal_* -> lfs3_btrv_*

The traversal type is becoming one of the more fundamental types in
littlefs, and if DIR and REG both get shortened names, it makes sense
for TRV to have one as well.

This also removes the temptation to use t for traversals, which is
probably an even worse name.

---

Note that lfs3_btree_traverse, lfs3_mtree_traverse, etc, remain
unaffected. This may change in the future, but it's interesting to note
that verbs seem to need much less typing than nouns.
2025-07-18 18:28:57 -05:00
Christopher Haster 4cea5af96f Renamed omdir -> handle
- lfs3_omdir_t -> lfs3_handle_t
- lfs3.omdirs -> lfs3.handles
- o -> h
- lfs3_omdir_* -> lfs3_handle_*
- lfs3_omdir_ismidopen -> lfs3_mid_isopen

From conversations with users, the term "handle" or "file handle" seems
to be the most common/easily understood term for the lfs3_file_t struct
itself. It makes sense to adopt this in our codebase.

I usually dislike inventing new names for things when prefixes can imply
a relationship (size -> ssize, cache -> rcache, shrub -> bshrub, etc),
but lfs3_omdirs_t was probably a bit much.
2025-07-18 16:42:54 -05:00
Christopher Haster 35d8c36dd1 tag-returning: Adopted tag-returns in lfs3_mtree_pathlookup
Last but not least, this adopts tag-returns in lfs3_mtree_pathlookup,
and indirectly in all of lfs3_mtree_pathlookup's callers (which is
almost every top-level filesystem function -- anything that needs to
look up a path).

At this level, the muxed tag/err type really shows its versatility. Take
the LFS3_ERR_NOENT and LFS3_TAG_ORPHAN tags/errs for example.
Conceptually, these take very different code paths, but after calling
lfs3_mtree_pathlookup, it's easy to switch on both as though they
represent the same file-not-found condition.

We have to be a bit more careful now to not confuse err and tag
variables in these functions, and `goto failed` is now a bit of a
landmine, but the end result is another nice chunk of code savings:

                       code          stack          ctx
  before:             36216           2336          656
  after:              36084 (-0.4%)   2336 (+0.0%)  656 (+0.0%)

---

I believe this finishes the tag-returning refactor, which means we can
take a step back and look at how effective tag/err muxing is as a code
size optimization:

                       code          stack          ctx
  before tag-returns: 36828           2368          656
  after tag-returns:  36084 (-2.0%)   2336 (-1.4%)  656 (+0.0%)

A free 744 bytes is not bad! Especially considering there's no real
downside to this.

The 32 bytes of stack savings is nice too, and suggests we had ~8
unnecessary tag out-pointers sitting on the stack hot-path.
2025-07-18 16:42:37 -05:00
Christopher Haster f9d7885edc tag-returning: Adopted tag-returns in mdir/mtree namelookup
- lfs3_mdir_namelookup
- lfs3_mtree_namelookup

These are interesting, because, unlike lfs3_rbyd_namelookup, we don't
care about how query mids compare with the found mid.

Adopting tag-returns does mean we no longer return the relevant tag
when the query mid is missing, but the fact that the tests are passing
means this is a non-issue.

Shaves off a bit more code:

           code          stack          ctx
  before: 36260           2336          656
  after:  36216 (-0.1%)   2336 (+0.0%)  656 (+0.0%)

Maybe these should have been updated in lock-step with
lfs3_mtree_pathlookup, but lfs3_mtree_pathlookup is going to impact a
lot more code...
2025-07-18 16:42:34 -05:00
Christopher Haster a549654618 tag-returning: Adopted tag-returns in mtree traversals
- lfs3_mtree_traverse_
- lfs3_mtree_traverse
- lfs3_mtree_gc

I like this one if only for the reduced API noise. All of these layers
need to inspect the tag to know what to do, moving the tag to the return
position means less mucking around with points in our core traversal
logic.

Shaves off a bit more code:

           code          stack          ctx
  before: 36348           2336          656
  after:  36260 (-0.2%)   2336 (+0.0%)  656 (+0.0%)
2025-07-18 16:42:28 -05:00
Christopher Haster bfab282b9e tag-returning: Adopted tag-returnn in mdir lookups
- lfs3_mdir_lookupnext
- lfs3_mdir_lookup

Like btree lookups, mdir lookups are also tag-inspection heavy, so we
see some nice savings:

           code          stack          ctx
  before: 36520           2352          656
  after:  36348 (-0.5%)   2336 (-0.7%)  656 (+0.0%)

lfs3_mdir_lookup also highlights how tag-returns help reduce API noise
around the tag mask bits. lfs3_mdir_lookup's tag out-pointer doesn't
really make sense with the default non-masked tags, and moving it to the
return position hides it aways a bit.
2025-07-18 16:42:22 -05:00
Christopher Haster 100fb66d37 tag-returning: Adopted tag-returns in btree lookups
- lfs3_btree_lookupleaf
- lfs3_btree_lookupnext
- lfs3_btree_lookup
- lfs3_btree_traverse
- NOT lfs3_btree_namelookup

Looks like we're starting to claw back stack usage a bit. This makes
sense as the btree logic involves the most layers -- with out-pointers
it needs more temporary copies to inspect tags along the way:

           code          stack          ctx
  before: 36576           2376          656
  after:  36520 (-0.2%)   2352 (-1.0%)  656 (+0.0%)
2025-07-18 16:42:16 -05: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 0828fd9bf3 Reverted LFS3_CKDATACKSUMREADS -> LFS3_CKDATACKSUMS
LFS3_CKDATACKSUMREADS is just too much.

The downside is it may not be clear how LFS3_CKDATACKSUMREADS interacts
with the future planned LFS3_CKREADS (LFS3_CKREADS implies
LFS3_CKDATACKSUMS + LFS3_CKMETAREDUND), but on the flip side you may
actually be able to type LFS3_CKDATACKSUMS on the first try.
2025-07-16 14:25:20 -05:00
Christopher Haster 0364ed5011 attr: Fixed custom attrs overflowing rattr.count
Not sure how this was missed. The whole tradeoff of shrinking
rattr.count was that by default lfs3_rattr_t would take up less space,
but user-provided buffers would need an indirect lfs3_data_t to support
arbitrary buffer sizes.

This managed to scrape by with a 16-bit count (15-bit really), but
fortunately failed test_attrs_fattr_resync_receive with an 8-bit count.
And only barely! 256 is the smallest possible custom attr that
overflows.

I guess a point towards making internal limitation as tight as possible
to catch mistakes like these earlier.

---

Added test_attrs_setattr_big and test_attrs_fattr_big to catch this in
the future.

Note that while this added some code, stack is unaffected. This is
because custom attribute handling is off the hot-path, which is why the
lfs3_rattr_t -> lfs3_rattr_t+lfs3_data_t split is worth it:

           code          stack          ctx
  before: 37016           2416          652
  after:  37052 (+0.1%)   2416 (+0.0%)  652 (+0.0%)
2025-07-15 16:50:11 -05:00
Christopher Haster 5b0ec8090a Adopted rattr.from for simpler appendrattr_ lazy encoding
This breaks down the previously 16-bit rattr.count field into two 8-bit
rattr.from and rattr.count fields. Now, instead of using a mixture of
rattr.tag and sign(rattr.count) to determine rattr encoding, we just
jump based on rattr.from:

  lfs3_rattr_t:
  .---+---+---+---.
  |  tag  |frm|cnt| -+-> 16-bit tag   - on-disk encoding + rbyd flags
  +---+---+---+---+  +->  8-bit from  - in-RAM encoding
  |     weight    |  '->  8-bit count - from-specific count
  +---+---+---+---+
  |      ptr      |
  '---+---+---+---'

The internal appendrattr_ ctx also saw a bit of rework, and now uses a
big union with multiple buffers instead of stacking a ridiculous number
of LFS_MAX calls. Expanding the LFS_MAX stack grows O(n^2), so this is
probably good for compile times.

And all rattr.from branches now generate an lfs3_data_t*. This was
already a side-effect of all the internal lfs3_data_from* functions, and
it simplifies the tail end of appendrattr_. No more relying on
data_count's sign bit.

Also rearranged rattr.from encoders to match source code order.

---

Unfortunately, while this did simplify the source code, it didn't really
lead to much improvement in code size:

           code          stack          ctx
  before: 37024           2416          652
  after:  37016 (-0.0%)   2416 (+0.0%)  652 (+0.0%)

I guess jump tables are more a performance optimization than a code size
one. That and the benefit of cheaper appendrattr_ logic is likely
overshadowed by the extra constants needed to populate rattr.from in
every LFS3_RATTR_* macro.

Also test_attrs_fattr_resync_receive is now failing, but I think that's
just because of an unrelated bug exposed by the shrinking count field.
In theory rattr.count should be limited to internal fixed-size buffers.
2025-07-15 16:50:11 -05:00
Christopher Haster 0bed3867d8 Adopted more single-char field names
Limited to nested struct fields where the names don't really matter:

- bptr.data -> bptr.d
- mdir.rbyd -> mdir.r

Ok it actually just ended up those two.

This is on the tail end of some optimization work that ended up
abandoned because of maintainability concerns. But it did highlight that
struct nesting gets a bit out-of-control when trying to both optimize
stack allocations and respect C99's strict aliasing.

Consider further fragmenting lfs3_rbyd_t for fine-grain stack
allocations:

  typedef struct lfs3_rbyd {
      struct lfs3_rtrunkcksum {
          struct lfs3_rtrunk {
              lfs3_rid_t weight;
              struct lfs3_rtrunktrunk {
                  lfs3_block_t blocks[2];
                  lfs3_size_t trunk;
              } rtrunktrunk;
          } rtrunk;
          uint32_t cksum;
      } rtrunkcksum;
      lfs3_size_t eoff;
  } lfs3_rbyd_t;

Accessing fields just starts to get silly:

  rbyd.rtrunkcksum.rtrunk.trunktrunk.trunk

At least single-char field names keeps a little bit of readability:

  rbyd.ck.t.t.trunk

Or for some real examples:

- file->b.o.mdir.rbyd.weight -> file->b.o.mdir.r.weight
- bptr->data.u.disk.block -> bptr->d.u.disk.block
2025-07-15 16:50:06 -05:00
Christopher Haster b700c8c819 Dropped fragmenting blocks > 1 fragment
So we now keep blocks around until they can be replaced with a single
fragment. This is simpler, cheaper, and reduces the number of commits
needed to graft (though note arbitrary range removals still keep this
unbounded).

---

So, this is a delicate tradeoff.

On one hand, not fully fragmenting blocks risks keeping around bptrs
containing very little data, depending on fragment_size.

On the other hand:

- It's expensive, and disk utilization during random _deletes_ is not
  the biggest of concerns.

  Note our crystallization algorithm should still clean up partial
  blocks _eventually_, so this doesn't really impact random writes.
  The main concerns are lfs3_file_truncate/fruncate, and in the future
  collapserange/punchhole.

- Fragmenting bptrs introduces more commits, which have their own
  prog/erase cost, and it's unclear how this impacts logging operations.

  There's no point in fragmenting blocks at the head of a log if we're
  going to fruncate them eventually.

I figure lets err on minimizing complexity/code size for now, and if
this turns out to be a mistake, we can always revert or introduce
fragmenting >1 fragment blocks as an optional feature in the future.

---

Saves a big chunk of code, stack, and even some ctx (no more
fragment_thresh):

           code          stack          ctx
  before: 37504           2448          656
  after:  37024 (-1.3%)   2416 (-1.3%)  652 (-0.6%)
2025-07-03 19:46:18 -05:00
Christopher Haster 4747477057 Tweaked lfs3_btree/bshrub_traverse to include weight
Not sure why we weren't already, it doesn't really make sense to return
bid without weight, and this matches lfs3_btree/bshrub_lookupnext.

Sure we don't need weight currently, but this is useful to include in
case we need it in the future (lfs3_bptr_fetch during traversal?).

And while we're not using it, the compiler is happy to optimize it out,
so no code changes:

           code          stack          ctx
  before: 37964           2424          636
  after   37964 (+0.0%)   2424 (+0.0%)  636 (+0.0%)
2025-06-28 19:08:42 -05:00
Christopher Haster 10c0a60ced Tried to dedup bptr/data fetching
Like the bshrub/btree dedup, this add lfs3_bptr_fetch to help dedup
bptr/data fetching.

The original plan was to eliminate bptrs from lfs3_file_lookupnext and
lfs3_file_traverse, and just return tagged data like the other
lookup/traverse functions. But this didn't work out very well. We return
arbitrary attrs from lfs3_file_traverse, so all this would've
accomplished is making every lfs3_file_lookupnext call messier.

But I think I'm still going to keep lfs3_bptr_fetch around as it
provides a nice place to deduplicate some other bits of logic:

- It makes sense to limit bptrs to compressed weights here, as opposed
  to the somewhat arbitrary lfs3_file_lookupnext function.

- And it would be a bit silly to not put the bptr's LFS3_CKFETCHES logic
  in lfs3_bptr_fetch.

  This may fetch more than previously (during crystallization pokes?),
  but better safe than sorry. LFS3_CKFETCHES will likely be a relatively
  niche feature anyways.

As for lfs3_file_traverse, I got rid of it completely.

We already have special logic in lfs3_mtree_traverse_ and lfs3_file_ck
for bptrs anyways, since bptrs, unlike data fragments, reference actual
blocks. And this disentangles lfs3_mtree_traverse_ from the file APIs,
which was a bit of an awkward design.

---

This adds a bit of code to the default build, but I think it's worth it
for the better code organization:

                     code          stack          ctx
  before:           37896           2424          636
  after:            37964 (+0.2%)   2424 (+0.0%)  636 (+0.0%)

It also saves some code in LFS3_CKFETCHES mode, thanks to deduping all
the fetch ckfetches fetch checkhes:

                     code          stack          ctx
  ckfetches before: 38144           2464          636
  ckfetches after:  38072 (-0.2%)   2472 (+0.3%)  636 (+0.0%)
2025-06-28 18:50:57 -05:00
Christopher Haster 2c27c61f25 kv: Added LFS3_KVONLY to opt-out of advanced file operations
One of the ideas behind the key-value API is that it is potentially much
cheaper than a full file API. With the key-value API, we get the
guarantee that all data must fit in RAM, and avoid headaches like
random reads/writes and needing to broadcast file state.

For an example of just how much complexity is avoided, the see the
difference between lfs3_file_flushonce_ vs the mess that is
lfs3_file_flush_ + lfs3_file_crystallize + lfs3_file_graft.

However, littlefs is designed around files, and a couple design
decisions hold back how much code saving is possible:

1. littlefs's shrubs are designed around being enrolled in the omdir
   linked-list, so internally we still have most of the file open/close
   code lumbering around.

2. Directories and traversals still exist, so we'd need the omdir
   linked-list anyways, and we still need to broadcast _some_ changes.

3. Despite being intended for small amounts of data, lfs3_set/get can
   still be used to create arbitrarily large files. So we still need all
   of the bshrub/btree logic.

   Which we still need for the mtree anyways, so this isn't really that
   much of a downside.

It also may be possible to save more code by aggressively rewriting the
_entire_ read/write path for lfs3_set/get, to not reuse any of the
existing file logic in LFS3_KVONLY mode. But I decided against this due
to concerns around maintainability.

The duplicate lfs3_file_read + lfs3_file_readonce and lfs3_file_flush_ +
lfs3_file_flushonce_ are already enough of a concern.

Anyways, here's LFS3_KVONLY:

                  code           stack           ctx
  default:       37824            2416           636
  kvonly:        30936 (-18.2%)   2168 (-10.3%)  636 (+0.0%)

LFS3_RDONLY + LFS3_KVONLY is also interesting:

                  code           stack           ctx
  rdonly:        10776             856           508
  rdonly+kvonly:  9904 (-8.1%)     888 (+3.7%)   508 (+0.0%)

---

This also added some noise to the default build's code, mainly due to
tweaks in lfs3_file_readnext to allow better reuse in LFS3_KVONLY:

           code          stack          ctx
  before: 37824           2416          636
  after:  37860 (+0.1%)   2416 (+0.0%)  636 (+0.0%)
2025-06-24 16:14:02 -05:00
Christopher Haster 92844cce3e kv: Added *_set_zero and *_set_null tests
These are high-risk corner cases for the key-value API, so we should
test them.

At one point I was relying on an optional buffer parameter in
lfs3_file_sync_, but that would have broken if lfs3_set's buffer was
NULL.
2025-06-22 15:36:53 -05:00
Christopher Haster a75537faff kv: Implemented a simple key-value API
This adds a couple functions that treat files as simple key-value pairs:

- lfs3_get    - Read a file
- lfs3_size   - Get the size of a file
- lfs3_set    - Write a file
- lfs3_remove - Remove a file (this one already exists!)

The idea is the only real difference between a filesystem and key-value
store in the microcontroller space is the API, and the key-value API
_is_ much easier to use.

It also opens the door to making the file API opt-out in the future to
trade code cost for feature set. littlefs will probably never be
competitive with other microcontroller-scale key-value stores, but it
may be interesting for systems already using littlefs for other storage.

And don't worry, these are still files, so they can always be opened
with the full file API when more advanced operations are needed.

These APIs also matches the custom attribute APIs, which makes sense
because they're both key-values. Any mismatch should be considered an
API bug, because the best user interface is a consistent one.

This new API is tested in tests/test_kv.toml.

---

At the moment the implementation is naive, just sitting on top of the
file API. This works remarkably well thanks to littlefs's cache
bypassing logic, but does have some downsides:

- lfs3_set always writes two commits: one for the stickynote and one for
  the file sync.

  Unfortunately this is a fundamental limitation of littlefs's file API.
  One nice benefit of lfs3_set is in theory we can bypass this
  limitation, but not if we just sit on top of the file API.

- There may be code savings from more tightly integrating the key-value
  code.

This also highlighted an awkward corner case with per-file cache
configuration in which the buffer needs to be non-null even if zero. Not
the end of the world, but just a bit awkward. Maybe this deserves
revisiting in the config API rework?

---

Code changes were relatively minimal given that this is a whole new API,
unfortunately the stack took quite a hit:

           code          stack          ctx
  before: 37352           2280          636
  after:  37644 (+0.8%)   2448 (+7.4%)  636 (+0.0%)

The stack surprised me, but in hindsight it makes sense. In sitting on
top of the reset of the codebase, the key-value API adds very little
code, but every stack allocation in these functions add to the stack
hot-path.

This isn't the end of the world, and it's actually probably a good thing
to have an lfs3_file_t allocated in the stack hot-path. lfs3_file_t's
size has been a bit difficult to track thanks to struct lfs3_info
dominating ctx measurements...
2025-06-22 15:22:21 -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 f7e17c8aad Added LFS_T_RDONLY, LFS_T_RDWR, etc
These mimic the relevant LFS_O_* flags, and allow users to assert
whether or not a traversal will mutate the filesystem:

  LFS_T_MODE          0x00000001  The traversal's access mode
  LFS_T_RDWR          0x00000000  Open traversal as read and write
  LFS_T_RDONLY        0x00000001  Open traversal as read only

In theory, these could also change internal allocations, but littlefs
doesn't really work that way.

Note we _don't_ add related LFS_GC_RDONLY, LFS_GC_RDWR, etc flags. These
are sort of implied by the relevant LFS_M_* flags.

Adds a bit more code, probably because of the slightly more complicated
internal constants for the internal traversals. But I think the
self-documentingness is worth it:

           code          stack          ctx
  before: 37200           2288          636
  after:  37220 (+0.1%)   2288 (+0.0%)  636 (+0.0%)
2025-05-24 23:27:10 -05:00
Christopher Haster f5dd6f69e8 Renamed LFS_CKMETAPARITY and LFS_CKDATACKSUMREADS
- LFS_CKPARITY -> LFS_CKMETAPARITY
- LFS_CKDATACKSUMS -> LFS_CKDATACKSUMREADS

The goal here is to provide hints for 1. what is being checked (META,
DATA, etc), and 2. on what operation (FETCHES, PROGS, READS, etc).

Note that LFS_CKDATACKSUMREADS is intended to eventually be a part of a
set of flags that can pull off closed fully-checked reads:

- LFS_CKMETAREDUNDREADS - Check data checksums on reads
- LFS_CKDATACKSUMREADS - Check metadata redund blocks on reads
- LFS_CKREADS - LFS_CKMETAREDUNDREADS + LFS_CKDATACKSUMREADS

Also it's probably not a bad idea for LFS_CKMETAPARITY to be harder to
use. It's really not worth enabling unless you understand its
limitations (<1 bit of error detection, yay).

No code changes.
2025-05-24 21:55:45 -05:00
Christopher Haster a1c90d2624 Reverted attempted per-btree leaf caches
See the relevant commit for why. These just added surprisingly little
performance benefit for the code/stack cost.

Maybe in a future performance-preferring littlefs driver.
2025-05-24 18:49:38 -05:00
Christopher Haster a49e13b992 Attempted to implement per-btree leaf caches
The idea here, is we give each lfsr_btree_t an optional leaf rbyd, in
addition to the root rbyd. This leaf rbyd acts as a cache for the most
recent leaf, allowing nearby btree lookups to skip the full btree walk.

Unfortunately, this failed on pretty much every measurable metric...

---

The motivation for this is that we often do a bunch of nearby btree
lookups:

- Btree iteration via lfsr_btree_lookupnext is a bit naive, walking from
  the root every step.

- Our crystallization algorithm requires a bunch of nearby lookups to
  figure out our crystallization heuristic. Currently at most 4, when
  you need to lookup both crystal neighbors and then _also_ both
  fragment neighbors for coalescing.

- Checksum collision resolution for dids and (FUTURE) ddkeys can require
  an unbounded number of sequential lookups.

  Though to be fair, this is an exceptional case if our checksum is any
  good.

- Bids with multiple rattrs require nearby lookups to resolve.

  Though currently this can be explicitly avoided via
  lfsr_btree_lookupleaf + lfsr_rbyd_lookup.

The theory was that cases like these could explicitly keep track of the
leaf rbyd to avoid full btree walks, but in practice this never really
worked out. Tracking if we're still in the relevant leaf rbyd just adds
too much logic/code cost.

But if this leaf tracking logic was implemented once in the btree
layer...

The other theoretical benefit was being able to move more rbyds off the
stack. Sure our btrees take up more RAM, but if that results in stack
savings, that may be a win.

Oh, and this would let our btree API and rbyd API converge without
performance concerns. Internal users could in theory call
lfsr_btree_lookupnext + lfsr_btree_lookup with the same performance as
explicitly tracking the rbyd.

---

But this was a complete failure!

First the good news: There was a modest speedup of around ~2x to linear
reads.

And that's the good news.

Now the bad news:

1. There was no noticeable performance gain in any other benchmarks.

   To be fair, we're at the early stages of benchmarking, so the
   benchmarks may not be the most thorough, but thinking about it, there
   are some explanations:

   - In any benchmark that writes, fetch + erase + prog dominates. Being
     able to skip fetches during lookups makes our btree lookups
     surprisingly cheap!

   - Any random read heavy benchmark is likely thrashing this cache,
     which is to be expected.

   - For small 1-block btrees, the leaf cache is useless because the
     entire btree is cache in the root rbyd.

     And keep in mind, our blocks are BIG. "Small" here could be on
     the order of ~128KiB-1MiB for NAND flash.

   - For the mtree, fetched mdirs actually already act as a sort of leaf
     cache.

     The extra btree leaf cache isn't doing _nothing_, but each layer of
     the mtree has diminishing returns due to btree's ridiculous
     branching factor.

   - For file btrees, we're explicitly caching the leaf fragments/
     blocks, so the extra btree leaf cache has diminishing returns for
     the same reason.

2. Code cost was bad, stack cost was worse:

              code          stack          ctx
     before: 37172           2288          636
     after:  38068 (+2.4%)   2416 (+5.6%)  664 (+4.4%)

   Tracking the leaf required more code, that's expected. And, to be
   fair, the current code has had a lot more time to congeal.

   What wasn't expected was the stack cost.

   Unfortunately these caches didn't really take any rbyds off the stack
   hot-path:

   - We _can_ get rid of the rbyd in lfsr_btree_lookup/namelookup, but
     we were already hacking our way around the critical one in
     lfsr_mtree_lookup/namelookup by reusing the mdir's rbyd!

   - We can't even abuse the leaf rbyd in the commit logic, since the
     target btree can end up iterated/traversed by lfs_alloc.

     That was a fun bug.

   And the addition of a second rbyd to lfsr_btree_t increases both ctx
   and stack anywhere btrees are allocated.

Maybe this will make more sense when we add the auxiliary btrees, or
after more benchmarking, but for now the theoretical performance
improvements just aren't worth it.

Will probably revert this, but I wanted to commit it in case the idea is
worth resurrecting in the future, if in the future nearby btree lookups
are a bigger penalty than they are now.
2025-05-24 18:37:37 -05:00
Christopher Haster b613b65921 Fixed a nasty overrecycling + shrub + ckprog bug
In lfsr_mdir_compact__, we rely on shrub_.block != mdir.block to avoid
compacting shrubs multiple times. This works for the most part because
we set shrub_.block = shrub.block (the old mdir block) at the beginning
of lfsr_mdir_commit. We don't actually reset shrub_.block on a bad prog,
but in theory that was ok because we never try to compact into the same
block twice.

But this falls apart if we overrecycle the mdir!

With overrecycling, if we encounter a bad prog during a compaction and
there are no more blocks to relocate to, we try one last time to compact
into the same block (this logic is mainly for recycle overflows, where
it makes a bit more sense).

Of course, compacting into the same block breaks the above shrub_.block
!= mdir.block invariant, which causes the shrub compaction to be
skipped, uses the old shrub_.trunk (which now points to garbage), and
breaks everything.

Fortunately the solution is relatively simple: Just discard any staged
shrubs that have been committed when we relocate/overrecycle.

---

While fixing this I went ahead and renamed overcompaction ->
overrecycling. To me, overcompaction implies something _very_ different,
and I think this better describes the relationship between overrecycling
and block_recycles.

Also added test_ck_ckprogs_overrecycling to nail this down and prevent a
regression in the future. This bug _was_ caught by
test_ck_spam_fwrite_fuzz, but only after unrelated fs changes.

Adds a bit of code, but a smaller + dysfunctional filesystem is not very
useful:

           code          stack          ctx
  before: 37056           2304 (+0.0%)  636 (+0.0%)
  after:  37088 (+0.1%)   2304 (+0.0%)  636 (+0.0%)
2025-05-23 13:26:16 -05:00
Christopher Haster 9ed326f3d3 Adopted file->leaf, reworked how we track crystallization
TLDR: Added file->leaf, which can track file fragments (read only) and
blocks independently from file->b.shrub. This speeds up linear
read/write performance at a heavy code/stack cost.

The jury is still out on if this ends up reverted.

---

This is another change motivated by benchmarking, specifically the
significant regression in linear reads.

The problem is that CTZ skip-lists are actually _really_ good at
appending blocks! (but only appending blocks) The entire state of the
file is contained in the last block, so file writes can resume without
any reads. With B-trees, we need at least 1 B-tree lookup to resume
appending, and this really adds up when writing extremely blocks.

To try to mitigate this, I added file->leaf, a single in-RAM bptr for
tracking the most recent leaf we've operated on. This avoids B-tree
lookups during linear reads, and allowing the leaf to fall out-of-sync
with the B-tree avoids both B-tree lookups and commits during writes.

Unfortunately this isn't a complete win for writes. If we write
fragments, i.e. cache_size < prog_size, we still need to incrementally
commit to the B-tree. Fragments are a bit annoying for caching as any
B-tree commit can discard the block they reside on.

For reading, however, this brings read performance back to roughly the
same as CTZ skip-lists.

---

This also turned into more-or-less a full rewrite of the lfsr_file_flush
-> lfsr_file_crystallize code path, which is probably a good thing. This
code needed some TLC.

file->leaf also replaces the previous eblock/eoff mechanism for
erased-state tracking via the new LFSR_BPTR_ISERASED flag. This should
be useful when exploring more erased-state tracking mechanisms (ddtree).

Unfortunately, all of this additional in-RAM state is very costly. I
think there's some cleanup that can be done (the current impl is a bit
of a mess/proof-of-concept), but this does add a significant chunk of
both code and stack:

           code          stack          ctx
  before: 36016           2296          636
  after:  37228 (+3.4%)   2328 (+1.4%)  636 (+0.0%)

file->leaf also increases the size of lfsr_file_t, but this doesn't show
up in ctx because struct lfs_info dominates:

  lfsr_file_t before: 116
  lfsr_file_t after:  136 (+17.2%)

Hm... Maybe ctx measurements should use a lower LFS_NAME_MAX?
2025-05-23 12:15:13 -05:00
Christopher Haster a3710d1d96 tests: Consistently align LOOKAHEAD_SIZE in tests 2025-05-15 13:44:07 -05:00
Christopher Haster 9f2f0b92e9 Renamed lfsr_fs_size -> lfsr_fs_usage
This better matches how other filesystems refer to the number of in-use
blocks.

Which makes sense when you consider that "size" could also refer to the
configured block_count. The term "usage" avoids this ambiguity.
2025-05-01 00:37:07 -05:00
Christopher Haster de7564e448 Added phase bits to cksum tags
This carves out two more bits in cksum tags to store the "phase" of the
rbyd block (maybe the name is too fancy, this is just the lowest 2 bits
of the block address):

  LFSR_TAG_CKSUM        0x300p  v-11 ---- ---- -pqq
                                                ^ ^
                                                | '-- phase bits
                                                '---- perturb bit

The intention here is to catch mrootanchors that are "out-of-phase",
i.e. they've been shifted by a small number of blocks.

This can happen if we find the wrong mrootanchor (after, say, a magic
scan), and risks filesystem corruption:

                formatted
  .-----------------'-----------------.
                          mounted
           .-----------------'-----------------.
  .--------+--------+--------+--------+ ...
  |(erased)| mroot  |
  |        | anchor |                   ...
  |        |        |
  '--------+--------+--------+--------+ ...

Including the lower 2 bits of the block address in cksum tags avoids
this, for up to a 3 block shift (the maximum number of redund
mrootanchors).

---

Note that cksum tags really are the only place we could put these bits.
Anywhere else and they would interfere with the canonical cksum, which
would break error correction. By definition these need to be different
per block.

We include these phase bits in every cksum tag (because it's easier),
but these don't really say much about mdirs that are not the
mrootanchor. Non-anchor mdirs can have arbitrary block addresses,
therefore arbitrary phase bits.

You _might_ be able to do something interesting if you sort the rbyd
addresses and use the index as the phase bits, but that would add quite
a bit of code for questionable benefit...

You could argue this adds noise to our cksums, but:

1. 2 bits seems like a really small amount of noise
2. our cksums are just crc32cs
3. the phase bits humorously never change when you rewrite a block

---

As with any feature this adds code, but only a small amount. I think
it's worth the extra protection:

           code          stack          ctx
  before: 35792           2368          636
  after:  35824 (+0.1%)   2368 (+0.0%)  636 (+0.0%)

Also added test_mount_incompat_out_of_phase to test this.

The dbg scripts _don't_ error (block mismatch seems likely when
debugging), but dbgrbyd.py at least adds phase mismatch notes in
-l/--log mode.
2025-04-30 00:57:17 -05:00
Christopher Haster f2e6b60f36 Reworked grm encoding a bit
This drops the leading count/mode byte, and instead uses mid=0 to
terminate grms. This shaves off 1 bytes from grmdeltas.

Previously, we needed the count/mode byte for a couple reasons:

- We needed to know the number of grm entries somehow, and there wasn't
  always an obvious sentinel value. mid=-1, for example, is
  unrepresentable with our unsigned leb128 encoding.

  But now that development has settled, we can use mid=0.0 to figure out
  the end-of-queue. mid=0.0 should always map to the root bookmark,
  which doesn't make sense to delete, so it makes for a reasonable null
  terminator here.

- It provided a route for future grm extensions, which could use the >2
  count/mode encodings.

  But I think we can use additional grm tag encodings for this.

  There's only one gdelta tag so far, but the current plan for future
  gdelta tags is to carve out the bottom 2 bits for redund like we do
  with the struct tags:

    LFSR_TAG_GDELTA        0x01tt  v--- ---1 -ttt ttrr
    LFSR_TAG_GRMDELTA      0x0100  v--- ---1 ---- ----
    LFSR_TAG_GBMAPDELTA    0x0104  v--- ---1 ---- -1rr
    LFSR_TAG_GDDTREEDELTA  0x0108  v--- ---1 ---- 1-rr
    LFSR_TAG_GPTREEDELTA   0x010c  v--- ---1 ---- 11rr
    ...

  Decoding is a bit more complicated for gstate, since we will need to
  xor those bits if mutable, but this avoids needing a full byte just
  for redund in every auxiliary tree.

  Long story short, we can leverage the lower 2 bits of the grm tag for
  future extensions using the same mechanism.

This may seem like a lot of effort for only a handful of bytes, but keep
in mind each gdelta lives in more-or-less every mdir in the filesystem.

Also saves a bit of code/ctx:

           code          stack          ctx
  before: 35772           2368          640
  after:  35768 (-0.0%)   2368 (+0.0%)  636 (-0.6%)
2025-04-30 00:53:33 -05:00
Christopher Haster 6c8fa28ae4 Reverted lfsr_mtree_*lookupleaf -> lfsr_mtree_lookup
Why?

- lfsr_mtree_lookupleaf vs lfsr_mtree_commit is inconsistent. Should
  lfsr_mdir_commit be called lfsr_mtree_commitleaf? That'd be weird.

  It's reasonable to call mdirs entries of the mtree, but it'd be weird
  to call rbyds entries of btrees, so the inconsistency there is
  expected.

- lfsr_mtree_lookup/lfsr_mtree_lookupnext (going mtree -> mdir) aren't
  actually useful.

- The lfsr_mtree_namelookup/lfsr_mtree_namelookupleaf split is just more
  of a headache than it's worth.

Saves a tiny bit of code:

           code          stack          ctx
  before: 35768           2392          640
  after:  35764 (-0.0%)   2392 (+0.0%)  640 (+0.0%)
2025-04-30 00:40:53 -05:00
Christopher Haster 6cde75d671 Require rbyd_/mdir_ out-pointers to be non-null
This makes all rbyd_/mdir_ out-pointers required, dropping all of the
internal copies needed to make lookup/namelookup/pathlookup/etc work.

Previously, the -- rough -- rule was to make out-pointers generally
optional (lfsr_data_read and other struct initers being notable
exceptions), the idea being you can opt-out of stack allocations where
possible.

In practice this kind of backfired, with many internal functions needing
redundant stack allocations in case the relevant parameter is NULL
(lfsr_btree_lookupleaf being an excellent example).

---

As an alternative rule, I think we should only expect optional
out-pointers for things you would pass-by-value (lfsr_rid_t, lfsr_tag_t,
lfsr_data_t, etc).

I've also developed a habit of naming optional out-pointers with a
trailing underscore_, to hopefully make this subtlety a bit less subtle.

This claws back all of the stack cost of BNAMEs/MNAMEs, and most of the
code cost:

           code          stack          ctx
  before: 35888           2480          640
  after:  35780 (-0.3%)   2408 (-2.9%)  640 (+0.0%)

Though we still have more function calls than we started with
(lfsr_mtree_*lookup mtree -> mdir lookups).
2025-04-30 00:33:24 -05:00
Christopher Haster 27dd339a6a Added big vestigial-name-split comment
This is the _nth_ time I've tried to force arbitrary btree name inserts
to work, so _clearly_ I need a bigger comment.

Hopefully this will prevent me from trying to delete the LFSR_RATTR_NOOP
in test_btree_find_general_fuzz _again_.

---

The gist is that insert-before-bid+1 is fundamentally different from
insert-after-bid when named btrees are involved:

    .-----f-----.    insert-after-d     .-------f-----.
  .-b--.     .--j-.        =>         .-b---.      .--j-.
  |   .-.   .-.   |                   |   .---.   .-.   |
  a   c d   h i   k                   a   c d e   h i   k
                                              ^
                     insert-before-h
                           =>           .-----f-------.
                                      .-b--.      .---j-.
                                      |   .-.   .---.   |
                                      a   c d   g h i   k
                                                ^

The problem is that lfsr_btree_commit_ needs to find the same leaf
rbyd as lfsr_btree_namelookup, and potentially insert-before the
first rid or insert-after the last rid.

Instead of separate insert-before/after flags, we make the first tag
in a commit insert-before, and all following non-grow tags
insert-after (splits).

This info is now captured in the above mentioned comment.
2025-04-30 00:28:40 -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 879a55add9 Another ckparity flaw
Found another ckparity flaw! Only detected now due to the reworked tag
encoding, but it's just luck this wasn't detected earlier.

Consider the following bit flip:

  03 04 80 04 80 04 6b ...  data w512 512
  43 04 80 04 80 04 6b ...  altble 0x304 w512 -512
  ^
  flip

Not only are leb128s problem for ckparity, but even the difference in
alt vs normal tag encoding presents a vulnerability.

So limiting the ckparity tests further, to just 47 of the first 48 bits.
2025-04-29 16:25:45 -05:00
Christopher Haster d308ec8322 Reworked tag encoding a little bit
Mainly to make room for some future planned stuff:

- Moved the mroot's redund bits from LFSR_TAG_GEOMETRY to
  LFSR_TAG_MAGIC:

    LFSR_TAG_MAGIC        0x003r  v--- ---- --11 --rr

  This has the benefit of living in a fixed location (off=0x5), which
  may make mounting/debugging easier. It also makes LFSR_TAG_GEOMETRY
  less of a special case (LFSR_TAG_MAGIC is already a _very_ special
  case).

  Unfortunately, this does get in the way of our previous magic=0x3
  encoding. To compensate (and to avoid conflicts with LFSR_TAG_NULL),
  I've added the 0x3_ prefix. This has the funny side-effect of
  rendering redunds 0-3 as ascii 0-3 (0x30-0x33), which is a complete
  accident but may actually be useful when debugging.

  Currently all config tags fit in the 0x3_ prefix, which is nice for
  debugging but not a hard requirement.

- Flipped LFSR_TAG_FILELIMIT/NAMELIMIT:

    LFSR_TAG_FILELIMIT    0x0039  v--- ---- --11 1--1
    LFSR_TAG_NAMELIMIT    0x003a  v--- ---- --11 1-1-

  The file limit is a _bit_ more fundamental. It's effectively the
  required integer size for the filesystem.

  These may also be followed by LFSR_TAG_ATTRLIMIT based on how future
  attr revisits go.

- Rearranged struct tags so that LFSR_TAG_BRANCH = 0x300:

    LFSR_TAG_BRANCH       0x030r  v--- --11 ---- --rr
    LFSR_TAG_DATA         0x0304  v--- --11 ---- -1--
    LFSR_TAG_BLOCK        0x0308  v--- --11 ---- 1err
    LFSR_TAG_DDKEY*       0x0310  v--- --11 ---1 ----
    LFSR_TAG_DID          0x0314  v--- --11 ---1 -1--
    LFSR_TAG_BSHRUB       0x0318  v--- --11 ---1 1---
    LFSR_TAG_BTREE        0x031c  v--- --11 ---1 11rr
    LFSR_TAG_MROOT        0x032r  v--- --11 --1- --rr
    LFSR_TAG_MDIR         0x0324  v--- --11 --1- -1rr
    LFSR_TAG_MTREE        0x032c  v--- --11 --1- 11rr

    *Planned

  LFSR_TAG_BRANCH is a very special tag when it comes to bshrub/btree
  traversal, so I think it deserves the subtype=0 slot.

  This also just makes everything fit together better, and makes room
  for the future planned ddkey tag.

Code changes minimal:

           code          stack          ctx
  before: 35728           2440          640
  after:  35732 (+0.0%)   2440 (+0.0%)  640 (+0.0%)
2025-04-29 16:25:00 -05:00
Christopher Haster 85778b2813 Ripped out most of LFS_O_SYNC, restrict to writes
This tears out most of the implied lfsr_file_sync calls, and restricts
LFS_O_SYNC to only imply lfsr_file_sync on _write_ operations. So only
lfsr_file_write, and maybe pwrite/writev/etc in the future.

This mainly affects lfsr_file_truncate/fruncate (and punchhole/
insertrange/collapserange in the future), while reverting the LFS_O_SYNC
related changes in lfsr_file_open:

- lfsr_file_open     + LFS_O_SYNC => does _not_ sync
- lfsr_file_close    + LFS_O_SYNC => syncs (unless desynced)
- lfsr_file_write    + LFS_O_SYNC => syncs
- lfsr_file_sync     + LFS_O_SYNC => syncs
- lfsr_file_truncate + LFS_O_SYNC => does _not_ sync
- lfsr_file_fruncate + LFS_O_SYNC => does _not_ sync

Note LFS_O_FLUSH is unaffected, it was always limited to
lfsr_file_write since that's the only function that touches file
buffers.

Also note I want this rule to apply to the future lfsr_file_punchhole/
insertrange/collapserange functions as well. Even though you can argue
these effectuate writes, they're at a level of sophistication that we
can just expect users to just call lfsr_file_sync if they want to.

---

Ok, so a number of reasons:

- This matches behavior of LFS_O_APPEND, which is intentionally
  restricted to only write operations.

  In that case I think the explicit limitation is easier to understand
  than trying to define an abstract model.

  This makes LFS_O_SYNC, LFS_O_FLUSH, and LFS_O_APPEND consistent in
  when the relevant behavior takes effect.

- This avoids the zero-sized files after powerloss. Which are just as
  likely, if not more, to trip up users vs missing syncs.

- Most truncate/fruncate operations are immediately followed by a write
  operation anyways. Which just makes the truncate/fruncate syncs wasted
  prog/erase cycles.

  Even in some of the more complicated truncate/function use cases, you
  just don't care about when fruncates/truncates hit the disk.

  Take logging via lfsr_file_fruncate for example. Yes the fruncate will
  usually happen _after_ the write operation, but this just means the
  log file will usually be one entry larger than expected. Which is a
  state you can end up with anyways after powerloss.

- This avoids confusing/conflicting LFS_O_SYNC + LFS_O_DESYNC behavior.

  Again, this simple rule is easier to reason about than a model.

You would think this would be well defined in POSIX, but it's really
not. POSIX limits O_SYNC to "write I/O operations", but doesn't really
define a "write" (it is a retroactive standard after all). ftruncate is
a bit funny in that it states "the extended area shall appear as if it
were zero-filled", but the term "write" doesn't appear in ftruncate's
documentation at all.

Searching through LKML, stack overflow, etc, it doesn't seem like anyone
else knows exactly what to do either. There was a bug report[1] in 2005
for ext3 + O_SYNC + ftruncate that was rejected, but a later bug
report[2] in 2012 for xfs + O_SYNC + fallocate that was fixed (but was
broken in almost every Linux fs?).

1: https://lore.kernel.org/lkml/1111610558.1998.193.camel@sisko.sctweedie.blueyonder.co.uk
2: https://lore.kernel.org/linux-ext4/20111116084256.GA22963@infradead.org

So, this may end up a bit controversial, but I'm going to go with the
simpler truncate/fruncate-do-not-imply-sync rule for the above reasons.

I think this is a bit more important for littlefs than other
filesystems, as it also defines the behavior of lfsr_file_open, and with
a rigorous powerloss model being core to the design.

---

This is also cheaper code/stack-wise, but if this was going to be a
deciding factor we should just put LFS_O_SYNC/LFS_O_FLUSH behind ifdefs:

                  code          stack          ctx
  before:        35816           2480          640
  after:         35740 (-0.2%)   2424 (-2.3%)  640 (+0.0%)

Compared to before the LFS_O_SYNC tweaks:

                  code          stack          ctx
  before-tweaks: 35780           2440          640
  before:        35816 (+0.1%)   2480 (+1.6%)  640 (+0.0%)
  after:         35740 (-0.1%)   2424 (-0.7%)  640 (+0.0%)
2025-04-26 18:01:16 -05:00
Christopher Haster 78f9dac162 Just assert on LFS_O_SYNC + lfsr_file_desync
This is the only way I can think of resolving the weirdness that is
LFS_O_SYNC + LFS_O_DESYNC. Just don't allow it.

LFS_O_SYNC and LFS_O_DESYNC are pretty much opposite behaviors, so an
LFS_O_SYNC + LFS_O_DESYNC file seems like a contradiction.

---

This does limit a little bit what's possible with the API, but hey that
just means fewer tests/smaller API surface area for users to stub their
toes on.

Saves a tiny bit of code:

           code          stack          ctx
  before: 35824           2480          640
  after:  35816 (-0.0%)   2480 (+0.0%)  640 (+0.0%)
2025-04-26 16:48:39 -05:00
Christopher Haster 7b81f01db4 Tweaked lfsr_file_open to only sync when unsynced
Do'h! I almost forgot about LFS_O_TRUNC. If LFS_O_CREAT + LFS_O_SYNC
implies lfsr_file_sync, clearly LFS_O_TRUNC + LFS_O_SYNC should as well.

This changes lfsr_file_open to only imply lfsr_file_sync if any open
operation sets the unsync flag, which is the only case where
lfsr_file_sync would do anything anyways.

This does have a subtle change in behavior when LFS_O_CREAT + LFS_O_SYNC
+ LFS_O_DESYNC, in that the desync flag is only cleared if the file did
not exist before. But I think this is more expected than unconditionally
syncing.

Note this matches the behavior of lfsr_file_write, which does _not_
imply lfsr_file_sync if the write is size=0.

---

Also added better tests over lfsr_file_open + LFS_O_TRUNC, this flag
isn't very well tested...

Which found a bug!

We were incorrectly setting LFS_o_UNFLUSH when opening with LFS_O_TRUNC,
when we should have set LFS_o_UNSYNC. This caused littlefs to never
bother updating the file's metadata unless some other write comes along
(which is what usually follows LFS_O_TRUNC).

To help catch bugs like this, I added an assert to lfsr_file_flush that
unflushed files are always marked unsynced. A synced + unflushed file is
weird and should never happen.

---

Code changes minimal:

           code          stack          ctx
  before: 35820           2480          640
  after:  35824 (+0.0%)   2480 (+0.0%)  640 (+0.0%)
2025-04-26 15:27:25 -05:00
Christopher Haster 4b87499605 Tweaked lfsr_file_open to only sync when LFS_O_SYNC + LFS_O_CREAT
So we keep the behavior of creating reg files with lfsr_file_open +
LFS_O_SYNC, but only clear the desync flag if lfsr_file_open would
mutate the filesystem.

This is hopefully a simpler model to reason about, and makes LFS_O_SYNC
+ LFS_O_DESYNC a bit less weird.

Saves a little bit of code:

           code          stack          ctx
  before: 35836           2488          640
  after:  35820 (-0.0%)   2480 (-0.3%)  640 (+0.0%)
2025-04-26 15:09:12 -05:00
Christopher Haster 0f4ad6d842 Tweaked lfsr_file_open to sync when LFS_O_SYNC
So now the following creates a reg file (instead of just a stickynote):

  lfsr_file_open(&lfs, &file, "test.txt",
          LFS_O_WRONLY | LFS_O_CREAT | LFS_O_EXCL | LFS_O_SYNC) => 0;
  // powerloss!!

  struct lfsr_info info;
  lfsr_stat(&lfs, "test.txt",  &info) => 0; // LFS_ERR_NOENT before
  assert(info.type == LFS_TYPE_REG);

This hopefully results in more intuitive behavior around lfsr_file_open
with LFS_O_SYNC. Which is important as LFS_O_SYNC is often used as an
escape hatch to avoid needing to reason about syncing things when
performance is not a big concern.

Unfortunately this does come with a surprisingly big code/stack cost,
but I'm thinking of putting these flags (LFS_O_FLUSH/LFS_O_SYNC) behind
ifdefs anyways (LFS_MAYBE_SYNC?):

           code          stack          ctx
  before: 35780           2440          640
  after:  35836 (+0.2%)   2488 (+2.0%)  640 (+0.0%)

Also added some more tests to make sure these open+LFS_O_SYNC cases are
explicitly covered:

- test_fsync_sync_o_wrr
- test_fsync_sync_o_wwrr
- test_fsync_desync_o_wdwrr
- test_fsync_resync_o_wdwyrr

This does make a bit of a mess when you combined LFS_O_SYNC +
LFS_O_DESYNC. What exactly should a SYNC + DESYNC file look like?

For now I've just made LFS_O_SYNC + LFS_O_DESYNC behave as if you opened
a file with LFS_O_SYNC and then immediately called lfsr_file_desync on
it. So it doesn't receive broadcasts, but _does_ create the reg file,
and _does_ sync on first write, clearing the desync flag.

But this may be worth revisiting. Maybe LFS_O_DESYNC files shouldn't
have their desync flags cleared unless lfsr_file_sync is explicitly
called? Or maybe LFS_O_SYNC + LFS_O_DESYNC should just be an error?
Unsure...
2025-04-26 14:45:19 -05:00
Christopher Haster b5e503ca85 Made lfsr_file_sync a noop if zombied
So now calling lfsr_file_sync on zombied files is a noop:

  // create a file
  lfsr_file_t a;
  lfsr_file_open(&lfs, &a, "a",
          LFS_O_RDWR | LFS_O_CREAT | LFS_O_EXCL) => 0;

  // remove, creating a zombie
  lfsr_remove(&lfs, "a") => 0;

  // sync, this is now a noop (previously LFS_ERR_NOENT)
  lfsr_file_sync(&lfs, &a) => 0;

  // close is also a noop
  lfsr_file_close(&lfs, &a) => 0;

I've been on the fence on this for a while, on one hand erroring
provides more information to the user, on the other hand a noop is less
surprising if the user comes from other systems.

Ended up making this a noop. I figured minimizing surprises is good API
design, and the user can always use lfsr_stat to check if the file still
exists.

This also matches POSIX, and, perhaps more importantly, the current
version of littlefs.

---

Note that lfsr_file_resync still errors with LFS_ERR_NOENT. It's hard to
argue the file "matches the state of disk" otherwise.

Code changes minimal:

           code          stack          ctx
  before: 35784           2440          640
  after:  35780 (-0.0%)   2440 (+0.0%)  640 (+0.0%)
2025-04-25 17:29:34 -05:00
Christopher Haster f29e4b9a6e Added test_fsync_*_zero tests
I thought we had a bug here, but we do not. Still, more tests isn't a
bad thing.
2025-04-25 16:21:11 -05:00