Commit Graph

1371 Commits

Author SHA1 Message Date
Christopher Haster 75ac29e8e6 Renamed the generated prettyasserts's extension to .a.c
So instead of preprocessing lfs.t.a.c -> lfs.t.c, we preprocess
lfs.t.c -> lfs.t.a.c.

Really the extension should indicate what tool it was generated by, not
what tool it should be consumed by.

The previous commit that changed this states:

> Changed prettyasserts.py rule to .a.c => .c, allowing other .a.c files
> in the future.

But I'm not really sure why we would ever not just run prettyasserts.py
on every C file...

External use of prettyasserts.py made it clear the previous naming was a
bit weird.
2024-02-15 17:09:17 -06:00
Christopher Haster 65dd669d83 Allowed overriding prettyasserts.py with an external prettyasserts tool
Unfortunately, prettyasserts.py is having a hard time keeping up with
the constantly increasing number of tests. This is creating real
friction when debugging, as it now takes ~6x the time to preprocess
asserts as it does to actually compile the thing:

  $ time ./scripts/prettyasserts.py \
      -a LFS_ASSERT -u LFS_UNREACHABLE \
      lfs.t.a.c -o lfs.t.c
  real    0m16.187s
  user    0m16.163s
  sys     0m0.025s

  $ time gcc -c -O0 -I. lfs.t.c -o lfs.o
  real    0m2.466s
  user    0m2.345s
  sys     0m0.105s

Externally, I've rewritten prettyasserts.py in Rust, with more attention
towards performance (prettyasserts.py does quite a number of string
allocations). The result is quite satisfying:

  $ time ~/prettyasserts/prettyasserts \
      -a LFS_ASSERT -u LFS_UNREACHABLE \
      lfs.t.a.c -o lfs.t.c
  real    0m0.504s
  user    0m0.464s
  sys     0m0.040s

However, adding Rust as a requirement to test littlefs would be, uh,
quite a big jump.

So instead, littlefs keeps prettyassert.py, so only Python is needed out
of the box, and if the slow preprocessing is too much users are welcome
to provide their own prettyasserts binary via the PRETTYASSERTS env
variable:

  $ time \
      DEBUG=1 \
      make test-runner -j
  real    0m22.204s
  user    0m44.841s
  sys     0m1.478s

  $ time \
      DEBUG=1 PRETTYASSERTS=~/prettyasserts/prettyasserts \
      make test-runner -j
  real    0m5.699s
  user    0m23.590s
  sys     0m1.151s
2024-02-15 16:26:48 -06:00
Christopher Haster 5128522fe2 Renamed script flag -Z/--depth -> -z/--depth
Previously, the intention of upper case -Z was the match -W/--width and
-H/--height, which are uppercase to avoid conflicts with -h/--help.

But -z/--depth isn't _really_ related to -W/-H.

This avoids a conflict with -Z/--lebesgue, but may conflict with
-z/--cat. Fortunately we don't currently have any conflicts with the
latter. Since -z/--depth and -Z/--lebesgue are both disk-layout related,
the risk of conflicts are probably much higher there.
2024-02-14 14:04:45 -06:00
Christopher Haster 2d2c0f19ff Renamed block-size flag in scripts from -B -> b
So now these should be invoked like so:

  $ ./scripts/dbglfs.py -b4096x256 disk

The motivation for this change is to better match other filesystem
tooling. Some prior art:

- mkfs.btrfs
  - -n/--nodesize   => node size in bytes, power of 2 >= sector
  - -s/--sectorsize => sector size in bytes, power of 2
- zfs create
  - -b => block size in bytes
- mkfs.xfs
  - -b => block size in bytes, power of 2 >= sector
  - -s => sector size in bytes, power of 2 >= 512
- mkfs.ext[234]
  - -b => block size in bytes, power of 2 >= 1024
- mkfs.ntfs
  - -c/--cluster-size => cluster size in bytes, power of 2 >= sector
  - -s/--sector-size  => sector size in bytes, power of 2 >= 256
- mkfs.fat
  - -s => cluster size in sectors, power of 2
  - -S => sector size in bytes, power of 2 >= 512

Why care so much about the flag naming for internal scripts? The
intention is for external tooling to eventually use the same set of
flags. And maybe even create publically consumable versions of the dbg
scripts. It's important that if/when this happens flags stay consistent.
Everyone familiar with the ssh -p/scp -P situation knows how annoying
this can be.

It's especially important for littlefs's -b/--block-size flag, since
this will likely end up used everywhere. Unlike other filesystems,
littlefs can't mount without knowing the block-size, so any tool that
mounts littlefs is going to need the -b/--block-size flag.

---

The original motivation for -B was to avoid conflicts with the -b/--by
flag that was already in use in all of the measurement scripts. But
these are internal, and not really littlefs-related, so I don't think
that's a good reason any more. Worst case we can just make the --by flag
-B, or just not have a short form (--by is only 4 letters after all).

Somehow we ended up with no scripts needing both -b/--block-size and
-b/--by so far.

Some other conflicts/inconsistencies tweaks were needed, here are all
the flag changes:

- -B/--block-size   -> -b/--block-size
- -M/--mleaf-weight -> -m/--mleaf-weight
- -b/--btree        -> -B/--btree
- -C/--block-cycles -> -c/--block-cycles  (in tracebd.py)
- -c/--coalesce     -> -S/--coalesce      (in tracebd.py)
- -m/--mdirs        -> -M/--mdirs         (in dbgbmap.py)
- -b/--btrees       -> -B/--btrees        (in dbgbmap.py)
- -d/--datas        -> -D/--datas         (in dbgbmap.py)
2024-02-14 12:45:30 -06:00
Christopher Haster 7d95a2ff29 Added ability to disable default patterns in prettyasserts.py
- -n/--no-defaults - disable default patterns

The default patterns can be brought back explicitly with:

- -a/--assert      - enable assert pattern
- -u/--unreachable - enable unreachable pattern
- -A/--arrow       - enable arrow patterns

Technically the default configuration is equivalent to the follow:

  $ ./scripts/prettyasserts.py \
      -a assert \
      -a __builtin_assert \
      -u unreachable \
      -u __builtin_unreachable \
      -A \
      input.a.c -o output.c

This isn't really useful for littlefs, but may be useful elsewhere
2024-02-14 12:22:19 -06:00
Christopher Haster 738dd86339 Extended prettyasserts.py to support unreachable statements
The main benefit is control over error reporting and avoiding the dive
into stdlib layers when debugging thanks to __builtin_trap().

This changes -p/--pattern -> -a/--assert

And adds -u/--unreachable
2024-02-14 01:59:03 -06:00
Christopher Haster 1422a61d16 Made generated prettyasserts more debuggable
The main star of the show is the adoption of __builtin_trap() for
aborting on assert failure. I discovered this GCC/Clang extension
recently and it integrates much, _much_ better with GDB.

With stdlib's abort(), GDB drops you off in several layers of internal
stdlib functions, which is a pain to navigate out of to get to where the
assert actually happened. With __builtin_trap(), GDB stops immediately,
making debugging quick and easy.

This is great! The pain of debugging needs to come from understanding
the error, not just getting to it.

---

Also tweaked a few things with the internal print functions to make
reading the generated source easier, though I realize this is a rare
thing to do.
2024-02-14 01:14:36 -06:00
Christopher Haster 06a360462a Simplified test/bench suite finding logic in test.py/bench.py
These just take normal paths now, we weren't even using the magic
test/bench suite finding logic since it's easier to just pass everything
explicitly in our Makefile.

The original test/bench suite finding logic was a bad idea anyways. This
is what globs are for, and having custom path chasing logic is
inconsistent and risks confusion.
2024-02-14 00:25:10 -06:00
Christopher Haster a124ee54e7 Reworked test/bench defines to map to global variables
Motivation:

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

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

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

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

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

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

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

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

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

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

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

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

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

---

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

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

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

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

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

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

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

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

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

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

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

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

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

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

And any performance impact is negligible (~630s +-~20s).
2024-02-13 18:59:58 -06:00
Christopher Haster ddb86af059 Dropped lfs_cmp for manual comparisons
So instead of:

  lfs_cmp(cmp) <= 0

You can do:

  cmp <= LFS_CMP_EQ

This is much simpler and still preserves the ability to use all of C's
comparison operators on the results of disk comparisons.
2024-02-11 00:36:01 -06:00
Christopher Haster 036047bbba Reverted little-leb128 decoder to just call the big-leb128 decoder
The duplicate decoder for little-leb128 avoided extra stack allocation
for the unaligned worst-case leb128 encoding, but did result in a
duplicate function and extra code cost.

Reasons for deduplicating:

- We'd definitely want to deduplicate these functions if they end up
  with the same encoding cost (28-bit littlefs mode?).

- Less code is less code.

- I noticed the stack savings are arch dependent because
  lfsr_data_readlleb128 only sometimes ends up on the "hot-path".
  thumb calls lfsr_data_readlleb128 on the hot-path, but x86 ends up in
  lfsr_bd_readtag. So it's not clear this stack savings is really
  valuable vs buffer reductions higher up the stack.

  Though I'm not really sure how much I trust stack.py based analysis
  right now...

- 8 bytes of RAM is more likely to be compiler noise than 100 bytes of
  code. Still, both are somewhat negligible and I should probably move
  on from this...

I did also try an internally deduplicated version, with an
lfsr_data_readleb128_ that takes a buffer provided by both
lfsr_data_readleb128 and lfsr_data_readlleb128, but this ended up the
worst of both worlds likely just due to compiler overhead. Abstractions
have cost!

                      code          stack
  duplicated:        33808           2792
  little-calls-big:  33700 (-0.3%)   2800 (+0.3%)
  dedup-via-buffer:  33796 (-0.0%)   2816 (+0.9%)
2024-02-10 21:08:48 -06:00
Christopher Haster 7759b0b43d Reduce stack allocation in the little-leb128 decoder
This avoids the extra stack allocation for the unaligned worst-case
leb128 encoding by duplicating most of the "big-leb128" decoder. The
upside is less stack usage, but at a code cost, since we basically have
two copies of this function now.

This is a bit of a tough call, the percentage change is basically the
same:
            code          stack
  before:  33700           2800
  after:   33808 (+0.3%)   2792 (-0.3%)

On one hand, we would want to deduplicate these functions if they end up
with the same encoding cost (28-bit littlefs mode?), and less code is
less code, on the other hand, RAM is in general more valuable than
code...

This may be worth reverting in the future...
2024-02-10 20:50:05 -06:00
Christopher Haster 42ec282a03 Limited block_size and in-block types to 28-bits
One downside of leb128 encoding is that the worst case encoded size is
not that well aligned due to a relatively underutilized last byte:

  0xffffffff => 0xff 0xff 0xff 0xff 0x0f

This normally doesn't really matter, the whole point of leb128 is that
larger encodings are statistically less likely. But in littlefs we need
to allocate the worst-case buffer size in order to encode/decode
leb128s, and these buffers need to stick around on the stack during
metadata commit calls, which are also the point of highest stack usage
in the system.

But 32-bits is somewhat arbitrary, it just happens to be our register
size. In fact, we're not really using 32-bits, but instead only 31-bits
to take advantage of the sign bit for ad-hoc sum types:

  0x7fffffff => 0xff 0xff 0xff 0xff 0x07

In theory, if we limit this further to 28-bits, we could save some stack
space:

  0x7fffffff => 0xff 0xff 0xff 0xff 0x07
  0x0fffffff => 0xff 0xff 0xff 0x7f

This may seem like a small amount of savings, but it also restores
alignment to the encoding, and should result in less wasted padding
around buffers.

Though it's important to note these are the most valuable bits, as the
range grows exponentially with each bit added. Reducing 31-bits to
28-bits reduces the range from ~2GiB to ~256MiB:

  0x7fffffff => 2,147,483,647
  0x0fffffff =>   268,435,455

---

At the moment I'm hesistant to reduce _all_ on-disk leb128s to 28-bits.

The signed-32-bit limit of ~2GiB is fairly well understood in this
space, mainly thanks to FAT, and reducing this to ~256MiB risks quite a
surprise to users (it's also a regression from the current littlefs
version).

But one type where this limit is pretty reasonable is our block_size.

I don't think we'll see devices with erase blocks >256MiB for a while,
and at the very least those devices will probably need a 64-bit
filesystem for other reasons anyways...

And limiting block_size to <=256MiB has a surprising number of knock-on
effects:

- The tag size/jump field never exceeds 28-bits, reducing worst-case tag
  dsize from 12 bytes -> 11 bytes.

  The also reduces our worst-case attr-estimate from 40 bytes ->
  37 bytes

- rbyd/btree trunks never exceed 28-bits, saving space in shrub/branch/
  btree encodings.

- The bptr encoding is reduced from 24 bytes -> 21 bytes, since several
  of its fields are in-block (size, off, cksize).

- The commit checksum encoding is reduced by a byte for every commit,
  from 12 bytes -> 11 bytes.

  This is due to needing to expand the cksum tag's size field to the
  worst possible leb128 encoding due to a catch-22 situation.

Unfortunately the actual stack savings is a bit underwhelming:

            code          stack
  before:  33688           2808
  after:   33700 (+0.0%)   2800 (-0.3%)

This may be because, by adopting 28-bits in only some fields, most
buffers still end up unaligned and the on-stack size doesn't change due
to padding. Or it could just be that I'm overestimating the cost of our
on-stack buffers.

Still, I think the change is worth keeping if only for the reducing
attr-estimate and saved byte on every on-disk commit.

In the future it would be interesting to explore additional
configurations, e.g. a 28-bit flavor of littlefs to compliment this
31-bit flavor. You could imagine the fitting into other register sized
flavors for different capacity/code cost/device compat tradeoffs:

  flavor               register  leb128   size-limit
  14-bit littlefs  =>  16-bit    2 bytes  ~16KiB
  15-bit littlefs  =>  16-bit    3 bytes  ~32KiB
  28-bit littlefs  =>  32-bit    4 bytes  ~256MiB
  31-bit littlefs  =>  32-bit    5 bytes  ~2GiB
  56-bit littlefs  =>  64-bit    8 bytes  ~64PiB
  63-bit littlefs  =>  64-bit    9 bytes  ~8ExiB

This is where the on-disk size-limit attr would really shine.

---

Note we don't need an additional on-disk limit attr for the block_size.
We already store the block_size in the superblock, so we just need to
error if attempting to mount a filesystem with block_size >256MiB.
2024-02-10 20:49:31 -06:00
Christopher Haster 6439650a0e Renamed ecksum.size -> ecksum.cksize
This matches bptr's cksize/cksum a bit better and helps avoids confusion
when discussing the various size fields used to encode a commit's
various checksum tags.
2024-02-09 17:16:12 -06:00
Christopher Haster a8a738e434 Added some ascii art over the on-disk encodings
I find these little diagrams useful for visualizing the actual on-disk
encoding, which doesn't really exist in the code outside of the
lfsr_data_from* and lfsr_data_read* functions.
2024-02-09 17:16:12 -06:00
Christopher Haster af5e3f7d2a Changed rbyd.weight to unsigned
This should really be unsigned, rbyd weights can not be negative.

Note this is different than data.size, etc, since the signedness there
is used to differentiate the underlying encoding. Accessing data.size
directly is usually an error, though we do access it directly in several
places when assuming the underlying encoding. Signedness warnings are
actually a good thing in that case.
2024-02-09 17:14:32 -06:00
Christopher Haster 6f1d110e01 Changed leb128 related functions to operate on uint32_t
So unsigned instead of signed. The original intention of using int32_t
was to hint that the sign bit should be reserved, but this may just
confuse if our leb128 encoding has a sign representation, which it does
not.
2024-02-09 14:35:23 -06:00
Christopher Haster 9f9653eb79 Dropped grm-specific gdelta functions
Yes these offered a tiny bit of typing savings, but they are used so
infrequently (really just lfsr_mdir_commit and friends) that they aren't
really worth it.

They also sort of break the object-related function pattern, since they
operated gdeltas (uint8_t[]) instead of grms (lfsr_grm_t). It's easy
enough to pass LFSR_GRM_DSIZE where needed.

This would probably only get worse if we add more gstate types.

This had no impact on code/stack. These functions were probably already
inlined.
2024-02-09 14:35:23 -06:00
Christopher Haster bd55822abc Reworked grm handling to prefer xoring, added lfsr_grm_xorgrm
The original motiviation was to make the gstate-related logic a bit more
coherent, but it turns out lfsr_grm_xorgrm is quite useful for
simplifying gstate handling in lfsr_mdir_commit.

As a plus it looks like we save a surprisingly amount of stack cost, but
I think this may just be a symptom of our tooling not being able to
understand shrinkwrapped function calls:

            code          stack
  before:  33716           2832
  after    33692 (-0.1%)   2808 (-0.9%)
2024-02-09 14:35:23 -06:00
Christopher Haster 0fa33b7776 Cleaned up post-mdir-commit state updates a bit
This code is a bit tricky since we need to reference the current mdir to
know how to update other opened mdirs, but then also update the current
mdir, which could also be in the list of opened mdirs. I think a hear a
functional language user laughing in the distance...

            code          stack
  before:  33764           2832
  after:   33716 (-0.1%)   2832 (+0.0%)
2024-02-09 14:35:18 -06:00
Christopher Haster 307d60299f Reinlined mtree/mroot commit logic into lfsr_mdir_commit
I think this is a case where separating the logic out into distinct
functions does more harm than good, by making it harder to understand
how all the different moving parts interact.

This is especially important for lfsr_mdir_commit, since this is where
all atomic operations in the filesystem get tied together. Having atomic
updates complete in different functions was particularly concerning
since it carries some implicit requirements (must not error after!).

The end result is a cumbersome function, but at least internally
relatively straightforward in how the commit propagates through the
mtree/mroot chain and internal state.

---

The other benefit of inlining is better code deduplication, since we
can treat the mroot as a normal mdir until it triggers a split or
relocation.

We can also deduplicate the grm patching, though there may be a better
way to implement this. There are still some awkward bits in the logic.

            code          stack
  before:  33856           2888
  after:   33764 (-0.3%)   2832 (-2.0%)
2024-02-08 13:18:43 -06:00
Christopher Haster 4b27c93f52 Brought back the opened namespace
- lfsr_isopened     -> lfsr_opened_isopen
- lfsr_addopened    -> lfsr_opened_add
- lfsr_removeopened -> lfsr_opened_remove
- lfsr_mid_isopened -> lfsr_mid_isopen
2024-02-06 17:10:38 -06:00
Christopher Haster 3dab5367a5 Dropped LFS_ERR_BADF
We just don't use this error since we assert. Having it in the error
enum may give the wrong impression we return it at points.

If we even end up needing it, it can be readded to the list.
2024-02-06 17:05:20 -06:00
Christopher Haster 31745c5835 Renamed traversal/iteration variables to one letter names
Hey if it's good enough for iterators (i), it's good enough for our
other traversals/iterators:

- iterator(?)   -> i
- traversal     -> t
- opened        -> o

Expressions involving these variable were getting quite long. At least
now our common opened-list iterator can take only one line.

This reduces lfs.c by 41 lines (16851 -> 16810).

I do wonder if the use of "o" as a variable will limit my future
employment opportunities though.
2024-02-06 16:55:03 -06:00
Christopher Haster c0e9406b0b Reverted to mweight -> mleaf_weight and made lfs_t const
We have bleafs (bleaves?) now, so the mleaf name just makes too much
sense. Even though it's used nowhere else outside of mid decoding, and
may be a bit confusing.

After all this time it feels weird to use a const lfs_t parameter, but
that's really what the mid/mleaf functions should take. These functions
are a bit of a special case as lfsr_mleafweight really wants to just be
a constant.

Code size did not change.
2024-02-06 15:55:17 -06:00
Christopher Haster 4e851c2d88 Added a couple attr-related helper functions
Some relatively-annoying states to check for:

- lfsr_attr_isnoop
- lfsr_attr_isinsert

And some accessors for marshalled pointers used by internal tags:

- lfsr_attr_grm
- lfsr_attr_mdir
- lfsr_attr_shrubcommit
- lfsr_attr_shrubtrunk
2024-02-06 15:32:08 -06:00
Christopher Haster 204f46a131 Reworked internal tests remove unnecessary shim functions
These shims, originally intended to remap the tests to new internal
APIs without a significant rewrite, are a long-outstanding piece of
technical debt. Now that the internal API is more stable, it's time for
that rewrite.

Reasons for not keeping the internal shims:

- They add more complexity to the test suites.
- They come with (out-of-date) constraints that limit what we can test.
- It's more difficult to debug test failures, with 2 layers and all.

I ended up writing a small tree editor out of tree to do most of this
rewrite.

Did it save time? Probably not. But it was quite a bit more fun than
manaully rewriting ~21K lines of code.
2024-02-03 18:39:13 -06:00
Christopher Haster 921fe2ba1b Tweaked documentation of implicit enums in test defines 2024-02-03 18:17:17 -06:00
Christopher Haster 5e633aa554 Switched from decimal to hexidecimal for test name suffixes
This compresses a bit better, which is useful since our dbg scripts
truncate into tight prefixes:

- 3 decimals     => 999  = <1000
- 3 hexidecimals => fff  = <4096
- 4 decimals     => 9999 = <10000
2024-02-03 18:17:15 -06:00
Christopher Haster 66a557d19d Dropped all alpha lookup table for 'a'+mod 26 arithmetic
I'm not really sure why I thought this required a lookup table...
2024-02-03 18:17:13 -06:00
Christopher Haster 15cd1d29e0 Added explicit test over directory ordering
It turned out the previous version had a subtle ordering bug when names
where the same length that went unnoticed for years. And at this point
is probably baked into the on-disk format permanently.

This redesign, with a named-ordered btree, relies quite a bit more on
name ordering, so it's unlikely the same mistake would make it through
without breaking something. And sure enough this bug was unintentionally
fixed at some point.

But still, better safe than sorry. Added tests over character ordering
and length ordering. Open to more ordering tests in the future.

Found by andriyndev
2024-02-03 18:17:12 -06:00
Christopher Haster 40b926b947 Removed mid argument from lfsr_mdir_lookup*
With lfsr_mdir_t being a logical cursor pointing to a specific metadata
entry in the on-disk mdir, we don't really need the mid to be provided
on every lookup call (may have jumped the gun a bit in the attr-list
changes).

In the rare case we need to lookup unrelated mids, we call always call
lfsr_rbyd_lookup on the underlying rbyd.

This saves a little bit of code/stack:

            code          stack
  before:  33964           2896
  after:   33852 (-0.3%)   2888 (-0.3%)
2024-02-03 18:17:10 -06:00
Christopher Haster d30ed42c7f Ported/cleaned up mtree tests
Well this was quite tedious, but these tests are valuable since our
mtree has a number of hard-to-reach edge cases.

This mainly ports over to the new attr-list format for mdir commits, but
also cleans up a couple of lingering tedious TODO things:

- mtree tests now use the new mdir commit attr-list format.

- Reoriented most tests to use namelookups instead of mid lookups.

  Using mid lookups in testing is/was really fragile, since it depends
  on exactly how mids get split and moved around.

  namelookups are more robust, by design they don't care about the
  underlying mtree structure. And really, namelookups are what we care
  about in the mtree, mids are just a mechanism for mtree updates to
  work.

  We don't remove all mid checks though, we just compare against
  namelookup-derived mids when it matters (the mtree_opened tests for
  example).

- The names we use in testing have also been updated to no longer create
  invalid mtrees, i.e. names are ordered correctly and always have a
  did.

  The previous mess always risked triggering asserts with false
  positives.

- By adopting namelookup in the tests, we can actually test the on-disk
  state of fuzz testing.

  Though note we can't change names once written, without invalidating
  our mtree. This limits fuzz testing a little bit, but it's still  a
  big improvement over the previous fuzz tests.

- Dropped mtree tests that no longer really make sense.

  Mainly that the did should never be deleted, so you can never end up
  with an empty mtree, dropping the left-most mdir, etc.

  There were still a few of things lingering around.

With this, all tests are working again with the attr-list changes. Wooh.
2024-02-03 18:17:09 -06:00
Christopher Haster 3a90d1046b Reverted insert tags appending, fixed insert issues in named btrees
Changing insert tags to append seems to have broken insertion into named
btrees in a subtle way.

Consider what happens when we insert immediately before a bid that
splits the btree:
1. namelookup returns the right rbyd, with rid=-1
2. converting this into a bid gives us the left rbyd, with rid=weight
3. the commit to insert the bid ends up inserting into the left rbyd

This doesn't initially seem like an issue, both entries are effectively
the same right? Well, not when you have names. The split name tells you
what _follows_, so this unintentional flipping causes the new name to
get placed in the wrong bucket.

It's not clear if it's possible to fix this, at least not without
inverting the split names to indicate what precedes, but that's a step
too far.

This was not detected earlier because I disabled the low-level
rbyd/btree/mtree tests temporarily due to high porting cost. Guess that
goes to show there's a cost to deferring test ports for too long.

---

This issue, along with being inconsistencies between rids/bids and mids,
and being a relatively unintuitive pattern, is the final nail in the
coffin for insert tags inserting after.

Now, insert tags insert before, like in most other systems, and insert
tags in attr-list just have an implicit +1 before them to allow splits
in attr-lists to work.

This is not a pure revert, as some of the changes with all the code
moving around revealed some better detail-level ideas.

And yes, rbyd/btree tests are up to date now. Unfortunately the mtree
tests require a bit more work.

---

One thing definitely worth noting, btree merges were broken! A mistake
in the has-parent condition meant we were never attempting to merge
btrees!

This hid some bugs in the actual btree merge code caused by mixing the
implicit swap of child rbyds to deduplicate code paths with btree commit
now needing to track bid/rid separately from the attr-list.

This should be fixed now. Interesting to note this bug has been in
lfsr_btree_commit_ for a while now! I think ever since we switched to
using trunks for the has-parent check. We just haven't been merging
btree nodes at all. But since not-merging isn't technically an error,
it's difficult to test for.

Code changes:

            code          stack
  before:  33808           2896
  after:   33964 (+0.5%)   2896 (+0.0%)
2024-02-03 18:17:07 -06:00
Christopher Haster 7868ec7122 Ported over most rbyd+btree tests to new attr-list format
Found a bug, and maybe a fundamental issue:

- The lfs_btree_lookupnext_ in lfsr_btree_commit_ no longer needs the
  min32, since we never commit with bid pointing past the end of the
  btree anymore.

  This was mixing the unsigned min32 with our now-signed bid type,
  causing the wrong btree leaf to be fetched when inserting at bid=-1 in
  a non-empty btree.

  Easy fix.

- lfsr_btree_commit_ with bid!=-1, rid=-1 (inserting at the beginning of
  not-the-first rbyd) now actually appends to the leaf to the left of
  the rbyd instead of inserting into the expected rbyd because of how
  lfs_btree_lookup_ works.

  Initially, this doesn't seem like it would be an issue, these should
  be more-or-less equivalent, but this doesn't match
  lfsr_btree_namelookup! This is a big problem!

  This wasn't noticed because it's rare for the high-level tests to
  trigger that many btree splits with names. Named btrees are only used
  for the mtree, and we need mdirs to split before the mtree even splits
  once.

  Not an easy fix.

On the upside, these low-level tests continue to prove themselves
valuable, if tedious to maintain...
2024-02-03 18:17:06 -06:00
Christopher Haster f323ea1bda Made mkdir tests a bit more paranoid
Unfortunately the current dir+bookmark+grm design has a high risk of
the system becoming out-of-sync and losing bookmarks. In theory this
should cause test failures, but the previous grm-mid-off-by-one bug has
left me a bit paranoid.

So when dbglfs.py starting flashing bookmark errors, I started
investigating. But just I can't reproduce these errors in a controlled
way, and they cause no test failures...

My current setup involves this script to copy the disk file
"atomically", so even though dbglfs.py is slow, we shouldn't be reading
blocks from different filesystem states. Uh, beauty is in the eye of the
beholder and all that jazz?:

  ./scripts/watch.py -b -Kdisk bash -c "cp disk disk_ \
      && ./scripts/dbglfs.py disk_ -B4096 \
          --color=always -s -a -T -f -g 2>&1 \
      | head -n32"

But some brief investigation suggests cp is not atomic. After all, how
could it be?

My guess is we occasionaly catch blocks from different filesystem states
when a write occurs during a cp operation. So a false positive.

Still, might as well keep these extra asserts for a bit of extra
confidence we're not losing bookmarks during heavy mkdir operations.
2024-02-03 18:17:04 -06:00
Christopher Haster b09e933e1e Eagerly discard attr-list in lfsr_mdir_commit__
This is an interesting optimization made possible by our attr-lists now
only operating on one mid. We can now discard the entire attr-list based
on if that mid is in the commit's filter range.

Unfortunately, while I had hoped this would lead to more
simplifications, we can't really push this up through many functions:

- While this eager discard works for splits, lfsr_btree_commit can also
  merge, which affects two separate bids. The attrs on these bids need
  to be split over the new btree inner-nodes, so we end up still needing
  filtering in lfsr_rbyd_appendattrs.

  We could move the filtering up into lfsr_btree_commit, but would that
  really gain anything?

- lfsr_mdir_commit_ needs the filter range because we leverage this in
  higher-layers to for lfsr_mdir_commit_ to omit non--1 attrs when
  committing to newly hollow mroots.

  In theory it might still be possible to push this up into
  lfsr_mdir_commit, but we would still need to unconditionally commit
  during mdir splits to append the cksum. This ends up with duplicate
  function calls which ends up annoyingly expensive. Though maybe there
  is a conditional count trick that could avoid this?

At least the code changes are ok:

            code          stack
  before:  33884           2896
  after:   33808 (-0.2%)   2896 (+0.0%)
2024-02-03 18:17:02 -06:00
Christopher Haster 4ebc7d0119 Reverted specifically mids to insert _before_ the current mid
This unfortunately makes inserts inconsistent between rbyd/btrees:

  insert(rid=-1) => rid=0
  insert(bid=-1) => bid=0
  insert(mid=0)  => mid=0

But seems to integrate the best throughout the rest of the codebase:

- No awkward rid=-1 encoding in the mid, mid=1.2 => bid=1, rid=2

- No need to tweak mid encoding when writing grms to disk

- Behavior of unrelated files in the mdir behave consistently
  irregardless of if our tag is an insert or not:

  - mid' >= mid => mid'=mid'+delta
  - mid' <  mid => mid'=mid'

  This is convenient because only the mid updates trigger tweaks of
  unrelated mids, rbyds/btrees don't really have this problem.

- We already have to do a bit of tweaking in lfsr_mdir_namelookup, since
  we're converting from "buckets" in the rbyd to ids we'd insert into.

  Mainly namelookup of left-most name returns rid/bid=0, but for mdirs
  should return mid=-1 (now mid=0):

                  left-most  left-most+1  left-most+2
    rid/bid:              0            0            1
    mid (before):        -1            0            1
    mid (after):          0            1            2

I think this may be a reasonable compromise between allowing splits in
rbyd/btrees, and intuitive behavior for insertions in the mdirs.

That, or I've just been staring at this code for too long...

            code          stack
  before:  33876           2896
  after:   33888 (+0.0%)   2896 (+0.0%)
2024-02-03 18:17:01 -06:00
Christopher Haster eb7c48fbd0 Fixed on-disk grm representation being off-by-one
The recent change to internally track mids as mid=mid+1 leaked onto disk
through the grm. This is currently the only place we actually write mids
to disk.

The mid=mid+1 encoding is a bit of a hack and probably should not be the
actual on-disk representation, since there are other ways to encode this
internally.

I did try to write some tests for this, but because the bug is on both
the encoding and decoding side it's difficult without reading the mdir
directly. I only noticed with the dbg scripts started throwing random
errors. Fortunately a regression here is unlikely.
2024-02-03 18:16:59 -06:00
Christopher Haster f2e8fdb5f1 Changed insert tags to insert _after_ the current rid
This atypical but not unreasonable behavior (most array insert functions
I've ran into like to insert _before_ the current index) makes split
commits no longer special behavior of appendattrs/commit, and seems to
fit better into rbyd append logic (though admittedly, some of the rbyd
append logic gets really weird with the whole right-leaning business).

Though this does come with a couple downsides:

- All rbyd-based data structures need to be able to represent a -1 id
  so we can insert into the first id. This is not a problems for
  rids/bids, but we need to tweak mids to support mid.rid=-1.

  The best solution I could come up with was to just increment rid by
  one, so, assuming mbits=8:

  - mid=0x100 => bid=0x100, rid=-1
  - mid=0x101 => bid=0x100, rid=0
  - mid=-1    => bid=-1,    rid=-1

- We need to be really careful with splits over our attr-list, since
  these can line up between the rid create tags reference and other
  following tags intended to stick to the new rid.

  This required some special handling in lfsr_rbyd_appendattrs and
  lfsr_mdir_commit__.

Other than that this change is quite promising, and removed what felt
like a bunch of hacks adjusting mids in lfsr_file_carve.

            code          stack
  before:  33992           2904
  after:   33868 (-0.4%)   2896 (-0.3%)
2024-02-03 18:16:58 -06:00
Christopher Haster aa0fe6c12b Dropped LFSR_ATTR_ and LFSR_ATTR_IF
It turns out we don't really need these
2024-02-03 18:16:57 -06:00
Christopher Haster 33ac8bfc80 Moved rids out of attr-lists
It turns out we never really need to commit to two unrelated rids in a
single commit. And some data structures, mainly btrees/bshrubs, don't
even allow commits to unrelated rids.

Well, sort of. There are some cases that seem to require unrelated rids,
but these are easy enough to work around:

1. btree/mdir splits/merges end up with two rids - but these either
   converge or diverge from one rid, so as long as we assume sequential
   inserts/deletes operate on the _neighboring_ rid, things work out.

2. grms/etc commit to mid=-1 irregardless of the file mid - but these
   are also very special flags that are already handled differently to
   manage the global state updates, nothing new was needed here.

So, in theory, we can move the rids out of the lfsr_attr_t struct and
infer and rid changes as we play out the attr-list, saving 4 bytes
(~17%) from every attr we allocate on the stack.

As a plus, we remove the need to manually calculate the changes to the
rid in the attr-list, reducing the likelihood of bugs here and saving a
decent amount of code.

Unfortunately the code/stack savings from this change were a bit
disappointing. The extra rid parameter in every commit function added
quite a bit of overhead, and we have to do some funky memmoves in
lfsr_file_carve to account for the new strict attr-list order:

            code         stack          lfsr_attr_t
  before:  33924          2912                   24
  after:   33992 (+0.2%)  2904 (-0.3%)           20 (-16.7%)

Still, this decreases the amount of code that can contain bugs, and more
closely matches the actual behavior of lfsr_btree/bshrub_commit.

Someone should really get around to updating the rbyd/btree/mtree
tests... Well, at least the non-internal (dir/dread/file/fwrite/etc)
tests are working.
2024-02-03 18:16:55 -06:00
Christopher Haster e04748dadd Renamed SUB/SUPWIDE -> SUB/SUPMASK
This name makes more sense to me given what these bits are doing. Though
that may just be from the embedded engineer side.
2024-02-03 18:16:54 -06:00
Christopher Haster 3c13afd5c2 Added explicit test over unreachable tag holes
Unreachable tag holes, null tags that _should_ be unreachable but
actually are reachable, are an unfortunate quirk to our alt tag
encoding. Because we only have an altgt, not altge, our "unreachable"
tag ends up encoded with an altgt 0, an alt, which you may notice, does
not guarantee unreachability.

Fortunately, tag 0, the null tag, should intentionally be unused. So as
long as we never lookup tag 0, nothing should break.

If you do lookup tag 0, you end up with spurious null tags, which can
complicate things.

The solution here is a tag_ = max(tag, 1) in lfsr_rbyd_lookupnext.

---

One interesting thing to note, as I was writing these tests I discovered
that setting tag=max(tag,1) in lfsr_rbyd_appendattr had no effect.
appendattr needs zip the rbyd tree to keep everything connected during
range removals, so tag=0/tag=1 both end up with the same tree.

So might as well drop the tag=max(tag,1) in lfsr_rbyd_appendattr.

A side effect of this, both before and after this commit, is that any
null tag holes created during range removals sort of stick around until
the next compaction.

---

Why altgt and not altge? altgt is the inverse of altle, requiring only
a single bit flip to flip between the two. And trust me, it would be
much more costly to make altle/altgt flips more complicated than a bit
flip.

---

Why altgt/altle and not altge/altlt? This is because our rbyds are
right-leaning, that is, lookups always find the requested rid+tag, or
the next smallest rid+tag.

Consider a simple tree:

       <5
  .----'|
 >=2    |
  |'-.  |
  1  2  5

What should lookup(3) return? If we are right-leaning, the answer
_should_ be 5. But we need to take the <5 branch to determine if there
is a hidden 3 or 4 in that subtree.

altgt/altle does not have that problem:

      <=2
  .----'|
  >1    |
  |'-.  |
  1  2  5

It might seem like you can workaround this by conservatively using the
neighbor +1 as the alt target, but this runs into tag overflow problems.
UATTR(0xff)+1 (0x057f+1) becomes UATTR(0x100) (0x0580) which is not
allowed due to reserving bit 7 for future subtype extensions.

Maybe you can workaround this workaround by using (tag+0x81)&~0x80
anywhere you need to increment (including lookupnext/iteration calls!),
but this becomes a bit of a mess. And there are still concerns about
overflows at the 0x77f boundary and 0xf7f boundary.
2024-02-03 18:16:52 -06:00
Christopher Haster 5f25f32ff1 Adopted SUPWIDE tag bit, parallel to the SUBWIDE (was WIDE) bit
Like SUBWIDE, SUPWIDE allows for "mask-like" operation during rbyd
commits, where you replace an entire subrange of tags with a single tag.

- SUBWIDE - Replace all subtypes of the given suptype - Useful for
  changing the subtype of an attr, for example replacing a BTREE with a
  BSHRUB.

- SUPWIDE - Replace all suptypes of the given rid - Useful for changing
  the suptype of an attr, for example replacing a REG file with an
  ORPHAN file.

These are effectively the same modifier, just with different ranges.

One benefit is this simplifies mid-level operations a bit, rename,
remove, etc, and decreases the stack cost of the related attr lists.
Though this isn't on the hot-path, so not measurable:

            code          stack
  before:  33956           2912
  after:   33928 (-0.1%)   2912 (+0.0%)

But the real motivation for this change is to remove cases where
lfsr_mdir_commit needs to operate on multiple mids. There may be an API
simplification here.
2024-02-03 18:16:50 -06:00
Christopher Haster e8b1be17fe Added bshrub support to dbgbmap.py
Forgot about this script.
2024-02-03 18:16:49 -06:00
Christopher Haster 991f04a4fb Dropped shrub struct, shoved shrub.estimate into shrub.eoff
We still have an lfsr_shrub_t, it's just a simple alias of lfsr_rbyd_t.

The only difference between these two structs was that lfsr_rbyd_t had
the eoff/cksum fields, to enable incremental commits, and lfsr_shrub_t
had the estimate field, to keep track of the current shrub estimate so
we evict before overflow.

Unfortunately C makes this overlap a bit annoying. We can either add a
union, making a mess of field accesses, or use probably problematic
casting of structs with common initial sequences.

Instead of dealing with this headache, I'm just going to shove the
shrub estimate into the rbyd's eoff field and ignore the name abuse.

In normal rbyd use, eoff does effectively contain the on-disk size of
the rbyd, so it's not too far from its intended use...

This does move our estimate to overlap the eoff field instead of the
cksum field, which means we need to be a bit more careful about setting
erased state for btrees. This adds a small code cost:

            code          stack
  before:  33928           2912
  after:   33956 (+0.1%)   2912 (+0.0%)
2024-02-03 18:16:47 -06:00
Christopher Haster bea13dcf8e Use sign bit of rbyd.trunk to indicate shrubness of rbyds
Shrubness should have always been a property of lfsr_rbyd_t.

You know you've made a good design decision when things just sort of
fall into place and the code somehow becomes cleaner.

The downside of this change is accessing rbyd trunks requires a mask,
which is annoying, but the upside is we don't need to signal shrubness
via extra booleans in internal functions anymore.

The funny thing is, the actual motivation for this change is was just to
free up a bit in our tag encoding. Simplifying some of the internal
functions was just a nice side effect.

            code          stack
  before:  33940           2928
  after:   33928 (-0.0%)   2912 (-0.5%)
2024-02-03 18:16:45 -06:00
Christopher Haster 4ce582bf9b Adopted case-as-label style in switch statements
So:

  switch (cond) {
  case 0:;
      // first case
      break;

  case 1:;
      // second case
      break;

  default:;
      // default case
      break;
  }

This basically adopts our current label style for the case statements in
switch statements. It initially looks like quite a monstrosity, but I
think it does a good job at highlighting that case statements in C are
no safer than labels and gotos.

I would not use this style in a language with better scoping in switch
statements.

I'd prefer not to use switch statements, their scoping rules in C are
just too error-prone, and the compiler usually optimizes things out
anyways, but there are some places where switch statements are clearly
the correct organization -- state machines such as lfsr_traversal_read
for example.

If you're curious about the ':;' ending, this is used in our current
style for labels to avoid "declaration is not a statement" warnings.
Which I think is just a bit of leftover from C historically not having
mixed statements/declarations.
2024-02-03 18:16:44 -06:00
Christopher Haster 6fc040db1a Adopted paren-cond ternary operator style
So:

  x = (cond) ? yes : no;

Where there are always parentheses around the condition, even if not
required for disambiguity. Additional parentheses are always allowed,
but the parenthesized condition helps signal that a ternary operator is
coming earlier in the expression.

This style has grown on me as I think it helps code readability. It
reminds me of the required parentheses for if/while statements.

Might as well adopt codebase-wide.
2024-02-03 18:16:42 -06:00