9d322741ca
TLDR: This drops the idea of different bmap strategies/modes, and sorts
out most of the compile-time/runtime conditional bmap interactions.
---
Motivation: Benchmarking (at least up to the 32-bit word limit) has
shown the bmap will unlikely be a significant bottleneck, even on large
disks. The largest disks tend to be NAND, and NAND's ridiculous block
size limits pressure on block allocation.
There are still concerns for areas I haven't measured yet:
- SD/eMMC/FTL - Small blocks, so more pressure on block allocation. In
theory the logical block size can be artificially increased, but this
comes with a granularity tradeoff.
- I've only measured throughput, latency is a whole other story.
However, users have reported lfs3_fs_gc is useful for mitigating this,
so maybe latency is less of a concern now?
But while there may still be room for improvement via alternative bmap
strategies, the risk a concerning amount of complexity. Yes,
configuration gets more complicated, but the real issue is any bmap
strategies that try to track _deallocations_ (the original idea being
treediffing) risk falling leaking blocks if all cases aren't covered.
The current "bmap cache" strategy strikes a really nice balance where it
reduces _amortized_ block allocation -> ~O(log n) without RAM, while
retaining the safe, bug-resistant, single-source-of-truth properties
that come with lookahead-based allocation.
---
So, long story short, dropping other strategies, and now the presence of
the bmap is a boolean flag.
This is also the first format-specific flag:
- Define LFS3_BMAP to enable the bmap logic, but note by default the
bmap will still not be used.
- Define LFS3_YES_BMAP to force the bmap to be used.
- With LFS3_BMAP, passing LFS3_F_GBMAP to lfs3_format will include the
on-disk block-map.
- No flag is needed during mount, the presence of the bmap is determined
by the on-disk wcompat flags (LFS3_WCOMPAT_GBMAP). This also prevents
rw mounting if the bmap is not supported, but rdonly mounting is
allowed.
- Users can check if the bmap is in use via lfs3_fs_stat, which reports
LFS3_I_GBMAP in the flags field.
There's still some missing pieces, but these will be a bit more
involved:
- lfs3_fs_grow needs to be made bmap aware!
- We probably want something like lfs3_fs_mkgbmap and lfs3_fs_rmgbmap to
allow converting between bmap backed/not-backed filesystem images.
Code changes minimal:
code stack ctx
before: 37172 2352 684
after: 37172 (+0.0%) 2352 (+0.0%) 684 (+0.0%)
code stack ctx
bmap before: 38844 2456 800
bmap after: 38852 (+0.0%) 2456 (+0.0%) 800 (+0.0%)
387 lines
12 KiB
TOML
387 lines
12 KiB
TOML
# Test some low-level block-map operations
|
|
#
|
|
# Note these is very much not-exhaustive, but the entire test suite can
|
|
# be run with the gbmap if you define LFS3_YES_BMAP:
|
|
#
|
|
# LFS3_YES_BMAP=1 make test -j
|
|
#
|
|
after = ['test_btree', 'test_mtree']
|
|
ifdef = 'LFS3_BMAP'
|
|
|
|
|
|
# test bmap operations
|
|
|
|
# test simple set operations
|
|
[cases.test_bmap_set_split]
|
|
in = 'lfs3.c'
|
|
code = '''
|
|
lfs3_t lfs3;
|
|
lfs3_format(&lfs3, LFS3_F_RDWR, CFG) => 0;
|
|
lfs3_mount(&lfs3, LFS3_M_RDWR, CFG) => 0;
|
|
lfs3_alloc_ckpoint(&lfs3);
|
|
|
|
// create an initial bmap
|
|
lfs3_btree_t bmap;
|
|
lfs3_btree_init(&bmap);
|
|
lfs3_bmap_commit(&lfs3, &bmap, 0, LFS3_RATTRS(
|
|
LFS3_RATTR(LFS3_TAG_BMFREE, +BLOCK_COUNT))) => 0;
|
|
|
|
// set some blocks as in-use, avoid any weird merges for now
|
|
lfs3_bmap_set(&lfs3, &bmap, 6, LFS3_TAG_BMINUSE) => 0;
|
|
lfs3_bmap_set(&lfs3, &bmap, 8, LFS3_TAG_BMINUSE) => 0;
|
|
lfs3_bmap_set(&lfs3, &bmap, 10, LFS3_TAG_BMINUSE) => 0;
|
|
printf("bmap: w%d 0x%x.%x\n",
|
|
bmap.r.weight,
|
|
bmap.r.blocks[0],
|
|
bmap.r.trunk);
|
|
|
|
// weight should stay the same
|
|
assert(bmap.r.weight == BLOCK_COUNT);
|
|
|
|
// check ranges
|
|
lfs3_bid_t bid_;
|
|
lfs3_bid_t weight_;
|
|
lfs3_bmap_lookupnext(&lfs3, &bmap, 0,
|
|
&bid_, &weight_) => LFS3_TAG_BMFREE;
|
|
assert(bid_ == 5);
|
|
assert(weight_ == 6);
|
|
lfs3_bmap_lookupnext(&lfs3, &bmap, 6,
|
|
&bid_, &weight_) => LFS3_TAG_BMINUSE;
|
|
assert(bid_ == 6);
|
|
assert(weight_ == 1);
|
|
lfs3_bmap_lookupnext(&lfs3, &bmap, 7,
|
|
&bid_, &weight_) => LFS3_TAG_BMFREE;
|
|
assert(bid_ == 7);
|
|
assert(weight_ == 1);
|
|
lfs3_bmap_lookupnext(&lfs3, &bmap, 8,
|
|
&bid_, &weight_) => LFS3_TAG_BMINUSE;
|
|
assert(bid_ == 8);
|
|
assert(weight_ == 1);
|
|
lfs3_bmap_lookupnext(&lfs3, &bmap, 9,
|
|
&bid_, &weight_) => LFS3_TAG_BMFREE;
|
|
assert(bid_ == 9);
|
|
assert(weight_ == 1);
|
|
lfs3_bmap_lookupnext(&lfs3, &bmap, 10,
|
|
&bid_, &weight_) => LFS3_TAG_BMINUSE;
|
|
assert(bid_ == 10);
|
|
assert(weight_ == 1);
|
|
lfs3_bmap_lookupnext(&lfs3, &bmap, 11,
|
|
&bid_, &weight_) => LFS3_TAG_BMFREE;
|
|
assert(bid_ == BLOCK_COUNT-1);
|
|
assert(weight_ == BLOCK_COUNT-11);
|
|
|
|
lfs3_unmount(&lfs3) => 0;
|
|
'''
|
|
|
|
[cases.test_bmap_set_replace]
|
|
in = 'lfs3.c'
|
|
code = '''
|
|
lfs3_t lfs3;
|
|
lfs3_format(&lfs3, LFS3_F_RDWR, CFG) => 0;
|
|
lfs3_mount(&lfs3, LFS3_M_RDWR, CFG) => 0;
|
|
lfs3_alloc_ckpoint(&lfs3);
|
|
|
|
// create an initial bmap
|
|
lfs3_btree_t bmap;
|
|
lfs3_btree_init(&bmap);
|
|
lfs3_bmap_commit(&lfs3, &bmap, 0, LFS3_RATTRS(
|
|
LFS3_RATTR(LFS3_TAG_BMFREE, +BLOCK_COUNT))) => 0;
|
|
|
|
// set some blocks as in-use, avoid any weird merges for now
|
|
lfs3_bmap_set(&lfs3, &bmap, 6, LFS3_TAG_BMINUSE) => 0;
|
|
lfs3_bmap_set(&lfs3, &bmap, 8, LFS3_TAG_BMINUSE) => 0;
|
|
lfs3_bmap_set(&lfs3, &bmap, 10, LFS3_TAG_BMINUSE) => 0;
|
|
// replace those blocks as bad, this tests deleting ranges
|
|
lfs3_bmap_set(&lfs3, &bmap, 6, LFS3_TAG_BMBAD) => 0;
|
|
lfs3_bmap_set(&lfs3, &bmap, 8, LFS3_TAG_BMBAD) => 0;
|
|
lfs3_bmap_set(&lfs3, &bmap, 10, LFS3_TAG_BMBAD) => 0;
|
|
printf("bmap: w%d 0x%x.%x\n",
|
|
bmap.r.weight,
|
|
bmap.r.blocks[0],
|
|
bmap.r.trunk);
|
|
|
|
// weight should stay the same
|
|
assert(bmap.r.weight == BLOCK_COUNT);
|
|
|
|
// check ranges
|
|
lfs3_bid_t bid_;
|
|
lfs3_bid_t weight_;
|
|
lfs3_bmap_lookupnext(&lfs3, &bmap, 0,
|
|
&bid_, &weight_) => LFS3_TAG_BMFREE;
|
|
assert(bid_ == 5);
|
|
assert(weight_ == 6);
|
|
lfs3_bmap_lookupnext(&lfs3, &bmap, 6,
|
|
&bid_, &weight_) => LFS3_TAG_BMBAD;
|
|
assert(bid_ == 6);
|
|
assert(weight_ == 1);
|
|
lfs3_bmap_lookupnext(&lfs3, &bmap, 7,
|
|
&bid_, &weight_) => LFS3_TAG_BMFREE;
|
|
assert(bid_ == 7);
|
|
assert(weight_ == 1);
|
|
lfs3_bmap_lookupnext(&lfs3, &bmap, 8,
|
|
&bid_, &weight_) => LFS3_TAG_BMBAD;
|
|
assert(bid_ == 8);
|
|
assert(weight_ == 1);
|
|
lfs3_bmap_lookupnext(&lfs3, &bmap, 9,
|
|
&bid_, &weight_) => LFS3_TAG_BMFREE;
|
|
assert(bid_ == 9);
|
|
assert(weight_ == 1);
|
|
lfs3_bmap_lookupnext(&lfs3, &bmap, 10,
|
|
&bid_, &weight_) => LFS3_TAG_BMBAD;
|
|
assert(bid_ == 10);
|
|
assert(weight_ == 1);
|
|
lfs3_bmap_lookupnext(&lfs3, &bmap, 11,
|
|
&bid_, &weight_) => LFS3_TAG_BMFREE;
|
|
assert(bid_ == BLOCK_COUNT-1);
|
|
assert(weight_ == BLOCK_COUNT-11);
|
|
|
|
lfs3_unmount(&lfs3) => 0;
|
|
'''
|
|
|
|
[cases.test_bmap_set_merge]
|
|
in = 'lfs3.c'
|
|
code = '''
|
|
lfs3_t lfs3;
|
|
lfs3_format(&lfs3, LFS3_F_RDWR, CFG) => 0;
|
|
lfs3_mount(&lfs3, LFS3_M_RDWR, CFG) => 0;
|
|
lfs3_alloc_ckpoint(&lfs3);
|
|
|
|
// create an initial bmap
|
|
lfs3_btree_t bmap;
|
|
lfs3_btree_init(&bmap);
|
|
lfs3_bmap_commit(&lfs3, &bmap, 0, LFS3_RATTRS(
|
|
LFS3_RATTR(LFS3_TAG_BMFREE, +BLOCK_COUNT))) => 0;
|
|
|
|
// set some blocks as in-use, avoid any weird merges for now
|
|
lfs3_bmap_set(&lfs3, &bmap, 6, LFS3_TAG_BMINUSE) => 0;
|
|
lfs3_bmap_set(&lfs3, &bmap, 8, LFS3_TAG_BMINUSE) => 0;
|
|
lfs3_bmap_set(&lfs3, &bmap, 10, LFS3_TAG_BMINUSE) => 0;
|
|
lfs3_bmap_set(&lfs3, &bmap, 12, LFS3_TAG_BMINUSE) => 0;
|
|
// set neighboring blocks as in-use, triggering merges
|
|
lfs3_bmap_set(&lfs3, &bmap, 5, LFS3_TAG_BMINUSE) => 0;
|
|
lfs3_bmap_set(&lfs3, &bmap, 9, LFS3_TAG_BMINUSE) => 0;
|
|
lfs3_bmap_set(&lfs3, &bmap, 13, LFS3_TAG_BMINUSE) => 0;
|
|
printf("bmap: w%d 0x%x.%x\n",
|
|
bmap.r.weight,
|
|
bmap.r.blocks[0],
|
|
bmap.r.trunk);
|
|
|
|
// weight should stay the same
|
|
assert(bmap.r.weight == BLOCK_COUNT);
|
|
|
|
// check ranges
|
|
lfs3_bid_t bid_;
|
|
lfs3_bid_t weight_;
|
|
lfs3_bmap_lookupnext(&lfs3, &bmap, 0,
|
|
&bid_, &weight_) => LFS3_TAG_BMFREE;
|
|
assert(bid_ == 4);
|
|
assert(weight_ == 5);
|
|
lfs3_bmap_lookupnext(&lfs3, &bmap, 5,
|
|
&bid_, &weight_) => LFS3_TAG_BMINUSE;
|
|
assert(bid_ == 6);
|
|
assert(weight_ == 2);
|
|
lfs3_bmap_lookupnext(&lfs3, &bmap, 7,
|
|
&bid_, &weight_) => LFS3_TAG_BMFREE;
|
|
assert(bid_ == 7);
|
|
assert(weight_ == 1);
|
|
lfs3_bmap_lookupnext(&lfs3, &bmap, 8,
|
|
&bid_, &weight_) => LFS3_TAG_BMINUSE;
|
|
assert(bid_ == 10);
|
|
assert(weight_ == 3);
|
|
lfs3_bmap_lookupnext(&lfs3, &bmap, 11,
|
|
&bid_, &weight_) => LFS3_TAG_BMFREE;
|
|
assert(bid_ == 11);
|
|
assert(weight_ == 1);
|
|
lfs3_bmap_lookupnext(&lfs3, &bmap, 12,
|
|
&bid_, &weight_) => LFS3_TAG_BMINUSE;
|
|
assert(bid_ == 13);
|
|
assert(weight_ == 2);
|
|
lfs3_bmap_lookupnext(&lfs3, &bmap, 14,
|
|
&bid_, &weight_) => LFS3_TAG_BMFREE;
|
|
assert(bid_ == BLOCK_COUNT-1);
|
|
assert(weight_ == BLOCK_COUNT-14);
|
|
|
|
lfs3_unmount(&lfs3) => 0;
|
|
'''
|
|
|
|
[cases.test_bmap_set_noop]
|
|
in = 'lfs3.c'
|
|
code = '''
|
|
lfs3_t lfs3;
|
|
lfs3_format(&lfs3, LFS3_F_RDWR, CFG) => 0;
|
|
lfs3_mount(&lfs3, LFS3_M_RDWR, CFG) => 0;
|
|
lfs3_alloc_ckpoint(&lfs3);
|
|
|
|
// create an initial bmap
|
|
lfs3_btree_t bmap;
|
|
lfs3_btree_init(&bmap);
|
|
lfs3_bmap_commit(&lfs3, &bmap, 0, LFS3_RATTRS(
|
|
LFS3_RATTR(LFS3_TAG_BMFREE, +BLOCK_COUNT))) => 0;
|
|
|
|
// set some blocks as in-use, avoid any weird merges for now
|
|
lfs3_bmap_set(&lfs3, &bmap, 6, LFS3_TAG_BMINUSE) => 0;
|
|
lfs3_bmap_set(&lfs3, &bmap, 8, LFS3_TAG_BMINUSE) => 0;
|
|
lfs3_bmap_set(&lfs3, &bmap, 10, LFS3_TAG_BMINUSE) => 0;
|
|
// test a bunch of noops
|
|
lfs3_bmap_set(&lfs3, &bmap, 6, LFS3_TAG_BMINUSE) => 0;
|
|
lfs3_bmap_set(&lfs3, &bmap, 7, LFS3_TAG_BMFREE) => 0;
|
|
lfs3_bmap_set(&lfs3, &bmap, 8, LFS3_TAG_BMINUSE) => 0;
|
|
lfs3_bmap_set(&lfs3, &bmap, 9, LFS3_TAG_BMFREE) => 0;
|
|
lfs3_bmap_set(&lfs3, &bmap, 10, LFS3_TAG_BMINUSE) => 0;
|
|
printf("bmap: w%d 0x%x.%x\n",
|
|
bmap.r.weight,
|
|
bmap.r.blocks[0],
|
|
bmap.r.trunk);
|
|
|
|
// weight should stay the same
|
|
assert(bmap.r.weight == BLOCK_COUNT);
|
|
|
|
// check ranges
|
|
lfs3_bid_t bid_;
|
|
lfs3_bid_t weight_;
|
|
lfs3_bmap_lookupnext(&lfs3, &bmap, 0,
|
|
&bid_, &weight_) => LFS3_TAG_BMFREE;
|
|
assert(bid_ == 5);
|
|
assert(weight_ == 6);
|
|
lfs3_bmap_lookupnext(&lfs3, &bmap, 6,
|
|
&bid_, &weight_) => LFS3_TAG_BMINUSE;
|
|
assert(bid_ == 6);
|
|
assert(weight_ == 1);
|
|
lfs3_bmap_lookupnext(&lfs3, &bmap, 7,
|
|
&bid_, &weight_) => LFS3_TAG_BMFREE;
|
|
assert(bid_ == 7);
|
|
assert(weight_ == 1);
|
|
lfs3_bmap_lookupnext(&lfs3, &bmap, 8,
|
|
&bid_, &weight_) => LFS3_TAG_BMINUSE;
|
|
assert(bid_ == 8);
|
|
assert(weight_ == 1);
|
|
lfs3_bmap_lookupnext(&lfs3, &bmap, 9,
|
|
&bid_, &weight_) => LFS3_TAG_BMFREE;
|
|
assert(bid_ == 9);
|
|
assert(weight_ == 1);
|
|
lfs3_bmap_lookupnext(&lfs3, &bmap, 10,
|
|
&bid_, &weight_) => LFS3_TAG_BMINUSE;
|
|
assert(bid_ == 10);
|
|
assert(weight_ == 1);
|
|
lfs3_bmap_lookupnext(&lfs3, &bmap, 11,
|
|
&bid_, &weight_) => LFS3_TAG_BMFREE;
|
|
assert(bid_ == BLOCK_COUNT-1);
|
|
assert(weight_ == BLOCK_COUNT-11);
|
|
|
|
lfs3_unmount(&lfs3) => 0;
|
|
'''
|
|
|
|
[cases.test_bmap_set_bounds]
|
|
in = 'lfs3.c'
|
|
code = '''
|
|
lfs3_t lfs3;
|
|
lfs3_format(&lfs3, LFS3_F_RDWR, CFG) => 0;
|
|
lfs3_mount(&lfs3, LFS3_M_RDWR, CFG) => 0;
|
|
lfs3_alloc_ckpoint(&lfs3);
|
|
|
|
// create an initial bmap
|
|
lfs3_btree_t bmap;
|
|
lfs3_btree_init(&bmap);
|
|
lfs3_bmap_commit(&lfs3, &bmap, 0, LFS3_RATTRS(
|
|
LFS3_RATTR(LFS3_TAG_BMFREE, +BLOCK_COUNT))) => 0;
|
|
|
|
// test that setting the first and last blocks don't break anything
|
|
//
|
|
// though in theory blocks 0x{0,1} are immutable...
|
|
lfs3_bmap_set(&lfs3, &bmap, 0, LFS3_TAG_BMINUSE) => 0;
|
|
lfs3_bmap_set(&lfs3, &bmap, BLOCK_COUNT-1, LFS3_TAG_BMINUSE) => 0;
|
|
printf("bmap: w%d 0x%x.%x\n",
|
|
bmap.r.weight,
|
|
bmap.r.blocks[0],
|
|
bmap.r.trunk);
|
|
|
|
// weight should stay the same
|
|
assert(bmap.r.weight == BLOCK_COUNT);
|
|
|
|
// check ranges
|
|
lfs3_bid_t bid_;
|
|
lfs3_bid_t weight_;
|
|
lfs3_bmap_lookupnext(&lfs3, &bmap, 0,
|
|
&bid_, &weight_) => LFS3_TAG_BMINUSE;
|
|
assert(bid_ == 0);
|
|
assert(weight_ == 1);
|
|
lfs3_bmap_lookupnext(&lfs3, &bmap, 1,
|
|
&bid_, &weight_) => LFS3_TAG_BMFREE;
|
|
assert(bid_ == BLOCK_COUNT-2);
|
|
assert(weight_ == BLOCK_COUNT-2);
|
|
lfs3_bmap_lookupnext(&lfs3, &bmap, BLOCK_COUNT-1,
|
|
&bid_, &weight_) => LFS3_TAG_BMINUSE;
|
|
assert(bid_ == BLOCK_COUNT-1);
|
|
assert(weight_ == 1);
|
|
|
|
lfs3_unmount(&lfs3) => 0;
|
|
'''
|
|
|
|
[cases.test_bmap_set_fuzz]
|
|
defines.N = [1, 2, 4, 8, 16, 32, 64, 128, 256, 512]
|
|
defines.TYPES = [2, 3, 4]
|
|
defines.SEED = 'range(20)'
|
|
fuzz = 'SEED'
|
|
in = 'lfs3.c'
|
|
code = '''
|
|
lfs3_t lfs3;
|
|
lfs3_format(&lfs3, LFS3_F_RDWR, CFG) => 0;
|
|
lfs3_mount(&lfs3, LFS3_M_RDWR, CFG) => 0;
|
|
lfs3_alloc_ckpoint(&lfs3);
|
|
|
|
// create an initial bmap
|
|
lfs3_btree_t bmap;
|
|
lfs3_btree_init(&bmap);
|
|
lfs3_bmap_commit(&lfs3, &bmap, 0, LFS3_RATTRS(
|
|
LFS3_RATTR(LFS3_TAG_BMFREE, +BLOCK_COUNT))) => 0;
|
|
|
|
// create a simulation to compare against
|
|
lfs3_tag_t *sim = malloc(BLOCK_COUNT*sizeof(lfs3_tag_t));
|
|
for (lfs3_size_t i = 0; i < BLOCK_COUNT; i++) {
|
|
sim[i] = LFS3_TAG_BMFREE;
|
|
}
|
|
|
|
uint32_t prng = SEED;
|
|
for (lfs3_size_t i = 0; i < N; i++) {
|
|
// choose a pseudo-random block
|
|
lfs3_block_t block = TEST_PRNG(&prng) % BLOCK_COUNT;
|
|
// and pseudo-random bmap type
|
|
lfs3_tag_t tag = LFS3_TAG_BMRANGE + (TEST_PRNG(&prng) % TYPES);
|
|
|
|
// set in bmap
|
|
lfs3_bmap_set(&lfs3, &bmap, block, tag) => 0;
|
|
|
|
// and set in sim
|
|
sim[block] = tag;
|
|
}
|
|
printf("bmap: w%d 0x%x.%x\n",
|
|
bmap.r.weight,
|
|
bmap.r.blocks[0],
|
|
bmap.r.trunk);
|
|
|
|
// check if bmap matches sim
|
|
lfs3_size_t i = 0;
|
|
while (i < BLOCK_COUNT) {
|
|
// we need to convert our sim's raw blocks to compressed ranges,
|
|
// in theory our bmap is optimal
|
|
lfs3_tag_t tag = sim[i];
|
|
lfs3_size_t d = 1;
|
|
while (i+d < BLOCK_COUNT && sim[i+d] == tag) {
|
|
d += 1;
|
|
}
|
|
|
|
// does our bmap contain the optimal range?
|
|
lfs3_bid_t bid_;
|
|
lfs3_bid_t weight_;
|
|
lfs3_bmap_lookupnext(&lfs3, &bmap, i,
|
|
&bid_, &weight_) => tag;
|
|
assert(bid_ == i+(weight_-1));
|
|
assert(weight_ == d);
|
|
|
|
i += d;
|
|
}
|
|
|
|
free(sim);
|
|
lfs3_unmount(&lfs3) => 0;
|
|
'''
|