From 83668e9782edcea64ee32ae5a2b7df45dbe82929 Mon Sep 17 00:00:00 2001 From: Christopher Haster Date: Sun, 20 Apr 2025 02:41:26 -0500 Subject: [PATCH] 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%) --- lfs.c | 59 ++++++++++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 46 insertions(+), 13 deletions(-) diff --git a/lfs.c b/lfs.c index 362cbfe9..a7b44623 100644 --- a/lfs.c +++ b/lfs.c @@ -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; } }