Implemented a block crystalization algorithm that actually works
The idea here is to:
1. Try to figure out the current "crystal" (set of fragments) we are a
part of.
2. Decide if our crystal has probably exceeded the configured crystal size
and needs to be compacted into a block.
3. Guess the local block alignment by looking at the entry immediately
left of our crystal.
Figuring out the state of our current crystal is done heuristically,
with a lookup 1 crystal-size to the left to find the start of our
crystal, followed by a lookup 1 crystal-size to the right of the crystal
start to find the end of our crystal:
-crs pos -crs pos
.-------| .-------|
-crs pos '---. | +crs '-------. +crs
.-------| |---|---. |------.
.---' | | | '---. | .---'
v v v v v v v
.---+---+---+---. +---.---+---+---. ---+---.---.---+
| crystal | blk | crystal | blk |crs| blk
'---+---+---+---' +---'---+---+---' ---+---'---'---+
This is a heuristic that doesn't catch any holes in our crystal, but
that's ok, we probably don't want small holes preventing block
compaction anyways.
Finding the block alignment then just requires looking up the entry to
the left of our crystal, if the left entry + crystal fits in a block, we
are the same block, otherwise we align to the left entry. Note this may
break our crystal during block compaction if the crystal itself is not
block aligned, but that's ok, we just recalculate the new crystal based
on the new block:
fits partial fit doesn't fit
.---+---+---+---. .---+---+---+---+---. .---+---+---+---+---.
| block |crs| | block | crs | | block |crs|
'---+---+---+---' '---+---+---+---+---' '---+---+---+---+---'
| | |
v v v
.---+---+---+---. .---+---+---+---+---. .---+---+---+---+---+- - -
| block | | block |crs| | block | block
'---+---+---+---' '---+---+---+---+---' '---+---+---+---+---+- - -
This involves at most 3 lookups, though there are some shortcuts: If
appending a file we never need to lookup the right crystal boundary,
and if we don't exceed our crystal size we don't need to figure out the
block alignment.
---
There's another variant of this scheme where we don't consider any
fragments to the right of the current fragment. This saves a lookup, but
more importantly would mean we could take advantage of ecksums when
partially rewriting part of a file.
As a tradeoff this variant does end up leaving any partially rewritten
files with >~2x storage overhead. Still, this may be interesting to
provide as an alternative write strategy in the future.
This commit is contained in:
@@ -9885,72 +9885,136 @@ static int lfsr_file_flushshrub(lfs_t *lfs, lfsr_file_t *file) {
|
||||
continue;
|
||||
|
||||
flush:;
|
||||
// first we need to figure out the best block alignment, to do this
|
||||
// we try to find a block to our left, at least one block_size away
|
||||
lfs_off_t left_align;
|
||||
if ((lfs_soff_t)(pos - lfs->cfg->block_size) < 0) {
|
||||
// left block impossible? align to 0
|
||||
left_align = 0;
|
||||
} else if ((lfs_soff_t)(pos - lfs->cfg->block_size)
|
||||
>= (lfs_soff_t)lfsr_tree_size(&file->tree)) {
|
||||
// tree too small? align arbitrarily
|
||||
left_align = pos;
|
||||
// first we need to figure out our current crystal, we do this
|
||||
// heuristically.
|
||||
//
|
||||
// note that we may end up including holes in our crystal, but this
|
||||
// is fine. we don't want small holes breaking up blocks anyways
|
||||
//
|
||||
lfs_off_t crystal_start;
|
||||
// at beginning of file?
|
||||
if (pos < lfs->cfg->crystal_size) {
|
||||
crystal_start = 0;
|
||||
|
||||
// beyond the end of the tree?
|
||||
} else if (pos - lfs->cfg->crystal_size
|
||||
>= lfsr_tree_size(&file->tree)) {
|
||||
crystal_start = pos;
|
||||
|
||||
// find left crystal neighbor
|
||||
} else {
|
||||
lfsr_bid_t bid_;
|
||||
lfsr_tag_t tag_;
|
||||
lfsr_bid_t weight_;
|
||||
lfsr_data_t data_;
|
||||
int err = lfsr_tree_lookupnext(lfs, &file->tree,
|
||||
pos - lfs->cfg->block_size,
|
||||
&bid_, NULL, &weight_, NULL);
|
||||
pos - lfs->cfg->crystal_size,
|
||||
&bid_, &tag_, &weight_, &data_);
|
||||
if (err) {
|
||||
LFS_ASSERT(err != LFS_ERR_NOENT);
|
||||
return err;
|
||||
}
|
||||
LFS_ASSERT(tag_ == LFSR_TAG_DATA
|
||||
|| tag_ == LFSR_TAG_BLOCK);
|
||||
|
||||
// our current pos can't belong in the left block, so align to next
|
||||
// theoretical block
|
||||
left_align = lfs_min32(bid_+1, pos);
|
||||
// if left crystal neighbor is a fragment and there is no hole
|
||||
// between our own crystal and our neighbor, include as a part of
|
||||
// our crystal
|
||||
if (tag_ == LFSR_TAG_DATA
|
||||
&& bid_-(weight_-1)+lfsr_data_size(&data_)
|
||||
>= pos - lfs->cfg->crystal_size) {
|
||||
crystal_start = bid_-(weight_-1);
|
||||
|
||||
// otherwise our neighbor determines our crystal boundary
|
||||
} else {
|
||||
crystal_start = lfs_min32(bid_+1, pos);
|
||||
}
|
||||
}
|
||||
|
||||
// from our left alignment, try to find right alignment, this may
|
||||
// squish us into less than a full block
|
||||
lfs_off_t right_align;
|
||||
if (left_align + lfs->cfg->block_size >= lfsr_tree_size(&file->tree)) {
|
||||
// tree too small? align to end of tree
|
||||
right_align = lfsr_tree_size(&file->tree);
|
||||
} else {
|
||||
// if we haven't already exceeded our crystallization threshold,
|
||||
// find right crystal neighbor
|
||||
lfs_off_t crystal_end = pos + d;
|
||||
if (crystal_end - crystal_start <= lfs->cfg->crystal_size
|
||||
&& crystal_start + lfs->cfg->crystal_size
|
||||
< lfsr_tree_size(&file->tree)) {
|
||||
lfsr_bid_t bid_;
|
||||
lfsr_tag_t tag_;
|
||||
lfsr_bid_t weight_;
|
||||
lfsr_data_t data_;
|
||||
int err = lfsr_tree_lookupnext(lfs, &file->tree,
|
||||
left_align + lfs->cfg->block_size,
|
||||
&bid_, NULL, &weight_, NULL);
|
||||
crystal_start + lfs->cfg->crystal_size,
|
||||
&bid_, &tag_, &weight_, &data_);
|
||||
if (err) {
|
||||
LFS_ASSERT(err != LFS_ERR_NOENT);
|
||||
return err;
|
||||
}
|
||||
LFS_ASSERT(tag_ == LFSR_TAG_DATA
|
||||
|| tag_ == LFSR_TAG_BLOCK);
|
||||
|
||||
// our block can't reside in the right block, so squish our block
|
||||
// to match its alignment
|
||||
right_align = bid_-(weight_-1);
|
||||
// if right crystal neighbor is a fragment, include as a part
|
||||
// of our crystal
|
||||
if (tag_ == LFSR_TAG_DATA) {
|
||||
crystal_end = lfs_max32(
|
||||
bid_-(weight_-1)+lfsr_data_size(&data_),
|
||||
pos + d);
|
||||
|
||||
// otherwise treat as crystal boundary
|
||||
} else {
|
||||
crystal_end = lfs_max32(
|
||||
bid_-(weight_-1),
|
||||
pos + d);
|
||||
}
|
||||
}
|
||||
|
||||
// bump right alignment to always include pending data
|
||||
right_align = lfs_max32(
|
||||
right_align,
|
||||
lfs_min32(pos + d, left_align + lfs->cfg->block_size));
|
||||
|
||||
LFS_ASSERT(pos >= left_align);
|
||||
LFS_ASSERT(pos < right_align);
|
||||
|
||||
// TODO check for becksums somewhere?
|
||||
|
||||
// does our block exceed our crystallization threshold? need to
|
||||
// has our crystal exceeded our crystallization threshold? time to
|
||||
// compact into a new block
|
||||
//
|
||||
// Note this is a just a heuristic. This block may end up containing
|
||||
// holes we don't account for, but we generally don't want a bunch of
|
||||
// small holes in our files anyways.
|
||||
//
|
||||
if (right_align - left_align > lfs->cfg->crystal_size) {
|
||||
if (crystal_end - crystal_start > lfs->cfg->crystal_size) {
|
||||
// TODO check for becksums somewhere?
|
||||
|
||||
// before we can compact we need to figure out the best block
|
||||
// alignment, we use the entry immediately to the left of our
|
||||
// crystal for this
|
||||
lfs_off_t block_off;
|
||||
// some corner cases where we align arbitrarily
|
||||
if (crystal_start == 0 || lfsr_tree_size(&file->tree) == 0) {
|
||||
block_off = crystal_start;
|
||||
|
||||
// find left block neighbor
|
||||
} else {
|
||||
lfsr_bid_t bid_;
|
||||
lfsr_tag_t tag_;
|
||||
lfsr_bid_t weight_;
|
||||
lfsr_data_t data_;
|
||||
int err = lfsr_tree_lookupnext(lfs, &file->tree,
|
||||
lfs_min32(
|
||||
crystal_start-1,
|
||||
lfsr_tree_size(&file->tree)-1),
|
||||
&bid_, &tag_, &weight_, &data_);
|
||||
if (err) {
|
||||
LFS_ASSERT(err != LFS_ERR_NOENT);
|
||||
return err;
|
||||
}
|
||||
LFS_ASSERT(tag_ == LFSR_TAG_DATA
|
||||
|| tag_ == LFSR_TAG_BLOCK);
|
||||
|
||||
// is our left neighbor in the same block?
|
||||
if (crystal_start - (bid_-(weight_-1)) < lfs->cfg->block_size
|
||||
&& lfsr_data_size(&data_) > 0) {
|
||||
block_off = bid_-(weight_-1);
|
||||
|
||||
// no? is our left neighbor at least our left block neighbor?
|
||||
// align to block alignment
|
||||
} else if (crystal_start - (bid_-(weight_-1))
|
||||
< 2*lfs->cfg->block_size
|
||||
&& lfsr_data_size(&data_) > 0) {
|
||||
block_off = bid_-(weight_-1) + lfs->cfg->block_size;
|
||||
|
||||
// no!? file is sparse, align arbitrarily
|
||||
} else {
|
||||
block_off = crystal_start;
|
||||
}
|
||||
}
|
||||
|
||||
// allocate a new block
|
||||
lfs_block_t block;
|
||||
int err = lfs_alloc(lfs, &block);
|
||||
@@ -9965,10 +10029,11 @@ static int lfsr_file_flushshrub(lfs_t *lfs, lfsr_file_t *file) {
|
||||
}
|
||||
|
||||
// copy any data underneath our block into our block
|
||||
lfs_off_t pos_ = left_align;
|
||||
while (pos_ < right_align) {
|
||||
lfs_off_t pos_ = block_off;
|
||||
while (pos_ < block_off + lfs->cfg->block_size) {
|
||||
lfsr_data_t data;
|
||||
err = lfsr_file_readnext(lfs, file, pos_, right_align - pos_,
|
||||
err = lfsr_file_readnext(lfs, file, pos_,
|
||||
block_off + lfs->cfg->block_size - pos_,
|
||||
&data);
|
||||
if (err) {
|
||||
// end of file?
|
||||
@@ -9979,8 +10044,16 @@ static int lfsr_file_flushshrub(lfs_t *lfs, lfsr_file_t *file) {
|
||||
}
|
||||
LFS_ASSERT(lfsr_data_size(&data) > 0);
|
||||
|
||||
// found a hole that goes all the way to the end? terminate
|
||||
// early
|
||||
if (lfsr_data_ishole(&data)
|
||||
&& pos_ + lfsr_data_size(&data)
|
||||
>= block_off + lfs->cfg->block_size) {
|
||||
break;
|
||||
}
|
||||
|
||||
// prog data/hole
|
||||
err = lfsr_bd_progdata(lfs, block, pos_ - left_align,
|
||||
err = lfsr_bd_progdata(lfs, block, pos_ - block_off,
|
||||
data,
|
||||
NULL);
|
||||
if (err) {
|
||||
@@ -9989,6 +10062,7 @@ static int lfsr_file_flushshrub(lfs_t *lfs, lfsr_file_t *file) {
|
||||
|
||||
pos_ += lfsr_data_size(&data);
|
||||
}
|
||||
lfs_off_t block_size = pos_ - block_off;
|
||||
|
||||
// TODO validate?
|
||||
// finalize our write
|
||||
@@ -10001,19 +10075,21 @@ static int lfsr_file_flushshrub(lfs_t *lfs, lfsr_file_t *file) {
|
||||
lfsr_bptr_t bptr = {
|
||||
.block = block,
|
||||
.off = 0,
|
||||
.size = right_align - left_align,
|
||||
.size = block_size,
|
||||
};
|
||||
|
||||
// and write it into our tree
|
||||
uint8_t bptr_buf[LFSR_BPTR_DSIZE];
|
||||
err = lfsr_tree_carve(lfs, &file->tree,
|
||||
left_align, right_align - left_align, 0,
|
||||
block_off, block_size, 0,
|
||||
LFSR_TAG_BLOCK, lfsr_data_frombptr(&bptr, bptr_buf));
|
||||
if (err) {
|
||||
return err;
|
||||
}
|
||||
|
||||
pos = right_align;
|
||||
// note due to block alignment we may not actually make progress
|
||||
// here until a second pass
|
||||
pos = lfs_max32(pos, block_off + block_size);
|
||||
|
||||
// fits in crystallization threshold? just append a fragment
|
||||
} else {
|
||||
@@ -10025,6 +10101,13 @@ static int lfsr_file_flushshrub(lfs_t *lfs, lfsr_file_t *file) {
|
||||
lfs_size_t data_count = 0;
|
||||
datas[data_count++] = data;
|
||||
|
||||
// TODO we should really coalesce fragments in our shrub here,
|
||||
// otherwise unaligned fragments risk a bunch of rewrites
|
||||
//
|
||||
// we can probably do this as a part of figuring out the first
|
||||
// fragment's alignment and not need to increase the maximum
|
||||
// number of concatenated datas
|
||||
|
||||
// do we have a left sibling?
|
||||
if (pos > 0 && lfsr_tree_size(&file->tree) >= pos) {
|
||||
// TODO can we do this here?
|
||||
|
||||
Reference in New Issue
Block a user