Commit Graph

774 Commits

Author SHA1 Message Date
Christopher Haster ade563cc24 scripts: Removed outdated non-terminating warning from scripts
All of these scripts have cycle detectors now, so this warning should
not longer be valid.
2024-11-04 18:26:22 -06:00
Christopher Haster c0a9af1e9a scripts: Moved recursive entry generation before table rendering
This fixes an issue where mixing recursive renderers (-t/--hot or
-z/--depth) with defines (-Dfunction=lfsr_mount) would not account for
children entry widths. An unexpected side-effect of no longer filtering
the children entries.

We could continue to try to estimate the width without table rendering,
but it would basically need two full recursive pass at this point...
Instead, I've just moved the recursive stuff before table rendering,
which should remove any issues with width calculation while also
deduplicating the recursive passes.

It's invasive for a small change, but probably worthwhile long term.

The downside is this does mean our recursive scripts now build the full
table (including all recursive calls!) before they start printing. When
mixed with unbounded recursive depth (-z0 or --depth=0) this can get
quite large and cause quite a slow start.

But I guess that was the tradeoff in adopting this sort of intermediate
table rendering... At least it does make the code simpler and less bug
prone...
2024-11-04 18:18:58 -06:00
Christopher Haster 0c3868f92c scripts: Fully connected graph in stack.py, no more recursive folding
This makes -D/--define more useful in stack.py/perf.py/perfbd.py by no
longer hiding undfined children entries.

For example:

  $ ./scripts/stack.py lfs.ci lfs_util.ci -Dfunction=lfsr_mount -t
  function                       frame    limit
  lfsr_mount                        96     2816
  |-> lfsr_fs_gc                    80     2720
  |-> lfsr_mtree_gc                176     2640
  |-> lfsr_mdir_commit             576     2464
  ... snip ...

Now shows all functions in the hot path of lfsr_mount, where before it
would only show functions in the hot path of lfsr_mount that were also
_named_ lfsr_mount.

The previous behavior was technically not wrong... but not very useful
(and confusing).

---

This was actually quite a bit annoying to get working because of the
possibility of function call cycles.

I ended up turning stack.py's result type into a fully connected graph,
which only works because Python has a cycle detector. (Actually this
script is so short-lived we probably wouldn't care if this leaked
memory.)

A nice side effect of this is now all the recursive scripts (stack.py,
perf.py, and perfbd.py) share the same internal result representation
and recursive printing logic, which is probably a good thing.
2024-11-04 18:12:57 -06:00
Christopher Haster 711cebfcf3 scripts: Simplified memoization of stack.py's limit calculation
While a decorator does a good job of separating concerns here, it's a
bit overkill for a single function.
2024-11-04 18:09:33 -06:00
Christopher Haster 48804c1236 scripts: Renamed -P/--propagate -> -g/--propagate
To better match -z/--depth and -t/--hot.

The fact that these short forms all don't match the first letter of the
long forms is humorous but unintentional. There's only so many letters
in the alphabet!
2024-11-04 18:05:12 -06:00
Christopher Haster d324333903 scripts: Fixed names/lines falling out of sync in diff table renderers
As a convenience, -d/--diff in our measurement scripts hides entries
that are unchanged by default.

Unfortunately this was broken during a recent refactor that ended up
filtering the line info but not the actual names.

Instead of reverting the broken part of the refactor, I've just moved the
filtering up to where we calculate the names. Hopefully this fixes the
bug while also simplifying this messy chunk of a logic a bit.
2024-11-04 18:04:58 -06:00
Christopher Haster e32af5cd8a scripts: Added -t/--hot to recursive scripts, stack.py, etc
This is mainly useful for stack.py, where -t/--hot lets you quickly see
everything that contributes to the stack limit for each function.

This was (and still is) possible with -s + -z, but it was pretty
annoying to use:

- The stack trace rendered _diagonally_ as a consequence of -z, which is
  probably the worst use of screen real estate.

- This trick only really worked with -s, which was the opposite order of
  what you usually want on the command line: -S.

Adding a special for-purpose -t/--hot flag makes looking at the hot path
much easier, at the cost of more hacky python code (and I _mean_ hacky,
making the hot path selection useful while following exising sort rules
was annoyingly complicated).

Also added -t/--hot to perf.py and perfbd.py for consistency, though it
makes a bit less sense there.

Also also reworked related code in all three scripts: stack.py, perf.py,
perfbd.py. The logic should be a bit more equivalent, and
perf.py/perfbd.py detect cycles now.
2024-11-04 18:03:59 -06:00
Christopher Haster 904c2eddd7 scripts: Memoized stack.py's limit calculation
This is a pretty classic case for memoization. We don't really need to
recalculate every stack limit at every call site.

Cuts the runtime in half:

  before: 0.335s
  after:  0.139s (-58.5%)

---

Unfortunately functools.cache was not fit for purpose. It's stuck using
all parameters as the key, which breaks on the "seen" parameter we use
for cycle detection that otherwise has no impact on results.

Fortunately decorators aren't too difficult in Python, so I just rolled
my own (cache1).
2024-11-04 17:54:42 -06:00
Christopher Haster fc45af3f6e scripts: Added better cycles detection to stack.py
stack.py actually already had a simple cycle detector, since we needed
one to calculate stack limits without getting stuck.

Copying this simple cycle detector into the actual table rendering code
lets us print a nice little "cycle detected" message, instead of just
vomiting to stdout forever:

    $ ./scripts/stack.py lfs.ci lfs_util.ci -z -s
    function                       frame    limit
    lfsr_format                      320        ∞
    |-> lfsr_mountinited             304        ∞
    |   |-> lfsr_mountmroot           80        ∞
    |   |   |-> lfsr_mountmroot       80        ∞ (cycle detected)
    |   |   |-> lfsr_mdir_lookup      48      576
    ... snip ...

The cycle detector is a bit naive, just building a new set each step,
but it gets the job done.

As for perf.py and perfbd.py, it turns out they can't actually create
cycles, so no need for a cycle detector. This is good because I didn't
really want to test these scripts again :)
2024-11-04 17:54:11 -06:00
Christopher Haster f539d3341c attrs: (Re)implemented lfsr_setattr/getattr/etc
These functions provide simple access to littlefs's custom attributes,
which are small pieces of user-specified metadata that can be attached
to files, dirs, root, etc:

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

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

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

Notable changes:

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

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

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

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

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

- No more padding with zeros!

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

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

- lfsr_setattr now takes flags.

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

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

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

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

---

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

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

Of course the real cost of custom attributes is how they interact with
open files, a detail which is conveniently missing for now...
2024-08-22 19:49:18 -05:00
Christopher Haster 4d8bfeae71 attrs: Reduced UATTR/SATTR range down to 7-bits
It would be nice to have a full 8-bit range for both user attrs and
system attrs, for both backwards compatibility and maximizing the
available attr space, but I think it just doesn't make sense from an API
perspective.

Sure we could finagle the user/sys bit into a flags argument, or provide
separate lfsr_getuattr/getsattr functions, but asking users to use a
9-bit int for higher-level operations (dynamic attrs, iteration, etc) is
a bit much...

So this reduces the two attr ranges down to 7-bits, requiring 8-bits
total to store all possible attr types in the current system:

  TAG_ATTR      0x0400  v--- -1-a -aaa aaaa
  TAG_UATTR     0x04aa  v--- -1-- -aaa aaaa
  TAG_SATTR     0x05aa  v--- -1-1 -aaa aaaa

This really just affects scripts, since we haven't actually implemented
attributes yet.

Worst case we still have the 9-bit encoding space carved out, so we can
always add an additional set of attrs in the future if we start running
into attr pressure.

Or, you know, just turn on the subtype leb128 encoding the 8th subtype
bit is reserved for. Then you'd only be limited by internal driver
details, probably 24-bits per attr range if we make tags 32-bits
internally. Though this would probably come with quite a code cost...
2024-08-22 00:59:09 -05:00
Christopher Haster c00e0b2af6 Fixed explicit trunks messing with canonical checksums
Updating the canonical checksum should only depend on if the tag is a
trunkish tag (not a checksum tag), and not if the tag is in the current
trunk. The trunk parameter to lfsr_rbyd_fetch should have no effect on
the canonical checksum.

Fixed in boath lfsr_rbyd_fetch and scripts.

Curiously no code changes:

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

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

---

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

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

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

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

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

---

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

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

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

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

But oh well. Note that some of these changes are good even without
ckreads, such as only parsing the last ecksum tag.
2024-08-16 01:04:03 -05:00
Christopher Haster 1044c9d2b7 Adopted odd-parity-zero rbyd perturb scheme
I've been scratching my head over our rbyd perturb scheme. It's gotten
rather clunky with needing to xor valid bits and whatnot.

But it's tricky with needing erased-state to be included in parity bits,
while at the same time excluded from our canonical checksum. If only
there was some way to flip the checksums parity without changing its
value...

Enter the crc32c odd-parity zero: 0xfca42daf!

This bends the definition of zero a bit, but it is one of two numbers in
our crc32c-ring with a very interesting property:

  crc32c(m) == crc32c(m xor 0xfca42daf) xor 0xfca42daf  // odd-p zero
  crc32c(m) == crc32c(m xor 0x00000000) xor 0x00000000  // even-p zero

Recall that crc32c's polynomial, 0x11edc6f41, is composed of two
polynomials: 0x3, the parity polynomial, and 0xf5b4253f, a maximally
sized irreducible polynomial. Because our polynomial breaks down into
two smaller polynomials, our crc32c space turns out to not be a field,
but rather a ring containing two smaller sub-fields. Because these
sub-fields are defined by their polynomials, one is the 31-bit crc
defined by the polynomial 0xf5b4253f, while the other is the current
parity.

We can move in the parity sub-field without changing our position in the
31-bit crc sub-field by xoring with a number that is one in the parity
sub-field, but zero in the 31-bit crc sub-field.

This number happens to be 0xf5b4253f (0xfca42daf bit-reversed)!

(crcs being bit-reversed will never not be annoying)

So long story short, xoring any crc32c with 0xfca42daf will change its
parity but not its value.

---

An that's basically our new perturb scheme. If we need to perturb, xor
with 0xfca42daf to change the parity, and after calculating/validating
the checksum, xor with 0xfca42daf to get our canonical checksum.

Isn't that neat!

There was one small hiccup: At first I assumed you could continue
including the valid bits in the checksum, which would have been nice for
bulk checksumming. But this doesn't work because while valid bits cancel
out so the parity doesn't change, changing valid bits _does_ change the
underlying 31-bit crc, poisoning our checksum and making everything a
mess.

So we still need to mask out valid bits, which is a bit annoying.

But then I stumbled on the funny realization that by masking our valid
bits, we accidentally end up with a fully functional parity scheme.
Because valid bits _don't_ include the previous valid bit, we can figure
out the parity for not only the entire commit, but also each individual
tag:

  80 03 00 08 6c 69 74 74 6c 65 66 73 80
  ^'----------------.---------------' ^
  |                 |                 |
  v       +       parity      =       v'

Or more simply:

  80 03 00 08 6c 69 74 74 6c 65 66 73 80
  '----------------.----------------' ^
                   |                  |
                 parity       =       v'

Double neat!

Some other notes:

- By keeping the commit checksum perturbed, but not the canonical
  checksum, the perturb state is self-validating. We no longer need to
  explicitly check the previous-perturb-bit (q) to avoid the perturb
  hole we ran into previously.

  I'm still keeping the previous-perturb-bit (q) around, since it's
  useful for debugging. We still need to know the perturb state
  internally at all times in order to xor out the canonical checksum
  correctly anyways.

- Thanks to all of our perturb iterations, we now know how to remove the
  valid bits from the checksum easily:

    cksum ^= 0x00000080 & (tag >> 8)

  This makes the whole omitting-valid-bits thing less of a pain point.

- It wasn't actually worth it to perturb the checksum when building
  commits, vs manually flipping each valid bit, as this would have made
  our internal appendattr API really weird.

  At least the perturbed checksum made fetch a bit simpler.

Not sure exactly how to draw this with our perturb scheme diagrams,
maybe something like this?

  .---+---+---+---. \   \   \   \
  |v|    tag      | |   |   |   |
  +---+---+---+---+ |   |   |   |
  |     commit    | |   |   |   |
  |               | +-. |   |   |
  +---+---+---+---+ / | |   |   |
  |v|qp-------------->p>p-->p   .
  +---+---+---+---+   | .   .   .
  |     cksum     |   | .   .   .
  +---+---+---+---+   | .   .   .
  |    padding    |   | .   .   .
  |               |   | .   .   .
  +---+---+---+---+   | |   |   |
  |v------------------' |   |   |
  +---+---+---+---+     |   |   |
  |     commit    |     +-. |   +- rbyd
  |               |     | | |   |  cksum
  +---+---+---+---+     / | +-. /
  |v----------------------' | |
  +-------+---+---+         / |
  |     cksum ----------------'
  +---+---+---+---+
  |    padding    |
  |               |
  +---+---+---+---+
  |     erased    |
  |               |
  .               .
  .               .

---

Code changes were minimal, saving a tiny bit of code:

           code          stack
  before: 36368           2664
  after:  36352 (-0.0%)   2672 (+0.3%)

There was a stack bump in lfsr_bd_readtag, but as far as I can tell it's
just compiler noise? I poked around a bit but couldn't figure out why it
changed...
2024-08-16 01:03:43 -05:00
Christopher Haster dd339e3090 scripts: Added parity.py for quick parity calculation
This is probably overkill, but who really wants to be calculating parity
by hand...
2024-08-01 16:15:34 -05:00
Christopher Haster fb73f78c91 Updated comments to prefer "canonical checksum" for rbyd checksums
I think this describes the goal of the non-perturbed rbyd checksums
decently. At the very least it's less wrong that "data checksum", and
calling it the "metadata checksum" would just be confusing. (Would our
commit checksum be the "metametadata checksum" then?)
2024-07-31 12:29:13 -05:00
Christopher Haster 9a9b3fc161 scripts: Reverted crc32c.py to naive CRC implementation
The naive implementation is simpler, less code, and more likely to be
correct, each of these are more valuable than speed in our debug
scripts.

We're in Python anyways (no offense Python!).

Plus I think it's good to show that the underlying logic of CRCs aren't
really that complex, at least until we throw optimizations into the mix.
2024-07-31 12:17:13 -05:00
Christopher Haster 23c82bd7e5 t: Replaced LFS_T_EXCL with LFS_I_DIRTY flag in lfsr_tinfo
This just forwards the internal LFS_I_DIRTY flag to the user via the
lfsr_tinfo flags field.

Benefits of this approach:

- Gives the user more flexibility on what to do if the filesystem is
  modified, maybe you want to keep traversing depending on some other
  logic.

- Can eventually add other flags to tinfo.flags, such as
  LFS_I_COMPACTED, LFS_I_REPAIRED, LFS_I_INCONSISTENT, etc.

- Avoids confusion around the very different behaviors of LFS_O_EXCL and
  LFS_T_EXCL.

  I tried to come up with a better name (maybe LFS_T_WATCH?) but it was
  a bit of a struggle... Switching to a flags approach sidesteps the
  issue.

- Can drop the LFS_ERR_BUSY error code for now.

Code changes were fairly insignificant:

           code          stack
  before: 35244           2680
  after:  35224 (-0.1%)   2680 (+0.0%)

The only concern is that the tests highlighted it's possible for our
flag scheme to miss mutation if it happens after/during the last set of
blocks... Not sure how to handle this yet...
2024-07-08 08:59:10 -05:00
Christopher Haster 227b804c7a Fixed dbgbmap.py missing btree nodes
The internal btree node traversal here is a bit different than in
dbgbtree.py, dbgmtree.py, etc, because we want to include both inner and
leaf nodes. We could decode the branch tags multiple times, but checking
for bid changes is a bit simpler.

It's interesting to note this went missed for so long, because, well,
it's hard to actually have more than one btree node. And the tests
explicitly covering large btree structures, test_btree, aren't parsable
by dbgbmap.py since they don't create real filesystem images.
2024-07-01 16:36:37 -05:00
Christopher Haster 635e1fe8d4 t: Added lookahead to lfsr_traversal_t, adopted in lfs_alloc
This sort of turned into a complete refactor of lfs_alloc in order to
move/reuse the lookahead buffer filling logic into lfsr_fs_traverse.

lfs_alloc now calls lfsr_fs_traverse to fill the lookahead buffer when
no more blocks are available, but also you can too with lfsr_traversal_t
+ LFS_T_LOOKAHEAD.

The one big caveat being if any mutation happens to the filesystem, any
incomplete lookahead needs to be tossed out. To help with this,
lfsr_traversal_read now returns LFS_ERR_BUSY (-16) instead of
LFS_ERR_NOENT (-2) if the filesystem has been modified since the
traversal was opened.

Note that by default lfsr_traversal_t will still try to keep traversing
blocks, but can be told to terminate immediately with LFS_T_EXCL.
Continuing the traversal is probably desired for checking checksums,
debugging, etc, as otherwise you could end up looping over only the
first couple blocks in a write-heavy system, but if you are trying to
populate the lookahead buffer you probably want to just abort and start
over.

I considered adding a flags field to lfs_tinfo for this, but decided
against it since it would be the only place in the current API where we
don't use error codes to convey behavior-changing information. Though
this may be worth reconsidering at some point...

---

In reworking lfs_alloc, a lot of the internal logic was broken up into
specific functions:

- lfs_alloc_ckpoint - checkpoint the allocator
- lfs_alloc_discard - discard any lookahead
- lfs_alloc_shift - discard/shift lookahead if progress can be made
- lfs_alloc_markinuse - mark a block as in-use
- lfs_alloc_markfree - mark any remaining blocks as free
- lfs_alloc_findnext - find the next free block in lookahead

If anything this probably makes lfs_alloc more readable, though the
original motivation was to allow lfsr_traversal_t to only shift/zero the
lookahead buffer if there's a chance we can make progress.

This was based on upstream work by opilat and myself.

Code changes:

           code          stack
  before: 34226           2560
  after:  34474 (+0.7%)   2552 (-0.3%)
2024-06-20 13:11:41 -05:00
Christopher Haster c739e18f6f Renamed LFSR_TAG_NOISE -> LFSR_TAG_NOTE
Sort of like SHT_NOTE in elf files, but with no defined format. Using
LFSR_TAG_NOTE for additional noise/nonces is still encouraged, but it
can also be used to add debug info.
2024-06-20 13:04:20 -05:00
Christopher Haster 096f968cbb Dropped prettyasserts.py --no-arrows shorthand form
This probably doesn't deserve a shorthand form as you can usually just
ignore the arrow parsing. This frees up -A for potential future use.
2024-06-20 13:04:12 -05:00
Christopher Haster 692db1327e Fixed watch.py not updating when no output
We redraw the output when we recieve a new line from the child process.
Which means if we never recieve a new line, nothing gets drawn.

Fixed by adding an extra case after the child process finishes.
2024-06-20 13:04:09 -05:00
Christopher Haster a735bcd667 Fixed hanging scripts trying to parse stderr
code.py, specifically, was getting messed up by inconsequential GCC
objdump errors on Clang -g3 generated binaries.

Now stderr from child processes is just redirected to /dev/null when
-v/--verbose is not provided.

If we actually depended on redirecting stderr->stdout these scripts
would have been broken when -v/--verbose was provided anyways. Not
really sure what the original code was trying to do...
2024-06-20 13:04:07 -05:00
Christopher Haster ae0e3348fe Added -l/--list to dbgtag.py
Inspired by errno's/dbgerr.py's -l/--list, this gives a quick and easy
list of the current tag encodings, which can be very useful:

  $ ./scripts/dbgtag.py -l
  LFSR_TAG_NULL       0x0000  v--- ---- ---- ----
  LFSR_TAG_CONFIG     0x00tt  v--- ---- -ttt tttt
  LFSR_TAG_MAGIC      0x0003  v--- ---- ---- --11
  LFSR_TAG_VERSION    0x0004  v--- ---- ---- -1--
  ... snip ...

We already need to keep dbgtag.py in-sync or risk a bad debugging
experience, so we might as well let it tell us all the information it
currently knows.

Also yay for self-inspecting code, I don't know if it's bad that I'm
becoming a fan of parsing information out of comments...
2024-06-20 13:02:08 -05:00
Christopher Haster 3dc597cb5f Added dbgerr.py for debugging littlefs's error codes
This is based on moreutils's errno program, but specialized for
littlefs's error codes. Everything is hardcoded in dbgerr.py, so this
should be portable to any OS really.

Hopefully this will be useful for debugging littlefs errors:

  $ ./scripts/dbgerr.py -84
  LFS_ERR_CORRUPT -84 Corrupted
2024-06-20 13:01:10 -05:00
Christopher Haster 898f916778 Fixed pl hole in perturb logic
Turns out there's very _very_ small powerloss hole in our current
perturb logic.

We rely on tag valid bits to validate perturb bits, but these
intentionally don't end up in the commit checksum. This means there will
always be a powerloss hole when we write the last valid bit. If we lose
power after writing that bit, suddenly the remaining commit and any
following commits may appear as valid.

Now, this is really unlikely considering we need to lose power exactly
when we write the cksum tag's valid bit, and our nonce helps protect
against this. But a hole is a hole.

The solution here is to include the _current_ perturb bit (q) in the
commit's cksum tag, alongside the _next_ perturb bit (p). This will be
included in the commit's checksum, but _not_ in the canonical checksum,
allowing the commit's checksum validate the current perturb state
without ruining our erased-state agnostic checksums:

  .---+---+---+---. . . .---+---+---+---. \   \   \   \
  |v|    tag      |     |v|    tag      | |   |   |   |
  +---+---+---+---+     +---+---+---+---+ |   |   |   |
  |     commit    |     |     commit    | |   |   |   |
  |               |     |               | +-. |   |   |
  +---+---+---+---+     +---+---+---+---+ / | |   |   |
  |v|qp-------------.   |v|qp| tag      |   | .   .   .
  +---+---+---+---+ |   +---+---+---+---+   | .   .   .
  |     cksum     | |   |     cksum     |   | .   .   .
  +---+---+---+---+ |   +---+---+---+---+   | .   .   .
  |    padding    | |   |    padding    |   | .   .   .
  |               | |   |               |   | .   .   .
  +---+---+---+---+ | . +---+---+---+---+   | |   |   |
  |     erased    | +-> |v------------------' |   |   |
  |               | |   +---+---+---+---+     |   |   |
  .               . |   |     commit    |     +-. |   +- rbyd
  .               . |   |.----------------.   | | |   |  cksum
                    |   +| -+---+---+---+ |   / | +-. /
                    +-> |v|qp| tag      | '-----' | |
                    |   +- ^ ---+---+---+         / |
                    '------'  cksum ----------------'
                        +---+---+---+---+
                        |    padding    |
                        |               |
                        +---+---+---+---+
                        |     erased    |
                        |               |
                        .               .
                        .               .

(Ok maybe this diagram needs work...)

This adds another thing that needs to be checked during rbyd fetch, and
note, we _do_ need to explicitly check this, but it solves the problem.
If power is loss after v, q would be invalid, and if power is lost after
q, our cksum would be invalid.

Note this would have also been an issue for the previous cksum + parity
perturb scheme.

Code changes:

           code          stack
  before: 33570           2592
  after:  33598 (+0.1%)   2592 (+0.0%)
2024-06-07 19:41:47 -05:00
Christopher Haster 8a4f6fcf68 Adopted a simpler rbyd perturb scheme
The previous cksum + parity scheme worked, but needing to calculate both
cksum + parity on slightly different sets of metadata felt overly
complicated. After taking a step back, I've realized the problem is that
we're trying to force perturb effects to be implicit via the parity. If we
instead actually implement perturb effects explicitly, things get quite
a bit simpler...

This does add a bit more logic to the read path, but I don't think it's
worse than the mess we needed to parse separate cksum + parity.

Now, the perturb bit has the explicit behavior of inverting all tag
valid bits in the following commit. Which is conveniently the same as
xoring the crc32c with 00000080 before parsing each tag:

  .---+---+---+---. . . .---+---+---+---. \   \   \   \
  |v|    tag      |     |v|    tag      | |   |   |   |
  +---+---+---+---+     +---+---+---+---+ |   |   |   |
  |     commit    |     |     commit    | |   |   |   |
  |               |     |               | +-. |   |   |
  +---+---+---+---+     +---+---+---+---+ / | |   |   |
  |v|p--------------.   |v|p|  tag      |   | .   .   .
  +---+---+---+---+ |   +---+---+---+---+   | .   .   .
  |     cksum     | |   |     cksum     |   | .   .   .
  +---+---+---+---+ |   +---+---+---+---+   | .   .   .
  |    padding    | |   |    padding    |   | .   .   .
  |               | |   |               |   | .   .   .
  +---+---+---+---+ | . +---+---+---+---+   | |   |   |
  |     erased    | +-> |v------------------' |   |   |
  |               | |   +---+---+---+---+     |   |   |
  .               . |   |     commit    |     +-. |   +- rbyd
  .               . |   |               |     | | |   |  cksum
                    |   +---+---+---+---+     / | +-. /
                    '-> |v----------------------' | |
                        +---+---+---+---+         / |
                        |     cksum ----------------'
                        +---+---+---+---+
                        |    padding    |
                        |               |
                        +---+---+---+---+
                        |     erased    |
                        |               |
                        .               .
                        .               .

With this scheme, we don't need to calculate a separate parity, because
each valid bit effectively validates the current state of the perturb
bit.

We also don't need extra logic to omit valid bits from the cksum,
because flipping all valid bits effectively makes perturb=0 the
canonical metadata encoding and cksum.

---

I also considered only inverting the first valid bit, which would have
the additional benefit of allowing entire commits to be crc32ced at
once, but since we don't actually track when we've started a commit
this turned out to be quite a bit more complicated than I thought.

We need someway to validate the first valid bit, otherwise it could be
flipped by a failed prog and we'd never notice. This is fine, we can
store a copy of the previous perturb bit in the next cksum tag, but it
does mean we need to track the perturb bit for the duration of the
commit. So we'd end up needing to track both start-of-commit and the
perturb bit state, which starts getting difficult to fit into our rbyd
struct...

It's easier and simpler to just flip every valid bit. As a plus this
means every valid bit contributes to validating the perturb bit.

---

Also renamed LFSR_TAG_PERTURB -> LFSR_TAG_NOISE just to avoid confusion.
Though not sure if this tag should stick around...

The end result is a nice bit of code/stack savings, which is what we'd
expect with a simpler scheme:

           code          stack
  before: 33746           2600
  after:  33570 (-0.5%)   2592 (-0.3%)
2024-06-07 18:24:13 -05:00
Christopher Haster 54d77da2f5 Dropped csv field prefixes in scripts
The original idea was to allow merging a whole bunch of different csv
results into a single lfs.csv file, but this never really happened. It's
much easier to operate on smaller context-specific csv files, where the
field prefix:

- Doesn't really add much information
- Requires more typing
- Is confusing in how it doesn't match the table field names.

We can always use summary.py -fcode_size=size to add prefixes when
necessary anyways.
2024-06-02 19:19:46 -05:00
Christopher Haster dc7e7b9ab1 Added -u/--use to summary.py
This is equivalent to just omitting the flag, but makes it a bit easier
to switch between summary.py and more specific scripts such as code.py,
where -u/--use is needed to operate on csv files.
2024-06-02 19:19:46 -05:00
Christopher Haster 169952dec0 Tweaked scripts to render new entry ratios as +∞%
We already rely on this symbol in these scripts, so might use it to
display the mathematically correct ratio for new entries.

This has the added benefit of ordering new entries vs extremely big
changes correctly:

  $ ./scripts/code.py -u test.after.csv -d test.before.csv
  function (1 added, 0 removed)      osize    nsize    dsize
  test_a                                 -       49      +49 (+∞%)
  test_b                                19      719     +700 (+3684.2%)
  test_c                                91      191     +100 (+109.9%)
  TOTAL                                110      959     +849 (+771.8%)
2024-06-02 19:19:46 -05:00
Christopher Haster 06bfed7a8b Interspersed precent/notes in measurement scripts
This is a bit more complicated, but make testmarks really showed how
confusing this could get.

Now, instead of:

  suite                             passed    time
  test_alloc                       304/304     1.6 (100.0%)
  test_badblocks                 6880/6880  1323.3 (100.0%)
  ... snip ...
  test_rbyd                  385878/385878   592.7 (100.0%)
  test_relocations               7899/7899   318.8 (100.0%)
  TOTAL                      548206/548206  6229.7 (100.0%)

Percents/notes are interspersed next to their relevant fields:

  suite                             passed             time
  test_alloc                       304/304 (100.0%)     1.6
  test_badblocks                 6880/6880 (100.0%)  1323.3
  ... snip ...
  test_rbyd                  385878/385878 (100.0%)   592.7
  test_relocations               7899/7899 (100.0%)   318.8
  TOTAL                      548206/548206 (100.0%)  6229.7

Note has no effect on scripts with only a single field (code.py, etc).

But it does make multi-field diffs a bit more readable:

  $ ./scripts/stack.py -u after.stack.csv -d before.stack.csv -p
  function                       frame             limit
  lfsr_bd_sync                       8 (+100.0%)     216 (+100.0%)
  lfsr_bd_flush                     40 (+25.0%)      208 (+4.0%)
  ... snip ...
  lfsr_file_flush                   32 (+0.0%)      2424 (-0.3%)
  lfsr_file_flush_                 216 (-3.6%)      2392 (-0.3%)
  TOTAL                           9008 (+0.4%)      2600 (-0.3%)
2024-06-02 19:19:38 -05:00
Christopher Haster a231bbac6e Added -^/--head to watch.py and other scripts, RingIO tweaks
This lets you view the first n lines of output instead of the last n
lines, as though the output was piped through head.

This is how the standard watch command works, and can be more useful
when most of the information is at the top, such as in our dbg*.py
scripts (watch.py was originally used as a sort of inotifywait-esque
build runner, which is the main reason it's different).

To make this work, RingIO (renamed from LinesIO) now uses terminal
height as a part of its canvas rendering. This has the added benefit of
more rigorously enforcing the canvas boundaries, but risks breaking when
not associated with a terminal. But that raises the question, does
RingIO even make sense without a terminal?

Worst case you can bypass all of this with -z/--cat.
2024-06-02 00:37:48 -05:00
Christopher Haster bb3ef46cdf Fixed B-tree rendering in dbgmtree.py, unexpected arg
Rbyd.btree_btree renders B-trees, not rbyds, the rbyd arg doesn't make
sense here. This was just an accidental refactor mistake.
2024-05-31 16:36:33 -05:00
Christopher Haster 3c5319e125 Tweaked test/bench id globbing to avoid duplicating cases
Before, globs that match both the suite name and case name would cause
end up running the case twice. Which is a bit of a problem, since all
cases contain their suite name as a prefix...

  test_f* => run test_files
             |-> run test_files_hello
             |-> run test_files_trunc
             ...
             run test_files_hello
             run test_files_trunc
             ...

Now we only run matching test cases if no suites were found.

This has the side-effect of making the universal glob, "*", equivalent
to no test ids, which is nice:

  $ ./scripts/test.py -j -b '*'  # equivalent
  $ ./scripts/test.py -j -b      #

This is useful for running a specific problematic test first before
running the all of the tests:

  $ ./scripts/test.py -j -b test_files_trunc '*'
2024-05-29 23:09:45 -05:00
Christopher Haster 31eebc1328 Added -a/--all to test.py/bench.py for bypass test/bench filters
These really shouldn't be used all that often. Test filters are usually
used to protect against invalid test configurations, so if you bypass
test filters, expect things to fail!

But some filters just prevent test cases from taking too long. In these
cases being able to manually bypass the filter is useful for debugging/
benchmarking/etc...
2024-05-28 16:46:40 -05:00
Christopher Haster 37f738cc71 Changed RFrac equality in scripts
Now, fractions are considered equal if they have the same ratio:

- 6/6 == 12/12 => True
- 3/6 == 3/12  => False
- 1/6 == 2/12  => True

It's interesting to note this implementation is actually more
numerically stable than float comparison, though that wasn't really the
goal.

The main reason for this is to allow other fields to take over when
sorting multi-field fractional data: cov (lines + branches), testmarks
(passed + time), etc. Before, sorting would usually stop after
mismatched fraction fields, which wasn't all that useful.
2024-05-28 15:19:11 -05:00
Christopher Haster e247135805 Fixed test.py crashing on malformed test ids
Sometimes, if test_runner errors before running any tests, the last test
id can end up being None. This broke test output writing, which expected
to be able to parse an id. Instead we should just ignore the malformed
id (it's not like we can write anything relevant about any tests here),
and report it to the user at a higher level.
2024-05-28 14:50:57 -05:00
Christopher Haster 9c9a409524 Added fuzz test attribute
This acts as a marker to indicate a fuzz test. It should reference a
define, usually SEED, that can be randomized to get interesting test
permutations.

This is currently unused, but could lead to some interesting uses such
as time-based fuzz testing. It's also just useful for inspecting the
tests (make test-list).
2024-05-28 12:44:44 -05:00
Christopher Haster 2e8012681b Tweaked dbg script headers to match the mount info log
The main difference being rendering the weight with a single letter "w"
prefix:

  $ ./scripts/dbglfs.py disk -b4096
  littlefs v0.0 4096x256 0x{1,0}.8b w2.512, rev eb7f2a0d
  ...

This lets us add valuable weight info without too much noise.

Adopting this in the dbg scripts is nice for consistency.
2024-05-24 14:56:11 -05:00
Christopher Haster 4d76551d6b Fixed parse errors in prettyasserts.py caused by ternary operators
Because of course ternary operators would cause problems.

The two problem:

  LFS_ASSERT((exists) ? !err : err == LFS_ERR_NOENT);
  lfsr_file_sync(&lfs, &file) => (zombie) ? 0 : LFS_ERR_NOENT;

We could work around these with parentheses, but with different assert
parsers floating around this issue is likely to crop up again in the
future.

Fortunately this just required separate "sep" vs "term" rules and a bit
more strict parsing.
2024-05-22 18:50:54 -05:00
Christopher Haster 56b18dfd9a Reworked revision count logic a bit, block_cycles -> block_recycles
The original goal here was to restore all of the revision count/
wear-leveling features that were intentionally ignored during
refactoring, but over time a few other ideas to better leverage our
revision count bits crept in, so this is sort of the amalgamation of
that...

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

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

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

Here's the new revision count breakdown:

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

- 4-bit relocation revision

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

- recycle-bits recycle counter

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

- pseudorandom nonce

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

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

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

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

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

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

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

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

---

Some other notes:

- Renamed block_cycles -> block_recycles.

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

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

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

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

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

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

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

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

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

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

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

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

Code changes:

           code          stack
  before: 33342           2640
  after:  33434 (+0.3%)   2640 (+0.0%)
2024-05-22 18:49:05 -05:00
Christopher Haster 4208aa21e2 Extended prettyasserts.py to support prefixed memcmp/strcmp
The move to lfs_memcmp/lfs_strcmp highlighted an interesting hole in
prettyasserts.py: the lack of support for custom memcmp/strcmp symbols.

Rather than just adding more flags for an increasing number of symbols,
I've added -p/--prefix and -P/--prefix-insensitive to generate relevant
symbols based on a prefix. In littlefs's case, we use -Plfs_, which
matches both lfs_memcmp and LFS_ASSERT (and LFS_MEMCMP and lfs_assert
but ignore those):

  $ ./scripts/prettyasserts.py -Plfs_ lfs.t.c -o lfs.t.a.c

Don't worry, you can still provide explicit symbols, but only via
long-form flags. This gets a bit noisy:

  $ ./scripts/prettyasserts.py \
      --assert=LFS_ASSERT \
      --unreachable=LFS_UNREACHABLE \
      --memcmp=lfs_memcmp \
      --strcmp=lfs_strcmp \
      lfs.t.c -o lfs.t.a.c

This commit also finally gives the prettyasserts.py's symbols actual
word boundaries, instead of the big error-prone hack of sorting by size.
2024-05-22 15:43:46 -05:00
Christopher Haster 4672fb59ca Fixed bug in dbglfs.py that prevented struct rendering
When we introduced erased-state agnostic rbyd cksums, we added a cksums
to the dbg scripts since their cksums were actually useful now.

Unfortunately this missed the explicit/hacky Rbyd constructions in
dbglfs.py used to render file structs. Fixed by falling by using
cksum=0 in these cases.
2024-05-22 15:43:46 -05:00
Christopher Haster 11c948678f Renamed size_limit -> file_limit
This limits the maximum size of a file, which is also implies the
maximum integer size required to mount.

The exact name is a bit of a toss-up. I originally went with size_limit
to avoid confusion around if file_limit reflected the file size or the
number of files, but since this ends up mapping to lfs_off_t and _not_
lfs_size_t, I think size_limit may be a bit of a bad choice.
2024-05-18 13:00:15 -05:00
Christopher Haster a9f6b6e903 Renamed internal script result types * -> R*
So Int -> RInt, Frac -> RFrac, etc. This just helps distinguish these
types from builtin types, which could be confusing.
2024-05-18 13:00:15 -05:00
Christopher Haster 03ea2e6ac5 Tweaked cov.py, summary.py, to render fraction percents as notes
This matches how diff percentages are rendered, and simplifies the
internal table rendering by making Frac less of a special case. It also
allows for other type notes in the future.

One concern is how all the notes are shoved to the side, which may make
it a bit harder to find related percentages. If this becomes annoying we
should probably look into interspersing all notes (including diff
percentages) between the relevant columns.

Before:

  function                                   lines            branches
  lfsr_rbyd_appendattr             230/231   99.6%     172/192   89.6%
  lfsr_rbyd_p_recolor                33/34   97.1%       11/12   91.7%
  lfs_alloc                          40/42   95.2%       21/24   87.5%
  lfsr_rbyd_appendcompaction         54/57   94.7%       39/42   92.9%
  ...

After:

  function                           lines    branches
  lfsr_rbyd_appendattr             230/231     172/192 (99.6%, 89.6%)
  lfsr_rbyd_p_recolor                33/34       11/12 (97.1%, 91.7%)
  lfs_alloc                          40/42       21/24 (95.2%, 87.5%)
  lfsr_rbyd_appendcompaction         54/57       39/42 (94.7%, 92.9%)
  ...
2024-05-18 13:00:15 -05:00
Christopher Haster 1d88fa9864 In scripts -d/--diff, show either all percentages or none
Previously, with -d/--diff, we would only show non-zero percentages. But
this was ambiguous/confusing when dealing with multiple results
(stack.py, summary.py, etc).

To help with this, I've switched to showing all percentages unless all
percentages are zero (no change). This matches the -d/--diff row-hiding
logic, so by default all rows should show all percentages.

Note -p/--percent did not change, as it already showed all percentages
all of the time.
2024-05-18 13:00:15 -05:00
Christopher Haster 7a7da9680a Avoid O(n^2) folding in summary.py
Noticed weird slowness when summarizing test results by suite vs case.
Turns out the way we accumulate results by overloading Python's __add__
quickly leads to O(n^2) behavior as we repeatedly concatenate
increasingly large lists.

Instead of doing anything sane, I've added a second, immutable length to
each list such that we can opportunistically reuse/mutate/append lists
in __add__. The end result should be O(n) most of the time.

Observe:

             lines           bytes
  test.csv: 537749  64551874 62MiB

  ./scripts/summary.py test.csv -ftest_time -S

               before     after
  -bcase:   0m51.772s  0m9.302s (-82.0%)
  -bsuite: 10m29.067s  0m9.357s (-98.5%)
2024-05-18 13:00:15 -05:00
Christopher Haster 4920cb092c Fixed summary.py's float diff rendering precision
The internal Float type was incorrectly inheriting the diff rendering
from Int, which casts to, well, an int.
2024-05-18 13:00:15 -05:00