Commit Graph

753 Commits

Author SHA1 Message Date
Christopher Haster 484b7dd1e8 scripts: csv.py: Ignore missing by fields in enumerate/accumulate
Note this matches the behavior of mods, e.g. I would expect this to not
break if ORDER is missing:

  ./scripts/csv.py \
      -bcase='%(case)s+%(probe)s+%(ORDER)s' \
      -ft=accumulate(bench_simtime, case, probe, ORDER)

Normally the expr compiler would force typechecking of ORDER, giving it
a default value of int(0) if missing, but we intentionally bypass
typechecking in enumerate/accumulate's by fields since they may be
strings.
2026-02-19 14:04:10 -06:00
Christopher Haster 8a35b9870b scripts: Tweaked table renderer to not hide conflicting results
I think this is currently only possible with overlapping by/field
fields, but hiding results with conflicting by fields is not ideal.
Especially since this function is central to so many scripts:

  cat test.csv
  a,b,c
  x,2,1
  x,1,2
  x,1,3

Before:

  ./scripts/csv.py test.csv -ba -bb -fb -fc
  warning: by fields are unstable
  a,b            b        c
  x,2            2        1
  TOTAL          4        6

After:

  ./scripts/csv.py test.csv -ba -bb -fb -fc
  a,b            b        c
  x,2            2        5
  x,2            2        1
  TOTAL          4        6

This solves the main issue with unstable by fields, so no more warning.

Note that some features rely on by being unique to work (added/removed
numbers, compare fields, etc). They shouldn't error, but may be
incorrect/unintuitive with conflicting by fields, so avoiding
conflicting by fields is still a good idea.
2026-02-19 14:01:35 -06:00
Christopher Haster a3082437df scripts: Relaxed lost results due to unstable by fields to a warning
So it turns out this _can_ happen, without an in-script coding error.

Consider the behavior of a script with overlapping by/field fields:

  $ cat test.csv
  a,b
  x,2
  x,1
  x,1
  $ ./scripts/csv.py test.csv -ba -bb -fb

During the first fold, rows 2 and 3 will contain b=1, but during the
second fold they will have been merged, resulting in b=2.

So, relaxing to a warning for now. Maybe the table renderer should be
rewritten to avoid folding? (note diffing results may be tricky)
2026-02-19 13:59:32 -06: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 85b7a48df7 scripts: csv.py: Added bounded examples to -l/--list-fields
Now -l/--list-fields includes however many results fit in 36 chars:

  $ ./scripts/csv.py --list-fields test.csv
  i              int    # 16,17,14,18,19,15,20,13,12,29,27,28,...
  suite          ?      # bench_p26_wt
  case           ?      # bench_p26_wt_linear,bench_p26_wt_ran...
  NO_FRUNCATE    int    # 0
  SIZE           int    # 2097152
  SEED           int    # 42

The whole point of -l/--list-fields is to give a quick information dump
about what's inside a csv file, and we're already parsing everything to
try to figure out types, so why not?

Much easier to read than head:

  $ head -n5 test.csv
  i,suite,case,NO_FRUNCATE,SIZE,SEED,BLOCK_SIZE,FILE_SIZE,SIM_...
  16,bench_p26_wt,bench_p26_wt_linear,0,2097152,42,65536,64,36...
  16,bench_p26_wt,bench_p26_wt_linear,0,2097152,42,65536,64,36...
  16,bench_p26_wt,bench_p26_wt_linear,0,2097152,42,65536,64,36...
  16,bench_p26_wt,bench_p26_wt_linear,0,2097152,42,65536,64,36...
2026-02-19 13:35:21 -06:00
Christopher Haster efde754f88 scripts: csv.py: Optional by fields for unique enumerates/accumulates
This extends csv.py's enumerate/accumulate exprs with optional by field
arguments. Each set of by fields gets its own state, allowing multiple
parallel enumerates/accumulates to be processed simultaneously.

This is especially useful when the number of by field sets is unknown.
In theory you could split/merge each by field set with a separate csv.py
call, but it'd be a real pain.

Consider some bench results:

  case,n,simtime
  bench_rbyd,1,100
  bench_rbyd,2,10
  bench_rbyd,3,100
  bench_btree,1,200
  bench_rbyd,4,10
  bench_btree,2,20
  bench_btree,3,2000
  bench_btree,4,200

It was a bit awkward to handle these with csv.py's accumulate, as
accumulate operated strictly per-row, ignoring the case field.

But now with optional by fields:

  $ ./scripts/csv.py test.csv \
        -bcase -bn \
        -fsimtime='accumulate(simtime, case)'
  case,n           simtime
  bench_btree,1        200
  bench_btree,2        220
  bench_btree,3       2220
  bench_btree,4       2420
  bench_rbyd,1         100
  bench_rbyd,2         110
  bench_rbyd,3         210
  bench_rbyd,4         220
  TOTAL               5700

Note that these by fields are a bit special in csv.py's grammar. So far,
they are the only fields in field exprs that aren't typechecked. The
alternative would be string types in csv.py, but I'm not sure I want to
go that far.

---

It's tempting to try to invert this logic (accumulate(simtime, n)), but
I'm not sure how it would work internally. The duplicate by fields
("case") do get annoying, but specifying them in the expr helps make the
relevant state explicit.

Keep in mind we don't evaluate the actual by fields until much later in
csv.py. Entangling these stages risks confusion (-ba='%(b)s'
-c='enumerate(n)'? hidden by fields? overlapping by+field fields?).
2026-02-19 13:07:08 -06:00
Christopher Haster cf7e0e3fef scripts: csv.py: Tweaked foldchecking to check that folds match
I mean, what would you expect this to do?

  max(a) + sum(b)

Whatever your answer is, it's wrong (the way csv.py works, we always
compute folds after expr evaluation). The best option is to error,
matching the behavior of mismatched types.
2026-02-19 13:07:01 -06:00
Christopher Haster 9a224a1c52 scripts: csv.py: Fixed incorrect fold type when type changes
csv.py's -L/--list-computed was returning some confusing types:

  $ ./scripts/csv.py /dev/null -fa='float(1)' -L
          a  int  sum
              ^-- huh!?

Turns out csv.py's fold typechecking was all broken. Folds can change
the type, but only at the invocation:

  $ ./scripts/csv.py /dev/null -fa='sum(float(1))' -L
          a  int  sum
  $ ./scripts/csv.py /dev/null -fa='avg(int(1))' -L
          a  float  avg
  $ ./scripts/csv.py /dev/null -fa='int(avg(1))' -L
          a  float  avg

This is maybe defensible for explicit folds, since their evaluation is
also lifted, but not so much for things like literals/fields/etc.

---

Fixed by allowing None to indicate a generic fold, and allowing types to
be lazily figured out in csv.compile.
2026-02-19 13:00:35 -06:00
Christopher Haster d37785cc9b scripts: test/bench.py: Sped up simple suite/case filters
By "simple" I mean any non-globbing suite/case ids.

Non-globbing suite/case ids can be filtered early in the test_runner.
But globbing ids require, surprise, globbing, which is currently handled
by test/bench.py.

---

This greatly speeds up valgrind testing of specific suites/cases,
otherwise things get bogged down during the initial --list-cases due to
the sheer number of test permutations we've accumulated.

Maybe we shouldn't be running the initial --list-cases under Valgrind,
but oh well. This mostly solves the problem without too many changes.
2026-02-19 12:39:01 -06:00
Christopher Haster 23ac67bf34 Prefer power-loss -> powerloss
Just trying to be a bit more consistent.
2026-02-13 13:56:20 -06:00
Christopher Haster f07ed90a63 runners: bench: Renamed bench m -> probe
This needed a different name, and "bench probe" is sort of reminiscent
of the "debug probes" you can use to measure things in the real world.

Maybe this is just my embedded engineering background poking through,
but honestly anything is better than a single char m, especially for a
non-integer field.
2026-02-13 13:45:01 -06:00
Christopher Haster fe93d62523 scripts: csv.py: Added -l and -L shortform flags
These seem useful enough to have shortform flags:

- -l/--list-fields - Input fields before processing
- -L/--list-computed - Computed fields and expr dependencies

Note while -L/--list-computed has more information, it's also more
likely to trigger an assert/error due to poorly implemented field exprs.
2026-02-13 13:45:01 -06:00
Christopher Haster 6093fa79ac scripts: csv.py: Tweaked --list-computed to infer all input field types
On one hand, only inferring the used input fields is conceptually
correct because that's how csv.py works. On the other, it doesn't really
make sense for --list-computed to show _less_ information than
--list-fields.

So, showing all inferred types now:

  $ ./scripts/csv.py --list-computed test.csv \
        -bcase='%(case)s+%(m)s' \
        -fsimtime='float(bench_simtime)/1.0e9' \
        -fsimthroughput='float(n)/max(float(bench_simtime)/1.0e9,1.0e-9)'
  i              int   .-->  case           ?    ?
  suite          ?     |.->  simtime        int  sum
  case           ?    -+|.>  simthroughput  int  sum
  SKIP_WARMUP    int   |||
  FILE_SIZE      int   |||
  SEED           int   |||
  ...
  m              ?    -'||
  n              int  ---+
  bench_reads    int    ||
  bench_progs    int    ||
  bench_erases   int    ||
  bench_readed   int    ||
  bench_progged  int    ||
  bench_erased   int    ||
  bench_simtime  int  ---'

I think this makes --list-computed a strict superset of --list-fields
now.
2026-02-13 13:45:01 -06:00
Christopher Haster 60dec6b77d scripts: csv.py: Tweaked expr-less -F to still typecheck
I was expecting -ba -Fa to sort numerically, but it was not. Turns out
hidden field fields (-F/--hidden-field) without exprs were never
typechecked.

This is not an issue for non-hidden field fields (-f/--field), because
we typecheck these explicitly in compile.
2026-02-13 13:45:01 -06:00
Christopher Haster 49e3b22907 scripts: csv.py: Fixed bottleneck from overlapping by/from fields
Found from some confusing behavior when by/from fields overlap. It turns
out when this happens (-bhi -Fhi, for example), the generated getattr
for the by field would trigger the __getattribute__ for the overlapping
field field, resulting in a fold on _every add operation_.

Hopefully you can see where this is a bit of a problem when summing a
large number of results (O(n^2)?).

---

Fixed by switching getattr to object.__getattribute__ and reconsidering
csv.py's entire design.
2026-02-13 13:45:01 -06:00
Christopher Haster 04b536eb79 runners: bench: Dropped cumulative results
Now that we have csv.py's accumulate(), this information is strictly
redundant!

  $ ./scripts/csv.py test.csv \
        -bspecific_permutation_here \
        -fbench_creaded='accumulate(bench_readed)'

The point of adding accumulate() was to drop these. We really shouldn't
be doubling the size of the csvs with redundant/derivable data.
2026-02-13 13:45:01 -06:00
Christopher Haster 68de9efd17 scripts: csv.py: Added --list-computed to expose expr deps/types/etc
Less useful than --list-fields, but fun.

This shows more of the internal expr eval info: input fields + types,
output fields + types + folds, and a small dependency graph showing what
goes where:

  $ ./scripts/csv.py --list-computed test.csv \
        -bcase='%(case)s+%(m)s' \
        -fsimtime='float(bench_simtime)/1.0e9' \
        -fsimthroughput='float(n)/max(float(bench_simtime)/1.0e9,1.0e-9)'
  i              ?     .-->  case           ?    ?
  suite          ?     |.->  simtime        int  sum
  case           ?    -+|.>  simthroughput  int  sum
  SKIP_WARMUP    ?     |||
  FILE_SIZE      ?     |||
  SEED           ?     |||
  ...
  m              ?    -'||
  n              int  ---+
  bench_reads    ?      ||
  bench_progs    ?      ||
  bench_erases   ?      ||
  bench_readed   ?      ||
  bench_progged  ?      ||
  bench_erased   ?      ||
  bench_simtime  int  ---'

Maybe I was just itching to write another ascii-art renderer.
2026-02-13 13:00:22 -06:00
Christopher Haster 187f35df61 scripts: csv.py: Added --list-fields for quick field access
One issue I keep running into with csv.py is that it's difficult to get
started with a new/unfamiliar csv file.

csv.py itself doesn't know what to do until you start specifying fields,
but you can't start specifying fields until you know what fields there
are. Add to this the fact that our csv files have so much info shoved in
them that their "human readability" is mostly theoretical.

The --list-fields flag provides a quick solution to this:

  $ ./scripts/csv.py --list-fields test.csv
  i              int
  suite          ?
  case           ?
  SKIP_WARMUP    int
  FILE_SIZE      int
  SEED           int
  ...

csv.py doesn't have much info at this stage, but we can at least include
the best-effort type guessing we use for field exprs.
2026-02-13 12:41:02 -06:00
Christopher Haster 6c458b321c scripts: csv.py: Simplified -i/--enumerate to alias -bi -Fi=enumerate()
Now that we have the enumerate expr, -i/--enumerate can be implemented
entirely during expr eval:

- -i/--enumerate        => -bi -Fi=enumerate()
- -I/--hidden-enumerate => -Bi -Fi=enumerate()

Instead of internally reimplementing the same behavior.

This is what our help text implies, so might as well put our money where
our mouth is. And the less special internals we have, the better.

I considered removing -i/-I completely, but it's quite a convenient flag
when debugging csv.py expressions.
2026-02-10 17:33:01 -06:00
Christopher Haster ac338e66f0 scripts: csv.py: Added explicit z field, reusing -Z/--children
In an effort to move away from magic usage of -i/--enumerate, this adds
an explicit z field for differentiating -r/--hot results (and for normal
recursive results).

Instead of trying to think of a new flag to control this, this just
piggybacks on -Z/--children, which now accepts a tuple:

- ./scripts/csv.py -z3 -Z
- ./scripts/csv.py -z3 -Zchildren
- ./scripts/csv.py -z3 -Zz,children

The only tricky bit was needing to insert z in front of the by fields,
otherwise it was mostly a simplification from the enumerate mess.

Another positive side-effect: -r/--hot (and -z/--depth) now implies
-Zz,children, removing the annoying/confusing behavior of hotify folding
results by default.
2026-02-10 17:33:01 -06:00
Christopher Haster 078a1fb4c6 scripts: Adopted explicit underscore in Result._prefix
For consistency with the --prefix flag.

I confused myself while debugging some script behavior, and that's
no good.
2026-02-10 17:32:55 -06:00
Christopher Haster 98279a0b36 scripts: csv.py: Simplified --prefix, moved to collect_csv
The current... attempt at an approach was broken and becoming horribly
unmaintainable. Two issues found without even looking:

1. Field inference didn't understand prefixes, leading to duplicate
   by/field fields when attempting to infer by fields with --prefix.

2. Sort wasn't working for some reason, probably because they behavior
   of sort, defines, etc are really weird since they apply to both by
   fields and field fields.

I considered just dropping support for --prefix completely, this really
isn't worth the time, but instead found a simple solution of moving
prefix handling to one of the first steps in collect_csv.

This has the downside of creating conflicts when a prefixed/non-prefixed
field has the same name, but I don't care. --prefix is a niche flag that
shouldn't mess with the rest of the code like this, and none of the
other scripts really handle field conflicts correctly anyways.
2026-02-10 17:14:49 -06:00
Christopher Haster 695b1e94df scripts: csv.py: Fixed issues with non-default children/notes fields
- Fixed the initial filter using explicit 'children'/'notes' literals

  Whoops, how did this happen?

- Fixed fold using default children/notes result attributes

  This one is a bit more excusable, self.children is easy to overlook.
  But not actual string literals, that's silly.
2026-02-10 17:13:13 -06:00
Christopher Haster 84b2e73a30 scripts: csv.py: Added enumerate/accumulate exprs
This adds two new exprs to csv.py, useful for sequential data:

  enumerate()    A number incremented each result
  accumulate(a)  A running sum across results

To make these work required adding support for cross-row state, thus the
new state field in CsvExpr.Expr.eval.

Once you have that cross-row state, implementing enumerate/accumulate is
pretty straightforward. The only complication being that we need to hash
state by the unique Python id (`id(self)`), otherwise multiple exprs
would share state, which would be pretty weird.

Note that csv.py's pipeline is now quite complex, and stage order is
important!

  input --> define    --> expr --> folding --> sorting --> output
            filtering     eval

As a result, it's unfortunately not possible to organize enumerate/
accumulate by by fields. I poked around with the idea but decided it was
too complex (aren't I supposed be building a filesystem?). The guiding
principle behind csv.py is most problems can be solved with more process
substitution.

---

This is a bit clunky since we can't use the existing fold system, but
csv.py is already a pile of hacks, so what's one more?

The reason for the clunkiness is that the original idea behind csv.py
was to treat each folded row independently and order-agnostic. Not the
greatest idea in hindsight, cross-row operations are useful!
2026-02-10 16:52:58 -06:00
Christopher Haster 3978a32156 scripts: csv.py: Started adding -g/--accumulate
The idea here is to add some sort of accumulate operation to csv.py, so
we can stop cumulative-result clunkiness. It would also be immensely
useful as a general function, and -i/--enumerate already sets a
precedent for this sort of cross-row behavior.

But I'm starting to think using flags here is not the best way, maybe
this would be better as a field expr?
2026-02-10 16:52:11 -06:00
Christopher Haster 45f1850028 scripts: csv.py: Fixed missing -F/--hidden-field parsing
For some reason -F/--hidden-field fields weren't being parsed as a
CsvExpr, breaking any attempt to use exprs with hidden fields. Probably
just broken during a refactor.

Fortunately an easy fix.
2026-02-10 16:40:15 -06:00
Christopher Haster cfafd07414 make: Leaned into simtime in Makefile
The value of simtime isn't actually the simtime value, but the
simulated throughput, which is easy enough for our csv.py script to
calculate (with a daintily placed max to avoid divide-by-zero).

Throughput has the benefit of being somewhat size-agnostic, making
cross-benchmark comparisons easier.

I guess it's technically possible to do something similar with
readed/progged/erased numbers, but conceptually that would be really
confusing...

---

Also renamed test/bench_time -> test/bench_runtime to hopefully prevent
confusion between the two time spaces.
2026-02-10 16:27:07 -06:00
Christopher Haster d3dd927de3 runners: emubd/kiwibd: Adopted emulated simtime API
This is based on some work in external benchmarks. What's worked well
there is emulating a global simtime based on per-byte estimates.

This moves the emulated simtime into emubd/kiwibd, and extends the idea
with both per-byte and per-op timing estimates for hopefully more
realistic results.

---

The problem is how NAND flash reads work.

Per-byte timing estimates are surprisingly accurate for NOR flash. There
is some overhead for sending the address, but it's mostly dominated by
bus cost (~20ns/B [1]).

NAND flash, on the otherhand, technically does support byte-level reads,
but first needs to read into 2KiB buffer. Surprisingly, these are pretty
close in cost (~19ns/B bus [2] vs ~12ns/B buffer [2]).

This close-ness makes modeling NAND flash difficult. If we set
read_size=1, we risk hiding the cost of small reads, which littlefs3 is
full of (rbyd lookups). If we set read_size=2048, we unfairly penalize
littlefs3 for the same reason.

---

The solution here is to expose both per-byte and per-op timing
estimates. This lets you model NAND reads using two data points:

  ^
  |                                realtime --> ...............o
  |                                             :    .....'''' :
  |                              ...............:''''  ^       :
  |                              :....'''''            |       :
  |               ..........::::::                  simtime    :
  |          .....:''''                                        :
  |o....:::::.....:                                            :
  |:                                                           :
  |:                                                           :
  +:-----------------------------------------------------------:>
   min read                                              max read

Where:

  bus_timing = 19ns
  buffer_timing = 25us
  buffer_size = 2KiB
  erase_size = 128KiB

  min_read = buffer_timing
  max_read = (erase_size/buffer_size)*buffer_timing - buffer_timing
  read_timing = min_read
  readed_timing = ((max_read - min_read)/erase_size) + bus_timing

  simtime = reads*read_timing + readed*readed_timing
            (per-op)            (per-byte)

This should correctly penalize small reads without complicating
emubd/kiwibd too much.

That's the idea anyways! It will take some use to understand if this is
a reasonable approach.

As a plus, this is a superset of the per-byte model, so both can be used
for realistic vs idealistic simulations (and to test the bus+buffer
model itself).

1: https://www.winbond.com/resource-files/W25Q256JV%20SPI%20RevQ%2002072025%20Plus.pdf
2: https://www.winbond.com/resource-files/W25N01GV%20Rev%20R%20070323.pdf
2026-02-10 15:28:32 -06:00
Christopher Haster 35c09db971 scripts: test.py/bench.py: Some small tweaks
- Delayed defines/permutations assignment until after generation. Just a
  bit of code smell.

- Expanded all __eq__, __ne__, __lt__, __gt__, etc magic methods, just
  to minimize surprises in the future.
2026-01-09 00:03:48 -06:00
Christopher Haster 0c6e455961 scripts: test.py/bench.py: Allowed expressions in ifdefs/ifndefs
This extends our ifdef/ifndef test attributes to support more
complicated logic expressions.

So far we haven't really needed this (ifdef/ifndef accepts an implicitly
anded list, which has covered everything so far), but I realized there's
a simple trick to make this work.

For example, in test.toml:

  ifdef = 'A && !(B || C)'

Generated ifdef:

  #if (defined(A) && !(defined(B) || defined(C)))

This doesn't require complex parsing or anything, just a simple regex:

  s/[a-zA-Z_0-9]\+/defined(&)/g

Is using #if defined(A) everywhere instead of #ifdef A more expensive
for the compiler? Not sure. But it seems like we're heavily dominated by
the single-threaded link time, so I'm not sure we care.
2026-01-09 00:03:45 -06:00
Christopher Haster 5511c100ed scripts: dbgflags.py: Added -d/--diff, lineno, better find reuse
This started with adding -d/--diff support to dbgflags.py, which is very
useful for comparing flags during test failure.

The flag asserts in our tests generally look like this:

  tests/test_mount.toml:171:assert: assert failed with 33570064,
  expected eq 33570576
      assert(fsinfo.flags == (

Which can now be quickly compared with dbgflags.py:

  $ ./scripts/dbgflags.py +i 33570064 -d 33570576
   LFS3_I_GBMAP         0x02000000  Global on-disk block-map in use
   LFS3_I_REVPERTURB    0x00000010  Mounted with LFS3_M_REVPERTURB
   LFS3_I_MKCONSISTENT  0x00000100  Filesystem needs mkconsistent to write
  -LFS3_I_LOOKAHEAD     0x00000200  Lookahead buffer is not full
   LFS3_I_PREERASE      0x00000400  Blocks can be pre-erased
   LFS3_I_COMPACT       0x00000800  Filesystem may have uncompacted metadata
   LFS3_I_CKMETA        0x00001000  Metadata checksums not checked recently
   LFS3_I_CKDATA        0x00002000  Data checksums not checked recently

The assert print is a bit more annoying than it needs to be, as it only
prints in decimal. But, since our prettyasserts.py only works at the
syntax layer, it's not possible to make it any smarter.

---

To make this diffing work required a couple more features in our
self-parsing Flag class:

- Keep track of lineno, mainly for ordering things
- Moved find logic into a staticmethod on all classes
- Added _sentinel based defaults to find functions
- Allowed self to be non-class in line functions to deduplicate "Unknown
  flag" messages

I went ahead and extended these to the other self-parsing classes (Err
and Tag) in case they're useful in the future.
2026-01-09 00:03:33 -06:00
Christopher Haster b3ab83d5b5 Added REVPERTURB, reworked how we handle revision counts
The main change is adding LFS3_M_REVPERTURB, which will be necessary for
preerase allocations, but I got distracted and ended up giving the
revision count subsystem a bit of a refactor.

Main changes:

- Added LFS3_M_REVPERTURB, which ensures the leading bit in the
  revision count changes after each allocation/relocation/compaction.

  This is generally optional, but will be required for preerase
  allocations. Our ecksum system is only reliable if we ensure at least
  one bit changes, otherwise the chance of ecksum collision is very
  high.

  The downside of LFS3_M_REVPERTURB is that we need to read the contents
  of the new block to figure out what the bit should change to. Probably
  a minimal cost in the system, but still a good reason to make the
  behavior optional.

  Does LFS3_M_REVPERTURB have any use outside of preerased allocation?
  I'm not sure. Maybe it has some niche use reducing the chance of bd
  ECC collisions?

- Dropped LFS3_M_REVDBG, but adding low-effort debug bits that are
  always enabled.

  Making LFS3_M_REVDBG conditional was probably overkill. The flag
  checks probably cost more than the actual debug bits when enabled.

  Instead, replaced with a simpler, low-effort debug bit system, where
  we only set the debug bits during mdir allocation/relocation. These
  bits shouldn't change during normal compaction, but we _don't_
  introduce debug bits if mounting a filesystem from a driver without
  these debug bits.

- Restricted recycle counter to at most 20-bits to make space for
  things. This ensures perturb/debug bits don't get overwritten (though
  we really only care about perturb bits).

  2^20 (~1M) recycles is probably enough for any device littlefs will
  run on, especially considering the recycle_count should probably be
  several orders of magnitude smaller than the device's expected erase
  cycles.

  Worst case this can always be increased in the future without
  backwards incompatible changes. The only hard requirement for revision
  counts is that the full 32-bits are comparable.

- Simplified lfs3_rev_inc and friends, and moved most of the
  disk-dependent revision count stuff down into lfs3_rbyd appendrev.

  This deduplicates the messy revision count handling in
  lfs3_btree_commit_.

  Though note the implicit lfs3_rbyd_appendrev now defaults to writing
  the btree debug bits ('b'). A bit of a hack, but works for littlefs.

Here's the resulting encoding:

  vvvv---- -------- -------- -ddddddd
  vvvvrrrr rrrrrr-- -------- -ddddddd
  vvvvrrrr rrrrrrnn nnnnnnnn pddddddd
  '-.''----.----''----.----' ^'--.--'
    '------|----------|------|---|---- 4-bit relocation revision
           '----------|------|---|---- recycle-bits recycle counter
                      '------|---|---- pseudorandom noise (if revnoise)
                             '---|---- perturb bit (if revperturb)
                                 '---- low-effort debug bits
                              11-1---  - h = mroot anchor
                              11-11-1  - m = mdir
                              11---1-  - b = btree node

Note we store revision counts as le32s, so the perturb bit should end up
as the leading bit in the first byte.

Costs a bit more code (mostly because the debug bits are now
unconditional, even if low-effort), but simplifies the codebase:

                        code          stack          ctx
  before:              35124           2136          660
  after:               35144 (+0.1%)   2136 (+0.0%)  660 (+0.0%)
  after+yesrevperturb: 35192 (+0.2%)   2136 (+0.0%)  660 (+0.0%)

                        code          stack          ctx
  gbmap+np before:     38252           2144          776
  gbmap+np after:      38272 (+0.1%)   2144 (+0.0%)  776 (+0.0%)
  gbmap+np after+yrp:  38328 (+0.2%)   2144 (+0.0%)  776 (+0.0%)

                        code          stack          ctx
  gbmap+yp before:     38832           2168          796
  gbmap+yp after:      38852 (+0.1%)   2168 (+0.0%)  796 (+0.0%)
  gbmap+yp after+yrp:  38908 (+0.2%)   2168 (+0.0%)  796 (+0.0%)
2026-01-09 00:02:05 -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 4f22d8c591 scripts: dbgflags.py: Added support for flag aliases
Also tweaked related flag comments a bit.

---

This adds another internal flag modifier ('a') to dbgflags.py to
indicate a flag is an alias for multiple other flags.

These still show up in -l/--list and name searches:

  $ ./scripts/dbgflags.py -l +ck
  LFS3_CK_MKCONSISTENT  0x00000100  Make the filesystem consistent
  LFS3_CK_LOOKAHEAD     0x00000200  Repopulate lookahead buffer
  LFS3_CK_COMPACT       0x00000800  Compact metadata logs
  LFS3_CK_CKMETA        0x00001000  Check metadata checksums
  LFS3_CK_CKDATA        0x00002000  Check metadata + data checksums
  LFS3_CK_CK            0x00003000  Alias for all check work
  LFS3_CK_GC            0x00003b00  Alias for all gc work

But are hidden from value searches, as they would be redundant and the
specific low-level flags are probably more useful:

  $ ./scripts/dbgflags.py +ck 0x00003000
  LFS3_CK_CKMETA  0x00001000  Check metadata checksums
  LFS3_CK_CKDATA  0x00002000  Check metadata + data checksums
2026-01-09 00:01:29 -06:00
Christopher Haster d54fef8099 Reorganized traversal flags again
One nice thing about merging LOOKAHEAD + LOOKGBMAP, is now our core
traversal flags fit in a single byte. This is useful for organizing
things, especially so as the traversal flags seem to permeate into
basically every flag set.

The main change was to actually group these flags into a byte, which
helps readability and in theory could make some bulk accesses cheaper
(in practice I don't think we currently leverage this):

  T_MODE             0x00000001  ---- ---- ---- ---- ---- ---- ---- ---1
  T_RDONLY           0x00000000  ---- ---- ---- ---- ---- ---- ---- ----
  T_RDWR             0x00000001  ---- ---- ---- ---- ---- ---- ---- ---1
  T_MTREEONLY        0x00000002  ---- ---- ---- ---- ---- ---- ---- --1-
  T_EXCL             0x00000008  ---- ---- ---- ---- ---- ---- ---- 1---
  T_MKCONSISTENT     0x00000100  ---- ---- ---- ---- ---- ---1 ---- ----
  T_LOOKAHEAD        0x00000200  ---- ---- ---- ---- ---- --1- ---- ----
  T_PREERASE*        0x00000400  ---- ---- ---- ---- ---- -1-- ---- ----
  T_COMPACT          0x00000800  ---- ---- ---- ---- ---- 1--- ---- ----
  T_CKMETA           0x00001000  ---- ---- ---- ---- ---1 ---- ---- ----
  T_CKDATA           0x00002000  ---- ---- ---- ---- --1- ---- ---- ----
  T_REPAIRMETA*      0x00004000  ---- ---- ---- ---- -1-- ---- ---- ----
  T_REPAIRDATA*      0x00008000  ---- ---- ---- ---- 1--- ---- ---- ----

  t_EVICT*           0x00000010  ---- ---- ---- ---- ---- ---- ---1 ----
  t_TYPE             0xf0000000  1111 ---- ---- ---- ---- ---- ---- ----
  t_ZOMBIE           0x08000000  ---- 1--- ---- ---- ---- ---- ---- ----
  t_CKPOINTED        0x04000000  ---- -1-- ---- ---- ---- ---- ---- ----
  t_DIRTY            0x02000000  ---- --1- ---- ---- ---- ---- ---- ----
  t_STALE            0x01000000  ---- ---1 ---- ---- ---- ---- ---- ----
  t_BTYPE            0x00ff0000  ---- ---- 1111 1111 ---- ---- ---- ----

  * Planned

This gives btype a full byte as well, which is a bit overkill, but can
be reduced in the future if we run into traversal flag pressure.

This also pushes some future planned flags (DEDUP, COMPR, etc) into
higher-order bits, but that's not the end of the world.

Code changes basically nothing:

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

                 code          stack          ctx
  gbmap before: 38080           2136          776
  gbmap after:  38076 (-0.0%)   2136 (+0.0%)  776 (+0.0%)
2026-01-09 00:01:07 -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 347c7b7290 scripts: gdb: Forward +flags to dbg scripts
dbgflags.py now uses +flags to indicate flag namespaces, but our gdb
script only forwarded -f/--flags, which made dbgflags a bit of a pain
to use in the debugger!

Fortunately an easy fix.

Now this works:

  (gdb) dbgflags +t trv.gc.t.h.flags
  LFS3_T_RDWR          0x00000000  Open traversal as read and write
  LFS3_T_MKCONSISTENT  0x00000100  Make the filesystem consistent
  LFS3_T_LOOKAHEAD     0x00000200  Repopulate lookahead buffer
  LFS3_t_TRAVERSAL     0x60000000  Type = traversal
  LFS3_t_MDIR          0x00010000  Btype = mdir
2026-01-09 00:00:57 -06:00
Christopher Haster ffc565508a alloc: Merged LOOKAHEAD+LOOKGBMAP -> single LOOKAHEAD flag
Our flag space is already really packed, and I'm not sure having these
as separate flags is meaningful or useful for users. They both indicate
to repopulate allocators, and most users probably won't care that there
are two subtly different allocators operating under the hood.

There's an argument that LOOKAHEAD not touching disk is a useful
distinction, but in practice you really only need LOOKAHEAD work when
mounted RDWR.

So, merged the behaviors of LOOKAHEAD + LOOKGBMAP such that
LFS3_*_LOOKAHEAD requests repopulation of all allocators based on
gc_lookahead_thresh and gc_lookgbmap_thresh.

In priority order (some notes below):

1. If max(lookahead, gbmap) < gc_lookahead_thresh => repop lookahead
2. If gbmap < gc_lookgbmap_thresh                 => repop gbmap

As a plus, this makes it easier to avoid LFS3_IFDEF_GBMAP mess.

---

It's interesting to note LFS3_*_LOOKAHEAD will still repopulate the
lookahead buffer when the gbmap is present, but only if this would gain
more knowledge than was is currently in the gbmap.

I considered disabling lookahead scans completely when we have a gbmap,
but repopulating the lookahead buffer is still useful if the gbmap is at
risk of exhaustion. This is what gc_lookahead_thresh is for anyways, and
users can set gc_lookahead_thresh=0 if they want to disable this
behavior.

Relatedly, lookahead scans are actually prioritized over gbmap scans
(when they would gain knowledge). In theory this minimizes gc latency,
as gbmap scans risk triggering a full lookahead scan when building the
new gbmap.

---

Code changes minimal:

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

                 code          stack          ctx
  gbmap before: 38076           2136          776
  gbmap after:  38080 (+0.0%)   2136 (+0.0%)  776 (+0.0%)
2026-01-09 00:00:53 -06:00
Christopher Haster cd7dd37888 scripts: dbglfs3.py: Adopted % as littlefs root dir character
The idea is this is similar to ~ for the home directory, hopefully
simplifying littlefs path parsing in scripts:

- /hi -> hi in root dir
- ./hi -> hi in current dir
- ../hi -> hi in parent dir
- ~/hi -> hi in home dir
- %/hi -> hi in littlefs root dir

Note this only works when standalone:

- ~hi != ~/hi
- %hi != %/hi

And files named % can still be referenced with a ./ prefix:

- ./% -> % in current dir
- %/% -> % in littlefs root dir

---

This is probably overkill for dbglfs3.py, as the arg ordering was
already enough to disambiguate disk path vs mroot address vs littlefs
path, but eventually I think the idea will be useful for more powerful
scripts.

A hypothetical:

  $ mklfs3 cp disk -b4096 -r image_files %/image_files
2025-11-18 00:58:31 -06:00
Christopher Haster 0d5cdeaeb8 scripts: dbgflags.py: Make SEEK_MODE non-internal
This is still a hack to make the seek _enum_ appear somewhat readable in
our dbg _flags_ script. But the previously internal SEEK_MODE was
causing all seek flags to be hidden from -l/--list confusingly.
2025-11-18 00:58:31 -06:00
Christopher Haster 192206b66d scripts: dbgflags.py: Renamed --o -> +o for prefix namespaces
Just a bit less typing than --o, and lowers risk of conflicts with
actual flags we may care about.

To be honest I was procrastinating because I thought this would be a lot
more work! I was prepared to write a hacky secondary parser, but argparse
already supports this natively with prefix_chars='-+'. Yay!
2025-11-18 00:58:31 -06:00
Christopher Haster 9728cda682 runners: Renamed -a/--all -> --force
Test/bench filters have proven to be mostly non-optional, protecting
against bad configuration that doesn't make any sense.

It's still valid to want to override test filters sometimes, but using a
more, uh, forceful verb probably makes sense here.

The shortform would conflict with -f/--fail, so no shortform flag for
this, but some argue --force should never have a shortform flag anyways.
2025-11-18 00:58:31 -06:00
Christopher Haster efdcb912f5 scripts: Renamed -w/--wait -> -t/--wait
I'm trying to avoid the inevitable conflict with -w/--word, which will
probably become important when exploring non-32-bit filesystem
configurations.

Renaming this to -t/--wait still conflicts with -t/--tree and -t/--tiny,
but as a debug-only flag, I think these are less important.

Oh, and -t/--trace, but test.py/bench.py are already quite different in
their flag naming  (see -d/--disk vs -d/--diff).

---

Renamed a few other flags while tweaking things:

- -t/--tiny -> --tiny (dropped shortform)
- -w/--word-bits -> -w/--word/--word-bits
- -t/--tree -> -R/--tree/--rbyd/--tree-rbyd
- -R/--tree-rbyd -> -Y/--rbyd-all/--tree-rbyd-all
- -B/--tree-btree -> -B/--btree/--tree-btree

After tinkering with it a bit, I think the -R/-Y/-B set of flags are a
decent way to organize the tree renderers. At least --tree-rbyd-all does
a better job of describing the difference between --tree-rbyd and
--tree-rbyd-all.
2025-11-18 00:58:31 -06:00
Christopher Haster 9bc41099f0 scripts: Changed -~/--sleep -> -w/--wait to sleep after -k/--keep-open
This changes -w/--wait to sleep _after_ -k/--keep-open, instead of
including the time spent waiting on inotifywait in the sleep time.

1. It's easier, no need to keep track of when we started waiting.

2. It's simpler to reason about.

3. It trivially avoids the multiple wakeup noise that plagued
   watch.py + vim (vim likes to do a bunch of renaming and stuff when
   saving files, including the file 4913 randomly?)

   Avoiding this was previously impossible because -~/--sleep was
   effectively a noop when combined with -k/--keep-open.

---

Also renamed from -~/--sleep -> -w/--wait, which is a bit more intuitive
and avoids possible shell issues with -~.

To make this work, dropped the -w/--block-cycles shortform flag in
dbgtrace.py. It's not like this flag is ever used anyways.

Though at the moment this is ignoring the possible conflict with
-w/--word-bits...
2025-11-18 00:58:27 -06:00
Christopher Haster 7da44f12ae Added redund hints to more tags
Well, kinda. At the moment we don't have any reund support (it's a
TODO), so arguably redund=0 and this is just a comment tweak.

Though our mdirs _are_ already redund=1... so maybe these should
actually set redund=1?

It's unclear, so for now I've just tweaked the comment, and we should
probably revisit when _actually_ implementing meta/data redundancy.

---

Note this only really affects struct tags:

  LFS3_TAG_STRUCT         0x04tt  v--- -1-- +ttt tttt
  LFS3_TAG_BRANCH         0x040r  v--- -1-- +--- --rr
  LFS3_TAG_DATA           0x0404  v--- -1-- +--- -1rr
  LFS3_TAG_BLOCK          0x0408  v--- -1-- +--- 1err
  LFS3_TAG_DDKEY*         0x0410  v--- -1-- +--1 --rr
  LFS3_TAG_DID            0x0420  v--- -1-- +-1- ----
  LFS3_TAG_BSHRUB         0x0428  v--- -1-- +-1- 1-rr
  LFS3_TAG_BTREE          0x042c  v--- -1-- +-1- 11rr
  LFS3_TAG_MROOT          0x0431  v--- -1-- +-11 --rr
  LFS3_TAG_MDIR           0x0435  v--- -1-- +-11 -1rr
  LFS3_TAG_MSHRUB+        0x0438  v--- -1-- +-11 1-rr
  LFS3_TAG_MTREE          0x043c  v--- -1-- +-11 11rr
  LFS3_TAG_BMRANGE        0x044u  v--- -1-- +1-- ++uu
  LFS3_TAG_BMFREE         0x0440  v--- -1-- +1-- ----
  LFS3_TAG_BMINUSE        0x0441  v--- -1-- +1-- ---1
  LFS3_TAG_BMERASED       0x0442  v--- -1-- +1-- --1-
  LFS3_TAG_BMBAD          0x0443  v--- -1-- +1-- --11
  LFS3_TAG_DDRC*          0x0450  v--- -1-- +1-1 ----
  LFS3_TAG_DDPCOEFF*      0x0451  v--- -1-- +1-1 ---1
  LFs3_TAG_PCOEFFMAP*     0x0460  v--- -1-- +11- ----

This redund hint may be useful for debugging and the theoretical
CKMETAREDUND feature.
2025-11-18 00:58:18 -06:00
Christopher Haster cf34ba9aca Rearranged tag encodings, reserved suptype=0 for internal tags
This was motivated by a discussion with a gh user, in which it was noted
that not having a reserved suptype for internal tags risks potential
issues with long-term future tag compatibility.

I think the risk is low, but, without a reserved suptype, it _is_
possible for a future tag to conflict with an internal tag in an older
driver version, potentially and unintentionally breaking compatibility.
Note this is especially concerning during mdir compactions, where we
copy tags we may not understand otherwise.

In littlefs2 we reserved suptype=0x100, though this was mostly an
accident due to saturating the 3-bit suptype space. With the larger tag
space in littlefs3, the reserved suptype=0x100 was dropped.

---

Long story short, this reserves suptype=0 for internal flags (well, and
null, which is _mostly_ internal only, but does get written to disk as
unreachable tags).

Unfortunately, adding a new suptype _did_ require moving a bunch of
stuff around:

  LFS3_TAG_NULL           0x0000  v--- ---- +--- ----
  LFS3_TAG_INTERNAL       0x00tt  v--- ---- +ttt tttt

  LFS3_TAG_CONFIG         0x01tt  v--- ---1 +ttt tttt
  LFS3_TAG_MAGIC          0x0131  v--- ---1 +-11 --rr
  LFS3_TAG_VERSION        0x0134  v--- ---1 +-11 -1--
  LFS3_TAG_RCOMPAT        0x0135  v--- ---1 +-11 -1-1
  LFS3_TAG_WCOMPAT        0x0136  v--- ---1 +-11 -11-
  LFS3_TAG_OCOMPAT        0x0137  v--- ---1 +-11 -111
  LFS3_TAG_GEOMETRY       0x0138  v--- ---1 +-11 1---
  LFS3_TAG_NAMELIMIT      0x0139  v--- ---1 +-11 1--1
  LFS3_TAG_FILELIMIT      0x013a  v--- ---1 +-11 1-1-
  LFS3_TAG_ATTRLIMIT?     0x013b  v--- ---1 +-11 1-11

  LFS3_TAG_GDELTA         0x02tt  v--- --1- +ttt tttt
  LFS3_TAG_GRMDELTA       0x0230  v--- --1- +-11 ----
  LFS3_TAG_GBMAPDELTA     0x0234  v--- --1- +-11 -1rr
  LFS3_TAG_GDDTREEDELTA*  0x0238  v--- --1- +-11 1-rr
  LFS3_TAG_GPTREEDELTA*   0x023c  v--- --1- +-11 11rr

  LFS3_TAG_NAME           0x03tt  v--- --11 +ttt tttt
  LFS3_TAG_BNAME          0x0300  v--- --11 +--- ----
  LFS3_TAG_REG            0x0301  v--- --11 +--- ---1
  LFS3_TAG_DIR            0x0302  v--- --11 +--- --1-
  LFS3_TAG_STICKYNOTE     0x0303  v--- --11 +--- --11
  LFS3_TAG_BOOKMARK       0x0304  v--- --11 +--- -1--
  LFS3_TAG_SYMLINK?       0x0305  v--- --11 +--- -1-1
  LFS3_TAG_SNAPSHOT?      0x0306  v--- --11 +--- -11-
  LFS3_TAG_MNAME          0x0330  v--- --11 +-11 ----
  LFS3_TAG_DDNAME*        0x0350  v--- --11 +1-1 ----
  LFS3_TAG_DDTOMB*        0x0351  v--- --11 +1-1 ---1

  LFS3_TAG_STRUCT         0x04tt  v--- -1-- +ttt tttt
  LFS3_TAG_BRANCH         0x040r  v--- -1-- +--- --rr
  LFS3_TAG_DATA           0x0404  v--- -1-- +--- -1--
  LFS3_TAG_BLOCK          0x0408  v--- -1-- +--- 1err
  LFS3_TAG_DDKEY*         0x0410  v--- -1-- +--1 ----
  LFS3_TAG_DID            0x0420  v--- -1-- +-1- ----
  LFS3_TAG_BSHRUB         0x0428  v--- -1-- +-1- 1---
  LFS3_TAG_BTREE          0x042c  v--- -1-- +-1- 11rr
  LFS3_TAG_MROOT          0x0431  v--- -1-- +-11 --rr
  LFS3_TAG_MDIR           0x0435  v--- -1-- +-11 -1rr
  LFS3_TAG_MSHRUB+        0x0438  v--- -1-- +-11 1---
  LFS3_TAG_MTREE          0x043c  v--- -1-- +-11 11rr
  LFS3_TAG_BMRANGE        0x044u  v--- -1-- +1-- ++uu
  LFS3_TAG_BMFREE         0x0440  v--- -1-- +1-- ----
  LFS3_TAG_BMINUSE        0x0441  v--- -1-- +1-- ---1
  LFS3_TAG_BMERASED       0x0442  v--- -1-- +1-- --1-
  LFS3_TAG_BMBAD          0x0443  v--- -1-- +1-- --11
  LFS3_TAG_DDRC*          0x0450  v--- -1-- +1-1 ----
  LFS3_TAG_DDPCOEFF*      0x0451  v--- -1-- +1-1 ---1
  LFs3_TAG_PCOEFFMAP*     0x0460  v--- -1-- +11- ----

  LFS3_TAG_ATTR           0x06aa  v--- -11a +aaa aaaa
  LFS3_TAG_UATTR          0x06aa  v--- -11- +aaa aaaa
  LFS3_TAG_SATTR          0x07aa  v--- -111 +aaa aaaa

  LFS3_TAG_SHRUB          0x1kkk  v--1 kkkk +kkk kkkk
  LFS3_TAG_ALT            0x4kkk  v1cd kkkk +kkk kkkk

  LFS3_TAG_CKSUM          0x300p  v-11 ---- ++++ +pqq
  LFS3_TAG_NOTE           0x3100  v-11 ---1 ++++ ++++
  LFS3_TAG_ECKSUM         0x3200  v-11 --1- ++++ ++++
  LFS3_TAG_GCKSUMDELTA    0x3300  v-11 --11 ++++ ++++

  * Planned
  + Reserved
  ? Hypothetical

Some additional notes:

- I was on the fence on keeping the 0x30 prefix on config tags now that
  it is not longer needed to differentiate from null, but ultimately
  decided to keep it because: 1. it's fun, 2. it decreases the chance
  of false positives, 3. it keeps the redund bits readable in hexdumps,
  and 4. it reserves some tags < config, which is useful since order
  matters.

  Instead, I pushed the 0x30 prefix to _more_ tags, mainly gstate.

  As a coincidence, meta related tags (MNAME, MROOT, MRTREE) all shifted
  to also have the 0x30 prefix, which is a nice bit of unexpected
  consistency.

- I also considered reserving the redund bits across the config tags
  similarly to what we've done in struct/gstate tags, but decided
  against it as 1. it significantly reduces the config tag space
  available, and 2. makes alignment with VERSION + R/W/OCOMPAT a bit
  awkward.

  Instead I think would should relax the redund bit alignment in other
  suptypes, though in practice the intermixing of non-redund and redund
  tags makes this a bit difficult.

  Maybe we should consider including redund bits as a hint for things
  like DATA? DDKEY? BSHRUB? etc?

- I created a bit more space for file btree struct tags, allowing for
  both the future planned DDKEY, and BLOCK with optional erased-bit. We
  don't currently use this, but it may be useful for the future planned
  gddtree, which in-theory can track erased-state in partially written
  file blocks.

  Currently tracking erased-state in file blocks is difficult due to
  the potential of multiple references, and inability to prevent ecksum
  conflicts in raw data blocks.

- UATTR/SATTR bumped up to 0x600/0x700 to keep the 1-bit alignment,
  leaving the suptype 0x500 unused. Though this may be useful if we ever
  run out of struct tags (suptype=0x400), which is likely where most new
  tags will go.

---

Code changes were minimal, but with a bunch of noise:

                 code          stack          ctx
  before:       35912           2280          660
  after:        35920 (+0.0%)   2280 (+0.0%)  660 (+0.0%)

                 code          stack          ctx
  gbmap before: 38800           2296          772
  gbmap after:  38812 (+0.0%)   2296 (+0.0%)  772 (+0.0%)
2025-11-18 00:56:48 -06:00
Christopher Haster c16c4a00d3 ck: Merged FSCK+CK -> CK flag namespace
Unintentionally arriving at the infamous "fsck" name is a bit funny.

But it's probably something we don't want to conflict with if we can
help it, on the off chance we want a sort of lfs3_fsck function in the
future. (This is all hypothetical, but lfs3_fsck may expect an unmounted
filesystem, and have a much larger scope than lfs3_fs_ck. Though typing
this out now I'm realizing how confusing that might be...)

Since lfs3_file_ck and lfs3_fs_ck share a subset of flags, it's not
_entirely_ unreasonable for lfs3_file_ck and lfs3_fs_ck to share the
same namespace.

There's a risk of confusing users around what flags lfs3_file_ck
accepts, but we have asserts, and said flags (LFS3_CK_MKCONSISTENT,
LFS3_CK_LOOKAHEAD, etc) just don't really make sense in lfs3_file_ck:

  fs file
  y     LFS3_CK_MKCONSISTENT 0x00000800  Make the filesystem consistent
  y     LFS3_CK_LOOKAHEAD    0x00001000  Repopulate lookahead buffer
  y     LFS3_CK_LOOKGBMAP    0x00002000  Repopulate the gbmap
  y     LFS3_CK_PREERASE*    0x00004000  Pre-erase unused blocks
  y     LFS3_CK_COMPACTMETA  0x00008000  Compact metadata logs
  y  y  LFS3_CK_CKMETA       0x00010000  Check metadata checksums
  y  y  LFS3_CK_CKDATA       0x00020000  Check metadata + data checksums
  y  y  LFS3_CK_REPAIRMETA*  0x00040000  Repair data blocks
  y  y  LFS3_CK_REPAIRDATA*  0x00080000  Repair metadata + data blocks

  * Planned

Another option would be to document that lfs3_fs_ck accepts both
LFS3_CK_* _and_ LFS3_GC_* flags, but I worry that would be more
confusing. It would also lock us into supporting all LFs3_GC_* flags in
lfs3_fs_ck, which may not always be the case.

Though this is an argument for doing away with the whole
LFS3_M/F/CK/GC/I_* duplication... (tbh another reason for this is to
reduce the number of namespaces by at least one).

No code changes.
2025-11-18 00:56:39 -06:00
Christopher Haster 5c0cebb00b ck: Traded ckmeta/ckdata for flag-based ck functions
TLDR: Replaced lfs3_file_ckmeta/ckdata and lfs3_fs_ckmeta/ckdata with
flag based ck functions:

- lfs3_file_ckmeta -> lfs3_file_ck + LFS3_CK_CKMETA
- lfs3_file_ckdata -> lfs3_file_ck + LFS3_CK_CKDATA
- lfs3_fs_ckmeta -> lfs3_fs_ck + LFS3_FSCK_CKMETA
- lfs3_fs_ckdata -> lfs3_fs_ck + LFS3_FSCK_CKDATA

Note lfs3_fs_ck is equivalent to lfs3_fs_gc, but:

1. Performs the work in one call (equivalent to littlefs2's lfs2_fs_gc)
2. Takes flags at call time (like lfs3_mount) instead of cfg time (like
   lfs3_fs_gc)
3. Avoids the constant RAM necessary to track incremental GC state

---

Motivation:

I've been thinking: It's a bit weird that users are able to one-shot
janitorial work in lfs3_mount, but there's no equivalent function after
the filesystem is mounted.

Originally this is what lfs3_fs_gc was for, but after adding support for
incremental GC, it made sense to hide lfs3_fs_gc behind the opt-in
LFS3_GC ifdef due to the extra (ironically non-gc-able) state.

In theory lfs3_trv_t fills a bit of the gap, but, without the internal
i_flag handling and traversal restarts, it's a bit hard to use. And
basically requires duplicating said log, which we need anyways for
lfs3_mount!

So ideally we'd add an explicit one-shot GC function, but now lfs3_fs_gc
is taken.

While thinking about alternative names, I realized we can just call this
lfs3_fs_ck and completely replace lfs3_fs_ckmeta/ckdata.

This has some extra benefits:

- Avoids an explosion of ckmeta/ckdata/repairmeta/repairdata functions
- Discourages redundant traversals that could accomplish more work
- Makes it less confusing that ckdata implies ckmeta

---

I also tweaked lfs3_file_ck to match, but note that lfs3_file_ck is
internally very different from lfs3_fs_ck. For one, lfs3_file_ck only
supports "actual" check flags (LFS3_CK_*) vs all gc flags (LFS3_FSCK_*):

lfs3_file_ck:

  LFS3_CK_CKMETA          0x00010000  Check metadata checksums
  LFS3_CK_CKDATA          0x00020000  Check metadata + data checksums
  LFS3_CK_REPAIRMETA*     0x00040000  Repair metadata blocks
  LFS3_CK_REPAIRDATA*     0x00080000  Repair metadata + data blocks

  * Planned

lfs3_fs_ck:

  LFS3_FSCK_MKCONSISTENT  0x00000800  Make the filesystem consistent
  LFS3_FSCK_LOOKAHEAD     0x00001000  Repopulate lookahead buffer
  LFS3_FSCK_LOOKGBMAP     0x00002000  Repopulate the gbmap
  LFS3_FSCK_PREERASE*     0x00004000  Pre-erase unused blocks
  LFS3_FSCK_COMPACTMETA   0x00008000  Compact metadata logs
  LFS3_FSCK_CKMETA        0x00010000  Check metadata checksums
  LFS3_FSCK_CKDATA        0x00020000  Check metadata + data checksums
  LFS3_FSCK_REPAIRMETA*   0x00040000  Repair metadata blocks
  LFS3_FSCK_REPAIRDATA*   0x00080000  Repair metadata + data blocks

  * Planned

As a plus, this also saves a bit of code:

                 code          stack          ctx
  before:       35968           2280          660
  after:        35924 (-0.1%)   2280 (+0.0%)  660 (+0.0%)

                 code          stack          ctx
  gbmap before: 38828           2296          772
  gbmap after:  38812 (-0.0%)   2296 (+0.0%)  772 (+0.0%)
2025-11-18 00:56:32 -06:00
Christopher Haster 2d68db965b Rearranged on-disk compat flags
Other than moving things around to make space for planned features, this
also adopts the idea of allowing compat flags to be ored into a single
32-bit integer, at least in the short-term.

Note though that these are still stored in separate wcompat/rcompat
tags, to make compat tests easier, and we may introduce conflicting
flags in the future if we run out of 32-bits. This is just an indulgence
to potentially make tooling/debugging easier until that happens.

Rcompat flags:

  RCOMPAT_NONSTANDARD+
                     0x00000001  ---- ---- ---- ---- ---- ---- ---- ---1
  RCOMPAT_WRONLY+    0x00000004  ---- ---- ---- ---- ---- ---- ---- -1--
  RCOMPAT_MMOSS      0x00000010  ---- ---- ---- ---- ---- ---- ---1 ----
  RCOMPAT_MSPROUT+   0x00000020  ---- ---- ---- ---- ---- ---- --1- ----
  RCOMPAT_MSHRUB+    0x00000040  ---- ---- ---- ---- ---- ---- -1-- ----
  RCOMPAT_MTREE      0x00000080  ---- ---- ---- ---- ---- ---- 1--- ----
  RCOMPAT_BMOSS+     0x00000100  ---- ---- ---- ---- ---- ---1 ---- ----
  RCOMPAT_BSPROUT+   0x00000200  ---- ---- ---- ---- ---- --1- ---- ----
  RCOMPAT_BSHRUB     0x00000400  ---- ---- ---- ---- ---- -1-- ---- ----
  RCOMPAT_BTREE      0x00000800  ---- ---- ---- ---- ---- 1--- ---- ----
  RCOMPAT_MDIRR1*    0x00001000  ---- ---- ---- ---- ---1 ---- ---- ----
  RCOMPAT_MDIRR2*    0x00002000  ---- ---- ---- ---- --1- ---- ---- ----
  RCOMPAT_MDIRR3*    0x00003000  ---- ---- ---- ---- --11 ---- ---- ----
  RCOMPAT_BTREER1*   0x00004000  ---- ---- ---- ---- -1-- ---- ---- ----
  RCOMPAT_BTREER2*   0x00008000  ---- ---- ---- ---- 1--- ---- ---- ----
  RCOMPAT_BTREER3*   0x0000c000  ---- ---- ---- ---- 11-- ---- ---- ----
  RCOMPAT_GRM        0x00010000  ---- ---- ---- ---1 ---- ---- ---- ----
  RCOMPAT_GMV?       0x00020000  ---- ---- ---- --1- ---- ---- ---- ----
  RCOMPAT_GDDTREE*   0x00100000  ---- ---- ---1 ---- ---- ---- ---- ----
  RCOMPAT_GPTREE*    0x00200000  ---- ---- --1- ---- ---- ---- ---- ----
  RCOMPAT_DATAR1*    0x00400000  ---- ---- -1-- ---- ---- ---- ---- ----
  RCOMPAT_DATAR2*    0x00800000  ---- ---- 1--- ---- ---- ---- ---- ----
  RCOMPAT_DATAR3*    0x00c00000  ---- ---- 11-- ---- ---- ---- ---- ----
  rcompat_OVERFLOW+  0x80000000  1--- ---- ---- ---- ---- ---- ---- ----

  * Planned
  + Reserved
  ? Hypothetical

Wcompat flags:

  WCOMPAT_NONSTANDARD+
                     0x00000001  ---- ---- ---- ---- ---- ---- ---- ---1
  WCOMPAT_RDONLY+    0x00000002  ---- ---- ---- ---- ---- ---- ---- --1-
  WCOMPAT_GCKSUM     0x00040000  ---- ---- ---- -1-- ---- ---- ---- ----
  WCOMPAT_GBMAP      0x00080000  ---- ---- ---- 1--- ---- ---- ---- ----
  WCOMPAT_DIR        0x01000000  ---- ---1 ---- ---- ---- ---- ---- ----
  WCOMPAT_SYMLINK?   0x02000000  ---- --1- ---- ---- ---- ---- ---- ----
  WCOMPAT_SNAPSHOT?  0x04000000  ---- -1-- ---- ---- ---- ---- ---- ----
  wcompat_OVERFLOW+  0x80000000  1--- ---- ---- ---- ---- ---- ---- ----

  + Reserved
  ? Hypothetical

Ocompat flags:

  OCOMPAT_NONSTANDARD+
                     0x00000001  ---- ---- ---- ---- ---- ---- ---- ---1
  ocompat_OVERFLOW+  0x80000000  1--- ---- ---- ---- ---- ---- ---- ----

  + Reserved

Other notes:

- M* and B* struct flags were reordered to match META -> DATA order
  elsewhere. This no longer matches the tag ordering, but there's an
  argument the B* tags apply more generally (all btrees) than the B*
  compat flag (only file btrees).

- MDIR/BTREE/DATA redund flags were moved near relevant flags, rather
  than sticking them in the higher-order bits as we are planning to do
  in the M_*/F_* flags. The compat flags already won't match because of
  the mdir/btree split (which is IMO too much detail to include in
  M_*/F_* flags, but hard to argue against in the compat flags), and
  this keeps the highest bit free for OVERFLOW, which is useful
  internally.

- Moving DIR to the current-highest bit makes it easy to add 6 more file
  types (7 if you ignore OVERFLOW), before things start getting cramped.

No code changes.
2025-11-13 16:14:56 -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