Implemented tree rebalancing during rbyd compaction
This isn't actually for performance reasons, but to reduce storage
overhead of the rbyd metadata tree, which was showing signs of being
problematic for small block sizes.
Originally, the plan for compaction was to rely on the self-balancing
rbyd append algorithm and simply append each tag to a new tree.
Unfortunately, since each append requires a rewrite of the trunk
(current search path), this introduces ~n*log(n) alts but only uses ~n alts
for the final tree. This really starts to put pressure on small blocks,
where the exponential-ness of the log doesn't kick in and overhead
limits are already tight.
Measuring lfsr_mdir_commit code size, this shows a ~556 byte cost on
thumb: 16416 -> 16972 (+3.4%). Though there are still some optimizations
on the table, this implementation needs a cleanup pass.
alt overhead code cost
rebalance: <= 28*n 16972
append: <= 24*n*log(n) 16416
Note these all assume worst case alt overhead, but we _need_ to assume
worst case for our rbyd estimations, or else the filesystem can get
stuck in unrecoverable compaction states.
Because of the code cost I'm not sure if rebalancing will stay, be
optional, or replace append-compaction completely yet.
Some implementation notes:
- Most tree balancing algorithms rely on true recursion, I suspect
recursion may be a hard requirement in general, but it's hard to find
bounded-ram algorithms.
This solution gets around the ram requirement by leveraging the fact
that our tags exist in a log to build up each layer in the tree
tail-recursively. It's interesting to note that this is a special
case of having little ram but lots of storage.
- Humorously this shouldn't result in a performance improvement. Rbyd
trees result in a worst case 2*log(n) height, and rebalancing gives us
a perfect worst case log(n) height, but, since we need an additional
alt pointer for each node in our tree, things bump back up to 2*log(n).
- Originally the plan was to terminate each node with an alt-always tag,
but during implementation I realized there was no easy way to get the
key that splits the children with awkward tree lookups. As a
workaround each node is terminated with an altle tag that contains the
key followed by an unreachable null tag. This is redundant information,
but makes the algorithm easier to implement.
Fortunately null tags use the smallest tag encoding, which isn't that
small, but that means this wastes at most 4*n bytes.
- Note this preserves the first-tag-always-ends-up-at-off=0x4 rule, which
is necessary for the littlefs magic to end up in a consistent place.
- I've dropped dropping vestigial names for now, which means vestigial
names can remain in btrees indefinitely. Need to revisit this.
This commit is contained in:
@@ -2568,11 +2568,220 @@ static int lfsr_rbyd_appendall(lfs_t *lfs, lfsr_rbyd_t *rbyd,
|
||||
static int lfsr_rbyd_compact(lfs_t *lfs, lfsr_rbyd_t *rbyd,
|
||||
lfs_ssize_t start_id, lfs_ssize_t end_id, bool drop_vestigial,
|
||||
const lfsr_rbyd_t *source) {
|
||||
#ifndef LFSR_NO_REBALANCE
|
||||
// must fetch before mutating!
|
||||
LFS_ASSERT(lfsr_rbyd_isfetched(rbyd));
|
||||
|
||||
// we can't do anything if we're not erased
|
||||
int err;
|
||||
if (rbyd->off >= lfs->cfg->block_size) {
|
||||
err = LFS_ERR_RANGE;
|
||||
goto failed;
|
||||
}
|
||||
|
||||
// make sure every rbyd starts with a revision count
|
||||
if (rbyd->off == 0) {
|
||||
err = lfsr_rbyd_appendrev(lfs, rbyd, 0);
|
||||
if (err) {
|
||||
goto failed;
|
||||
}
|
||||
}
|
||||
|
||||
// optionally drop the first name in our rbyd, a so-called "vestigial"
|
||||
// name, see lfsr_btree_commit for why we need to do this
|
||||
lfs_ssize_t id = start_id;
|
||||
lfsr_tag_t tag = (drop_vestigial ? lfsr_tag_next(LFSR_TAG_BRANCH) : 0);
|
||||
|
||||
// TODO vestigial names?
|
||||
//lfsr_tag_t tag = (drop_vestigial ? lfsr_tag_next(LFSR_TAG_BRANCH) : 0);
|
||||
lfsr_tag_t tag = 0;
|
||||
|
||||
// first copy over raw tags, note this doesn't create a tree
|
||||
lfs_size_t trunk_count = 0;
|
||||
lfs_size_t trunk_w = 0;
|
||||
lfs_off_t layer_start = rbyd->off;
|
||||
while (true) {
|
||||
lfs_size_t w;
|
||||
lfsr_data_t data;
|
||||
int err = lfsr_rbyd_lookupnext(lfs, source, id, lfsr_tag_next(tag),
|
||||
&id, &tag, &w, &data);
|
||||
if (err && err != LFS_ERR_NOENT) {
|
||||
return err;
|
||||
}
|
||||
if (err == LFS_ERR_NOENT || (end_id >= 0 && id >= end_id)) {
|
||||
break;
|
||||
}
|
||||
|
||||
// keep track of the layer weight
|
||||
trunk_w += w;
|
||||
|
||||
// write the tag
|
||||
lfs_ssize_t d = lfsr_bd_progtag(lfs, rbyd->block, rbyd->off,
|
||||
tag, w, lfsr_data_size(data),
|
||||
&rbyd->crc);
|
||||
if (d < 0) {
|
||||
err = d;
|
||||
goto failed;
|
||||
}
|
||||
rbyd->off += d;
|
||||
|
||||
// and the data
|
||||
err = lfsr_bd_progdata(lfs, rbyd->block, rbyd->off, data,
|
||||
&rbyd->crc);
|
||||
if (err) {
|
||||
goto failed;
|
||||
}
|
||||
rbyd->off += lfsr_data_size(data);
|
||||
|
||||
trunk_count += 1;
|
||||
}
|
||||
lfs_off_t layer_end = rbyd->off;
|
||||
|
||||
// build each layer of a perfectly balanced tree upwards
|
||||
while (trunk_count > 1) {
|
||||
lfs_off_t off = layer_start;
|
||||
trunk_count = 0;
|
||||
trunk_w = 0;
|
||||
layer_start = rbyd->off;
|
||||
while (off < layer_end) {
|
||||
// read two trunks
|
||||
lfs_off_t loff = off;
|
||||
lfsr_tag_t ltag = 0;
|
||||
lfs_size_t lw = 0;
|
||||
while (true) {
|
||||
lfsr_tag_t tag;
|
||||
lfs_size_t w;
|
||||
lfs_size_t size;
|
||||
lfs_ssize_t d = lfsr_bd_readtag(lfs, rbyd->block, off,
|
||||
layer_end-off,
|
||||
&tag, &w, &size, NULL);
|
||||
if (d < 0) {
|
||||
err = d;
|
||||
goto failed;
|
||||
}
|
||||
off += d;
|
||||
lw += w;
|
||||
trunk_w += w;
|
||||
|
||||
// keep track of last non-null tag
|
||||
if (tag) {
|
||||
ltag = tag;
|
||||
}
|
||||
|
||||
// skip the data
|
||||
if (!lfsr_tag_isalt(tag)) {
|
||||
off += size;
|
||||
}
|
||||
|
||||
// read all tags in the trunk
|
||||
if (!lfsr_tag_isalt(tag)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
lfs_off_t roff = off;
|
||||
lfsr_tag_t rtag = 0;
|
||||
lfs_size_t rw = 0;
|
||||
if (off >= layer_end) {
|
||||
roff = loff;
|
||||
rtag = ltag;
|
||||
rw = lw;
|
||||
} else {
|
||||
while (true) {
|
||||
lfsr_tag_t tag;
|
||||
lfs_size_t w;
|
||||
lfs_size_t size;
|
||||
lfs_ssize_t d = lfsr_bd_readtag(lfs, rbyd->block, off,
|
||||
layer_end-off,
|
||||
&tag, &w, &size, NULL);
|
||||
if (d < 0) {
|
||||
err = d;
|
||||
goto failed;
|
||||
}
|
||||
off += d;
|
||||
rw += w;
|
||||
trunk_w += w;
|
||||
|
||||
// keep track of last non-null tag
|
||||
if (tag) {
|
||||
rtag = tag;
|
||||
}
|
||||
|
||||
// skip the data
|
||||
if (!lfsr_tag_isalt(tag)) {
|
||||
off += size;
|
||||
}
|
||||
|
||||
// read all tags in the trunk
|
||||
if (!lfsr_tag_isalt(tag)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// connect ltag with an altle
|
||||
lfs_ssize_t d = lfsr_bd_progtag(lfs, rbyd->block, rbyd->off,
|
||||
LFSR_TAG_ALTLE(false, lfsr_tag_key(ltag)),
|
||||
lw,
|
||||
rbyd->off - loff,
|
||||
&rbyd->crc);
|
||||
if (d < 0) {
|
||||
err = d;
|
||||
goto failed;
|
||||
}
|
||||
rbyd->off += d;
|
||||
|
||||
trunk_w = lw + rw;
|
||||
}
|
||||
|
||||
lfs_ssize_t d = lfsr_bd_progtag(lfs, rbyd->block, rbyd->off,
|
||||
LFSR_TAG_ALTLE(false, lfsr_tag_key(rtag)),
|
||||
rw,
|
||||
rbyd->off - roff,
|
||||
&rbyd->crc);
|
||||
if (d < 0) {
|
||||
err = d;
|
||||
goto failed;
|
||||
}
|
||||
rbyd->off += d;
|
||||
|
||||
// terminate with a null tag
|
||||
d = lfsr_bd_progtag(lfs, rbyd->block, rbyd->off,
|
||||
LFSR_TAG_NULL, 0, 0,
|
||||
&rbyd->crc);
|
||||
if (d < 0) {
|
||||
err = d;
|
||||
goto failed;
|
||||
}
|
||||
rbyd->off += d;
|
||||
|
||||
trunk_count += 1;
|
||||
}
|
||||
layer_end = rbyd->off;
|
||||
}
|
||||
|
||||
// TODO if we use trunk=0 special this way, should fetch
|
||||
// also use trunk=0 for its "no-mdir" test?
|
||||
|
||||
// done! just need to update our trunk/weight
|
||||
if (trunk_count >= 1) {
|
||||
rbyd->trunk = layer_start;
|
||||
}
|
||||
rbyd->weight = trunk_w;
|
||||
|
||||
return 0;
|
||||
|
||||
failed:;
|
||||
// if we fail mark the rbyd as unerased and release the pcache
|
||||
lfs_cache_zero(lfs, &lfs->pcache);
|
||||
rbyd->off = lfs->cfg->block_size;
|
||||
return err;
|
||||
|
||||
#else
|
||||
// optionally drop the first name in our rbyd, a so-called "vestigial"
|
||||
// name, see lfsr_btree_commit for why we need to do this
|
||||
lfs_ssize_t id = start_id;
|
||||
// TODO vestigial names?
|
||||
//lfsr_tag_t tag = (drop_vestigial ? lfsr_tag_next(LFSR_TAG_BRANCH) : 0);
|
||||
lfsr_tag_t tag = 0;
|
||||
|
||||
// try to copy over tags
|
||||
while (true) {
|
||||
lfsr_data_t data;
|
||||
@@ -2598,6 +2807,7 @@ static int lfsr_rbyd_compact(lfs_t *lfs, lfsr_rbyd_t *rbyd,
|
||||
return err;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
static int lfsr_rbyd_commit(lfs_t *lfs, lfsr_rbyd_t *rbyd,
|
||||
|
||||
@@ -87,6 +87,7 @@ def fromleb128(data):
|
||||
return word, len(data)
|
||||
|
||||
def fromtag(data):
|
||||
data = data.ljust(4, b'\0')
|
||||
tag = (data[0] << 8) | data[1]
|
||||
weight, d = fromleb128(data[2:])
|
||||
size, d_ = fromleb128(data[2+d:])
|
||||
|
||||
@@ -87,6 +87,7 @@ def fromleb128(data):
|
||||
return word, len(data)
|
||||
|
||||
def fromtag(data):
|
||||
data = data.ljust(4, b'\0')
|
||||
tag = (data[0] << 8) | data[1]
|
||||
weight, d = fromleb128(data[2:])
|
||||
size, d_ = fromleb128(data[2+d:])
|
||||
|
||||
@@ -96,6 +96,7 @@ def fromleb128(data):
|
||||
return word, len(data)
|
||||
|
||||
def fromtag(data):
|
||||
data = data.ljust(4, b'\0')
|
||||
tag = (data[0] << 8) | data[1]
|
||||
weight, d = fromleb128(data[2:])
|
||||
size, d_ = fromleb128(data[2+d:])
|
||||
|
||||
+63
-13
@@ -3,7 +3,7 @@
|
||||
defines.LOOKAHEAD_SIZE = 'BLOCK_COUNT / 8'
|
||||
|
||||
# test a single mroot
|
||||
[cases.test_mtree_one_mroot]
|
||||
[cases.test_mtree_mroot]
|
||||
code = '''
|
||||
lfs_t lfs;
|
||||
lfsr_format(&lfs, cfg) => 0;
|
||||
@@ -11,21 +11,27 @@ code = '''
|
||||
lfsr_unmount(&lfs) => 0;
|
||||
'''
|
||||
|
||||
# test a single mroot with a custom attribute
|
||||
[cases.test_mtree_one_mroot_attr]
|
||||
# test a single mroot with attributes
|
||||
[cases.test_mtree_mroot_attrs]
|
||||
defines.N = [1, 3]
|
||||
in = 'lfs.c'
|
||||
code = '''
|
||||
const char *alphas = "abcdefghijklmnopqrstuvwxyz";
|
||||
lfs_t lfs;
|
||||
lfsr_format(&lfs, cfg) => 0;
|
||||
lfsr_mount(&lfs, cfg) => 0;
|
||||
|
||||
lfsr_mdir_commit(&lfs, &lfs.mroot, &(lfs_ssize_t){-1}, LFSR_ATTRS(
|
||||
LFSR_ATTR(-1, UATTR(1), 0, "ardvark", 7))) => 0;
|
||||
for (lfs_size_t i = 0; i < N; i++) {
|
||||
lfsr_mdir_commit(&lfs, &lfs.mroot, &(lfs_ssize_t){-1}, LFSR_ATTRS(
|
||||
LFSR_ATTR(-1, UATTR(i), 0, &alphas[i % 26], 1))) => 0;
|
||||
}
|
||||
|
||||
uint8_t buffer[7];
|
||||
lfsr_mdir_get(&lfs, &lfs.mroot,
|
||||
-1, LFSR_TAG_UATTR(1), buffer, 7) => 7;
|
||||
assert(memcmp(buffer, "ardvark", 7) == 0);
|
||||
for (lfs_size_t i = 0; i < N; i++) {
|
||||
uint8_t buffer[1];
|
||||
lfsr_mdir_get(&lfs, &lfs.mroot,
|
||||
-1, LFSR_TAG_UATTR(i), buffer, 1) => 1;
|
||||
assert(memcmp(buffer, &alphas[i % 26], 1) == 0);
|
||||
}
|
||||
|
||||
lfsr_unmount(&lfs) => 0;
|
||||
|
||||
@@ -33,15 +39,59 @@ code = '''
|
||||
// check things stay sane after remount
|
||||
lfsr_mount(&lfs, cfg) => 0;
|
||||
|
||||
lfsr_mdir_get(&lfs, &lfs.mroot,
|
||||
-1, LFSR_TAG_UATTR(1), buffer, 7) => 7;
|
||||
assert(memcmp(buffer, "ardvark", 7) == 0);
|
||||
for (lfs_size_t i = 0; i < N; i++) {
|
||||
uint8_t buffer[1];
|
||||
lfsr_mdir_get(&lfs, &lfs.mroot,
|
||||
-1, LFSR_TAG_UATTR(i), buffer, 1) => 1;
|
||||
assert(memcmp(buffer, &alphas[i % 26], 1) == 0);
|
||||
}
|
||||
|
||||
lfsr_unmount(&lfs) => 0;
|
||||
'''
|
||||
|
||||
# test a single mroot with forced compaction
|
||||
[cases.test_mtree_mroot_compact]
|
||||
defines.N = [1, 3]
|
||||
in = 'lfs.c'
|
||||
code = '''
|
||||
const char *alphas = "abcdefghijklmnopqrstuvwxyz";
|
||||
lfs_t lfs;
|
||||
lfsr_format(&lfs, cfg) => 0;
|
||||
lfsr_mount(&lfs, cfg) => 0;
|
||||
|
||||
for (lfs_size_t i = 0; i < N; i++) {
|
||||
// force mroot to compact
|
||||
lfs.mroot.rbyd.off = BLOCK_SIZE;
|
||||
|
||||
lfsr_mdir_commit(&lfs, &lfs.mroot, &(lfs_ssize_t){-1}, LFSR_ATTRS(
|
||||
LFSR_ATTR(-1, UATTR(i), 0, &alphas[i % 26], 1))) => 0;
|
||||
}
|
||||
|
||||
for (lfs_size_t i = 0; i < N; i++) {
|
||||
uint8_t buffer[1];
|
||||
lfsr_mdir_get(&lfs, &lfs.mroot,
|
||||
-1, LFSR_TAG_UATTR(i), buffer, 1) => 1;
|
||||
assert(memcmp(buffer, &alphas[i % 26], 1) == 0);
|
||||
}
|
||||
|
||||
lfsr_unmount(&lfs) => 0;
|
||||
|
||||
|
||||
// check things stay sane after remount
|
||||
lfsr_mount(&lfs, cfg) => 0;
|
||||
|
||||
for (lfs_size_t i = 0; i < N; i++) {
|
||||
uint8_t buffer[1];
|
||||
lfsr_mdir_get(&lfs, &lfs.mroot,
|
||||
-1, LFSR_TAG_UATTR(i), buffer, 1) => 1;
|
||||
assert(memcmp(buffer, &alphas[i % 26], 1) == 0);
|
||||
}
|
||||
|
||||
lfsr_unmount(&lfs) => 0;
|
||||
'''
|
||||
|
||||
# test a single mroot with many commits
|
||||
[cases.test_mtree_one_mroot_many_commits]
|
||||
[cases.test_mtree_mroot_many_commits]
|
||||
defines.N = [5, 5000]
|
||||
in = 'lfs.c'
|
||||
code = '''
|
||||
|
||||
Reference in New Issue
Block a user