t: Implemented gc_compact_thresh over bshrub nodes

These aren't really different than btree nodes, except bshrubs need to
be enrolled in our opened list for commits to work.

Fortunately this is already true for explicit traversals, which are
currently the only traversals where we need to simultaneously mutate the
filesystem. This mainly just required adding additional checks for
LFS_TYPE_TRAVERSAL bshrubs, tests, and making sure traversal.bshrub is
never in an invalid state.

This continues to add code/stack cost for what is ultimately a
relatively niche feature:

           code          stack
  before: 35268           2776
  after:  35448 (+0.5%)   2800 (+0.9%)

Maybe btree/bshrub compactions should be disabled by default?
This commit is contained in:
Christopher Haster
2024-07-01 13:22:35 -05:00
parent 227b804c7a
commit f3446abfa7
3 changed files with 525 additions and 30 deletions
+486 -7
View File
@@ -6365,11 +6365,491 @@ code = '''
lfsr_unmount(&lfs) => 0;
'''
# TODO
# [cases.test_traversal_compact_bshrub]
# [cases.test_traversal_compact_bshrub_open]
# [cases.test_traversal_compact_bshrub_orphan]
# [cases.test_traversal_compact_bshrub_desync]
[cases.test_traversal_compact_bshrub]
defines.CKMETA = [false, true]
defines.CK = [false, true]
defines.LOOKAHEAD = [false, true]
# this configuration should create a 2-layer bshrub, which may be
# a bit delicate
defines.INLINE_SIZE = 'BLOCK_SIZE/4'
defines.CRYSTAL_THRESH = -1
defines.FRAGMENT_SIZE = 'BLOCK_SIZE/8'
defines.SIZE = 'BLOCK_SIZE'
# set compact thresh to minimum
defines.GC_COMPACT_THRESH = 'BLOCK_SIZE/2'
in = 'lfs.c'
code = '''
lfs_t lfs;
lfsr_format(&lfs, CFG) => 0;
lfsr_mount(&lfs, CFG) => 0;
uint32_t prng = 42;
// create a file
lfsr_file_t file;
lfsr_file_open(&lfs, &file, "jellyfish",
LFS_O_WRONLY | LFS_O_CREAT | LFS_O_EXCL) => 0;
uint8_t wbuf[SIZE];
for (lfs_size_t j = 0; j < SIZE; j++) {
wbuf[j] = 'a' + (TEST_PRNG(&prng) % 26);
}
lfsr_file_write(&lfs, &file, wbuf, SIZE) => SIZE;
// rewrite part of our file until bshrub is >gc_compact_thresh full
while (true) {
// we need internals to check this
lfsr_mdir_t mdir;
// ckmeta needed for eoff
lfsr_mtraversal_t mt = LFSR_MTRAVERSAL(LFS_T_CKMETA);
lfsr_mtinfo_t mtinfo;
lfsr_mtree_traverse(&lfs, &mdir, &mt, &mtinfo) => 0;
assert(mtinfo.tag == LFSR_TAG_MDIR);
lfsr_mtree_traverse(&lfs, &mdir, &mt, &mtinfo) => 0;
assert(mtinfo.tag == LFSR_TAG_BRANCH);
if (lfsr_rbyd_eoff(&mtinfo.u.rbyd) > GC_COMPACT_THRESH) {
break;
}
lfsr_file_rewind(&lfs, &file) => 0;
for (lfs_size_t j = 0; j < FRAGMENT_SIZE; j++) {
wbuf[j] = 'a' + (TEST_PRNG(&prng) % 26);
}
lfsr_file_write(&lfs, &file, wbuf, FRAGMENT_SIZE) => FRAGMENT_SIZE;
}
lfsr_file_close(&lfs, &file) => 0;
// try traversing and compacting
lfsr_traversal_t t;
lfsr_traversal_open(&lfs, &t,
LFS_T_COMPACT
| ((CKMETA) ? LFS_T_CKMETA : 0)
| ((CK) ? LFS_T_CK : 0)
| ((LOOKAHEAD) ? LFS_T_LOOKAHEAD : 0)) => 0;
// traverse mroot
struct lfs_tinfo tinfo;
lfsr_traversal_read(&lfs, &t, &tinfo) => 0;
assert(tinfo.btype == LFS_BTYPE_MDIR);
assert(tinfo.block == 0 || tinfo.block == 1);
lfsr_traversal_read(&lfs, &t, &tinfo) => 0;
assert(tinfo.btype == LFS_BTYPE_MDIR);
assert(tinfo.block == 0 || tinfo.block == 1);
// compacting our bshrub nodes may cause them to split, so we may
// need to traverse more nodes than we started with
for (lfs_size_t i = 0; i < 5; i++) {
// traverse bshrub
lfsr_traversal_read(&lfs, &t, &tinfo) => 0;
assert(tinfo.btype == LFS_BTYPE_BTREE);
}
lfsr_traversal_read(&lfs, &t, &tinfo) => LFS_ERR_NOENT;
lfsr_traversal_close(&lfs, &t) => 0;
// bshrub should have been compacted
lfsr_mdir_t mdir;
lfsr_mtraversal_t mt = LFSR_MTRAVERSAL(LFS_T_CKMETA);
lfsr_mtinfo_t mtinfo;
lfsr_mtree_traverse(&lfs, &mdir, &mt, &mtinfo) => 0;
assert(mtinfo.tag == LFSR_TAG_MDIR);
while (true) {
int err = lfsr_mtree_traverse(&lfs, &mdir, &mt, &mtinfo);
assert(!err || err == LFS_ERR_NOENT);
if (err == LFS_ERR_NOENT) {
break;
}
assert(mtinfo.tag == LFSR_TAG_BRANCH);
assert(lfsr_rbyd_eoff(&mtinfo.u.rbyd) <= GC_COMPACT_THRESH);
}
// check we can still read the file
for (int remount = 0; remount < 2; remount++) {
// remount?
if (remount) {
lfsr_unmount(&lfs) => 0;
lfsr_mount(&lfs, CFG) => 0;
}
lfsr_file_open(&lfs, &file, "jellyfish", LFS_O_RDONLY) => 0;
lfsr_file_rewind(&lfs, &file) => 0;
uint8_t rbuf[SIZE];
lfsr_file_read(&lfs, &file, rbuf, SIZE) => SIZE;
assert(memcmp(rbuf, wbuf, SIZE) == 0);
lfsr_file_close(&lfs, &file) => 0;
}
lfsr_unmount(&lfs) => 0;
'''
[cases.test_traversal_compact_bshrub_open]
defines.CKMETA = [false, true]
defines.CK = [false, true]
defines.LOOKAHEAD = [false, true]
# this configuration should create a 2-layer bshrub, which may be
# a bit delicate
defines.INLINE_SIZE = 'BLOCK_SIZE/4'
defines.CRYSTAL_THRESH = -1
defines.FRAGMENT_SIZE = 'BLOCK_SIZE/8'
defines.SIZE = 'BLOCK_SIZE'
# set compact thresh to minimum
defines.GC_COMPACT_THRESH = 'BLOCK_SIZE/2'
in = 'lfs.c'
code = '''
lfs_t lfs;
lfsr_format(&lfs, CFG) => 0;
lfsr_mount(&lfs, CFG) => 0;
uint32_t prng = 42;
// create a file
lfsr_file_t file;
lfsr_file_open(&lfs, &file, "jellyfish",
LFS_O_RDWR | LFS_O_CREAT | LFS_O_EXCL) => 0;
uint8_t wbuf[SIZE];
for (lfs_size_t j = 0; j < SIZE; j++) {
wbuf[j] = 'a' + (TEST_PRNG(&prng) % 26);
}
lfsr_file_write(&lfs, &file, wbuf, SIZE) => SIZE;
// rewrite part of our file until bshrub is >gc_compact_thresh full
while (true) {
// we need internals to check this
lfsr_mdir_t mdir;
// ckmeta needed for eoff
lfsr_mtraversal_t mt = LFSR_MTRAVERSAL(LFS_T_CKMETA);
lfsr_mtinfo_t mtinfo;
lfsr_mtree_traverse(&lfs, &mdir, &mt, &mtinfo) => 0;
assert(mtinfo.tag == LFSR_TAG_MDIR);
lfsr_mtree_traverse(&lfs, &mdir, &mt, &mtinfo) => 0;
assert(mtinfo.tag == LFSR_TAG_BRANCH);
if (lfsr_rbyd_eoff(&mtinfo.u.rbyd) > GC_COMPACT_THRESH) {
break;
}
lfsr_file_rewind(&lfs, &file) => 0;
for (lfs_size_t j = 0; j < FRAGMENT_SIZE; j++) {
wbuf[j] = 'a' + (TEST_PRNG(&prng) % 26);
}
lfsr_file_write(&lfs, &file, wbuf, FRAGMENT_SIZE) => FRAGMENT_SIZE;
}
lfsr_file_sync(&lfs, &file) => 0;
// try traversing and compacting
lfsr_traversal_t t;
lfsr_traversal_open(&lfs, &t,
LFS_T_COMPACT
| ((CKMETA) ? LFS_T_CKMETA : 0)
| ((CK) ? LFS_T_CK : 0)
| ((LOOKAHEAD) ? LFS_T_LOOKAHEAD : 0)) => 0;
// traverse mroot
struct lfs_tinfo tinfo;
lfsr_traversal_read(&lfs, &t, &tinfo) => 0;
assert(tinfo.btype == LFS_BTYPE_MDIR);
assert(tinfo.block == 0 || tinfo.block == 1);
lfsr_traversal_read(&lfs, &t, &tinfo) => 0;
assert(tinfo.btype == LFS_BTYPE_MDIR);
assert(tinfo.block == 0 || tinfo.block == 1);
// compacting our bshrub nodes may cause them to split, so we may
// need to traverse more nodes than we started with
for (lfs_size_t i = 0; i < 5; i++) {
// traverse bshrub
lfsr_traversal_read(&lfs, &t, &tinfo) => 0;
assert(tinfo.btype == LFS_BTYPE_BTREE);
}
lfsr_traversal_read(&lfs, &t, &tinfo) => LFS_ERR_NOENT;
lfsr_traversal_close(&lfs, &t) => 0;
// bshrub should have been compacted
lfsr_mdir_t mdir;
lfsr_mtraversal_t mt = LFSR_MTRAVERSAL(LFS_T_CKMETA);
lfsr_mtinfo_t mtinfo;
lfsr_mtree_traverse(&lfs, &mdir, &mt, &mtinfo) => 0;
assert(mtinfo.tag == LFSR_TAG_MDIR);
while (true) {
int err = lfsr_mtree_traverse(&lfs, &mdir, &mt, &mtinfo);
assert(!err || err == LFS_ERR_NOENT);
if (err == LFS_ERR_NOENT) {
break;
}
assert(mtinfo.tag == LFSR_TAG_BRANCH);
assert(lfsr_rbyd_eoff(&mtinfo.u.rbyd) <= GC_COMPACT_THRESH);
}
// check we can still read the file
for (int remount = 0; remount < 2; remount++) {
// remount?
if (remount) {
lfsr_file_close(&lfs, &file) => 0;
lfsr_unmount(&lfs) => 0;
lfsr_mount(&lfs, CFG) => 0;
lfsr_file_open(&lfs, &file, "jellyfish", LFS_O_RDONLY) => 0;
}
lfsr_file_rewind(&lfs, &file) => 0;
uint8_t rbuf[SIZE];
lfsr_file_read(&lfs, &file, rbuf, SIZE) => SIZE;
assert(memcmp(rbuf, wbuf, SIZE) == 0);
}
lfsr_file_close(&lfs, &file) => 0;
lfsr_unmount(&lfs) => 0;
'''
[cases.test_traversal_compact_bshrub_orphan]
defines.CKMETA = [false, true]
defines.CK = [false, true]
defines.LOOKAHEAD = [false, true]
# this configuration should create a 2-layer bshrub, which may be
# a bit delicate
defines.INLINE_SIZE = 'BLOCK_SIZE/4'
defines.CRYSTAL_THRESH = -1
defines.FRAGMENT_SIZE = 'BLOCK_SIZE/8'
defines.SIZE = 'BLOCK_SIZE'
# set compact thresh to minimum
defines.GC_COMPACT_THRESH = 'BLOCK_SIZE/2'
in = 'lfs.c'
code = '''
lfs_t lfs;
lfsr_format(&lfs, CFG) => 0;
lfsr_mount(&lfs, CFG) => 0;
uint32_t prng = 42;
// create a file
lfsr_file_t file;
lfsr_file_open(&lfs, &file, "jellyfish",
LFS_O_RDWR | LFS_O_CREAT | LFS_O_EXCL) => 0;
uint8_t wbuf[SIZE];
for (lfs_size_t j = 0; j < SIZE; j++) {
wbuf[j] = 'a' + (TEST_PRNG(&prng) % 26);
}
lfsr_file_write(&lfs, &file, wbuf, SIZE) => SIZE;
// rewrite part of our file until bshrub is >gc_compact_thresh full
while (true) {
// we need internals to check this
lfsr_mdir_t mdir;
// ckmeta needed for eoff
lfsr_mtraversal_t mt = LFSR_MTRAVERSAL(LFS_T_CKMETA);
lfsr_mtinfo_t mtinfo;
lfsr_mtree_traverse(&lfs, &mdir, &mt, &mtinfo) => 0;
assert(mtinfo.tag == LFSR_TAG_MDIR);
lfsr_mtree_traverse(&lfs, &mdir, &mt, &mtinfo) => 0;
assert(mtinfo.tag == LFSR_TAG_BRANCH);
if (lfsr_rbyd_eoff(&mtinfo.u.rbyd) > GC_COMPACT_THRESH) {
break;
}
lfsr_file_rewind(&lfs, &file) => 0;
for (lfs_size_t j = 0; j < FRAGMENT_SIZE; j++) {
wbuf[j] = 'a' + (TEST_PRNG(&prng) % 26);
}
lfsr_file_write(&lfs, &file, wbuf, FRAGMENT_SIZE) => FRAGMENT_SIZE;
}
// try traversing and compacting
lfsr_traversal_t t;
lfsr_traversal_open(&lfs, &t,
LFS_T_COMPACT
| ((CKMETA) ? LFS_T_CKMETA : 0)
| ((CK) ? LFS_T_CK : 0)
| ((LOOKAHEAD) ? LFS_T_LOOKAHEAD : 0)) => 0;
// traverse mroot
struct lfs_tinfo tinfo;
lfsr_traversal_read(&lfs, &t, &tinfo) => 0;
assert(tinfo.btype == LFS_BTYPE_MDIR);
assert(tinfo.block == 0 || tinfo.block == 1);
lfsr_traversal_read(&lfs, &t, &tinfo) => 0;
assert(tinfo.btype == LFS_BTYPE_MDIR);
assert(tinfo.block == 0 || tinfo.block == 1);
// compacting our bshrub nodes may cause them to split, so we may
// need to traverse more nodes than we started with
for (lfs_size_t i = 0; i < 5; i++) {
// traverse bshrub
lfsr_traversal_read(&lfs, &t, &tinfo) => 0;
assert(tinfo.btype == LFS_BTYPE_BTREE);
}
lfsr_traversal_read(&lfs, &t, &tinfo) => LFS_ERR_NOENT;
lfsr_traversal_close(&lfs, &t) => 0;
// bshrub should have been compacted
lfsr_mdir_t mdir;
lfsr_mtraversal_t mt = LFSR_MTRAVERSAL(LFS_T_CKMETA);
lfsr_mtinfo_t mtinfo;
lfsr_mtree_traverse(&lfs, &mdir, &mt, &mtinfo) => 0;
assert(mtinfo.tag == LFSR_TAG_MDIR);
while (true) {
int err = lfsr_mtree_traverse(&lfs, &mdir, &mt, &mtinfo);
assert(!err || err == LFS_ERR_NOENT);
if (err == LFS_ERR_NOENT) {
break;
}
assert(mtinfo.tag == LFSR_TAG_BRANCH);
assert(lfsr_rbyd_eoff(&mtinfo.u.rbyd) <= GC_COMPACT_THRESH);
}
// check we can still read the file
for (int remount = 0; remount < 2; remount++) {
// remount?
if (remount) {
lfsr_file_close(&lfs, &file) => 0;
lfsr_unmount(&lfs) => 0;
lfsr_mount(&lfs, CFG) => 0;
lfsr_file_open(&lfs, &file, "jellyfish", LFS_O_RDONLY) => 0;
}
lfsr_file_rewind(&lfs, &file) => 0;
uint8_t rbuf[SIZE];
lfsr_file_read(&lfs, &file, rbuf, SIZE) => SIZE;
assert(memcmp(rbuf, wbuf, SIZE) == 0);
}
lfsr_file_close(&lfs, &file) => 0;
lfsr_unmount(&lfs) => 0;
'''
[cases.test_traversal_compact_bshrub_desync]
defines.CKMETA = [false, true]
defines.CK = [false, true]
defines.LOOKAHEAD = [false, true]
# this configuration should create a 2-layer bshrub, which may be
# a bit delicate
defines.INLINE_SIZE = 'BLOCK_SIZE/4'
defines.CRYSTAL_THRESH = -1
defines.FRAGMENT_SIZE = 'BLOCK_SIZE/8'
defines.SIZE = 'BLOCK_SIZE'
# set compact thresh to minimum
defines.GC_COMPACT_THRESH = 'BLOCK_SIZE/2'
in = 'lfs.c'
code = '''
lfs_t lfs;
lfsr_format(&lfs, CFG) => 0;
lfsr_mount(&lfs, CFG) => 0;
uint32_t prng = 42;
// create a desync file
lfsr_file_t file1;
lfsr_file_open(&lfs, &file1, "jellyfish",
LFS_O_RDWR | LFS_O_CREAT | LFS_O_EXCL | LFS_O_DESYNC) => 0;
uint8_t wbuf1[SIZE];
for (lfs_size_t j = 0; j < SIZE; j++) {
wbuf1[j] = 'a' + (TEST_PRNG(&prng) % 26);
}
lfsr_file_write(&lfs, &file1, wbuf1, SIZE) => SIZE;
// rewrite part of our file until bshrub is >gc_compact_thresh full
while (true) {
// we need internals to check this
lfsr_mdir_t mdir;
// ckmeta needed for eoff
lfsr_mtraversal_t mt = LFSR_MTRAVERSAL(LFS_T_CKMETA);
lfsr_mtinfo_t mtinfo;
lfsr_mtree_traverse(&lfs, &mdir, &mt, &mtinfo) => 0;
assert(mtinfo.tag == LFSR_TAG_MDIR);
lfsr_mtree_traverse(&lfs, &mdir, &mt, &mtinfo) => 0;
assert(mtinfo.tag == LFSR_TAG_BRANCH);
if (lfsr_rbyd_eoff(&mtinfo.u.rbyd) > GC_COMPACT_THRESH) {
break;
}
lfsr_file_rewind(&lfs, &file1) => 0;
for (lfs_size_t j = 0; j < FRAGMENT_SIZE; j++) {
wbuf1[j] = 'a' + (TEST_PRNG(&prng) % 26);
}
lfsr_file_write(&lfs, &file1, wbuf1, FRAGMENT_SIZE) => FRAGMENT_SIZE;
}
// create some overlapping files, these should not get messed with
lfsr_file_t file2;
lfsr_file_open(&lfs, &file2, "jellyfish",
LFS_O_RDWR | LFS_O_CREAT | LFS_O_EXCL) => 0;
uint8_t wbuf2[SIZE];
for (lfs_size_t j = 0; j < SIZE; j++) {
wbuf2[j] = 'a' + (TEST_PRNG(&prng) % 26);
}
lfsr_file_write(&lfs, &file2, wbuf2, SIZE) => SIZE;
lfsr_file_sync(&lfs, &file2) => 0;
lfsr_file_t file3;
lfsr_file_open(&lfs, &file3, "jellyfish",
LFS_O_RDWR) => 0;
uint8_t wbuf3[SIZE];
for (lfs_size_t j = 0; j < SIZE; j++) {
wbuf3[j] = 'a' + (TEST_PRNG(&prng) % 26);
}
lfsr_file_write(&lfs, &file3, wbuf3, SIZE) => SIZE;
lfsr_file_desync(&lfs, &file3) => 0;
// try traversing and compacting
lfsr_traversal_t t;
lfsr_traversal_open(&lfs, &t,
LFS_T_COMPACT
| ((CKMETA) ? LFS_T_CKMETA : 0)
| ((CK) ? LFS_T_CK : 0)
| ((LOOKAHEAD) ? LFS_T_LOOKAHEAD : 0)) => 0;
// traverse mroot
struct lfs_tinfo tinfo;
lfsr_traversal_read(&lfs, &t, &tinfo) => 0;
assert(tinfo.btype == LFS_BTYPE_MDIR);
assert(tinfo.block == 0 || tinfo.block == 1);
lfsr_traversal_read(&lfs, &t, &tinfo) => 0;
assert(tinfo.btype == LFS_BTYPE_MDIR);
assert(tinfo.block == 0 || tinfo.block == 1);
// compacting our bshrub nodes may cause them to split, so we may
// need to traverse more nodes than we started with
for (lfs_size_t i = 0; i < 3*5; i++) {
// traverse bshrub
lfsr_traversal_read(&lfs, &t, &tinfo) => 0;
assert(tinfo.btype == LFS_BTYPE_BTREE);
}
lfsr_traversal_read(&lfs, &t, &tinfo) => LFS_ERR_NOENT;
lfsr_traversal_close(&lfs, &t) => 0;
// bshrub should have been compacted
lfsr_mdir_t mdir;
lfsr_mtraversal_t mt = LFSR_MTRAVERSAL(LFS_T_CKMETA);
lfsr_mtinfo_t mtinfo;
lfsr_mtree_traverse(&lfs, &mdir, &mt, &mtinfo) => 0;
assert(mtinfo.tag == LFSR_TAG_MDIR);
while (true) {
int err = lfsr_mtree_traverse(&lfs, &mdir, &mt, &mtinfo);
assert(!err || err == LFS_ERR_NOENT);
if (err == LFS_ERR_NOENT) {
break;
}
assert(mtinfo.tag == LFSR_TAG_BRANCH);
assert(lfsr_rbyd_eoff(&mtinfo.u.rbyd) <= GC_COMPACT_THRESH);
}
// check we can still read the files
lfsr_file_rewind(&lfs, &file1) => 0;
uint8_t rbuf[SIZE];
lfsr_file_read(&lfs, &file1, rbuf, SIZE) => SIZE;
assert(memcmp(rbuf, wbuf1, SIZE) == 0);
lfsr_file_rewind(&lfs, &file2) => 0;
lfsr_file_read(&lfs, &file2, rbuf, SIZE) => SIZE;
assert(memcmp(rbuf, wbuf2, SIZE) == 0);
lfsr_file_rewind(&lfs, &file3) => 0;
lfsr_file_read(&lfs, &file3, rbuf, SIZE) => SIZE;
assert(memcmp(rbuf, wbuf3, SIZE) == 0);
// at least try closing/opening our synced file
lfsr_file_close(&lfs, &file2) => 0;
lfsr_file_open(&lfs, &file2, "jellyfish", LFS_O_RDONLY) => 0;
lfsr_file_read(&lfs, &file2, rbuf, SIZE) => SIZE;
assert(memcmp(rbuf, wbuf2, SIZE) == 0);
lfsr_file_close(&lfs, &file1) => 0;
lfsr_file_close(&lfs, &file2) => 0;
lfsr_file_close(&lfs, &file3) => 0;
lfsr_unmount(&lfs) => 0;
'''
@@ -7024,8 +7504,7 @@ defines.EXCL = [false, true]
defines.CKMETA = [true]
defines.CK = [true]
defines.LOOKAHEAD = [false, true]
# TODO !!! Enable this when bshrub compaction is working
defines.COMPACT = [false]
defines.COMPACT = [false, true]
# set compact thresh to minimum
defines.GC_COMPACT_THRESH = 'BLOCK_SIZE/2'
defines.OPS = 20