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%)
This commit is contained in:
Christopher Haster
2025-04-20 02:41:26 -05:00
parent 31e34f54f3
commit 83668e9782
+46 -13
View File
@@ -11650,11 +11650,27 @@ static int lfsr_file_carve(lfs_t *lfs, lfsr_file_t *file,
&l.data);
// carve bptr?
} else {
//
// make sure we're not creating a dag! we can't allow this
// to happen or it would break our littlefs-is-a-tree
// invariant
} else if (!(pos+weight < bid+1
&& lfsr_data_size(r.data) > lfs->cfg->fragment_size)) {
rattrs[rattr_count++] = LFSR_RATTR_BPTR(
LFSR_TAG_GROW | LFSR_TAG_MASK8 | LFSR_TAG_BLOCK,
-(bid+1 - pos),
&l);
// uh oh, keeping both siblings would create a dag? we have
// no choice but to rewrite one into a new block
//
// our crystallization algorithm currently prevents this from
// happening, but it may be a problem in the future (more
// advanced hole APIs, alternative write strategies, etc)
} else {
// this is where we would split dags if an algorithm
// needed it
LFS_UNREACHABLE();
}
// completely overwriting this entry?
@@ -11757,6 +11773,8 @@ static int lfsr_file_flush_(lfs_t *lfs, lfsr_file_t *file,
bool aligned = false;
// iteratively write blocks
lfs_off_t crystal_start;
lfs_off_t crystal_end;
while (size > 0) {
// first we need to figure out our current crystal, we do this
// heuristically.
@@ -11765,8 +11783,8 @@ static int lfsr_file_flush_(lfs_t *lfs, lfsr_file_t *file,
// is fine. we don't want small holes breaking up blocks anyways
// default to arbitrary alignment
lfs_off_t crystal_start = pos;
lfs_off_t crystal_end = pos + size;
crystal_start = pos;
crystal_end = pos + size;
lfs_off_t block_start;
lfs_off_t block_end;
lfs_sblock_t block;
@@ -11813,7 +11831,7 @@ static int lfsr_file_flush_(lfs_t *lfs, lfsr_file_t *file,
&& bptr.data.u.disk.off + lfsr_data_size(bptr.data)
== file->eoff
// not clobbering data?
&& crystal_start - (bid-(weight-1))
&& pos - (bid-(weight-1))
>= lfsr_data_size(bptr.data)
// enough for prog alignment?
&& crystal_end - crystal_start
@@ -11829,7 +11847,7 @@ static int lfsr_file_flush_(lfs_t *lfs, lfsr_file_t *file,
off = bptr.data.u.disk.off;
eoff = lfsr_bptr_cksize(&bptr);
cksum = lfsr_bptr_cksum(&bptr);
goto crystallize;
goto compact;
}
}
}
@@ -11856,13 +11874,13 @@ static int lfsr_file_flush_(lfs_t *lfs, lfsr_file_t *file,
if (!lfsr_bptr_isbptr(&bptr)) {
crystal_end = lfs_max(
bid-(weight-1)+lfsr_data_size(bptr.data),
pos + size);
crystal_end);
// otherwise treat as crystal boundary
} else {
crystal_end = lfs_max(
bid-(weight-1),
pos + size);
crystal_end);
}
}
@@ -11879,7 +11897,6 @@ static int lfsr_file_flush_(lfs_t *lfs, lfsr_file_t *file,
// before we can crystallize we need to figure out the best
// block alignment, we use the entry immediately to the left of
// our crystal for this
block_start = crystal_start;
if (crystal_start > 0
&& file->b.shrub.weight > 0
// don't bother to lookup left after the first block
@@ -11901,7 +11918,7 @@ static int lfsr_file_flush_(lfs_t *lfs, lfsr_file_t *file,
if (crystal_start - (bid-(weight-1))
< lfs->cfg->block_size
&& lfsr_data_size(bptr.data) > 0) {
block_start = bid-(weight-1);
crystal_start = bid-(weight-1);
// wait, found erased-state?
if (lfsr_bptr_isbptr(&bptr)
@@ -11909,19 +11926,20 @@ static int lfsr_file_flush_(lfs_t *lfs, lfsr_file_t *file,
&& bptr.data.u.disk.off + lfsr_data_size(bptr.data)
== file->eoff
// not clobbering data?
&& crystal_start - (bid-(weight-1))
&& pos - (bid-(weight-1))
>= lfsr_data_size(bptr.data)) {
// mark as unerased in case of failure
file->eblock = 0;
file->eoff = -1;
// try to use erased-state
block_start = bid-(weight-1);
block_end = block_start + lfsr_data_size(bptr.data);
block = bptr.data.u.disk.block;
off = bptr.data.u.disk.off;
eoff = lfsr_bptr_cksize(&bptr);
cksum = lfsr_bptr_cksum(&bptr);
goto crystallize;
goto compact;
}
// no? is our left neighbor at least our left block neighbor?
@@ -11929,10 +11947,13 @@ static int lfsr_file_flush_(lfs_t *lfs, lfsr_file_t *file,
} else if (crystal_start - (bid-(weight-1))
< 2*lfs->cfg->block_size
&& lfsr_data_size(bptr.data) > 0) {
block_start = bid-(weight-1) + lfs->cfg->block_size;
crystal_start = bid-(weight-1) + lfs->cfg->block_size;
}
}
crystallize:;
block_start = crystal_start;
relocate:;
// allocate a new block
//
@@ -11948,7 +11969,7 @@ static int lfsr_file_flush_(lfs_t *lfs, lfsr_file_t *file,
eoff = 0;
cksum = 0;
crystallize:;
compact:;
// crystallize data into our block
//
// eagerly merge any right neighbors we see unless that would
@@ -12192,6 +12213,18 @@ fragment:;
fragment_end = fragment_start + lfs_min(
fragment_end - (bid-(weight-1)),
lfs->cfg->fragment_size);
// uh oh, would we end up creating a dag?
//
// we would be forced to split the dag in lfsr_file_carve in
// order to keep the littlefs-is-a-tree invariant, so we might
// as well try to recrystallize the left sibling
} else if (lfsr_bptr_isbptr(&bptr)
&& fragment_end
< bid-(weight-1) + lfsr_data_size(bptr.data)) {
crystal_start = bid-(weight-1);
crystal_end = fragment_end;
goto crystallize;
}
}