Tobias Koppers
90d · built 2026-09-30
Performance
What Tobias Koppers shipped in the selected window, measured in ETV, and how it compares with the 90 days before it.
Effective capacity
+0.9engineers
delivers like 1.9 (1.9x pre-AI)
Output (ETV)
26.6ETV
+4.1% vs 25.5 prior
Features share
34.5%
+3.5 pp vs prior window
Fixes share
21.3%
+7.3 pp vs prior window
Work mix
34.5% Features6.9% Maintenance33.2% Tests4.1% Docs21.3% Fixes
95 commits over 90 days, ending 2026-09-30.
Where this dev ranks
Percentile against the global top-100 leaderboard (all-time totals).
- By commits
- Top 43 %
- By Features share
- Top 53 %
Daily performance
Daily ETV, stacked by Features, Maintenance, Tests, Docs and Fixes.
Repository spread
Where this developer's commits land. Concentrated work (top1 > 80%) vs polymath spread (top1 < 30%).
Most impactful commits
Top 10 by ETV in the last 90 days.
- 2.3ETVturbo-tasks: execute scheduled tasks inline when they are read (#96808) ### What? Two changes to how the async turbo-tasks engine handles a read whose value isn't ready yet: 1. When a read finds its task merely *scheduled* (queued, not started), it takes the task out of the scheduler queue and executes it on the reading thread instead of waiting for a worker to pick it up. If the execution completes without yielding, the read returns synchronously; if it yields, the partially-polled execution is handed to tokio and the read waits as before. 2. When a read finds its task's execution already **in progress** (a worker is running it), it now waits directly on the task's completion event instead of also attempting to take it over — avoiding a futile acquisition of the scheduler's queue lock, which is otherwise the most contended lock in the system. An inline execution is visible in traces: the executed task's span carries `inline_execution = "complete"` when the reader's poll finished it, or `"partial"` when it yielded and was handed to the runtime. A task a worker executed leaves the field unset. Optional diagnostics (`InlineExecutionStats`) count queue pushes, claim attempts and their outcomes, and reads that waited for an already-running task. They are behind the `inline_execution_stats` Cargo feature, off by default, so a normal build has no counters, no atomics and no extra fields. ### Why? A large share of read misses target tasks that have merely been scheduled, not tasks actually being computed elsewhere. Parking on those adds two avoidable thread hops (schedule → worker pickup → wake) for work the reader could simply do itself. Conversely, a read of a task a worker is already executing gains nothing from trying to claim it, so it shouldn't pay for trying. ### How? - `PriorityRunner` gained a keyed claim: queued entries are indexed by a `ScheduleKey` (task or local task) and stored in a slot store, so one entry can be removed by key under the existing queue lock while the priority heap keeps a tombstone that a popping worker skips. Every scheduled execution still runs exactly once. - The backend already distinguished a queued task from one already executing (`InProgressState::Scheduled` vs. `InProgress`) when building a listener for a read, and discarded that distinction. It's now threaded through as `ReadOutcome<T> = Value(T) | Scheduled(EventListener) | InProgress(EventListener)`; only `Scheduled` reads attempt a claim. The state is a hint — a worker pops a task off the queue before it marks it started, so a read can see `Scheduled` for a task that is no longer claimable. Acting on a stale hint costs one failed claim, never correctness. - Inline execution nests (A reads B inline, B reads C inline, ...), so it's capped at 16 levels per thread to bound stack growth; at the cap, reads fall back to waiting for a worker as before. The cap is a thread-local counter because it guards the *thread* stack, not the task stack. - Recording the outcome on the *executed task's* span needs a small handoff: when `poll_once_or_spawn` returns, that task's span has already been exited, so the executor registers the span it instruments the task body with in a slot that only exists while a claimed task is polled inline. - Added `turbo-tasks-backend/tests/inline_read_execution.rs` (8 tests), `inline_execution_span.rs` and `inline_execution_span_worker.rs`, plus unit tests in `priority_runner.rs`/`manager.rs` covering keyed claim/exactly-once, inline completion for global and local tasks, the in-progress/scheduled distinction, nested inline executions, restoring from a persistent cache, and a deep dependency chain (stack-depth guard). ### Benchmarks `bench/nested-deps-app-router-many-pages` (1000 pages, 3020 generated components, 3010 routes), release builds of `@next/swc` for this branch and for its merge base, runs interleaved, first run of each variant discarded as warm-up. Shared 8-vCPU VM. **Cold `next build --turbopack`** — 5 measured runs per variant: | metric | canary (median) | this branch (median) | delta | | --- | ---: | ---: | ---: | | total build | 96.70 s | 89.29 s | **−7.7 %** | | turbopack compile phase | 68 s | 61 s | **−10.3 %** | Raw totals (ms) — canary: 93566, 95354, 96702, 96717, 97545; branch: 89070, 89140, 89287, 89892, 90462. The distributions don't overlap (the branch is faster in all 25 pairwise comparisons, exact two-sided Mann-Whitney p ≈ 0.008), and the 7.4 s median gap is about twice canary's own 4.0 s run-to-run spread. **Incremental build** (second build reusing the persistent cache) — 3 measured runs per variant: 36.14 s vs 37.29 s median, compile phase 10.4 s vs 10.5 s. The 1.2 s gap is *smaller* than canary's own 1.2 s spread, so this is **no measurable difference** — the incremental case is dominated by restore and by static generation in Node workers rather than by the scheduling this PR changes. **Cache-hit reads** (`task_overhead/turbo-cached-*`, criterion): no regression. Measured before the rebase, on the same read-path code — the hit path never enters this code, since the inline attempt only happens on the branch where a read reports a miss. Caveats: one app and one workload shape, on a shared VM, so only the relative comparison on the same machine is meaningful, not the absolute seconds. <!-- NEXT_JS_LLM --> Co-authored-by: Luke Sandberg <210140+lukesandberg@users.noreply.github.com> --------- Co-authored-by: vercel-fleet-prod[bot] <318278635+vercel-fleet-prod[bot]@users.noreply.github.com> Co-authored-by: Tobias Koppers <1365881+sokra@users.noreply.github.com> Co-authored-by: Luke Sandberg <210140+lukesandberg@users.noreply.github.com>github.com-vercel-next.js · 33b7edfc · 2026-08-24
- 2.2ETVTurbopack: mangle exported names for smaller bundle sizes (#97672) ### What? Adds export-name mangling to Turbopack, behind a new experimental option `experimental.turbopackMangleExportNames` (default `false`). It is independent of minification: `--no-mangling` is a minifier flag and does not affect it. When enabled, each ECMAScript module's *used* export names — including `default` and `__esModule` — are replaced by short keys in the emitted output, both where the module registers its exports and where every consumer reads them. Modules whose export names could be observed by user code keep their original names, decided per module. This is a reland of #89060 (on top of the already-merged #89406), originally written by Matt Mastracci, who is credited as a co-author on the commit. Stacked on top: #97676 flips the default to `true` on canary releases, so Next.js's own CI exercises the feature broadly before it is considered for stable. ### Why? Bundle size. A module's export keys exist only to link modules together: the producing module emits `{ someVeryLongExportName: … }` and every consumer reads `ns["someVeryLongExportName"]`. Both sides are generated by us, so as long as producer and consumer agree — and the name isn't observable from user code — the key can be a single character. Long export names are extremely common in real dependency graphs (icon sets, utility packages, barrel files), and each one is paid for once in the module that defines it and once per importing module. ### How? **Ported, not rebased.** `canary` is ~2500 commits past the original stack's base, and the files it touched were independently rewritten in the meantime (export-analysis refactor #92781, CJS analysis for scope hoisting #95826, the `module_fragments` subsystem #95978). A probe rebase produced 16 conflicting files on the first commit alone, so the original branches were used as a reference implementation — for intent, the identifier alphabet, and test coverage — and the feature was rebuilt on today's infrastructure. #89561 from the original stack (erasing the Next.js wrapper module types) is deliberately **not** part of this change; it turned out to be unnecessary, because those modules already declare whole-module export usage and therefore back off on their own. **The name table** (`references/esm/mangle/table.rs`) hashes each name into a table of all valid JS identifiers of the smallest length that fits the name set — 15 exports get single-character keys — and resolves collisions by open addressing. Hashing rather than assigning `a`, `b`, `c`, … is what keeps names stable: an unrelated edit elsewhere in the module doesn't renumber every other export, and a collision only perturbs its own cluster. Assignment happens in two passes: every name that is *already* a valid identifier at the chosen length keeps itself and reserves its bucket first, and only then is anything hashed — so an export called `a` keeps `a`, and nothing else can be assigned it. Both passes iterate in sorted order, so the mapping depends only on the set of names. A module with exactly **one** mangleable export is special-cased to a fixed key, `f`, rather than a hashed one. `f` is the most common character in JS keywords (`if`, `for`, `function`), and every single-export module in the graph then emits the same `.f` / `.f()` byte sequences, which gzip's back-references pick up across the whole bundle — a bigger win than hashing, at the cost of that one key changing when a second export is added. A fixed list, `RESERVED_KEYS`, is withheld from every table for two different reasons: JS reserved words (`if`, `in`, `do`, `for`, `let`, `new`, `try`, `var`) are legal as quoted property keys but a minifier will not fold `ns["if"]` into the shorter `ns.if`, so handing one out costs bytes instead of saving them; and `__esModule` is withheld because the runtime's `esm()` helper defines that property on every module's exports object regardless of what the module itself exports, so an assigned key landing on it would collide. (`default` needs no such protection — once it is mangled like any other export, nothing else emits a property under that literal name.) **One source of truth for the mapping.** `mangled_export_names(module, chunking_context)` is a turbo-task that both the producing side (`EsmExports::code_generation`) and the consuming side (`ReferencedAssetIdent::Module`, the single place a cross-module export access is materialized) ask for the *target* module's map. Neither side computes a table of its own, so they cannot disagree, and the task derives export usage from the chunking context itself rather than accepting it as an argument, so a caller can't supply usage from the wrong graph. Re-export chains need no special handling, because the consumer side already resolves through re-exports to the module that produces the binding. The mangling decision itself lives on `EsmExports` as a `mangle_export_names: bool` field, rather than a separate trait method every module type has to override. A module that derives its exports from another one (a facade, a locals module, a part, a rename) inherits the flag with the data, which removed seven hand-written delegations and the possibility of a new wrapper type forgetting one. **A mangling decision must not cross module identities.** A few module types hand out *another* module's exports value as their own (the WASM loader module, the module-fragments side-effects wrapper, the client-reference proxy). If that borrowed value carried a real mangling decision, the producing and consuming sides would key their lookups on two different modules and could compute two different keys for the same export — this actually broke every WASM- and `@vercel/og`-based test once the default-on layer exercised it in CI. `EcmascriptExports::borrowed()` is the one place this is handled: it always returns an unmangled view, and every such pass-through site uses it. **Back-off is per module**, built on the export-usage information that landed after the original PR (`BindingUsageInfo` / `ModuleExportUsageInfo`) rather than the original's locals/facade-split heuristic. A module keeps its names when its usage is `All` (a namespace import that couldn't be lowered, a computed property access, an unresolvable `export *`, or a chunk-group entry — which covers client references and the Next.js wrapper modules), when it is read through a namespace value at all, when its exports are dynamic or not statically known ESM, or when names aren't being mangled in this build. `__webpack_exports_info__` gains `canMangle` and `mangledName` per export, which is how a running test can observe the mapping; with the option off it emits exactly what it emitted before. ### Testing - `turbo-tasks-hash`/table unit tests: encode/decode round-trip, degenerate-name rejection, table sizing, the single-export fixed key (including its own reservation), reserved-word withholding (including a reserved bucket-count test that stays in sync with the reserved list), the preserved-name pass running before any hashing, uniqueness under heavy collision, order independence, wrap-around probing, and same-tier stability. - 13 `turbopack-tests` execution fixtures under `tests/execution/turbopack/exports/mangle-*`, several ported from the original PR and from webpack's `test/configCases/mangle`: named imports, re-export chains and default exports (including one literally named `__esModule`), escaping namespaces (`Object.keys`, `delete ns.missing`, `export * as`, CJS interop), destructuring, prototype-shadowing names (`toString`, `$1`, `__1`), a 60-export two-character table, dynamic `import()` with `webpackExports` / `turbopackExports`, a CommonJS consumer of an ESM module, dynamic re-exports, scope hoisting on and off, and a control with the option off. - 3 committed snapshot fixtures under `tests/snapshot/mangle-exports`, so the emitted keys, the back-off, and the fixed single-export key are visible in review. - One fixture under `__skipped__`, which the harness asserts *fails*, recording the namespace-materialization gap below. - Full suite: 546 unit + 272 execution + 125 snapshot tests pass, with no snapshot churn across the several refactors this PR went through in review. - Verified against real builds: targeted app-dir, worker, WASM, and `@vercel/og`-based e2e suites pass with the option forced on (the failures that remain are external-network tests that fail identically with it off), and a small two-page app shrank by 0.76% of total emitted JS / 0.53% gzipped. ### Known limitations, each intentional - **A module read through `import * as ns` is never mangled**, even when every read is statically tracked, because the analysis doesn't yet distinguish a lowered named read from a materialized namespace object. Namespace imports are common, so this leaves real wins on the table; unlocking it is the highest-value follow-up. - **Escaping namespaces back off entirely.** Webpack instead keeps mangling and materializes a namespace object keyed by the original names. That is the `__skipped__` fixture: implementing it turns the suite red until the fixture is moved out. - **CommonJS export mangling is out of scope.** The producing side is reachable, but the consuming accesses live in user source and nothing rewrites them today; it needs its own design pass. Closes PACK-435 <!-- NEXT_JS_LLM --> Co-authored-by: Luke Sandberg <210140+lukesandberg@users.noreply.github.com> <!-- fleet ecdfa248-cd54-41ac-b4a2-c9d49e2a67ee --> --------- Co-authored-by: vercel-fleet-prod[bot] <318278635+vercel-fleet-prod[bot]@users.noreply.github.com> Co-authored-by: Tobias Koppers <1365881+sokra@users.noreply.github.com> Co-authored-by: Luke Sandberg <210140+lukesandberg@users.noreply.github.com>github.com-vercel-next.js · a84bc8de · 2026-08-28
- 2.2ETVGuard filesystem reads against unresolved symlinks (#97902) ### What? Adds debug-only OS realpath validation to successful `DiskFileSystem` file and directory reads. When a successfully canonicalized path differs from the supplied path, the read returns a normal task error naming both paths. Fixes pattern/glob traversal and NFT tracing so physical filesystem access uses resolved paths while logical paths remain available for user-visible specifiers and complete symlink-chain recovery. ### Why? Reading through an unresolved symlink parent gives the same filesystem object multiple path identities. That can make Turbo Tasks dependency tracking and invalidation inconsistent and can produce invalid deployment ZIPs when NFT output contains files below unresolved links. The checks return errors rather than asserting because paths can disagree temporarily under eventual consistency. Propagating a task error avoids panicking a worker thread while still exposing invalid callers during development. ### How? The validation lives directly in `DiskFileSystem::read` and `DiskFileSystem::raw_read_dir`. It calls the OS canonicalization API inline instead of the Turbo Tasks realpath task, keeping the diagnostic out of the task dependency graph. The guard runs only after the OS read succeeds, so missing/non-directory probes preserve their existing behavior. `read_matches` resolves each physical directory immediately before enumeration while retaining logical `PatternMatch` paths. `read_glob` and `track_glob` now resolve their initial directory before enumeration. Symlinks discovered later through wildcard segments are also traversed through resolved targets. `ReadGlobResult` deliberately retains logical paths rooted at the supplied base, allowing consumers to call `realpath_with_links` and recover the complete symlink chain. Consumers follow that contract explicitly: - NFT includes expand each logical match with `realpath_with_links`, emit resolved files and every traversed symlink, skip resolved directory targets, and deterministically deduplicate/sort output. - `import.meta.glob` uses recursive logical keys as the source of user-visible requests, while module resolution follows and tracks symlinks. - the hash-glob example resolves returned logical paths before reading. Webpack-loader context dependencies are covered for both `path/to/symlink/inner/path/*` and `path/to/*/inner/path/*`. The loader fixture performs its directory read with Node `fs`, reports the directory using `addContextDependency`, and Turbopack tracks the resolved target. ### Verification - `cargo fmt -p turbo-tasks-fs -p turbopack-ecmascript -p next-api -- --check` - `cargo clippy -p turbo-tasks-fs -p turbopack-ecmascript -p next-api --all-targets` - `cargo test -p turbo-tasks-fs` (128 passed) - `cargo test -p next-api` (7 passed) - `cargo check -p turbo-tasks-fs --examples` - `import.meta.glob` symlink execution fixture (1 passed) - Nine targeted node-file-trace CI cases with `release-with-assertions` (9 passed) - `pnpm build-all` - `webpack-loader-fs` Turbopack dev e2e (1 passed) - `build-trace-extra-entries-turbo` Turbopack production e2e (1 passed) - twoslash Turbopack production, normal mode (4 passed) - twoslash Turbopack production, cache-components mode (4 passed) - `bench/heavy-npm-deps` Turbopack development smoke test (HTTP 200) ### Notes The disk guard is cross-platform, while its symlink-parent regression test is Unix-only, matching neighbouring symlink tests. On Windows, OS canonicalization can also normalize casing and 8.3 short names; a debug read using a non-canonical spelling will therefore return the same diagnostic error. <!-- NEXT_JS_LLM --> <!-- fleet 1d32e12c-f4ec-4f22-862a-c85f0005805c --> --------- Co-authored-by: vercel-fleet-prod[bot] <318278635+vercel-fleet-prod[bot]@users.noreply.github.com> Co-authored-by: Tobias Koppers <1365881+sokra@users.noreply.github.com>github.com-vercel-next.js · 17a901a7 · 2026-08-28
- 2.0ETVturbo-persistence: add TURBO_PERSISTENCE_MMAP=0 to disable mmap (#97873) ## What? Add `TURBO_PERSISTENCE_MMAP=0` to disable memory-mapped persistence I/O. The default remains mmap; file mode reads SST blocks positionally, keeps block offsets in memory for random access, and reads meta/blob data without creating mappings. This is **1/3** in a dependent PR series. - Next: #97888 ## Why? Some environments cannot use mmap reliably or have constrained virtual address space. The flag provides an opt-out without changing the on-disk format or penalizing the default mmap path. ## How? - Thread a cached `AccessMode` through database configuration. - Select mmap or file-backed storage independently for SST lookup, compaction, metadata, and blobs. - Keep canary's existing `ArcBytes`/`RcBytes` split, zero-copy mmap filters, CRC-once bitmap, fast big-endian readers, mmap cache-bypass behavior, and key-order handling for blocks that omit hashes. - Exercise lookup, restore, compaction, metadata, and blob behavior in both modes. ## Verification - `cargo fmt --all -- --check` - `cargo check -p turbo-persistence --all-targets --all-features` - `cargo check -p turbo-tasks-backend` - `cargo check -p turbo-persistence --benches` - `cargo test -p turbo-persistence --all-features` — 109 passed <!-- NEXT_JS_LLM --> <!-- fleet 088efa77-2680-49f1-8941-92bb5f92b169 --> Co-authored-by: vercel-fleet-prod[bot] <318278635+vercel-fleet-prod[bot]@users.noreply.github.com> Co-authored-by: Tobias Koppers <1365881+sokra@users.noreply.github.com>github.com-vercel-next.js · 705b3269 · 2026-09-09
- 1.6ETVTurbopack: compact re-export registration (#98617) ## Summary Adds a compact Turbopack runtime path for modules whose exports are entirely forwarded from other modules. It replaces one generated wrapper getter per binding with a flat re-export description, while preserving live-binding semantics and source evaluation order. This revision also makes the optimization generic for synthetic re-export facades, rather than leaving facade integration in the stacked export-mangling PR: - synthetic facades use `ExportRegistrationMode::Reexport`; - their explicit reference list supplies ordering because they have no source `ImportMap`; - an evaluation-only locals reference can be represented as an empty group; - references whose import is performed by the compact registration do not emit it twice; - pruned references and merged evaluation-only references are omitted, because they deliberately produce no namespace binding and may have no module factory in the chunk; - async modules and known export-cycle circuit breakers retain the general registration path. The runtime now inspects source property descriptors. Constant exports reuse the source value, live exports reuse the exact receiver-independent Turbopack namespace getter, and dynamic/proxy/missing properties fall back to a wrapper getter. Destination descriptors remain enumerable, non-configurable, read-only ESM namespace properties; setters are never forwarded. ## Output examples A re-export-only module can suppress its redundant import and use a module-id group: ```js // before var ns = __turbopack_context__.i("a.js") __turbopack_context__.s(["a", () => ns.a, "b", () => ns.b]) // after __turbopack_context__.S(["a.js", "a,a,b,b"]) ``` Two source modules become groups separated by `0`: ```js __turbopack_context__.S([ "first.js", "a,a,b,b", 0, "second.js", "c,c", ]) ``` Encoding is selected independently per group. A comma-containing name uses explicit pairs while another group can remain packed: ```js __turbopack_context__.S([ "first.js", "b", "b", "has,comma", "has,comma", 0, "second.js", "c,c", ]) ``` A synthetic facade includes its evaluation-only locals part as an empty group, then its forwarded bindings in reference order: ```js __turbopack_context__.S([ "reexports.js <locals>", 0, "x.js", "x,x", 0, "y.js", "y,y", ]) ``` Inside a scope-hoisted factory, imports are retained and existing namespace objects are used as heads because another logical module in the merged factory may read them: ```js var locals = __turbopack_context__.i("reexports.js <locals>") var x = __turbopack_context__.i("x.js") __turbopack_context__.S([locals, 0, x, "x,x"], facadeId) ``` A merged evaluation-only locals reference is omitted from that list: scope hoisting supplies only an in-factory ordering placeholder, not a namespace variable that `S` could read. ## Runtime representation For each forwarded property, `S` reads the source descriptor: - data descriptor → capture the value with the runtime's value tag; - getter descriptor → reuse that exact getter (Turbopack-generated namespace getters are receiver-independent); - no usable own descriptor → use `() => namespace[name]` as a defensive fallback. Tests pin constant primitives, frozen objects, function values, live updates, exact getter identity, receiver independence, destination descriptor flags, omitted setters, and the fallback path. ## Verification - `cargo test -p turbopack-ecmascript -p turbopack-tests` — **590 unit, 302 execution, 3 auxiliary, 131 snapshot** tests passed on this layer - Full stacked #97770 verification — **590 unit, 303 execution, 3 auxiliary, 131 snapshot** tests passed - `cargo clippy -p turbopack-ecmascript -p turbopack-core -- -D warnings` - `cargo fmt --all -- --check` - Runtime TypeScript check (`pnpm check` in `turbopack-ecmascript-runtime/js`) - `pnpm build-all` - Dedicated coverage for ordinary import/re-export ordering, transformed `DUMMY_SP` declarations, callable CommonJS namespaces, synthetic facade ordering, scope-hoisted retained imports, pruned and merged evaluation references, async fallback, and export-cycle circuit breakers ## Incremental size on top of #98932 Scratch comparison only, as requested: #98617 stayed based on `canary`. Three apps were built with scope hoisting both disabled and enabled. Client reports raw and deterministic gzip (`gzip -9 -n`, sorted JS files); server reports raw only. Every row was repeated byte-for-byte. | benchmark | scope hoisting | client raw | client gzip | server raw | real `S` sites (client/server) | | --- | --- | ---: | ---: | ---: | ---: | | `basic-app` | off | +510 B | +214 B | **−6,114 B** | 0 / 56 | | `basic-app` | on | +510 B | +164 B | +8,618 B | 0 / 0 | | `heavy-npm-deps` *(lodash + Mantine + Mermaid)* | off | **−2,268 B** | **−432 B** | +4,680 B | 3 / 8 | | `heavy-npm-deps` *(lodash + Mantine + Mermaid)* | on | +522 B | +191 B | +8,630 B | 2 / 2 | | `module-cost` | off | +510 B | +263 B | +7,432 B | 0 / 6 | | `module-cost` | on | +510 B | +265 B | +8,624 B | 0 / 1 | Aggregate raw movement across the six rows is **+32,164 B**. The shared helper is only amortized in configurations with many eligible facade calls; scope hoisting commonly resolves forwarded bindings to locals, leaving few calls while every runtime copy still carries the helper. Descriptor reuse does **not** change generated module code or call-site counts. Compared with the same facade integration using wrapper getters, it adds **155 B client raw** and **527 B per runtime copy** (**1,054 B server raw** in these apps). Its purpose is avoiding per-binding wrapper allocation / access indirection where a source value or getter already exists, not reducing bundle bytes. The size result was reported before continuing, and descriptor work proceeded by explicit maintainer direction. <!-- NEXT_JS_LLM --> <!-- fleet ecdfa248-cd54-41ac-b4a2-c9d49e2a67ee --> --------- Co-authored-by: vercel-fleet-prod[bot] <318278635+vercel-fleet-prod[bot]@users.noreply.github.com> Co-authored-by: Tobias Koppers <1365881+sokra@users.noreply.github.com> Co-authored-by: Luke Sandberg <210140+lukesandberg@users.noreply.github.com>github.com-vercel-next.js · 812c88f3 · 2026-09-22
- 1.1ETVtest(turbo-tasks): run the wasm tests with a Node wasi host (#97789) ### What? Adds a Node host so `wasm32-wasip1-threads` test binaries can run, and wires the `turbo-rcstr`, `turbo-tasks`, and `turbo-tasks-backend` library tests into the WASI CI job. It also provides the WASI allocation symbols expected by the `lz4-sys` dependency introduced by the base PR, allowing those test binaries to link. ### Why? The Rust test binaries require host functionality that stock WASI runners do not provide. Turbopack stores its link-time task registries in WebAssembly custom sections and imports a host callback to read them. Its multithreaded runtime also requires shared memory and thread spawning through Node Workers. The base PR replaces `lzzzz` with `lz4`. `lz4-sys 1.11.1` compiles its C sources against custom allocation shims on `wasm32-wasi*`, but excludes the Rust implementations when the target OS is WASI. Without equivalent symbols supplied by the consuming crate, test links fail on unresolved malloc, calloc, and free shims. Running these suites in CI gives the remaining WASI layers direct regression coverage instead of relying on native-only tests. ### How? The Node cargo target runner provides WASI preview1, imported shared memory, and the complete two-phase [`link-section` custom-section protocol](https://github.com/mmastrac/linktime/blob/ae29e51d94a955df2442ed6418b8a712c1f2bfb3/link-section/docs/PREAMBLE.md#wasm). [`@emnapi/wasi-threads`](https://github.com/toyobayashi/emnapi/tree/main/packages/wasi-threads) supplies Worker lifecycle, compiled-module transfer, load/start ordering, and cleanup for the [`wasi.thread-spawn`](https://github.com/WebAssembly/wasi-threads) import that Node's WASI runtime does not provide. The module is compiled once, then the structured-cloneable `WebAssembly.Module` and shared memory are sent to Workers. A small adapter around the package's lower-level `ThreadManager` and `ThreadMessageHandler` APIs preserves Rust's original one-argument ABI: the high-level `WASIThreads` wrapper requires guest `malloc`/`free` exports for that ABI, and Rust test binaries do not export them. Worker identifiers come from a shared atomic counter because Tokio Workers can spawn additional Workers while the main WebAssembly thread is parked. On WASI, `turbo-persistence` supplies the three allocation symbols required by liblz4 and forwards them to WASI libc, keeping allocation and deallocation within the same allocator. Native builds do not compile these wrappers. Tests that depend on unavailable platform behavior remain visible as WASI-only ignored tests with explicit reasons. Native runs keep the full test set enabled. A following stacked PR replaces the unsupported `parking_lot` parker and re-enables the corresponding contention tests. The workflow installs and builds the workspace before running the host because both the package dependency and compiled production support used by a later stack layer must be available. ### Verification - Node 20 host unit tests: 9/9 - Exact combined WASI CI command: `turbo-rcstr` 12 passed / 1 ignored; `turbo-tasks` 68 / 44; `turbo-tasks-backend` 65 / 35 - Full 111-test `turbo-tasks` binary through the package-managed host: 95 passed / 16 ignored - No Worker-side `WebAssembly.compile`; the compiled module is transferred by `ThreadManager` - `pnpm install --lockfile-only --offline` with a frozen lockfile <!-- NEXT_JS_LLM --> <!-- fleet b6d0486f-97c7-42a7-bdaf-3490774cdec3 --> Co-authored-by: vercel-fleet-prod[bot] <318278635+vercel-fleet-prod[bot]@users.noreply.github.com> Co-authored-by: Luke Sandberg <210140+lukesandberg@users.noreply.github.com> Co-authored-by: Tobias Koppers <1365881+sokra@users.noreply.github.com>github.com-vercel-next.js · 4811e49e · 2026-09-23
- 0.9ETVfix(turbo-persistence): preserve write errors on writer drop (#98840) ### What? Preserve write failures from streaming SST construction instead of replacing them with debug-only writer lifecycle assertions. This adds explicit cancellation, immediate best-effort cleanup of incomplete SSTs, and focused coverage for failed writes and finalization. ### Why? `std::thread::panicking()` is false while an error is propagated through `Result`. On disk-full and similar failures, writers and their owning collectors are therefore dropped before a caller begins panic unwinding, allowing lifecycle assertions to obscure the original I/O error. Leaving partial files until startup recovery also delays reclaiming scarce disk space. ### How? `StreamingSstWriter::cancel()` consumes an incomplete writer, discards buffered bytes without flushing or finalizing, closes the raw handle, and best-effort removes the partial SST. Failed close attempts and unfinished Drop paths use the same cleanup, while deletion failures are ignored so they cannot replace the root error; existing startup recovery remains the fallback. Writer-owning scopes preserve the original error, explicitly cancel their writers, and then return that error. The compaction merge scope cancels both collectors on every error exit so a sibling collector cannot mask the failure. Successful close marks the finalized SST for preservation, and healthy writers still have to call `close()` or `cancel()` to satisfy the debug lifecycle invariant. The regression tests substitute an unbuffered read-only output handle to force deterministic add and close failures without requiring disk pressure. ### Verification - `cargo test -p turbo-persistence` (114 passed) - `cargo check --release -p turbo-persistence` - `cargo fmt --package turbo-persistence -- --check` <!-- NEXT_JS_LLM --> <!-- fleet da0c4e98-b86a-4c93-a519-8a63147e3f5c --> --------- Co-authored-by: vercel-fleet-prod[bot] <318278635+vercel-fleet-prod[bot]@users.noreply.github.com> Co-authored-by: Tobias Koppers <1365881+sokra@users.noreply.github.com>github.com-vercel-next.js · becee66e · 2026-09-22
- 0.9ETVSequence CI for branch-based PR stacks (#99086) ### What? Replace the Graphite-specific CI optimizer with a **read-only TypeScript gate** for PR stacks formed by ordinary GitHub base/head branch links. This PR includes the work previously reviewed in #99116 and #99122. ### Why? Rebasing a large stack starts expensive `build_and_test` jobs on every PR at once. Let the first three PRs and the top PR run immediately, but defer middle PRs until nearby CI provides a useful signal. A deferred PR must **not** appear mergeable just because its jobs have not started. ### How? - **Find the stack from branch relationships**, not GitHub stack metadata. Middle PRs poll their three nearest predecessors every **five minutes**. Any predecessor with a successful, current-head/current-base `thank you, next` check releases full CI; three unsuccessful results **fail the gate and required aggregate**. While waiting, the gate stays in progress. After five hours without a decision, it releases full CI rather than stranding the PR. - **Revalidate before deciding.** Refresh PR and check data before release/failure so rebases, retargeting and reruns cannot make a stale check decisive. Transient GitHub 5xx/429 errors retry; permanent errors or ambiguous topology start full CI rather than claiming CI passed. - **Keep polling cheap and read-only.** Reuse PR head/base data from `pulls.list` (11 → **8 REST reads** per ordinary waiting poll), while still checking *all three* predecessors. The bundled action is checked out at the current test-merge `${{ github.sha }}` without persisted credentials; forks bypass the checkout/gate and run full CI. The six-hour gate stays on `ubuntu-latest` because `ubuntu-slim` has a 15-minute limit. <details> <summary>Implementation and review tradeoffs</summary> - The local action bundles a pinned Octokit. Isolated Jest tests use fake timers and a real check-run response fixture. CI runs the existing Next.js lint/examples/externals/browser checks before action-specific install, typecheck, build and Jest; a generated-diff guard catches stale bundles. - `uses: ./...` requires action files already in the job workspace; it does not fetch them like `owner/repo@ref`. The sparse checkout fetches only `action.yml` and `dist/index.js`. Caller and reusable workflow grant read-only `checks`, `contents` and `pull-requests` permissions; the gate inherits no secrets. - Cheap-check graph splitting was deliberately deferred. Job-level concurrency does not enforce predecessor *success*; stopping at the first pending predecessor misses an older success. GraphQL batching needs a verified check-association projection and measured cost before replacing the current REST lookup. - When the child commits became reachable from this branch, GitHub automatically marked #99116 and #99122 merged; no PR merge operation was used. [Action README](https://github.com/vercel/next.js/blob/ci/branch-stack-polling-gate/.github/actions/pr-stack-ci-gate/README.md) documents the contract. </details> ### Verification - **Focused checks:** TypeScript, Prettier, deterministic bundle/diff checks and **24/24 Jest** passed. The final comment-only change in `src/gate.ts` produced a byte-identical bundle. The [current root run](https://github.com/vercel/next.js/actions/runs/35967795126) passed its gate, lint and required `thank you, next` on **attempt 2** (same SHA); attempt 1 failed an intermittent redbox test. - **Live six-PR test on the compiled action:** the first three and top passed the gate immediately. Middle #99097 and #99098 **waited**. On initial attempts, both **failed closed** when their three predecessors were unsuccessful, with no expensive jobs started. On rerun, #99097 **released on #99086's success** despite #99096 failing and #99095 still pending; full CI ran but failed unrelated tests. #99098 waited for *its own* three predecessors, then failed closed again. Gate release never marked full CI successful. - **Known limitation:** product-test failures—not the gate—left four immediate/released PRs with failed full CI on various attempts. All five test drafts remain open for reuse. Workflow reruns in this experiment were initiated externally; this agent's GitHub integration received 403 (`actions=write` required) when attempting a rerun. <details> <summary>Current compiled-action six-PR results, with workflow and gate logs (2026-09-24)</summary> Each draft is one Jest assertion commit over its signed parent; the cumulative focused suite passes **24/24**. Times below are UTC. “Required” refers to `thank you, next`, not the gate check. | Position | PR / signed head / workflow | Gate outcome | Required check | Expensive work | | :-- | :-- | :-- | :-- | :-- | | 1 | [#99086](https://github.com/vercel/next.js/actions/runs/35967795126) `85444c9e` | immediate pass | failed attempt 1; **passed attempt 2** | ran twice | | 2 | [#99095](https://github.com/vercel/next.js/actions/runs/35969902709) `bc19787a` | immediate pass | failed attempts 1 and 2 | ran twice | | 3 | [#99096](https://github.com/vercel/next.js/actions/runs/35970016367) `98bfab58` | immediate pass | failed | ran | | 4 | [#99097](https://github.com/vercel/next.js/actions/runs/35970109573) `e260bf96` | [attempt 1](https://github.com/vercel/next.js/actions/runs/35970109573/job/107537584401): waited → failed 08:02; [attempt 2](https://github.com/vercel/next.js/actions/runs/35970109573/job/107609256427): **released 11:19** | failed both attempts (attempt 2: product tests) | none on attempt 1; full CI ran on attempt 2 | | 5 | [#99098](https://github.com/vercel/next.js/actions/runs/35970206069) `01222d4a` | [attempt 1](https://github.com/vercel/next.js/actions/runs/35970206069/job/107537887530): waited → failed 08:03; [attempt 2](https://github.com/vercel/next.js/actions/runs/35970206069/job/107609367420): waited → failed 11:50 | failed both attempts | **never started** | | 6 (top) | [#99099](https://github.com/vercel/next.js/actions/runs/35970312588) `c6f3cf4a` | immediate pass | failed attempts 1 and 2 | ran twice | **Decision evidence:** On attempt 1, #99097 saw #99096/#99095/#99086 all unsuccessful, and #99098 saw #99097/#99096/#99095 all unsuccessful; their required checks failed and downstream jobs were skipped *because their gates failed*. After #99086's successful rerun, #99097 attempt 2 logged `#99096=unsuccessful, #99095=waiting, #99086=success`; its gate passed and [build-next actually ran and passed](https://github.com/vercel/next.js/actions/runs/35970109573/job/107609336752). #99098 could not use #99086 (outside its nearest-three window); it failed at 11:50 when #99097/#99096/#99095 had all finished without success. Completed failed gates do not restart automatically when a predecessor reruns. </details> <details> <summary>Unrelated CI failures and recovered flaky tests</summary> - #99086's attempt 1 failed `lazy-dynamic-imports › does not parse a dynamic import target before activation`: redbox source was `null` after all in-job retries ([job](https://github.com/vercel/next.js/actions/runs/35967795126/job/107530613870)). **The same-head workflow passed on attempt 2.** - #99095, #99096, #99097 attempt 2 and #99099 failed `instant-insights-tab-overlay › should wrap the mobile overlay header only when it does not fit`: observed header top difference **24**, expected **< 4**, after in-job retries ([example job](https://github.com/vercel/next.js/actions/runs/35969902709/job/107537341995)). #99096 also failed `turbopack-loader-file-dependencies › should update when a build dependency changes` (`build-one` instead of `build-two`, [job](https://github.com/vercel/next.js/actions/runs/35970016367/job/107537932737)). #99097 attempt 2 additionally failed the `lazy-dynamic-imports` redbox assertion in an experimental cache-components shard ([job](https://github.com/vercel/next.js/actions/runs/35970109573/job/107609732882)). These failures are not gate-test failures. - **Recovered in-job flakes:** #99096 `instant-validation/head-and-reporting › invalid - runtime prefetch - dynamic viewport blocks navigation` (redbox did not open; passed retry 1/2); #99095 `use-cache-without-experimental-flag › should recover from the build error if useCache flag is set` (Playwright execution context destroyed on navigation; passed retry 1/2); #99097 attempt 2 `lazy-dynamic-imports` in a different Turbopack-dev shard (passed retry 2/2), `instant-validation/suspense-boundaries` (redbox did not open; passed retry 2/2), and `instant-validation/head-and-reporting.partial-prefetching` (redbox did not open; passed retry 1/2). </details> <details> <summary>Earlier implementation smoke tests and CJS-gate stack runs</summary> - The [folded-code root head `89f0ca34` full run](https://github.com/vercel/next.js/actions/runs/35965443069) passed gate, build, lint (including **24/24 Jest**) and required `thank you, next`. The earlier [compiled-action child head `25647f00` run](https://github.com/vercel/next.js/actions/runs/35922700289) also passed full CI, proving same-SHA sparse checkout before the lint-order follow-up was folded into root. - **CJS-gate fail-closed run (base `ec0abf41`, 2026-09-23):** [#99086](https://github.com/vercel/next.js/actions/runs/35875541530), [#99095](https://github.com/vercel/next.js/actions/runs/35875598833), [#99096](https://github.com/vercel/next.js/actions/runs/35875651890) and top [#99099](https://github.com/vercel/next.js/actions/runs/35875952426) ran full CI and failed unrelated tests. Middle [#99097](https://github.com/vercel/next.js/actions/runs/35875701404/job/107231099534) failed its gate at 15:20 and [#99098](https://github.com/vercel/next.js/actions/runs/35875873698/job/107231827956) at 15:21; required checks failed and expensive jobs never started. A Turbopack source-map snapshot failure was subsequently fixed in [#99106](https://github.com/vercel/next.js/pull/99106). - **CJS-gate release run (base `8ff7f2bd`, 2026-09-23):** [#99086](https://github.com/vercel/next.js/actions/runs/35887913444) and [#99095](https://github.com/vercel/next.js/actions/runs/35887957948) passed full CI. [#99097](https://github.com/vercel/next.js/actions/runs/35888074168/job/107273254039) and [#99098](https://github.com/vercel/next.js/actions/runs/35888146874/job/107273514756) waited, then released at the next poll on #99095's green check while a nearer predecessor was unresolved. Their later full CI failed; top [#99099](https://github.com/vercel/next.js/actions/runs/35888202575) started immediately (later canceled). These precede the TypeScript action and are not claimed as evidence for its current head. </details> <!-- NEXT_JS_LLM --> <!-- fleet d2ae64f1-7106-4ba4-b88a-7785c19904c6 --> --------- Co-authored-by: vercel-fleet-prod[bot] <318278635+vercel-fleet-prod[bot]@users.noreply.github.com> Co-authored-by: Tobias Koppers <1365881+sokra@users.noreply.github.com>github.com-vercel-next.js · 10c5a6ac · 2026-09-25
- 0.7ETVfix(turbopack): resolve `../` and `/`-rooted `import.meta.glob` patterns (#96557) ### What? `import.meta.glob()` now resolves `../`-relative and `/`-project-root-absolute patterns (and `base`), validates every pattern (positive and negative) for whether it escapes the filesystem root, and shares the underlying "resolve leading `./`/`../` against a directory" primitive with `next-core::relativize_glob` (used by `outputFileTracingIncludes`) instead of duplicating it. ### Why? `import.meta.glob()` handed its patterns straight to Turbopack's `Glob`, which matches relative to the scanned directory and understands neither a leading `/` nor `..` segments. `import.meta.glob('../../content/*.js')` and `import.meta.glob('/content/*.js')` therefore silently returned `{}`, and a `base` that walked above the filesystem root failed with an internal error and a panic report instead of a normal diagnostic. A negative (exclusion) pattern that escaped the root was silently dropped, which is the worst outcome for an exclusion — it would silently include files the user asked to exclude. ### How? - Patterns are split into the directory they're rooted in and the remaining glob, matched relative to a single common scan directory covering every positive and negative pattern. `/` is resolved from the project root, matching Vite's rule that patterns are relative to the importing file or absolute from the project root. - Map keys follow the pattern too: `base` now produces base-relative keys (`'./zeta.js'` instead of `'../../content/zeta.js'`, matching Vite), a project-root-absolute pattern produces project-root-absolute keys, and origin-relative keys are unchanged otherwise. - Every pattern is validated up front; one that leaves the project (a `base`, a positive pattern, or a negative one) reports an error issue at the call site and yields an empty object, instead of crashing or being silently dropped. - The `./`/`../`-resolution loop is extracted into `turbo_tasks_fs::glob::relativize_glob`, a crate both `next-core` and `turbopack-ecmascript` already depend on. `next-core::relativize_glob` is now a thin wrapper over it (same name, signature and error message); its 5 unit tests are removed since the shared primitive's own tests cover the same cases. <!-- NEXT_JS_LLM --> Co-authored-by: Niklas Mischkulnig <4586894+mischnic@users.noreply.github.com> --------- Co-authored-by: vercel-fleet-prod[bot] <318278635+vercel-fleet-prod[bot]@users.noreply.github.com> Co-authored-by: Niklas Mischkulnig <4586894+mischnic@users.noreply.github.com> Co-authored-by: Tobias Koppers <1365881+sokra@users.noreply.github.com>github.com-vercel-next.js · ed1aab5d · 2026-09-10
- 0.5ETVOptimize strict module factory chunks (#98421) ### What? Reduce Turbopack ECMAScript chunk size by emitting strict module factories in a shared strict-mode scope instead of repeating `"use strict"` in every factory. The chunk format now chooses among the existing flat representation, a mixed representation with flat non-strict factories plus a nested strict array, and an all-strict representation that wraps the complete chunk in a strict IIFE. Each factory is generated through the existing single code path; the production minimizer removes its redundant directive when the factory is created inside a strict wrapper. Browser and Node.js emitters share the mode selection and serialization implementation, while the runtime remains compatible with the existing flat format. ### Why? ECMAScript modules are always strict, so large chunks currently repeat the same directive across many factories. Moving strictness to the scope where those factories are created removes redundant bytes without copying arrays or changing strict/sloppy execution semantics. For `bench/basic-app`, this reduces total raw JavaScript by 16,831 bytes (0.161%). Partitioning factories changes compression behavior, resulting in a 1,286-byte (0.051%) gzip increase across all JavaScript; format selection therefore remains based on emitted raw bytes. ### How? - Determine each chunk item's strictness from the already-collected chunk metadata. - Generate every factory once with its normal directive, then rely on the minimizer to remove redundant directives inside strict wrappers. - Compare the expected minified directive bytes removed with the exact wrapper bytes added and keep small chunks in the existing flat format. - Keep non-strict factories flat in mixed chunks and append strict factories as an array produced by a strict IIFE. - Wrap the complete registration/export in a strict IIFE when every factory is strict, keeping those factories flat and avoiding a nested array entirely. - Use the shorter arrow IIFE when the target supports it, with a function IIFE fallback for older targets. - Preserve source-map sections, scope-hoisted module IDs, factory naming, and ordering within each partition. - Add focused strict, sloppy, mixed, all-strict, old-target, and below-threshold coverage and update affected Turbopack snapshots. ### Verification - `cargo nextest run -p turbopack-tests -E 'test(snapshot)'` - `cargo clippy -p turbopack-ecmascript -p turbopack-browser -p turbopack-nodejs` - `pnpm build-all` - `pnpm --dir turbopack/crates/turbopack-ecmascript-runtime/js check:nodejs` - `bench/basic-app` production Turbopack build, repeated to confirm deterministic size totals <!-- NEXT_JS_LLM --> <!-- fleet 528ffe69-eb27-466b-a132-842272a782cf --> --------- Co-authored-by: vercel-fleet-prod[bot] <318278635+vercel-fleet-prod[bot]@users.noreply.github.com> Co-authored-by: Tobias Koppers <1365881+sokra@users.noreply.github.com>github.com-vercel-next.js · 39f5d797 · 2026-09-14