From c815c19c2081d913e2b1d5f4234a4b12859825cb Mon Sep 17 00:00:00 2001 From: Christopher Haster Date: Fri, 20 Oct 2023 01:22:44 -0500 Subject: [PATCH] New "fragmenting" write strategy The attempt to implement in-rbyd data slicing, being lazily coalesced during rbyd compaction, failed pretty much completely. Slicing is a very enticing write strategy, getting both minimal overhead post-compaction and fast random write speeds, but the idea has some fundamental conflicts with how we play out attrs post-compaction. This idea might work in a more powerful filesystem, but brings back the need to simulate rbyds in RAM, which is something I really don't want to do (complex, bug-prone, likely adds code cost, may not even be tractable). So, third time's the charm? --- This new write strategy writes only datas and bptrs, and avoids dagging by completely rewriting any regions of data larger than a configurable crystallization threshold. This loses most of the benefits of data crystallization, random writes will now usually need to rewrite a full block, but as a tradeoff our data at rest is always stored with optimal overhead. And at least data crystallization still saves space when our data isn't block aligned, or in sparse files. From reading up on some other filesystem designs it seems this is a desirable optimization sometimes referred to as "tail-packing" or "block suballocation" Some other changes from just having more time to think about the problem: 1. Instead of scanning to figure out our current crystal size, we can use a simple heuristic of 1. look up left block, 2. look up right block, 3. assume any data between these blocks contribute to our current crystal. This is just a heuristic, so worst case you write the first and last byte of a block which is enough to trigger compaction into a block. But on the plus side this avoids issues with small holes preventing blocks from being formed. This approach brings the number of btree lookups down from O(crystallize_size) to 2. 2. I've gone ahead and dropped the previous scheme of coalesce_size + fragment_size and instead adopted a single fragment_size that controls the size of, well, fragments, i.e. data elements stored directly in trees. This affects both the inlined shrub as well as fragments stored in the inner nodes of the btree. I believe it's very similar to what is often called "pages" in logging filesystems, though I'm going to avoid that term for now because it's a bit overloaded. Previously, neighboring writes that, when combined, would exceed our coalesce_size, they just weren't combined. Now they are combined up to our fragment size, potentially splitting the right fragment. Before (fragment_size=8): .---+---+---+---+---+---+---+---. | 8 bytes | '---+---+---+---+---+---+---+---' + .---+---+---+---+---. | 5 bytes | '---+---+---+---+---' = .---+---+---+---+---+---+---+---+---+---. | 5 bytes | 5 bytes | '---+---+---+---+---+---+---+---+---+---' After: .---+---+---+---+---+---+---+---. | 8 bytes | '---+---+---+---+---+---+---+---' + .---+---+---+---+---. | 5 bytes | '---+---+---+---+---' = .---+---+---+---+---+---+---+---+---+---. | 8 bytes |2 bytes| '---+---+---+---+---+---+---+---+---+---' This leads to better fragment alignment (much like our block strategy), and minimizes tree overhead. Any neighboring data to the right is only coalesced if it fits in the current fragment, or would be rewritten (carved) anyways, to avoid unnecessary data rewriting. For example (fragment_size=8): .---+---+---+---+---+---+---+---+---+---+---+---+---+---. | 6 bytes | 6 bytes |2 bytes| '---+---+---+---+---+---+---+---+---+---+---+---+---+---' + .---+---+---+---+---. | 5 bytes | '---+---+---+---+---' = .---+---+---+---+---+---+---+---+---+---+---+---+---+---. | 8 bytes | 4 bytes |2 bytes| '---+---+---+---+---+---+---+---+---+---+---+---+---+---' Other than these changes this commit is mostly a bunch of carveshrub rewriting again, which continues to be nuanced and annoying to get bug free. --- lfs.c | 864 ++++++++++++++++++++++++++++------------- lfs.h | 2 +- runners/bench_runner.h | 8 +- runners/test_runner.h | 8 +- tests/test_files.toml | 2 +- 5 files changed, 608 insertions(+), 276 deletions(-) diff --git a/lfs.c b/lfs.c index cdc1234e..662e84f1 100644 --- a/lfs.c +++ b/lfs.c @@ -9297,7 +9297,7 @@ static int lfsr_file_readnext(lfs_t *lfs, const lfsr_file_t *file, // data get a strict read hint d = lfs_min32( d, - lfsr_data_size(&file->shrub.u.data)); + lfsr_data_size(&file->shrub.u.data) - pos); if (weight_) { *weight_ = d; @@ -9454,317 +9454,510 @@ lfs_ssize_t lfsr_file_read(lfs_t *lfs, lfsr_file_t *file, ? LFSR_SLICE_DSIZE \ : LFSR_BPTR_DSIZE)) -static lfs_ssize_t lfsr_tree_buildcarve(lfs_t *lfs, const lfsr_tree_t *tree, +//static lfs_ssize_t lfsr_tree_buildcarve(lfs_t *lfs, const lfsr_tree_t *tree, +// lfs_off_t pos, lfs_off_t weight, lfs_soff_t delta, +// lfsr_tag_t tag, lfsr_data_t data, +// lfs_soff_t *estimate_, +// lfsr_attr_t attrs[static LFSR_CARVE_ATTRS], +// uint8_t buffer[static LFSR_CARVE_DSIZE]) { +// // TODO need size? +// // we should never try to shove more data than can fit in a given weight +// LFS_ASSERT(lfsr_data_size(&data) <= weight + delta); +// +// // allocate attrs/buffer as necessary +// lfs_size_t attr_count = 0; +// lfs_size_t buffer_off = 0; +// +// // keeep track of removed data, we combined this into a single attr, +// // and keep track of how our changes will affect our size estimate +// lfs_off_t rm = 0; +// lfs_soff_t estimate = 0; +// +// // try to carve any existing data +// lfs_off_t pos_ = pos; +// while (pos_ < lfs_min32(pos+weight, lfsr_tree_size(tree))) { +// lfsr_bid_t bid_; +// lfsr_tag_t tag_; +// lfsr_bid_t weight_; +// lfsr_data_t data_; +// // the tree may be aliased to either a sprout or block pointer if +// // we're creating a new btree, assume sprout if we're a shrub, +// // otherwise a block pointer +// if (!lfsr_tree_hasbtree(tree) && lfsr_tag_isshrub(tag)) { +// const lfsr_data_t *slice = (const lfsr_data_t*)tree; +// bid_ = lfsr_data_size(slice)-1; +// tag_ = tag; +// weight_ = lfsr_data_size(slice); +// data_ = *slice; +// } else if (!lfsr_tree_hasbtree(tree)) { +// // TODO +// LFS_ASSERT(false); +// } else { +// int err = lfsr_btree_lookupnext(lfs, &tree->u.btree, pos_, +// &bid_, &tag_, &weight_, &data_); +// if (err) { +// LFS_ASSERT(err != LFS_ERR_NOENT); +// return err; +// } +// } +// LFS_ASSERT(lfsr_tag_key(tag_) == LFSR_TAG_DATA +// || lfsr_tag_key(tag_) == LFSR_TAG_SLICE +// || lfsr_tag_key(tag_) == LFSR_TAG_BLOCK); +// +// // go ahead and dereference to get to the actual data +// lfsr_data_t slice_; +// lfsr_bptr_t bptr_; +// int err = lfsr_data_deref(lfs, tag_, data_, &slice_, &bptr_); +// if (err) { +// return err; +// } +// LFS_ASSERT(lfsr_data_size(&slice_) <= weight_); +// +//// // get the physical data size +//// lfs_size_t size_; +//// err = lfsr_data_derefsize(lfs, tag_, data_, &size_); +//// if (err) { +//// return err; +//// } +//// LFS_ASSERT(size_ <= weight_); +// +// // note an entry can be both a left and right sibling! +// +// // found left sibling? +// if (pos > bid_-(weight_-1)) { +// lfs_off_t overlap_ = (bid_+1) - pos; +// LFS_ASSERT((lfs_soff_t)overlap_ >= 0); +// +// // carve in-rbyd data? +// if (lfsr_tag_key(tag_) == LFSR_TAG_DATA +// || lfsr_tag_key(tag_) == LFSR_TAG_SLICE) { +// lfsr_data_t slice__ = LFSR_DATA_DISK( +// slice_.u.disk.block, +// slice_.u.disk.off, +// lfs_min32( +// weight_ - overlap_, +// lfsr_data_size(&slice_))); +// +// // some special cases require a new tag: if we're creating a +// // new btree +// if (!lfsr_tree_hasbtree(tree)) { +// attrs[attr_count++] = LFSR_ATTR(0, +// TAG(lfsr_tag_mode(tag) +// | LFSR_TAG_SLICE), +(weight_ - overlap_), +// FROMSLICE(&slice__, &buffer[buffer_off])); +// buffer += LFSR_SLICE_DSIZE; +// +// // update our estimate +// estimate += lfsr_data_size(&slice__); +// +// } else { +// attrs[attr_count++] = LFSR_ATTR(bid_, +// TAG(lfsr_tag_mode(tag) +// | LFSR_TAG_GROW(WIDE(SLICE))), -overlap_, +// FROMSLICE(&slice__, &buffer[buffer_off])); +// buffer += LFSR_SLICE_DSIZE; +// +// // update our estimate +// estimate -= (lfsr_data_size(&slice_) +// - lfsr_data_size(&slice__)); +// // we may remove a hole here +// if (lfsr_data_size(&slice_) < weight_ +// && lfsr_data_size(&slice__) == weight_ - overlap_) { +// estimate -= LFSR_ATTR_ESTIMATE; +// } +// } +// +// // carve block pointer? +// } else if (lfsr_tag_key(tag_) == LFSR_TAG_BLOCK) { +// // TODO +// LFS_ASSERT(false); +//// lfsr_bptr_t bptr_; +//// int err = lfsr_data_readbptr(lfs, &data_, &bptr_); +//// if (err) { +//// return err; +//// } +//// LFS_ASSERT(bptr_.size <= weight_); +//// +//// // are we carving the block too small? +//// if (bptr_.size <= lfs->cfg->crystallize_size) { +//// attrs[attr_count++] = LFSR_ATTR(bid_, +//// GROW(), -overlap_, DATA( +//// data_.u.disk.block, +//// data_.u.disk.off, +//// weight_ - overlap_})); +//// } else { +//// attrs[attr_count++] = LFSR_ATTR(bid_, +//// GROW(), -overlap_, FROMBPTR( +//// (&(lfsr_bptr_t){ +//// .block=data_.u.disk.block, +//// .off=data_.u.disk.off, +//// .size=weight_ - overlap_}), +//// &buffer[buffer_off])); +//// buffer += LFSR_BPTR_DSIZE; +//// } +// } +// +// // TODO wait is this the same on all branches? +// pos_ = bid_+1; +// } +// +// // found right sibling? +// if (pos + weight < bid_+1) { +// lfs_off_t overlap_ = (pos + weight) - (bid_-(weight_-1)); +// LFS_ASSERT((lfs_soff_t)overlap_ >= 0); +// +// // carve in-rbyd data? +// if (lfsr_tag_key(tag_) == LFSR_TAG_DATA +// || lfsr_tag_key(tag_) == LFSR_TAG_SLICE) { +// lfsr_data_t slice__ = LFSR_DATA_DISK( +// slice_.u.disk.block, +// slice_.u.disk.off + lfs_min32( +// overlap_, +// lfsr_data_size(&slice_)), +// lfsr_data_size(&slice_) - lfs_min32( +// overlap_, +// lfsr_data_size(&slice_))); +// +// // some special cases require a new tag: if we're creating a +// // new btree, or if left/right siblings are the same +// if (!lfsr_tree_hasbtree(tree) || overlap_ > weight) { +// attrs[attr_count++] = LFSR_ATTR(pos, +// TAG(lfsr_tag_mode(tag) +// | LFSR_TAG_SLICE), +(weight_ - overlap_), +// FROMSLICE(&slice__, &buffer[buffer_off])); +// buffer += LFSR_SLICE_DSIZE; +// +// // update our estimate +// estimate += lfsr_data_size(&slice__); +// // if we split, we may actually create a hole +// if (lfsr_data_size(&slice__) < weight_ - overlap_) { +// estimate += LFSR_ATTR_ESTIMATE; +// } +// +// } else { +// // we need to account for changes to left sibling here +// attrs[attr_count++] = LFSR_ATTR(pos+rm+weight_-1, +// TAG(lfsr_tag_mode(tag) +// | LFSR_TAG_GROW(WIDE(SLICE))), -overlap_, +// FROMSLICE(&slice__, &buffer[buffer_off])); +// buffer += LFSR_SLICE_DSIZE; +// +// // update our estimate +// estimate -= (lfsr_data_size(&slice_) +// - lfsr_data_size(&slice__)); +// } +// +// // carve block pointer? +// } else if (lfsr_tag_key(tag_) == LFSR_TAG_BLOCK) { +// // TODO +// // TODO note this needs to handle both siblings being +// // larger than crystallize size (force one to crystallize) +// LFS_ASSERT(false); +// } +// +// pos_ = bid_+1; +// } +// +// // found fully overwritten data? +// if (pos <= bid_-(weight_-1) && pos + weight >= bid_+1) { +// // combine all rms into a single attr +// rm += weight_; +// +// // update estimate +// estimate -= lfsr_data_size(&slice_); +// // include any removed holes +// if (lfsr_data_size(&slice_) < weight_) { +// estimate -= LFSR_ATTR_ESTIMATE; +// } +// +// pos_ = bid_+1; +// } +// } +// +// // write the combined rm attribute +// if (rm > 0) { +// attrs[attr_count++] = LFSR_ATTR(pos+rm-1, +// TAG(lfsr_tag_mode(tag) +// | LFSR_TAG_RM), -rm, +// NULL); +// } +// +// // TODO make sure data-less holes get coalesced, though not we don't +// // accurately track coalesced holes with our estimate because it simplifies +// // things... document? +// +// // there's a few cases we need to append an extra tag: we're appended data +// // past the end of our btree and need a hole, or we're creating a new +// // btree and didn't carve the existing data +// if (!lfsr_tree_hasbtree(tree) +// && lfsr_tree_size(tree) > 0 +// && (pos >= lfsr_tree_size(tree) +// || pos + weight == 0)) { +// // note we never grow and rm a btree at the same time +// LFS_ASSERT(rm == 0); +// +// attrs[attr_count++] = LFSR_ATTR(0, +// TAG(lfsr_tag_mode(tag) +// | LFSR_TAG_DATA), +lfs_max32(pos, lfsr_tree_size(tree)), +// DATA(*(const lfsr_data_t*)tree)); +// +// // update our estimate +// estimate += lfsr_data_size((const lfsr_data_t*)tree); +// // we may be making a hole here +// if (pos > lfsr_tree_size(tree)) { +// estimate += LFSR_ATTR_ESTIMATE; +// } +// +// // need a hole? +// } else if (pos > lfsr_tree_size(tree)) { +// // note we never grow and rm a btree at the same time +// LFS_ASSERT(rm == 0); +// +// attrs[attr_count++] = LFSR_ATTR(lfsr_tree_size(tree), +// TAG(lfsr_tag_mode(tag) +// | LFSR_TAG_DATA), +(pos - lfsr_tree_size(tree)), +// NULL); +// +// // turns out making a hole, makes a hole +// estimate += LFSR_ATTR_ESTIMATE; +// } +// +// // finally append our data +// if (weight + delta > 0) { +// attrs[attr_count++] = LFSR_ATTR(pos, +// TAG(tag), +(weight + delta), DATA(data)); +// +// // update our estimate +// estimate += lfsr_data_size(&data); +// // include the overhead of any new holes +// if (lfsr_data_size(&data) < weight + delta) { +// estimate += LFSR_ATTR_ESTIMATE; +// } +// } +// +// LFS_ASSERT(attr_count <= LFSR_CARVE_ATTRS); +// LFS_ASSERT(buffer_off <= LFSR_CARVE_DSIZE); +// if (estimate_) { +// *estimate_ = estimate; +// } +// return attr_count; +//} + +// TODO can lfsr_file_carveshrub and lfsr_file_carvetree be combined somehow? +static int lfsr_file_carveshrub(lfs_t *lfs, lfsr_file_t *file, lfs_off_t pos, lfs_off_t weight, lfs_soff_t delta, - lfsr_tag_t tag, lfsr_data_t data, - lfs_soff_t *estimate_, - lfsr_attr_t attrs[static LFSR_CARVE_ATTRS], - uint8_t buffer[static LFSR_CARVE_DSIZE]) { - // TODO need size? + lfsr_tag_t tag, lfsr_data_t data) { + // only inlined data is supported in shrubs + LFS_ASSERT(tag == LFSR_TAG_SHRUB(DATA)); // we should never try to shove more data than can fit in a given weight LFS_ASSERT(lfsr_data_size(&data) <= weight + delta); // allocate attrs/buffer as necessary + lfsr_attr_t attrs[4]; lfs_size_t attr_count = 0; - lfs_size_t buffer_off = 0; // keeep track of removed data, we combined this into a single attr, // and keep track of how our changes will affect our size estimate lfs_off_t rm = 0; - lfs_soff_t estimate = 0; + // TODO just use another variable in file for shrub estimate? I don't + // think unionizing this variable gets us anything + lfs_off_t estimate = (lfsr_shrub_hasshrub(&file->shrub)) + ? file->shrub.u.shrub.estimate + : 0; + + // do we need to add sprout data to our shrub? + // + // we only do this here if we aren't carving + if (lfsr_shrub_hassprout(&file->shrub) + && (pos >= lfsr_shrub_size(&file->shrub) + || pos + weight == 0)) { + attrs[attr_count++] = LFSR_ATTR(0, + SHRUB(DATA), +lfsr_shrub_size(&file->shrub), + DATA(file->shrub.u.data)); + + // update our estimate + estimate += LFSR_ATTR_ESTIMATE + lfsr_shrub_size(&file->shrub); + } // try to carve any existing data lfs_off_t pos_ = pos; - while (pos_ < lfs_min32(pos+weight, lfsr_tree_size(tree))) { - lfsr_bid_t bid_; + while (pos_ < lfs_min32(pos+weight, lfsr_shrub_size(&file->shrub))) { + lfsr_srid_t rid_; lfsr_tag_t tag_; - lfsr_bid_t weight_; + lfsr_rid_t weight_; lfsr_data_t data_; - // the tree may be aliased to either a sprout or block pointer if - // we're creating a new btree, assume sprout if we're a shrub, - // otherwise a block pointer - if (!lfsr_tree_hasbtree(tree) && lfsr_tag_isshrub(tag)) { - const lfsr_data_t *slice = (const lfsr_data_t*)tree; - bid_ = lfsr_data_size(slice)-1; - tag_ = tag; - weight_ = lfsr_data_size(slice); - data_ = *slice; - } else if (!lfsr_tree_hasbtree(tree)) { - // TODO - LFS_ASSERT(false); + // sprout or shrub? + if (!lfsr_shrub_hasshrub(&file->shrub)) { + rid_ = lfsr_data_size(&file->shrub.u.data)-1; + tag_ = LFSR_TAG_DATA; + weight_ = lfsr_data_size(&file->shrub.u.data); + data_ = file->shrub.u.data; } else { - int err = lfsr_btree_lookupnext(lfs, &tree->u.btree, pos_, - &bid_, &tag_, &weight_, &data_); + int err = lfsr_rbyd_lookupnext(lfs, &file->shrub.u.rbyd, + pos_, 0, + &rid_, &tag_, &weight_, &data_); if (err) { LFS_ASSERT(err != LFS_ERR_NOENT); return err; } } - LFS_ASSERT(lfsr_tag_key(tag_) == LFSR_TAG_DATA - || lfsr_tag_key(tag_) == LFSR_TAG_SLICE - || lfsr_tag_key(tag_) == LFSR_TAG_BLOCK); - - // go ahead and dereference to get to the actual data - lfsr_data_t slice_; - lfsr_bptr_t bptr_; - int err = lfsr_data_deref(lfs, tag_, data_, &slice_, &bptr_); - if (err) { - return err; - } - LFS_ASSERT(lfsr_data_size(&slice_) <= weight_); - -// // get the physical data size -// lfs_size_t size_; -// err = lfsr_data_derefsize(lfs, tag_, data_, &size_); -// if (err) { -// return err; -// } -// LFS_ASSERT(size_ <= weight_); + LFS_ASSERT(lfsr_tag_key(tag_) == LFSR_TAG_DATA); // note an entry can be both a left and right sibling! + // TODO! are we coalescing holes? + // found left sibling? - if (pos > bid_-(weight_-1)) { - lfs_off_t overlap_ = (bid_+1) - pos; + if (pos > rid_-(weight_-1)) { + lfs_off_t overlap_ = (rid_+1) - pos; LFS_ASSERT((lfs_soff_t)overlap_ >= 0); - // carve in-rbyd data? - if (lfsr_tag_key(tag_) == LFSR_TAG_DATA - || lfsr_tag_key(tag_) == LFSR_TAG_SLICE) { - lfsr_data_t slice__ = LFSR_DATA_DISK( - slice_.u.disk.block, - slice_.u.disk.off, - lfs_min32( - weight_ - overlap_, - lfsr_data_size(&slice_))); + // carve + lfsr_data_t data__ = LFSR_DATA_DISK( + data_.u.disk.block, + data_.u.disk.off, + lfs_min32( + weight_ - overlap_, + lfsr_data_size(&data_))); - // some special cases require a new tag: if we're creating a - // new btree - if (!lfsr_tree_hasbtree(tree)) { - attrs[attr_count++] = LFSR_ATTR(0, - TAG(lfsr_tag_mode(tag) - | LFSR_TAG_SLICE), +(weight_ - overlap_), - FROMSLICE(&slice__, &buffer[buffer_off])); - buffer += LFSR_SLICE_DSIZE; + // some special cases require a new tag: if we're creating a + // new shrub + if (!lfsr_shrub_hasshrub(&file->shrub)) { + attrs[attr_count++] = LFSR_ATTR(0, + SHRUB(DATA), +(weight_ - overlap_), + DATA(data__)); - // update our estimate - estimate += lfsr_data_size(&slice__); + // update our estimate + estimate += LFSR_ATTR_ESTIMATE + lfsr_data_size(&data__); - } else { - attrs[attr_count++] = LFSR_ATTR(bid_, - TAG(lfsr_tag_mode(tag) - | LFSR_TAG_GROW(WIDE(SLICE))), -overlap_, - FROMSLICE(&slice__, &buffer[buffer_off])); - buffer += LFSR_SLICE_DSIZE; + // we can get away with a grow attribute in some cases, avoiding + // a data copy + } else if (lfsr_data_size(&data_) == lfsr_data_size(&data__)) { + attrs[attr_count++] = LFSR_ATTR(rid_, + SHRUB(GROW), -overlap_, + NULL); - // update our estimate - estimate -= (lfsr_data_size(&slice_) - - lfsr_data_size(&slice__)); - // we may remove a hole here - if (lfsr_data_size(&slice_) < weight_ - && lfsr_data_size(&slice__) == weight_ - overlap_) { - estimate -= LFSR_ATTR_ESTIMATE; - } - } + } else { + attrs[attr_count++] = LFSR_ATTR(rid_, + SHRUB(GROW(WIDE(DATA))), -overlap_, + DATA(data__)); - // carve block pointer? - } else if (lfsr_tag_key(tag_) == LFSR_TAG_BLOCK) { - // TODO - LFS_ASSERT(false); -// lfsr_bptr_t bptr_; -// int err = lfsr_data_readbptr(lfs, &data_, &bptr_); -// if (err) { -// return err; -// } -// LFS_ASSERT(bptr_.size <= weight_); -// -// // are we carving the block too small? -// if (bptr_.size <= lfs->cfg->crystallize_size) { -// attrs[attr_count++] = LFSR_ATTR(bid_, -// GROW(), -overlap_, DATA( -// data_.u.disk.block, -// data_.u.disk.off, -// weight_ - overlap_})); -// } else { -// attrs[attr_count++] = LFSR_ATTR(bid_, -// GROW(), -overlap_, FROMBPTR( -// (&(lfsr_bptr_t){ -// .block=data_.u.disk.block, -// .off=data_.u.disk.off, -// .size=weight_ - overlap_}), -// &buffer[buffer_off])); -// buffer += LFSR_BPTR_DSIZE; -// } + // update our estimate + estimate -= lfsr_data_size(&data_) - lfsr_data_size(&data__); } - - // TODO wait is this the same on all branches? - pos_ = bid_+1; } - // found right sibling? - if (pos + weight < bid_+1) { - lfs_off_t overlap_ = (pos + weight) - (bid_-(weight_-1)); + // found right sibling? carve + if (pos + weight < (lfs_off_t)rid_+1) { + lfs_off_t overlap_ = (pos + weight) - (rid_-(weight_-1)); LFS_ASSERT((lfs_soff_t)overlap_ >= 0); // carve in-rbyd data? - if (lfsr_tag_key(tag_) == LFSR_TAG_DATA - || lfsr_tag_key(tag_) == LFSR_TAG_SLICE) { - lfsr_data_t slice__ = LFSR_DATA_DISK( - slice_.u.disk.block, - slice_.u.disk.off + lfs_min32( - overlap_, - lfsr_data_size(&slice_)), - lfsr_data_size(&slice_) - lfs_min32( - overlap_, - lfsr_data_size(&slice_))); + lfsr_data_t data__ = LFSR_DATA_DISK( + data_.u.disk.block, + data_.u.disk.off + lfs_min32( + overlap_, + lfsr_data_size(&data_)), + lfsr_data_size(&data_) - lfs_min32( + overlap_, + lfsr_data_size(&data_))); - // some special cases require a new tag: if we're creating a - // new btree, or if left/right siblings are the same - if (!lfsr_tree_hasbtree(tree) || overlap_ > weight) { - attrs[attr_count++] = LFSR_ATTR(pos, - TAG(lfsr_tag_mode(tag) - | LFSR_TAG_SLICE), +(weight_ - overlap_), - FROMSLICE(&slice__, &buffer[buffer_off])); - buffer += LFSR_SLICE_DSIZE; + // can we coalesce a hole? + if (lfsr_data_size(&data__) == 0) { + delta += rid_+1 - (pos + weight); + + // add to rm if left/right siblings aren't the same + if (overlap_ <= weight) { + rm += weight_; // update our estimate - estimate += lfsr_data_size(&slice__); - // if we split, we may actually create a hole - if (lfsr_data_size(&slice__) < weight_ - overlap_) { - estimate += LFSR_ATTR_ESTIMATE; - } - - } else { - // we need to account for changes to left sibling here - attrs[attr_count++] = LFSR_ATTR(pos+rm+weight_-1, - TAG(lfsr_tag_mode(tag) - | LFSR_TAG_GROW(WIDE(SLICE))), -overlap_, - FROMSLICE(&slice__, &buffer[buffer_off])); - buffer += LFSR_SLICE_DSIZE; - - // update our estimate - estimate -= (lfsr_data_size(&slice_) - - lfsr_data_size(&slice__)); + estimate -= LFSR_ATTR_ESTIMATE + lfsr_data_size(&data_); } - // carve block pointer? - } else if (lfsr_tag_key(tag_) == LFSR_TAG_BLOCK) { - // TODO - // TODO note this needs to handle both siblings being - // larger than crystallize size (force one to crystallize) - LFS_ASSERT(false); - } + // some special cases require a new tag: if we're creating a + // new btree, or if left/right siblings are the same + } else if (!lfsr_shrub_hasshrub(&file->shrub) + || overlap_ > weight) { + attrs[attr_count++] = LFSR_ATTR(pos, + SHRUB(DATA), +(weight_ - overlap_), + DATA(data__)); - pos_ = bid_+1; + // update our estimate + estimate += LFSR_ATTR_ESTIMATE + lfsr_data_size(&data__); + + } else { + // we need to account for changes to left sibling here + attrs[attr_count++] = LFSR_ATTR(pos+rm+weight_-1, + SHRUB(GROW(WIDE(DATA))), -overlap_, + DATA(data__)); + + // update our estimate + estimate -= lfsr_data_size(&data_) - lfsr_data_size(&data__); + } } // found fully overwritten data? - if (pos <= bid_-(weight_-1) && pos + weight >= bid_+1) { + if (pos <= rid_-(weight_-1) + && pos + weight >= (lfs_off_t)rid_+1 + && lfsr_shrub_hasshrub(&file->shrub)) { // combine all rms into a single attr rm += weight_; // update estimate - estimate -= lfsr_data_size(&slice_); - // include any removed holes - if (lfsr_data_size(&slice_) < weight_) { - estimate -= LFSR_ATTR_ESTIMATE; - } - - pos_ = bid_+1; + estimate -= LFSR_ATTR_ESTIMATE + lfsr_data_size(&data_); } + + pos_ = rid_+1; } + // TODO can grow tags span multiple rids? if so can we merge + // the rm attribute with the data tag? maybe also use this for + // cheaper hole coalescing? + // // write the combined rm attribute if (rm > 0) { attrs[attr_count++] = LFSR_ATTR(pos+rm-1, - TAG(lfsr_tag_mode(tag) - | LFSR_TAG_RM), -rm, - NULL); + SHRUB(RM), -rm, NULL); } - // TODO make sure data-less holes get coalesced, though not we don't - // accurately track coalesced holes with our estimate because it simplifies - // things... document? + // need a hole? + if (pos > lfsr_shrub_size(&file->shrub) + // if we have no data we can coalesce our hole here + || (weight + delta > 0 && lfsr_data_size(&data) == 0)) { + lfs_off_t pos_ = lfs_min32(pos, lfsr_shrub_size(&file->shrub)); + lfs_off_t hole = pos - pos_ + + ((lfsr_data_size(&data) == 0) ? weight + delta : 0); - // there's a few cases we need to append an extra tag: we're appended data - // past the end of our btree and need a hole, or we're creating a new - // btree and didn't carve the existing data - if (!lfsr_tree_hasbtree(tree) - && lfsr_tree_size(tree) > 0 - && (pos >= lfsr_tree_size(tree) - || pos + weight == 0)) { - // note we never grow and rm a btree at the same time - LFS_ASSERT(rm == 0); + // we can usually get away with a simple grow attribute + if (pos_ > 0) { + attrs[attr_count++] = LFSR_ATTR(pos_-1, + SHRUB(GROW), +hole, NULL); + + } else { + attrs[attr_count++] = LFSR_ATTR(pos_, + SHRUB(DATA), +hole, NULL); - attrs[attr_count++] = LFSR_ATTR(0, - TAG(lfsr_tag_mode(tag) - | LFSR_TAG_DATA), +lfs_max32(pos, lfsr_tree_size(tree)), - DATA(*(const lfsr_data_t*)tree)); - - // update our estimate - estimate += lfsr_data_size((const lfsr_data_t*)tree); - // we may be making a hole here - if (pos > lfsr_tree_size(tree)) { + // update our estimate estimate += LFSR_ATTR_ESTIMATE; } - - // need a hole? - } else if (pos > lfsr_tree_size(tree)) { - // note we never grow and rm a btree at the same time - LFS_ASSERT(rm == 0); - - attrs[attr_count++] = LFSR_ATTR(lfsr_tree_size(tree), - TAG(lfsr_tag_mode(tag) - | LFSR_TAG_DATA), +(pos - lfsr_tree_size(tree)), - NULL); - - // turns out making a hole, makes a hole - estimate += LFSR_ATTR_ESTIMATE; } // finally append our data - if (weight + delta > 0) { + if (weight + delta > 0 && lfsr_data_size(&data) != 0) { attrs[attr_count++] = LFSR_ATTR(pos, TAG(tag), +(weight + delta), DATA(data)); // update our estimate - estimate += lfsr_data_size(&data); - // include the overhead of any new holes - if (lfsr_data_size(&data) < weight + delta) { - estimate += LFSR_ATTR_ESTIMATE; - } + estimate += LFSR_ATTR_ESTIMATE + lfsr_data_size(&data); } - LFS_ASSERT(attr_count <= LFSR_CARVE_ATTRS); - LFS_ASSERT(buffer_off <= LFSR_CARVE_DSIZE); - if (estimate_) { - *estimate_ = estimate; - } - return attr_count; -} - -static int lfsr_file_carveshrub(lfs_t *lfs, lfsr_file_t *file, - lfs_off_t pos, lfs_off_t weight, lfs_soff_t delta, - lfsr_tag_t tag, lfsr_data_t data) { - // we can pretend our shrub is a small btree to save a bit of code, we - // just need to commit the generated attributes through lfsr_mdir_commit - lfsr_attr_t attrs[LFSR_CARVE_ATTRS]; - uint8_t attr_buf[LFSR_CARVE_DSIZE]; - lfs_soff_t estimate; - lfs_ssize_t attr_count = lfsr_tree_buildcarve(lfs, - (const lfsr_tree_t*)&file->shrub.u.rbyd, - pos, weight, delta, - tag, data, - &estimate, - attrs, attr_buf); - if (attr_count < 0) { - return attr_count; - } + // we didn't overflow our attr buffer did we? + LFS_ASSERT(attr_count <= 4); // do our attributes push us past our inline_size? we need to flush // our shrub then - LFS_ASSERT(estimate >= -(lfs_soff_t)file->shrub.u.shrub.estimate); - if (file->shrub.u.shrub.estimate + estimate > lfs->cfg->inline_size) { + LFS_ASSERT((lfs_soff_t)estimate >= 0); + if (estimate > lfs->cfg->inline_size) { // TODO // return LFS_ERR_RANGE; } @@ -9778,7 +9971,146 @@ static int lfsr_file_carveshrub(lfs_t *lfs, lfsr_file_t *file, } // update our estimate - file->shrub.u.shrub.estimate += estimate; + file->shrub.u.shrub.estimate = estimate; + return 0; +} + +// TODO this should also take in extra data during writes +static int lfsr_file_flushbuffer(lfs_t *lfs, lfsr_file_t *file) { + // Two things we need to do: + // + // 1. Try to coalesce with neighboring data + // if it fits in our fragment size. + // + // 2. Fragment writes into our fragment size. + // + lfs_size_t i = 0; + while (i < file->buffer_size) { + // truncate to our fragment size + lfs_off_t pos = file->buffer_pos + i; + lfsr_data_t data = LFSR_DATA_BUF( + file->buffer + i, + lfs_min32( + file->buffer_size - i, + lfs->cfg->fragment_size)); + lfs_off_t d = lfsr_data_size(&data); + + lfsr_data_t datas[3]; + lfs_size_t data_count = 0; + datas[data_count++] = data; + + // do we have a left neighbor? + if (pos > 0 && lfsr_shrub_size(&file->shrub) >= pos + // don't bother to lookup left after first fragment + && i == 0) { + // TODO should these shrub lookups be deduplicated? + // lfsr_shrub_lookupnext? + lfsr_srid_t rid_; + lfsr_tag_t tag_; + lfsr_rid_t weight_; + lfsr_data_t data_; + // sprout or shrub? + if (!lfsr_shrub_hasshrub(&file->shrub)) { + rid_ = lfsr_data_size(&file->shrub.u.data)-1; + tag_ = LFSR_TAG_DATA; + weight_ = lfsr_data_size(&file->shrub.u.data); + data_ = file->shrub.u.data; + } else { + int err = lfsr_rbyd_lookupnext(lfs, &file->shrub.u.rbyd, + pos-1, 0, + &rid_, &tag_, &weight_, &data_); + if (err) { + LFS_ASSERT(err != LFS_ERR_NOENT); + return err; + } + } + LFS_ASSERT(lfsr_tag_key(tag_) == LFSR_TAG_DATA); + + // can we coalesce? + if (rid_-(weight_-1) + lfsr_data_size(&data_) >= pos + && lfsr_data_size(&data_) < lfs->cfg->fragment_size) { + // TODO this is a bit of a hacky way to prepend data... + // coalesce, but truncate to our fragment size + LFS_ASSERT(data_count == 1); + datas[0] = LFSR_DATA_DISK( + data_.u.disk.block, + data_.u.disk.off, + pos - (rid_-(weight_-1))); + datas[1] = LFSR_DATA_BUF( + file->buffer + i, + lfs_min32( + file->buffer_size - i, + lfs->cfg->fragment_size - lfsr_data_size(&data_))); + data_count = 2; + data = lfsr_data_fromcat(datas, data_count); + + pos = rid_-(weight_-1); + d = lfsr_data_size(&datas[1]); + } + } + + // do we have a right neighbor? + // + // note this may the same as our left neighbor + if (pos + lfsr_data_size(&data) < lfsr_shrub_size(&file->shrub) + // don't bother to lookup right if fragment is already full + && lfsr_data_size(&data) < lfs->cfg->fragment_size) { + // TODO should these shrub lookups be deduplicated? + // lfsr_shrub_lookupnext? + lfsr_srid_t rid_; + lfsr_tag_t tag_; + lfsr_rid_t weight_; + lfsr_data_t data_; + // sprout or shrub? + if (!lfsr_shrub_hasshrub(&file->shrub)) { + rid_ = lfsr_data_size(&file->shrub.u.data)-1; + tag_ = LFSR_TAG_DATA; + weight_ = lfsr_data_size(&file->shrub.u.data); + data_ = file->shrub.u.data; + } else { + int err = lfsr_rbyd_lookupnext(lfs, &file->shrub.u.rbyd, + pos+lfsr_data_size(&data), 0, + &rid_, &tag_, &weight_, &data_); + if (err) { + LFS_ASSERT(err != LFS_ERR_NOENT); + return err; + } + } + LFS_ASSERT(lfsr_tag_key(tag_) == LFSR_TAG_DATA); + + // can we coalesce? + if (pos+lfsr_data_size(&data) + < rid_-(weight_-1) + lfsr_data_size(&data_) + && lfsr_data_size(&data) + lfsr_data_size(&data_) + - (pos+lfsr_data_size(&data) - (rid_-(weight_-1))) + <= lfs->cfg->fragment_size) { + datas[data_count++] = LFSR_DATA_DISK( + data_.u.disk.block, + data_.u.disk.off + + (pos+lfsr_data_size(&data) - (rid_-(weight_-1))), + lfsr_data_size(&data_) + - (pos+lfsr_data_size(&data) - (rid_-(weight_-1)))); + data = lfsr_data_fromcat(datas, data_count); + } + } + + // make sure we didn't overflow our data buffer + LFS_ASSERT(data_count <= 3); + + // once we've figured out what fragment to write, carve it into + // our shrub + int err = lfsr_file_carveshrub(lfs, file, + pos, lfsr_data_size(&data), 0, + LFSR_TAG_SHRUB(DATA), data); + if (err) { + return err; + } + + // and update our buffer + i += d; + } + + file->buffer_size = 0; return 0; } @@ -10708,34 +11040,34 @@ static int lfsr_file_flushshrub(lfs_t *lfs, lfsr_file_t *file) { return 0; } -static int lfsr_file_flushbuffer(lfs_t *lfs, lfsr_file_t *file) { - // TODO restructure this based on how flushshrub interacts with - // our buffer? - while (file->buffer_size > 0) { - // try to flush our buffer into our shrub - int err = lfsr_file_carveshrub(lfs, file, - file->buffer_pos, file->buffer_size, 0, - LFSR_TAG_SHRUB(DATA), - LFSR_DATA_BUF(file->buffer, file->buffer_size)); - if (err) { - // uh oh, doesn't fit? flush inlined data and try again - if (err == LFS_ERR_RANGE) { - err = lfsr_file_flushshrub(lfs, file); - if (err) { - return err; - } - - continue; - } - return err; - } - - // we've flushed our buffer - file->buffer_size = 0; - } - - return 0; -} +//static int lfsr_file_flushbuffer(lfs_t *lfs, lfsr_file_t *file) { +// // TODO restructure this based on how flushshrub interacts with +// // our buffer? +// while (file->buffer_size > 0) { +// // try to flush our buffer into our shrub +// int err = lfsr_file_carveshrub(lfs, file, +// file->buffer_pos, file->buffer_size, 0, +// LFSR_TAG_SHRUB(DATA), +// LFSR_DATA_BUF(file->buffer, file->buffer_size)); +// if (err) { +// // uh oh, doesn't fit? flush inlined data and try again +// if (err == LFS_ERR_RANGE) { +// err = lfsr_file_flushshrub(lfs, file); +// if (err) { +// return err; +// } +// +// continue; +// } +// return err; +// } +// +// // we've flushed our buffer +// file->buffer_size = 0; +// } +// +// return 0; +//} lfs_ssize_t lfsr_file_write(lfs_t *lfs, lfsr_file_t *file, const void *buffer, lfs_size_t size) { diff --git a/lfs.h b/lfs.h index 262bafc8..5409fd6f 100644 --- a/lfs.h +++ b/lfs.h @@ -279,7 +279,7 @@ struct lfs_config { // TODO document lfs_size_t inline_size; - lfs_size_t coalesce_size; + lfs_size_t fragment_size; lfs_size_t crystallize_size; }; diff --git a/runners/bench_runner.h b/runners/bench_runner.h index 8b34d775..a1c2c28c 100644 --- a/runners/bench_runner.h +++ b/runners/bench_runner.h @@ -108,7 +108,7 @@ intmax_t bench_define(size_t define); #define DISK_SIZE_i 4 #define CACHE_SIZE_i 5 #define INLINE_SIZE_i 6 -#define COALESCE_SIZE_i 7 +#define FRAGMENT_SIZE_i 7 #define CRYSTALLIZE_SIZE_i 8 #define LOOKAHEAD_SIZE_i 9 #define BLOCK_CYCLES_i 10 @@ -124,7 +124,7 @@ intmax_t bench_define(size_t define); #define DISK_SIZE bench_define(DISK_SIZE_i) #define CACHE_SIZE bench_define(CACHE_SIZE_i) #define INLINE_SIZE bench_define(INLINE_SIZE_i) -#define COALESCE_SIZE bench_define(COALESCE_SIZE_i) +#define FRAGMENT_SIZE bench_define(FRAGMENT_SIZE_i) #define CRYSTALLIZE_SIZE bench_define(CRYSTALLIZE_SIZE_i) #define LOOKAHEAD_SIZE bench_define(LOOKAHEAD_SIZE_i) #define BLOCK_CYCLES bench_define(BLOCK_CYCLES_i) @@ -142,7 +142,7 @@ intmax_t bench_define(size_t define); BENCH_DEF(DISK_SIZE, 1024*1024 ) \ BENCH_DEF(CACHE_SIZE, lfs_max(16, lfs_max(READ_SIZE, PROG_SIZE))) \ BENCH_DEF(INLINE_SIZE, BLOCK_SIZE/8 ) \ - BENCH_DEF(COALESCE_SIZE, CACHE_SIZE ) \ + BENCH_DEF(FRAGMENT_SIZE, CACHE_SIZE ) \ BENCH_DEF(CRYSTALLIZE_SIZE, BLOCK_SIZE/4 ) \ BENCH_DEF(LOOKAHEAD_SIZE, 16 ) \ BENCH_DEF(BLOCK_CYCLES, -1 ) \ @@ -167,7 +167,7 @@ intmax_t bench_define(size_t define); .block_cycles = BLOCK_CYCLES, \ .cache_size = CACHE_SIZE, \ .inline_size = INLINE_SIZE, \ - .coalesce_size = COALESCE_SIZE, \ + .fragment_size = FRAGMENT_SIZE, \ .crystallize_size = CRYSTALLIZE_SIZE, \ .lookahead_size = LOOKAHEAD_SIZE, diff --git a/runners/test_runner.h b/runners/test_runner.h index 015eb4c9..89cc37c5 100644 --- a/runners/test_runner.h +++ b/runners/test_runner.h @@ -108,7 +108,7 @@ intmax_t test_define(size_t define); #define DISK_SIZE_i 4 #define CACHE_SIZE_i 5 #define INLINE_SIZE_i 6 -#define COALESCE_SIZE_i 7 +#define FRAGMENT_SIZE_i 7 #define CRYSTALLIZE_SIZE_i 8 #define LOOKAHEAD_SIZE_i 9 #define BLOCK_CYCLES_i 10 @@ -124,7 +124,7 @@ intmax_t test_define(size_t define); #define DISK_SIZE TEST_DEFINE(DISK_SIZE_i) #define CACHE_SIZE TEST_DEFINE(CACHE_SIZE_i) #define INLINE_SIZE TEST_DEFINE(INLINE_SIZE_i) -#define COALESCE_SIZE TEST_DEFINE(COALESCE_SIZE_i) +#define FRAGMENT_SIZE TEST_DEFINE(FRAGMENT_SIZE_i) #define CRYSTALLIZE_SIZE TEST_DEFINE(CRYSTALLIZE_SIZE_i) #define LOOKAHEAD_SIZE TEST_DEFINE(LOOKAHEAD_SIZE_i) #define BLOCK_CYCLES TEST_DEFINE(BLOCK_CYCLES_i) @@ -142,7 +142,7 @@ intmax_t test_define(size_t define); TEST_DEF(DISK_SIZE, 1024*1024 ) \ TEST_DEF(CACHE_SIZE, lfs_max(16, lfs_max(READ_SIZE, PROG_SIZE)) ) \ TEST_DEF(INLINE_SIZE, BLOCK_SIZE/8 ) \ - TEST_DEF(COALESCE_SIZE, CACHE_SIZE ) \ + TEST_DEF(FRAGMENT_SIZE, CACHE_SIZE ) \ TEST_DEF(CRYSTALLIZE_SIZE, BLOCK_SIZE/4 ) \ TEST_DEF(LOOKAHEAD_SIZE, 16 ) \ TEST_DEF(BLOCK_CYCLES, -1 ) \ @@ -167,7 +167,7 @@ intmax_t test_define(size_t define); .block_cycles = BLOCK_CYCLES, \ .cache_size = CACHE_SIZE, \ .inline_size = INLINE_SIZE, \ - .coalesce_size = COALESCE_SIZE, \ + .fragment_size = FRAGMENT_SIZE, \ .crystallize_size = CRYSTALLIZE_SIZE, \ .lookahead_size = LOOKAHEAD_SIZE, diff --git a/tests/test_files.toml b/tests/test_files.toml index 9bbb2d0c..07158374 100644 --- a/tests/test_files.toml +++ b/tests/test_files.toml @@ -2,7 +2,7 @@ after = ['test_dtree', 'test_btree'] # test both with and without coalescing -defines.COALESCE_SIZE = ['0', 'CACHE_SIZE'] +defines.FRAGMENT_SIZE = ['1', 'CACHE_SIZE'] # test creation/deletion