Commit Graph

2202 Commits

Author SHA1 Message Date
Christopher Haster b3669f02c2 Dropped __cplusplus guards
littlefs is not a C++ project, and it's important to make sure users are
aware of that in case the header file ever breaks C++ (C++ is _not_
compatible with C99).

So dropping these guards.

C++ users should wrap the relevant includes with extern "C":

  extern "C" {
  #include "lfs.h"
  }
2025-04-23 23:21:18 -05:00
Christopher Haster 5f7647dc0c make: Forward all LFS_* prefixed environment variables as defines
So instead of:

  CFLAGS='-DLFS_YES_REVDBG=1' make

You can just do:

  LFS_YES_REVDBG=1 make

I've been hesitant to add this, as I've never seen this pattern in
another project (why?), but it's just too convenient to not give it a
try.
2025-04-23 23:21:04 -05:00
Christopher Haster 3ccd7d39be Added LFS_YES_* ifdefs for all mount/format flags
This lets you specify mount/format flags globally, via -DLFS_YES_REVDBG,
for example.

In addition to the convenience of not needing to edit code, these flags
may also be able to reduce code cost by eliminating the various flag
checks and untaken code paths.

At the moment this relies on dead code elimination via the lfsr_m_is*
functions, to keep the codebase from exploding too much.
2025-04-23 23:20:59 -05:00
Christopher Haster 96eb38c8c2 Added LFS_REVDBG, tweaked LFS_REVNOISE
This tweaks a number of extended revision count things:

- Added LFS_REVDBG, which adds debug info to revision counts.

  This initializes the bottom 12 bits of every revision count with a
  hint based on rbyd type, which may be useful when debugging:

  - 68 69 21 v0 (hi!.) => mroot anchor
  - 6d 72 7e v0 (mr~.) => mroot
  - 6d 64 7e v0 (md~.) => mdir
  - 62 74 7e v0 (bt~.) => file btree node
  - 62 6d 7e v0 (bm~.) => mtree node

  This may be overwritten by the recycle counter if it overlaps, worst
  case the recycle counter takes up the entire revision count, but these
  have been chosen to at least keep some info if partially overwritten.

  To make this work required the LFS_i_INMTREE hack (yay global state),
  but a hack for debug info isn't the end of the world.

  Note we don't have control over data blocks, so there's always a
  chance they end up containing what looks like one of the above
  revision counts.

- Renamed LFS_NOISY -> LFS_REVNOISE

- LFS_REVDBG and LFS_REVNOISE are incompatible, so using both asserts.

  This also frees up the theoretical 0x00000030 state for an additional
  rev mode in the future.

- Adopted LFS_REVNOISE (and LFS_REVDBG) in btree nodes as well.

  If you need rev noise, you probably want it in all rbyds/metadata
  blocks, not just mdirs.

---

This had no effect on the default code size, but did affect
LFS_REVNOISE:

                    code          stack          ctx
  before:          35688           2440          640
  after:           35688 (+0.0%)   2440 (+0.0%)  640 (+0.0%)

  revnoise before: 35744           2440          640
  revnoise after:  35880 (+0.4%)   2440 (+0.0%)  640 (+0.0%)

  default:         35688           2440          640
  revdbg:          35912 (+0.6%)   2448 (+0.3%)  640 (+0.0%)
  revnoise:        35880 (+0.5%)   2440 (+0.0%)  640 (+0.0%)
2025-04-23 23:20:49 -05:00
Christopher Haster 7cd4c1f12f Adopted shift-table hack for lfsr_tag_mask
This is based off the parity impl in Sean Eron Anderson's Bit Twiddling
Hacks, who attributes the idea to Mathew Hendry.

Basically the idea is to encode a small lookup table in an integer, and
extract using a shift + mask:

                          .-- LFSR_TAG_MASK0
                         .|-- LFSR_TAG_MASK2
                        .||-- LFSR_TAG_MASK8
                       .|||-- LFSR_TAG_MASK12
                       vvvv
  0x0fff & (-1U << ((0xc820 >> (4*((tag >> 12) & 0x3))) & 0xf))
  '--.-'      ^                   '--------.--------'
  key mask  gcc complains w/o this     mask bits

Saves a bit of code at the cost of some stack. I guess because GCC is
trying to avoid multiple constant pool lookups? This may just be
compiler noise:

           code          stack          ctx
  before: 35692           2432          640
  after:  35688 (-0.0%)   2440 (+0.3%)  640 (+0.0%)
2025-04-23 23:20:36 -05:00
Christopher Haster 670b8e6732 A couple tweaks to bit twiddling utils
- Gave lfs_parity its own backup implementation.

  Since these are static inline functions, shared implementations don't
  matter as much here, so why do more work than we have to.

  Save a bit of code too:

                         code          stack          ctx
    yes-builtins:       35692           2432          640
    no-builtins before: 35996 (-0.9%)   2504 (+3.0%)  640 (+0.0%)
    no-builtins after:  35960 (-0.8%)   2504 (+3.0%)  640 (+0.0%)

  Though maybe this is an argument for these functions not being static
  inline...

- Tweaked lfs_popc for readability (the 7-digit mask was annoying me).

- Added a link to Sean Eron Anderson's Bit Twiddling Hacks page:
  https://graphics.stanford.edu/~seander/bithacks.html

  These have been published as public domain, so I don't think this is
  strictly necessary, but the page is a great resource and deserves
  mention.
2025-04-23 23:20:26 -05:00
Christopher Haster 306ca25970 make: Dropped ascii-art codemap rules
Instead, make codemap/codemap-tiny just generate the relevant .svgs:

- dropped make codemap
- dropped make stackmap
- dropped make ctxmap
- make codemap-svg -> make codemap
- make codemap-tiny-svg -> make codemap-tiny

The ascii-art codemaps just really aren't useful due to their low
resolution. We might as well repurpose the relevant make rules to save
keystrokes.

Though I did keep the ascii-art as a step in make codemap/codemap-tiny,
just for fun.
2025-04-23 23:20:19 -05:00
Christopher Haster 83668e9782 Eliminated dags from file bshrubs/btrees
Velociraptors inbound.

This eliminates dags (directed acyclic graphs) from file bshrubs/btrees,
which were the only source of dags in the filesystem. This means
littlefs is now strictly a pure tree, in that no blocks have more than
one parent (ignoring in-RAM references!).

Up until this point, dags could be created in file bshrubs/btrees via
random writes that place fragments in the middle of a block:

  .-------------.      .-------------------.
  | aaaaaaaaaaa |  ->  | aaaaa | b | aaaaa |
  '-------------'      '-------------------'
         |                  |    v    |
         v                  |   .-.   |
  .-------------.           |   |b|   |
  | aaaaaaaaaaa |           v   '-'   v
  '-------------'         .-------------.
                          | aaaaaaaaaaa |
                          '-------------'

Now, fragments that would create dags instead trigger block
recrystallization, rewriting the left sibling into a new block if
necessary:

  .-------------.      .----------------.
  | aaaaaaaaaaa |  ->  | aaaaab | aaaaa |
  '-------------'      '----------------'
         |                 |        '-.
         v                 v          v
  .-------------.      .--------. .-------.
  | aaaaaaaaaaa |      | aaaaab | | aaaaa |
  '-------------'      '--------' '-------'

Allowing dags was great for random-write performance, but it creates
problems for future planned features:

1. Current plans for more advanced block allocators rely on blocks only
   having one parent. Otherwise it's difficult to know which reference
   is the last reference to a block.

2. Dags create a really funny problem for error correction via block
   redundancy. Naively, if you try to repair blocks every time you
   encounter a given block error, you will end up exploding the block
   into n copies, 1 for every parent. Not great!

---

Eliminating these dags was a bit... tricky...

Originally I was planning to just alloc/rewrite blocks in
lfsr_file_carve, but it turns out we can make lfsr_file_flush_ do all
the work with an extra would-dag checks. Handling dags in
lfsr_file_flush_ also gives us a chance to merge any pending data and
get the most out of the block rewrite.

This does give us a bit of technical debt in that we will probably still
need the block splitting in lfsr_file_carve for future features
(advanced hole APIs, alternative write strategies, etc), but it's
probably worth it for code savings in the default build.

Unfortunately this does add to the mess that is lfsr_file_flush_'s
control flow graph:

         lfsr_file_flush_
               |
               v
  .--> lookup left crystal   .--> lookup left sibling <-.
  |            |             |            |             |
  |            v             |            v             |
  |         erased?          |           dag? (new!)    |
  | .---------y n            | .---------y n            |
  | |           v            | |           v            |
  | |  lookup right crystal  | |  lookup right sibling  |
  | |          |             | |          |             |
  | |          v             | |          v             |
  | |   >=crystal_thresh?    | |       coalesce         |
  | |         y n------------' |          |             |
  | |         v                |          v             |
  | |  lookup left neighbor    |        carve-----------'
  | |          |               |
  | |          v               |
  | |       erased?            |
  | +---------y n              |
  | |           v              |
  | |        alloc <---+-------'
  | |          |       |
  | |          v       |
  | '---> crystallize  |
  |            |       |
  |            v       |
  |          good?     |
  |           y n------'
  |           v
  '----------carve

I did scratch my head for a bit trying to think if there was a better
way to organize this, but came up empty.

It looks complicated, but we really only have two* loops (ignoring the
relocation loop): One that crystallizes blocks, and one that coalesces
fragments. The problem is that we end jumping between the two depending
on what we find in the btree.

In a sane system, this would be implemented as mutually recursive
functions, but this is littlefs, the whole point is that we don't use
recursion.

---

The good news is that this added surprisingly little code (and saved
stack?):

           code          stack          ctx
  before: 35600           2448          640
  after:  35692 (+0.3%)   2432 (-0.7%)  640 (+0.0%)
2025-04-23 23:20:10 -05:00
Christopher Haster 31e34f54f3 Added test_fwrite_truncate/fruncate_litmus_zero
Just to help build confidence that the internal bshrub/btree logic is
behaving as expected.
2025-04-23 23:20:03 -05:00
Christopher Haster 385199d4b5 Tweaked some crystallization comments
- Trying to prefer crystal over compact verbiage to try to avoid
  confusion with metadata/rbyd compaction

- crystal_thresh >= block_size implying a fully-fragmented file was a
  mistake, it should be crystal_thresh > block_size.

  crystal_thresh == block_size has the behavior of waiting until the
  last moment to crystallize a block, but this still breaks the
  fully-fragmented random-write guarantee.

  This changed during development, so the comment was probably just
  outdated.
2025-04-23 23:19:50 -05:00
Christopher Haster a73f221317 scripts: Fixed issue where rbyd lookups rejected shrub tags
This was caused by including the shrub bit in the tag comparison in
Rbyd.lookup.

Fixed by adding an extra key mask (0xfff). Note this is already how
lfsr_rbyd_lookup works in lfs.c.
2025-04-23 23:19:37 -05:00
Christopher Haster fc095af472 Fixed never fully fragmenting bptrs
Bit of a silly, but problematic, bug, probably introduced during the
various lfsr_bptr_t/lfsr_data_t reworks, but basically we never actually
fragmented the last fragment in a bptr.

We were fragmenting all fragments in a bptr _above_ fragment_size, but
then we'd stop at the last fragment and keep it around as a bptr,
completely wasting all of the work to fragment the block. The reason for
the different behavior being that we can combine the last fragment with
the carved data to avoid an additional commit.

Fortunately the solution is pretty non-invasive. We can just assume any
bptrs <= fragment_size should be written out as fragments.

Added test_fwrite_truncate_litmus_fragment and
test_fwrite_fruncate_litmus_fragment to catch this in the future.

Code changes:

           code          stack          ctx
  before: 35588           2448          640
  after:  35600 (+0.0%)   2448 (+0.0%)  640 (+0.0%)
2025-04-23 23:19:22 -05:00
Christopher Haster 6d97398efc scripts: dbglfs.py: Fixed a couple mid=-1 issues
- Fixed Mtree.lookupleaf accepting mbid=0, which caused dbglfs.py to
  double print all files with mbid=-1

- Fixed grm mids not being mapped to mbid=-1 and related orphan false
  positives
2025-04-23 23:19:05 -05:00
Christopher Haster 2909da9c13 scripts: tracebd.py: Prioritize erases when rendering
I've made this mistake before!

One would think that it would be more interesting to show progs over
erases when they overlap, since progs always subset erases and show more
detail. However, erases occur much more rarely and are usually followed
by progs, so when rendering is low resolution (ascii) it's easy for
progs to completely cover up all erase operations.

Prioritizing erases prevents this.

At least this nuance is better documented this time around.
2025-04-23 23:18:49 -05:00
Christopher Haster 9820e369a3 Dropped the commitleaf set of functions
- dropped lfsr_btree_commitleaf
- dropped lfsr_bshrub_commitleaf
- dropped lfsr_file_commitleaf

The problem is that, thanks to rbyd compactions/splits/merges/etc, we
end up leaving the leaf rbyd in a more-or-less undefined state.

I was trying to adopt commitleaf in lfsr_file_carve, the function with
the most glaring potential for commitleaf, but the leaf rbyd behavior is
extremely error prone and requires quite a bit of extra circuitry to use
correctly.

The end result looked like it would need more code, more stack
(lfsr_file_carve _is_ on the stack hot path), for a minor speed
improvement. So I decided to drop the idea. We can probably expect file
carving to be dominated by progs/erases anyways.

lfsr_btree_commit_ still needs to lookup parent rbyds, so it would have
only saved ~1 out of O(log_b n) btree lookups (though this may still be
significant given the ridiculous branching factor of btrees).

---

But it _is_ interesting to note that there is still potential
performance savings on the floor if we didn't care about code size.
Without necessarily sacrificing our bounded RAM constraint.

I could imagine a build in the future that prioritizes performance over
code size by strictly using leaf rbyd functions, iterating over leaf
rbyds before iterating the parent btree, etc.

But that's the future. Simply getting things working is the priority
right now.

---

Saves a bit of code/stack:

           code          stack          ctx
  before: 35600           2456          640
  after:  35588 (-0.0%)   2448 (-0.3%)  640 (+0.0%)

Note that lookupleaf is still useful for the case where bids have
multiple attrs attached (none so far, but the plan is for the dedup tree
to leverage this).
2025-04-23 23:18:27 -05:00
Christopher Haster 00705b15a5 Reduced lfsr_file_carve's rattr scratch buffer 5 -> 3
This should have been updated when we dropped becksums (way back in
5fa85583!), we only ever need at most 3 rattrs to complete a carve
operation (left sibling, rattr, right sibling).

Just a free 24 byte stack savings sitting right there:

           code          stack          ctx
  before: 35600           2480          640
  after:  35600 (+0.0%)   2456 (-1.0%)  640 (+0.0%)
2025-04-20 15:53:18 -05:00
Christopher Haster c5efe35ab2 Split crystal_thresh into crystal_thresh + fragment_thresh
So now crystal_thresh only controls when fragments are compacted into
blocks, while fragment_thresh controls when blocks are broken into
fragments. Setting fragment_thresh=-1 will follow crystal_thresh and
keeps the previous behavior.

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

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

---

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

           code          stack          ctx
  before: 35584           2480          636
  after:  35600 (+0.0%)   2480 (+0.0%)  640 (+0.6%)
2025-04-20 15:53:18 -05:00
Christopher Haster 200830aafe Adopted mask bits for tag lookup/append
This lets us cram in one more mask for potential redund bits:

  name                 tag    mask
  LFSR_TAG_MASK0    0x0000  0x0fff  ---- 1111 1111 1111
  LFSR_TAG_MASK2    0x1000  0x0ffc  ---- 1111 1111 11--
  LFSR_TAG_MASK8    0x2000  0x0f00  ---- 1111 ---- ----
  LFSR_TAG_MASK12   0x3000  0x0000  ---- ---- ---- ----
                                    '.-' '.-' '---.---'
                          mode bits -'    |       |   ^
                            suptype ------'       |   |
                            subtype --------------'   |
                        redund bits ------------------'

I toyed around with a bitwise alternative to the lookup table, but
couldn't come up with anything simpler than these:

- 0xfff & ~((((1<<((i>>1)*8))-1) << ((i&1)*4)) | ((1<<(i*2))-1))
- 0xfff & ~((1 << (((i>>1)*8)+((i&1)<<(1+(i>>1)))))-1)
- 0xfff & ~((1<<(2*i*i))-1) (requires multiply and 32-bit shift)

---

This also replaces the mdir/rbyd/btree/mtree lookup/sublookup/suplookup
functions with a single flexible lookup function that accepts tag masks.

This ended up adding a bit of code/stack (the extra NULL args are
surprisingly pricey), but will hopefully make the redund bits
easier/cheaper to use:

           code          stack          ctx
  before: 35548           2472          636
  after:  35584 (+0.1%)   2480 (+0.3%)  636 (+0.0%)
2025-04-20 15:53:18 -05:00
Christopher Haster 8f1ccf089e Adopted lookupleaf, reworked internal btree APIs
This was a surprising side-effect the script rework: Realizing the
internal btree/rbyd lookup APIs were awkwardly inconsistent and could be
improved with a couple tweaks:

- Adopted lookupleaf name for functions that return leaf rbyds/mdirs.

  There's an argument this should be called lookupnextleaf, since it
  returns the next bid, unlike lookup, but I'm going to ignore that
  argument because:

  1. A non-next lookupleaf doesn't really make sense for trees where
     you don't have to fetch the leaf (the mtree)

  2. It would be a bit too verbose

- Adopted commitleaf name for functions that accept leaf rbyds.

  This makes the lfsr_bshrub_commit -> lfsr_btree_commit__ mess a bit
  more readable.

- Strictly limited lookup and lookupnext to return rattrs, even in
  complex trees like the mtree.

  Most use cases will probably stick to the lookupleaf variants, but at
  least the behavior will be consistent.

- Strictly limited lookup to expect a known bid/rid.

  This only really matters for lfsr_btree/bshrub_lookup, which as a
  quirk of their implementation _can_ lookup both bid + rattr at the
  same time. But I don't think we'll need this functionality, and
  limited the behavior may allow for future optimizations.

  Note there is no lfsr_file_lookup. File btrees currently only ever
  have a single leaf rattr, so this API doesn't really make sense.

Internal API changes:

- lfsr_btree_lookupnext_ -> lfsr_btree_lookupleaf
- lfsr_btree_lookupnext  -> lfsr_btree_lookupnext
- lfsr_btree_lookup      -> lfsr_btree_lookup
- added                     lfsr_btree_namelookupleaf
- lfsr_btree_namelookup  -> lfsr_btree_namelookup
- lfsr_btree_commit__    -> lfsr_btree_commit_
- lfsr_btree_commit_     -> lfsr_btree_commitleaf
- lfsr_btree_commit      -> lfsr_btree_commit

- added                     lfsr_bshrub_lookupleaf
- lfsr_bshrub_lookupnext -> lfsr_bshrub_lookupnext
- lfsr_bshrub_lookup     -> lfsr_bshrub_lookup
- lfsr_bshrub_commit_    -> lfsr_bshrub_commitleaf
- lfsr_bshrub_commit     -> lfsr_bshrub_commit

- lfsr_mtree_lookup      -> lfsr_mtree_lookupleaf
- added                     lfsr_mtree_lookupnext
- added                     lfsr_mtree_lookup
- added                     lfsr_mtree_namelookupleaf
- lfsr_mtree_namelookup  -> lfsr_mtree_namelookup

- added                     lfsr_file_lookupleaf
- lfsr_file_lookupnext   -> lfsr_file_lookupnext
- added                     lfsr_file_commitleaf
- lfsr_file_commit       -> lfsr_file_commit

Also added lookupnext to Mdir/Mtree in the dbg scripts.

Unfortunately this did add both code and stack, but only because of the
optional mdir returns in the mtree lookups:

           code          stack          ctx
  before: 35520           2440          636
  after:  35548 (+0.1%)   2472 (+1.3%)  636 (+0.0%)
2025-04-20 15:53:18 -05:00
Christopher Haster 95eca09d12 Renamed LFS_DEBUG* -> LFS_DBG*
The exception being LFS_DEBUG. A bit inconsistent, but the at least
consistent with LFS_ERR* vs LFS_ERROR, and may help reduce name
conflicts:

- LFS_DEBUGRBYDFETCHES -> LFS_DBGRBYDFETCHES
- LFS_DEBUGRBYDBALANCE -> LFS_DBGRBYDBALANCE
- LFS_DEBUGRBYDCOMMITS -> LFS_DBGRBYDCOMMITS
- LFS_DEBUGBTREEFETCHES -> LFS_DBGBTREEFETCHES
- LFS_DEBUGBTREECOMMITS -> LFS_DBGBTREECOMMITS
- LFS_DEBUGMDIRFETCHES -> LFS_DBGMDIRFETCHES
- LFS_DEBUGMDIRCOMMITS -> LFS_DBGMDIRCOMMITS
- LFS_DEBUGALLOCS -> LFS_DBGALLOCS
2025-04-20 15:53:18 -05:00
Christopher Haster 3ca6670dcd Always log mbid=-1 for mroots and inlined mdirs
So mbid=0 now implies the mdir is not inlined.

Downsides:

- A bit more work to calculate
- May lose information due to masking everything when mtree.weight==0
- Risk of confusion when in-lfs.c state doesn't match (mbid=-1 is
  implied by mtree.weight==0)

Upsides:

- Includes more information about the topology of the mtree
- Avoids multiple dbgmbids for the same physical mdir

Also added lfsr_dbgmbid and lfsr_dbgmrid to help make logging
easier/more consistent.

And updated dbg scripts.
2025-04-20 15:53:18 -05:00
Christopher Haster 89356fc697 Renamed a couple mbit related things
- mdir_bits -> mbits
- lfsr_mid_bid -> lfsr_mbid
- lfsr_mid_rid -> lfsr_mrid

These now match the naming in the dbg scripts.

I feel like this is more terse in a way that is also more readable, but
maybe that's just me.
2025-04-20 15:53:18 -05:00
Christopher Haster 04d3002f3a Adopted ceiling division in mbits formula
So now:
               (block_size)
  mbits = nlog2(----------) = nlog2(block_size) - 3
               (     8    )

Instead of:

               (     (block_size))
  mbits = nlog2(floor(----------)) = nlog2(block_size & ~0x7) - 3
               (     (     8    ))

This makes the post-log - 3 formula simpler, which we probably want to
prefer as it avoids a division. And ceiling is arguably more intuitive
corner case behavior.

This may seem like a minor detail, but because mbits is purely
block_size derived and not configurable, any quirks here will become
a permanent compatibility requirement.

And hey, it saves a couple bytes (I'm not really sure why, the division
should've been optimized to a shift):

           code          stack          ctx
  before: 35528           2440          636
  after:  35520 (-0.0%)   2440 (+0.0%)  636 (+0.0%)
2025-04-20 15:53:18 -05:00
Christopher Haster 84b3bdda52 make: Adopted script changes in the Makefile
Mainly adopting the added flexibility in csv.py, also adding make
codemap-svg and friends for code map generation:

- Split result commands into separate result, result-csv, and
  result-diff commands so csv generation is explicit.

  So make result no longer implicitly overwrites csv files:

    make code
    make code-csv  -.
    make code       |
    make code-diff <'

  This gives more control over result diffing.

  make code-csv _is_ more or less just a dependency on the lfs.code.csv
  rule, but it avoids BUILDDIR mess and is easier to remember.

- Added make codemap/stackmap/ctxmap for in-terminal code/stack/ctx
  ascii art.

  I was a bit on the fence on these, since the result is more pretty
  than useful, but eh, can always drop them in the future.

- Added make codemap-svg/codemap-tiny-svg for generating interactive
  codemap svgs.

  This raised an interesting question if the make commands should
  generate light or dark mode svgs. I settled on dark mode since that's
  what I personally find the most useful.

  I think the way this will breakdown is with dark mode generally used
  for development, and light mode generally used for published material.
  And it's not too hard to run the script outside of the Makefile for
  publishing. Or override CODEMAPFLAGS.

- Adopted implicit prefixing, -q, etc. This simplifies some of the more
  complicated csv.py invocations (make summary, make funcs, etc).

See make help for a full list of commands.
2025-04-20 15:53:12 -05:00
Christopher Haster ea535faaba scripts: codemap[d3].py: Added --tile-* for tiling by stack/ctx/etc
This replaces the previous fallback-to-what's-available behavior with
explicit flags:

- --tile-code - Tile based on code size (the default)
- --tile-stack - Tile based on stack limits
- --tile-frames - Tile based on stack frames
- --tile-ctx - Tile based on function context
- --tile-1 - Tile functions evenly

This has the benefit of 1. being easier to toggle, 2. being explicit,
and 3. allowing code/stack/ctx in punescapes (titles, labels, etc).

There is an interesting question if --no-stack should be implicit, since
showing two stack treemaps may be confusing, but I think that's trying
to be too clever. Instead I just added the -S/--no-stack shortform to
make it easier to toggle.

Also updated ctx.py's description string. Probably need to check what
else is out of date in other scripts as well.
2025-04-16 15:23:14 -05:00
Christopher Haster 7c26bfc0a3 scripts: Simplified csv.py's func/uop/bop/top helpers
Now that I know my way around the weirdness that is Python's class
scope, this just required another function indirection to capture the
class-level dicts correctly.

I was considering using the __subclasses__ trick, but it seems like that
would actually be more complicated here.
2025-04-16 15:23:13 -05:00
Christopher Haster bd70270e11 scripts: Added -w/--word-bits to bound dbgleb128/dbgle32 parsing
This is limited to dbgle32.py, dbgleb128.py, and dbgtag.py for now.

This more closely matches how littlefs behaves, in that we read a
bounded number of bytes before leb128 decoding. This minimizes bugs
related to leb128 overflow and avoids reading inherently undecodable
data.

The previous unbounded behavior is still available with -w0.

Note this gives dbgle32.py much more flexibility in that it can now
decode other integer widths. Uh, ignore the name for now. At least it's
self documenting that the default is 32-bits...

---

Also fixed a bug in fromleb128 where size was reported incorrectly on
offset + truncated leb128.
2025-04-16 15:23:12 -05:00
Christopher Haster 0cea8b96fb scripts: Fixed O(n^2) slicing in Rbyd.fetch
Do you see the O(n^2) behavior in this loop?

  j = 0
  while j < len(data):
      word, d = fromleb(data[j:])
      j += d

The slice, data[j:], creates a O(n) copy every iteration of the loop.

A bit tricky. Or at least I found it tricky to notice. Maybe because
array indexing being cheap is baked into my brain...

Long story short, this repeated slicing resulted in O(n^2) behavior in
Rbyd.fetch and probably some other functions. Even though we don't care
_too_ much about performance in these scripts, having Rbyd.fetch run in
O(n^2) isn't great.

Tweaking all from* functions to take an optional index solves this, at
least on paper.

---

In practice I didn't actually find any measurable performance gain. I
guess array slicing in Python is optimized enough that the constant
factor takes over?

(Maybe it's being helped by us limiting Rbyd.fetch to block_size in most
scripts? I haven't tested NAND block sizes yet...)

Still, it's good to at least know this isn't a bottleneck.
2025-04-16 15:23:11 -05:00
Christopher Haster 8b11cea3f2 scripts: Added dbgleb128.py and dbgle32.py
These mimic dbgtag.py, but provide debugging for the lower-level integer
primitives in littlefs:

  $ ./scripts/dbgleb128.py -x 2a 80 80 a8 01
  2a             42
  80 80 a8 01    2752512

  $ ./scripts/dbgle32.py -x 2a 00 00 00 00 00 2a 00
  2a 00 00 00    42
  00 00 2a 00    2752512

dbgleb128.py is probably going to be more useful, but I figured we might
as well include both for completeness. Though dbgle32.py is begging to
be generalized.
2025-04-16 15:23:11 -05:00
Christopher Haster b5c3b97ae1 scripts: Reworked dbgtag.py, added -i/--input, included hex in output
This just gives dbgtag.py a few more bells and whistles that may be
useful:

- Can now parse multiple tags from hex:

    $ ./scripts/dbgtag.py -x 71 01 01 01 12 02 02 02
    71 01 01 01    altrgt 0x101 w1 -1
    12 02 02 02    shrubdir w2 2

  Note this _does_ skip attached data, which risks some confusion but
  not skipping attached data will probably end up printing a bunch of
  garbage for most use cases:

    $ ./scripts/dbgtag.py -x 01 01 01 04 02 02 02 02 03 03 03 03
    01 01 01 04    gdelta 0x01 w1 4
    03 03 03 03    struct 0x03 w3 3

- Included hex in output. This is helpful for learning about the tag
  encoding and also helps identify tags when parsing multiple tags.

  I considered also included offsets, which might help with
  understanding attached data, but decided it would be too noisy. At
  some point you should probably jump to dbgrbyd.py anyways...

- Added -i/--input to read tags from a file. This is roughly the same as
  -x/--hex, but allows piping from other scripts:

    $ ./scripts/dbgcat.py disk -b4096 0 -n4,8 | ./scripts/dbgtag.py -i-
    80 03 00 08    magic 8

  Note this reads the entire file in before processing. We'd need to fit
  everything into RAM anyways to figure out padding.
2025-04-16 15:23:10 -05:00
Christopher Haster 9085f1fdd9 scripts: crc32c.py/parity.py: Show string with -s/--string
This matches the behavior of paths and helps figure out which string is
associated with which crc32c/parity when checksumming multiple strings:

  $ ./scripts/crc32c.py -s hi hello
  f59dd9c2  hi
  9a71bb4c  hello

It also might help clear up confusion if someone forgets to quote a
string with spaces inside it.
2025-04-16 15:23:09 -05:00
Christopher Haster a5747bb2b2 scripts: dbgmtree.py: Fixed minor mtree rendering/traversal issues
- Added TreeArt __bool__ and __len__.

  This was causing a crash in _treeartfrommtreertree when rtree was
  empty.

  The code was not updated in the set -> TreeArt class transition, and
  went unnoticed because it's unlikely to be hit unless the filesystem
  is corrupt.

  Fortunately(?) realtime rendering creates a bunch of transiently
  corrupt filesystem images.

- Tweaked lookupleaf to not include mroots in their own paths.

  This matches the behavior of leaf mdirs, and is intentionally
  different from btree's lookupleaf which needs to lookup the leaf rattr
  to terminate.

- Tweaked leaves to not remove the last path entry if it is an mdir.

  This hid the previous lookupleaf inconsistency. We only remove the
  last rbyd from the path because it is redundant, and for mdirs/mroots
  it should never be redundant.

  I ended up just replacing the corrupt check with an explicit check
  that the rbyd is redundant. This should be more precise and avoid
  issues like this in the future.

  Also adopted explicit redundant checks in Btree.leaves and
  Lfs.File.leaves.
2025-04-16 15:23:08 -05:00
Christopher Haster 71930a5c01 scripts: Tweaked openio comment
Dang, this touched like every single script.
2025-04-16 15:23:06 -05:00
Christopher Haster 57c77b1b72 scripts: Fixed most flickering issues in RingIO
Two new tricks:

1. Hide the cursor while redrawing the ring buffer.

2. Build up the entire redraw in RAM first, and render everything in a
   single write call.

These _mostly_ get rid of the cursor flickering issues in rapidly
updating scripts.
2025-04-16 15:23:05 -05:00
Christopher Haster 0af90ea44b scripts: dbgtrace.py: Allow no-arg -w/--block-cycles to imply wear
This allows -w to provide a shortform flag for both --wear and
--block-cycles, depending on if you include a cycles argument:

- -w    => --wear
- -w100 => --block-cycles=100

I was originally hesitant to add this since it's inconsistent from
--read/--prog/--erase, which can't have shortforms due to flag
conflicts, but --wear is probably a special enough case.
2025-04-16 15:23:04 -05:00
Christopher Haster c63ed79c5f scripts: Prefer .a for single entry namedtuples
- CsvInt.x -> CsvInt.a
- CsvFloat.x -> CsvFloat.a
- Rev.x -> Rev.a

This matches CsvFrac.a (paired with CsvFrac.b), and avoids confusion
with x/y variables such as Tile.x and Tile.y.

The other contender was .v, since these are cs*v* related types, but
sticking with .a gets the point across that the name really doesn't have
any meaning.

There's also some irony that we're forcing namedtuples to have
meaningless names, but it is useful to have a quick accessor for the
internal value.
2025-04-16 15:23:03 -05:00
Christopher Haster 98b16a9013 scripts: Renamed RInt (and friends) -> CsvInt (and friends)
This prefix was extremely arbitrary anyways.

The prefix Csv* has slightly more meaning than R*, since these scripts
interact with .csv files quite a bit, and it avoids confusion with
rbyd-related things such as Rattr, Ralt, etc.
2025-04-16 15:23:02 -05:00
Christopher Haster 26a29bda31 scripts: Tweaked RFrac to return +-∞ when evaluated as a float
This affects the table renderers as well as csv.py's ratio expr.

This is a bit more correct, handwaving 0/0 (mapping 0/0 -> 100% is
useful for cov.py, please don't kill me mathematicians):

  frac(1,0) => 1/0 (∞%)
  frac(0,0) => 0/0 (100.0%)
  frac(0,1) => 0/1 (0.0%)
2025-04-16 15:23:02 -05:00
Christopher Haster 613fa0f27a scripts: Reverted to -p/--percent not providing a path
So now the result scripts always require -d/--diff to diff:

- before: ./scripts/csv.py a.csv -pb.csv
- after:  ./scripts/csv.py a.csv -db.csv -p

For a couple reasons:

- Easier to toggle
- Simpler internally to only have one diff path flag
- The previous behavior was a bit unintuitive
2025-04-16 15:23:00 -05:00
Christopher Haster a5e59b2190 scripts: maps: Reverted all padding for status strings
After all, who doesn't love a good bit of flickering.

I think I was trying to be too clever, so reverting.

Printing these with no padding is the simplest solution, provides the
best information density, and worst case you can always add -s1 to limit
the update frequency if flickering is hurting readability.
2025-04-16 15:22:59 -05:00
Christopher Haster 27152ec597 scripts: maps: Adopted persistent padding for status strings
This automatically minimizes the status strings without flickering, all
it took was a bit of ~*global state*~.

---

If I'm remembering correctly, this was actually how tracebd.py used to
work before dbgbmap.py was added. The idea was dropped with dbgbmap.py
since dbgbmap.py relied on watch.py for real-time rendering and couldn't
persist state.

But now dbgbmap.py has its own -k/--keep-open flag, so that's not a
problem.
2025-04-16 15:22:58 -05:00
Christopher Haster 97c2287177 scripts: maps: Assume percentages never hit 100.0%
This isn't true, especially for dbgbmap.py, 100% is very possible in
filesystems with small files. But by limiting padding to 99.9%, we avoid
the annoying wasted space caused by the rare but occasional 100.0%.
2025-04-16 15:22:57 -05:00
Christopher Haster eb4c4c612e scripts: Dropped --padding from ascii art scripts
No one is realistically ever going to use this.

Ascii art is just too low resolution, trying to pad anything just wastes
terminal space. So we might as well not support --padding and save on
the additional corner cases.

Worst case, in the future we can always find this commit and revert
things.
2025-04-16 15:22:56 -05:00
Christopher Haster 9008e8c82c scripts: Renamed tracebd.py -> dbgtrace.py
This matches dbgbmap.py and fits in with the other dbg scripts.

The choice of tracebd.py for the name was arbitrary anyways, we just
needed something that wouldn't conflict with other scripts.
2025-04-16 15:22:55 -05:00
Christopher Haster 5e817be9cc scripts: maps: Cleaned up comments and junk
This took a bit of a messy route, but these scripts should be good to go
now.
2025-04-16 15:22:54 -05:00
Christopher Haster 50f652d44f scripts: maps: Cleaned up/moved header generation before rendering
Should've probably been two commits, but:

1. Cleaned up tracebd.py's header generation to be consistent with
   dbgbmap.py and other scripts.

   Percentage fields are now consistently floats in all scripts,
   allowing user-specified precision when punescaping.

2. Moved header generation up to where we still have the disk open (in
   dbgbmap[d3].py), to avoid issues with lazy Lfs attrs trying to access
   the disk after it's been closed.

   Found while testing with --title='cksum %(cksum)08x'. Lfs tries to
   validate the gcksum last minute and things break.
2025-04-16 15:22:53 -05:00
Christopher Haster f0b8d34230 scripts: maps: Fixed divide-by-zero when packing blocks into small maps
This can be hit when dealing with very small maps, which is common since
we're rendering to the terminal. Not crashing here at least allows the
header/usage string to be shown.
2025-04-16 15:22:52 -05:00
Christopher Haster cffa9ec67e scripts: Adopted ring name for stdout substitution 2025-04-16 15:22:51 -05:00
Christopher Haster 5952431660 scripts: Consistently use color='auto' default in main 2025-04-16 15:22:50 -05:00
Christopher Haster 61ce23ce7e scripts: maps: Fixed some aspect ratio issues, limited scope
Replacing -R/--aspect-ratio, --to-ratio now calculates the width/height
_before_ adding decoration such as headers, stack info, etc.

I toying around with generalizing -R/--aspect-ratio to include
decorations, but when Wolfram Alpha spit this mess for the post-header
formula:

      header*r - sqrt(4*v*r + padding^2*r)
  w = ------------------------------------
                        2

I decided maybe a generalized -R/--aspect-ratio is a _bit_ too
complicated for what are supposed to be small standalone Python
scripts...

---

Also fixed the scaling formula, which should've taken the sqrt _after_
multiplying by the aspect ratio:

  w = sqrt(v*r)

I only noticed while trying to solve for the more complicated
post-decoration formula, the difference is pretty minor.
2025-04-16 15:22:48 -05:00