Commit Graph

1094 Commits

Author SHA1 Message Date
Christopher Haster 1d92169e5b Tweaked cache size to temporarily avoid pathological shrub overflows
This will stop being a problem when we actually have btrees, but for now
the fragmentation caused by byte-level syncs was easily enough to
overflow an mdir when cache size is big.

A smaller cache size is also nicer for debugging, since smaller cache
sizes results in data getting flushed to disk earlier, which is easier
to inspect than in-device buffers. And a 16-byte cache still provides
decent test coverage over cache interactions.

---

Also dropped inline_size to block_size/8. I realized while debugging
that opened shrubs take up additional space until we sync, so we need to
expect up to 2 temporary copies of shrubs when writing files.
2023-10-13 23:35:24 -05:00
Christopher Haster 3fb4350ce7 Updated dbg scripts to support shrub trees
- Added shrub tags to tagrepr
- Modified dbgrbyd.py to use last non-shrub trunk by default
- Tweaked dbgrbyd's log mode to find maximum seen weight for id padding
2023-10-13 23:35:03 -05:00
Christopher Haster dc8bc447e6 Renamed deferred/inlined trees to shrubs
Get it? Because they're small trees!

Joking aside, having a new term for these helps structure and describe
the filesystem at a high-level without needing to say "inlined trees"
all the time.

Shrub trees are small rbyd trees inlined directly in a file's mdir.
2023-10-13 23:31:58 -05:00
Christopher Haster 67bf64f45b Inlined tree compactions are now working via some more ~hacks~ features
The main issue was that we need to potentially overwrite our staging are
if we fail a compaction and need to split. This clobbers any inlined
state staged at higher-levels, such as creating new inlined trees.

The solution here is to just initialize new inlined trees in the low
level lfsr_mdir_commit__ commit. This is a bit of a hack, but makes
things work, which is always a plus.
2023-10-13 23:27:21 -05:00
Christopher Haster 9f0160556f Made significant progress around inlined-file state during mdir commits
The main improvement is moving the special inlined-file compaction logic
up into lfsr_mdir_compact__. We only need this logic for files stored in
mdirs, and thanks to its recursive nature, we weren't getting any
benefit from handling this at a lower level anyways.

This is a nice logical restructuring that probably saves a bit of code
cost in the end.

Another significant improvement is moving the staging copy of the
inlined tree's state up into the file struct itself. This solves the
problem of needed N copies of temporary inlined state when you have N
open files.

It also provides a central place to stage changes when compacting
inlined trees, which happens across several different places in the mdir
commit logic. Though some may see this as more a hack than a feature.

Also note-worthy, but minor: these changes required an additional
opened-mdir linked-list to know when the mdir is a file and may contain
an inlined tree.
2023-10-13 23:19:24 -05:00
Christopher Haster 541fb07da4 Made progress torwards inlined files surviving compaction
Inlined files are unfortunately turning out to have more cost than
expected, mainly due to our strict no-recursion requirement.

It turns out recursively nesting (bounded) trees in a system without
recursion is a recipe for duplicating code. Though there may be other
ways to structure this.

One interesting hiccup during development is the need to have both NULL
tags and DEFERREDNULL tags in order to tell inlined trees apart from the
main tree during compaction.
2023-10-13 23:13:51 -05:00
Christopher Haster 2f38822820 Still missing quite a bit, but rudimentary inlined-trees are now working
And by working, I mean you can create inlined trees, just don't
compact/split/move/etc anything. But this does outline the path files
take when writing buffers into inlined trees.

"Inlined trees" in littlefs are entire small rbyd trees embedded as
secondary trees in an mdir's main rbyd tree. When fetching, we can
indicate if a given trunk belongs to the main tree or secondary tree by
setting one of the unused mode bits in the trunk's tag, now called the
"deferred" bit. This bit doesn't need to be included in the alt's "key"
field, so there's no issue with it conflicting with the alt's mode bits.

This requires a bit of tweaking lfsr_rbyd_fetch, since it needs to fall
back to the previous trunk if it discovers the most recent trunk belongs
to an inlined tree. But as a benefit we can leverage the full power of
rbyds in inlined files, including holes, partial updates, etc.

One downside is it looks like these inlined trees may involve more work
in maintining their state correctly, since they need to be sort of
"brought along" when mdirs are compacted, even if they don't actually
have a reference in the mdir yet. But the sheer amount of flexibility
this gives inlined files may make this overhead worth it.
2023-10-13 23:11:35 -05:00
Christopher Haster c3533ab816 Some progress, with deferred attributes taking shape
Ran into an interesting macro-related bug. Turns out the way we are
doing implicit prefixing in TAG/ATTR macros sort of breaks how C macros
work a bit. The following does not compile:

  lfsr_mdir_commit(lfs, &file->m.mdir, LFSR_ATTRS(
          LFSR_ATTR(file->m.mdir.mid, DEFER, 0, DEFER(
              (lfsr_rbyd_t*)&file->inlined,
              LFSR_ATTR(file->buffer_pos,
                  DEFERRED(INLINED), +file->buffer_size, BUF(
                      file->buffer, file->buffer_size))))));

Or to distill it down, this does not compile:

  #define LFSR_ATTR(_data)  (LFSR_##_data)
  #define LFSR_DEFER(_data) (LFSR_##_data)
  #define LFSR_DATA(_data)  (_data)

  int a = LFSR_ATTR(DEFER(ATTR(DATA(1))));

But this does:

  #define LFSR_ATTR(_data)  (_data)
  #define LFSR_DEFER(_data) (_data)
  #define LFSR_DATA(_data)  (_data)

  int a = LFSR_ATTR(LFSR_DEFER(LFSR_ATTR(LFSR_DATA(1))));

Why? Well it turns out the whole way nested C macro's work is a big
hack.

A very reasonable design decision in C is to disallow recursive macro
expansions. Unlike C++, we don't want our preprocessor to suddenly stack
overflow. This rule is enforced by stopping macro expansion when a macro
contains itself. For example:

  #define A() B()
  #define B() A()

  A()

Expands to:

  A()
      -> B()
      -> A() (stops, probably erroring with 'A' undeclared)

But it _is_ common to want to recursively expand macro arguments. Macros
are a part of C's syntax after all, and users usually expect
expressions, such as arguments, to be context-free:

  #define A(x) (x) + 1

  A(A(A(A(A(0)))))

Naively this would expand to:

  A(A(A(A(A(0)))))
      -> (A(A(A(A(0))))) + 1 (stops)

The big hack that makes this work in C's preprocessor is the "Argument
prescan". Instead of expanding the "called" macro first, we expand any macro
inside our argument list, _then_ expand the "called" macro, and _then_
expand any new macros produced as a result of the expansion again just
for good measure.

So the above actually expands to:

  A(A(A(A(A(0)))))
      -> A(A(A(A((0) + 1))))
      -> A(A(A(((0) + 1) + 1)))
      -> A(A((((0) + 1) + 1) + 1))
      -> A(((((0) + 1) + 1) + 1) + 1)
      -> (((((0) + 1) + 1) + 1) + 1) + 1

This is still recursive actually! But the recursion is limited to the
actual length of the source code, so the developers likely thought this
was a reasonable tradeoff.

But what does this mean for our implicit prefixing?

  #define P_A(x) P_##x
  #define P_B(x) P_##x
  #define P_C(x) (x)

  P_A(B(A(C(0))))

None of A, B, C are in scope without prefixes, so they get expanded
after the "called" macro's expansion:

  P_A(B(A(C)))
      -> P_B(A(C(0)))
      -> P_A(C(0)) (stops)

But this breaks when we hit the nested P_A macro.

---

For now I've gone with the temporary, and extra hacky, solution of
introducing a second LFSR_ATTR_ macro. This nesting of ATTR macros only
happens because of shrubs, and only ever goes 2 layers deep.

In the future maybe we should move away from implicit prefixing. They
have a few rough corners and may be a bit confusing for anyone new to
the code.
2023-10-13 22:00:55 -05:00
Christopher Haster 6daa503ee2 Moved around some bits in internal tags to make space for a deferred bit
This is in order to support deferred-inlined files, which involves
intertwining secondary trees into an rbyd. In order to know which trunks
go to which trees, we need an additional bit to indicate if a tag is on
the primary tree or a secondary tree.

We were using pretty much all of our tag bits, but the rm and valid
bits can be combined. They more-or-less serve the same purpose.

New tag modes:

  v000tttt 0ttttttt - normal tags
  v001tttt 0ttttttt - deferred tags
  v010tttt 0ttttttt - checksum tags
  v1dckkkk 0kkkkkkk - alt tags
  ^'+''-----+-----'
  '-|-------|- valid bit
    '-------|- tag mode
            '- tag type/key

Note that once we have a trunk, we don't need this deferred bit to
traverse the rbyd. This is why we can get away with using a mode bit
that would normally collide with the alt tag's encoding.

Also tweaked lfsr_rbyd_appendattr so GROW tags don't need to set the rm
bit anymore. This is just an internal usability thing.
2023-09-17 20:34:25 -05:00
Christopher Haster c74ec1c133 Initial commit of basic file creation
Currently limited to inlined files and only simpler truncate-writes.

But still this lets us test file creation/deletion.

This is also enough logic to make it clear that, even though we have
some powerful high-level primitives, mapping file operations onto these
is still going to be non-trivial.
2023-09-17 11:04:44 -05:00
Christopher Haster e7bf5ad82f Added scripts/crc32c.py
This seems like a useful script to have.
2023-09-15 18:42:48 -05:00
Christopher Haster dd6a4e6496 Dropped the header from dbg scripts
I had never noticed xxd has no header until comparing its output against
dbgblock.py. Turns out these headers aren't really all that useful, and
even sometimes wrong in dbglfs.py.
2023-09-15 17:45:17 -05:00
Christopher Haster 2cdd03c8fd Added dbgblock.py for quicker hex dumps
This script is basically the same as xxd, but with the other debug
script's block address format:

  $ ./scripts/dbgblock.py disk -B4096 1
  block 0x1, size 20
  off       data
  00000000: 00 00 00 00 00 03 00 08 6c 69 74 74 6c 65 66 73  ........littlefs
  00000010: 40 03 00 0c 80 04 00 02 02 00 d0 03 00 16 c0 04  @...............
  ...
2023-09-15 17:45:13 -05:00
Christopher Haster c1fe64314c Reworked how filesystem-level config is stored
Now, instead of storing a single contiguous block of config data, config
is stored as tagged metadata like any other attribute.

This allows more flexibility towards adding/removing config in the
future, without cluttering up the config with deprecated entries (see
ATA's "IDENTIFY DEVICE" response).

Most of the config entries are single leb128 limits on various integer
types, with the exception of the magic string and version (major/minor
pair).

---

Note this also includes some semantic changes to the config:

- Limits are stored as size-1. This avoid issues with integer overflow
  at extreme ranges.

  This was also adopted for block size (block limit) and block count
  (disk limit). This deviation between on-disk config and user-facing
  config risks confusion, but allows the potential for the full 2^31 range
  for these values.

- The default cksum type, crc32c, has been changed to 0.

  Originally this was 2 to allow the type to map to the crc width for
  crc8, crc16, crc32c, crc64, etc. But dropping this idea and numbering
  checksums as they are implemented simplifies things.

  May come back to this.

- Storing these configs as attributes opens up of the option of on-disk
  defaults when configs are missing.

  I'm being a bit conservative with this one, as it's not clear to me if
  we should prefer default configs (less code/storage, risk of untested
  config parsing) or prefer explicit on-disk configs.

  Currently the following have defaults since they seem the most obvious
  to me:

  - cksum type  => defaults to crc32c
  - redund type => defaults to parity (TODO, should this default to
    no redund?)
  - utag_limit  => defaults to 0x7f (no special tag decoding)
  - uattr_limit => defaults to block_limit (implicit)
2023-09-15 14:51:25 -05:00
Christopher Haster 5f3994c83b Renamed mbits/mlimit to mleaf_bits/mleaf_limit
- mbits -> mleaf_bits
- mlimit -> mleaf_limit
- mweight -> mleaf_weight
- lfsr_mridmask -> lfsr_midrmask
- lfsr_mbidmask -> lfsr_midbmask

This is a bit tricky to name, since we want to clarify it's not the
mtree limit and not the mdir's actual rbyd weight. But this also risks
confusing around the difference between mdirs/mleaves (mdirs are
mtree's leaves).
2023-09-15 14:09:42 -05:00
Christopher Haster 5504936b10 Added mlimit to the superconfig, dropped mtreelimit
This should be stored in the superconfig, and we should use it during
mount instead of rederiving it from the block_size (TODO).

Note that this stores the "mlimit", (1 << mbits)-1, not the mbits
directly. littlefs will probably always be limited to powers-of-two for
this, since mbits is fairly arbitrary, but storing the expanded value
allows for non-powers-of-two _just in case_.
2023-09-14 15:02:55 -05:00
Christopher Haster d44f9bdcd0 Prefer function-like macros when the result is a struct 2023-09-14 13:35:49 -05:00
Christopher Haster 518e9634e7 Tweaked gstate after mid changes
- Fixed LFSR_GRM_DSIZE upper bound, since our mids now fit in a single
  leb128.

- Renamed pgrm -> ggrm. To be honest I don't have a great name for this
  variable.
2023-09-14 13:27:31 -05:00
Christopher Haster d0c5bf1210 Adopted lfsr_data_from* pattern for internal data encoding
Taking advantage of the fact that these functions should never error,
changing the return type to lfsr_data_t allows all of the encoding
information to be passed around quite easily.

And, by giving each lfsr_data_from* function an LFSR_DATA_FROM* macro,
these functions can participate in our attr-list generating macros:

  LFSR_ATTR(-1, MTREE, 0, FROMBTREE(lfs, mtree, mtree_buf))

Though one thing to watch out for is the borrowed buffer that stores the
actual data. This might welcome use-after-free bugs since it's not super
clear the buffer remains borrowed. Will need to watch out for this.
2023-09-14 13:20:49 -05:00
Christopher Haster e4032de089 Tried to clean up err handling a bit
- Removed redundant int err declarations.

- Preferred combining "if (err)" conditions such that err gets tested
  before any gotos/breaks/etc. The compiler is smart enough to figure
  this out on its own, but it makes the code more readable in some
  places.
2023-09-14 11:31:33 -05:00
Christopher Haster 7aa9280897 Added lfsr_rid/bid/mid/did_t types, tried using types more consistently
Adopted lfsr_rid/bid/mid/did_t where appropriate. This includes using
lfsr_rid_t for tag/rbyd weights. Although I am using lfsr_srid_t for
rbyd weights now, since it both captures the use of the sign bit and
reduces the number of casts a bit in the code.

I learned recently Zig has any-bit integers (e.g. uint31_t), and I'm
realizing how nice it would be to have those in this codebase.

Also tried to use lfs_size_t/lfs_off_t more correctly. In Linux/BSD,
only off_t is used for file-size-related operations and is usually much
larger than size_t. These were used interchangably in littlefs and their
original meaning kind of fell by the wayside. Getting their use right
will be important if littlefs ever supports different integer widths.
2023-09-14 11:31:28 -05:00
Christopher Haster b5c9b8eb49 Dropped lfsr_tag_next for tag+1
We were already using tag-1 several places anyways.
2023-09-14 00:37:42 -05:00
Christopher Haster 900ea807ae Changed to a shifted mid=bid.rid representation for debugging
This only matters for developers, not users, but it still helps a lot to
get debug representations right.

Since the exact mid encoding depends on the block_size in an unintuitive
manner, it's tricky to render in a debug-friendly way that is useful
both with and without tools.

Previously, I avoided shifting the bid representation, since this would
be closer to the value in the device, but this hides the actual
structure of the mtree. Now the bid is shifted, showing the underlying
mtree/mdir structure, at the cost of needing to know the number of mbits
to encode the mid back into an integer.

So for example, on a device with 4KiB blocks, or 8 mbits:

  mid=1
  mid=258
  mid=515

Becomes:

  mid=0.1
  mid=1.2
  mid=2.3

This continues to make the mbits a more fundamental part of littlefs,
but that's probably just how that's going to be.
2023-09-14 00:36:13 -05:00
Christopher Haster f9bd2c56e3 Created an explicit local copy of the mdir's mid in lfsr_mdir_commit
Knowing C's issues with pointer aliasing, I was wondering if this might
save some code cost by avoiding unnecessary indirect loads of the mid.

But, as is often the case, the compiler is smarter than it first appears:

            code          stack
  before:  21052           1744
  after:   21048 (-0.0%)   1744 (+0.0%)

Still, sometimes an optimization is better when written out explicitly,
so I'll keep this for now.
2023-09-14 00:36:13 -05:00
Christopher Haster ba571cf83f Reverted the second mdir in lfsr_dir_t, simplified dir updates
This reverts the big hack of treating the lfsr_dir_t as an mdir array in
lfsr_mdir_commit in an effort to deduplicate the bookmark/pos mid
updates.

It worked, but lets be honest, it was a big hack and probably not very
maintainable. It made other opened-mdir updates, such as propagation of
unerases more complex, and is made the code a bit unreadable.

We also don't really need a full mdir for the dir's bookmark, since
rewinds really aren't that common, and a single mtree lookup in that
case gets the job done. Removing the bookmark mdir (though we still need
the bookmark mid to adjust the dir pos correctly) saves 24 bytes from
every lfsr_dir_t.

It would be nice to deduplicate some of the mid logic here, but that's
been difficult because of mid-related side-effects, such as updating the
mdir's pos. There may be room for improvement here.

---

This looks pretty bad, with the additional loop over the attr-list to
update just the dir's bookmark, but it's really not that bad when
compiled, and probably worth the code readability:

            code          stack          structs
  before:  20958           1744              864
  after:   21052 (+0.4%)   1744 (+0.0%)      840 (-2.9%)
2023-09-14 00:36:13 -05:00
Christopher Haster cced7d66ef Implemented unerased-propagation in commit functions
This is a tricky nuance of how rbyd's erased state interacts with
possible errors during commits.

- If an rbyd passes its ecksum during rbyd-fetch, it's erased and we can
  write to it.

- If an rbyd is committed to successfully and still has erased space
  remaining, it's erased and we can write to it.

- But if we fail to commit to the rbyd, we can't be sure the trailing
  data is still erased. It most likely isn't, and we would need to fetch
  again to check the ecksum. And since errors are exceptional here, we
  might as well just mark any failed commits as unerased, triggering a
  compaction on the next write to the rbyd.

To make things more annoying, changing state in all error routes is
tricky to get right, and trickier to test. To keep this relatively
simple and robust, all rbyd/btree/mdir operations mark the original copy
as unerased until the commit succeeds, and then clears the unerased
state. This fits in well with how we make copies of the rbyd/btree/mdir
structs in the relevant functions.

Note this needs to affect _all_ copies of the rbyd, including any opened
mdirs, mroots, etc. This will probably still lead to some bugs in the
future...
2023-09-14 00:36:13 -05:00
Christopher Haster 2b98d62637 Tweaked mchildroot propagation again, adopted "mblocks" more consistently 2023-09-14 00:36:13 -05:00
Christopher Haster 610d290797 Dropped strict pcache asserts, cleaned up mdir/rbyd error handling
This assert in lfs_bd_prog, which detects if a pcache gets reused
without either a flush or drop, has been the source of quite a number of
debugging experiences, ensuring that pcaches are always in an intentioned,
managed state.

This serves to... make this assert happy.

Really, why did I keep this around for so long. It effectively forces a
sort of manual memory management on a resource that doesn't really need
to be managed. It's extra messy and tricky thanks to the number of
(poorly tested) routes errors can go through, making recovery after an
error a risky gamble with this assert enabled.

So this commit drops this strict pcache assert, instead detecting when
the targetted block changes and implicitly zeroing the cache in that
case.

This simplifies rbyd/mdir error handling, where internal errors, such as
RANGE on rbyd overflow, are common and part of normal operation.

---

This also cleans up mdir error handling a bit, and makes mdir drops a
NOENT error. mdir drops are a bit special in that they don't finish the
commit and can't be read from again (which has already led to a couple
bugs), so making the exceptional behavior of mdir drops more clear is
probably a good thing...
2023-09-14 00:36:13 -05:00
Christopher Haster 17400c5c34 Cleaned up reworked commit logic, deduplicated rbyd/attr things again
This attempts to clean up and deduplicate rbyd operations where
possible, without losing the cleaner logic introduced by the commit
rework.

Some tradeoffs were made:

- In btree merges, we append the split name after the compaction.

  This means the split name doesn't get compacted when we merge, but
  avoids making the merge compactions special cases.

- We never clean up vestigial names.

  This one bothers me, since it means we can end up with names that
  never get cleaned up. But then again, that's already true of any names
  that get pushed up in the btree inner nodes that aren't the leading
  btree entry.

  By never cleaning these up, all rbyd compactions in the system behave
  the same.

- We don't push gstate into the mroot during relocations.

  This would be a nice-to-have, but would require lfsr_mdir_commit__ to
  know if we are relocating or extending. And mdirs need to reserve space
  for gstate anyways, so it's not the end of the world to leave a bit
  of extra gstate around.

Also some attr-list operations are not deduplicated due to how special
they are:

- The writing of attrs in lfsr_mdir_commit__, this is where we adjust
  mids->rids and handle special internal attr.

  This is a pain, since we end up duplicating the attr-list range
  operations, but on the plus side keeps the special mdir attrs out of
  the rbyd layers, and saves a bit of RAM from the hot-path.

- The copying of config attrs during mroot extensions.

  This one is just tricky because we want to keep the config attrs, but
  not the gstate attrs or any custom attributes. An explicit compaction
  of only the subrange of config attrs gets the job done.

These changes get our code/RAM costs pretty much back where they
started:

                        code          stack
  before mdir rework:  20826           1744
  after mdir rework:   21434 (+2.8%)   1768 (+1.4%)
  after mdir cleanup:  20850 (+0.1%)   1736 (-0.5%)

It's interesting to note the slight tradeoff of code/RAM here (though
this is very close to the compiler noise floor) comes from the moving of
special mdir attr logic up into lfsr_mdir_commit__.

I wasn't expecting this, but it makes sense since this moves the special
attr handling out of the hot-path going through the mtree commit.
2023-09-14 00:35:53 -05:00
Christopher Haster fc937a7060 Restructured high-level commit logic (across rbyds/btrees/mdirs)
This flattens a number of low-level APIs, mainly the rbyd-attr-list
APIs, into higher-level logic in an effort to remove special flags,
awkward hacks, etc. This comes at a cost, should probably be cleaned
up/deduplicated a bit more, but creates a level of code transparency
that hopefully helps reveal where some logic can be simplified.

One change is the addition of incremental compaction APIs:

- lfsr_rbyd_appendcompactattr
- lfsr_rbyd_compact

These allow upper-layers to build rbyd compactions incrementally, as
long as they ensure attrs are written in order. This makes the btree
merge no longer a special case and even allows us to write the split
name into the rbyd during compaction.

Another big change is the inversion of the mdir commit/compaction logic.
Previously, lfsr_mdir_compact_ was the ground-level mdir operation, but
since lfsr_mdir_compact_ still needs to write out the attrs after
compaction, this led to a lot of mdir logic leaking into the rbyd
functions.

Now, there is a mid-level lfsr_mdir_commit_ that handles both normal
commits and compactions, with a low-level lfsr_mdir_commit__ that
handles only the writing of mdir attributes.

This also leads to a bit better code reuse, as upper-layer mdir logic
often needs to do a low-level commit with the expectation of no
compaction. No more special mdir compaction "reason" enum.

Before:

  lfsr_mdir_commit
  '-> lfsr_mdir_commit_
      |-> lfsr_rbyd_commit
      '-> lfsr_mdir_compact_
          '-> lfsr_rbyd_compact

After:

  lfsr_mdir_commit
  '-> lfsr_mdir_commit_
      |-> lfsr_mdir_commit__
      |   '-> lfsr_rbyd_commit
      '-> lfsr_rbyd_compact

Also, thanks to inlining the compaction logic, our mroot extension can
now copy the config attrs directly from the previousl mrootanchor,
instead of the previous roundabout method of committing the explicit
config attributes we want to keep.

            code          stack
  before:  20826           1744
  after:   21434 (+2.8%)   1768 (+1.4%)
2023-09-11 11:08:40 -05:00
Christopher Haster 78cdd8008c A number of small tweaks to mdir/btree commit
Mostly just moving things around in what seems like a fruitless effort
to make this code more readable.

The biggest change is the deduplication of the special split-drop cases
in mdir commits by sprinkling in a few gotos. xkcd.com/292 seems
relevant, but this does get the job done...

            code          stack
  before:  20918           1744
  after:   20826 (-0.4%)   1744 (+0.0%)
2023-09-11 11:07:32 -05:00
Christopher Haster b06d48364d Switched to mid=-1 to detect removed mids, drops lfsr_mdir_isdropped
This is a simpler way to track dropped mids. Setting trunk=0 was more a
workaround that worked but added more purpose to the trunk field than
originally needed. The mdir's trunk usually still exists after all.

Using mid=-1 previously didn't work due to conflict with mid=-1 to
indicate an mdir is an mroot, but since removed mids only appear in the
opened-mdir list, and the opened-mdir list stores inlined mdirs as
mid=0, this is no longer a problem.

One downside of this change is we no longer get implicit NOENT behavior
from lfsr_rbyd_lookup when attempting to lookup a removed mid, but it
wasn't clear this behavior was going to be very useful...
2023-09-11 10:52:15 -05:00
Christopher Haster 441181d3d7 Added some more tests over reading dirs during fs mutation
These tests serve as a direct example of why we can't just return the
difference between the dir's bookmark mid and position mid, which is
unfortunate.
2023-09-05 10:51:46 -05:00
Christopher Haster c56124f90f Added handling of readonly grms to the mtree layer
This bit of code allows us to mount an "inconsistent" filesystem after
powerloss and behave as though we've fixed any pending grms without
actually fixing the grms. This lets the filesystem appear consistent
without needing to modify the disk, and allows truely readonly mounts
without sacrificing powerloss-resilience.

This works by just checking any readonly mid operations against pending
grms and returning NOENT if a fix would remove the mid. Fortunately the
more complex mid operations occur when mutating the filesystem, which we
can ignore as any mutation must be preceded by fixing pending grms.

This check has been added to lfsr_mtree_namelookup and lfsr_mtree_seek,
which should propagate the behavior to high-level functions with minimal
code impact.

This leaves only lfsr_mtree_lookup ignoring pending grms, which is useful
because we need it to actually fix the grms. I don't believe this
function will ever be called by a high-level function directly...

Coverage of readonly grms have also been added to the tests.
2023-09-05 10:10:33 -05:00
Christopher Haster cf90398197 Some small tweaks to mdir functions
- Added lfsr_mdir_lookupnext, for iteration through only a single mid.
  This is useful for MOVE attributes.

- Renamed LFSR_MDIR_MROOTANCHOR -> LFSR_MROOTANCHOR.

- Renamed functions that operate on mdir blocks lfsr_mdir_* ->
  lfsr_mblocks_*.

- Reordered arguments in lfsr_mdir_fetch.

- Renamed mrid_bits/mbid_weight -> mbits/mweight.
2023-09-05 10:10:33 -05:00
Christopher Haster f7900edc1c Updated dbg scripts with changes, adopted mbid.mrid in debug prints
This format for mids is a compromise in readability vs debugability.

For example, if our mbid weight is 256 (4KiB blocks), the 19th entry
in the second mdir would be the raw integer 275. With this mid format,
we would print it as 256.19.

The idea is to make it easy to see it's the 19th entry in the mdir while
still making it relatively easy to see that 256.19 and 275 are
equivalent when debugging.

---

The scripts also took some tweaking due to the mid change. Tried to keep
the names consistent, but I don't think it's worthwhile to change too
much of the scripts while they are working.
2023-09-05 10:10:30 -05:00
Christopher Haster a9b81820b0 Adopted rid-bound-dependent compressed mids.
This adopts a previously discarded idea for compressed mids with a few
tweaks to avoiding decoding the bid/rid portions as much as possible.

The idea of compressed mids is to shove both the mid bid and mid rid
into a single integer, saving RAM and potentially helping filesystem
integration where a unique per-file integer is useful.

Unfortunately this has proven tricky. littlefs fundamentally needs two
ids, one "bid" to lookup which mdir our entry resides on, and one "rid"
to lookup the entry in the mdir. It's tempting to use two half-sized
integers (16-bit for example), but this risks surprising limitations
around the number of files when blocks are either really large or
really small.

Optimally, we'd limit the number of bits reserved for the rid to the
upper bound of number of rids that can fit in a single mdir. This would
allows for more bids when the block size is small, and more rids when
the block size is large. This should roughly approximate the limits of
a per-file integer.

With a bit of math we can estimate the upper bound to be <=block_size/16
with our current compaction strategy.

This idea was previously discarded due to the overhead of extracting the
bids/rids when we need them, but the RAM savings and file-to-integer
mapping is too useful to give up. When it became clear half-width
integers wasn't really going to work, compressed mids became the new
plan:

  0bbbbbbb bbbbbbbb bbbbbbbb rrrrrrrr
  ^'-----------+-----------' '---+--'
  '------------|-----------------|---- sign-bit, reserved for driver
               |                 '---- nlog2(bs/16) bits for rid
               |                       (8-bits for 4KiB blocks)
               '---------------------- remaining bits for bid
                                       (23-bits for 4KiB blocks)

To reduce the overhead of encoding/decode bids/rids a few extra features
were added to the internal mdir APIs:

1. The mtree has been changed to store mids directly. Giving each mdir
   the upper bound as a weight. This allows direct lookup of mids
   without any sort of bid decoding, though does bake the upper bound
   estimate into the metadata of the filesystem, which isn't the
   cleanest design, but if it works it works.

   On the plus side, with this upper bound baked in to the filesystems,
   GRMs can be encoded in a single leb128, which is nice. This may have
   other savings if we ever store mids anywhere else in the filesystem.

2. rids are now mid relative in lfsr_mdir_lookup when non-negative. This
   is implemented with a simple condition that is hopefully optimized
   out when inlined, though there may be some room for improvement here.

3. rids are now mid relative in lfsr_mdir_commit. This was a bit tricky,
   but we can leverage the existing mechanisms for bid-relative rids
   used in the btree implementation.

The above changes make it so you can pass the mid around directly for
most of the mdir functions, hopefully reducing the mid decoding
overhead. This savings should only grow as more high-level filesystem
APIs are added.

Here is the resulting code/RAM changes for this entire change (from
before we adopted the mroot bit):

            code          stack          structs
  before:  20590           1784              908
  after:   20890 (+1.4%)   1744 (-2.3%)      864 (-5.1%)
2023-08-31 14:34:54 -05:00
Christopher Haster 2ea569c746 Fixed inlined mid -1/0 equivalence issue, made mid.bid left-leaning
We weren't comparing mid=-1/mid=0 correctly in lfsr_mdir_commit, which
can happend now thanks to inlining mids in our mroot. This went
unnoticed because we were just copying mroot.mid in our tests so we
never actually tested with mid=0. This is fixed now and the tests test
with a literal mid=0.

This also changes the mbids to be left-leaning, carving out an
mrid-sized number of bits from the mbid, making the route to compressed
mids easier.
2023-08-31 13:57:18 -05:00
Christopher Haster 19f2b24161 Dropped mroot bit, rely on context to determine mroots
This greatly simplifies mid handling at the cost of increased subtlety
around determining if a given mdir is an mroot.

Fortunately it turns out we can rely on context to determine if an mdir
is an mroot or not:

1. If an mdir's mid.bid == -1, it's an mroot. This is always true for
   fake mroots, since they can't hold any inlined mids.

2. If the mtree is inlined (mtree.weight == 0), all mdirs are mroots.
   This lets us use mid.bid=0 for inlined mids. We just need to check
   if the mtree is inlined before deciding if the mdir is an mroot or
   not.

The makes it so that for any non-mroot mdir, mid.bid=-1 is always a
reserved value. Which is very useful for compressed mids.
2023-08-31 13:45:24 -05:00
Christopher Haster dbf6d4579d Tweaked lfsr_rbyd_appendattrs to better support compressed mids
lfsr_rbyd_appendattrs has looked this way before. This is a revert of a
previous change to merge the bid and start_rid arguments of
lfsr_rbyd_appendattrs, which is appealing since they do very similar
things.

Unfortunately merging bid/start_rids isn't as simple the moment you want
include -1 rids with a non-zero bid, which is the current plan for
compressed mids.
2023-08-31 13:40:50 -05:00
Christopher Haster 94941806c7 Changed mtree to be weighted by mdir upper bound
More on this when explaining compressed mids, but basically the idea is
instead of just storing all mdirs in our mtree as single element
entries, store each mdir in as a weighted entry, where the weight is a
known upper bound on the possible number of mid entries in a single
mdir.

With the current mid representation, this just complicates things
without much benefits. But with compressed mids it allows us to lookup
mdirs with the mid directly, and avoid decoding the bid from the mid in
some cases.

The mid-per-mdir upper bound is derived from the block size. We know:

1. Each tag needs <=2 alts+null with our current compaction strategy
2. Each tag/alt encodes to a minimum of 4 bytes

This gives us ~4*4 or ~16 bytes per mid at minimum. If we cram an mdir
with the smallest possible mids, this gives us at most ~block_size/16
mids in a single mdir before the mdir runs out of space.

Note we can't assume ~1/2 block utilization here, as an mdir may
temporarily fill with more mids before compaction occurs.
2023-08-31 13:32:21 -05:00
Christopher Haster af5f4bff21 Rearranged lfsr_mkdir to avoid tracking an imaginary mid
lfsr_mkdir creates two mid entries atomically, one for the bookmark and
one for the actual dir entry. It looks up where to insert both, which is
necessary for some other checks, and then inserts one mid while tracking
the position of the other mid using our opened-mdir subsystem.

But it's a bit of a challenge to track an mid that hasn't been created
yet.

Consider what happends if we create a bookmark immediately adjacent to
our dir entry:

  0.0 bookmark parent  ->  0.0 bookmark parent  ->  0.0 bookmark parent
  (tracking 0.1)           0.1 bookmark child       0.1 bookmark child
                           (tracking 0.2)           0.2 dir child

That's not right.

To fix this we could add some special handling to our opened-mdir
subsystem to track opened-mdirs specially if they don't actually exist.

Or, as it turns out, just create the dir/bookmark entries in the
opposite order. Which happens to avoid this problem completely:

  0.0 bookmark parent  ->  0.0 bookmark parent  ->  0.0 bookmark parent
  (tracking 0.1)           0.1 dir child            0.1 dir child
                           (tracking 0.2)           0.2 bookmark child

The reason this works is that, thanks to our bookmarks, we can never
actually have a bookmark immediately preceding our dir entry. The
imaginary dir entry will always be preceded by either our parent or
other entry thanks to ordering by did first:

  (tracking 0.0)       ->  (tracking 0.0)       ->  0.0 bookmark child
  0.0 bookmark parent      0.0 bookmark parent      0.1 bookmark parent
  (tracking 0.1)           0.1 dir child            0.2 dir child

Previously we relied on rid=-1 to handle this as a special case, but
this workaround would stop working with compressed mids, where rid=-1
becomes unrepresentable.
2023-08-31 13:17:16 -05:00
Christopher Haster 302bfa17de Tweaked grms to use bid=-1 as indicator of unused grm slots
This is just a proof-of-concept for compressed mids. We can't rely on
rid=-1 as a special indicator as before.
2023-08-31 13:16:06 -05:00
Christopher Haster 9f18b1fd50 Tweaked mid to use sign-bit to indicate mroots
This is an intermediate commit as a part of a tangent into compressed
mids.

The idea here, is instead of using bid=-1 as a special value for mroots,
use only the top bit to indicate mroots. This allows you to compare
against the grm/other uninlined mids by masking instead of signed
comparison.

This is valuable for compressed mids since extracting bids relies on
knowledge of the block size, and becomes quite a bit more expensive.

            mroot bid                             mroot cmp
  before:  0xffffffff  lfs_smax32(a, 0) == lfs_smax32(b, 0)
  after:   0x80000000  (a & 0x7fffffff) == (b & 0x7fffffff)

The implementation here is a bit clumsy. I think GCC may be not that
great at optimizing out copies of structs being passed around via
inlined functions. But this is only a proof-of-concept.
2023-08-31 13:15:58 -05:00
Christopher Haster ff87aa41f2 Reverted 16-bit mbid/mrid mids to full 32-bits
The possibility of 16-bit mbid/mrids being a problematic limit is too
high for me to be able to confidently move forward with this internal
encoding.

Consider a filesystem with small blocks and/or large amounts of metadata
per file. If a single file almost fills up an mdir, it risks an
effective limit on the filesystem of 2^16 files. Not a deal-breaker, but
certainly a surprising limit on a supposed "32-bit" filesystem.

We can add more granular integer-limit configurations, but with simple
configurations, the option to increase the integer-limit filesystem-wide
to 64-bits would cost more RAM than just increasing the mbid/mrids
limits.

Though this is always up for reconsideration in the future.

            code          stack
  before:  20762           1720
  after:   20590 (-0.8%)   1784 (+3.7%)

It's interesting to not the code/RAM tradeoff here. RAM sees a
significant hit, but code improves, likely because of better instruction
sequences for 32-bit operations (this is targeting ARM thumb,
32-bit MCUs).

Though the code savings likely varies widely across instruction sets,
and I would guess swings negative on 16/8-bit MCUs.
2023-08-25 00:02:26 -05:00
Christopher Haster 256430213d Dropped separate BTREE/BRANCH encodings
There is a bit of redundancy here, as we already know the weights of
btree's inner-branches from their parents. But in theory sharing the
same encoding for both the top level btree reference and inner-branches
should offer more chance for deduplication and hopefully less code.

This also moves some members around in the btree encoding so that the
redund blocks are at the beginning. This _might_ simplify decoding of
the variable-length redund blocks at some point.

Current btree encoding:

  .----+----+----+----.
  |       blocks    ...  redund leb128s (1-20 bytes)
  :                   :
  |----+----+----+----|
  |       trunk     ...  1 leb128 (1-5 bytes)
  |----+----+----+----|
  |       weight    ...  1 leb128 (1-5 bytes)
  |----+----+----+----|
  |       cksum       |  1 le32 (4 bytes)
  '----+----+----+----'

This also partially reverts some tag name changes:

- BNAME -> BRANCH
- DMARK -> BOOKMARK
2023-08-22 13:20:37 -05:00
Christopher Haster f70e7fe709 Unrolled the sibling estimate loop in btree merge
This simplifies control flow at a code cost. Unrolling for now as it
avoids all of the iteration derived special handling (which obscures the
underlying logic) and it may be possible to recoup the code cost through
more shared branch utility functions.

            code          stack
  before:  20650           1712
  after:   20742 (+0.4%)   1712 (+0.0%)

May revert in the future.
2023-08-22 12:45:09 -05:00
Christopher Haster 20c036038a Some small tweaks to tests
- Adopted -1 as a cheap way to mark rbyds as unerased.

- Replaced literal references to alphas (alphas[0 % 26]) with their
  actual characters.
2023-08-22 00:15:23 -05:00
Christopher Haster 0d78fcefd6 Added assertions on inline size to btree inline code
This was missed when moving to use lfsr_data_t more. This is interesting
conflict between implicit truncation in lfsr_data_read and strict
assertions that inlined btree don't lose data.

Found in the btree tests when, you guessed it, inlined btrees lost data.

An alternative route would be to make btrees uninline when faced with an
entry too big to inlined. This may be valuable future work, but probably
depends on the file implementation to know if such a feature is useful.
2023-08-20 01:41:35 -05:00
Christopher Haster 105a0a12ce Added rbyd < 1/4 block_size condition to btree merges
This just avoids the overhead of estimating our sibling's sizes, which
we don't really want to do every compact.
2023-08-20 01:18:05 -05:00