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:
Christopher Haster
2023-11-10 21:11:35 -06:00
parent 0c3fea4a6e
commit b0e1d49efe
+133 -50
View File
@@ -9885,72 +9885,136 @@ static int lfsr_file_flushshrub(lfs_t *lfs, lfsr_file_t *file) {
continue; continue;
flush:; flush:;
// first we need to figure out the best block alignment, to do this // first we need to figure out our current crystal, we do this
// we try to find a block to our left, at least one block_size away // heuristically.
lfs_off_t left_align; //
if ((lfs_soff_t)(pos - lfs->cfg->block_size) < 0) { // note that we may end up including holes in our crystal, but this
// left block impossible? align to 0 // is fine. we don't want small holes breaking up blocks anyways
left_align = 0; //
} else if ((lfs_soff_t)(pos - lfs->cfg->block_size) lfs_off_t crystal_start;
>= (lfs_soff_t)lfsr_tree_size(&file->tree)) { // at beginning of file?
// tree too small? align arbitrarily if (pos < lfs->cfg->crystal_size) {
left_align = pos; 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 { } else {
lfsr_bid_t bid_; lfsr_bid_t bid_;
lfsr_tag_t tag_;
lfsr_bid_t weight_; lfsr_bid_t weight_;
lfsr_data_t data_;
int err = lfsr_tree_lookupnext(lfs, &file->tree, int err = lfsr_tree_lookupnext(lfs, &file->tree,
pos - lfs->cfg->block_size, pos - lfs->cfg->crystal_size,
&bid_, NULL, &weight_, NULL); &bid_, &tag_, &weight_, &data_);
if (err) { if (err) {
LFS_ASSERT(err != LFS_ERR_NOENT); LFS_ASSERT(err != LFS_ERR_NOENT);
return err; 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 // if left crystal neighbor is a fragment and there is no hole
// theoretical block // between our own crystal and our neighbor, include as a part of
left_align = lfs_min32(bid_+1, pos); // 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 // if we haven't already exceeded our crystallization threshold,
// squish us into less than a full block // find right crystal neighbor
lfs_off_t right_align; lfs_off_t crystal_end = pos + d;
if (left_align + lfs->cfg->block_size >= lfsr_tree_size(&file->tree)) { if (crystal_end - crystal_start <= lfs->cfg->crystal_size
// tree too small? align to end of tree && crystal_start + lfs->cfg->crystal_size
right_align = lfsr_tree_size(&file->tree); < lfsr_tree_size(&file->tree)) {
} else {
lfsr_bid_t bid_; lfsr_bid_t bid_;
lfsr_tag_t tag_;
lfsr_bid_t weight_; lfsr_bid_t weight_;
lfsr_data_t data_;
int err = lfsr_tree_lookupnext(lfs, &file->tree, int err = lfsr_tree_lookupnext(lfs, &file->tree,
left_align + lfs->cfg->block_size, crystal_start + lfs->cfg->crystal_size,
&bid_, NULL, &weight_, NULL); &bid_, &tag_, &weight_, &data_);
if (err) { if (err) {
LFS_ASSERT(err != LFS_ERR_NOENT); LFS_ASSERT(err != LFS_ERR_NOENT);
return err; 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 // if right crystal neighbor is a fragment, include as a part
// to match its alignment // of our crystal
right_align = bid_-(weight_-1); 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 // has our crystal exceeded our crystallization threshold? time to
right_align = lfs_max32( // compact into a new block
right_align, if (crystal_end - crystal_start > lfs->cfg->crystal_size) {
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? // TODO check for becksums somewhere?
// does our block exceed our crystallization threshold? need to // before we can compact we need to figure out the best block
// compact into a new block // alignment, we use the entry immediately to the left of our
// // crystal for this
// Note this is a just a heuristic. This block may end up containing lfs_off_t block_off;
// holes we don't account for, but we generally don't want a bunch of // some corner cases where we align arbitrarily
// small holes in our files anyways. if (crystal_start == 0 || lfsr_tree_size(&file->tree) == 0) {
// block_off = crystal_start;
if (right_align - left_align > lfs->cfg->crystal_size) {
// 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 // allocate a new block
lfs_block_t block; lfs_block_t block;
int err = lfs_alloc(lfs, &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 // copy any data underneath our block into our block
lfs_off_t pos_ = left_align; lfs_off_t pos_ = block_off;
while (pos_ < right_align) { while (pos_ < block_off + lfs->cfg->block_size) {
lfsr_data_t data; 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); &data);
if (err) { if (err) {
// end of file? // 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); 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 // prog data/hole
err = lfsr_bd_progdata(lfs, block, pos_ - left_align, err = lfsr_bd_progdata(lfs, block, pos_ - block_off,
data, data,
NULL); NULL);
if (err) { if (err) {
@@ -9989,6 +10062,7 @@ static int lfsr_file_flushshrub(lfs_t *lfs, lfsr_file_t *file) {
pos_ += lfsr_data_size(&data); pos_ += lfsr_data_size(&data);
} }
lfs_off_t block_size = pos_ - block_off;
// TODO validate? // TODO validate?
// finalize our write // finalize our write
@@ -10001,19 +10075,21 @@ static int lfsr_file_flushshrub(lfs_t *lfs, lfsr_file_t *file) {
lfsr_bptr_t bptr = { lfsr_bptr_t bptr = {
.block = block, .block = block,
.off = 0, .off = 0,
.size = right_align - left_align, .size = block_size,
}; };
// and write it into our tree // and write it into our tree
uint8_t bptr_buf[LFSR_BPTR_DSIZE]; uint8_t bptr_buf[LFSR_BPTR_DSIZE];
err = lfsr_tree_carve(lfs, &file->tree, 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)); LFSR_TAG_BLOCK, lfsr_data_frombptr(&bptr, bptr_buf));
if (err) { if (err) {
return 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 // fits in crystallization threshold? just append a fragment
} else { } else {
@@ -10025,6 +10101,13 @@ static int lfsr_file_flushshrub(lfs_t *lfs, lfsr_file_t *file) {
lfs_size_t data_count = 0; lfs_size_t data_count = 0;
datas[data_count++] = data; 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? // do we have a left sibling?
if (pos > 0 && lfsr_tree_size(&file->tree) >= pos) { if (pos > 0 && lfsr_tree_size(&file->tree) >= pos) {
// TODO can we do this here? // TODO can we do this here?