Conversation
folbricht
force-pushed
the
inplace-seed
branch
2 times, most recently
from
September 26, 2026 17:07
925fa94 to
234a6b1
Compare
folbricht
force-pushed
the
inplace-seed
branch
from
September 26, 2026 17:13
234a6b1 to
b452723
Compare
folbricht
force-pushed
the
inplace-seed
branch
from
September 26, 2026 17:51
b452723 to
15604a2
Compare
Replace the sequencer with a plan that decides where every chunk of the target comes from before anything is written, and runs the resulting steps as a graph with explicit dependencies. Every kind of step declares the range of the target it writes and the range it reads, either the content from before assembly or what other steps write, and the order of the steps is derived from that. A seed whose data file is the target itself, found by os.SameFile, is used in place: chunks already in position are skipped, chunks elsewhere in the file are moved to where they belong, and only the rest comes from the store. Moves are ordered so each reads its source before anything overwrites it. Moves in a dependency cycle hold their source in a buffer, filled on demand by the step about to overwrite it, within the memory left under GOMEMLIMIT; a buffer that doesn't fit makes its move take the chunk from the store. Consecutive chunks that shift together become one move, split again only where a cycle runs through it, and such runs are cloned on filesystems with reflinks. Chunks are taken from a source that clones them before one that copies, and from seeds before the target's own data. Seed indexes are validated against their files before assembly instead of reading back what was written; a seed file that changes during assembly is noticed by its size and modification time, and the chunks of the output that don't match are then written from the store again. A target larger than the output is shrunk after assembly, once its data can't be needed anymore. Closes #312
Workers that wait for a step exit without error when the context is cancelled, so an assembly stopped by the dispatcher reported success with an incomplete target, which could then be renamed over the output. Return the context's error when steps remain. The extract help still said a seed modified during extraction could corrupt the target. The target is verified and backfilled from the store in that case now.
folbricht
force-pushed
the
inplace-seed
branch
from
September 27, 2026 06:12
cf1f2c8 to
985f0c8
Compare
Buffer also named the scratch memory moves copy through. Stash is what Android's block-based OTA updates call it. splitCycles becomes splitRunsInCycles, it splits runs, not cycles.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Reworks assembly around a plan that decides where every chunk comes from before anything is written, and uses that to rearrange the data a target file already holds. Extracting a new version of a large image over the old one then moves the data that's still needed within the file instead of fetching it from the store.
assemblePlanthat resolves a source for every chunk position up front, then hands out the resulting steps as a DAG with explicit dependenciesos.SameFile, becomes in-place moves rather than a file seed that would read the file while it's being writtenbreakCyclesruns a depth-first search and marks the tail of every back edgeGOMEMLIMIT; a stash that doesn't fit is dropped and its move takes the chunk from the storeskipInPlace,inPlaceCopy,fileSeedSource,selfSeedSegmentandcopyFromStore, each of which knows how to execute itself, what to record in the stats, and what it does to the target: the range it writes and the range it reads, either the content from before assembly or what other steps write. All dependencies and stash fills are derived from those ranges in one pass: a step reading what others write waits for them, a step reading the old content runs before anything overwrites it or has its stash filled firstchunks-backfilledstatPerformance
master (ad89f62) against this PR (1bfb50e) on a 512 MiB image of random data, with variants built from 16 MiB blocks. Local store,
-n 4, btrfs with reflink support, 4 CPUs, warm page cache. Each figure is the median of 5 runs per build, with the builds interleaved; the times don't include writing back to disk.At a glance
Extract time
Wall and CPU time, master → this PR. Bold marks a difference where the ranges of the two builds across the runs don't overlap; the factor is in parentheses.
Nothing to reuse:
Reusing data from a seed or from the image itself:
Updating in place
The old version's index as the seed of the target itself, which fails on master:
Only the chunks the old version doesn't have come from the store. The inserted block shifts everything behind it by a multiple of the block size, so those runs are cloned and almost nothing is written. Rearranged regions depend on each other and move chunk by chunk, and a 1000 byte shift overlaps every chunk with its own source; those moves are copied.
Disk layout
The data cloned, the data written and the extents of the result, master → this PR:
Cloning is tried before copying, so a chunk found both in a seed it can only be copied from and within the image at a cloneable offset is cloned. Writing repeated data as one range per run rather than per chunk also leaves the file in 18 extents instead of 14713.
Seeds that change during assembly
The data written from a seed isn't read back and hashed afterwards anymore; the seed indexes are validated against their files before assembly starts. To catch a seed file that's written to while it's read, its size and modification time are compared after assembly. When a seed changed, every chunk of the output is hashed and the ones that don't match are written from the store. The whole output is checked, since the self-seed may have copied a damaged chunk elsewhere by then.
Without a change this costs a
statper seed file. A triggered check reads the output once more, 4.3 s instead of about 1 s for the file seed case above. It's also triggered for seeds modified within two seconds of the extract, as some filesystems keep modification times to the second or two, and for block devices, whose modification time means nothing. A writer that restores the modification time, or writes through a memory map that isn't flushed yet, isn't noticed.Closes #312