Commit Graph

87 Commits

Author SHA1 Message Date
Christopher Haster 10e77d9177 runners: bench: Added BENCH_SIMTIME/SIMRESET/PAUSE/RESUME/etc
- BENCH_SIMTIME()   => lfs3_kiwibd_simtime()
- BENCH_SIMRESET()  => lfs3_kiwibd_simreset()
- BENCH_SIMPAUSE()  => lfs3_kiwibd_simpause()
- BENCH_SIMRESUME() => lfs3_kiwibd_simresume()
- BENCH_RESET()     => lfs3_kiwibd_simreset() + BENCH_STACK/HEAP_RESET()
- BENCH_PAUSE()     => lfs3_kiwibd_simpause() + BENCH_STACK/HEAP_PAUSE()
- BENCH_RESUME()    => lfs3_kiwibd_simresume() + BENCH_STACK/HEAP_RESUME()

This does two things:

1. Adds pause/resume counters to bd counters to make it easier to
   exclude operations from the current bench (potentially useful for
   seq+disk usage).

2. Exposes bd simtime operations as BENCH_* macros, to make it a bit
   easier to interact with simtime without tying all the benches to
   kiwibd. (Not that we'll ever probably not use kiwibd, but still).

Also adopted 32-bit counters for stack/heap pause state instead of a
32-bit stack. Not that either are at a risk of overflowing, but better
safe than sorry.
2026-03-09 22:54:49 -05:00
Christopher Haster f62d91c9b5 runners: test: Ripped out stack/heap measurements
The point of having separate test/bench runners is to minimize
complexity when different concerns overlap, and the stack/heap
measurements haven't proven necessary for testing yet.

Keeping them around just adds a maintenance burden, and risks messy
interactions with test features if you ever try to turn them on
(heap + powerloss = memory leaks yay).

So removing for now.

If they are useful in the future (cheaper Valgrind-esque checks?),
copying from bench_runner.c -> test_runner.c is super easy.

---

Note these are still available and enabled by default in the bench
runner.
2026-03-09 22:54:46 -05:00
Christopher Haster c722bc08f5 runners: Intercept logs/printf and exclude from stack/heap measurements
Logging is one of those things that's very useful to keep around, but
has a high-risk of stack/heap costs that shouldn't count towards any
benchmarks (you can always disable logging).

So, lets exclude them from stack/heap measurements.

This could've been done by defining all of littlefs's LFS3_DEBUG/INFO/
WARN/ERROR macros, but intercepting printf directly is a bit less
tedious. As a plus, we eliminate logging costs from any other filesystem
we benchmark, without need to fiddle with everyone's logging APIs.

---

Hmm. Actually, now that I've done a test run, these changes seem to have
no effect.

Which makes sense in hindsight:

1. For efficiencies sake, printf likely tries to allocate infrequently.
   Maybe only during the first call?

   And we print the bench id before entering the bench.

2. The way our stack measurements work, we only count them if we enter a
   bd op or call BENCH_STACK_PAUSE().

   So any printfs encountered previously would have been ignored by our
   stack measurements.

Still, better safe than sorry.
2026-03-09 22:53:34 -05:00
Christopher Haster c1b86ac9db runners: A number of stack/heap measurement tweaks
- Renamed BENCH_STACK/HEAP -> BENCH_STACK/HEAP_WATERMARK

- Renamed BENCH_YES_STACK/HEAP -> BENCH_STACK/HEAP

- Tweaked stack/heap watermarks to hopefully be easier to access when
  debugging. Now also exposed as global variables
  (bench_stack/heap_watermark).

  I considered changing BENCH_STACK/HEAP_WATERMARK to be the variable
  itself, to be consistent with TEST_PLS, but decided against it:

  1. BENCH_STACK_CURRENT() is a bit magic in that it relies on
     __attribute__((noinline)) to force a new stack frame. This wouldn't
     really be possible with a variable.

  2. TEST_PLS is at least constant from the _current run_'s perspective.
     This isn't true for the stack/heap watermarks.

- Reworked internals a bit to hopefully be simpler
2026-03-09 22:53:32 -05:00
Christopher Haster 4405ad47e4 runners: Reworked test/bench for out-of-tree extensions
The main changes:

- Added TEST_DEFINES and BENCH_DEFINES to allow overriding the default
  test/bench define header:

    -DTEST_DEFINES=my_test_defines.h

  Note these are VERY different from LFS_DEFINES upstream. They aren't a
  typical header file, and are included multiple times with various
  query macros.

  It's hacky, but works surprisingly well.

  Or maybe I'll just do anything to avoid having to write multiline
  macros. Ugh, backslashes.

- Moved more logic into bench/test_defines.h, including everything
  needed to integrate other filesystems out-of-tree.

  This mostly meant moving all of the cfg initialization logic into its
  own query macro (replacing the BENCH/TEST_CFG field macros).

But this also includes a bunch of small tweaks encountered while trying
to get external benchmarks running again.

The external benchmarks include several other filesystems (littlefs2,
SPIFFS, Yaffs2), and I'm hoping this injectable/queryable header thing
will do a good job at avoiding a maintenance headache. (At least a
better job than forking bench_runner.c, which was the previous
solution.)
2026-02-19 13:42:49 -06:00
Christopher Haster 356d7065be runners: Added stack/heap measurement functions
These have been battle-tested in external benchmarks, and have proven
useful for finding a runtime estimate on stack+heap usage.

Of course, to be realistic they need to be cross-compiled and run under
QEMU (which does work!), but even on x86_64 they provide a nice insight
into RAM usage. In practice the only real difference is pointer width
anyways.

---

Enabled by default for the bench runner, these are available if
TEST/BENCH_YES_HEAP and/or TEST/BENCH_YES_STACK are defined.

(This default is provided by the Makefile. At least heap measurements
rely on linker flags, so it probably doesn't make sense to default
enable in the bench runner itself.)

Stack vs heap rely on slightly different mechanisms:

- Stack: Uses GCC's __builtin_frame_address(0) to measure the current
  stack usage on entry to every bd operation.

- Heap: Relies on GCC's -Wl,--wrap flags to intercept every malloc/free
  call, to track the current heap usage.

These are available via bench/test macros:

- BENCH_STACK()         - Maximum stack usage of the current run
- BENCH_STACK_CURRENT() - Current stack usage
- BENCH_HEAP()          - Maximum heap usage of the current run
- BENCH_HEAP_CURRENT()  - Current heap usage

Note BENCH_STACK_CURRENT() can be useful for separating out the bench's
ctx from total stack usage, similarly to our static analysis.

---

One surprising outcome is that these heap hooks trivially implement a
memory leak detector. Maybe that could be useful in the test_runner as a
cheaper alternative to Valgrind?
2026-02-13 13:59:44 -06:00
Christopher Haster ab39a8fde9 runners: Adopted compile-time optional kiwibd as emubd alternative
kiwibd has been used extensively in external benchmarks, it makes sense
to make it the default bd for the bench runner:

- test_runner - defaults to emubd - more testing features
- bench_runner - defaults to kiwibd - lighter-weight disks

The benefit of kiwibd is the disk is just one big blob of RAM, so
basically no overhead. This is important when benchmarking on multi-GiB
disks.

emubd is much heavy, but as a tradeoff can do quite a bit more:
bad-block simulation, wear simulation, snapshotting, etc.

---

In theory the bd used by each runner can be controlled at compile-time
by defining -DBENCH_EMUBD, etc, but I have a feeling no one will ever
use this.
2026-02-13 13:49:32 -06:00
Christopher Haster 1b70c1f199 runners: Moved test/bench defines into test/bench_defines.h
The big TEST_IMPLICIT_DEFINES and TEST_CFG macros have been a big
pain-in-the-ass to maintain. Mostly due to C preprocessor annoyances
(bleh escaped newlines) and no-ifdef workarounds, which make a real mess
of things.

This does two things:

1. Moves all the defines out of test_runner.h and into test_defines.h
   (same for benches).

2. Inverts the include logic such that test_defines.h gets included many
   times with various "query macros" defined.

   Currently just two, but can easily add more:

   1. TEST_DEFINE(name, value) - name and default value for a define
   2. TEST_CFG(name, value) - name and value for a cfg field

   This seems to work surprisingly well. It solves all of the above C
   preprocessor issues, and provides a flexible method for defining test
   defines.

   Note an important part of making this work is that test_defines.h
   expands to an empty string by default.
2026-02-10 15:22:38 -06:00
Christopher Haster ecd780a313 preerase: Inverted LFS3_PREERASE to be opt-in
The original motivation for making LFS3_PREERASE opt-out, is that it
makes sense for LFS3_GBMAP to bring in all gbmap-related features
(PREERASE, BADBLOCKS (future)). However, after a bit of use, I think
this just complicates our ifdef logic too much.

So instead, LFS3_PREERASE is now opt-in, with the intention of making
all ifdefs relative only to the default build. I think this will make it
easier to reason about ifdefs, at least internally.

Eventually, I want to look into alternative default builds (LFS3_BIGGER,
LFS3_BIGGERR, ..., LFS3_BIGGEST), which would provide an alternative way
to enable all gbmap-related features. Though these builds have a
high-risk of bikeshedding (LFS3_GC?), so we'll see.

---

That being said, the main ergonomic improvement was probably adding
a #error, so we don't have to check ifdef GBMAP everywhere.

Maybe this should be extended to LFS3_RDONLY? Or maybe not, LFS3_RDONLY
is a bit of a special case.

No code changes:

                    code          stack          ctx
  before:          35144           2136          660
  after:           35144 (+0.0%)   2136 (+0.0%)  660 (+0.0%)

                    code          stack          ctx
  gbmap before:    38380           2144          776
  gbmap after:     38380 (+0.0%)   2144 (+0.0%)  776 (+0.0%)

                    code          stack          ctx
  preerase before: 38920           2168          796
  preerase after:  38920 (+0.0%)   2168 (+0.0%)  796 (+0.0%)
2026-01-09 00:02:42 -06:00
Christopher Haster 843412cc79 preerase: Implemented the gc side of preerase
Allocating pre-erased blocks gets quite complicated due to our
restricted flash model, but at least the actual pre-erasing is
relatively straightforward:

- We keep track of known preerased state in lfs3->gbmap.preeraser.

- If LFS3_GC_PREERASE is provided during gc work, we increment the
  preeraser's known window by scanning the gbmap.

- Any BMFREE ranges we find, we erase a block at a time, and store the
  resulting ecksum in a BMERASED range in the gbmap.

- We keep track of how many blocks we erased, and stop early if this
  exceeds cfg.gc_preerase_count. This just lets users tune how many
  blocks to preerase in case something (?) prevents preerased blocks
  from being used.

Some notes:

- We don't really do anything with ranges in lfs3_alloc_preerase. In
  theory we could bulk in erase to minimize the number of commits to the
  gbmap, but we expect erase to dominate, so this probably isn't worth
  it.

  And if erase doesn't dominate, why would you bother pre-erasing
  blocks?

- Preerasing isn't really a traversal operation, and is managed by a
  sort of secondary state machine in lfs3_fs_gc_.

  This also means lfs3_trv_read with LFS3_T_PREERASE does nothing, but I
  guess that is ok? It's tempting to try to make lfs3_trv_read also
  preerase, but it's unclear what block it should return -- it's
  probably the wrong API.

- Introducing ecksums actually went quite a bit smoother than I
  expected. Though it helps ecksums are the only optional payload, no
  type punning or anything.

  Ecksums do muddy the gbmap's design a bit, unfortunately. The main
  issue being that we can only merge BMERASED ranges with equal ecksums.
  This makes BMERASED ranges less compressable than the others, and may
  be one reason to limit cfg.gc_preerase_count.

  However:

  1. This is where I think it's useful to emphasize that the gbmap's
     responsibility is to track _free_ blocks, in-use blocks are
     secondary.

     When allocating, we're going to stop at the first BMFREE/BMERASED,
     but may need to skip over an unbounded number of BMINUSE/BMBAD
     blocks. So the compressability of BMFREE/BMERASED ranges should
     have less of an impact on block allocation.

  2. In practice, most flash uses consistent erase values, so the
     resulting ecksums will probably be compressable. The exceptions are
     noop-erases (SD/eMMC, RAM, NVRAM, etc), and encryption with block
     address permutation?

     Though noop-erases are a pretty big exception.

Code changes:

                    code          stack          ctx
  before:          35116           2136          660
  after:           35116 (+0.0%)   2136 (+0.0%)  660 (+0.0%)

                    code          stack          ctx
  gbmap+np before: 38040           2136          776
  gbmap+np after:  38188 (+0.4%)   2144 (+0.4%)  776 (+0.0%)

                    code          stack          ctx
  gbmap+yp before: 38040           2136          776
  gbmap+yp after:  38608 (+1.5%)   2144 (+0.4%)  796 (+2.6%)
2026-01-09 00:01:42 -06:00
Christopher Haster 124afb3034 Adopted LFS3_*_CK and LFS3_*_GC aliases, replacing LFS3_GC_ALL
This extends the hopefully useful LFS3_GC_ALL flag alias to the other
functions, without trying to figure out what the heck LFS3_M_ALL should
mean semantically.

Current definitions:

  // an alias for ck work
  *_CK              0x00003000  ---- ---- ---- ---- --11 ---- ---- ----

  // an alias for all possible gc work
  *_GC              0x00003b00  ---- ---- ---- ---- ++11 11+1 ---- ----

  *_MKCONSISTENT    0x00000100  ---- ---- ---- ---- ---- ---1 ---- ----
  *_LOOKAHEAD       0x00000200  ---- ---- ---- ---- ---- --1- ---- ----
  *_PREERASE+       0x00000400  ---- ---- ---- ---- ---- -+-- ---- ----
  *_COMPACT         0x00000800  ---- ---- ---- ---- ---- 1--- ---- ----
  *_CKMETA          0x00001000  ---- ---- ---- ---- ---1 ---- ---- ----
  *_CKDATA          0x00002000  ---- ---- ---- ---- --1- ---- ---- ----
  *_REPAIRMETA+     0x00004000  ---- ---- ---- ---- -+-- ---- ---- ----
  *_REPAIRDATA+     0x00008000  ---- ---- ---- ---- +--- ---- ---- ----

  + Planned

One weird artifact of this is that LFS3_GC_CK ~= LFS3_GC_CKDATA, but I'm
not sure that's a bad thing? Note this is very much not true for info
flags, LFS3_I_CK != LFS3_I_CKDATA.

No code changes.
2026-01-09 00:01:15 -06:00
Christopher Haster 7a57b1e2bd Renamed LFS3_T_COMPACTMETA -> LFS3_T_COMPACT (and gc_compact_thresh)
This effectively reverts 1f824a0:

- LFS3_T_COMPACTMETA -> LFS3_T_COMPACT
- gc_compactmeta_thresh -> gc_compact_thresh

And friends.

After using LFS3_T_COMPACTMETA for a bit, I think it just adds noise
without much value. Especially when next to LFS3_T_LOOKAHEAD,
LFS3_GC_PREERASE, LFS3_M_SYNC, etc.

It's interesting that we already have some very distinct verbs for this
sort of thing based on data type (compact => metadata, garbage-collect
=> disk, compress => data).
2026-01-09 00:01:05 -06:00
Christopher Haster 8233ac9dfe Renamed RELOOKAHEAD -> LOOKAHEAD, REGBMAP -> LOOKGBMAP
Yeah, after using these for a bit, the RE* names were not great.

Trying LOOK* now, as an alternative that hopefully still implies the
similar behavior without needing an additional prefix for LOOKAHEAD:

- LFS3_*_RELOOKAHEAD        -> LFS3_*_LOOKAHEAD
- LFS3_*_REGBMAP            -> LFS3_*_LOOKGBMAP
- cfg.regbmap_thresh        -> cfg.lookgbmap_thresh
- cfg.gc_relookahead_thresh -> cfg.gc_lookahead_thresh
- cfg.gc_regbmap_thresh     -> cfg.gc_lookgbmap_thresh
2025-11-13 16:14:56 -06:00
Christopher Haster 3ab7ecb2b0 Renamed file_cache -> fcache and gbmap_re -> regbmap
This walks back some of the attempt at strict object namespacing in
struct lfs3_cfg:

- cfg.file_cache_size  -> cfg.fcache_size
- filecfg.cache_size   -> filecfg.fcache_size
- filecfg.cache_buffer -> filecfg.fcache_buffer
- cfg.gbmap_re_thresh  -> cfg.regbmap_thresh

Motivation:

- cfg.regbmap_thresh now matches cfg.gc_regbmap_thresh, instead of using
  awkwardly different namespacing patterns.

- Giving fcache a more unique name is useful for discussion. Having
  pcache, rcache, and then file_cache was a bit awkward.

  Hopefully it's also more clear that cfg.fcache_size and
  filecfg.fcache_size are related.

- Config in struct lfs3_cfg is named a bit more consistently, well, if
  you ignore gc_*_* options.

- Less typing.

Though this gets into pretty subjective naming territory. May revert
this if the new terms are uncomfortable after use.
2025-10-24 00:18:54 -05:00
Christopher Haster b49d9e9ece Renamed REPOP* -> RE*
So:

- cfg.gc_repoplookahead_thresh -> cfg.gc_relookahead_thresh
- cfg.gc_repopgbmap_thresh     -> cfg.gc_regbmap_thresh
- cfg.gbmap_repop_thresh       -> cfg.gbmap_re_thresh
- LFS3_*_REPOPLOOKAHEAD        -> LFS3_*_RELOOKAHEAD
- LFS3_*_REPOPGBMAP            -> LFS3_*_REGBMAP

Mainly trying to reduce the mouthful that is REPOPLOOKAHEAD and
REPOPGBMAP.

As a plus this also avoids potential confusion of "repop" as a push/pop
related operation.
2025-10-24 00:16:37 -05:00
Christopher Haster 12874bff76 gbmap: Added gc_repoplookahead_thresh and gc_repopgbmap_thresh
To allow relaxing when LFS3_I_REPOPLOOKAHEAD and LFS3_I_REPOPGBMAP will
be set, potentially reducing gc workload after allocating only a couple
blocks.

The relevant cfg comments have quite a bit more info.

Note -1 (not the default, 0, maybe we should explicitly flip this?)
restores the previous functionality of setting these flags on the first
block allocation.

---

Also tweaked gbmap repops during gc/traversals to _not_ try to repop
unless LFS3_I_REPOPGBMAP is set. We probably should have done this from
the beginning since repopulating the gbmap writes to disk and is
potentially destructive.

Adds code, though hopefully we can claw this back with future config
rework:

                 code          stack          ctx
  before:       37176           2352          684
  after:        37208 (+0.1%)   2352 (+0.0%)  688 (+0.6%)

                 code          stack          ctx
  gbmap before: 40024           2368          848
  gbmap after:  40120 (+0.2%)   2368 (+0.0%)  856 (+0.9%)
2025-10-23 23:56:50 -05:00
Christopher Haster 1dc1a26f11 gc: Added LFS3_GC_ALL to make running all gc work easier
This is an alias for all possible gc work, which is a bit more
complicated than you might think due to compile-time features (example:
LFS3_GC_REPOPGBMAP).

The intention is to make loops like the following easy to write:

  struct lfs3_fsinfo fsinfo;
  lfs3_fs_stat(&lfs3, &fsinfo) => 0;

  lfs3_trv_t trv;
  lfs3_trv_open(&lfs3, &trv, fsinfo.flags & LFS3_GC_ALL) => 0;
  ...

It's possible to do this by explicitly setting all gc flags, but that
requires quite a bit of knowledge from the user.

Another option is allowing -1 for gc/traversal flags, but that loses
assert protection against unknown/misplaced flags.

---

This raises more questions about the prefix naming: it feels a bit weird
to take LFS3_I_* flags, mask with LFS3_GC_* flags, and pass them as
LFS3_T_* flags, but it gets the job done.

Limiting LFS3_GC_ALL to the LFS3_GC_* namespace avoids issues with
opt-out/mode flags such as LFS3_T_RDONLY, LFS3_T_MTREEONLY, etc. For
this reason it probably doesn't make sense to add something similar to
the other namespaces.
2025-10-23 23:55:54 -05:00
Christopher Haster 1f824a029b Renamed LFS3_T_COMPACT -> LFS3_T_COMPACTMETA (and gc_compactmeta_thresh)
- LFS3_T_COMPACT -> LFS3_T_COMPACTMETA
- gc_compact_thresh -> gc_compactmeta_thresh

And friends:

  LFS3_M_COMPACTMETA   0x00000800  Compact metadata logs
  LFS3_GC_COMPACTMETA  0x00000800  Compact metadata logs
  LFS3_I_COMPACTMETA   0x00000800  Filesystem may have uncompacted metadata
  LFS3_T_COMPACTMETA   0x00000800  Compact metadata logs

---

This does two things:

1. Highlights that LFS3_T_COMPACTMETA only interacts with metadata logs,
   and has no effect on data blocks.

2. Better matches the verb+noun names used for other gc/traversal flags
   (REPOPGBMAP, CKMETA, etc).

It is a bit more of a mouthful, but I'm not sure that's entirely a bad
thing. These are pretty low-level flags.
2025-10-23 23:54:57 -05:00
Christopher Haster ced63a4c73 Renamed inline_size -> shrub_size
There's a strong argument for naming this inline_size as that's more
likely what users expect, but shrub_size is just the more correct name
and avoids confusion around having multiple names for the same thing.

It also highlights that shrubs in littlefs3 are a bit different than
inline files in littlefs2, and that this config also affects large files
with a shrubbed root.

May rerevert this in the future, but probably only if there is
significant user confusion.
2025-10-23 23:53:02 -05:00
Christopher Haster 3b4e1e9e0b gbmap: Renamed gbmap_rebuild_thresh -> gbmap_repop_thresh
And tweaked a few related comments.

I'm still on the fence with this name, I don't think it's great, but it
at least betters describes the "repopulation" operation than
"rebuilding". The important distinction is that we don't throw away
information. Bad/erased block info (future) is still carried over into
the new gbmap snapshot, and persists unless you explicitly call
rmgbmap + mkgbmap.

So, adopting gbmap_repop_thresh for now to see if it's just a habit
thing, but may adopt a different name in the future.

As a plus, gbmap_repop_thresh is two characters shorter.
2025-10-23 23:51:18 -05:00
Christopher Haster cb9bda5a94 gbmap: Renamed gbmap_scan_thresh -> gbmap_rebuild_thresh
I think a good rule of thumb is if you refer to some variable/config/
field with a different name in comments/writing/etc more often than not,
you should just rename the variable/config/field to match.

So yeah, gbmap_rebuild_thresh controls when the gbmap is rebuilt.

Also touched up the doc comment a bit.
2025-10-09 14:33:27 -05:00
Christopher Haster 9b4ee982bc gbmap: Tried to adopt the gbmap name more consistently
Having gbmap/bmap used in different places for the same thing was
confusing. Preferring gbmap as it is consistent with other gstate (grm
queue, gcksums), even if it is a bit noisy.

It's interesting to note what didn't change:

- The BM* range tags: LFS3_TAG_BMFREE, etc. These already differs from
  the GBMAP* prefix enough, and adopting GBM* would risk confusion for
  actual gstate.

- The gbmap revdbg string: "bb~r". We don't have enough characters for
  anything else!

- dbgbmap.py/dbgbmapsvg.py. These aren't actually related to the gbmap,
  so the name difference is a good thing.
2025-10-09 14:33:27 -05:00
Christopher Haster 14d0c4121c bmap: Dropped treediff buffers for now
We're not currently using these (at the moment it's unclear if the
original intention behind the treediff algorithms is worth pursuing),
and they are showing up in our heap benchmarks.

The good news is that means our heap benchmarks are working.

Also saves a bit of code/ctx in bmap mode:

                code          stack          ctx
  before:      37024           2352          684
  after:       37024 (+0.0%)   2352 (+0.0%)  684 (+0.0%)

                code          stack          ctx
  bmap before: 38752           2456          812
  bmap after:  38704 (-0.1%)   2456 (+0.0%)  800 (-1.5%)
2025-10-01 17:57:42 -05:00
Christopher Haster 316ca1cc05 bmap: The initial bmapcache algorithm seems to be working
At least at a proof-of-concept level, there's still a lot of cleanup
needed.

To make things work, lfs3_alloc_ckpoint now takes an mdir, which
provides the target for gbmap gstate updates.

When the bmap is close to empty (configurable via bmap_scan_thresh), we
opportunistically rebuild it during lfs3_alloc_ckpoints. The nice thing
about lfs3_alloc_ckpoint is we know the state of all in-flight blocks,
so rebuilding the bmap just requires traversing the filesystem + in-RAM
state.

We might still fall back to the lookahead buffer, but in theory a well
tuned bmap_scan_thresh can prevent this from becoming a bottleneck (at
the cost of more frequent bmap rebuilds).

---

This is also probably a good time to resume measuring code/ram costs,
though it's worth repeating the above note about the bmap work still
needing cleanup:

             code          stack          ctx
  before:   36840           2368          684
  after:    36920 (+0.2%)   2368 (+0.0%)  684 (+0.0%)

Haha, no, the bmap isn't basically free, it's just an opt-in features.
With -DLFS3_YES_BMAP=1:

             code          stack          ctx
  no bmap:  36920           2368          684
  yes bmap: 38552 (+4.4%)   2472 (+4.4%)  812 (+18.7%)
2025-10-01 17:56:14 -05:00
Christopher Haster 88180b6081 bmap: Initial scaffolding for on-disk block map
This is pretty exploratory work, so I'm going to try to be less thorough
in commit messages until the dust settles.

---

New tag for gbmapdelta:

  LFS3_TAG_GBMAPDELTA   0x0104  v--- ---1 ---- -1rr

New tags for in-bmap block types:

  LFS3_TAG_BMRANGE      0x033u  v--- --11 --11 uuuu
  LFS3_TAG_BMFREE       0x0330  v--- --11 --11 ----
  LFS3_TAG_BMINFLIGHT   0x0331  v--- --11 --11 ---1
  LFS3_TAG_BMINUSE      0x0332  v--- --11 --11 --1-
  LFS3_TAG_BMBAD        0x0333  v--- --11 --11 --11
  LFS3_TAG_BMERASED     0x0334  v--- --11 --11 -1--

New gstate decoding for gbmap:

  .---+- -+- -+- -+- -. cursor: 1 leb128  <=5 bytes
  | cursor            | known:  1 leb128  <=5 bytes
  +---+- -+- -+- -+- -+ block:  1 leb128  <=5 bytes
  | known             | trunk:  1 leb128  <=4 bytes
  +---+- -+- -+- -+- -+ cksum:  1 le32    4 bytes
  | block             | total:            23 bytes
  +---+- -+- -+- -+- -'
  | trunk         |
  +---+- -+- -+- -+
  |     cksum     |
  '---+---+---+---'

New bmap node revdbg string:

  vvv---- -111111- -11---1- -11---1-  (62 62 7e v0  bb~r)  bmap node

New mount/format/info flags (still unsure about these):

  LFS3_M_BMAPMODE     0x03000000  On-disk block map mode
  LFS3_M_BMAPNONE     0x00000000  Don't use the bmap
  LFS3_M_BMAPCACHE    0x01000000  Use the bmap to cache lookahead scans
  LFS3_M_BMAPSLOW     0x02000000  Use the slow bmap algorithm
  LFS3_M_BMAPFAST     0x03000000  Use the fast bmap algorithm

New gbmap wcompat flag:

  LFS3_WCOMPAT_GBMAP  0x00002000  Global block-map in use
2025-10-01 17:55:13 -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 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 6eba1180c8 Big rename! Renamed lfs -> lfs3 and lfsr -> lfs3 2025-05-28 15:00:04 -05:00
Christopher Haster bf00c4d427 Limited FRAGMENT_SIZE to 512 bytes in the test/bench runners
This prevents runaway O(n^2) behavior on devices with extremely large
block sizes (NAND, bs=~128KiB - ~1MiB).

The whole point of shrubs is to avoid this O(n^2) runaway when inline
files become necessarily large. Setting FRAGMENT_SIZE to a factor of the
BLOCK_SIZE humorously defeats this.

The 512 byte cutoff is somewhat arbitrary, it's the natural BLOCK_SIZE/8
FRAGMENT_SIZE on most NOR flash (bs=4096), but it's probably worth
tuning based on actual device performance.
2025-05-15 17:19:35 -05:00
Christopher Haster c5efe35ab2 Split crystal_thresh into crystal_thresh + fragment_thresh
So now crystal_thresh only controls when fragments are compacted into
blocks, while fragment_thresh controls when blocks are broken into
fragments. Setting fragment_thresh=-1 will follow crystal_thresh and
keeps the previous behavior.

These were already two separate pieces of logic, so it makes sense to
provide two separate knobs for tuning.

Setting fragment_thresh lower than crystal_thresh has some potential to
reduce hysteresis in cases where random writes push blocks close to
crystal_thresh. It will be interesting to explore this more when
benchmarking.

---

The additional config option adds a bit of code/ctx, but hopefully that
will go away in the future config rework:

           code          stack          ctx
  before: 35584           2480          636
  after:  35600 (+0.0%)   2480 (+0.0%)  640 (+0.6%)
2025-04-20 15:53:18 -05:00
Christopher Haster 19a23c7788 Renamed/reverted file->buffer -> file->cache
And the related config options:

- cfg->file_buffer_size -> cfg->file_cache_size
- file->cfg->buffer_size -> file->cfg->cache_size
- file->cfg->buffer -> file->cfg->cache_buffer

The original motivation to rename this to file->buffer was to better
align with what other filesystems call this, but I think this is a case
where internal consistency is more important than external consistency.

file->cache better matches lfs->pcache and lfs->rcache, and makes it
easier to read code involving both file->cache and other user-provided
buffers.

Keeping the upstream name also helps with continuity.
2025-02-13 16:02:46 -06:00
Christopher Haster bac2464b8f Renamed lfs->cfg->shrub_size -> lfs->cfg->inline_size
While I think shrub_size is probably the more correct name at a
technical level, inline_size is probably more what users expect and
doesn't require a deeper understanding of filesystem details.

The only risk is that users may think inline_size has no effect on large
files, when in fact it still controls how much of the btree root can be
inlined.

There's also the point that sticking with inline_size maintains
compatibility with both the upstream version and any future version that
has other file representations.

May revisit this, but renaming to lfs->cfg->inline_size for now.
2025-02-11 02:50:38 -06:00
Christopher Haster 6cd29bede2 Dropped lfs->cfg->inline_size
Now that we no longer have bmoss files, inline_size and shrub_size are
effectively the same thing.

We weren't using this, so no code change, but it does save a word of
ctx:

           code          stack          ctx
  before: 36280           2576          640
  after:  36280 (+0.0%)   2576 (+0.0%)  636 (-0.6%)
2025-02-11 02:50:38 -06:00
Christopher Haster 66f5fa152a emubd: Renamed LFS_EMUBD_POWERLOSS_NOOP -> LFS_EMUBD_POWERLOSS_ATOMIC
Mainly to avoid ambiguity with PROGNOOP/ERASENOOP and make it clear
emubd still simulates powerloss, but also because I think the name
sounds cooler.
2025-02-08 14:53:47 -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 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 acfae9e072 Extended lfsr_mount to accept mount flags
This has been a long-time coming, mount flags are just too useful for
configuring a filesystem at runtime.

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

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

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

  * Hypothetical

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

As for the new mount flags:

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

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

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

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

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

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

           code          stack
  before: 35880           2672
  after:  35932 (+0.1%)   2680 (+0.3%)
2024-07-17 20:39:31 -05:00
Christopher Haster fc486ca4f7 Reworked lfsr_fs_gc to be incremental
Thinking about use case a bit, most lfsr_fs_gc will be to perform
background work, and can benefit from being incremental.

We already support incremental gc and all the mess associated with
traversal invalidation via the traversal API, so we might as well expose
this through lfsr_fs_gc.

The main downside is that we need to store an lfsr_traversal_t object
somewhere, which is not exactly a cheap struct. I was originally
considering limiting incremental gc to the traversal API for this
reason, but I think the value add of an incremental lfsr_fs_gc is too
compelling... Though we really should add a compile-time option
(LFS_NO_GC? LFS_NO_INCRGC?) to allow users to opt-out of this RAM cost
if they're never going to call this function.

Oh, and lfs_t also becomes self-referential, which might become a
problem for higher-level language users...

---

The incremental behavior of lfsr_fs_gc can be controlled by the new
gc_steps config option. This allows more than one step to be performed
at a time, which may allow for more progress when intermixed with
write-heavy filesystem operations. Setting gc_steps=-1 performs a full
traversal every call, which guarantees always making some amount of
progress.

This adds a bit of code, since we now need to check for/resume existing
traversals. But the real cost is the added RAM to lfs_t, which is
unfortunately wasted if you never call lfsr_fs_gc:

          code           stack          lfs_t
  before: 35708           2672            164
  after:  35756 (+0.1%)   2672 (+0.0%)    296 (+80.5%)
2024-07-17 18:08:32 -05:00
Christopher Haster 4d06fc2e0e t: (Re)implemented gc_compact_thresh, at least over mdirs
lfs_fs_gc is still not reimplemented, but this is accessible through the
traversal API with LFS_T_COMPACT.

This is also the first traversal operation that can mutate the
filesystem, which brings its own set of problems:

- We need to set LFS_F_DIRTY in lfsr_mtree_gc now, which really
  highlights how much of a mess having two flag fields is...

  We do _not_ clobber in this case, since we assume lfsr_mtree_gc knows
  what it's doing.

- We can now commit to an mroot in the mroot chain outside of the normal
  mroot chain update logic.

  This is a bit scary, but should just work.

  The only issue so far is that we need to allow mdirs to follow the
  mroot during mroot splits if mid=-1, even if they aren't lfs_t's mroot
  mdir.

  This should now be decently tested with the new
  test_traversal_compact_* tests.

- It's easy for mtraversal's mdir and mtinfo's mdir to fall out of sync
  when mutating... Why do we have two of these?

The actual compaction itself is pretty straightforward: just mark as
unerased, eoff=-1, and call lfsr_mdir_commit with an empty commit. This
is now wrapped up in lfsr_mdir_compact.

Code changes:

           code          stack
  before: 34528           2640
  after:  34652 (+0.4%)   2640 (+0.0%)

Though the real hard part will be implementing gc_compact_thresh over
btree nodes...
2024-06-24 21:09:54 -05:00
Christopher Haster b4af52bc72 Implemented SOMEBITS/MOSTBITS emubd powerloss behavior
These emulate powerloss behavior where only some of the bits being
progged are actually progged if there is a powerloss. This behavior was
the original motivation for our ecksums/fcrcs, so it's good to have this
tested.

As a simplification, these only test the extremes:

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

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

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

Good news is testing so far suggests our ecksum design is sound.
2024-06-06 16:58:18 -05:00
Christopher Haster b5370d6001 Cherry-picked upstream out-of-order emubd testing
More information upstream (f2a6f45, fc2aa33, 7873d81), but this adds
LFS_EMUBD_POWERLOS_OOO for testing out-of-order block devices that
require sync to be called for things to serialize. It's a simple
implementation, just reverts the first write since last sync on
powerloss, but gets the job done.

Cherry-picking these changes required reverting emubd's scratch buffer,
but carrying around an extra ~block_size of memory isn't a big deal
here.
2024-05-30 13:26:16 -05:00
Christopher Haster 1ecb346cec Renamed fbuffer_size -> file_buffer_size 2024-05-30 11:52:07 -05:00
Christopher Haster c648f96dc5 Added check_progs for immediate prog validation
This configuration option enables the previous behavior of reading back
every prog to check that the data was written correctly.

Unfortunately, this brings a bit of baggage, thanks to our cache
interactions being more complicated now:

- We really want to reuse the rcache for prog validation, despite the
  cache performance implications. Unfortunately, we simply can't, thanks
  to the new bd utility functions tying up the rcache. lfsr_bd_cpy, for
  example, does not expect rcache to be invalidated between a read and
  prog, and if it is, things break (I may or may not have found this by
  experience).

  These bd utilities are valuable, so we really need some other way to
  validate our progs.

- Since we can't rely on the rcache, this leaves checksumming as the
  only option for validating progs. Checksumming isn't perfect, as there
  is a decent chance of false negatives, but to be honest it's probably
  good enough for anything that's not malicious.

- This also adds the new constraint that we need to be able to read back
  any prog into the pcache, which implies read_size <= prog_size. This
  constraint didn't exist when we could clobber our rcache, but this is
  not worth throwing away the new bd utilities. Not to mention
  clobbering our rcache could hurt cache performance.

  Why not make read_size <= prog_size conditional on check_progs?

  The main reason is convenience. One very compelling use case for
  check_progs is to help debug unknown filesystem/integration failures,
  buf if you can't enable check_progs without changing the filesystem
  configuration, you can't really rely on check_progs for debugging.

  This helps future proof what we expect from block devices, in case
  future error detection/correction mechanisms can benefit from our
  prog_size always being readable.

Code changes were not that significant, however there was a surprising
stack cost. This seems to be because lfsr_bd_read__ can now be called
from multiple places, causing it to no longer be inlined in
lfsr_bd_read_, costing a bit of stack for the additional function call:

  before: 33566           2624
  after:  33682 (+0.3%)   2640 (+0.6%)
2024-05-29 23:09:41 -05:00
Christopher Haster 9c9a409524 Added fuzz test attribute
This acts as a marker to indicate a fuzz test. It should reference a
define, usually SEED, that can be randomized to get interesting test
permutations.

This is currently unused, but could lead to some interesting uses such
as time-based fuzz testing. It's also just useful for inspecting the
tests (make test-list).
2024-05-28 12:44:44 -05:00
Christopher Haster 56b18dfd9a Reworked revision count logic a bit, block_cycles -> block_recycles
The original goal here was to restore all of the revision count/
wear-leveling features that were intentionally ignored during
refactoring, but over time a few other ideas to better leverage our
revision count bits crept in, so this is sort of the amalgamation of
that...

Note! None of these changes affect reading. mdir fetch strictly needs
only to look at the revision count as a big 32-bit counter to determine
which block is the most recent.

The interesting thing about the original definition of the revision
count, a simple 32-bit counter, is that it actually only needs 2-bits to
work. Well, three states really: 1. most recent, 2. less recent, 3.
future most recent. This means the remaining bits are sort of up for
grabs to other things.

Previously, we've used the extra revision count bits as a heuristic for
wear-leveling. Here we reintroduce that, a bit more rigorously, while
also carving out space for a nonce to help with commit collisions.

Here's the new revision count breakdown:

  vvvvrrrr rrrrrrnn nnnnnnnn nnnnnnnn
  '-.''----.----''---------.--------'
    '------|---------------|---------- 4-bit relocation revision
           '---------------|---------- recycle-bits recycle counter
                           '---------- pseudorandom nonce

- 4-bit relocation revision

  We technically only need 2-bits to tell which block is the most
  recent, but I've bumped it up to 4-bits just to be safe and to make
  it a bit more readable in hex form.

- recycle-bits recycle counter

  A user configurable counter, this counter tracks how many times a
  metadata block has been erased. When it overflows we return the block
  to the allocator to participate in block-level wear-leveling again.
  This implements our copy-on-bounded-write strategy.

- pseudorandom nonce

  The remaining bits we fill with a pseudorandom nonce derived from the
  filesystem's prng. Note this prng isn't the greatest (it's just the
  xor of all mdir cksums), but it gets the job done. It should also be
  reproducible, which can be a good thing.

  Suggested by ithinuel, the addition of a nonce should help with the
  commit collision issue caused by noop erases. It doesn't completely
  solve things, since we're only using crc32c cksums not collision
  resistant cryptographic hashes, but we still have the existing
  valid/perturb bit system to fall back on.

When we allocate a new mdir, we want to zero the recycle counter. This
is where our relocation revision is useful for indicating which block is
the most recent:

  initial state: 10101010 10101010 10101010 10101010
                 '-.'
                  +1     zero           random
                   v .----'----..---------'--------.
  lfsr_rev_init: 10110000 00000011 01110010 11101111

When we increment, we increment recycle counter and xor in a new nonce:

  initial state: 10110000 00000011 01110010 11101111
                 '--------.----''---------.--------'
                         +1              xor <-- random
                          v               v
  lfsr_rev_init: 10110000 00000111 01010100 01000000

And when the recycle counter overflows, we relocate the mdir.

If we aren't wear-leveling, we just increment the relocation revision to
maximize the nonce.

---

Some other notes:

- Renamed block_cycles -> block_recycles.

  This is intended to help avoid confusing block_cycles with the actual
  physical number of erase cycles supported by the device.

  I've noticed this happening a few times, and it's unfortunately
  equivalent to disabling wear-leveling completely. This can be improved
  with better documentation, but also changing the name doesn't hurt.

- We now relocate both blocks in the mdir at the same time.

  Previously we only relocated one block in the mdir per recycle. This
  was necessary to keep our threaded linked-list in sync, but the
  threaded linked-list is now no more!

  Relocating both blocks is simpler, updates the mtree less often,
  compatible with metadata redundancy, and avoids aliasing issues that
  were a problem when relocating one block.

  Note that block_recycles is internally multiplied by 2 so each block
  sees the correct number of erase cycles.

- block_recycles is now rounded down to a power-of-2.

  This makes the counter logic easier to work with and takes up less RAM
  in lfs_t. This is a rough heuristic anyways.

- Moved the lfs->seed updates into lfsr_mountinited + lfsr_mdir_commit.

  This avoids readonly operations affecting the seed and should help
  reproducibility.

- Changed rev count in dbg scripts to render as hex, similar to cksums.

  Now that we using most of the bits in the revision count, the decimal
  version is, uh, not helpful...

Code changes:

           code          stack
  before: 33342           2640
  after:  33434 (+0.3%)   2640 (+0.0%)
2024-05-22 18:49:05 -05:00
Christopher Haster 5c70013c11 Adopted compile-time LFS_MIN/LFS_MAX in test defines
These seem fitting here, even if the test defines aren't "real defines".
The duplicate expressions should still be side-effect free and easy to
optimize out.

This should also avoid future lfs_min32 vs intmax_t issues.
2024-05-22 15:43:46 -05:00
Christopher Haster 186fd1b5f2 Separated cache_size out into rcache_size/pcache_size/fbuffer_size
A much requested feature, this allows much finer control of how RAM is
allocated for the system.

It was difficult to introduce this in previous versions of littlefs due
to how we steal caches during certain file operations, but now we don't
do that and treat the caches much more transparently.

Managing separate cache sizes does add a bit of code, but this is well
worth the potential for RAM savings due to increased flexibility:

           code          stack
  before: 33656           2632
  after:  33714 (+0.2%)   2640 (+0.3%)

Also interesting to note this reduces alignment requirements for the
rcache/pcache, since they don't need to share alignment, and completely
removes any alignment requirement from the file buffers.
2024-05-22 15:43:10 -05:00
Christopher Haster f5beacf6ee Added some comments over lfs_config's fragment_size/crystal_thresh/etc
Also added related asserts to lfs_init.

Note the fragment_size <= block_size/8 limit is to avoid wasteful corner
cases where only one fragment can fit in a block. The shrub_size <=
block_size/4 limit is looser because of how shrubs temporarily
overcommit.

As for the other limits, inline_size is bounded by shrub_size, and
crystal_thresh technically doesn't have a limit, though values >
block_size stop having an effect.
2024-05-18 13:00:15 -05:00
Christopher Haster a124ee54e7 Reworked test/bench defines to map to global variables
Motivation:

- Debuggability. Accessing the current test/bench defines from inside
  gdb was basically impossible for some dumb macro-debug-info reason I
  can't figure out.

  In theory, GCC provides a .debug_macro section when compiled with -g3.
  I can see this section with objdump --dwarf=macro, but somehow gdb
  can't seem to find any definitions? I'm guess the #line source
  remapping is causing things to break somehow...

  Though even if macro-debugging gets fixed, which would be valuable,
  accessing defines in the current test/bench runner can trigger quite
  a bit of hidden machinery. This risks side-effects, which is never
  great when debugging.

  All of this is quite annoying because the test/bench defines is
  usually the most important piece of information when debugging!

  This replaces the previous hidden define machinery with simple global
  variables, which gdb can access no problem.

- Also when debugging we no longer awkwardly step into the test_define
  function all the time!

- In theory, global variables, being a simple memory access, should be
  quite a bit faster than the hidden define machinery. This does matter
  because running tests _is_ a dev bottleneck.

  In practice though, any performance benefit is below the noise floor,
  which isn't too surprising (~630s +-~20s).

- Using global variables for defines simplifies the test/bench runner
  quite a bit.

  Though some of the previous complexity was due to a whole internal
  define caching system, which was supposed to lazily evaluate test
  defines to avoid evaluating defines we don't use. This all proved to
  be useless because the first thing we do when running each test is
  evaluate all defines to generate the test id (lol).

So now, instead of lazily evaluating and caching defines, we just
generate global variables during compilation and evaluate all defines
for each test permutation immediately before running.

This relies heavily on __attribute__((weak)) symbols, and lets the
linker really shine.

As a funny perk this also effectively interns all test/bench defines by
the address of the resulting global variable. So we don't even need to
do string comparisons when mapping suite-level defines to the
runner-level defines.

---

Perhaps the more interesting thing to note, is the change in strategy in
how we actually evaluate the test defines.

This ends up being a surprisingly tricky problem, due to the potential
of mutual recursion between our defines.

Previously, because our define machinery was lazy, we could just
evaluate each define on demand. If a define required another define, it
would lazily trigger another evaluation, implicitly recursing through
C's stack. If cyclic, this would eventually lead to a stack overflow,
but that's ok because it's a user error to let this happen.

The "correct" way, at least in terms of being computationally optimal,
would be to topologically sort the defines and evaluate the resulting
tree from the leaves up.

But I ain't got time for that, so the solution here is equal parts
hacky, simple, and effective.

Basically, we just evaluate the defines repeatedly until they stop
changing:

- Initially, mutually recursive defines may read the uninitialized
  values of their dependencies, and end up with some arbitrarily wrong
  result. But as the defines are repeatedly evaluated, assuming no
  cycles, the correct results should eventually bubble up the tree until
  all defines converge to the correct value.

- This is O(n*e) vs O(n+e), but our define graph is usually quite
  shallow.

- To prevent non-halting, we error after an arbitrary 1000 iterations.
  If you hit this, it's likely because there is a cycle in the define
  graph.

  This is runtime configurable via the new --define-depth flag.

- To keep things consistent and reproducible, we zero initialize all
  defines before the first evaluation.

  I don't think this is strictly necessary, but it's important for the
  test runner to have the exact same results on every run. No one wants
  a "works on my machine" situation when the tests are involved.

Experimentation shows we only need an evaluation depth of 2 to
successfully evaluate the current set of defines:

  $ ./runners/test_runner --list-defines --define-depth=2

And any performance impact is negligible (~630s +-~20s).
2024-02-13 18:59:58 -06:00