Commit Graph

427 Commits

Author SHA1 Message Date
Christopher Haster a4c74967ec Renamed LFS_I_* flags to match LFS_GC_*
- LFS_I_INCONSISTENT -> LFS_I_MKCONSISTENT
- LFS_I_CANLOOKAHEAD -> LFS_I_LOOKAHEAD
- LFS_I_UNCOMPACTED  -> LFS_I_COMPACT
- LFS_I_CANCKMETA    -> LFS_I_CKMETA
- LFS_I_CANCKDATA    -> LFS_I_CKDATA

This just makes everything easier to read/pattern match, even if it's
a bit inaccurate english-wise. The imperative transformations were also
wildly inconsistent...
2025-01-28 14:41:45 -06:00
Christopher Haster 9c9a23e27b gc: Renamed lfsr_gc -> lfsr_fs_gc, keep lfsr_fs_unck in non-gc
- lfsr_gc -> lfsr_fs_gc
- lfsr_gc_unck -> lfsr_fs_unck

lfsr_fs_unck is surprisingly still useful in non-gc builds, since we
still have ckmeta/ckdata state. These flags can still be queried with
lfsr_fs_stat and cleared with lfsr_fs_ckmeta/ckdata/lfsr_traversal_t, so
it seems useful to keep this function around.

It's also a relatively cheap function.

Though this does mean it deserves a rename. Dropping the gc prefix
hopefully makes it clearer this function is not entirely gc-specific.

And since we no longer have lfsr_gc_setflags/setsteps, it makes sense to
rename lfsr_gc back to lfsr_fs_gc, to be consistent with the other
filesystem-wide utilities.

Code changes, apparently lfsr_fs_unck costs 12 bytes:

                   code          stack          ctx
  default before: 37792           2608          620
  default after:  37804 (+0.0%)   2608 (+0.0%)  620 (+0.0%)

  gc before:      37938           2608          768
  gc after:       37940 (+0.0%)   2608 (+0.0%)  768 (+0.0%)
2025-01-28 14:41:45 -06:00
Christopher Haster 39d488a1ef gc: Made CKMETA/CKDATA progressable, added lfsr_gc_unck
LFS_GC_CKMETA and LFS_GC_CKDATA are a bit unique in that their work is
never really done.

Where LFS_GC_MKCONSISTENT/COMPACT can prove things about the system,
LFS_GC_CKMETA/CKDATA can't, because it's always possible for new
bit-errors to develop. Even _during_ an LFS_GC_CKMETA/CKDATA traversal.

But while this is technically true, it's not a very useful state of
things for our lfsr_gc API...

---

What we really want is some way to know if ckmeta/ckdata has completed
"recently" (for some definition of recently), and to let users indicate
when they need another ckmeta/ckdata scan.

To try to solve this:

1. Added LFS_I_CANCKMETA and LFS_I_CANCKDATA to indicate when lfsr_gc
   has not checked metadata/data.

   These are set during mount (unless mounting with
   LFS_M_CKMETA/CKDATA), and cleared when either lfsr_gc completes or
   lfsr_fs_ckmeta/data is called. Once cleared, littlefs will not reset
   them on its own.

2. Added lfsr_gc_unck to allow users to explicitly reset LFS_I_CKMETA
   and/or LFS_I_CKDATA, which will tell lfsr_gc to check metadata/data
   again on the next call.

   There is some subtlety around clobbering ongoing traversals, but a
   mask and some tests should prevent this from being a problem.

   Currently, lfsr_gc_unck also allows clearing of other gc flags, but
   I'm not sure there's any real use-case for this...

Note that you can still get the previous behavior if you just call
lfsr_gc_unck after every lfsr_gc call.

This also changes info flag behavior slightly in default mode, with
LFS_I_CANCKMETA/CANCKDATA telling you if metadata/data has been checked
since mount. Which does seem useful? Maybe these flags deserve a better
name?

Code changes:

                   code          stack          ctx
  default before: 37796 (+0.0%)   2608 (+0.0%)  620 (+0.0%)
  default after:  37792 (+0.0%)   2608 (+0.0%)  620 (+0.0%)

  gc before:      37896           2608          768
  gc after:       37938 (+0.1%)   2608 (+0.0%)  768 (+0.0.%)
2025-01-28 14:41:45 -06:00
Christopher Haster 0617244aa3 gc: Dropped lfsr_gc_setflags/setsteps
Now that you can provide gc_flags/gc_steps in lfs_config, I think it's a
bit more clear that _mutating_ the flags/steps is a niche feature, and
not worth implementing/testing.

It raises the question why not have a similar lfsr_setflags or
lfsr_file_setflags, and the answer there is it would be a pain-in-the-
ass to make sure all possible corner cases are covered.

It actually already was a pain-in-the-ass to test lfsr_gcsetflags/
setsteps... but just because we already did the work is not a good
reason for keeping complexity around.

---

Note that most of the use cases for lfsr_gc_setflags/setsteps can be
covered by either remounting the filesystem or through the
lfsr_traversal_t APIs directly.

The end result is a bit of code savings when incremental gc is enabled:

                   code          stack          ctx
  default before: 37796           2608          620
  default after:  37796 (+0.0%)   2608 (+0.0%)  620 (+0.0%)

  gc before:      37944           2608          768
  gc after        37896 (-0.1%)   2608 (+0.0%)  768 (+0.0%)
2025-01-28 14:41:45 -06:00
Christopher Haster 1b3054db89 gc: Moved incremental gc behind ifdef LFS_GC
Incremental gc, being stateful and not gc-able (ironic), was always
going to need to be conditionally compilable.

This moves incremental gc behind the LFS_GC define, so that we can focus
on the "default" costs. This cuts lfs_t in nearly half!

  lfs_t with LFS_GC:   308
  lfs_t without LFS_C: 168 (-45.5%)

This does save less code than one might expect though. We still need
most of the internal traversal/gc logic for things like block allocation
and orphan cleanup, so most of the savings is limited to the RAM storing
the incremental state:

                          code          stack          ctx
  before:                37916           2608          768
  after with LFS_CFG:    37944 (+0.1%)   2608 (+0.0%)  768 (+0.0%)
  after without LFS_CFG: 37796 (-0.3%)   2608 (+0.0%)  620 (-19.3%)

On the flip side, this does mean most of the incremental gc
functionality is still availables in the lfsr_traversal_t APIs.

Applications with more advanced gc use-cases may actually benefit from
_not_ enabling the incremental gc APIs, and instead use the
lfsr_traversal_t APIs directly.
2025-01-28 14:41:45 -06:00
Christopher Haster 5d756fe698 gc: Tweaked lfsr_gc API to be more stateful
Before:

  int lfsr_fs_gc(lfs_t *lfs, lfs_soff_t steps, uint32_t flags);

After:

  int lfsr_gc(lfs_t *lfs);
  int lfsr_gc_setflags(lfs_t *lfs, uint32_t flags);
  int lfsr_gc_setsteps(lfs_t *lfs, lfs_soff_t steps);

---

The interesting thing about the lfsr_gc API is that the caller will
often be very different from whoever configures the system. One example
being an OS calling lfsr_gc in a background loop, while leaving
configuration up to the user.

The idea here, is instead of forcing the OS to come up with its own
stateful system to pass flags to lfsr_gc, we just embed this state in
littlefs directly. The whole point of lfsr_gc is that it's a stateful
system anyways.

Unfortunately this state does require a bit more logic to maintain,
which adds code/ctx cost:

           code          stack          ctx
  before: 37812           2608          752
  after:  37916 (+0.3%)   2608 (+0.0%)  768 (+2.1%)
2025-01-28 14:41:45 -06:00
Christopher Haster 0839ac73d6 Reverted inlined lfsr_data_t representation
See previous commit for more details on why this doesn't work:

1. Losing the simple/compiler friendly lfsr_data_t costs more code/stack
   than we save inlined small pieces of data (dids, leb128s, flags,
   etc).

2. Inlined lfsr_data_t is fundamentally incompatible with the new
   lightweight lfsr_rat_t representation for simple data.

Though I did add a comment, and marked lfsr_data_fromslice as inline.

The compiler was apparently already inlining lfsr_data_fromslice (and
it's a valuable optimization!), but making this explicit helps document/
influence future changes.

Code changes:

           code          stack          ctx
  before: 38128           2672          752
  after:  38060 (-0.2%)   2608 (-2.4%)  752 (+0.0%)
2025-01-28 14:41:45 -06:00
Christopher Haster 0b9f46e7cb Attempted to re-add inlined lfsr_data_t representation
The idea, which has floated up a few times, is to add a third
representation of lfsr_data_t where the data is inlined in the struct
directly. In theory saving RAM for small pieces of data such as dids,
leb128s, flags, etc:

  inlined:             in-RAM buffer:       on-disk:
  .---+---+---+---.    .---+---+---+---.    .---+---+---+---.
  |01|   size     |    |00|   size     |    |1|    size     |
  +---+---+---+---+    +---+---+---+---+    +---+---+---+---+
  | inlined data  |    |      ptr -------.  |     block     |
  +               +    +---+---+---+---+ |  +---+---+---+---+
  |               |    |    (unused)   | |  |      off      |
  '---+---+---+---'    '---+---+---+---' |  '---+---+---+---'
                       .---+---+---+---. |
                       |     data      |<'
                       :       :       :

Unfortunately in practice this just doesn't work out.

It turns out we benefit a lot from the _simplicity_ of lfsr_data_t. When
lfsr_data_t is built out of simple words, the compiler can make some
pretty strong assumptions and basically break it down into simple
register operations.

When you stick a byte array in the middle of the struct, this sort of
breaks down.

---

We can see this in our code measurements. After adding inlined data, but
before implementing slicing (in lfsr_data_fromslice), we can see decent
stack savings. But as soon as we add the memmove to lfsr_data_fromslice,
any benefit is lost:

                        code          stack          ctx
  before:              38060           2608          752
  without slicing:     38056 (-0.0%)   2568 (-1.5%)  752 (+0.0%)
  after:               38128 (+0.2%)   2672 (+2.5%)  752 (+0.0%)

One reason for this is the extra logic does cause lfsr_data_fromslice to
be no longer inlined, but adding __attribute__((always_inline)) only
claws back some of the code/stack savings (though it's interesting to
note the compiler heuristic failure here):

                        code          stack          ctx
  before:              38060           2608          752
  after+inline:        38128 (+0.2%)   2672 (+2.5%)  752 (+0.0%)
  after+always_inline: 38684 (+1.6%)   2656 (+1.8%)  752 (+0.0%)

---

Oh, and inlined lfsr_data_t is no longer compatible with LFSR_RAT's
simple data conversion, since lfsr_rat_t's can only point to existing
buffers. This causes tests to fail rather quickly.

This should be reverted, but I think the hidden cost of inlined
lfsr_data_t is surprising and interesting to note.
2025-01-28 14:41:45 -06:00
Christopher Haster 6307cba8bb Folded lfsr_ck_t into lfsr_data_t
This reduces lfsr_ck_t to just the cksize/cksum fields, and moves all of
the compile-time ifdef LFS_CKDATACKSUMS logic up into the relevant
lfsr_data_* functions.

This doesn't solve the lfsr_data_t/lfsr_bptr_t duplication problem,
unfortunately, but does simplify the code base a bit.

No significant code changes:

                        code          stack          ctx
  default before:      38128           2624          752
  default after:       38128 (+0.0%)   2624 (+0.0%)  752 (+0.0%)

  ckdatacksums before: 39240           3008          752
  ckdatacksums after:  39232 (-0.0%)   3008 (+0.0%)  752 (+0.0%)
2025-01-28 14:41:45 -06:00
Christopher Haster 1d21355707 Renamed ckcksums -> ckdatacksums
To clarify this only checks data reads, and to makes space for future
theoretical ck-operations:

- ckmetaredund - likely
- ckdataredund - unlikely, expensive
- ckmetacksums - unlikely, expensive
- ckdatacksums - implemented

This also tweaks the relevant mount/format/info flags a bit:

  LFS_M_CKPROGS       0x00100000 Check progs by reading back progged data
  LFS_M_CKFETCHES     0x00200000 Check block checksums before first use
  LFS_M_CKPARITY      0x00400000 Check metadata tag parity bits
  LFS_M_CKMETAREDUND+ 0x01000000 Check metadata redund blocks on reads
  LFS_M_CKDATAREDUND* 0x02000000 Check data redund blocks on reads
  LFS_M_CKMETACKSUMS* 0x04000000 Check metadata checksums on reads
  LFS_M_CKDATACKSUMS  0x08000000 Check data checksums on reads

  +Planned
  *Hypothetical

No code changes.
2025-01-28 14:41:45 -06:00
Christopher Haster 377e744acd Renamed tailck -> tailp
Mainly just to emphasize that this no longer holds cksum information.
2025-01-28 14:41:45 -06:00
Christopher Haster 2e35def6e8 Reduced scope of ckparity to lfsr_bd_readtag_
Unfortunately ckparity has proven itself to be much less useful than
originally thought.

The use of leb128 encoding in our tags means that ckparity can't even
detect single bit-errors reliably. Which raises the question: is
ckparity really worth all of the extra baggage necessary to track parity
in our codebase?

Fortunately we don't have to toss out ckparity entirely!

If we only check parity bits in lfsr_bd_readtag_, instead of on every
read, we still have a reasonable chance of noticing parity errors during
metadata lookups.

This does weaken ckparity, but allows us to drop a lot of lfsr_data_t's
ckparity baggage, at the cost of no longer, uh, unreliably detecting
parity errors during reads?

The limited error detection of ckparity means we can't reliably detect
errors during reads anyways, so we might as well keep the code/RAM/
maintenance implications at a minimum to make ckparity remotely worth
it.

---

Note the significant savings for both LFS_CKPARITY and LFS_CKCKSUMS.
Tracking parity info in lfsr_data_t had a heavy cost:

                    code          stack           ctx
  default before:  38128           2624           752
  default after:   38128 (+0.0%)   2624 (+0.0%)   752 (+0.0%)

  ckparity before: 39700           3048           760
  ckparity after:  38476 (-3.1%)   2696 (-11.5%)  760 (+0.0%)

  ckcksums before: 39396           3096           760
  ckcksums after:  39240 (-0.4%)   3008 (-2.8%)   752 (-1.1%)

This also means lfsr_ck_ckprefix/cksuffix calls always have a ckoff of 0
(bptrs only), which means even more code savings, yay!
2025-01-28 14:41:45 -06:00
Christopher Haster 7edb3b231f Limited ckcksums to check data cksums
So... Long store short, checking metadata cksums is just intractably
slow.

But data cksums?

Yes checking data cksums is still O(b^2), but unlike metadata lookups,
which involve many small backwards reads, data reads are very easy to
cache. So instead of O(b^2), it's more like O(b^2/c), where c is your
rcache size.

Still O(b^2) when c << b, but I'm not sure that's avoidable without
adding more cksums.

At the very least, if you have enough RAM, c == b reduces this to O(b),
which is nice for "large" systems that want hardened reads without a
performance loss.

---

But why bother checking data cksums if we still have a read-hole with
metadata cksums?

Well, while considering the problem in the context of future features, I
noticed something _really interesting_:

- ckredund + metadata - reasonable ✓
- ckredund + data     - impractical ✗, parity fanout + O(f+r) is bad
- ckcksums + metadata - impractical ✗, small reads + O(b^2) is bad
- ckcksums + data     - reasonable ✓, assuming enough rcache

The current planned design for data redundancy makes it also intractably
slow to check every read, since it would require xoring all blocks that
contribute to the relevant parity block, but this isn't a problem for
metadata redundancy.

So while neither ckredund nor ckcksums can tractably close the read-hole
on their own, it looks like together they will be able to cover
everything without completely sacrificing performance. Neat!

Of course this isn't possible if ckcksums/ckredund imply checking both
metadata and data, so they need to be split apart.

And I don't really see a point in keeping the intractable variants
around in the codebase.

---

Dropping metadata ckcksums also means we can get rid of the ugly
lfsr_bd_ckrbydprefix and lfsr_bd_ckrbydsuffix functions, which were
basically duplicating all of lfsr_rbyd_fetch. That was quite a wart!

This saves a nice chunk of code when ckcksums is enabled:

                    code          stack          ctx
  default before:  38128           2624          752
  default after:   38128 (+0.0%)   2624 (+0.0%)  752 (+0.0%)

  ckparity before: 39724           3048          764
  ckparity after:  39700 (-0.1%)   3048 (+0.0%)  760 (-0.5%)

  ckcksums before: 40612           3184          772
  ckcksums after:  39396 (-3.0%)   3096 (-2.8%)  760 (-1.6%)
2025-01-28 14:41:45 -06:00
Christopher Haster 6e63920338 Dropped the HASORPHAN scan in lfsr_mount
The motivation here is to simplify lfsr_mount, but there's a number of
knock-on effects.

For one, lfsr_mount should now be faster on filesystems with large
blocks:

  O(nb(log b)(log_b n)) -> O(nb(log_b n))

But we now no longer check if our filesystem contains orphaned
stickynotes or unknown filetypes:

- Orphaned stickynotes turned out to not be a big deal. If we find
  orphans we'd need to do a second traversal to remove them anyways (no
  mutation allowed in lfsr_mount), so this actually ends up a net
  improvement in the found-orphan case.

  If anything, doing a traversal on first write sets user expectations
  correctly, and can be offloaded with lfsr_fs_mkconsistent or
  lfsr_fs_gc.

- Unknown filetypes are a bit more annoying (I actually forgot about
  this check), but unknown filetypes that require special care should
  probably set WCOMPAT/RCOMPAT flags.

  Allowing unknown filetypes is a bit more flexible in cases where a
  filesystem image is being shared between drivers with different
  features (bootloader + app for example).

  Though we should probably add more checks/tests that we're handling
  these correctly now that we no longer just bail during mount...

Also renamed LFS_I_HASORPHANS -> LFS_I_UNTIDY.

Not doing something is cheaper than doing something, so this saves a bit
of code:

           code          stack          ctx
  before: 38120           2624          752
  after:  38020 (-0.3%)   2624 (+0.0%)  752 (+0.0%)
2025-01-28 14:41:45 -06:00
Christopher Haster 7159248051 Reverted LFS_O_ORPHAN -> LFS_O_UNCREAT
As a part of the effort to undo the overuse of the term "orphan".

I can't really think of a better name, and uncreat gets the point
across. At least it matches LFS_O_UNSYNC/LFS_O_UNFLUSH.

Apparently the Uncreated are a race of aliens in the Marvel universe?
2025-01-28 14:41:45 -06:00
Christopher Haster 66bf005bb8 Renamed LFSR_TAG_ORPHAN -> LFSR_TAG_STICKYNOTE
I've been unhappy with LFSR_TAG_ORPHAN for a while now. While it's true
these represent orphaned files, they also represent zombied files. And
as long as a reference to the file exists in-RAM, I find it hard to say
these files are truely "orphaned".

We're also just using the term "orphan" for too many things.

Really this tag just represents an mid reservation. The term stickynote
works well enough for this, and fits in with the other internal tag,
LFSR_TAG_BOOKMARK.
2025-01-28 14:41:45 -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 9980323e3f attrs: Dropped lfsr_setattr flags
After running into issues with LFS_A_CREAT/EXCL in file-attached custom
attributes, we're left in a really weird place:

- None of lfs_setattr's flags are valid in lfs_attr
- None of lfs_attr's flags are valid in lfs_setattr

I also started thinking about the actual use case for LFS_A_CREAT/EXCL,
and it's really not clear.

littlefs really doesn't care about interprocess communication the same
way POSIX/other filesystem APIs do. We can always rely on integration
layers wrapping up multiple operations in a single mutex, so offering
flexible creation semantics has diminished value. LFS_A_CREAT and
LFS_A_EXCL can both be emulated by calling lfsr_getattr first and
checking its return value.

Thinking ahead to the hypothetical lfsr_set API. The main purpose of
lfsr_set is to provide an API that's easier to use but less powerful
than lfsr_file_open. And adding a flags argument seems to run counter to
that.

For example, if you saw this code with no knowledge of littlefs:

  lfsr_setattr(&lfs, "cat", 'a', "meow", 4, 0);

You would probably be surprised that it returns LFS_ERR_NOENT without
additional flags.

I realize Linux sidesteps this with XATTR_CREATE/REPLACE by making 0
default to implicitly creating, but I didn't want to introduce
inconsistent flag behavior like this unless I had to.

---

So for now dropping LFS_A_CREAT/EXCL and flags argument to lfsr_setattr.

Code savings minimal, this was mostly for API ergonomics:

           code          stack
  before: 38104           2624
  after:  38084 (-0.1%)   2624 (+0.0%)
2024-08-23 01:11:25 -05:00
Christopher Haster f80db15c7e attrs: (Re)implemented file-attached custom attributes
Unlike lfsr_setattr/getattr/etc, file-attached custom attributes are
RAM-backed snapshots attached to, well, files, that can be committed
atomically along with the file's contents. Great for power-loss
resilience, but boy does it make a mess of an API.

This API was really where custom attributes needed some TLC.

The biggest change is how file-attached custom attributes interact with
file sync broadcasting.

A common complaint from users is that setting custom attributes did not
update attributes in open file handles. This behavior is _very_
inconsistent with other filesystems and created a lot of confusion.
Since we're nailing down littlefs's snapshot/broadcasting model as a
part of larger changes, it makes sense to also nail down how custom
attributes interact.

In the new model:

- Custom attributes are still in-RAM snapshots. Updates do not
  immediately take effect, even across write calls.

- On lfsr_file_sync or lfsr_file_close, custom attributes are written
  atomically to disk and broadcasted to all open file handles.

- lfsr_setattr/removeattr also take part in attribute broadcasting. When
  called, lfsr_setattr/removeattr updates the attribute on disk and
  broadcasts the attribute changes to all open file handles.

- Desynced files do _not_ recieve any attribute broadcasts in the same
  way they do not recieve any data broadcasts.

This should hopefully make littlefs behave much more consistently with
other filesystems, while still maintaining a well-defined snapshot and
power-loss properties.

---

The lfs_attr struct also gained several new fields:

  // Custom attribute structure, used to describe custom attributes
  // committed atomically during file writes.
  struct lfs_attr {
      // Type of attribute
      //
      // Note some of this range is reserved:
      // 0x00-0x7f - Free for custom attributes
      // 0x80-0xff - May be assigned a standard attribute
      uint8_t type;

      // Flags that control how attr is read/written/removed
      uint8_t flags;

      // Pointer the buffer where the attr will be read/written
      void *buffer;

      // Size of the attr buffer in bytes, this can be set to
      // LFS_ERR_NOATTR to remove the attr
      lfs_ssize_t buffer_size;

      // Optional pointer to a mutable attr size, updated on read/write,
      // set to LFS_ERR_NOATTR if attr does not exist
      //
      // Defaults to buffer_size if NULL
      lfs_ssize_t *size;
  };

Which are useful for several new features:

- lfs_attr now supports LFS_A_RDONLY/WRONLY/RDWR modes.

  One of the blockers for attribute broadcasting was in-ROM attributes,
  where broadcast updates would hard-fault. But now if you mark in-ROM
  attributes as WRONLY, and in-RAM attributes as RDWR, this problem goes
  away.

- When opened, lfs_attr now optionally writes the attribute size to the
  indirect size field.

  No more hacky zero padding and not knowing an attribute's size.

  Note this follows the same rules as lfsr_getattr, so it does truncate
  if the buffer is too small.

  The size field can also be set to NULL, in which case lfs_attr
  defaults to the buffer_size. This can be quite useful for pure
  ROM-backed attributes.

- Missing attributes are now represented with size=LFS_ERR_NOATTR.

  No more zero-sized vs missing attribute ambiguity.

  This also makes it possible to remove attributes via lfs_attr, by
  setting the size to LFS_ERR_NOATTR manually.

  This does lead to a bit of a quirk where buffer_size can be
  LFS_ERR_NOATTR, which is a bit weird but at least consistent.

- Changes to lfs_attrs will now always trigger file syncs by default.

  Previously, if you changed an attribute, you had to also change the
  file's contents for it to get written to disk. As pointed out by users
  this is both surprising and difficult to work around.

  Solving this is quite tricky since there's no real signalling
  mechanism between attribute buffers and littlefs. The best I could
  come up with is to read attributes from disk during lfsr_file_sync to
  see if anything changed.

  At the very least, the new flag LFS_A_LAZY restores the old behavior
  in case the extra reads in lfsr_file_sync are problematic.

  Though I suspect _most_ calls to lfsr_file_sync immediately follow
  intentional changes to a file. It would be interesting to know of
  examples where this is not the case...

These new fields do increase the size of lfs_attr, which is a downside,
but thanks to flags fitting in type's padding, this is only an increase
from 3 words (12 bytes) -> 4 words (16 bytes).

---

Other implementation notes:

- I did try to implement LFS_A_CREAT/EXCL in lfs_attr but this proved
  to be too messy and inconsistent, so I dropped the idea for now.

  The idea was to error with NOATTR/EXIST if the lfs_attr flag in
  incompatible with what's on disk, but this led to a lot of complexity
  for what is a pretty niche use case.

  It's also inconsistent with rdonly attrs, which do _not_ error with
  NOATTR during lfsr_file_opencfg, because that would be kind of
  annoying.

- Having both `struct lfs_attr` and `lfsr_attr_t` to represent different
  things in the codebase is both fragile and confusing. One of these
  needs to change, probably `lfsr_attr_t`.

  If only I could think of a good name...

  One of the nice side-effects of the now-dropped uattr/sattr split was
  avoiding this conflict.

- We still need more tests related to how custom attributes interact
  with other filesystem operations, but I wanted to get what is
  currently working committed, see the TODOs in test_attrs.toml.

All of the new bells and whistles unfortunately do add up.
lfsr_file_sync is also the root of our current stack hot-path, so the
additional attr also adds a bit of stack:

           code          stack
  before: 37116           2608
  after:  38104 (+2.7%)   2624 (+0.6%)

Still, having a consistent and flexible API is well worth it.

Though I do think at some point we should add a compile-time option to
opt-out of custom attributes (LFS_NO_ATTR?).
2024-08-23 01:10:16 -05:00
Christopher Haster f539d3341c attrs: (Re)implemented lfsr_setattr/getattr/etc
These functions provide simple access to littlefs's custom attributes,
which are small pieces of user-specified metadata that can be attached
to files, dirs, root, etc:

- lfsr_getattr    - Reads an attribute
- lfsr_sizeattr   - Gets the size of an attribute
- lfsr_setattr    - Writes an attribute
- lfsr_removeattr - Removes an attribute

You may notice these functions look quite a bit different from their
previous incarnations. This is because the custom attribute API is
getting an overhaul based on feedback provided by users

The previous API had some real design flaws that interfered with
usability, but now that things have had some time to settle (6 years!),
hopefully most of the pain points are clear.

Notable changes:

- lfsr_getattr's return value is now limited by buffer size.

  The intention of the previous API, where lfsr_getattr always returns
  the attr size, even if it's larger than the buffer, was to allow users
  to find the attr size without an infinitely large buffer.

  In defense of this design, Linux's getxattr does something somewhat
  similar, returning the attr size when the buffer size equals zero.
  Though getxattr does truncate when buffer size is non-zero, which is
  probably safer.

  But, let's be honest, this multipurpose abuse of lfsr_getattr's return
  value is inconsistent with other read functions and potentially
  dangerous for users.

  I think one of the reasons for this API in Linux-land is the limited
  syscall numbers discouraging new functions, but we have no such
  limitation here! We might as well add a dedicated function for
  this: lfsr_sizeattr.

- No more padding with zeros!

  This was a cludge to get around the lack of returned size in custom
  attributes attached to files, but is inconsistent with other read
  functions, so needs to go.

  In general, inconsistencies violate user assumptions, and are usually
  a sign of a bad API.

- lfsr_setattr now takes flags.

  This gives lfsr_setattr more flexiblity in how it operates, and may
  make future extensions easier.

  lfsr_setattr currently supports two flags, which may look a bit
  familiar:

    LFS_A_CREAT     0x04  // Create an attr if it does not exist
    LFS_A_EXCL      0x08  // Fail if an attr already exists

  One long-term idea is to eventually add a simple lfsr_set function to
  make it easier to create small files, so this sort of design overlap
  between lfsr_setattr and lfsr_file_open is hopefully a good thing.

---

Code-wise, these function are really not that bad. Adding functions adds
code, but these are just small wrappers over our internal lookup/commit
functions:

           code          stack
  before: 36556           2608
  after:  37116 (+1.5%)   2608 (+0.0%)

Of course the real cost of custom attributes is how they interact with
open files, a detail which is conveniently missing for now...
2024-08-22 19:49:18 -05:00
Christopher Haster ed96e304de Added lfsr_file_resync
lfsr_file_resync discards the current working state of a file and
reverts it to the contents on disk. It also clears the desynced flag
from files, so provides an alternative to lfsr_file_sync for when you
don't want to write to the filesystem:

  disk=A file=A        disk=A file=A
        | write B            | write B
        v                    v
  disk=A file=B        disk=A file=B
        | sync               | resync
        v                    v
  disk=B file=B        disk=A file=A

The main motivation for this is to provide a way to mark desynced
readonly files as in-sync, without putting them into a weird state where
they are "in-sync" but don't match disk.

It's also a bit safer if the file is desynced due to an error, since
errors aren't currently guaranteed to leave file data in a defined
state. Needed to resync to recover from errors avoids accidentally
syncing partial writes.

This exact behavior can also be accomplished by closing+opening the
file, but lfsr_file_resync makes it much easier without _that_ much
extra code. It may even pay for itself if you consider what code it
saves on the user's side of things.

I considered naming this lfsr_file_discard because I think it sounds
cooler, but I figured including sync in the name provides a stronger
hint that it affects the file's desync status.

---

You may think it's not possible for a readonly file to become
out-of-sync from disk, since it's, well, readonly. But it is possible
thanks to desynced files ignoring other sync broadcasts.

Consider what happens if you open a file readonly, and write+sync the
file with another file handle at the same time:

  disk=A f1=A f2=A
         | desync f2
         v
  disk=A f1=A f2=A
         | write f1=B
         v
  disk=A f1=B f2=A
         | sync f1
         v
  disk=B f1=B f2=A  <-- f2 is out-of-sync without any writes

---

This commit also changes lfsr_file_sync/flush to assert if the file is
readonly. Previously we allowed lfsr_file_sync to be called on readonly
files if it would be a noop, but lfsr_file_resync makes this
unnecessary.

More code means more code, but I think it is well worth it for the
additional flexibility:

           code          stack
  before: 36412           2616
  after:  36748 (+0.9%)   2616 (+0.0%)
2024-08-20 19:59:08 -05:00
Christopher Haster ea017d33fe Moved info flags to overlap with traversal flags
We just have too many flags! Mount flags specifically are already close
to filling up with the currently planned features.

Fortunately the info flags, used internally to track filesystem state,
are never needed at the same time as the traversal flags which specify
one-time traversals during lfsr_mount. So we can move these to overlap
and free up quite a bit more space:

              8     8     8     8
            .----++----++----++----.
            .----..-..-..----------.
  o_flags:  |type||f||t||    o     |
            |----||-|:-:'--.-.-----'
            |----||-|:-:---:-:-----.
  d_flags:  |type||f|: :   : :     |
            |----||-|:-:---:-:-----'
            |----||-|:-'--..-..----.
  t_flags:  |type||f|| t  ||f||tstt|
            '----''-'|----|'-''----'
            .--------|----|:-:-----.
  gc_flags: |        | t  |: :     |
            '--------|----|:-:-----'
            .-------.|----|.-------.
  f_flags:  |   m   || t  ||   f   |
            |-------||----|'-------'
            |-------||----|:-:.----.
  m_flags:  |   m   || t  ||o|| m  |
            |-------|'----'|-||----|
            |-------|.----.|-||----|
  i_flags:  |   m   || i  ||o|| m  |
            '-------''----''-''----'

The only downside is a bit more masking and not having this info
available when debugging.

The overlap is also convenient for lfsr_fs_gc and lets us remove some
shifts, which humorously perfectly canceled out the added cost of the
masks:

           code          stack
  before: 36416           2616
  after:  36416 (+0.0%)   2616 (+0.0%)
2024-08-20 12:39:16 -05:00
Christopher Haster 2f11fa71f4 Implemented ckcksums
Since we already need all the machinery to track ck info for ckparity, I
figured we might as well implement a full ckcksums option as well.

Ckcksums closes the checksum-read-hole by reading enough data to check a
relevant checksum on ever read, even if this ends up being significantly
more data than the initial request. This should always detect detectable
bit-errors, even if they occur between consecutive reads.

If this sounds naive, that's because it is. Performance will be awful.

To be clear, ckcksums should probably never be used in production. I
can't think of a use case that isn't better handled by either ECC in the
block device or the future-planned ckredund feature. Just look at the
runtime complexities:

                  small-reads  rbyd-lookup  rbyd-compaction
  ckcksums:            O(b^2)   O(b log b)     O(b^2 log b)
  ckredund*: O(log_b(n) + xb)     O(log b)       O(b log b)
  eccbd*:                O(b)     O(log b)       O(b log b)

  * theoretical

We've already seen that O(b^2) compactions turns a performance problem
into a tractability problem, so I think O(b^2 log b) compactions will be
a bit too much for most applications.

We can already seen this in our test_ck_ckcksums_* tests (which do pass
by the way!). Compare to test_ck_ckprogs_*, which is basically the same
set of tests:

  test_ck_ckprogs_*:   6.08s
  test_ck_ckcksums_*: 64.88s

Or consider test_rbyd with/without ckcksums:

  test_rbyd:           12.21s
  test_rbyd+ckcksums: 389.94s

Still, ckcksums is an interesting proof-of-concept, and does manage to
close the checksum-read-hole.

---

Like ckprogs/ckfetches/ckparity/etc, ckcksums is an opt-in feature,
requiring both 1. defining LFS_CKCKSUMS and 2. passing LFS_M_CKCKSUMS at
mount time.

Like ckparity, ckcksums requires a significant code and stack increase
to track ck info in lfsr_data_t:

                 code          stack
  before:       36416           2616
  yes-ckcksums: 38872 (+6.7%)   3176 (+21.4%)
  no-ckcksums:  36416 (+0.0%)   2616 (+0.0%)

It's interesting to note how this compares to all of the current
ck-modes, though each has their own set of tradeoffs:

                 code          stack
  default:      36416           2616
  ckprogs:      36468 (+0.1%)   2616 (+0.0%)
  ckfetches:    36666 (+0.7%)   2648 (+1.2%)
  ckparity:     37996 (+4.3%)   3040 (+16.2%)
  ckcksums:     38872 (+6.7%)   3176 (+21.4%)

---

Note that even though ckcksums is opt-in, it may still be worth removing
from the codebase in the future, for a couple reasons:

- Every feature, even if unused, adds developer/maintenance burden.

- Ck info is particularly messy with how it interacts with all
  lfsr_data_t APIs. Though getting rid of ck info would also require
  getting rid of ckparity.

- It's possible for a user to see ckcksums in the codebase,
  misunderstand its tradeoffs, enable it, and get the impression that
  littlefs itself is just unusably slow.
2024-08-20 00:32:00 -05:00
Christopher Haster 464311b2f8 ckparity: Tweaked lfsr_data/ck_t to track parity
So instead of always reading the parity byte on demand, we read it once
in lfsr_bd_readtag, and store it in an unused bit in lfsr_data/ck_t.

The main reason for this is to avoid rereading that byte all the time.

Though I suppose there is also an ever-so-tiny increase in chance of
catching a bit-error after lfsr_bd_readtag. Assuming RAM is more
reliable than disk...

It also keeps the read-parity-byte mess limited to lfsr_bd_readtag, and
simplifies lfsr_bd_ckprefix/cksuffix a bit, which is nice. Though at the
cost of making lfsr_bd_readtag's API a bit most awkward with the
addition of the ckparity-specific parity_ parameter.

This adds a bit more code, but ends up saving some stack:

                    code          stack
  default before:  36412           2616
  default after:   36416 (+0.0%)   2616 (+0.0%)

  ckparity before: 37900           3048
  ckparity after:  37948 (+0.1%)   3032 (-0.5%)

The extra 4-bytes in our non-ckparity build comes from us moving the
saving of the ecksum to after checksum calculation, since we need to
know the parity in the ckparity build. So just compiler noise.
2024-08-20 00:32:00 -05:00
Christopher Haster 2d121c8d19 Relegated ckreads -> ckparity
Ckparity is pretty flawed in littlefs, for several reasons. The biggest
one being that we can't even reliably detect single-bit errors.

But! It can still provide an extra layer of safety in a system where you
don't care about the extra code/stack cost.

And, for ckreads, performance cost...

Performance isn't a big problem for parity-checking. We can assume
metadata tags are going to relatively small (and can be controlled by
fragment_size). But for data checksums, ckreads risks O(b^2) when
performing many small reads, which can be a bit of a problem.

And since ckreads doesn't really prove anything interesting about the
system anymore, it makes sense to unbundle these two checks, rename
ckreads -> ckparity, and limit it to only checking parity bits.

This way, you can enable ckparity for a bit of extra safety, with a
code/stack cost hit, but without sacrificing performance.

---

I was hoping more code/stack savings, but since we still need to track
parity context in lfsr_data_t, and still need to intercept bd_read/cmp/
cpy calls that reference metadata, we end up needing to keep most of
the ck circuitry around:

                    code          stack
  default before:  36464           2672
  default after:   36464 (+0.0%)   2672 (+0.0%)

                    code          stack
  ckparity before: 38036           3080
  ckparity after:  38024 (-0.0%)   3080 (+0.0%)

We even end up still tracking checksum context for bptrs! Maybe we
should just go ahead and add ckcksums as a joke...
2024-08-20 00:30:29 -05:00
Christopher Haster 2cefcbdddc Dropped lfsr_mptr_t as a struct
This replaces the lfsr_mptr_t struct with simple arrays.

The main motivation for this is C99's strict aliasing. It saves a
decent amount of stack to reference the mdir's internal block array as
an mptr directly, but we were only able to accomplish this in
lfsr_mdir_mptr by violating C99's strict aliasing rules.

The main downside of this is C's wonderful array-to-pointer decay
resulting in more implicit references and chances for things to get
clobbered (the original motivation for lfsr_mptr_t was due to bugs
introduced this way).

If I know one thing about C99's strict aliasing it's that it sure loves
to make code less safe.

No significant code changes, which is probably a good thing:

                     code          stack
  default before:   36436           2672
  default after:    36432 (-0.0%)   2672 (+0.0%)

  ckfetches before: 36674           2704
  ckfetches after:  36666 (-0.0%)   2704 (+0.0%)
2024-08-20 00:28:55 -05:00
Christopher Haster 5502fe55ab Implemented ckfetches
Ckfetches implements what might be your first idea on how to check
checksums in a filesystem: Check each block/mdir on first access
(fetch) to make sure the data is sound.

Unfortunately, there are two problems with this approach, both which
come from the fact that blocks are big and can't fit in RAM:

1. We still have a checksum-read hole.

   We can't keep a whole block around in RAM, so reads after a fetch may
   need to reread from disk, at which point new bit-errors may slip in
   undetected.

   This is especially problematic for traversing our rbyds, which
   involves a lot of small reads in a block.

2. Ckfetches may have a surprisingly negative performance impact.

   Consider the case of reading a large file with a bunch of small
   reads. Because we don't cache blocks, each read may need a btree
   lookup, and a full block fetch. On paper this can quickly end up
   O(b^2), which is not great.

   Though this is helped by the file buffer. It will be interesting to
   benchmark and see if this theoretical O(b^2) translates to poor
   performance in practice.

   Note ckreads has this same performance issue.

Still, despite these problems, ckfetches may be useful for cases where
you just want an extra layer of safety, or don't care about the tiny
chance an error is introduced between a fetch an subsequent read.

---

Like ckprogs/ckreads, ckfetches is an opt-in feature, and requires both
1. defining LFS_CKFETCHES, and 2. passing LFS_M_CKFETCHES during mount.

This is a bit of a quick implementation to get testing in place, so the
code cost is probably higher than strictly necessary. If we can refactor
the code internally to avoid all the duplicate lfsr_rbyd_fetchck/
lfsr_bptr_ck calls, we can probably bring this down a bit:

                  code          stack
  before:        36428           2680
  yes-ckfetches: 36848 (+1.2%)   2680 (+0.0%)
  no-ckfetches:  36428 (+0.0%)   2680 (+0.0%)

Oh, and also added lfs_emubd_flipbit to allow tests to manually flip
bits themselves. LFS_EMUBD_BADBLOCK_PROGFLIP is quick to find the above
mentioned checksum-read hole.

This could be done manually with read+erase+prog, but no reason to make
it harder than it needs to be.
2024-08-16 01:04:26 -05:00
Christopher Haster 10feccf18c Moved ckprogs behind LFS_CKPROGS ifdef
So just like ckreads, ckprogs is now opt-in, requiring both 1. defining
LFS_CKPROGS at compile-time, and 2. passing the LFS_M_CKPROGS flag
during lfsr_mount.

_Unlike_ ckreads, ckprogs is actually a very lightweight feature. So the
difference between compiling with/without ckprogs is really quite small:

                code          stack
  before:      36480           2680
  yes-ckprogs: 36480 (+0.0%)   2680 (+0.0%)
  no-ckprogs:  36428 (-0.1%)   2680 (+0.0%)

It's almost not worth putting behind an ifdef if not for consistency
with ckreads.
2024-08-16 01:04:24 -05:00
Christopher Haster e536300606 Rearranged flags a bit
Mainly to make space for more shared open/mount flags that are future
planned.

The nice thing about our flags is they don't live on-disk, so we can
always change them whenever we need to.

It gets a bit messy, but this is what the current layout looks like:

              8     8     8     8
            .----++----++----++----.
            .----..-..-..----------.
  o_flags:  |type||f||t||    o     |
            |----||-|:-:'--.-.-----'
            |----||-|:-:---:-:-----.
  d_flags:  |type||f|: :   : :     |
            |----||-|:-:---:-:-----'
            |----||-|:-'--..-..----.
  t_flags:  |type||f|| t  ||f||tstt|
            '----''-'|----|'-''----'
            .--------|----|:-:-----.
  gc_flags: |        | t  |: :     |
            '--------|----|:-:-----'
            .----..-.|----|:-:.----.
  f_flags:  | f  ||m|| t  |: :| f  |
            '----'|-||----|:-:'----'
            .----.|-||----||-|.----.
  m_flags:  | i  ||m|| t  ||o|| m  |
            |----||-|'----'|-||----|
            |----||-|------|-||----|
  i_flags:  | i  ||m|      |o|| m  |
            '----''-'------'-''----'

The main downside of this layout is that some of the traversal flags,
LFS_T_DIRTY/MUTATED, now risk ambiguity with open/mount flags. But I
don't think this is really avoidable as traversals are already using
almost the entire 32-bit encoding space...

No code changes:

           code          stack
  before: 36480           2680
  after:  36480 (+0.0%)   2680 (+0.0%)
2024-08-16 01:04:21 -05:00
Christopher Haster 80ef963bec Renamed LFS_I_ORPHANS -> LFS_I_HASORPHANS
This better matches how other flags sometimes include the relevant verb,
LFS_RBYD_ISSHRUB, LFSR_DATA_ONDISK, etc, and feels a bit more
consistent.
2024-08-16 01:04:19 -05:00
Christopher Haster 6d0b05da6c Extended lfsr_format with some gc flags
These fall out quite naturally when you consider that we call
lfsr_mountinited internally to check that our format was successful.

That being said... they don't really do anything right now since we only
write a single mdir:

- LFS_F_COMPACT - The only gc operation that _might_ actually do
  something is LFS_F_COMPACT, but only if our fs config exceeds >1/2 the
  block size. But I'm not sure littlefs will even be able to write file
  metadata if this happens...

- LFS_F_CKMETA - We already check the only mdir by calling
  lfsr_mountinited, which implicitly fetches the mrootanchor.

- LFS_F_CKDATA - We uh, don't have any data immediately after
  lfsr_format. But I guess it doesn't hurt to keep this around for
  consistency, it at least implies CKMETA.

Hopefully these flags will be more interesting if/when we start adding
auxiliary trees to the filesystem, otherwise they may be worth reverting
in the future...

Until then, they at least provide some consistency, and I guess a way to
triply check that format was successful.

---

This could probably be better deduplicated, but calling lfsr_fs_gc from
both lfsr_mount and lfsr_format provides a bit better code organization:

           code          stack
  before: 36448           2680
  after:  36480 (+0.1%)   2680 (+0.0%)
2024-08-16 01:04:16 -05:00
Christopher Haster acad3a3143 Added format flags to lfsr_format
This is mainly to solve the weird check-hole where passing CKPROGS/
CKREADS as mount flags has no effect on lfsr_format (I mean, it'd be a
bit silly if it did somehow):

  LFS_F_RDWR              0  // Format the filesystem as read and write
  LFS_F_CKPROGS  0x00000010  // Check progs by reading back progged data
  LFS_F_CKREADS  0x00000020  // Check reads via parity bits/checksums

This makes lfsr_format a more cumbersome interface, but I don't know if
this is necessarily a bad thing. There's always risk of data loss when
calling lfsr_format, so maybe it should be a pain to call.

At the very least, format flags may be useful in the future for
enabling/disabling format-time things such as the planned block-map,
parity-tree, etc. Though it's unclear if such significant settings
should be format flags or somehow encoded as fields in our config
struct.

---

The LFS_F_* format flags of course ended up conflicting with our
internal LFS_F_* flags, so I renamed most of the internal flags to match
the closest flag set they participate in:

- LFS_F_TYPE        -> LFS_O_TYPE
- LFS_F_UNFLUSH     -> LFS_O_UNFLUSH
- LFS_F_UNSYNC      -> LFS_O_UNSYNC
- LFS_F_ORPHAN      -> LFS_O_ORPHAN
- LFS_F_ZOMBIE      -> LFS_O_ZOMBIE

- LFS_F_ORPHANS     -> LFS_I_ORPHANS
- LFS_F_UNCOMPACTED -> LFS_I_UNCOMPACTED

- LFS_F_TSTATE      -> LFS_T_TSTATE
- LFS_F_BTYPE       -> LFS_T_BTYPE
- LFS_F_DIRTY       -> LFS_T_DIRTY
- LFS_F_MUTATED     -> LFS_T_MUTATED

This may make it a bit less clear which flags are a part of the public
API, vs intended only for internal use, but at the very least our asserts
in format/mount/open/etc should catch most of these mistakes.

---

Code cost ended up being pretty minimal. Actually negative. This is the
second time we're _adding_ a feature that somehow saves code, though the
reality for this one is we're really just pushing constants up into the
user's stack frame. Still, it's a good indication the cost of format
flags is small:

           code          stack
  before: 36452           2680
  after:  36448 (-0.0%)   2680 (+0.0%)
2024-08-16 01:04:13 -05:00
Christopher Haster 6e2af5bf80 Carved out ckreads, disabled at compile-time by default
This moves all ckread-related logic behind the new opt-in compile-time
LFS_CKREADS flag. So in order to use ckreads you need to 1. define
LFS_CKREADS at compile time, and 2. pass LFS_M_CKREADS during
lfsr_mount.

This was always the plan since, even if ckreads worked perfectly, it
adds a significant amount of baggage (stack mostly) to track the
ck context of all reads.

---

This is the first non-trivial opt-in define in littlefs, so more test
framework features!

test.py and build.py now support the optional ifdef attribute, which
makes it easy to indicate a test suite/case should not be compiled when
a feature is missing.

Also interesting to note is the addition of LFS_IFDEF_CKREADS, which
solves several issues (and general ugliness) related to #ifdefs in
expression. For example:

  // does not compile :( (can't embed ifdefs in macros)
  LFS_ASSERT(flags == (
          LFS_M_CKPROGS
              #ifdef LFS_CKREADS
              | LFS_M_CKREADS
              #endif
              ))

  // does compile :)
  LFS_ASSERT(flags == (
          LFS_M_CKPROGS
              | LFS_IFDEF_CKREADS(LFS_M_CKREADS, 0)));

---

This brings us way back down to our pre-ckread levels of code/stack:

                   code          stack
  before-ckreads: 36352           2672
  ckreads:        38060 (+4.7%)   3056 (+14.4%)
  after-ckreads:  36428 (+0.2%)   2680 (+0.3%)

Unfortunately, we do end up with a bit more code cost than where we
started. Mainly due to code moving around to support the ckread
infrastructure:

                   code          stack
  lfsr_bd_readtag:  +52 (+23.2%)    +8 (+10.0%)
  lfsr_rbyd_fetch:  +36 (+5.0%)     +8 (+6.2%, cold)
  lfs_toleb128:     -12 (-25.0%)    -4 (-20.0%, cold)
  total:            +76 (+0.2%)     +8 (+0.3%)

But oh well. Note that some of these changes are good even without
ckreads, such as only parsing the last ecksum tag.
2024-08-16 01:04:03 -05:00
Christopher Haster 185f209dbf Moved ckreads behind the LFS_M_CKREADS flag
Added some code, though we don't _really_ care:

           code          stack
  before: 37872           3048
  after:  38060 (+0.5%)   3056 (+0.3%)

Also interesting to note the difference in testing time, this highlights
_some_ of the performance cost of ckreads:

  with ckreads:    1135.92s
  without ckreads:  821.24s
2024-08-16 01:04:00 -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 e66170308e Renamed public API params to match internal names
- traversal -> t
- config -> cfg

This is just to make things consistent in case users want to peek behind
the curtain.
2024-07-27 00:47:45 -05:00
Christopher Haster b37bff377b Added mount-time LFS_M_FLUSH/SYNC
These simply imply LFS_O_FLUSH/SYNC on all open writable files.
LFS_M_SYNC is equivalent to MS_SYNCHRONOUS in Linux/etc, while
LFS_M_FLUSH is just provided for consistency.

As pure conveniences, these may seem a bit out of scope for littlefs,
except they are _very_ cheap:

           code          stack
  before: 36356           2664
  after:  36356 (+0.0%)   2664 (+0.0%)

Ok, they're not _completely_ free! It just turns out they cost 8 bytes,
and a bit of simplification around flag checking in lfsr_mount saved
8 bytes:

                  code          stack
  before:        36356           2664
  m_flush/sync:  36364 (+0.0%)   2664 (+0.0%)
  mount-no-mask: 36356 (+0.0%)   2664 (+0.0%)
2024-07-27 00:47:45 -05:00
Christopher Haster d79e4ae455 Added LFS_O_CKMETA/CKDATA flags
These flags just call lfsr_file_ckmeta/ckdata under the hood, but make
it very easy to check metadata/data when opening a file. As an extra
plus they implicitly close the file on failure, so might make cleanup
easier.

Of course, everything has a cost:

           code          stack
  before: 36368           2664
  after:  36424 (+0.2%)   2664 (+0.0%)

These also ruin my previous "you don't pay for what you don't call"
assertion, since runtime flags unfortunately always pull in code.

We should add a compile-time switch for these evntually.
2024-07-27 00:47:45 -05:00
Christopher Haster e2c238c30d Added lfsr_file_ckmeta/ckdata
These are basically the same as lfsr_fs_ckmeta/ckdata but limited to a
single file. They may be useful when you need to validate a file but
don't want to bother validating the entire filesystem:

  // Check a file for metadata errors
  int lfsr_file_ckmeta(lfs_t *lfs, lfsr_file_t *file);

  // Check a file for metadata + data errors
  int lfsr_file_ckdata(lfs_t *lfs, lfsr_file_t *file);

I've also added test_ck to test these and added some more
lfsr_fs_ckmeta/ckdata tests there. These currently just test simple
full-block clobbering, but we should eventually test more interesting
error patterns.

Unfortunately lfsr_file_ckmeta/ckdata can't reuse the internal
lfsr_mtree_traverse in quite the same way lfsr_fs_ckmeta/ckdata can, so
they're actually a bit more expensive. Though keep in mind with
link-time gc you won't pay the cost unless you call these functions:

           code          stack
  before: 36024           2696
  after:  36368 (+1.0%)   2664 (-1.2%)

Oh, and the multiple calls to lfsr_btree/bshrub_traverse apparently
uninlined it out of lfsr_mtree_traverse, saving the stack cost in the
stack hot-path... Yay?
2024-07-27 00:47:45 -05:00
Christopher Haster e812ac4a8c Reverting most of internal LFS_F_CANLOOKAHEAD
It's really not that much code (36 bytes, and only if you call
lfsr_fs_gc), and implicit state is better the explicit state (less
things that can fall out of sync).

I'm keeping the fancy F/GC flag masking in lfsr_fs_gc though.

Code changes:

           code          stack
  before: 35988           2696
  after:  36024 (+0.1%)   2696 (+0.0%)
2024-07-27 00:47:45 -05:00
Christopher Haster b7e7313ef0 Added internal LFS_F_CANLOOKAHEAD flag
This is equivalent to the user-facing LFS_I_CANLOOKAHEAD flag, but
explicitly set in lfs_alloc/lfs_alloc_markfree, rather than being
implied.

Usually, I prefer implicit state, as this means less things that can
fall out-of-sync if there is a filesystem bug, but for
LFS_F_CANLOOKAHEAD explicit state might be warranted.

The main benefit is we can take advantage of the matching F/GC bit
patterns to simplify lfsr_fs_gc's progress checks.

This ends up saving a bit of code:

           code          stack
  before: 36048           2696
  after:  35988 (-0.2%)   2696 (+0.0%)
2024-07-27 00:47:45 -05:00
Christopher Haster 0893c1f6be Increased internal flags 16 bits -> 32 bits
If we add CKMETA/CKDATA and eventually REPAIRMETA/REPAIRDATA to the file
open flags, we'll end up with 17 flags total (13 user-facing,
4 internal), which is a bit (heh) too much for a 16-bit flags field!

There are a few ways to solve this, dropping features for one, instead
I've decided to expand the fields flag to 32-bits. Fortunately this was
already the field size for all user-facing fields.

To avoid a RAM increase, I've also shoved the opened-file types and
traversal tstates into the same field.

We have various flags in quite a few places now, here's how
everything fits together:

              8     8     8     8
            .----++----++----++----.
            .----..---..--..-------.
  o_flags:  |type|| f ||t ||   o   |
            |----||---|:--:'-------'
            |----||---|:--:--------.
  d_flags:  |type|| f |:  :        |
            |----||---|:--:--------'
            |----||---|:--'--..----.
  t_flags:  |type|| f ||  t  ||tstt|
            '----''---'|-----|'----'
            .----------|-----|-----.
  gc_flags: |          |  t  |     |
            '----------|-----|-----'
            .-----.---.|-----|.----.
  m_flags:  |     | m ||  t  || m  |
            '-----|---|'-----'|----|
            .----.|---|-------|----|
  i_flags:  | i  || m |       | m  |
            '----''---'-------'----'

Unfortunately, using the full 32-bit flag space highlights that C99's
enum types are kind of garbage...

In C99 enums are strictly signed ints, which means attempting to use
them for 32-bit bit fields overflows. There is no way around this so
I've switched our flag definitions to #defines.

I've kept types as enums for now but I'm keeping my eye on them...

---

The tradeoff of merging the type/btype/tstate/flags fields is that it
takes more code to extract/encode the various subfields. Since these
fields our heavily used in our codebase, this really adds up:

           code          stack
  before: 35888           2696
  after:  36048 (+0.4%)   2696 (+0.0%)

At least in theory the type fields can be optimized to a byte load, but
not btype/tstate. Also accessing bits in higher positions may be adding
cost.
2024-07-27 00:46:53 -05:00
Christopher Haster 631bfbc1e8 gc: Made lfsr_fs_gc a bit smarter when flags change
Now we consider if it's still possible for the current traversal to make
progress. If it can, we continue with the relevant masked flags,
otherwise we restart. This should prevent us from traversing the
filesystem for no reason.

I also reverted the ckedmeta/ckeddata flags, these ended up just adding
code cost. We're not in the stack hot-path anyways...

Code changes:

           code          stack
  before: 36244           2680
  after:  36228 (-0.0%)   2680 (+0.0%)
2024-07-20 01:27:45 -05:00
Christopher Haster c58a48c02e gc: Consider ckmeta/ckdata successful even if we mutated the filesystem
Also moved ckmeta/ckdata progress into lfs->flags. We have the bits
available so we might as well use them instead of allocating bools on
the stack...

Whether or not to consider ckmeta/ckdata successful when the filesystem
has been mutated is a bit nuanced.

Initially, I thought we trigger a re-traversal, since we may have
introduced new blocks that haven't been checked. But think about it,
where did those blocks come from?

Any new blocks introduced by filesystem mutation will have just been
written. And if a write introduces corruption you probably have bigger
problems...

... Actually as I write this I realized mounting without ckprogs makes
this even more nuanced, but since ckmeta/ckdata is more intended for
data-at-rest error detection I'm going to keep the change for now.

If you want to catch write errors, you really should enable ckprogs.
This is only a problem for lfsr_fs_gc, and the use cases for
ckmeta/ckdata in lfsr_fs_gc will probably catch any write errors on the
next cycle anyways...

Code changes:

           code          stack
  before: 36208           2680
  after:  36244 (+0.1%)   2680 (+0.0%)
2024-07-20 01:27:45 -05:00
Christopher Haster eced943685 Changed gc_steps into a runtime parameter, better dedup mount gc
So instead of configuring gc_steps at mount time (or eventually compile
time), lfsr_fs_gc now takes a steps parameter that controls how much gc
work to attempt:

  int lfsr_fs_gc(lfs_t *lfs, lfs_soff_t steps, uint32_t flags);

This API was needed internally to better deduplicate on-mount gc, and I
figured it might also be useful for users to be able to easily change
gc_steps per lfsr_fs_gc call.

I realize this could also be accomplished with the theoretical
lfsr_fs_gccfg, but it's a bit easier to not need a struct every call.

Most likely, depending on project/system, users will always call
lfsr_fs_gc with either 1 (minimal work) or -1 (maximal work), or, worst
case, can define a system-wide GC_STEPS somewhere.

---

Deduplicating on-mount gc work better saved some code, though it's worth
noting this could have been done internally and not exposed to users:

           code          stack
  before: 36476           2680 (+0.0%)
  after:  36316 (-0.4%)   2680 (+0.0%)
2024-07-18 20:46:58 -05:00
Christopher Haster 4fc03f95a7 Reworked lookahead buffer (again) to avoid shifting bits
The main reason for this change is to allow keeping track of existing
known-free blocks while trying to find more free blocks. This makes it
so failed filesystem traversals don't result in negative progress, which
is nice.

This was difficult in the previous lookahead scheme, since we we'd need
to shift the lookahead buffer to keep off=0 rooted at the first bit.
Shifting bytes is relatively easily with memmove, but it gets tricky
when shifting bits:

  lookahead before: ???? ???? ???? ??00 1101 0101 00?? ????
                                     ^              ^
                                    off          off+size

  shift:            0011 0101 0100 ???? ???? ???? ???? ????
                    ^              ^
                   off          off+size

  traverse:         0011 0101 0100 0000 0000 0000 1100 0000
                    ^                                       ^
                   off                                   off+size

Instead, we now just let the lookahead buffer wrap around. No shifting
required:

  lookahead before: ???? ???? ???? ??00 1101 0101 00?? ????
                                     ^              ^
                                    off          off+size

  traverse:         0000 0000 1100 0000 1101 0101 0000 0000
                                     ^
                                    off
                                     ^
                                  off+size

This gets a bit confusing with the lookahead window also wrapping around
disk, but the math works out with enough modulos (if modulos are too
expensive, we should eventually be able to optimize these into simple
bit masks via compile-time config).

In the future, if we move away from the const config struct, it would
also be nice to try to reducing the number of modulos by storing the
lookahead buffer size in bits instead of bytes...

Note that if the lookahead buffer is larger than disk, the lookahead
window will sort of travel around the underlying buffer. This isn't
inherently a problem, but it did cause some bugs.

To avoid similar bit-related problems with zeroing, lfs_alloc_inc now
also zeros bits as we allocate/skip them, so bits should always be zero
when we start a lookahead traversal. Though note we still need to
manually memset the buffer when discarding lookahead state in init/grow.

---

The end result is surprisingly a net savings in terms of code size. I
guess mainly due to dropping all the lfs_alloc_shift calls:

           code          stack
  before: 36472           2680
  after:  36412 (-0.2%)   2680 (+0.0%)
2024-07-17 22:15:31 -05:00
Christopher Haster 4fe46a983f Added simple lfsr_fs_ckmeta/ckdata functions
These functions provide an easy API for checking all metadata/data
checksums in the filesystem:

  // Check the filesystem for metadata errors
  int lfsr_fs_ckmeta(lfs_t *lfs);

  // Check the filesystem for metadata + data errors
  int lfsr_fs_ckdata(lfs_t *lfs);

These are more-or-less the same as calling lfsr_fs_gc with
LFS_GC_CKMETA/CKDATA, but don't involve the gc/traversal-invalidation
machinery, and may be a bit easier for users to pick up.

---

Unfortunately, for simple wrappers, we're again hit with a somewhat
surprising code cost:

           code          stack
  before: 36288           2680
  after:  36472 (+0.5%)   2680 (+0.0%)

But I think we can again blame the high overhead of LFS_TRAVERSAL/
lfsr_mtree_gc. We should look into reducing/deduplicating this logic...
2024-07-17 22:15:08 -05:00
Christopher Haster 2f08662fb9 Added on-mount traversal flags: LFS_M_MKCONSISTENT/CKMETA/CKDATA/etc
These tell littlefs to do the relevant gc work during mount, which may
be more convenient than calling lfsr_mount and then lfsr_fs_gc.

It also implicitly tears down the filesystem on error, which you can
imagine would be quite useful for LFS_M_CKMETA/LFS_M_CKDATA.

Some flags are more useful here than other (is LFS_M_LOOKAHEAD/COMPACT
really useful?), but since we just pass these directly to our traversal
APIs, we might as well support all of them for consistency.

Also note that since these only change mount's behavior, and have no
effect on the rest of the filesystem, these LFS_M_* flags don't have
related LFS_I_* flags and are not returned by lfsr_fs_stat.

---

This added quite a chunk of code, considering that this is entirely for
convenience:

           code          stack
  before: 35932           2680
  after:  36280 (+1.0%)   2680 (+0.0%)

But I think this is mostly because our low-level traversal state is
relatively costly to manage. It may be possible to deduplicate this a
bit better...
2024-07-17 21:40:37 -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