Commit Graph

6 Commits

Author SHA1 Message Date
Christopher Haster 582dc5f1b2 Added some tests, quick seek impl, fixed bugs
Turns out it's hard to test file holes without seek.

It's interesting to note most of seek's buffer flush work actually
occurs lazily in lfsr_file_write, so lfsr_file_seek turns out to be a
relatively simple function.
2023-10-14 00:09:27 -05:00
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 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 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