Commit Graph

2705 Commits

Author SHA1 Message Date
Christopher Haster 052fc200c8 util: More parens in LFS3_MIN/MAX
Previously this had a very naive number of parens, which led to a very
confusing night trying to debug some code that looked roughly like this:

  LFS3_MAX(1, (false) ? 64 : 256) => 1 ???

Fixed by adding more parens.
2025-10-01 17:58:09 -05:00
Christopher Haster 2a2d3173ce btree: Implemented quick-fetches to try to speed up btree commits
I've had this trick in my back pocket for a while, but didn't think it
would be worth the code cost. Benchmarks suggested this was a
bottleneck, so gave it an impl...

But it turned out to be a red herring...

At least the code cost is ridiculously cheap?

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

Oh, sidenote, this also removes shrub trunk fetching, repurposing that
bit as an internal flag for quick-fetches. I don't think fetching shrubs
makes sense anymore? This code was probably leftover from a less-correct
traversal implementation.

---

The basic idea: the most recent trunk contains all the info we need to
fetch a btree node for committing:

- We can infer the rbyd weight from one trunk: The total weight is just
  the sum of alt pointer weights + the leaf weight.

- The checksum tags provide the perturb bit, ecksum, etc.

The only thing we can't find from the most recent trunk is the checksum,
but this is already implicit in our CoW branch pointers! (Technically the
weight is as well, but we have to scan the alts anyways.)

So we don't need to scan the entire rbyd if we know the checksum, just
the most recent trunk + checksum tags.

In theory, quick-fetches drop our btree commit runtime from
O(b log_b n + (log b)(log_b^2 n)) -> O((log b)(log_b^2 n)).

---

In practice, this doesn't seem to matter, even on NAND with 128KiB
blocks. We're still dominated by compaction costs, perhaps due to the
poor granularity of NAND's read size?

I'm going to keep this for now just for the peace-of-mind while
benchmarking, but it may be worth removing in the future (or maybe not?
the code size is much less than I was expecting).

At least it simplifies the runtime complexity...
2025-10-01 17:58:06 -05:00
Christopher Haster b94f9fe071 runners: Fixed 64-bit overflow when size_t < bench_io_t
Long story short: %zd != %jd!

This was a simple oversight when writing the bench printing code, and
easy to miss on x86_64 and other modern PCs, but the mistake becomes
very apparent when trying to bench under qemu in thumb mode!
2025-10-01 17:58:05 -05:00
Christopher Haster 0698c49e1b Allow crystal_thresh to go below prog_size
After sleeping on it, allowing crystal_thresh < prog_size makes more
sense than I initially thought, if only to better support the case where
prog_size = block_size (SD/eMMC).

It's true fragments + crystal_thresh were intended to avoid needing to
write padding to raw data blocks, but this only makes sense up until
block padding is cheaper than rbyd overheads. At ~block_size/4, rbyds vs
padded data blocks have roughly the same cost, and at ~block_size/2
rbyds use ~2x the storage due to logging/splitting. At ~block_size/2 we
definitely want to crystallize even if this is still below prog_size.

And it turns out allowing crystal_thresh < prog_size fixes the 512B
block size issues on SD/eMMC we were running into earlier!

---

Implementing this required some tweaks to lfs3_file_crystallize_:

1. We intentionally do not align down partial crystallizations if we
   can't satisfy prog alignment, as we risk making no progress
   in this case.

2. If we can't satisfy prog alignment, don't mark the bptr as erased.
   Resuming crystallization to an unaligned block is an error.

Unaligned progs should already be implicitly padded by the lower bd
caching logic, so not aligning should be all we need to do to pad data
blocks.

Oh, and also relax the crystal_thresh >= prog_size constraints.

Adds a bit of code, but the improved block usage on SD/eMMC will
hopefully be valuable:

           code          stack          ctx
  before: 37112           2352          684
  after:  37156 (+0.1%)   2352 (+0.0%)  684 (+0.0%)
2025-10-01 17:58:03 -05:00
Christopher Haster 2f6f7705f1 Limit crystal_thresh to >=prog_size
I confused myself a bit while benchmarking because crystal_thresh <
prog_size was showing some very confusing results. But it turns out the
relevant code was just not written well enough to support this
configuration.

And, to be fair, this configuration really doesn't make sense. The whole
point of the fragment + crystallization system is so we never have to
write unaligned data to blocks. I mean, we could explicitly write
padding in this case, but why?

---

This should probably eventually be either an assert or mutable limit,
but in the meantime I'm just adjusting crystal_thresh at runtime, which
adds a bit of code:

           code          stack          ctx
  before: 37076           2352          684
  after:  37112 (+0.1%)   2352 (+0.0%)  684 (+0.0%)

On the plus side, this prevents crystal_thresh=0 issues much more
elegantly.
2025-10-01 17:58:01 -05:00
Christopher Haster 8cc91ffa9e Prevent oscillation when crystal_thresh < fragment_size
When crystal_thresh < fragment_size, there was a risk that repeated
write operations would oscillate between crystallizing and fragmenting
every operation. Not only would this wreck performance, it would also
violently wear down blocks as each crystallization would trigger an
erase.

Fortunately all we need to do to prevent this is check both
fragment_size and crystal_thresh before fragmenting. Note this also
affects the fragment checks in truncate/fruncate.

---

crystal_thresh < fragment_size is kind of a weird configuration, to be
honest we should probably just assert if configured this way (we never
write fragments > crystal_thresh, because at that point we would just
crystallize).

But at the moment the extra leniency is useful for benchmarking.

Adds a bit of code, but will probably either assert or mutably limit in
the future:

           code          stack          ctx
  before: 37028           2352          684
  after:  37076 (+0.1%)   2352 (+0.0%)  684 (+0.0%)
2025-10-01 17:57:58 -05:00
Christopher Haster eab526ad9f Fixed crystal_thresh=0 bugs
There was a mismatch between the lfs3_cfg comment and the actual
crystal_thresh math where crystal_thresh=0 would break things:

- In lfs3_file_flush_, crystal_thresh=0 meant we would never resume
  crystallization, leading to terrible, _terrible_, linear write
  performance.

- In lfs3_file_sync and lfs3_set, it's unclear if small file commit
  optimizations were working properly. I went ahead and added a
  lfs3_max(lfs3->cfg->crystal_thresh, 1) just to be safe.

The other references to crystal_thresh all check for >= crystal_thresh
conditions, so shouldn't be broken (except for an unrelated bug in
lfs3_file_flushset_).

The reason for this is because crystal_thresh=1 is technically the lower
bound for this math. Allowing crystal_thresh=0 is just a convenience,
and honestly allowing it may have a been a bad idea. Maybe we should
require crystal_thresh=1 at minimum? I added a TODO.

All the new v3 config needs revisiting anyways, for defaults, etc.

---

Curiously, this actually saved code? My best guess is maybe some weird
code path in lfs3_file_flush_ was eliminated:

           code          stack          ctx
  before: 37036           2352          684
  after:  37028 (-0.0%)   2352 (+0.0%)  684 (+0.0%)
2025-10-01 17:57:54 -05:00
Christopher Haster 2c67fb1ea2 scripts: Dropped -e/--exec shortform flag, now just --exec
Too much room for confusion, and potential flag conflicts in the future.
Note it already conflicted with -e/--error-* flags.

--exec is a rather technical flag anyways, and will probably be wrapped
in other ci/script scaffolding most of the time.
2025-10-01 17:57:52 -05:00
Christopher Haster be118ab93d scripts: Fixed -s/-S sorting of .csv/.json outputs
I'm not sure if this was ever implemented, or broken during a refactor,
but we were ignoring -s/-S flags when writing .csv/.json output with
-o/-O.

Curious, because the functionality _was_ implemented in fold, just
unused. All this required was passing -s/-S to fold correctly.

Note we _don't_ sort diff_results, because these are never written to
.csv/.json output.

At some point this behavior may have been a bit more questionable, since
we use to allow mixing -o/-O and table rendering. But now that -o/-O is
considered an exclusive operation, ignoring -s/-S doesn't really make
sense.

---

Why did this come up? Well imagine my frustration when:

1. In tikz/pgfplots, \addplot table only really works with sorted data

2. csv.py has a -s/-S flag for sorting!

3. -s/-S doesn't work!
2025-10-01 17:57:49 -05:00
Christopher Haster c33182b49b Relax recrystallization when fruncating/logging
This was a nasty performance hole found while benchmarking.

Basically, any time crystallization is triggered, the crystallization
algorithm tries to pack as much data into as few blocks as possible.
When fruncating (the common, and performance sensitive, use case being
logging), this can lead to the algorithm rewriting fruncated blocks.

What the crystallization algorithm doesn't realize, however, is that
when fruncating/logging, we're probably going to fruncate again on the
next call, so rewriting the block is a waste of effort.

Worst case -- a 1 block file -- this can cause littlefs to rewrite the
entire file on every append.

---

The solution implemented here, which is a bit of a hack, is to use the
actual block start for block alignment instead of the logical
start-of-block referenced by our btree/bshrub.

This solves the fruncating/logging performance hole, with the tradeoff
of using more storage than is strictly necessary. This tradeoff is
probably expected with logging however.

Code changes minimal:

           code          stack          ctx
  before: 37024           2352          684
  after:  37036 (+0.0%)   2352 (+0.0%)  684 (+0.0%)
2025-10-01 17:57:47 -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 232f039ccc kiwibd: Added kiwibd, a lighter-weight variant of emubd
Useful for emulating much larger disks in a file (or in RAM). kiwibd
doesn't have all the features of emubd, but this allows it to prioritize
disk size and speed for benchmarking.

kiwibd still keeps some features useful for benchmarking/emulation:

- Optional erase value emulation, including nor-masking

- Read/prog/erase trackers for measuring bd operations

- Read/prog/erase sleeps for slowing down the simulation to a human
  viewable speed
2025-10-01 17:57:39 -05:00
Christopher Haster 6ba3204816 scripts: Some csv script tweaks to better interact with other scripts
- Added --small-total. Like --small-header, this omits the first column
  which usually just has the informative text TOTAL.

- Tweaked -Q/--small-table so it renders with --small-total if
  -Y/--summary is provided.

- Added --total as an alias for --summary + --no-header + --small-total,
  i.e. printing only the totals (which may be multiple columns) and no
  other decoration.

  This is useful for scripting, now it's possible to extract just, say,
  the sum of some csv and embed with $():

    echo $(./scripts/code.py lfs3.o --total)

- Tweaked total to always output a number (0) instead of a dash (-),
  even if we have no results.

  This relies on Result() with no args, which risks breaking scripts
  where the Result type expects an argument. To hopefully catch this
  early, the table renderer currently creates a Result() before trying
  to fold the total result.

- If first column is empty (--small-total + --small-header, --no-header,
  etc) collapse width to zero. This avoids a bunch of extra whitespace,
  but still includes the two spaces normal used to separate names from
  fields.

  But I think those spaces are a good thing. It makes it hard to miss
  the implicit padding in the table renderer that risks breaking
  dependent scripts.
2025-10-01 17:57:37 -05:00
Christopher Haster 3e8f304138 scripts: ctx.py/structs.py: Worked around incomplete structs/unions
Found when trying to measure ctx of yaffs2, which relies on incomplete
structs to hide some internal state (yaffs_summary_tags, yaffs_DIR).
This is less common in microcontroller filesystems since almost all
structs end up statically/stack allocated, and you can't statically
allocate incomplete structs.

It's not too surprising, but incomplete structs have no associated
DW_AT_byte_size in the relevant dwarf info, which broke ctx.py and
structs.py...

As a workaround, I'm now defaulting to size=0 if DW_AT_byte_size is
missing.

---

With this fix, at least structs.py is able to pick up the later internal
definition of yaffs_summary_tags. ctx.py doesn't because it only looks
at the unique dwarf offset referenced by the function definition, but
I'm hesitant to try anything more clever here.

yaffs_DIR is noteworthy in that there is simply no complete definition.
Internally, yaffs_DIR pointers alias yaffsfs_DirSearchContext structs.
In this case I think returning size=0 is the only reasonable option.
2025-10-01 17:57:35 -05:00
Christopher Haster c9691503bc scripts: plot[mpl].py: Added --x/ylim-ratio for simpler limits
I've been struggling to keep plots readable with --x/ylim-stddev, it may
have been the wrong tool for the job.

This adds --x/ylim-ratio as an alternative, which just sets the limit to
include x-percent of the data (I avoided "percen"t in the name because
it should be --x/ylim-ratio=0.98, not 98, though I'm not sure "ratio" is
great either...).

Like --x/ylim-stddev, this can be used in both one and two argument
forms:

  $ ./scripts/plot.py --ylim-ratio=0.98
  $ ./scripts/plot.py --ylim-=-0.98,+0.98

So far, --x/ylim-ratio has proven much easier to use, maybe because our
amortized results don't follow a normal distribution? --x/ylim-ratio
seems to do a good job of clipping runaway amortized results without too
much information loss.
2025-10-01 17:57:32 -05:00
Christopher Haster 92af5de3ca emubd: Added optional nor-masking emulation
This adds NOR-style masking emulation to emubd when erase_value is set
to -2:

  erase     => 0xff
  prog 0xf0 => 0xf0
  prog 0xcc => 0xc0

We do _not_ rely on this property in littlefs, and so this feature will
probably go unused in our tests, but it's useful for running other
filesystems (SPIFFS) on top of emubd.

It may be a bit of a scope violation to merge this into littlefs's core
repo, but it's useful to centralize emubd's features somewhere...
2025-10-01 17:57:28 -05:00
Christopher Haster 6a57258558 make: Adopted lowercase for foreach variables
This seems to be the common style in other Makefiles, and avoids
confusion with global/env variables.
2025-10-01 17:57:23 -05:00
Christopher Haster a1b75497d6 bmap: rdonly: Got LFS3_RDONLY + LFS3_BMAP compiling
Counterintuitively, LFS3_RDONLY + LFS3_BMAP _does_ make sense for cases
where you want to include the bmap in things like ckmeta/ckdata scans.

Though this is another argument for a LFS3_RDONLY + LFS3_NO_TRV build.
Traversals add quite a bit of code to the rdonly build that is probably
not always needed.

---

This just required another bunch of ifdefs.

Current bmap rdonly code size:

                code          stack          ctx
  rdonly:      10616            896          532
  rdonly+bmap: 10892 (+2.6%)    896 (+0.0%)  636 (+19.5%)
2025-10-01 17:57:15 -05:00
Christopher Haster 60ef118dcd rdonly: Got LFS3_RDONLY compiling again
Just a few alloc/eoff references slipped through in the bmap work.

Current rdonly code size:

            code           stack           ctx
  default: 37024            2352           684
  rdonly:  10616 (-71.3%)    896 (-61.9%)  532 (-22.2%)

This biggest change was tweaking our mtortoise again to use the unused
trunk field for the power-of-two bound. The original intention of using
eoff was an extra precaution to avoid the mtortoise looking like a valid
shrub at any point, but eoff is not available in LFS3_RDONLY.

And we definitely want our mtortoise in LFS3_RDONLY!

---

Note I haven't actually tested LFS3_RDONLY + LFS3_BMAP. Does this config
even make sense? I guess ckmeta/ckdata will need to traverse the bmap,
so, counterintuitively, yes?
2025-10-01 17:57:14 -05:00
Christopher Haster 664d99dbeb Fixed crystallize_ losing track of ungrafted leaves on error
Whoops, this was an oversight when readopting lazy grafting.

It turns out the crystallization refactor that led to
lfs3_file_crystallize_ operating directly on file->leaf.bptr was a bit
incompatible with lazy grafting.

If we encounter an error and need to relocate, we need to rewrite any
data in our crystal, _including data in ungrafted leaves_.

By pure luck, the previous lazy grafting implementation side-stepped
this issue by including ungrafted leaves in lfs3_file_lookupnext calls.
This implicitly included the ungrafted leaf in any recrystallizations,
as long as it wasn't modified on error.

---

The fix required two tweaks:

- Recrystallize into a copy in case we hit an error.

  Instead of a full lfs3_bptr_t, I just copied the relevant
  block_/off_/pos_ pieces we need.

- Include file leaves in the crystallization logic.

  Fortunately the multi-data-prioritization loop we already have for
  any cached data was relatively easy to adapt for this.

As a plus lfs3_file_crystallize_ can also now short-circuit a
bshrub/btree lookup if the data we're crystallizing happens to be in the
file leaf.

This adds a bit more code, but doesn't break if we hit an error. In
theory this would add stack for the recrystallization copy, but
lfs3_file_crystallize_ is just off the stack hot-path:

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

Another fix I considered -- calling lfs3_file_graft on error -- may have
been a bit less code, but would have moved the stack hot-path under
lfs3_file_crystallize_. An error triggering _more_ progs/commits also
doesn't really sound like the greatest of ideas.

Found by test_ck_spam_fwrite_fuzz.
2025-10-01 17:57:12 -05:00
Christopher Haster 8c04482ea3 Disable LFS3_BMAP when LFS3_BIGGEST for now
Currently LFS3_BMAP implies LFS3_YES_BMAP, which is an ugly hack because
I don't want to figure out the BMAP flag logic right now.

As a side-effect, this makes it impossible to test LFS3_BIGGEST without
LFS3_BMAP, which breaks a number of tests that have not been updated to
support >2 format blocks.
2025-10-01 17:57:09 -05:00
Christopher Haster 83196ed67a Dropped redundant isuncryst check in lfs3_file_flush_
Saves a bit of code, at the cost of making this logic a bit more
difficult to read:

           code          stack          ctx
  before: 36992           2352          684
  after:  36972 (-0.1%)   2352 (+0.0%)  684 (+0.0%)
2025-10-01 17:57:08 -05:00
Christopher Haster be3e61dd13 Dropped lfs3_file_weight_
Now that ungrafted leaves are much more limited in scope, I'm not sure
lfs3_file_weight_ still makes sense as a separate function.

The only call was in lfs3_file_read, where we decide if we bother
flushing things before actually flushing things. Given that we should
now generally graft before expecting lfs3_file_lookupnext to make sense,
relying on lfs3_file_weight_ too much probably hints at a logic mistake.

This logic ends up inlined anyways, so no code changes:

           code          stack          ctx
  before: 36992           2352          684
  after:  36992 (+0.0%)   2352 (+0.0%)  684 (+0.0%)
2025-10-01 17:57:06 -05:00
Christopher Haster 15c3d2f87a Flattened lfs3_file_crystallize_
We no longer need to discard the leaf, since we can just leave ungrafted
leaves around as long as LFS3_o_UNGRAFT is set.

This let's us flatten lfs3_file_crystallize_, saving a bit of code and
cleaning up the logic a bit:

           code          stack          ctx
  before: 37032           2352          684
  after:  36992 (-0.1%)   2352 (+0.0%)  684 (+0.0%)

Note that lfs3_file_crystallize_ is still NOINLINE to force it off the
stack hot-path in lfs3_file_flush_, etc.
2025-10-01 17:57:04 -05:00
Christopher Haster 58c5506e85 Brought back lazy grafting, but not too lazy
Continued benchmarking efforts are indicating this isn't really an
optional optimization.

This brings back lazy grafting, where the file leaf is allowed to fall
out-of-date to minimize bshrub/btree updates. This is controlled by
LFS3_o_UNGRAFT, which is similar, but independent from LFS3_o_UNCRYST:

- LFS3_o_UNCRYST - File's leaf not fully crystallized
- LFS3_o_UNGRAFT - File's leaf does not match disk

Note it makes sense for files to be UNGRAFT only, in the case where the
current crystal terminates at the end-of-file but future appends are
likely. And it makes sense for files to be UNCRYST only, in cases where
we graft uncrystallized blocks so the bshrub/btree makes sense.

Which brings us to the main change from the previous lazy-grafting
implementation: lfs3_file_lookupnext no longer includes ungrafted
leaves.

Instead, functions should call lfs3_file_graft if they need
lfs3_file_lookupnext to make sense.

This significantly reduces the code cost of lazy grafting, at the risk
of needing to graft more frequently. Fortunately we don't actually need
to call lfs3_file_graft all that often:

- lfs3_file_read already flushes caches/leaves before attempting any
  bshrub/btree reads for simplicity (heavy are not currently considered
  a priority, if you need this consider opening two file handles).

- lfs3_file_flush_ _does_ need to call lfs3_file_graft before the
  crystallization heuristic pokes, but if we can't resume
  crystallization, we would probably need to graft the crystal to
  satisfy the flush anyways.

---

Lazy grafting, i.e. procrastinating on bshrub/btree updates during block
appends, is an optimization previously dropped due to perceived
nicheness:

- We can only lazily graft blocks, inlined data fragments always require
  bshrub/btree updates since they live in the bshrub/btree.

- Sync forces bshrub/btree updates anyways, so lazy grafting has no
  benefit for most logging applications.

- This performance penalty of eagerly grafting goes away if your caches
  are large enough.

Note that the last argument is a non-argument in littlefs's case. They
whole point of littlefs is that you _don't_ need RAM to fix things.

However these arguments are all moot when you consider that the "niche
use case" -- linear file writes -- is the default bottleneck for most
applications. Any file operation becomes a linear write bottleneck when
the arguments are large enough. And this becomes a noticeable issue when
benchmarking.

So... This brings back lazy grafting. But with a more limited scope
w.r.t. internal file operations (the above lfs3_file_lookupnext/
lfs3_file_graft changes).

---

Long story short, lazy grafting is back again, reverting the ~3x
performance regression for linear file writes.

But now with quite a bit less code/stack cost:

           code          stack          ctx
  before: 36820           2368          684
  after:  37032 (+0.6%)   2352 (-0.7%)  684 (+0.0%)
2025-10-01 17:57:01 -05:00
Christopher Haster 68424f8cda bmap: t: Added the on-disk bmap (bmap_p) to mtree traversals
I was wrong! New bmaps containing the old bmap is _not_ sufficient for
lookahead scans, in the case where we rebuild the bmap multiple times
before an mdir commit! The previous bug with lfs3_trv_read +
lfs3_alloc_ckpoint _was_ a bug because the traversal was rdonly, but in
theory the bmap shouldn't have been corrupted.

Since this case is possible, we also need to traverse the on-disk bmap.
Fortunately only when the on-disk bmap does not match the active bmap.

This is probably safer behavior anyways, and means ckmeta/ckdata will
traverse both the in-RAM and on-disk bmaps, which is probably a good
thing in case we need to revert to the on-disk bmap due to power-loss/
error.

---

The implementation got a bit messy, since we only track the on-disk
encoding of the on-disk bmap (we need the encoding for gdeltas to work).
This ended up adding code, and an annoying (avoidable? TODO?) stack
cost, but correct behavior is better than incorrect behavior:

                code          stack          ctx
  before:      36856           2368          684
  after:       36820 (-0.1%)   2368 (+0.0%)  684 (+0.0%)

                code          stack          ctx
  bmap before: 38452           2400          812
  bmap after:  38504 (+0.1%)   2464 (+2.7%)  812 (+0.0%)

Once again the inaccuracy of our stack frame calculations strike
again...
2025-10-01 17:56:58 -05:00
Christopher Haster 27a722456e scripts: Added support for SI-prefixes as iI punescape modifiers
This adds %i and %I as punescape modifiers for limited printing of
integers with SI prefixes:

- %(field)i - base-10 SI prefixes
  - 100   => 100
  - 10000 => 10K
  - 0.01  => 10m

- %(field)I - base-2SI prefixes
  - 128   => 128
  - 10240 => 10Ki
  - 0.125 => 128mi

These can also easily include units as a part of the punescape string:

- %(field)iops/s => 10Kops/s
- %(field)IB => 10KiB

This is particularly useful in plotmpl.py for adding explicit
x/yticklabels without sacrificing the automatic SI-prefixes.
2025-10-01 17:56:51 -05:00
Christopher Haster 2a4e0496b6 scripts: csv.py: Fixed lexing of signed float exponents
So now these lex correctly:

- 1e9  =>  1000000000
- 1e+9 =>  1000000000
- 1e-9 => -1000000000

A bit tricky when you think about how these could be confused for binary
addition/subtraction. To fix we just eagerly grab any signs after the e.

These are particularly useful for manipulating simulated benchmarks,
where we need to convert things to/from nanoseconds.
2025-10-01 17:56:29 -05:00
Christopher Haster 18d1f68445 t: Limited lfs3_alloc_ckpoint to LFS3_T_LOOKAHEAD
This was causing a problem where the bmap was being rebuilt on every
lfs3_trv_read, even though the traversal was opened LFS3_T_RDONLY!

Also added a note on why we don't need to traverse both the active and
on-disk bmaps. Counterintuitively, we don't need to because the new bmap
always contains the entirety of the on-disk bmap.

Code changes minimal:

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

                code          stack          ctx
  bmap before: 38440           2400          812
  bmap after:  38452 (+0.0%)   2400 (+0.0%)  812 (+0.0%)
2025-10-01 17:56:27 -05:00
Christopher Haster 507c04db70 Fixed clobbered shrub estimates when redundantly syncing shrubs
Found while benchmarking, our shrub estimates were being recalculated
much more frequently than they should be (every shrub commit). The
problem is that we never staged shrub estimates!

In theory this is fine, shrub estimates don't necessarily need staging.
We update shrub.r.eoff/estimate directly in lfs3_bshrub_commitroot_
after the mdir commit succeeds. But then we _redundantly_ sync
shrub_ -> shrub.r in lfs3_bshrub_commit. Since we never staged the
shrub estimate, we end up with garbage.

---

It's not clear to me this (staging the shrub estimate) is the best fix
for this, but the reason for the redundant shrub sync is the shared
bshrub/btree post-commit path. Added a TODO comment and should look at
this again when not in a time crunch.

Code changes minimal:

           code          stack          ctx
  before: 36836           2368          684
  after:  36840 (+0.0%)   2368 (+0.0%)  684 (+0.0%)
2025-10-01 17:56:25 -05:00
Christopher Haster 7289619859 Tweaked lfs3_mdir_commit to imply lfs3_alloc_ckpoint
Now that lfs3_alloc_ckpoint is more complicated, and can error, it makes
sense for lfs3_alloc_ckpoint to be implied by lfs3_mdir_commit.

Most lfs3_mdir_commit calls represent an atomic transaction from one
state -> another, so this saves a bit of code:

                code          stack          ctx
  before:      36912           2368          684
  after:       36836 (-0.2%)   2368 (+0.0%)  684 (+0.0%)

                code          stack          ctx
  bmap before: 38512           2400          812
  bmap after:  38436 (-0.2%)   2400 (+0.0%)  812 (+0.0%)

The notable exception being bshrub-related commits in
lfs3_bshrub_commitroot_. Bshrub commits are trying to resolve an
in-flight btree, so the relevant blocks are very much _not_ at rest.

---

I've been hesitant to adopt this mostly just because it makes the
lfs3_mdir_commit* names even more of a mess:

- lfs3_mdir_commit__   -> lfs3_mdir_commit___
- lfs3_mdir_commit_    -> lfs3_mdir_commit__
- lfs3_mdir_commit     -> lfs3_mdir_commit_
- added lfs3_mdir_commit
- lfs3_mdir_compact    -> lfs3_mdir_compact_
- add lfs3_mdir_compact
- lfs3_mdir_alloc__    -> lfs3_mdir_alloc___
- lfs3_mdir_estimate__ -> lfs3_mdir_estimate___
- lfs3_mdir_swap__     -> lfs3_mdir_swap___
2025-10-01 17:56:24 -05:00
Christopher Haster 27e3e10634 bmap: Added error propagation to ckpoints and cleaned up test TODOs
The main change is error propagation in lfs3_alloc_ckpoint. Since
lfs3_alloc_ckpoint writes to disk during bmap rebuilds, it can now fail
in all sorts of ways. Fortunately lfs3_alloc_ckpoint should only ever be
called by write operations, where these errors are be expected.

With bmap rebuild errors now reported correctly, this unblocks most of
the remaining test TODOs:

- Passing test_badblocks
- Passing test_ck
- Passing test_trvs

With this, LFS3_YES_BMAP is now passing all but two tests, which are
still ifndef-disabled as a temporary measure:

- test_btree - We make some low-level assumptions about the lookahead
  allocator when testing btrees. It's probably not worth trying to get
  this passing with the bmap allocator.

- test_grow - This one does need fixing! We currently don't update
  on-disk bmaps correctly when growing the filesystem.

Code changes minimal:

                code          stack          ctx
  before:      36912           2368          684
  after:       36912 (+0.0%)   2368 (+0.0%)  684 (+0.0%)

                code          stack          ctx
  bmap before: 38456           2400          812
  bmap after:  38512 (+0.1%)   2400 (+0.0%)  812 (+0.0%)
2025-10-01 17:56:22 -05:00
Christopher Haster 41be512272 bmap: Fixed up low-hanging fruit, tests and things
- Consistent grm_op -> alloc_ckpoint -> mdir_commit order
- Drop some low priority TODOs
- Got test_alloc at least passing existing tests
- Got test_gc passing
- Got test_mount passing
- test_relocations was already passing, lol

No code changes:

                code          stack          ctx
  before:      36912           2368          684
  after:       36912 (+0.0%)   2368 (+0.0%)  684 (+0.0%)

                code          stack          ctx
  bmap before: 38456           2400          812
  bmap after:  38456 (+0.0%)   2400 (+0.0%)  812 (+0.0%)
2025-10-01 17:56:20 -05:00
Christopher Haster 047fb83b62 dread: Fixed lingering orphans affecting dir positions
We need to adjust mids to ignore orphans during dir traversal, but we
shouldn't also adjust the dir position. In theory it shouldn't matter if
we use adjusted/non-adjusted dir positions, but it becomes a problem if
intermediate writes cause those orphans to be cleaned up. Now all your
dir positions are wrong.

Not entirely sure why this only started to fail with the bmap. I'm
guessing it's just due to the additional gstate causing the mdirs to
split differently.

Code changes minimal:

           code          stack          ctx
  before: 36920           2368          684
  after:  36912 (-0.0%)   2368 (+0.0%)  684 (+0.0%)

Tangential, but toss this on the pile of problems with dir positions.
I'm increasingly convinced we should just remove the concept if we can
get away with it.
2025-10-01 17:56:17 -05:00
Christopher Haster 726cccfe76 bmap: Tweaked bmapcache algo to piggyback on mdir commits
There's really no reason to immediately commit the bmap to disk, at
least no until the first mdir commit, when we need to at least discard
the previous bmap state.

We already do all the gstate handling in lfs3_mdir_commit anyways, and
piggybacking on mdir commit lets us get rid of the annoying extra mdir
param in lfs3_alloc_ckpoint.

This does mean a slightly higher risk of needing to re-rebuild the bmap
after a powerloss, but in theory only if the user does something weird
like writing to a file and never calling sync. Most on-disk operations
terminate in an mdir commit as that's how any state change becomes
atomically visibile in littlefs.

Saves a nice bit of stack:

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

                code          stack          ctx
  bmap before: 38552           2472          812
  bmap after:  38464 (-0.2%)   2400 (-2.9%)  812 (+0.0%)
2025-10-01 17:56:16 -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 8666830515 bmap: scripts: Fixed missing geometry race condition
The gbmap's weight is defined by the block count stored in the geometry
config field, which should always be present in valid littlefs3 images.

But our scripts routinely try to parse _invalid_ littlefs3 images when
running in parallel with benchmarks/tests (littlefs3 does _not_ support
multiple read/writers), so this was causing exceptions to be thrown.

The fix is to just assume weight=0 when the geometry field is missing.
The image isn't valid, and the gbmap is optional anyways.
2025-10-01 17:56:13 -05:00
Christopher Haster 71b9ad2412 bmap: Enabled at least opportunistic bmap allocations
This doesn't fully replace the lookahead buffer, but at least augments
it with known bmap state when available.

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

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

---

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

This order also makes more sense if we want to check mdirs before we
consider the gstate to be trustworthy enough for gbmap traversal.
2025-10-01 17:56:10 -05:00
Christopher Haster ebae43898e bmap: Changing direction, store bmap mode in wcompat flags
The idea behind separate ctrled+unctrled airspaces was to try to avoid
multiple interpretations of the on-disk bmap, but I'm starting to think
this adds more complexity than it solves.

The main conflict is the meaning of "in-flight" blocks. When using the
"uncontrolled" bmap algorithm, in-flight blocks need to be
double-checked by traversing the filesystem. But in the "controlled"
bmap algorithm, blocks are only marked as "in-flight" while they are
truly in-flight (in-use in RAM, but not yet in use on disk).
Representing these both with the same "in-flight" state risks
incompatible algorithms misinterpreting the bmap across different
mounts.

In theory the separate airspaces solve this, but now all the algorithms
need to know how to convert the bmap from different modes, adding
complexity and code cost.

Well, in theory at least. I'm unsure separate airspaces actually solves
this due to subtleties between what "in-flight" means in the different
algorithms (note both in-use and free blocks are "in-flight" in the
unknown airspace!). It really depends on how the "controlled" algorithm
actually works, which isn't implemented/fully designed yet.

---

Long story short, due to a time crunch, I'm ripping this out for now and
just storing the current algorithm in the wcompat flags:

  LFS3_WCOMPAT_GBMAP       0x00006000  Global block-map in use
  LFS3_WCOMPAT_GBMAPNONE   0x00000000  Gbmap not in use
  LFS3_WCOMPAT_GBMAPCACHE  0x00002000  Gbmap in cache mode
  LFS3_WCOMPAT_GBMAPVFR    0x00004000  Gbmap in VFR mode
  LFS3_WCOMPAT_GBMAPIFR    0x00006000  Gbmap in IFR mode

Note GBMAPVFR/IFR != BMAPSLOW/FAST! At least BMAPSLOW/FAST can share
bmap representations:

- GBMAPVFR => Uncontrolled airspace, i.e. in-flight blocks may or may
  not be in use, need to traverse open files.

- GBMAPIFR => Controlled airspace, i.e. in-flight blocks are in use,
  at least until powerloss, no traversal needed, but requires more bmap
  writes.

- BMAPSLOW => Treediff by checking what blocks are in B but not in A,
  and what blocks are in A but not in B, O(n^2), but minimizes bmap
  updates.

  Can be optimized with a bloom filter.

- BMAPFAST => Treediff by clearing all blocks in A, and then setting all
  blocks in B, O(n), but also writes all blocks to the bmap twice even
  on small changes.

  Can be optimized with a sliding bitmap window (or a block hashtable,
  though a bitmap converges to the same thing in both algorithms when
  >=disk_size).

It will probably be worth unifying the bmap representation later (the
more algorithm-specific flags there are, the harder interop becomes for
users, but for now this opens a path to implementing/experimenting with
bmap algorithms without dealing with this headache.
2025-10-01 17:56:08 -05:00
Christopher Haster beb1f1346a bmap: Started implementing ctrled/unctrled lfs3_alloc paths 2025-10-01 17:56:07 -05:00
Christopher Haster e7c3755e21 bmap: Split known into ctrled+unctrled 2025-10-01 17:56:05 -05:00
Christopher Haster 732d6079e3 bmap: Added low-level bmap set algorithm and related tests
The neat thing about the on-disk bmap is that it's a range tree. We can
leverage order-statistic properties to compactly represent ranges of
similar blocks.

However, this does make updating the bmap slightly more complicated...
2025-10-01 17:55:39 -05:00
Christopher Haster 98f016b07e bmap: Added initial gbatc interactions, up until out-of-known or remount
This only works immediately after format, and only for one pass of the
disk, but it's a good way to test bmap lookups/allocation without
worrying about more complicated filesystem-wide interactions.
2025-10-01 17:55:31 -05:00
Christopher Haster 357526e775 rbyd: Allow refetching after claiming erased-state
Except for niche file snapshotting, most btree updates until this point
are probably linear, i.e. a successful commit replaces any internal
rbyd state that has been claimed. In this model it makes sense to mark
claimed rbyd as "invalid", since failure to replace the claimed state
indicates something went wrong during the commit.

But this isn't necessarily true when snapshotting, since we don't
replace the state of claimed snapshots.

But wait, shouldn't snapshotted rbyds become readonly? Not necessarily!
Rbyds can have multiple trunks with unrelated (or in this case, shared)
histories, so there's nothing wrong with refetching an rbyd and
continuing to commit after another snapshot commits to tbe block.

Eventually both snapshots will need to compact and diverge into two
blocks, but until then sharing an rbyd makes the most of available
erased state.

At least in theory, experience will show us how well this works.

---

Also note this is not true for mdirs. We view mdirs as atomic and always
up-to-date, so snapshotting doesn't really make sense.
2025-10-01 17:55:29 -05:00
Christopher Haster 59a4ae6f61 bmap: Taught littlefs how to traverse the gbmap
Fortunately the btree traversal logic is pretty reusable, so this just
required an additional tstate (LFS3_TSTATE_BMAP).

This raises an interesting question: _when_ do we traverse the bmap? We
need to wait until at least mtree traversal completes for gstate to be
reconstructed during lfs3_mount, but I think traversing before file
btrees makes sense.
2025-10-01 17:55:27 -05:00
Christopher Haster 1537f6a430 bmap: Decoding the gbmap in gstate now works
This is just a copy + edit of the grm logic, which raises the question
of if this logic can be generalized.
2025-10-01 17:55:24 -05:00
Christopher Haster 5f65b49ef8 bmap: scripts: Added on-disk bmap traversal to dbgbmap and friends
And yes, dbgbmapsvg.py's parents are working, thanks to a hacky blocks
@property (Python to the rescue!)
2025-10-01 17:55:16 -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 238dbc705d Abandoned data-backed cache, use indirect lfs3_data_t on stack
This abandons the data-backed cache idea due to concerns around
readability and maintainability. Mixing const/mutable buffers in
lfs3_data_t was not great.

Instead, we now just allocate an indirect lfs3_data_t on the stack in
lfs3_file_sync_ to avoid the previous undefined behavior.

This actually results in less stack usage total, due to lfs3_file_t
allocations in lfs3_set/read, and avoid the more long-term memory cost
in lfs3_file_t:

              code          stack          ctx
  before:    36832           2376          684
  after:     36840 (+0.0%)   2368 (-0.3%)  684 (+0.0%)

Oh. And lfs3_file_sync_ isn't even on the stack hot-path, so this is a
net benefit over the previous cache -> data cast:

              code          stack          ctx
  before sa: 36844           2368          684
  after sa:  36840 (-0.0%)   2368 (+0.0%)  684 (+0.0%)

Still less cool though.
2025-07-22 13:39:43 -05:00