Repository navigation
turbopack: compose chunk outputs from persisted chunk item cells (CellRope) - #99969
Draft
lukesandberg wants to merge 3 commits into
Draft
lukesandberg wants to merge 3 commits into
lukesandberg wants to merge 3 commits into
Conversation
Contributor
Tests PassedCommit: 6a215f6 |
lukesandberg
force-pushed
the
lukesandberg/turbopack-async-rope
branch
from
October 10, 2026 17:23
ad98ead to
2911ed4
Compare
lukesandberg
force-pushed
the
lukesandberg/turbopack-async-rope
branch
from
October 29, 2026 06:26
4e96512 to
4e7be29
Compare
lukesandberg
force-pushed
the
lukesandberg/turbopack-async-rope
branch
from
October 29, 2026 07:31
4e7be29 to
6a6d51f
Compare
…lRope) Co-authored-by: Luke Sandberg <210140+lukesandberg@users.noreply.github.com>
Co-authored-by: Luke Sandberg <210140+lukesandberg@users.noreply.github.com>
Strict factories inside a strict chunk wrapper reference their code cell together with a factory-owned cell holding the directive's byte range. The CellRope applies it with Rope::skip when read, so chunk compositions stay independent of the directive's position. Co-authored-by: Luke Sandberg <210140+lukesandberg@users.noreply.github.com>
lukesandberg
force-pushed
the
lukesandberg/turbopack-async-rope
branch
from
October 29, 2026 08:04
6a6d51f to
6a215f6
Compare
This branch has not been deployed
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
Chunk outputs (
.jsand.js.map) are now composed from their chunk items' cells instead of being persisted as flat bytes.turbo-tasks-fs:CellRope, a flat list of inline segments (scaffolding),ResolvedVc<Rope>cell references and nestedResolvedVc<CellRope>references.read()concatenates them without copying. The encoding stores references, not the referenced bytes.File::from_cell_rope: aFilecan carry theCellRopeit was read from, in a field ignored by equality, ordering and hashing. Readers see normal bytes.DiskFileSystem::writepersists only theCellRopefor such files, instead ofPersistedFileContent.ApplyError::Retrypath, which re-runs the producer. Stale or mixed bytes are never written.File's equality ignores the composition, so a cell can keep an older, equalFilewhose rope no longer reads its bytes (turbo-tasks keeps the old value when the new one is equal).writetherefore only uses the rope if it reads exactly the file's bytes, and otherwise writes flat. Without this guard such a write fails with "gave up applying effects after 4 retries" (covered by a test).turbopack-core:CodeCellsreplacesPersistedCode. The code and each section's source map are their own persistedRopecells.compositiontask that builds aCodeComposition(the bodyCellRopeplus references to the items'CodeCells) without reading item bytes. A separate reader task (composed_code) builds the flatCode, its hash and the source-map offsets. The output file's rope nests the body (prefix + Nested(body) + suffix) instead of copying its segment list. See below.turbopack-ecmascript: module factories are stored asCodeCells. With per-item minification (turbopack: experimental minify-before-chunking feature #98760), each minified factory is its own persisted task (minified_module_factory) instead of an unpersisted intermediate.CodeModuleIdAndPathnow holds only references (ResolvedVc<CodeCells>plus the code cell), so it stays equal when only an item's bytes change.Why the
Filefield instead of a newAssetContentvariant: 21 files matchAssetContent::File, many withlet … else(Next's devversioned_content_map,next-coreemit.rs, version hashing, the snapshot harness). They would have silently treated a composed file as "not a file". With the field, no reader changes.Update: keep compositions stable across item edits (
4e965124f2)Measuring warm writes on the first commit showed that a dev item edit still rewrote ~5.7 MiB lz4 of composition-task data. The backend persists a task's data (all cells, cell hashes and dependency sets) as one blob, and
composed_codeheld the body rope plus content-derived values (the flatCode, its hash, map cells keyed by chunk byte offset). It re-ran on every item edit.contentalso stored a second copy of the body's segments.This commit:
composed_codeinto a structure-onlycompositiontask and a reader. The composition only depends on item order, ids, strictness and cell references, so a payload-only edit (even one that changes the item's length) doesn't re-execute it and its body isn't rewritten;CodeComposition::build_code/generate_source_map);CellRopeSegment::Nestedso the file rope references the body instead of copying it.Dev
leafedit (shared module edited, single-snapshot runs, lz4 task data fromprint_cache_item_size_with_compressed; on-disk = polled snapshot SST bytes):compositiontasks write no data entries in the edit step (meta-only, ~0.45 MiB).content: 1.91 → 0.21 MiB (references plus the comment tail).composed_code: 3.07 → 0.61 MiB.Warning
Unresolved: in one warm
next devrestart with this commit's binary, the process aborted with a turbo-tasks panic:task 1128469 (task, MustExist): task is missing in memory or persistent storage(no backtrace). It hasn't recurred in ~16 later attempts withRUST_BACKTRACE=fulland GC-collection logging (including 500 ms snapshot intervals to force GC during requests). The first commit had 0 panics in ~6.5 comparable control runs. These counts are too small to say whether this commit makes it more likely. In the control, GC sometimes collected whole chunk subtrees including per-item factory tasks; with this commit it never did. The cause isn't established. A canary GC/restore race is plausible but unproven.New tests in this commit:
code_builder::tests::piece_edits_dont_recompose: a length-changing item edit updates bytes and source map (equal toCodeBuilder) without re-executing the composition; the body is unchanged.turbopack-core/tests/composition_persistence.rs: GC enabled, persisted, three sessions; direct-body and nested-with-tail files throughDiskFileSystem::write; content edit; equal-bytes swap of an item to another task (GC collects the orphan); restore and rewrite of deleted files.CellRope: nested reads (tracked and untracked) equal the concatenation, encode as references only, follow inner cell changes; errors surface through nested ropes.Strict directives (
2c38a90a7d)#98760 removes a strict factory's
"use strict"when it places the factory inside a strict wrapper (the mixed IIFE or the all-strict wrapper). Copying the factory's bytes without the directive would defeat referencing its cell, so the removal is deferred to read time instead:CellRopeBuilder::push_cell_skipping(cell, skip)references a cell plus anOptionByteRangecell. When the rope is read, the range it holds is left out with turbopack: experimental minify-before-chunking feature #98760's O(1)Rope::skip;Noneleaves the bytes unchanged, and a range outside the cell fails the read.CellRopestays generic: it doesn't know about directives.CodeCellsgets astrict_directive_rangecell, created by the task that owns the code cell.CodeCompositionBuilder::push_code_cells_without_strict_directivereferences both, so the composition stays structure-only: it depends on strictness and layout, not on where the directive is.build_codeandgenerate_source_mapapply the same removal, so bytes and maps equal theCodeBuilderpath.skipped_directives_follow_piece_edits_without_recomposingedits the code before a directive (moving it). The output updates, the composition doesn't re-execute, its body stays equal, and every section map is still referenced as a cell.Failure semantics
content(), then the write. The previous file is left untouched. (In practice, chunk-item codegen errors are already turned into a throwing factory plus an issue.)Verification (current tip)
composition_persistence), turbopack-ecmascript, turbopack-browser and turbopack-nodejs: 982 passed.bench/nested-deps-app-router-many-pagesoutput vs turbopack: experimental minify-before-chunking feature #98760 fornext build(per-item minification) andnext dev, cold and warm.cargo fmt; clippy-D warnings(--all-targets --no-deps) on the touched crates.Measurements (first commit)
bench/nested-deps-app-router-many-pages(100 pages), local debug build of next-swc,TURBO_ENGINE_VERSIONpinned so the filesystem cache is enabled. Single run per configuration. "Snapshot bytes" are the cache bytes written by snapshot commits in the session. Compaction output is reported separately, because it depends heavily on timing.Dev (
next dev, all pages fetched; then sessions without edits, editing a shared leaf module, and editing one page)Prod (
next build,turbopackMinifyBeforeChunkingon)These are polled SST sizes, not block-device I/O: transient files may be missed and metadata/LOG writes aren't included.
.js/.mapbytes were persisted again on top of the items'PersistedCode.DiskFileSystem::write's guard cost ~35 MB of dev cache (dependency edges to every item cell), so the guard reads untracked.Open questions / follow-ups
Ropecould decode known static strings likeRcStrdoes (orthogonal).Design choices
read()yields a regularRopesharing item buffers. The fs effect still captures a temporary Rope before applying a whole-file write; no output-file-write savings are claimed.