From ebddafd66f646265f2eeab89c597991ebf8c74bd Mon Sep 17 00:00:00 2001 From: Christopher Haster Date: Sun, 7 Jan 2024 00:24:58 -0600 Subject: [PATCH] Dropped file-level idempotent errors This is a big compromise in robustness in the face of errors, vs code size and stack size. To be clear, errors here refer to runtime errors, such as ENOSPC, EIO, etc, not on-disk errors, though disk errors could result in a runtime error if unrecoverable. The question is what happens to the filesystem after it reports one of these errors to the user. Operations are necessarily interrupted, so some in-device state may be lost. The way I see it, there are three options, increasing in robustness, but also increasing in code/stack cost: 1. Leave the filesystem in an inconsistent state, require an unmount+mount cycle to continue using the filesystem. 2. Use on-stack copies to prevent corrupted state until disk commits complete. This does not protect against intermediary states during file operations. 3. Use on-stack copies and file snapshots to fully revert any failed filesystem operation. My initial thought, since this is supposed to be a robust filesystem, was that we should try 3., fully revert any failed filesystem operation. With some stack tradeoff, this isn't too much of a problem, until we get to files. Files present some real problems: 1. File bshrubs need to be tracked in order to be compacted correctly. This means our on-stack copies need to be tracked, which complicates things a bit. 2. Bshrub estimates need to conservatively include all snapshots to avoid compaction issues. This means if we are tracking on-stack copies, we are effectively multiplying bshrub cost by ~3x vs ~2x. 3. We only have one file buffer. Being able to revert buffer updates would require either unecessary disk flushes and some weird mechanism to handle small files, or ~2x the RAM cost. See the previous commit for more info on this. These issues can _probably_ be worked around, with some tradeoffs, but I think the writing is on the wall. Full reverts on errors just isn't worth the cost for littlefs's use case. With the snapshotting features of littlefs, it shouldn't be too hard to still handle errors gracefully in littlefs, either by keeping two file handles around, or reopening the file after an error. This adds cost at the user-level, but consider that the alternative is that all users pay roughly this cost at the filesystem-level. Maybe in the future we should additional LFS_GLASS/LFS_TEMPERED modes to provide all three of the above options? Let the user chose their robustness vs code/RAM tradeoff? --- To be clear, this change makes it so all filesystem operations are error-idempotent, with the exception of the _contents_ of files after an error. If an error occurs during a file operation, the contents of that file is undefined (but also desynced, so disk is unaffected). Code changes: code stack before: 33964 3080 after: 33286 (-2.0%) 2968 (-3.6%) With these changes, O_SYNC/O_FLUSH are also much cheaper to implement. We can see their specific costs, which, to be honest, is a bit more than I expected since these are now just a flag check and function call: code stack default: 33174 2944 O_SYNC: 33224 (+0.2%) 2968 (+0.8%) O_SYNC+O_FLUSH: 33286 (+0.3%) 2968 (+0.8%) --- lfs.c | 809 +++++++++++++++++++++++++--------------------------------- 1 file changed, 341 insertions(+), 468 deletions(-) diff --git a/lfs.c b/lfs.c index aed2baf9..0d44b0f8 100644 --- a/lfs.c +++ b/lfs.c @@ -654,8 +654,9 @@ enum lfsr_tag { LFSR_TAG_INTERNAL = 0x0800, LFSR_TAG_GRM = 0x0800, LFSR_TAG_MOVE = 0x0801, - LFSR_TAG_SHRUBCOMMIT = 0x0802, - LFSR_TAG_SHRUBTRUNK = 0x0803, + LFSR_TAG_SHRUBALLOC = 0x0802, + LFSR_TAG_SHRUBCOMMIT = 0x0803, + LFSR_TAG_SHRUBTRUNK = 0x0804, // some in-device only tag modifiers LFSR_TAG_RM = 0x8000, @@ -3959,6 +3960,7 @@ static int lfsr_btree_parent(lfs_t *lfs, const lfsr_btree_t *btree, // core btree algorithm +// this is atomic TODO not yet static int lfsr_btree_commit_(lfs_t *lfs, lfsr_btree_t *btree, bool shrub, lfsr_attr_t scratch_attrs[static 4], @@ -4001,17 +4003,16 @@ static int lfsr_btree_commit_(lfs_t *lfs, lfsr_srid_t rid = -1; // are we root? if (rbyd.blocks[0] == btree->blocks[0] || rbyd.trunk == 0) { - // new root? shrub root? yield creation of new roots to - // higher-level bshrub/btree logic - if (shrub || rbyd.trunk == 0) { - *btree = rbyd; + // new root? shrub root? yield the final root commit to + // higher-level btree/bshrub logic + if (rbyd.trunk == 0 || shrub) { if (attrs_) { *attrs_ = attrs; } if (attr_count_) { *attr_count_ = attr_count; } - return 0; + return (rbyd.trunk == 0) ? LFS_ERR_RANGE : 0; } // mark btree as unerased in case of failure, our btree rbyd and @@ -4467,6 +4468,7 @@ static int lfsr_btree_commit_(lfs_t *lfs, } } +// this is atomic static int lfsr_btree_commit(lfs_t *lfs, lfsr_btree_t *btree, const lfsr_attr_t *attrs, lfs_size_t attr_count) { // we need some scratch space for tail-recursive attrs @@ -4478,22 +4480,27 @@ static int lfsr_btree_commit(lfs_t *lfs, lfsr_btree_t *btree, scratch_attrs, scratch_buf, attrs, attr_count, &attrs, &attr_count); - if (err) { + if (err && err != LFS_ERR_RANGE) { return err; } // needs a new root? - if (attr_count > 0) { - err = lfsr_rbyd_alloc(lfs, btree); + if (err == LFS_ERR_RANGE) { + LFS_ASSERT(attr_count > 0); + + lfsr_rbyd_t rbyd; + err = lfsr_rbyd_alloc(lfs, &rbyd); if (err) { return err; } - err = lfsr_rbyd_commit(lfs, btree, attrs, attr_count); + err = lfsr_rbyd_commit(lfs, &rbyd, attrs, attr_count); if (err) { LFS_ASSERT(err != LFS_ERR_RANGE); return err; } + + *btree = rbyd; } LFS_ASSERT(btree->trunk != 0); @@ -5276,10 +5283,20 @@ static int lfsr_mdir_commit__(lfs_t *lfs, lfsr_mdir_t *mdir, // shrub tags append a set of attributes to an unrelated trunk // in our rbyd - } else if (attrs[i].tag == LFSR_TAG_SHRUBCOMMIT) { + } else if (attrs[i].tag == LFSR_TAG_SHRUBALLOC + || attrs[i].tag == LFSR_TAG_SHRUBCOMMIT) { const lfsr_bshrubcommit_t *bshrubcommit = (const lfsr_bshrubcommit_t*) attrs[i].data.u.buf.buffer; + // SHRUBALLOC is roughly the same as SHRUBCOMMIT but also + // resets the shrub, we need to do this here so bshrub root + // extensions are atomic + if (attrs[i].tag == LFSR_TAG_SHRUBALLOC) { + bshrubcommit->bshrub->rbyd_.blocks[0] = rbyd_.blocks[0]; + bshrubcommit->bshrub->rbyd_.trunk = 0; + bshrubcommit->bshrub->rbyd_.weight = 0; + } + int err = lfsr_bshrub_commit__(lfs, &rbyd_, bshrubcommit->bshrub, &bshrubcommit->bshrub->rbyd_.trunk, @@ -6885,8 +6902,8 @@ static int lfsr_bshrub_commit__(lfs_t *lfs, lfsr_rbyd_t *rbyd_, lfs_size_t *trunk_, lfsr_srid_t *weight_, const lfsr_attr_t *attrs, lfs_size_t attr_count) { // swap out our trunk/weight temporarily, note we're - // operating on a copy so if this fails not _too_ many - // things will get messed up + // operating on a copy so if this fails we shouldn't mess + // things up too much // // it is important that these rbyds share eoff/cksum/etc lfs_size_t trunk = rbyd_->trunk; @@ -7017,9 +7034,7 @@ static int lfsr_bshrub_lookup(lfs_t *lfs, // find a tight upper bound on the _full_ bshrub size, this includes // any on-disk bshrubs, and all pending bshrubs -static lfs_ssize_t lfsr_bshrub_estimate(lfs_t *lfs, - lfsr_mdir_t *mdir, const lfsr_bshrub_t *bshrub) { - (void)bshrub; +static lfs_ssize_t lfsr_bshrub_estimate(lfs_t *lfs, lfsr_mdir_t *mdir) { lfs_size_t estimate = 0; // include all unique sprouts/shrubs related to our file, @@ -7087,6 +7102,7 @@ static lfs_ssize_t lfsr_bshrub_estimate(lfs_t *lfs, return estimate; } +// this is atomic static int lfsr_bshrub_commit(lfs_t *lfs, lfsr_mdir_t *mdir, lfsr_bshrub_t *bshrub, const lfsr_attr_t *attrs, lfs_size_t attr_count) { @@ -7101,26 +7117,15 @@ static int lfsr_bshrub_commit(lfs_t *lfs, scratch_attrs, scratch_buf, attrs, attr_count, &attrs, &attr_count); - if (err) { + if (err && err != LFS_ERR_RANGE) { return err; } + LFS_ASSERT(!err || attr_count > 0); + bool alloc = (err == LFS_ERR_RANGE); // when btree is shrubbed, lfsr_btree_commit_ stops at the root // and returns with pending attrs - // - // note! lfsr_bshrub_isbshrub may have changed state due to collapsed - // parents, splits, etc - // if (attr_count > 0) { - // new bshrub? - if (bshrub->rbyd.trunk == 0) { - bshrub->rbyd.blocks[0] = mdir->rbyd.blocks[0]; - bshrub->rbyd.trunk = 0; - bshrub->rbyd.weight = 0; - // force estimate recalculation - bshrub->estimate = -1; - } - // we need to prevent our shrub from overflowing our mdir somehow // // maintaining an accurate estimate is tricky and error-prone, @@ -7142,7 +7147,7 @@ static int lfsr_bshrub_commit(lfs_t *lfs, } // avoid some overflow issues here - lfs_ssize_t estimate = bshrub->estimate; + lfs_ssize_t estimate = (alloc) ? (lfs_size_t)-1 : bshrub->estimate; if ((lfs_size_t)estimate <= lfs->cfg->shrub_size) { estimate += commit_estimate; } @@ -7150,11 +7155,12 @@ static int lfsr_bshrub_commit(lfs_t *lfs, // does our estimate exceed our shrub_size? need to recalculate an // accurate our estimate if ((lfs_size_t)estimate > lfs->cfg->shrub_size) { - estimate = lfsr_bshrub_estimate(lfs, mdir, bshrub); + estimate = lfsr_bshrub_estimate(lfs, mdir); if (estimate < 0) { return estimate; } + // TODO defer this? // don't forget to include our pending commit estimate += commit_estimate; @@ -7166,13 +7172,18 @@ static int lfsr_bshrub_commit(lfs_t *lfs, } // commit to shrub - err = lfsr_mdir_commit(lfs, mdir, LFSR_ATTRS( + int err = lfsr_mdir_commit(lfs, mdir, LFSR_ATTRS( LFSR_ATTR(mdir->mid, - SHRUBCOMMIT, 0, SHRUBCOMMIT( + TAG((alloc) + ? LFSR_TAG_SHRUBALLOC + : LFSR_TAG_SHRUBCOMMIT), 0, + SHRUBCOMMIT( bshrub, attrs, attr_count)))); if (err) { return err; } + // manually unstage in case we are a new shrub + bshrub->rbyd = bshrub->rbyd_; // update _all_ shrubs with the new estimate for (lfsr_openedmdir_t *opened = lfs->opened[ @@ -7186,7 +7197,9 @@ static int lfsr_bshrub_commit(lfs_t *lfs, } } } - LFS_ASSERT(bshrub->estimate == (lfs_size_t)estimate); + LFS_ASSERT(bshrub->estimate = (lfs_size_t)estimate); + + return 0; } LFS_ASSERT(bshrub->rbyd.trunk != 0); @@ -7194,31 +7207,34 @@ static int lfsr_bshrub_commit(lfs_t *lfs, evict:; // convert to btree - err = lfsr_rbyd_alloc(lfs, &bshrub->rbyd_); + lfsr_rbyd_t rbyd; + err = lfsr_rbyd_alloc(lfs, &rbyd); if (err) { return err; } - err = lfsr_rbyd_compact(lfs, &bshrub->rbyd_, -1, -1, - &bshrub->rbyd); - if (err) { - LFS_ASSERT(err != LFS_ERR_RANGE); - return err; + // note this may be a new root + if (!alloc) { + err = lfsr_rbyd_compact(lfs, &rbyd, -1, -1, + &bshrub->rbyd); + if (err) { + LFS_ASSERT(err != LFS_ERR_RANGE); + return err; + } } - err = lfsr_rbyd_appendattrs(lfs, &bshrub->rbyd_, -1, -1, + err = lfsr_rbyd_appendattrs(lfs, &rbyd, -1, -1, attrs, attr_count); if (err) { return err; } - err = lfsr_rbyd_appendcksum(lfs, &bshrub->rbyd_); + err = lfsr_rbyd_appendcksum(lfs, &rbyd); if (err) { return err; } - bshrub->rbyd = bshrub->rbyd_; - LFS_ASSERT(bshrub->rbyd.trunk != 0); + bshrub->rbyd = rbyd; return 0; } @@ -9704,47 +9720,40 @@ static int lfsr_ftree_carve(lfs_t *lfs, lfsr_ftree_t *ftree, // always convert to bshrub/btree when this function is called if (!lfsr_ftree_isbshruborbtree(ftree)) { - lfsr_attr_t attrs_[2]; - lfs_size_t attr_count_ = 0; - uint8_t buf[LFSR_BPTR_DSIZE+LFSR_ECKSUM_DSIZE]; - lfs_size_t buf_size = 0; - - // these also check if ftree is non-zero - if (lfsr_ftree_isbsprout(ftree)) { - attrs_[attr_count_++] = LFSR_ATTR(0, - DATA, +lfsr_ftree_size(ftree), - DATA(ftree->u.bsprout.data)); - - } else if (lfsr_ftree_isbleaf(ftree)) { - attrs_[attr_count_++] = LFSR_ATTR(0, - BLOCK, +lfsr_ftree_size(ftree), - FROMBPTR(&ftree->u.bptr, &buf[buf_size])); - buf_size += LFSR_BPTR_DSIZE; - - // append becksum? - if (ftree->u.bleaf.becksum.size != -1) { - attrs_[attr_count_++] = LFSR_ATTR(lfsr_ftree_size(ftree)-1, - BECKSUM, 0, - FROMECKSUM(&ftree->u.bleaf.becksum, &buf[buf_size])); - buf_size += LFSR_ECKSUM_DSIZE; - } - } - - ftree->u.bshrub.rbyd.blocks[0] = ftree->mdir.rbyd.blocks[0]; - ftree->u.bshrub.rbyd.trunk = 0; - ftree->u.bshrub.rbyd.weight = 0; + lfsr_bshrub_t bshrub; + bshrub.rbyd.blocks[0] = ftree->mdir.rbyd.blocks[0]; + bshrub.rbyd.trunk = 0; + bshrub.rbyd.weight = 0; // force estimate recalculation - ftree->u.bshrub.estimate = -1; + bshrub.estimate = -1; - if (attr_count_ > 0) { - LFS_ASSERT(attr_count_ <= sizeof(attrs_)/sizeof(lfsr_attr_t)); - LFS_ASSERT(buf_size <= sizeof(buf)); - int err = lfsr_bshrub_commit(lfs, &ftree->mdir, &ftree->u.bshrub, - attrs_, attr_count_); + if (lfsr_ftree_size(ftree) > 0) { + // note only new bshrubs can be committed without tracking, since + // they have no state that needs to be compacted + uint8_t bptr_buf[LFSR_BPTR_DSIZE]; + uint8_t becksum_buf[LFSR_ECKSUM_DSIZE]; + int err = lfsr_bshrub_commit(lfs, &ftree->mdir, &bshrub, + LFSR_ATTRS( + (lfsr_ftree_isbsprout(ftree)) + ? LFSR_ATTR(0, + DATA, +lfsr_ftree_size(ftree), + DATA(ftree->u.bsprout.data)) + : LFSR_ATTR(0, + BLOCK, +lfsr_ftree_size(ftree), + FROMBPTR(&ftree->u.bptr, bptr_buf)), + (lfsr_ftree_isbleaf(ftree) + && ftree->u.bleaf.becksum.size != -1) + ? LFSR_ATTR(lfsr_ftree_size(ftree)-1, + BECKSUM, 0, + FROMECKSUM(&ftree->u.bleaf.becksum, + becksum_buf)) + : LFSR_ATTR_NOOP())); if (err) { return err; } } + + ftree->u.bshrub = bshrub; } // TODO adopt this pattern for other scratch attrs @@ -10483,80 +10492,6 @@ static int lfsr_ftree_flush(lfs_t *lfs, lfsr_ftree_t *ftree, return 0; } -static int lfsr_ftree_sync(lfs_t *lfs, lfsr_ftree_t *ftree, uint32_t flags, - lfs_off_t buffer_pos, const uint8_t *buffer, lfs_size_t buffer_size) { - // note because of small-file caching and our current write - // strategy, we never actually end up with only a direct data - // or bptr - // - // this is convenient because bptrs are a bit annoying to commit - LFS_ASSERT(!lfsr_ftree_isbsprout(ftree)); - LFS_ASSERT(!lfsr_ftree_isbleaf(ftree)); - // small files should start as zero, const prop should optimize this out - LFS_ASSERT(!lfsr_f_isunflushed(flags) || buffer_pos == 0); - // small files/ftree should be exclusive here - LFS_ASSERT(!lfsr_f_isunflushed(flags) || lfsr_ftree_size(ftree) == 0); - // small files must be inlined entirely in our buffer - LFS_ASSERT(!lfsr_f_isunflushed(flags) - || (buffer_size <= lfs->cfg->cache_size - && buffer_size <= lfs->cfg->inline_size - && buffer_size <= lfs->cfg->fragment_size)); - - // don't write to disk if disk is already in-sync - if (lfsr_f_isunsynced(flags)) { - // commit our file's metadata - uint8_t buf[LFSR_BTREE_DSIZE]; - int err = lfsr_mdir_commit(lfs, &ftree->mdir, LFSR_ATTRS( - (lfsr_f_isunflushed(flags) && buffer_size == 0) - ? LFSR_ATTR(ftree->mdir.mid, - WIDE(RM(STRUCT)), 0, - NULL()) - : (lfsr_f_isunflushed(flags)) - ? LFSR_ATTR(ftree->mdir.mid, - WIDE(DATA), 0, - BUF(buffer, buffer_size)) - : (lfsr_ftree_isbshrub(ftree)) - ? LFSR_ATTR(ftree->mdir.mid, - WIDE(SHRUBTRUNK), 0, - SHRUBTRUNK(&ftree->u.bshrub)) - : LFSR_ATTR(ftree->mdir.mid, - WIDE(BTREE), 0, - FROMBTREE(&ftree->u.btree, buf)))); - if (err) { - return err; - } - } - - // but do update other file handles - for (lfsr_openedmdir_t *opened = lfs->opened[ - LFS_TYPE_REG-LFS_TYPE_REG]; - opened; - opened = opened->next) { - lfsr_file_t *file_ = (lfsr_file_t*)opened; - if (file_->ftree.mdir.mid == ftree->mdir.mid - // don't double update - && &file_->ftree != ftree - // don't update desynced file handles - && !lfsr_o_isdesync(file_->flags)) { - file_->flags &= ~LFS_F_UNSYNCED; - if (lfsr_f_isunflushed(flags)) { - file_->flags |= LFS_F_UNFLUSHED; - file_->size = buffer_size; - } else { - file_->flags &= ~LFS_F_UNFLUSHED; - file_->size = lfsr_ftree_size(ftree); - } - file_->ftree.u = ftree->u; - file_->buffer_pos = buffer_pos; - LFS_ASSERT(buffer_size <= lfs->cfg->cache_size); - memcpy(file_->buffer, buffer, buffer_size); - file_->buffer_size = buffer_size; - } - } - - return 0; -} - // our high-level file operations lfs_ssize_t lfsr_file_read(lfs_t *lfs, lfsr_file_t *file, @@ -10666,39 +10601,30 @@ lfs_ssize_t lfsr_file_write(lfs_t *lfs, lfsr_file_t *file, return 0; } - // copy state so we can recover from errors - bool unflushed_ = lfsr_f_isunflushed(file->flags); - lfs_off_t buffer_pos_ = file->buffer_pos; - const uint8_t *buffer_ = file->buffer; - lfs_size_t buffer_size_ = file->buffer_size; - lfsr_ftree_t ftree_ = file->ftree; - // add to tracked mdirs - lfsr_mdir_addopened(lfs, LFS_TYPE_REG, (lfsr_openedmdir_t*)&ftree_); - int err; - // checkpoint the allocator lfs_alloc_ckpoint(lfs); + int err; // update pos if we are appending - lfs_off_t pos__ = file->pos; + lfs_off_t pos = file->pos; if (lfsr_o_isappend(file->flags)) { - pos__ = file->size; + pos = file->size; } // if we're a small file, we may need to append zeros - if (pos__ > file->size - && pos__ <= lfs->cfg->cache_size - && pos__ <= lfs->cfg->inline_size - && pos__ <= lfs->cfg->fragment_size) { - LFS_ASSERT(unflushed_); - LFS_ASSERT(file->size == buffer_size_); - memset(&file->buffer[buffer_size_], + if (pos > file->size + && pos <= lfs->cfg->cache_size + && pos <= lfs->cfg->inline_size + && pos <= lfs->cfg->fragment_size) { + LFS_ASSERT(lfsr_f_isunflushed(file->flags)); + LFS_ASSERT(file->size == file->buffer_size); + memset(&file->buffer[file->buffer_size], 0, - pos__ - buffer_size_); - buffer_size_ = pos__; + pos - file->buffer_size); + file->buffer_size = pos; } - const uint8_t *buffer__ = buffer; + const uint8_t *buffer_ = buffer; while (size > 0) { // bypass buffer? // @@ -10706,9 +10632,10 @@ lfs_ssize_t lfsr_file_write(lfs_t *lfs, lfsr_file_t *file, // strictly necessary, but enforces a more intuitive write order // and avoids weird cases with low-level write heuristics // - if (!unflushed_ && size >= lfs->cfg->cache_size) { - err = lfsr_ftree_flush(lfs, &ftree_, - pos__, buffer__, size); + if (!lfsr_f_isunflushed(file->flags) + && size >= lfs->cfg->cache_size) { + err = lfsr_ftree_flush(lfs, &file->ftree, + pos, buffer_, size); if (err) { goto failed; } @@ -10717,12 +10644,14 @@ lfs_ssize_t lfsr_file_write(lfs_t *lfs, lfsr_file_t *file, // // note we need to clear the buffer anyways to avoid any // out-of-date data - buffer_pos_ = pos__ + size - lfs->cfg->cache_size; - buffer_ = &buffer__[size - lfs->cfg->cache_size]; - buffer_size_ = lfs->cfg->cache_size; + file->buffer_pos = pos + size - lfs->cfg->cache_size; + memcpy(file->buffer, + &buffer_[size - lfs->cfg->cache_size], + lfs->cfg->cache_size); + file->buffer_size = lfs->cfg->cache_size; - pos__ += size; - buffer__ += size; + pos += size; + buffer_ += size; size -= size; continue; } @@ -10737,112 +10666,82 @@ lfs_ssize_t lfsr_file_write(lfs_t *lfs, lfsr_file_t *file, // 2. Bypassing the buffer above means we only write to the // buffer once, and flush at most twice. // - if (!unflushed_ - || (pos__ >= buffer_pos_ - && pos__ <= buffer_pos_ + buffer_size_ - && pos__ < buffer_pos_ + lfs->cfg->cache_size)) { + if (!lfsr_f_isunflushed(file->flags) + || (pos >= file->buffer_pos + && pos <= file->buffer_pos + file->buffer_size + && pos < file->buffer_pos + lfs->cfg->cache_size)) { // unused buffer? we can move it where we need it - lfs_size_t d; - if (!unflushed_) { - d = lfs_min32( - size, - lfs->cfg->cache_size); - buffer_pos_ = pos__; - buffer_ = buffer__; - buffer_size_ = d; - } else { - d = lfs_min32( - size, - lfs->cfg->cache_size - (pos__ - buffer_pos_)); - LFS_ASSERT(file->buffer == buffer_); - memcpy(&file->buffer[pos__ - buffer_pos_], buffer__, d); - buffer_size_ = lfs_max32( - buffer_size_, - pos__+d - buffer_pos_); + if (!lfsr_f_isunflushed(file->flags)) { + file->buffer_pos = pos; + file->buffer_size = 0; } - unflushed_ = true; - pos__ += d; - buffer__ += d; + lfs_size_t d = lfs_min32( + size, + lfs->cfg->cache_size - (pos - file->buffer_pos)); + memcpy(&file->buffer[pos - file->buffer_pos], buffer_, d); + file->buffer_size = lfs_max32( + file->buffer_size, + pos+d - file->buffer_pos); + + file->flags |= LFS_F_UNFLUSHED; + pos += d; + buffer_ += d; size -= d; continue; } // flush our buffer so the above can't fail - err = lfsr_ftree_flush(lfs, &ftree_, - buffer_pos_, buffer_, buffer_size_); + err = lfsr_ftree_flush(lfs, &file->ftree, + file->buffer_pos, file->buffer, file->buffer_size); if (err) { goto failed; } - unflushed_ = false; - } - - lfs_off_t size__ = lfs_max32(file->size, pos__); - bool unsynced_ = true; - // flush if requested - if (lfsr_o_isflush(file->flags) || lfsr_o_issync(file->flags)) { - // keep small files unflushed - if (unflushed_ && !( - size__ <= lfs->cfg->cache_size - && size__ <= lfs->cfg->inline_size - && size__ <= lfs->cfg->fragment_size)) { - // flush - err = lfsr_ftree_flush(lfs, &ftree_, - buffer_pos_, buffer_, buffer_size_); - if (err) { - goto failed; - } - unflushed_ = false; - } - } - // sync if requested - if (lfsr_o_issync(file->flags)) { - // sync - err = lfsr_ftree_sync(lfs, &ftree_, - LFS_F_UNSYNCED | ((unflushed_) ? LFS_F_UNFLUSHED : 0), - buffer_pos_, buffer_, buffer_size_); - if (err) { - goto failed; - } - - // mark as in-sync - unsynced_ = false; - file->flags &= ~LFS_O_DESYNC; - } - - // remove from tracked mdirs - lfsr_mdir_removeopened(lfs, LFS_TYPE_REG, (lfsr_openedmdir_t*)&ftree_); - // mark as unflushed and unsynced, update file, and return amount written - lfs_size_t written; - if (lfsr_o_isappend(file->flags)) { - written = pos__ - file->size; - } else { - written = pos__ - file->pos; - } - if (unsynced_) { - file->flags |= LFS_F_UNSYNCED; - } else { - file->flags &= ~LFS_F_UNSYNCED; - } - if (unflushed_) { - file->flags |= LFS_F_UNFLUSHED; - } else { file->flags &= ~LFS_F_UNFLUSHED; } - file->pos = pos__; - file->size = size__; - file->buffer_pos = buffer_pos_; - if (buffer_ != file->buffer) { - LFS_ASSERT(buffer_size_ <= lfs->cfg->cache_size); - memcpy(file->buffer, buffer_, buffer_size_); + + // mark as unsynced + file->flags |= LFS_F_UNSYNCED; + // return amount written + lfs_size_t written; + if (lfsr_o_isappend(file->flags)) { + written = pos - file->size; + } else { + written = pos - file->pos; } - file->buffer_size = buffer_size_; - file->ftree.u = ftree_.u; + // update our pos and size + file->pos = pos; + file->size = lfs_max32(file->size, pos); + LFS_ASSERT(file->size == lfs_max32( + file->buffer_pos + file->buffer_size, + lfsr_ftree_size(&file->ftree))); + + // flush if requested + // + // this seems unreachable, but it's possible if we transition from + // a small file to a non-small file + if (lfsr_o_isflush(file->flags)) { + err = lfsr_file_flush(lfs, file); + if (err) { + goto failed; + } + } + + // sync if requested + if (lfsr_o_issync(file->flags)) { + err = lfsr_file_sync(lfs, file); + if (err) { + goto failed; + } + } + return written; failed:; - // remove from tracked mdirs - lfsr_mdir_removeopened(lfs, LFS_TYPE_REG, (lfsr_openedmdir_t*)&ftree_); + // keep size up to date + file->size = lfs_max32( + file->buffer_pos + file->buffer_size, + lfsr_ftree_size(&file->ftree)); // mark as desync so lfsr_file_close doesn't write to disk file->flags |= LFS_O_DESYNC; return err; @@ -10877,33 +10776,24 @@ int lfsr_file_flush(lfs_t *lfs, lfsr_file_t *file) { // checkpoint the allocator lfs_alloc_ckpoint(lfs); - - // copy state so we can recover from errors - lfsr_ftree_t ftree_ = file->ftree; - // add to tracked mdirs - lfsr_mdir_addopened(lfs, LFS_TYPE_REG, (lfsr_openedmdir_t*)&ftree_); int err; // flush our buffer if it contains any unwritten data - if (lfsr_f_isunflushed(file->flags) && file->buffer_size != 0) { + if (lfsr_f_isunflushed(file->flags) + && file->buffer_size != 0) { // flush - err = lfsr_ftree_flush(lfs, &ftree_, + err = lfsr_ftree_flush(lfs, &file->ftree, file->buffer_pos, file->buffer, file->buffer_size); if (err) { goto failed; } } - // remove from tracked mdirs - lfsr_mdir_removeopened(lfs, LFS_TYPE_REG, (lfsr_openedmdir_t*)&ftree_); - // mark as flushed and update our file - file->ftree.u = ftree_.u; + // mark as flushed file->flags &= ~LFS_F_UNFLUSHED; return 0; failed:; - // remove from tracked mdirs - lfsr_mdir_removeopened(lfs, LFS_TYPE_REG, (lfsr_openedmdir_t*)&ftree_); // mark as desync so lfsr_file_close doesn't write to disk file->flags |= LFS_O_DESYNC; return err; @@ -10935,23 +10825,86 @@ int lfsr_file_sync(lfs_t *lfs, lfsr_file_t *file) { goto failed; } - // checkpoint the allocator again - lfs_alloc_ckpoint(lfs); - + // note because of small-file caching and our current write + // strategy, we never actually end up with only a direct data + // or bptr + // + // this is convenient because bptrs are a bit annoying to commit + LFS_ASSERT(!lfsr_ftree_isbsprout(&file->ftree)); + LFS_ASSERT(!lfsr_ftree_isbleaf(&file->ftree)); + // small files should start as zero, const prop should optimize this out + LFS_ASSERT(!lfsr_f_isunflushed(file->flags) + || file->buffer_pos == 0); + // small files/ftree should be exclusive here + LFS_ASSERT(!lfsr_f_isunflushed(file->flags) + || lfsr_ftree_size(&file->ftree) == 0); + // small files must be inlined entirely in our buffer + LFS_ASSERT(!lfsr_f_isunflushed(file->flags) + || (file->buffer_size <= lfs->cfg->cache_size + && file->buffer_size <= lfs->cfg->inline_size + && file->buffer_size <= lfs->cfg->fragment_size)); // if this invariant breaks something has gone horribly wrong LFS_ASSERT(file->size == lfs_max32( file->buffer_pos + file->buffer_size, lfsr_ftree_size(&file->ftree))); - // commit our file's metadata - err = lfsr_ftree_sync(lfs, &file->ftree, file->flags, - file->buffer_pos, file->buffer, file->buffer_size); - if (err) { - goto failed; + // checkpoint the allocator again + lfs_alloc_ckpoint(lfs); + + // don't write to disk if disk is already in-sync + if (lfsr_f_isunsynced(file->flags)) { + // commit our file's metadata + uint8_t buf[LFSR_BTREE_DSIZE]; + int err = lfsr_mdir_commit(lfs, &file->ftree.mdir, LFSR_ATTRS( + (lfsr_f_isunflushed(file->flags) && file->buffer_size == 0) + ? LFSR_ATTR(file->ftree.mdir.mid, + WIDE(RM(STRUCT)), 0, + NULL()) + : (lfsr_f_isunflushed(file->flags)) + ? LFSR_ATTR(file->ftree.mdir.mid, + WIDE(DATA), 0, + BUF(file->buffer, file->buffer_size)) + : (lfsr_ftree_isbshrub(&file->ftree)) + ? LFSR_ATTR(file->ftree.mdir.mid, + WIDE(SHRUBTRUNK), 0, + SHRUBTRUNK(&file->ftree.u.bshrub)) + : LFSR_ATTR(file->ftree.mdir.mid, + WIDE(BTREE), 0, + FROMBTREE(&file->ftree.u.btree, buf)))); + if (err) { + return err; + } } - // mark as in-sync - file->flags &= ~LFS_O_SYNC & ~LFS_O_DESYNC; + // but do update other file handles + for (lfsr_openedmdir_t *opened_ = lfs->opened[ + LFS_TYPE_REG-LFS_TYPE_REG]; + opened_; + opened_ = opened_->next) { + lfsr_file_t *file_ = (lfsr_file_t*)opened_; + if (file_->ftree.mdir.mid == file->ftree.mdir.mid + // don't double update + && file_ != file + // don't update desynced file handles + && !lfsr_o_isdesync(file_->flags)) { + file_->flags &= ~LFS_F_UNSYNCED; + if (lfsr_f_isunflushed(file->flags)) { + file_->flags |= LFS_F_UNFLUSHED; + file_->size = file->buffer_size; + } else { + file_->flags &= ~LFS_F_UNFLUSHED; + file_->size = lfsr_ftree_size(&file->ftree); + } + file_->ftree.u = file->ftree.u; + file_->buffer_pos = file->buffer_pos; + LFS_ASSERT(file->buffer_size <= lfs->cfg->cache_size); + memcpy(file_->buffer, file->buffer, file->buffer_size); + file_->buffer_size = file->buffer_size; + } + } + + // mark as synced + file->flags &= ~LFS_F_UNSYNCED & ~LFS_O_DESYNC; return 0; failed:; @@ -11020,14 +10973,6 @@ int lfsr_file_truncate(lfs_t *lfs, lfsr_file_t *file, lfs_off_t size) { // checkpoint the allocator lfs_alloc_ckpoint(lfs); - - // copy state so we can recover from errors - bool unflushed_ = lfsr_f_isunflushed(file->flags); - lfs_off_t buffer_pos_ = file->buffer_pos; - lfs_size_t buffer_size_ = file->buffer_size; - lfsr_ftree_t ftree_ = file->ftree; - // add to tracked mdirs - lfsr_mdir_addopened(lfs, LFS_TYPE_REG, (lfsr_openedmdir_t*)&ftree_); int err; // does our file become small? @@ -11038,12 +10983,6 @@ int lfsr_file_truncate(lfs_t *lfs, lfsr_file_t *file, lfs_off_t size) { // need to flush so our buffer is available to hold everything if (file->buffer_pos > 0 || file->buffer_size < lfs_min32(size, file->size)) { - // TODO use ftree flush to avoid double ftree tracking? - // TODO or move this before our tracking here? we MUST update - // the file to use its buffer - // - // note that flush does not change the actual file data, so if - // a read fails it's ok to fall back to our flushed state err = lfsr_file_flush(lfs, file); if (err) { goto failed; @@ -11057,27 +10996,27 @@ int lfsr_file_truncate(lfs_t *lfs, lfsr_file_t *file, lfs_off_t size) { err = d; goto failed; } - buffer_pos_ = 0; - buffer_size_ = size; + file->buffer_pos = 0; + file->buffer_size = size; } // we may need to zero some of our buffer - if (size > buffer_size_) { - memset(&file->buffer[buffer_size_], + if (size > file->buffer_size) { + memset(&file->buffer[file->buffer_size], 0, - size - buffer_size_); + size - file->buffer_size); } // small files remain perpetually unflushed - unflushed_ = true; - buffer_pos_ = 0; - buffer_size_ = size; - ftree_.u.size = LFSR_FTREE_NULL; + file->flags |= LFS_F_UNFLUSHED; + file->buffer_pos = 0; + file->buffer_size = size; + file->ftree.u.size = LFSR_FTREE_NULL; // truncate our file normally } else { // truncate our ftree - err = lfsr_ftree_carve(lfs, &ftree_, + err = lfsr_ftree_carve(lfs, &file->ftree, lfs_min32(file->size, size), file->size - lfs_min32(file->size, size), +size - file->size, @@ -11087,72 +11026,46 @@ int lfsr_file_truncate(lfs_t *lfs, lfsr_file_t *file, lfs_off_t size) { } // truncate our buffer - buffer_pos_ = lfs_min32(buffer_pos_, size); - buffer_size_ = lfs_min32( - buffer_size_, - size - lfs_min32(buffer_pos_, size)); + file->buffer_pos = lfs_min32(file->buffer_pos, size); + file->buffer_size = lfs_min32( + file->buffer_size, + size - lfs_min32(file->buffer_pos, size)); } - bool unsynced_ = true; - // flush if requested - // - // this initially seems unreachable, but it's possible if we transition - // from a small file to a non-small file - if (lfsr_o_isflush(file->flags) || lfsr_o_issync(file->flags)) { - // keep small files unflushed - if (unflushed_ && !( - size <= lfs->cfg->cache_size - && size <= lfs->cfg->inline_size - && size <= lfs->cfg->fragment_size)) { - // flush - err = lfsr_ftree_flush(lfs, &ftree_, - buffer_pos_, file->buffer, buffer_size_); - if (err) { - goto failed; - } - unflushed_ = false; - } - } - // sync if requested - if (lfsr_o_issync(file->flags)) { - // sync - err = lfsr_ftree_sync(lfs, &ftree_, - LFS_F_UNSYNCED | ((unflushed_) ? LFS_F_UNFLUSHED : 0), - buffer_pos_, file->buffer, buffer_size_); - if (err) { - goto failed; - } - - // mark as in-sync - unsynced_ = false; - file->flags &= ~LFS_O_DESYNC; - } - - // remove from tracked mdirs - lfsr_mdir_removeopened(lfs, LFS_TYPE_REG, (lfsr_openedmdir_t*)&ftree_); - // mark as unsynced and update our size - if (unflushed_) { - file->flags |= LFS_F_UNFLUSHED; - } else { - file->flags &= ~LFS_F_UNFLUSHED; - } - if (unsynced_) { - file->flags |= LFS_F_UNSYNCED; - } else { - file->flags &= ~LFS_F_UNSYNCED; - } + // mark as unsynced + file->flags |= LFS_F_UNSYNCED; + // update our size file->size = size; - file->ftree.u = ftree_.u; - file->buffer_pos = buffer_pos_; - file->buffer_size = buffer_size_; LFS_ASSERT(file->size == lfs_max32( file->buffer_pos + file->buffer_size, lfsr_ftree_size(&file->ftree))); + + // flush if requested + // + // this seems unreachable, but it's possible if we transition from + // a small file to a non-small file + if (lfsr_o_isflush(file->flags)) { + err = lfsr_file_flush(lfs, file); + if (err) { + goto failed; + } + } + + // sync if requested + if (lfsr_o_issync(file->flags)) { + err = lfsr_file_sync(lfs, file); + if (err) { + goto failed; + } + } + return 0; failed:; - // remove from tracked mdirs - lfsr_mdir_removeopened(lfs, LFS_TYPE_REG, (lfsr_openedmdir_t*)&ftree_); + // keep size up to date + file->size = lfs_max32( + file->buffer_pos + file->buffer_size, + lfsr_ftree_size(&file->ftree)); // mark as desync so lfsr_file_close doesn't write to disk file->flags |= LFS_O_DESYNC; return err; @@ -11171,14 +11084,6 @@ int lfsr_file_fruncate(lfs_t *lfs, lfsr_file_t *file, lfs_off_t size) { // checkpoint the allocator lfs_alloc_ckpoint(lfs); - - // copy state so we can recover from errors - bool unflushed_ = lfsr_f_isunflushed(file->flags); - lfs_off_t buffer_pos_ = file->buffer_pos; - lfs_size_t buffer_size_ = file->buffer_size; - lfsr_ftree_t ftree_ = file->ftree; - // add to tracked mdirs - lfsr_mdir_addopened(lfs, LFS_TYPE_REG, (lfsr_openedmdir_t*)&ftree_); int err; // does our file become small? @@ -11189,12 +11094,6 @@ int lfsr_file_fruncate(lfs_t *lfs, lfsr_file_t *file, lfs_off_t size) { // need to flush so our buffer is available to hold everything if (file->buffer_pos + file->buffer_size < file->size || file->buffer_size < lfs_min32(size, file->size)) { - // TODO use ftree flush to avoid double ftree tracking? - // TODO or move this before our tracking here? we MUST update - // the file to use its buffer - // - // note that flush does not change the actual file data, so if - // a read fails it's ok to fall back to our flushed state err = lfsr_file_flush(lfs, file); if (err) { goto failed; @@ -11209,36 +11108,36 @@ int lfsr_file_fruncate(lfs_t *lfs, lfsr_file_t *file, lfs_off_t size) { err = d; goto failed; } - buffer_pos_ = 0; - buffer_size_ = size; + file->buffer_pos = 0; + file->buffer_size = size; } // we may need to move the data in our buffer - if (buffer_size_ > size) { + if (file->buffer_size > size) { memmove(file->buffer, - &file->buffer[buffer_size_ - size], - buffer_size_); + &file->buffer[file->buffer_size - size], + file->buffer_size); } // we may need to zero some of our buffer - if (size > buffer_size_) { - memmove(&file->buffer[size - buffer_size_], + if (size > file->buffer_size) { + memmove(&file->buffer[size - file->buffer_size], file->buffer, - buffer_size_); + file->buffer_size); memset(file->buffer, 0, - size - buffer_size_); + size - file->buffer_size); } // small files remain perpetually unflushed - unflushed_ = true; - buffer_pos_ = 0; - buffer_size_ = size; - ftree_.u.size = LFSR_FTREE_NULL; + file->flags |= LFS_F_UNFLUSHED; + file->buffer_pos = 0; + file->buffer_size = size; + file->ftree.u.size = LFSR_FTREE_NULL; // fruncate our file normally } else { // fruncate our ftree - err = lfsr_ftree_carve(lfs, &ftree_, + err = lfsr_ftree_carve(lfs, &file->ftree, 0, lfs_smax32(file->size - size, 0), +size - file->size, @@ -11250,77 +11149,51 @@ int lfsr_file_fruncate(lfs_t *lfs, lfsr_file_t *file, lfs_off_t size) { // fruncate our buffer memmove(file->buffer, &file->buffer[lfs_min32( - lfs_smax32(file->size - size - buffer_pos_, 0), - buffer_size_)], - buffer_size_ - lfs_min32( - lfs_smax32(file->size - size - buffer_pos_, 0), - buffer_size_)); - buffer_size_ -= lfs_min32( - lfs_smax32(file->size - size - buffer_pos_, 0), - buffer_size_); - buffer_pos_ -= lfs_smin32(file->size - size, buffer_pos_); + lfs_smax32(file->size - size - file->buffer_pos, 0), + file->buffer_size)], + file->buffer_size - lfs_min32( + lfs_smax32(file->size - size - file->buffer_pos, 0), + file->buffer_size)); + file->buffer_size -= lfs_min32( + lfs_smax32(file->size - size - file->buffer_pos, 0), + file->buffer_size); + file->buffer_pos -= lfs_smin32(file->size - size, file->buffer_pos); } - bool unsynced_ = true; - // flush if requested - // - // this initially seems unreachable, but it's possible if we transition - // from a small file to a non-small file - if (lfsr_o_isflush(file->flags) || lfsr_o_issync(file->flags)) { - // keep small files unflushed - if (unflushed_ && !( - size <= lfs->cfg->cache_size - && size <= lfs->cfg->inline_size - && size <= lfs->cfg->fragment_size)) { - // flush - err = lfsr_ftree_flush(lfs, &ftree_, - buffer_pos_, file->buffer, buffer_size_); - if (err) { - goto failed; - } - unflushed_ = false; - } - } - // sync if requested - if (lfsr_o_issync(file->flags)) { - // sync - err = lfsr_ftree_sync(lfs, &ftree_, - LFS_F_UNSYNCED | ((unflushed_) ? LFS_F_UNFLUSHED : 0), - buffer_pos_, file->buffer, buffer_size_); - if (err) { - goto failed; - } - - // mark as in-sync - unsynced_ = false; - file->flags &= ~LFS_O_DESYNC; - } - - // remove from tracked mdirs - lfsr_mdir_removeopened(lfs, LFS_TYPE_REG, (lfsr_openedmdir_t*)&ftree_); - // mark as unsynced and update our size - if (unflushed_) { - file->flags |= LFS_F_UNFLUSHED; - } else { - file->flags &= ~LFS_F_UNFLUSHED; - } - if (unsynced_) { - file->flags |= LFS_F_UNSYNCED; - } else { - file->flags &= ~LFS_F_UNSYNCED; - } + // mark as unsynced + file->flags |= LFS_F_UNSYNCED; + // update our size file->size = size; - file->ftree.u = ftree_.u; - file->buffer_pos = buffer_pos_; - file->buffer_size = buffer_size_; LFS_ASSERT(file->size == lfs_max32( file->buffer_pos + file->buffer_size, lfsr_ftree_size(&file->ftree))); + + // flush if requested + // + // this seems unreachable, but it's possible if we transition from + // a small file to a non-small file + if (lfsr_o_isflush(file->flags)) { + err = lfsr_file_flush(lfs, file); + if (err) { + goto failed; + } + } + + // sync if requested + if (lfsr_o_issync(file->flags)) { + err = lfsr_file_sync(lfs, file); + if (err) { + goto failed; + } + } + return 0; failed:; - // remove from tracked mdirs - lfsr_mdir_removeopened(lfs, LFS_TYPE_REG, (lfsr_openedmdir_t*)&ftree_); + // keep size up to date + file->size = lfs_max32( + file->buffer_pos + file->buffer_size, + lfsr_ftree_size(&file->ftree)); // mark as desync so lfsr_file_close doesn't write to disk file->flags |= LFS_O_DESYNC; return err;