755 Commits

Author SHA1 Message Date
Luke Sandberg 228df5fd74 [backport] Retain fewer stale cache versions and use a TTL, plus the mtime fallback (#97304)
Backports the following to `next-16-3`, as a pair:

- #96941 — "[turbopack] Retain fewer stale cache versions and use a TTL"
(`ae1e53a`)
- #97268 — "Add the mtime fallback back" (`52a3410`)

The second fixes a bug in the first: #96941 changes the `CURRENT` file
format from a bare
big-endian `u32` to a JSON object, and an existing cache directory in
the old format could no
longer be aged. #97268 adds the mtime fallback so an unparsable
`CURRENT` is aged by its mtime
instead of failing. They only make sense together, hence one branch.

Both cherry-picked with `-x`, no conflicts. Every touched source file is
byte-identical to canary.

### Verification on this branch

- `cargo check`, `cargo clippy --all-targets`, `cargo fmt --check` on
`turbo-persistence` and `turbo-tasks-backend` — clean
- `cargo test -p turbo-persistence -p turbo-tasks-backend` — all pass, 0
failures
- includes the `db_versioning` tests, notably
`test_unparsable_current_falls_back_to_mtime::case_1_old_u32_format`
(the old-format compatibility case #97268 restores)
- `cargo metadata --locked` — `Cargo.lock` consistent with the
manifests, no lockfile update needed
- `pnpm --filter=next types`, prettier, eslint — clean
- `pnpm build-all` — 18/18 tasks
- `test/production/next-server-nft` — 5/5 tests, 5/5 snapshots pass
(covers the traced `turbopack-cache-seed.js`)

### End-to-end verification against a real old-format cache

`create-next-app@16.3.0` + `next build` (with
`turbopackFileSystemCacheForBuild`) on released
`next@16.3.0` writes a `CURRENT` in the old pre-JSON format — a bare
big-endian `u32`
(`0000 0008`), 30M cache. This branch was then applied with
`pnpm pack-next -p <app> -- --release` (release binary confirmed to
contain
`TURBO_ENGINE_VERSION_TTL_DAYS` and the new `CURRENT` field names), and
two arms were run from
an identical starting state — same cache, same code, only the env var
differing:

| Arm | Build | Cache dirs after |
| --- | --- | --- |
| `TURBO_ENGINE_VERSION_TTL_DAYS=0` | exit 0 | old dir **deleted**, 1
remains |
| default TTL (3 days) | exit 0 | old dir **retained**, intact at 30M, 2
remain |

The retention arm is the meaningful control: without #97268's fallback,
`read_current_version`
would error on the bare-`u32` file, the age would become
`Duration::MAX`, and the directory would
be evicted in *both* arms. Retention under the default TTL is direct
evidence the mtime fallback
works.

Also confirmed:

- new `CURRENT` is written as JSON:
`{"max_sequence_number":8,"commit_time":"..."}`
- retention cap holds: seeded 4 version dirs → build left exactly 2
(current + one most-recently-used other)
- clean deletion, no `DELETION_PREFIX` leftovers in any run
- `TURBO_ENGINE_VERSION_TTL_DAYS=notanumber` warns (`expected a whole
number of days`) and falls back to the 3-day default rather than failing

<!-- NEXT_JS_LLM -->
2026-08-13 21:43:26 +00:00
Luke Sandberg 6b3677887a [backport] [turbopack] Add turbopack_ecmascript and turbopack_wasm's embeded FS to internal_assets_conditions (#96655)
Backports #96600 ([turbopack] Add `turbopack_ecmascript` and
`turbopack_wasm`'s embeded FS to `internal_assets_conditions`) to
`next-16-3`.


<!-- NEXT_JS_LLM -->

Co-authored-by: Sam Poder <sampoder@berkeley.edu>
2026-08-04 18:38:30 +00:00
Will Binns-Smith 8cfa551041 Turbopack: extract chunk list version/update into turbopack-ecmascript (#95661)
This moves modules out of `turbopack-browser` and into
`turbopack-ecmascript` so it can be used in the server hmr
implementation as well.

Move the chunk-list version, update, and merged-update wire types out of
`turbopack-browser` into a shared `turbopack-ecmascript::chunk_list`
module so both the browser and node chunking contexts can build on them.
Rename `EcmascriptDevChunkListVersion` to `ChunkListVersion` and make
`update_chunk_list` runtime-agnostic (plain path->`VersionedContent`
maps).

Pure refactor: no behavior change.

<!-- NEXT_JS_LLM_PR -->

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-07-28 13:49:30 -07:00
Benjamin Woodruff 3214020b2d Turbopack: Fix missing canonicalization of paths and always use verbatim paths internally for Windows (#95668)
I was seeing failures on https://github.com/vercel/next.js/pull/95628
and went down this rabbit hole.

- We were (incorrectly) not canonicalizing the `DiskFileSystem` root
dir. This happened to work out for us because pnpm also wasn't
canonicalizing junction point targets, and so the root dir prefix
matched. However after `#95628`, pnpm started canonicalizing junction
point targets because we added `pnpm-workspace.yaml` files. This was
just coincidence and indicative of much deeper problems.

- `dunce` isn't a good fit for us. It picks a win32 or verbatim
representation based on path length, which would mean that long paths
inside of a short root path would never work because we'd never be able
to strip the prefix, since we'd be looking for a win32 path prefix.
Omnipath is better for what we want to do:
https://docs.rs/omnipath/latest/omnipath/windows/trait.WinPathExt.html

- The windows verbatim path format works with long paths (>260
characters) and behaves a lot closer to cross-platform unix paths, which
is what we want. E.g. if somebody tries to import a JS module from a
Windows 8.3 short path, it should fail. This will fail with verbatim
paths (good!). This PR *always* uses the verbatim path format internally
within `DiskFileSystem`.

- `read_link` was re-inventing `try_from_sys_path` but badly. Just call
`try_from_sys_path`.

- `turbopack/crates/turbo-tasks-fs/src/embed/file.rs` was dead code.

- `validate_path_length_inner` was incorrectly referring to verbatim
paths as UNC paths. These are two different things.

- Canonicalize any absolute symlink targets outside of the
`DiskFileSystem` root path, resolving any symlinks they may have,
normalizing case-insensitive paths, and handling Windows 8.3 short name
format.

## Some Context About Windows Path Formats:

- win32 paths: These are your normal user-friendly `C:\foo\blah` paths.
When calling most filesystem APIs, these are limited to 260 characters
unless the user has enabled long paths on the system, and avoiding the
limit with these paths also requires a [`longPathAware` application
manifest](https://learn.microsoft.com/en-us/windows/win32/sbscs/application-manifests#longPathAware).
You can mix `/` and `\` in win32 paths, and Windows will normalize the
path separator for you.

- verbatim paths: This is a rust-specific name (used by the stdlib) for
the "extended length" prefixed paths. These are paths starting with
`\\?\`. These paths can be up to ~32,767 characters long. They do not
support Windows 8.3 shortnames. They do not normalize between `/` and
`\`, and do not support `.` or `..` components, however
[`Path::join`](https://doc.rust-lang.org/std/path/struct.Path.html#method.join)
will do this normalization if given a verbatim path.

- Windows 8.3 shortnames: A legacy format where paths can be represented
by 8 characters, followed by a tilde and a digit (or hash in some
extreme cases where there are many collisions), followed by a 3
character file extension. NTFS supports this,
[ReFS](https://learn.microsoft.com/en-us/windows-server/storage/refs/refs-overview)
(used by [Windows Dev
Drive](https://learn.microsoft.com/en-us/windows/dev-drive/)) does not.

- UNC ("Universal Naming Convention"): This is a specific representation
of paths used for remote files on network servers.

- `file://` URIs: These must be formed from [win32 style
paths](https://docs.rs/url/latest/url/struct.Url.html#method.from_file_path),
though the normal 260 character limit typically does not apply here.

- canonicalize: Corresponds to
[`std::fs::canonicalize`](https://doc.rust-lang.org/stable/std/fs/fn.canonicalize.html).
This involves actual fs operations (that we can't easily track), and
recursively resolves symlinks. It will fail if the file path does not
exist. On windows, this always returns a verbatim path.

- wide format: Windows paths are utf-16. Generally we don't support
paths that can't be encoded to utf-8, but we should try to fail as
obviously as we can in these cases. We don't have good filesystem issue
handling yet though.

- case sensitivity: Generally paths are not case-sensitive on Windows or
macOS, but are case-sensitive on most Linux filesystems. The exact
definition of how case sensitivity works with unicode paths is highly
platform and filesystem dependent.

- junction points: The windows equivalent of symlinks (symlinks are also
supported, but require developer mode to be turned on). These can
contain an arbitrary path reference in any format (win32, verbatim, etc)
with any case and with windows 8.3 short names. Junction points are
always target absolute paths to directories.
2026-07-21 14:54:01 -07:00
Luke Sandberg aaf2fe3d58 [turbopack] Optimize the implementation of AutoMap/AutoSet
(#95694)

Optimize the implementation of AutoMap/AutoSet.

Previously the `List` variant was backed by a `SmallVec` in inline mode,
which needs at least 24 bytes of header (`len`, `cap`, `ptr` — all
`usize`) even though the list never holds more than 32 elements.

This replaces it with the `TinyVec` struct from turbo-tasks-backend
which is now enhanced with support for an `inline` array. The length is
a `NonZeroU8`, which reserves a niche the `AutoMap` enum folds its
discriminant into — dropping `AutoMap`'s minimum size to 16 bytes. This
in turn shrinks `TaskStorage` and the `LazyField` enum. Some inline
structs in TaskStorage are increased in size so we keep the 128 byte
footprint
2026-07-14 16:35:00 -07:00
Niklas Mischkulnig d774f7db29 Upgrade to swc 73 (#95731)
- [x] Waiting for a swc major bump in
https://github.com/swc-project/plugins...
2026-07-14 08:03:43 +02:00
Luke Sandberg cd24304296 Turbopack: add more context to persistence file errors (#95318)
### What?

Adds path- and operation-level context to the filesystem errors raised
by the `turbo-persistence` commit/rollback flow and the
`turbo-tasks-backend` database invalidation/versioning helpers.

### Why?

A user reported a bare `Access is denied. (OS error 5)` on Windows 11
that surfaced out of the cache `commit` flow with no indication of which
file or which filesystem operation failed:
https://github.com/vercel/next.js/discussions/95130#discussioncomment-17479429
The underlying issue was fixed in #95495 but this adds more information
for future issues.

Raw `io::Error`s from `?` carry an OS error code but no path, so there
was no way to tell whether the failure was on `CURRENT`, a `.del` file,
a per-file `fsync`, the directory `fsync`, or a cleanup `remove_file`
etc.

### How?

- Adopted the `fs_err` crate for all fs operations in the persistence
layer
- Added additional context to some flows
- Unified `CURRENT` file writing and adopted an 'atomic write' pattern.

<!-- NEXT_JS_LLM_PR -->
2026-07-07 23:25:42 +00:00
Niklas Mischkulnig 3e11c728e5 Upgrade to swc 72 and rust-react-compiler (#95536)
Fixes #95257
Fixes #95372
2026-07-07 07:27:34 +02:00
Luke Sandberg 1b1cf7b4b9 Fix #95015 by pulling a fix to scattered-collect (#95098)
Bump link-section to pick up
https://github.com/mmastrac/linktime/pull/481

This fixes a bug in linktime that could trigger a miscompilation on
windows, see https://github.com/mmastrac/linktime/issues/479 for a
detailed description

@bgw and I both confirmed that this fixed the windows crash, though we
have failed to produce a reproduction. This appears to not be triggering
in CI due to a different release profile.
2026-06-23 20:30:54 +00:00
Niklas Mischkulnig 2d1d5268fb Upgrade to swc 70 (#94814)
<!-- Thanks for opening a PR! Your contribution is much appreciated.
To make sure your PR is handled as smoothly as possible we request that
you follow the checklist sections below.
Choose the right checklist for the change(s) that you're making:

## For Contributors

### Improving Documentation

- Run `pnpm prettier-fix` to fix formatting issues before opening the
PR.
- Read the Docs Contribution Guide to ensure your contribution follows
the docs guidelines:
https://nextjs.org/docs/community/contribution-guide

### Fixing a bug

- Related issues linked using `fixes #number`
- Tests added. See:
https://github.com/vercel/next.js/blob/canary/contributing/core/testing.md#writing-tests-for-nextjs
- Errors have a helpful link attached, see
https://github.com/vercel/next.js/blob/canary/contributing.md

### Adding a feature

- Implements an existing feature request or RFC. Make sure the feature
request has been accepted for implementation before opening a PR. (A
discussion must be opened, see
https://github.com/vercel/next.js/discussions/new?category=ideas)
- Related issues/discussions are linked using `fixes #number`
- e2e tests added
(https://github.com/vercel/next.js/blob/canary/contributing/core/testing.md#writing-tests-for-nextjs)
- Documentation added
- Telemetry added. In case of a feature if it's used or not.
- Errors have a helpful link attached, see
https://github.com/vercel/next.js/blob/canary/contributing.md

### Signed commits

- This repository requires verified commit signatures on protected
branches.
- If this pull request is blocked for unsigned commits, re-sign the
commits and force-push the branch.
- A `Signed-off-by` line in the commit message is not enough.


## For Maintainers

- Minimal description (aim for explaining to someone not on the team to
understand the PR)
- When linking to a Slack thread, you might want to share details of the
conclusion
- Link both the Linear (Fixes NEXT-xxx) and the GitHub issues
- Add review comments if necessary to explain to the reviewer the logic
behind a change

### What?

### Why?

### How?

Closes NEXT-
Fixes #

-->

---------

Co-authored-by: Will Binns-Smith <wbinnssmith@gmail.com>
2026-06-18 17:31:43 -07:00
Luke Sandberg 723c06200d switch our test benchmark runs to release-with-assertions (#94538)
In ci when testing our benchmarks use our test profile
`release-with-assertions`

This avoids an expensive `lto` step and ensures that our tests run with
debug asserts.

I noticed that our workflow for 'test cargo benches' was very slow and a
lot of that time was the build presumably due to lto overheads across so
many benchmark binaries.


Compare:

* cargo benches for this pr:
https://github.com/vercel/next.js/runs/79974366863?pr=94538
   - build=4m42s
   - test=1m46s
* cargo benches before this pr:
https://github.com/vercel/next.js/actions/runs/27097792020/job/79973082715#logs
   - build=8m23s
   - test=6m56s

Of course doing this revealed a few things

* a birthday paradox panic in one of the persistence tests
* some 'top level read' errors to address in the benchmarks and in the
turbopack-cli server used for some benchmarks.

And finally, that the persistence tests are just extremely slow in test
mode due to their initialization overhead (populating >1GB dbs), so that
harness is rewritten to remove some variants and also to reduce max
sizes in test mode
2026-06-17 16:05:05 +00:00
Will Binns-Smith 77f02f8583 Turbopack: add experimental React compiler support (#94573)
- Adds experimental.rustReactCompiler config option to the next.js
config to opt in. It should be used alongside `config.reactCompiler` and
errors if not set.
- Only runs the compiler on client code (and SSR), not RSC
- When enabled, runs the compiler directly on Turbopack's swc AST with
no gen+reparse. This required passing through the original source text
into source transforms.
2026-06-15 15:31:02 -07:00
Luke Sandberg f2ba54b4cf migrate turbo-tasks to scattered collect (#94503)
# What
This uses `scatter_collect` from @mmastrac to register all values,
traits and value impls of traits. now all data can be consistently
accessed when constructing the VALUEs registry, eliminating a number of
racy access opportunities.


# Why
Scatter collect uses link sections to gather registries at link time,
this resolve some inherent brittleness in constructor functions since
the relative execution order is **not guaranteed** and it is also not
even possible to _detect_ execution that is too early (early access to
an `inventory` collection will miss some entries silently)

We have a single report that appears to be due to a race like this

```
web:dev: thread 'tokio-rt-worker' (41574444) panicked at turbopack/crates/turbo-tasks/src/macro_helpers.rs:274:13:
web:dev: no trait impl registered for value type turbopack_core::module_graph::ModuleGraphImportTracer
web:dev: note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
```

I do not entirely understand how this is possible since constructors
should complete prior to the first napi entry point (other than
`napi::module_init` which is itself a constructor).

Our initialization logic is fundamentally brittle since it relies on

* an `inventory` of all value types
* an `inventory `of all trait types
* a set of ctor functions to register trait implementations (basically
(value, trait) pairs)

All of these need to be initialized/invoked before the `VALUES` registry
is accessed, but the above panic implies that the ctor function for
`ModuleGraphImportTracer` to register its implementation of
`ImportTracer` hasn't occurred yet.

By moving the registies to link time we completely eliminate all startup
races since the linker does all the 'gathering', now the only possible
concern is that we attempt to downcast a trait prior the the `VALUES`
registry completing which does not appear to be possible.


# Binary size impact

Measured on `next-swc.darwin-arm64.node` built with `pnpm --filter
@next/swc build-native-release` (release profile), this branch vs
`canary`. Stripped = `strip -x`; gzip = `gzip -9` of the stripped
binary.

| Metric | branch | canary | Δ | Δ% |
|---|---:|---:|---:|---:|
| **raw** | 122,879,888 B | 124,184,976 B | **−1,305,088 B** |
**−1.051%** |
| **stripped** | 83,505,264 B | 84,127,232 B | **−621,968 B** |
**−0.739%** |
| **gzip+stripped** | 28,800,547 B | 28,925,679 B | **−125,132 B** |
**−0.433%** |

The addon shrinks on every metric. Replacing the generated per-impl
registration functions (and their `ctor`/`inventory` machinery) with
link-time scattered collection removes real code/data — ~622 KB even
after stripping — and laying the registries out contiguously at link
time compresses better, so the gzip win holds up too.

<!-- NEXT_JS_LLM_PR -->
2026-06-11 21:15:48 +00:00
Luke Sandberg a683d06fbb [tubopack] migrate rcstr! to use scattered collect (#94498)
### What?
Adopt
`[scattered_collect`](https://docs.rs/scattered-collect/latest/scattered_collect/)
for collecting static rcstr values.

### Why?

Built by our good friend @mmastrac to solve some of our problems.
Replaces inventory with a simpler linker driver approach.

Basically inventory works using constructor functions to build a linked
list, then iterating follows the list. `scattered_collect` on the
otherhand uses a linker section for the data, so it all gets moved to a
dedicated part of the binary and bingo, a slice!. This means we don't
need `init` time logic.

This is preparation for migrating other uses of `inventory` just to
prove it out. Which was a good idea because we found and fixed a bug!
See
https://github.com/mmastrac/linktime/releases/tag/link-section-0.18.2

## Wasm

scatter_collect supports wasm, but requires us to register a 'section
reader' function with the runtime, see
https://docs.rs/link-section/latest/link_section/#wasm. The instructions
are staightforward but a little tricky since we rely on `wasm-bindgen`
to constrct the `Wasm.Module` instance and it is encapsulated. So to
'inject' that function we would need to patch some of the bootstrapping.
Given that this registry is only an optimization for deserialization
which never happens in a wasm build i just flagged it all off.
2026-06-11 13:18:36 -07:00
Sam Poder 409fa64d16 [turbopack] Use an arena for JSValues (#94297)
This PR takes the arena introduced in
https://github.com/vercel/next.js/pull/94296 and uses it! It's large but
shouldn't change functionality beyond the use of the arena. Some syntax
related to pattern-matching had to change because `bumpalo`'s `Box`
doesn't support the same pattern matching that the standard library's
`Box` does but the code should be equivalent.

`bumpalo`'s `Bump` type implements `Send` but not `Sync`, to allow us to
pass a `Bump` into async-methods we wrap it in `ThreadLocal` (see
https://docs.rs/thread_local/latest/thread_local/). `ThreadLocal`
implements `Sync` by creating a new bumpalo allocator for each thread.
We've tried to minimise the usage of `ThreadLocal` to just where it was
needed. The downside of `ThreadLocal` is that we may find ourselves
allocating memory for multiple arenas instead of just one. This is more
time allocating if there are multiple threads compared to one shared
allocation, however, it still appears to be a performance improvement
over individually allocating memory for each `JSValue`

---------

Co-authored-by: Benjamin Woodruff <benjamin.woodruff@vercel.com>
2026-06-08 17:46:03 -07:00
Luke Sandberg 804b9a0bd4 [turbopack] Enable Effects to be evicted (#94173)
### What?
Enable Effects to be evicted.

Currently `EffectInstance` transitively holds a
`ReadRef<PersistentFileContent>` which means eviction cannot not free
the memory for the outputs. Because the `ReadRefs` are captured by the
task producing the `Effect` we cannot just drop them (otherwise we could
need to somehow re-execute the write to recover them). So instead we
need a new 'effect processing phase'

Here is our new state machine:
 * Phase 1: `emit` effects.
- At this point we capture the content as a `ResolvedVc` and the hash as
a value.
- In principle this means the hash and content can get out of sync, but
only within an eventual consistency race which callers should be
handling.
  * Phase 2: `take_effects`.
- This will call `capture()` on all the effects which will conditionally
read the content cell if the hash doesn't match the `EffectStateStorage`
  * Phase 3: `Effects::apply`.
- This will apply the effects for all effects whose hashes don't match
storage.
- At this point we may discover that a hash in storage changed between
`capture` and `apply` which will trigger a special `Retry` error and
`invalidate` `Phase 2`.

To facilitate the `Retry` logic a
`read_strongly_consistent_and_apply_effects` helper function handles it.
Which allows for a simple circuit breaker and some verbose
warnings/errors when we retry too many times.

Specifically:
- Split the `Effect` trait into `Effect` (emit-side, returns a
`Captured` payload) and `CapturedEffect` (apply-side). `
- Switch `Effects` to `cell = "new"`. Every producer re-execution
allocates a fresh cell value; the prior cell (and its captured payload)
drops on overwrite, cascade-releasing upstream
`ReadRef<PersistedFileContent>` strong counts. The captured payload also
drops naturally when the cell is evicted.
- Push the per-key state machine (`Unapplied` / `InProgress` / `Applied
{ value_hash, result }`, `EventGuard` panic recovery, `Event` listener
coordination) down into `EffectStateStorage::run_apply`.
`Effects::apply` becomes a thin aggregator over `dyn_apply()` calls.
- New `ApplyOutcome<E> { Failed(E), Retry }`. `CapturedEffect::apply`
returns this; `Retry` signals "capture elided content but storage
diverged before apply" — `Effects::apply` collects across the batch,
fires the producer's invalidator once, and surfaces
`EffectsError::Retry`.
- New `EffectStateStorage::matches_applied(key, hash)`.
`WriteEffect::capture` / `WriteLinkEffect::capture` consult this and
skip the `ReadRef<PersistedFileContent>` / `ReadRef<LinkContent>`
materialization when storage already holds `Applied { matching, Ok(())
}` — avoiding the disk-read + decompression hit on producer re-runs that
don't actually change output.
- 10 integration tests in
`turbopack/crates/turbo-tasks-backend/tests/effects.rs` cover the new
lifecycle: duplicate apply, sibling stomp re-apply, repeated apply with
unchanged state dedupe, `cell = "new"` producing distinct cells per
producer run, capture-skip on storage match, and the
capture-skip-then-stomp race that exercises the `Retry` path.

### Why?

A memory audit showed `EffectInstance` cells accumulating the majority
of the ram in an eviction session

#### Capture-time storage check (the perf story)

`Effect::capture` may consult `EffectStateStorage::matches_applied` and
elide the content materialization when storage already records the
target hash. `CapturedWriteEffect.content` becomes
`Option<ReadRef<PersistedFileContent>>` — `None` means "capture observed
`Applied { matching }`; no `body` to pass to `run_apply`". The
apply-side state machine then either dedup-hits (storage still matches →
cached `Ok`) or enters the no-body branch and returns
`ApplyOutcome::Retry`.

Reading shared mutable `EffectStateStorage` from inside a turbo-tasks
task is normally suspect (the state isn't a tracked input). It is sound
here because the apply-time re-check fires the producer's invalidator on
mismatch, turning the otherwise-untracked read into an
explicitly-tracked one via the `Retry` pathway. The producer reruns,
`capture` sees the new storage state, materializes content, and the next
`apply` succeeds.

#### `ApplyOutcome::Retry` and the invalidator

`Effects::apply` collects `Retry` signals across the parallel batch via
`AtomicBool`. `Failed(e)` errors fail-fast. After the batch, if any
`Retry` fired (and no `Failed`), the producer's invalidator runs once
and `EffectsError::Retry` propagates. Callers re-read the operation; the
producer reruns; fresh `capture` either materializes content (storage
diverged) or dedup-hits cleanly.

#### `turbo-tasks-fs`

`WriteEffect::capture` / `WriteLinkEffect::capture` call
`matches_applied` first and conditionally `.await?` the content
`ResolvedVc`. `CapturedWriteEffect::apply` /
`CapturedWriteLinkEffect::apply` dispatch through `run_apply(key, hash,
body)`, where `body` is `Some(closure)` iff content is `Some`.
`apply_inner` takes the unwrapped `&ReadRef<…>` as a parameter so the
option-unwrap is at the dispatch boundary (type-enforced).

### Risk: infinite retry loop

**Failure mode.** Two sibling producers A and B contend on the same key
with different hashes (e.g. two routes that both want to write
`chunks/foo.js` with different content). Each `Retry` from A's apply
invalidates A's producer; each `Retry` from B's apply invalidates B's
producer. In the worst case, A and B perpetually re-capture, race to
apply, stomp each other's storage state, and re-trigger Retry —
invalidating themselves on each cycle.

**Why we believe it terminates in practice.** A `Retry` only fires when
(a) capture observed `Applied { matching }` and elided content, and (b)
by apply time storage was stomped to a different hash. Each `Retry`
invalidates only its own producer (no cross-producer invalidation), so
without external input churn the system reaches a fixed point —
whichever producer applies last wins, and subsequent re-runs of the
loser's producer materialize content (because storage no longer matches)
and apply cleanly. The pathological case requires a real, ongoing race
fundamentally triggered by external invalidations (file edits).


<!-- NEXT_JS_LLM_PR -->
2026-06-07 22:33:00 +00:00
Luke Sandberg a0dd232358 Simplify turbo-tasks-backend: collapse single-impl traits and remove redundant Arc layers (#93983)
### What?

Simplifies `turbo-tasks-backend` by removing abstraction layers that
each had a
single implementation, plus several redundant `Arc` indirections that
were no
longer load-bearing.

**Traits removed / collapsed (each had exactly one implementor):**

- **`KeyValueDatabase`** — folded into inherent methods on its only
impl,
  `TurboKeyValueDatabase`.
- **`BackingStorage` + `BackingStorageSealed`** — folded into inherent
methods on
  the renamed concrete type `TurboBackingStorage` (was the
  `KeyValueDatabaseBackingStorage<T>` generic + a type alias).
- **`ConcurrentWriteBatch`** — folded into inherent methods on
`TurboWriteBatch`
  (it existed to support an old backend implementation that's gone).
- The **`B: BackingStorage` generic parameter** is dropped from
`TurboTasksBackend`, `TurboTasksBackendInner`, `ExecuteContextImpl`, and
`ChildExecuteContextImpl`. Every instantiation in the workspace already
  resolved to one concrete type.

The only remaining trait in the crate's public role is `Backend`
(defined in
`turbo-tasks`), which stays — removing it would invert the
`turbo-tasks` → backend dependency.

**`Arc` indirections removed:**

- `TurboKeyValueDatabase.db: Arc<TurboPersistence>` → `TurboPersistence`
  (the write batch only borrowed it; the clone in `new` was gratuitous).
- The `self: &Arc<Self>` receivers and `self.clone()` calls in
`run_backend_job`,
`idle_start`, and `try_read_task_output` — the deferred/background work
reaches
the backend through the pinned `turbo_tasks` handle instead, whose `Arc`
already
  keeps the backend alive.
- With no remaining sharer, the backend's own
`Arc<TurboTasksBackendInner>` and
the `TurboTasksBackend` newtype were collapsed: the inner fields now
live
directly in `TurboTasksBackend`, stored inline in `TurboTasks` (which
already
  shares it via its own `Arc<TurboTasks>`).

**Other cleanups surfaced by the above:**

- `noop_backing_storage()` is now an empty, read-only instance of the
real
`TurboPersistence` instead of a separate `NoopKvDb` impl — same concrete
type as
`turbo_backing_storage()`, so the `Either<…>` wrappers in napi and
turbopack-cli
  go away.
- `TurboTasksBackend::backing_storage()` accessor replaced with a
focused
`invalidate_storage(reason_code)` method, so the backend no longer leaks
its
  storage object.
- Dead code removed (`CellDependency::key`, `ExecuteContext::schedule` /
`suspending_requested`, the non-stats `execute_with_stats` variant) and
  feature/test-only items gated behind their corresponding `cfg`.

### Why?

The no-op storage work removed the last thing forcing these abstractions
to be
generic/dynamic, so the single-impl traits and the extra `Arc` layers
were pure
indirection. This is a self-contained simplification: behavior is
unchanged.

### Tradeoff

- One `Either`/match in every backing-storage method becomes one
`is_empty`
  atomic load (`Relaxed` `AtomicBool`) in `should_restore` — essentially
  equivalent.
- The backend is now stored inline in `TurboTasks` rather than behind a
pointer:
  one fewer indirection on every backend access, at the cost of a larger
  `TurboTasks` struct.

<!-- NEXT_JS_LLM_PR -->
2026-06-05 10:34:12 -07:00
Sam Poder c97ac7e54b [turbopack] Add BumpVec (vector implementation that takes a Bump) (#94296)
This PR implements `BumpVec` this is a custom vector implementation that
takes in a `Bump` as a type (from bumpalo) so that we get `Send`/`Sync`
on our `JSValue` vectors when we make that switch in:
https://github.com/vercel/next.js/pull/94297.
2026-06-03 14:43:46 -07:00
Niklas Mischkulnig 87ee4fe431 Turbopack: refactor NFT to add Endpoint.traced_files (#94224)
Refactor the module-graph bit of NFT so that we can provide
`Endpoint.traced_files()` which is needed for the bundle analyzer

There should be no logic change whatsoever
2026-06-01 14:57:41 +02:00
Tobias Koppers 11c865c1d6 Update ctor from 0.10 to 1.0.6 (#94045)
### What?

Updates the `ctor` crate dependency from `0.10` to `1.0.6` in the
workspace and migrates the two call sites to the new API.

### Why?

We've been running into a rust-analyzer issue with `ctor` 0.10 that the
newer release looks like it should fix. While bumping, the major version
jump also brings in a number of stability and safety improvements
(post-libc-init default priority, required `unsafe` marker, declarative
re-export form, expanded platform support).

### How?

`ctor` 1.x has several breaking changes that affect us:

1. **`#[ctor]` now requires an explicit `unsafe` marker.** Both call
sites were updated:
- `turbopack/crates/turbo-tasks-macros-tests/tests/trybuild.rs`:
`#[ctor::ctor]` → `#[ctor::ctor(unsafe)]`. The function body already
wraps `std::env::remove_var` in `unsafe`, so no further change was
needed.
- `turbopack/crates/turbo-tasks-macros/src/value_impl_macro.rs`: the
proc-macro emits a vtable registration ctor for every
`#[turbo_tasks::value_impl]`. Migrated to the new declarative form (see
next bullet).

2. **`crate_path = ...` is deprecated in favor of
`ctor::declarative::ctor!`.** The ctor 1.0.6 docs explicitly recommend
the declarative macro for crates that re-export ctor (`turbo-tasks`
re-exports it via `macro_helpers::ctor` so user crates don't need a
direct dep). The emission in `value_impl_macro.rs` was rewritten from

   ```rust
   #[turbo_tasks::macro_helpers::ctor::ctor(
       crate_path = turbo_tasks::macro_helpers::ctor,
   )]
   #[allow(non_snake_case)]
   fn #vtable_register_ident() { ... }
   ```

   to

   ```rust
   turbo_tasks::macro_helpers::ctor::declarative::ctor! {
       #[ctor(unsafe)]
       #[allow(non_snake_case)]
       fn #vtable_register_ident() { ... }
   }
   ```

3. **Default priority changed.** In 0.x the default priority was `0`
(run before C library init); in 1.x the default is `default` (≈500,
after libc init). We don't specify a priority, and the registration
ctors only push into a `Vec` guarded by a registry — they don't touch
libc — so the later timing is safe and we accept the new default. The
lazy `VTableRegistry::finalize` step (driven by the `VALUES` `LazyLock`)
is unchanged, so visible ordering semantics for downstream code are
preserved.

4. **Other 1.x breaking changes verified as non-issues for this repo:**
   - `dtor` was split into its own crate — not used here.
   - `priority = naked` syntax removed — not used.
- MSRV bumps: base is 1.60, only the optional `priority` feature
requires 1.85 — we don't enable `priority`.
- `used_linker` / `no_warn_on_missing_unsafe` were replaced by `--cfg
linktime_*` flags — neither was used here.

#### Verification

- `cargo check` / `cargo clippy` clean across `turbo-tasks`,
`turbo-tasks-backend`, `turbo-tasks-macros`, `turbo-tasks-macros-tests`,
and `turbopack-tests`.
- `cargo test -p turbo-tasks-backend`: all tests pass (58 in the largest
suite, 0 failures across all integration tests, 0 doc-test failures).
- `cargo test -p turbopack-tests`: 218 + 87 tests pass, 0 failures, 1
ignored.

#### Lockfile changes

`Cargo.lock` reflects the new dep graph: `ctor` jumps to `1.0.6`,
`link-section` to `0.17.2`, a new `linktime-proc-macro` is pulled in,
and the old `ctor-proc-macro` / `dtor` / `dtor-proc-macro` entries are
dropped (they were transitive deps of `ctor` 0.10).

<!-- NEXT_JS_LLM_PR -->

Co-authored-by: v-work-app[bot] <262237222+v-work-app[bot]@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Tobias Koppers <sokra@users.noreply.github.com>
2026-06-01 14:25:48 +02:00
Niklas Mischkulnig 5ee7005200 Turbopack: use module graph for NFT (#92901)
- [x] skip traced edges properly in all graph traversals
- [x] iter_nodes and enumerate_nodes include the traced modules
    - [x] Don't propagate is_self_async inside of traced references as a perf optimization
- [x] fix performance
- [x] validate that NFT JSON files are identical
- [x] ~~validate/adjust bundle analyzer output~~ will be in followup
- [x] audit `fn chunking_type()` changes

Benchmark:
```
* ced85b3108 - (2 weeks ago) refactor - Niklas Mischkulnig (HEAD -> mischnic/nft-module-graph, origin/mischnic/nft-module-graph)

TURBOPACK_PERSISTENT_CACHE=0 NEXT_TURBOPACK_TRACING= pnpm next build   334.96s user 44.57s system 788% cpu 48.152 total
TURBOPACK_PERSISTENT_CACHE=0 NEXT_TURBOPACK_TRACING= pnpm next build   343.39s user 42.61s system 809% cpu 47.659 total

* 5edf48238f - (77 minutes ago) v16.3.0-canary.32 - next-js-bot[bot] (HEAD, tag: v16.3.0-canary.32)

TURBOPACK_PERSISTENT_CACHE=0 NEXT_TURBOPACK_TRACING= pnpm next build   333.42s user 43.57s system 712% cpu 52.877 total
TURBOPACK_PERSISTENT_CACHE=0 NEXT_TURBOPACK_TRACING= pnpm next build   332.95s user 43.15s system 791% cpu 47.515 total
TURBOPACK_PERSISTENT_CACHE=0 NEXT_TURBOPACK_TRACING= pnpm next build   337.59s user 41.28s system 771% cpu 49.117 total
TURBOPACK_PERSISTENT_CACHE=0 NEXT_TURBOPACK_TRACING= pnpm next build   335.25s user 45.53s system 718% cpu 52.975 total
```

<details>
<summary>Previous benchmarks</summary>

```
52212418699f362904d64c08e544d4994e8d27fa mischnic/nft-module-graph

pnpm next build   371.41s user 66.39s system 737% cpu 59.353 total
pnpm next build   380.13s user 59.62s system 751% cpu 58.548 total
pnpm next build   374.25s user 60.55s system 761% cpu 57.102 total

8e4cfc5062 canary

pnpm next build   370.77s user 53.89s system 762% cpu 55.725 total
pnpm next build   383.16s user 55.61s system 744% cpu 58.921 total
pnpm next build   382.34s user 53.51s system 757% cpu 57.512 total
pnpm next build   381.15s user 54.50s system 751% cpu 57.943 total
```


```
b0d5411a79 mischnic/nft-module-graph
pnpm next build --experimental-build-mode=compil  368.75s user 62.05s system 749% cpu 57.445 total
pnpm next build --experimental-build-mode=compil  368.95s user 58.42s system 763% cpu 56.004 total

0e3ad2768bd3b036e96f66e4b770a16bb0b38705 mischnic/nft-module-graph
pnpm next build --experimental-build-mode=compil  369.48s user 60.62s system 748% cpu 57.494 total
pnpm next build --experimental-build-mode=compil  374.89s user 52.27s system 730% cpu 58.449 total
pnpm next build --experimental-build-mode=compil  375.02s user 52.89s system 747% cpu 57.274 total

f37073e1d9 mischnic/speed-next-dynamic-map
pnpm next build --experimental-build-mode=compil  389.58s user 55.98s system 704% cpu 1:03.21 total

e599b11e2d6b09a92600cfca6d9b338ed1f7537d mischnic/fix-side-effect-edge-removal
pnpm next build --experimental-build-mode=compil  386.60s user 53.34s system 720% cpu 1:01.09 total

f68f70b044 canary
pnpm next build --experimental-build-mode=compil  387.46s user 53.72s system 738% cpu 59.716 total
pnpm next build --experimental-build-mode=compil  390.33s user 53.54s system 734% cpu 1:00.43 total
pnpm next build --experimental-build-mode=compil  385.33s user 56.55s system 722% cpu 1:01.18 total
pnpm next build --experimental-build-mode=compil  386.49s user 56.44s system 726% cpu 1:00.96 total
```



```
b1ed4553e6 origin/mischnic/nft-module-graph
pnpm next build --experimental-build-mode=compile  467.28s user 75.24s system 791% cpu 1:08.56 total
pnpm next build --experimental-build-mode=compile  465.00s user 76.80s system 831% cpu 1:05.20 total
pnpm next build --experimental-build-mode=compile  473.86s user 70.73s system 790% cpu 1:08.90 total

3cb77c844b canary
pnpm next build --experimental-build-mode=compile  367.91s user 58.06s system 726% cpu 58.672 total
pnpm next build --experimental-build-mode=compile  362.97s user 57.68s system 746% cpu 56.379 total
pnpm next build --experimental-build-mode=compile  364.59s user 57.01s system 724% cpu 58.167 total
```

</details>
2026-05-28 20:10:34 +02:00
Sam Poder 1095b9ebd5 Fix short circuit evaluation of AND / OR in the Turbopack analyzer for string & nullish-related methods (#94159)
Here is the original description of the bug:

```
1. shortcircuit_if_known predicates are swapped for Logical(And) and
Logical(Or) (lines 2541–2548, 2573–2582, 2634–2643)

is_nullish, is_string, and is_empty_string all do this for AND/OR:

LogicalOperator::And => shortcircuit_if_known(list, JsValue::is_truthy, …),
LogicalOperator::Or => shortcircuit_if_known(list, JsValue::is_falsy, …),

shortcircuit_if_known returns item_value(item) for the first item where
use_item == Some(true). But:
- a && b returns a when a is falsy, else b → use_item should be is_falsy.
- a || b returns a when a is truthy, else b → use_item should be is_truthy.

Concretely:
- is_nullish(1 && null): code does is_truthy(1) = Some(true) → return
is_nullish(1) = Some(false). JS answer is true. Wrong.
- is_nullish(null || 2): code does is_falsy(null) = Some(true) → return
is_nullish(null) = Some(true). JS answer is false. Wrong.
```

As mentioned, in the issue `is_string`, `is_empty_string`, and
`is_nullish`'s short circuit evaluation of `&&` and `||` was reversed.
This PR reverses them so that for AND expressions, the first false-y
value is evaluated (otherwise the last value is). For OR expressions,
the first truth-y value is evaluated (otherwise the last value is).

I’ve added unit tests to this PR. To verify these unit tests with
JavaScript’s behaviour I used Claude Code to generate this node script:

```javascript
const PREDICATES = {
  is_string: (v) => typeof v === 'string',
  is_empty_string: (v) => v === '',
  is_nullish: (v) => v == null,
  is_not_nullish: (v) => v != null,
}

const cases = [
  {
    test: 'is_string_constant',
    rust: `assert_eq!(value.is_string(), Some(true));`,
    expr: `'hello'`,
    predicate: 'is_string',
    expected: true,
  },

  {
    test: 'is_string_and_short_circuit',
    rust: `assert_eq!(eval("'hello' && 2").is_string(), Some(false));`,
    expr: `'hello' && 2`,
    predicate: 'is_string',
    expected: false,
  },
  {
    test: 'is_string_and_short_circuit',
    rust: `assert_eq!(eval("2 && 1 && 'hello'").is_string(), Some(true));`,
    expr: `2 && 1 && 'hello'`,
    predicate: 'is_string',
    expected: true,
  },

  {
    test: 'is_string_or_short_circuit',
    rust: `assert_eq!(eval("'hello' || 'bye' || 2").is_string(), Some(true));`,
    expr: `'hello' || 'bye' || 2`,
    predicate: 'is_string',
    expected: true,
  },
  {
    test: 'is_string_or_short_circuit',
    rust: `assert_eq!(eval("'hello' || 2 || 1 || 'bye'").is_string(), Some(true));`,
    expr: `'hello' || 2 || 1 || 'bye'`,
    predicate: 'is_string',
    expected: true,
  },
  {
    test: 'is_string_or_short_circuit',
    rust: `assert_eq!(eval("2 || 1 || 'hello' || 'bye'").is_string(), Some(false));`,
    expr: `2 || 1 || 'hello' || 'bye'`,
    predicate: 'is_string',
    expected: false,
  },

  {
    test: 'is_empty_string_and_short_circuit',
    rust: `assert_eq!(eval("'' && 'string'").is_empty_string(), Some(true));`,
    expr: `'' && 'string'`,
    predicate: 'is_empty_string',
    expected: true,
  },
  {
    test: 'is_empty_string_and_short_circuit',
    rust: `assert_eq!(eval("false && ''").is_empty_string(), Some(false));`,
    expr: `false && ''`,
    predicate: 'is_empty_string',
    expected: false,
  },
  {
    test: 'is_empty_string_and_short_circuit',
    rust: `assert_eq!(eval("0 && false && ''").is_empty_string(), Some(false));`,
    expr: `0 && false && ''`,
    predicate: 'is_empty_string',
    expected: false,
  },

  {
    test: 'is_empty_string_or_short_circuit',
    rust: `assert_eq!(eval("'' || 'string'").is_empty_string(), Some(false));`,
    expr: `'' || 'string'`,
    predicate: 'is_empty_string',
    expected: false,
  },
  {
    test: 'is_empty_string_or_short_circuit',
    rust: `assert_eq!(eval("false || ''").is_empty_string(), Some(true));`,
    expr: `false || ''`,
    predicate: 'is_empty_string',
    expected: true,
  },
  {
    test: 'is_empty_string_or_short_circuit',
    rust: `assert_eq!(eval("0 || false || ''").is_empty_string(), Some(true));`,
    expr: `0 || false || ''`,
    predicate: 'is_empty_string',
    expected: true,
  },

  {
    test: 'is_nullish_and_short_circuit',
    rust: `assert_eq!(eval("'' && null").is_nullish(), Some(false));`,
    expr: `'' && null`,
    predicate: 'is_nullish',
    expected: false,
  },
  {
    test: 'is_nullish_and_short_circuit',
    rust: `assert_eq!(eval("null && ''").is_nullish(), Some(true));`,
    expr: `null && ''`,
    predicate: 'is_nullish',
    expected: true,
  },
  {
    test: 'is_nullish_and_short_circuit',
    rust: `assert_eq!(eval("0 && null && ''").is_nullish(), Some(false));`,
    expr: `0 && null && ''`,
    predicate: 'is_nullish',
    expected: false,
  },
  {
    test: 'is_nullish_and_short_circuit',
    rust: `assert_eq!(eval("null && 1 && 2").is_nullish(), Some(true));`,
    expr: `null && 1 && 2`,
    predicate: 'is_nullish',
    expected: true,
  },

  {
    test: 'is_nullish_or_short_circuit',
    rust: `assert_eq!(eval("'' || null").is_nullish(), Some(true));`,
    expr: `'' || null`,
    predicate: 'is_nullish',
    expected: true,
  },
  {
    test: 'is_nullish_or_short_circuit',
    rust: `assert_eq!(eval("null || ''").is_nullish(), Some(false));`,
    expr: `null || ''`,
    predicate: 'is_nullish',
    expected: false,
  },
  {
    test: 'is_nullish_or_short_circuit',
    rust: `assert_eq!(eval("0 || '' || null").is_nullish(), Some(true));`,
    expr: `0 || '' || null`,
    predicate: 'is_nullish',
    expected: true,
  },
  {
    test: 'is_nullish_or_short_circuit',
    rust: `assert_eq!(eval("null || 1 || 2").is_nullish(), Some(false));`,
    expr: `null || 1 || 2`,
    predicate: 'is_nullish',
    expected: false,
  },

  {
    test: 'is_not_nullish_and_short_circuit',
    rust: `assert_eq!(eval("'' && null").is_not_nullish(), Some(true));`,
    expr: `'' && null`,
    predicate: 'is_not_nullish',
    expected: true,
  },
  {
    test: 'is_not_nullish_and_short_circuit',
    rust: `assert_eq!(eval("null && ''").is_not_nullish(), Some(false));`,
    expr: `null && ''`,
    predicate: 'is_not_nullish',
    expected: false,
  },
  {
    test: 'is_not_nullish_and_short_circuit',
    rust: `assert_eq!(eval("0 && null && ''").is_not_nullish(), Some(true));`,
    expr: `0 && null && ''`,
    predicate: 'is_not_nullish',
    expected: true,
  },
  {
    test: 'is_not_nullish_and_short_circuit',
    rust: `assert_eq!(eval("null && 1 && 2").is_not_nullish(), Some(false));`,
    expr: `null && 1 && 2`,
    predicate: 'is_not_nullish',
    expected: false,
  },

  {
    test: 'is_not_nullish_or_short_circuit',
    rust: `assert_eq!(eval("'' || null").is_not_nullish(), Some(false));`,
    expr: `'' || null`,
    predicate: 'is_not_nullish',
    expected: false,
  },
  {
    test: 'is_not_nullish_or_short_circuit',
    rust: `assert_eq!(eval("null || ''").is_not_nullish(), Some(true));`,
    expr: `null || ''`,
    predicate: 'is_not_nullish',
    expected: true,
  },
  {
    test: 'is_not_nullish_or_short_circuit',
    rust: `assert_eq!(eval("0 || '' || null").is_not_nullish(), Some(false));`,
    expr: `0 || '' || null`,
    predicate: 'is_not_nullish',
    expected: false,
  },
  {
    test: 'is_not_nullish_or_short_circuit',
    rust: `assert_eq!(eval("null || 1 || 2").is_not_nullish(), Some(true));`,
    expr: `null || 1 || 2`,
    predicate: 'is_not_nullish',
    expected: true,
  },
]

let passed = 0
let failed = 0
let currentTest = null

for (const c of cases) {
  if (c.test !== currentTest) {
    console.log(`\n#[test] fn ${c.test}()`)
    currentTest = c.test
  }
  console.log(`  ${c.rust}`)

  const result = (0, eval)(c.expr)
  const actual = PREDICATES[c.predicate](result)
  const ok = actual === c.expected
  const status = ok ? 'PASS' : 'FAIL'
  console.log(
    `    -> JS result = ${JSON.stringify(result)}, ${c.predicate} = ${actual}, expected = ${c.expected}  [${status}]`
  )

  if (ok) passed++
  else failed++
}

console.log(`\n${passed} passed, ${failed} failed`)
```

After running it on my machine:

```
28 passed, 0 failed
```

---------

Co-authored-by: vercel[bot] <35613825+vercel[bot]@users.noreply.github.com>
Co-authored-by: Luke Sandberg <lukesandberg@users.noreply.github.com>
2026-05-27 23:27:26 -07:00
Luke Sandberg 21d47a456a Enable mimalloc in benchmarks (#93388)
this brings them closer to production
2026-05-19 18:31:51 -07:00
Luke Sandberg a9323da293 Stop pinning compiled chunk source on EcmascriptBuildNodeChunkVersion.
(#93807)

## What

Stop pinning compiled chunk source on `EcmascriptBuildNodeChunkVersion`.

The struct previously held a `Vec<ReadRef<CodeAndIds>>` for every module
in the chunk, even though the HMR update path only needs the bytes for
the *changed* subset and the bytes are already on disk. That field was
also forcing the upstream `CodeAndIds` / `BatchGroupCodeAndIds` tasks to
stay `serialization = "skip"`, so warm restarts had to re-walk every
module and re-hash its source to rebuild `entries_hashes`.

This change mirrors the browser-side pattern: a new
`EcmascriptBuildNodeChunkContentEntries` task lives on the chunk content
and holds `ResolvedVc<Code>` + `ResolvedVc<u64>` per module. The version
struct shrinks to `{ chunk_path, minify_type, entries_hashes }`, drops
`serialization = "skip"`, and switches `chunk_path` from `String` to
`RcStr`.
`update_ecmascript_node_chunk_content` now resolves entries lazily, only
when an added or modified module actually needs its code shipped.

## Why

Two wins, both for dev sessions starting against a warm filesystem
cache:

- **Memory.** The version no longer transitively pins every module's
compiled `Rope` in heap — those bytes can stay on disk until HMR
actually needs them.
- **Warm-restart CPU.** `entries_hashes` is sourced from the per-module
`Code::source_code_hash()` task (already cached) and the version itself
now round-trips through the persistent cache, so we don't re-hash
anything on warm start.

The HMR payload shape is unchanged.

## Perf
This should speed up warm builds a bit but the major benefit is not
recomputing node outputs and keeping them in ram

measuring v0 after loading the main route
Branch: Cold: 12.3G Warm: 7.34G
Canary: Cold 12.3G Warm: 8.5G

The trace file confirms the recomputations are gone and the heap
measurements confirm we trimming ~1.1g of ram

Using the devlow benchmarks i was able to confirm a possible small
progression

```
# canary
chat dev startup build=warm: root page = 23.96 s (from root page/start)
chat dev startup build=warm: root page = 21.21 s (from root page/start)
chat dev startup build=warm: root page = 22.70 s (from root page/start)

# branch
chat dev startup build=warm: root page = 20.94 s (from root page/start)
chat dev startup build=warm: root page = 19.43 s (from root page/start)
chat dev startup build=warm: root page = 23.49 s (from root page/start)

```


## Tests

I added a new integration test to ensure we don't accidentally regress
here. Which confirms that 'clean warm builds' run nothing and 'clean
warm dev sessions' just set up HMR session infra
<!-- NEXT_JS_LLM_PR -->
2026-05-19 15:07:46 -07:00
Luke Sandberg d81d5ab7df Proof of concept: task eviction after snapshot for turbo-tasks-backend (#91790)
> **Note:** This is a **proof of concept** implementation. It is not yet
ready for production use.

## Summary

Implements memory eviction for the turbo-tasks engine. After a
persistence snapshot completes, tasks that are safe to remove are
evicted from in-memory storage and transparently restored from disk on
next access.

### Eviction levels

- **Full eviction**: Entire task removed from the in-memory map
(restored from disk on access). Only possible when the task has no
meaningful transient state (and other state is already on disk)
- **DataAndMeta eviction**: Both data and meta categories cleared, but
the task stays in the map to preserve transient state (e.g.
`current_session_clean`, aggregated session-clean counts).
- **DataOnly eviction**: Only data-category fields cleared; meta (graph
structure, output, dirty state) stays in memory.
- **MetaOnly eviction**: Only meta-category fields cleared; data stays
in memory.

Data and meta evictability are computed independently — if one category
is modified but the other is clean, the clean category can still be
dropped.

Eviction is gated behind `BackendOptions::evict_after_snapshot` (off by
default), and can be enabled in Next.js via the
`TURBO_ENGINE_EVICT_AFTER_SNAPSHOT=1` env var for testing.

## Key changes

- **Orthogonal eviction decision tree** (`storage_schema.rs`): Data and
meta evictability are computed independently. Full eviction additionally
requires no meaningful transient state (session-clean flags, aggregated
session-clean counts). Replaces the previous sequential bail-out
approach which was too aggressive on full eviction (losing transient
session state on leaf tasks) and not aggressive enough on partial
eviction (blocking all eviction when only one category was modified).

- **`drop_partial()` codegen** (`task_storage_macro.rs`): New generated
methods to drop data

- **`restore_from_*()` codegen changes** (`task_storage_macro.rs`): New
semantics for merging persistent data from the backend with transient
data stored in memory.

- **`task_cache` moved into `Storage`** (`storage.rs`): The
`CachedTaskType → TaskId` deduplication map was previously a separate
field on `TurboTasksBackendInner`. It is now owned by `Storage` so
eviction can remove entries when a task is fully evicted. Because
`task_cache` is a pure performance cache (entries are re-populated by
`task_by_type()` on miss once the task type is persisted to backing
storage), evicting entries is safe. After bulk eviction the map is
shrunk when it is less than half full.

- **Parallel shard eviction** (`storage.rs`): Eviction iterates all
storage shards in parallel after snapshot, applying the appropriate
eviction level per task. Each shard is shrunk after bulk eviction to
reclaim slack capacity.
- In principle this is O(N) work to scan, but because each pass drops
>98% of tasks there isn't wasted work and the logic is fast, taking
<100ms for even the largest applications.


## Design notes

- **SessionDependent tasks**: SessionDependent tasks can still be
evicted but if `current_session_clean` is set we prevent full eviction
to avoid rechecking. Within a session the file-watchers are responsible
for invalidations after setting `current_session_clean`.

## Known limitations (proof of concept)

- No LRU or access-frequency tracking — all eligible tasks are evicted
on every snapshot cycle
- No memory pressure feedback — eviction runs on a timer, not in
response to actual memory pressure
- Only runs after snapshotting which tends to be a high point in memory
- Future work will explore interleaving this logic with snapshotting to
trim the peak



<!-- NEXT_JS_LLM_PR -->
2026-05-10 22:39:51 +00:00
Luke Sandberg ee39f8f47f turbo-rcstr: replace rcstr! macro_rules with proc macro (#93551)
### What?

Replaces the `rcstr!` macro in `turbo-rcstr` with a proc-macro
implementation in a new `turbo-rcstr-macros` crate. The proc macro
inspects the literal at expansion time and emits only the relevant arm —
no dead `else` branch.

### Why?

The previous `macro_rules!` macro emitted both an inline arm and a
static + `inventory::submit!` arm at every call site, expecting `const`
evaluation to discard one. `inventory::submit!` adds `#[used]` to its
constructor, so the dead arm shipped in the binary even when const eval
determined the live arm was the inline one.

Across the workspace there are ~852 inlinable `rcstr!` literals (≤ 7
bytes); each previously emitted a wasted `PrehashedString` static, an
inventory node, and a startup `ctor`.

### How?

A `#[proc_macro] rcstr` in the new `turbo-rcstr-macros` crate decides at
expansion time:

- inlinable literal → `inline_atom(lit).unwrap()`. No static, no
inventory entry, no dead branch.
- non-inlinable literal → static + `inventory::submit!` + `from_static`,
no inline arm.
- constant identifiers / `concat!(…)` / non-literal inputs → fall back
to the original both-branches expansion so const eval picks the arm. ~11
such call sites.

`turbo-rcstr`'s `atom_size_64` / `atom_size_128` features forward to the
proc-macro crate so the threshold is exact for every build configuration
without a runtime const branch.

The proc macro deliberately avoids `syn`, `quote`, and `proc-macro2`. It
pattern-matches on `proc_macro::TokenTree` directly, asks the compiler
for the literal's unescaped value via `Literal::str_value` (gated on the
unstable `proc_macro_value` feature), and emits the chosen expansion by
parsing a string template via `TokenStream::from_str`. The proc-macro
crate compiles in ~0.7 s with zero third-party dependencies.

### Compile time

Clean rebuild of `turbopack-core` in release mode (143 `rcstr!` call
sites). Dependencies cached; only `turbopack-core` itself rebuilt each
iteration. `darwin-aarch64`.

|                    |   n | min      | mean     | median   | stdev    |
| ------------------ | ---:| --------:| --------:| --------:| --------:|
| canary             | 7   | 27.11 s  | 28.00 s  | 28.12 s  | 0.61 s   |
| `rcstr_inventory`  | 7   | 26.60 s  | 27.99 s  | 27.96 s  | 0.80 s   |
| **delta** | | | **−0.01 s (−0.06 %)** | −0.16 s | |

Within noise. The proc-macro per-invocation overhead is offset by
reduced emitted-token volume (~40 % less code across all `rcstr!` sites
— the 852 inlinable literals collapse from ~80 tokens to ~6 tokens
each), so the compiler does materially less parsing and type-checking on
the macro output.

### Binary size

Release build of `next-napi-bindings` cdylib on `darwin-aarch64`:

| | canary | `rcstr_inventory` | delta |
| ---------------------------- | ------------------------------- |
------------------------------- | ------------------------------ |
| Release `.dylib` (raw) | 123,699,312 B (117.97 MiB) | 123,349,424 B
(117.64 MiB) | **−349,888 B (−0.28 %)** |
| Stripped (`strip -x`) | 83,502,264 B (79.63 MiB) | 83,434,984 B (79.57
MiB) | **−67,280 B (−0.08 %)** |
| Stripped + `gzip -9` | 28,967,762 B (27.63 MiB) | 28,958,198 B (27.62
MiB) | **−9,564 B (−0.03 %)** |

The raw release build shrinks by ~342 KiB — the 852 dead `static
PrehashedString` + inventory node + ctor entries no longer being emitted
for inlinable call sites. Most of that compresses well (dead data is
stripped and gzip handles the rest), so the user-facing impact in the
gzipped npm tarball is ~9 KiB.

<!-- NEXT_JS_LLM_PR -->
2026-05-07 17:05:05 +00:00
Benjamin Woodruff dccb95c3b1 Turbopack: Cache effect errors, replace the write_lock Mutex with a lazily created Notify instance (#93476)
This is a mostly-by-hand refactor of some of the caching logic in
https://github.com/vercel/next.js/pull/92300

- **Cache errors.** We often end up applying effects multiple times in
frequent succession, and many filesystem effects already have their own
backoff/retry loops, which can make them very slow when they fail, so it
seems generally better to cache the failures.
- **Add `EffectLastApplied` enum:** It's a matter of opinion, but I
think the `EffectLastApplied` enum makes it easier to reason about the
different states in the system. `Option` was difficult to reason about,
and it would've been even more confiusing if it was an
`Option<Result<(), Arc<dyn EffectError>>`.
- `tokio::sync::Mutex<()>` is overkill here, `tokio::sync::Notify` is a
simpler (and smaller) primitive that does the core of what we needed the
`Mutex` for. We could also use `EventListener` here, but I didn't.
(`EventListener` is really just smol's version of `Notify`)
- We can lazily allocate the `Notify` instance inside of `last_applied`.
We have to grab this lock anyways to check for the cached fast-path. If
we're not in the fast-path, we can re-use the lock guard to create and
store the `Notify` instance.
- **Perf:** The old code's slow path had to always lock `last_applied`
three times. Now, if there's no concurrent writer (common case), it only
locks twice: Once before application, and once after.
- **Perf:** Use a smallvec when computing unique indices to avoid a
bunch of heap allocations. Unless there's a bug, we don't expect any of
the keys to have more than one value (returns an error).
- **Perf:** Store keys as `Box<[u8]>` instead of `Vec<u8>`, saving 8
bytes per key.

Currently, if `effect.dyn_apply()` panics, we just do our best to mark
the effect as unapplied to avoid a deadlock. This could alternatively
poison the object.

## Memory Usage

Memory usage can increase if we're storing errors (heap allocated,
stored in `Arc`) we previously wouldn't have, but if we assume that
errors are uncommon, this refactor decreases the size of the data
structures involved.

Before this change, eagerly allocating a `tokio::sync::Mutex` was
costing 40 bytes per effect, making the entire `EffectEntryState` struct
64 bytes:

<img
src="https://app.graphite.com/user-attachments/assets/989ac229-9e2f-4370-8e32-b14e10e09248.png"
width=500>
<img
src="https://app.graphite.com/user-attachments/assets/f2e14416-17c3-4961-8c00-fa7a81846fa0.png"
width=500>

After the changes, the entire `EffectLastApplied` enum is 40 bytes

<img
src="https://app.graphite.com/user-attachments/assets/18e4fc0a-e7f8-4fad-b9bc-c22ba3eefb6c.png"
width=500>

After wrapping the `EffectLastApplied` enum in a `parking_lot::Mutex`,
it comes in at 48 bytes (after alignment), 16 bytes smaller than the old
struct.
2026-05-06 23:23:54 +00:00
Niklas Mischkulnig 8f77276c71 Upgrade to swc 65 (#93325)
https://github.com/swc-project/swc/compare/a0716503cc2b359ff6620d0ae5c0e8afaa18c185...b815eb24190dc54443486ccee161a419e247826b

Closes https://github.com/vercel/next.js/issues/93353
2026-05-04 10:02:50 +02:00
Matt Mastracci 26afa89200 Replace Lazy<FxHashMap> static lookups with phf::Map (#93096)
## What

Trivial change: LazyLock<FxHashMap<...>> -> phf::Map.
2026-05-02 02:12:22 +00:00
Matt Mastracci 908717af2d Drop once_cell from all direct crate deps (#93095)
## What

Purely mechanical change from once_cell to stdlib primitives.

---------

Co-authored-by: Luke Sandberg <lukesandberg@users.noreply.github.com>
2026-05-02 00:33:16 +00:00
Niklas Mischkulnig 6eb0126dc0 Turbopack: delete unused turbo-static crate (#93324)
Added in https://github.com/vercel/turborepo/pull/8037

Nobody uses this, and it's also the only crate that uses the lsp-server
dependency.
2026-04-29 14:18:50 +00:00
Tobias Koppers 292a3fba32 Report OS memory pressure in TurboMalloc and trace samples (#93333)
### What?

Adds a new `TurboMalloc::memory_pressure()` method that returns a
normalized OS-level memory pressure value in the range `0..=100` as
`Option<u8>`. That value is attached to every memory sample in the
tracing layer (`TraceRow::MemorySample`) and propagated through the
trace-server so that span queries return a `memory_pressure_samples`
vector next to the existing `memory_samples`.

### Why?

Our current tracing only records the in-process allocator usage
(`TurboMalloc::memory_usage()`), which does not tell us when the
*operating system* is actually under memory pressure. We want that
signal in the trace output to:

1. Surface real OS memory pressure in trace dashboards alongside our own
allocation totals.
2. Eventually use it as input to task-eviction decisions in
`turbo-tasks` (see branch description). This PR lands the plumbing; the
eviction heuristic is not part of this change.

### How?

**`TurboMalloc::memory_pressure() -> Option<u8>`** — new, in
`turbopack/crates/turbo-tasks-malloc/`. Values are normalized so that
`0` = no pressure, `100` = maximum pressure. Platform-specific backends:

| Platform | Source | Notes |
|---|---|---|
| Linux | `/proc/pressure/memory` (`some` `avg10`), fallback to
`(MemTotal - MemAvailable) / MemTotal` from `/proc/meminfo` | PSI is not
available on all kernels (< 4.20, without `CONFIG_PSI`, restricted
containers). The meminfo fallback keeps the signal meaningful on any
standard Linux system and matches the semantics of Windows'
`dwMemoryLoad`. |
| macOS | `kern.memorystatus_level` sysctl (% free memory) | Pressure =
`100 - level`, read via `libc::sysctlbyname`. |
| Windows | `MEMORYSTATUSEX::dwMemoryLoad` via `GlobalMemoryStatusEx`
(`windows-sys`) | Already a 0–100 percentage of physical memory in use.
|
| Other / wasm | — | Returns `None`. |

All runtime failures (missing file, sysctl error, failed API call,
unparseable content) silently yield `None` rather than panicking.

**Wiring into tracing:**

- `TraceRow::MemorySample` gains a `memory_pressure: u8` field. `0` is
used when `memory_pressure()` returns `None` on unsupported platforms.
- `RawTraceLayer::maybe_report_memory_sample` populates it on every
sample (sampling cadence unchanged).
- This is a breaking change to the postcard wire format of
`MemorySample`; old trace files cannot be read by the new
`turbopack-trace-server`. Given the dev-only nature of this data that
seemed acceptable — let me know if a migration is desired.

**Wiring into the trace-server:**

- `Store::memory_samples` is now `Vec<(Timestamp, u64, u8)>` and
`add_memory_sample(ts, memory, memory_pressure)`.
- A new `Store::memory_pressure_samples_for_range(start, end) ->
Vec<u8>` mirrors `memory_samples_for_range`: same `MAX_MEMORY_SAMPLES =
200` cap and same group-and-max downsampling, so both vectors align
index-by-index for a given span query.
- `ServerToClientMessage::QueryResult` gains a `memory_pressure_samples:
Vec<u8>` field next to `memory_samples`.

**Dependencies:** `libc` (macOS only, `cfg`-gated), `windows-sys` with
the `Win32_System_SystemInformation` feature (Windows only,
`cfg`-gated). No new deps on Linux.

### Verification

- `cargo build -p turbo-tasks-malloc -p turbopack-trace-utils -p
turbopack-trace-server`
- `cargo clippy -p turbo-tasks-malloc -p turbopack-trace-utils -p
turbopack-trace-server --all-targets -- -D warnings`
- `cargo test -p turbo-tasks-malloc` — 7 tests pass, including:
- `memory_pressure_is_in_range`: asserts `Some(_)` and `≤ 100` on Linux,
macOS and Windows (via `cfg`-gated `.expect()`), and allows `None`
elsewhere.
- Parser tests for both PSI and `/proc/meminfo` code paths (typical
content, malformed input, clamping).
- Runtime sanity check on the Linux sandbox: `/proc/pressure/memory` is
absent (kernel 5.10 without `CONFIG_PSI`); the `/proc/meminfo` fallback
returned `Some(3)` as expected.

<!-- NEXT_JS_LLM_PR -->

---------

Co-authored-by: Tobias Koppers <sokra@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
2026-04-29 11:39:23 +02:00
Matt Mastracci e1bb911c14 turbo-tasks: precompute trait-impl vtables at compile time (#93092)
## What

Remove another lazy lock from trait vtables.

---------

Co-authored-by: Luke Sandberg <lukesandberg@users.noreply.github.com>
2026-04-29 00:27:00 +00:00
Tobias Koppers 99bbaa00c4 [rust-deps] Update tokio (#93195)
## What?

Updates the tokio and tokio-util dependencies to their latest versions:
- `tokio`: `~1.47.3` → `1.52.1`
- `tokio-util`: `0.7.13` → `0.7.18`

## Why?

Keep Rust dependencies up-to-date to benefit from bug fixes, performance
improvements, and security patches.

## How?

Updated version constraints in `Cargo.toml` and regenerated
`Cargo.lock`.

<!-- NEXT_JS_LLM_PR -->
2026-04-28 16:09:31 -07:00
Luke Sandberg 8dee7acb85 [turbopack-trace-server] optimize loading (#93264)
Land a few optimizations to the trace server

* Change `SpanEvent` so it is 32 bytes instead of 40 bytes by triggering a `niche` optimization
* Change `args` and `events` to be a `smallvec` with inline size 1
  * for `args` it is size <=1 ~31% of the time
  * for `events` it is size <=1 69% of the time
* Compute min/max timestamps in a single pass instead of 2 when inserting into the selftimetree
* Bundle dynamically computed 'total' fields behind a single OnceLock
   * saves 40 bytes per span due to `Oncelock` overheads
* Inline SpanTimeData and SpanNames into Span
   * We get little benefit from deferring the allocations and by inlining we save time and improve memory locality.
     * post load SpanTimeData is allocated for 94% of spans, but after loading `trace.nextjs.org` it is 100%
     * post load SpanNames is allocated for 0% of spans, but after loading it is 96.2% of spans
* Remove the `inner` `OnceLocks` from `SpanNames` we can just allocate these all together


Measuring with one 10gb trace file I see loading times progress from 75.7s (33G of ram)  to 60.5s (19.5G of ram).  With loading times hitting >200mb/s occasionally
2026-04-28 15:46:47 -07:00
Tobias Koppers 44722b808c Turbopack: import.meta.glob docs + edge case support (#92729)
## What?

Adds documentation and TypeScript types for `import.meta.glob` in
Turbopack, and implements additional edge cases for Vite compatibility.

### Documentation
- New `## import.meta.glob` section in the [Turbopack reference
page](/docs/app/api-reference/turbopack) covering: lazy/eager loading,
named imports, query strings (including the object form), multiple
patterns, negation, TypeScript types, and a full options table.
- `import.meta.glob` row added to the Module Resolution feature table.
- Migration note added to the [from-Vite
guide](/docs/app/guides/migrating/from-vite) explaining that
`import.meta.glob` works out of the box with Turbopack, with
before/after for the deprecated `as` → `query` migration.

### TypeScript types
- Added `ImportMetaGlobOptions` interface and overloaded `glob()`
signatures to `ImportMeta` in `packages/next/types/global.d.ts`.
- The overloads return `Record<string, unknown>` when `eager: true` is
passed and `Record<string, () => Promise<unknown>>` otherwise.

### Implementation (edge cases)
- **`import: '*'`** (namespace import): Treated the same as omitting the
`import` option (returns the whole module namespace), matching Vite
semantics. Previously would have generated broken `module["*"]` access.
- **`query` as object literal**: `{ query: { bar: 'foo', raw: true } }`
is now supported. Keys and values are URL-encoded and joined into a
query string (`?bar=foo&raw=true`).
- **Stricter option validation**: A non-object-literal second argument
(e.g. `import.meta.glob('./*.js', 'eager')`) is now a compile-time error
instead of a warning, since the options cannot be safely defaulted.

### Tests
- Execution tests for new features: namespace `import: '*'`, query
object, combining query with negation, explicit dotfile patterns.
- New `import-meta-glob-errors` execution test with issue snapshots
covering: too many arguments, non-string pattern, and non-object options
argument.

## Why?

1. The feature shipped in #92640 but had no user-facing documentation
and no TypeScript types.
2. Several Vite-compatible edge cases were not handled: namespace
imports (`import: '*'`) and query objects. These gaps would cause subtle
behavioral differences for users migrating from Vite.
3. Option validation was too lenient — invalid second arguments fell
through to defaults silently.

## How?

### Docs & types
- `docs/01-app/03-api-reference/08-turbopack.mdx` — new section, table
row, TypeScript subsection
- `docs/01-app/02-guides/migrating/from-vite.mdx` — migration note
- `packages/next/types/global.d.ts` — `ImportMetaGlobOptions` +
`ImportMeta.glob()` overloads

### Implementation
-
`turbopack/crates/turbopack-ecmascript/src/references/import_meta_glob.rs`:
- `parse_import_meta_glob()`: `import: '*'` normalized to `None`; query
object parsing via `urlencoding::encode()`; non-object options argument
now emits an error and returns `None`.
- `turbopack/crates/turbopack-ecmascript/Cargo.toml`: added
`urlencoding` workspace dep.

Note: dotfile/dot-directory filtering is not needed in
`import.meta.glob` itself — the underlying `Glob` pattern engine and
`read_glob_internal` already handle dotfile semantics correctly
(explicit `./.foo/*.js` matches, wildcards include them).

### Tests
-
`turbopack-tests/tests/execution/turbopack/resolving/import-meta-glob/`
— new cases for `import: '*'`, query object, query + negation, explicit
dotfile pattern, wildcard including dotfiles
-
`turbopack-tests/tests/execution/turbopack/resolving/import-meta-glob-errors/`
— new test suite for fatal parse errors with issue snapshots

### Checklist
- [x] `pnpm prettier-fix` / `cargo fmt` run
- [x] Documentation follows the [docs contribution
guide](https://nextjs.org/docs/community/contribution-guide)
- [x] Execution tests pass (`cargo test -p turbopack-tests --test
execution -- "import_meta_glob"`)
- [x] `cargo clippy` clean
- [x] `pnpm --filter=next types` passes

<!-- NEXT_JS_LLM_PR -->

---------

Co-authored-by: Tobias Koppers <sokra@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
2026-04-27 07:37:53 +00:00
Tobias Koppers e7bcba89dc [rust-deps] Update bytes, crc32fast, memchr, pin-project-lite, quote, rand, regex, ringmap, roaring (#93197)
### What?

Updates Rust dependencies to their latest versions:

| Crate | From | To |
|-------|------|-----|
| bytes | 1.1.0 | 1.11.1 |
| crc32fast | 1.4.2 | 1.5.0 |
| memchr | 2.7.4 | 2.8.0 |
| pin-project-lite | 0.2.9 | 0.2.17 |
| quote | 1.0.23 | 1.0.45 |
| rand | 0.10.0 | 0.10.1 |
| regex | 1.10.6 | 1.12.3 |
| ringmap | 0.2.3 | 0.2.5 |
| roaring | 0.10.10 | 0.11.4 |

### Why?

Keep dependencies up to date with latest bug fixes, performance
improvements, and security patches.

### How?

Version bumps in `Cargo.toml` with corresponding `Cargo.lock` updates.

<!-- NEXT_JS_LLM_PR -->
2026-04-27 09:33:35 +02:00
Tobias Koppers 5d3232fb2b [rust-deps] Update tracing (#93194)
### What?

Updates the `tracing` crate and its related dependencies to their latest
versions:

- `tracing`: 0.1.37 → 0.1.44
- `tracing-attributes`: 0.1.30 → 0.1.31  
- `tracing-core`: 0.1.34 → 0.1.36

### Why?

Keep Rust dependencies up to date with security patches and performance
improvements.

### How?

Updated the version specification in `Cargo.toml` and regenerated
`Cargo.lock`.

<!-- NEXT_JS_LLM_PR -->
2026-04-24 14:24:36 +00:00
Tobias Koppers 1e4782ad7b [rust-deps] Update serde (#93196)
### What?

Updates the serde family of crates to their latest versions:

- `serde`: 1.0.217 → 1.0.228
- `serde_json`: 1.0.138 → 1.0.149
- `serde_path_to_error`: 0.1.16 → 0.1.20
- `serde_qs`: 0.13.0 → 1.1.1
- `serde_with`: 3.12.0 → 3.18.0

### Why?

Keep dependencies up to date with bug fixes and performance
improvements.

### How?

Updated version constraints in `Cargo.toml` and ran `cargo update` to
regenerate the lockfile.

<!-- NEXT_JS_LLM_PR -->
2026-04-24 16:04:42 +02:00
Matt Mastracci c916c80b2f turbo-tasks: drop once_cell, const-init TRAIT_METHOD, ctor WORKER_TASKS (#93090)
## What

Clean up the Lazy-locks we re-export or use internally in the
turbo-tasks crates, migrating to `std::sync::LazyLock` /
`std::cell::LazyCell` where still needed and eliminating a couple of
them where they weren't.

## Why

One less dep, using standard Rust features, a smaller number of Lazy
items.

## Details

- **Re-exports:** drop unused `macro_helpers::OnceCell` and the
now-unused `macro_helpers::phf` (since `TraitType` no longer stores a
`phf::Map`). Replace `once_cell::sync::Lazy` with `std::sync::LazyLock`
in `macro_helpers::TRAIT_CAST_FNS` and in the two macro-generated call
sites.
- **`once_cell::sync::Lazy` → `std::sync::LazyLock`** in `scope.rs`,
`registry/mod.rs`, and `turbo-tasks-backend-fuzz`'s `graph.rs`.
- **`once_cell::unsync::Lazy` → `std::cell::LazyCell`** in
`update_cell.rs`.
- **`TRAIT_METHOD` → plain const-init static:** `TraitType::methods`
changes from `phf::Map<&'static str, TraitMethod>` to `&'static
[TraitMethod]` (parallel to `method_names`), and `TraitType::get`
becomes a `const fn` that does `index_of_name` + array indexing. The
`value_trait` macro now emits a slice literal instead of `phf_map!`, and
the `function` macro emits `static TRAIT_METHOD: &'static TraitMethod =
TRAIT.get(stringify!(..));` — evaluated at const-eval time rather than
behind a `LazyLock<&TraitMethod>`.
- **Dropped `once_cell` and `phf` Cargo deps** from `turbo-tasks`, and
`once_cell` from `turbo-tasks-backend` and its fuzz crate.
2026-04-21 21:06:32 +00:00
Tobias Koppers e2e8ba9aca turbo-trace-server: reduce allocation overhead when loading large trace files (#92896)
### What?

Reduces allocation overhead in `turbo-trace-server` when reading large
trace files. Measured improvement: **114 MB/s → 156 MB/s** (~37%
throughput increase) on a 16 GB trace with 46 million spans.

### Why?

Profiling showed the reader thread spending significant time in heap
allocation and deallocation during the initial bulk-load phase. The hot
spots were:

1. `to_string()` / `into_owned()` in `process_internal_row_queue`
allocating a new `String` for every span name, category, and arg
key/value — even when the same string (e.g. `"turbo_tasks::function"`)
appeared in millions of spans.
2. `Vec<SelfTimeEntry>::push` growing one-at-a-time, triggering many
`realloc` calls.
3. `OnceLock<Box<SpanTimeData>>` — inherent to the lazy-init design;
mitigated by the allocator switch.
4. `Vec<TraceRow>::push` in the deserialization loop re-growing during
the initial read.
5. `process_internal_row` allocating a fresh `Vec` queue on every call
(one per row).
6. `Vec<Span>::push` and hash map rehashing in `Store::add_span`.
7. `into_static()` cloning forcing borrowed `Cow` strings to heap for
queued rows.

### How?

**1. mimalloc as global allocator (`main.rs`, `Cargo.toml`)**

Added `turbo-tasks-malloc` as a dependency with a default-on
`custom_allocator` feature (matching the pattern in `turbopack-cli` and
`turbopack-nft`). Set `#[global_allocator] static ALLOC: TurboMalloc =
TurboMalloc`. Also wired `TurboMalloc::thread_stop()` via rayon's
`exit_handler` so each worker thread returns its mimalloc thread-local
heap to the global pool on exit.

**2. `RcStrInterning` in `turbo-rcstr`**

New `RcStrInterning` struct backed by `FxHashSet<RcStr>`. Methods:
`intern(&str)`, `intern_cow(Cow<str>)`, `intern_display(&Display)`.
Strings shorter than the inline threshold (7 bytes on 64-bit) are
already zero-allocation inline atoms and skip the set entirely.

**3. `String` → `RcStr` for span fields**

Changed `Span.name`, `Span.category`, and `Span.args` key/value types
from `String`/`Vec<(String, String)>` to `RcStr`/`Vec<(RcStr, RcStr)>`.
Same for `SpanNames` and `SpanBottomUpBuilder`. All `to_string()` /
`into_owned()` calls in `process_internal_row_queue` are replaced with
`interner.intern()` / `intern_cow()` / `intern_display()` calls on a
per-`TurbopackFormat` `RcStrInterning`. Repeated names like
`"turbo_tasks::function"` now share a single allocation via refcount
bump instead of a fresh heap allocation each time. The `into_static()`
path for queued rows also benefits: cloning an already-interned `RcStr`
is a refcount increment, not a string copy.

**4. Capacity hints**

Pre-allocated major collections to avoid repeated reallocation during
the initial bulk load:
- `Store::spans`: `Vec::with_capacity(131_072)`
- `TurbopackFormat::id_mapping`: `with_capacity(131_072)`
- `TurbopackFormat::outdated_spans`: `with_capacity(8_192)`
- `TurbopackFormat::queued_rows`: `with_capacity(1_024)`
- `TurbopackFormat::thread_stacks`, `thread_allocation_counters`:
`with_capacity(64)`
- `TurbopackFormat::self_time_started`: `with_capacity(256)`

**5. Reused queue Vec (hot spot #8)**

`TurbopackFormat` now holds `row_queue: Vec<InternalRow<'static>>` as a
field. `process_internal_row` takes it via `take` and puts it back after
the loop, retaining the allocated capacity across calls instead of
allocating a new `Vec::new()` for every row processed.

**6. `TraceFormat::create_reused()` hook (hot spot #4)**

Added an overridable `create_reused()` method to the `TraceFormat`
trait. `TurbopackFormat` overrides it to return
`Vec::with_capacity(4_096)`, giving the row deserialization buffer
reasonable initial capacity to avoid repeated doubling during the
initial bulk read.

**7. `SelfTimeTree` leaf capacity (hot spot #2)**

New child nodes created at split time get
`Vec::with_capacity(SPLIT_COUNT / 2)` = 64 entries, matching the
expected post-split occupancy and avoiding incremental doubling after
every split.

<!-- NEXT_JS_LLM_PR -->

---------

Co-authored-by: Tobias Koppers <sokra@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
2026-04-17 13:39:04 +02:00
Luke Sandberg 31a1b635b4 turbo-tasks: Reduce allocations on cache hits (#92756)
### What?

Reduce heap allocations when turbo-tasks functions get cache hits (~85% of calls).

### Why?

Every turbo-tasks function call (generated by `#[turbo_tasks::function]`) was boxing its arguments into `Box<dyn MagicAny>` before looking up the task cache. This allocation is wasted on cache hits, which are the overwhelmingly common case.

### How?

**Deferred boxing via `StackMagicAny` trait object:**

Introduce a `StackMagicAny` trait that abstracts over a stack-resident `Option<T>`:

- `as_ref(&self) -> &dyn MagicAny` — borrow the argument for hash/equality (cache lookup)
- `take_box(&mut self) -> Box<dyn MagicAny>` — move the value to the heap (zero clones)
- `as_any_mut(&mut self) -> &mut dyn Any` — downcast to concrete type without boxing

The data flow:

1. **Callsite** (macro-generated): creates `StackMagicAnySlot::new((args...))` on the stack, calls `dynamic_call(..., &mut arg)`
2. **`dynamic_call`**: checks resolution via `arg.as_ref()`, routes to `native_call` (resolved) or boxes via `arg.take_box()` for `LocalTaskSpec` (unresolved)
3. **Backend `get_or_create_task_inner`**: does a read-only `raw_get` lookup using `hash_from_components` + `eq_components` with the borrowed `&dyn MagicAny`. On cache hit (~85%), returns immediately — **zero heap allocation**. On cache miss, re-checks under write lock using the same borrowed reference, and only calls `arg.take_box()` in the vacant entry case (true cache miss).

Boxing is now deferred past all of these:
- **Memory cache hit** — the common case, no allocation at all
- **Backing storage hit** — found in persistent storage, no allocation needed
- **Lost race under write lock** — another thread inserted while we upgraded; we use their task_id, still no allocation
- **Trait method dispatch (no filtering)** — `filter_owned` is now `Option<FilterOwnedArgsFunctor>`; when `None` (the common case where all args are used), the original `&mut dyn StackMagicAny` passes straight through to `dynamic_call` without boxing

**Optimized `filter_owned` for traits:**

When trait methods *do* need argument filtering (unused `_`-prefixed parameters), the old path did `take_box()` → `downcast_args_owned()` → dereference → repack. This is an unnecessary heap round-trip. The new `downcast_stack_args_owned()` function uses `as_any_mut()` to downcast directly to `&mut StackMagicAnySlot<T>` and calls `take()` on the inner `Option`, skipping the intermediate `Box` entirely.

**Additional changes:**
- `Backend::get_or_create_*_task` now takes decomposed parameters (`native_fn`, `this`, `&mut dyn StackMagicAny`) instead of a pre-constructed `CachedTaskType`
- Persistent and transient task creation merged into a shared `get_or_create_task_inner(transient: bool)`
- `connect_child` uses eagerly-set `persistent_task_type` from `initialize_new_task`
- `OwnedMagicAny` adapter wraps already-boxed args (from async resolution tasks) to fit the `StackMagicAny` interface
- Both `dynamic_call` and `trait_call` take `&mut dyn StackMagicAny` (trait dispatch also benefits)
- `CachedTaskType::hash_encode` now delegates to `hash_encode_components` (deduplicated)
- Removed `try_native_call`, `native_call_if_consistent`, `try_get_or_create_*` — the deferred boxing approach subsumes these

### Binary size

Binary size is neutral (linux-x86_64, `--release`, stripped + gzipped: 30.9 MB on both canary and this branch).

### Overhead benchmark (turbo-tasks-backend, median, lower is better)

Measured on an isolated Firecracker microVM (linux-x86_64). Variance is nontrivial on this environment, but the direction is consistently positive across all turbo-tasks benchmarks.

| Benchmark | Canary | This PR | Delta |
|---|---|---|---|
| `turbo-cached-same-keys/1` | 512.6 ns | 490.2 ns | -4.4% |
| `turbo-cached-same-keys/10` | 493.4 ns | 483.8 ns | -2.0% |
| `turbo-cached-same-keys/100` | 502.1 ns | 484.9 ns | -3.4% |
| `turbo-cached-same-keys/1000` | 1.31 µs | 774.2 ns | -41.0% |
| `turbo-cached-different-keys/1` | 1.02 µs | 1.00 µs | -1.4% |
| `turbo-cached-different-keys/10` | 1.13 µs | 1.11 µs | -1.8% |
| `turbo-cached-different-keys/100` | 1.29 µs | 1.24 µs | -3.8% |
| `turbo-cached-different-keys/1000` | 2.37 µs | 2.32 µs | -2.5% |
| `turbo-uncached/1` | 23.43 µs | 20.47 µs | -12.6% |
| `turbo-uncached/10` | 32.23 µs | 29.71 µs | -7.8% |
| `turbo-uncached/100` | 126.01 µs | 124.95 µs | -0.8% |
| `turbo-uncached/1000` | 1.079 ms | 1.048 ms | -2.9% |
| `turbo-uncached-parallel/1` | 6.16 µs | 5.67 µs | -8.0% |
| `turbo-uncached-parallel/10` | 5.47 µs | 5.12 µs | -6.4% |
| `turbo-uncached-parallel/100` | 14.65 µs | 14.55 µs | -0.7% |
| `turbo-uncached-parallel/1000` | 132.09 µs | 129.13 µs | -2.2% |

<!-- NEXT_JS_LLM_PR -->
2026-04-16 14:56:35 -07:00
Benjamin Woodruff 8a2c5e017e Turbopack: Use FrozenMap for module export information (#92802) 2026-04-15 10:24:23 +02:00
Tobias Koppers b9cd44892c Add TurboMalloc::thread_park() to flush and collect on thread park (#92804)
### What?

Adds `TurboMalloc::thread_park()` — a new method on `TurboMalloc` that
calls `libmimalloc_sys::mi_collect(false)` to process mimalloc's lazy
free lists when a Tokio worker thread parks.

Registers it as an `on_thread_park` callback in every Tokio runtime that
already registers `on_thread_stop`:
- `turbopack-cli/src/main.rs`
- `turbopack-cli/benches/small_apps.rs`
- `next-napi-bindings/src/lib.rs`
- `next-build-test/src/main.rs`
- `turbo-tasks-backend/fuzz/src/graph.rs`

### Why?

mimalloc uses deferred freeing: memory freed by one thread is not
reclaimed immediately but is queued until that thread performs N more
allocations. Tokio worker threads that are parked (waiting for work)
don't allocate, so deferred frees accumulate indefinitely — memory is
never returned to the OS until the thread wakes up and does real work.

Calling `mi_collect(false)` on park processes the lazy free lists,
allowing mimalloc to reclaim that memory, while avoiding the syscalls
and globally synchronized operations that `mi_collect(true)` would
trigger.

### How?

- `thread_park()` only calls `mi_collect(false)` — it intentionally does
**not** call `flush()`. `flush()` transfers the thread-local allocation
counter to the global atomic and resets it to zero, which would violate
the trace server's assumption that per-thread counters are monotonically
increasing. Counter sync remains exclusively in `thread_stop()`.
- `thread_park()` is feature-gated behind `cfg(all(feature =
"custom_allocator", not(target_family = "wasm")))` and is a no-op
otherwise.
- `libmimalloc-sys` is added as a direct optional dependency under
`[target.'cfg(not(target_family = "wasm"))'.dependencies]` (it was
already a transitive dep via `mimalloc`) to access `mi_collect`
directly, since `mimalloc`'s `use ffi::*` is not a public re-export.
- In each `on_thread_park` callback, `TurboMalloc::thread_park()` is
called **after** the existing SWC atom store GC, so atoms freed by that
GC pass are also eligible for reclamation in the same collection.

<!-- NEXT_JS_LLM_PR -->

---------

Co-authored-by: Tobias Koppers <sokra@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
2026-04-15 03:43:39 +02:00
Luke Sandberg 3bbbb32fa9 Enable wasm plugins on windows arm (#92544)
Not because we should, but because we can.

Previously we disabled this due to an incompatibility between windows arm and wasmer, however we recently switched to wasmtime which appears to work.

So simplify the CI configuration and the rust feature flags

https://github.com/vercel/next.js/actions/runs/24271405735 is a build-and-deploy.yml run and this is the [windows arm build](https://github.com/vercel/next.js/actions/runs/24271405735/job/70877042113)
2026-04-14 16:52:37 -07:00
Evan Yu efb699acbb chore: Bump reqwest to 0.13.2 (#92713)
Fixes #91653

Running `next dev` with the latest Turbopack fails to fetch fonts on
Windows ARM machines, resulting in an `http2 feature is not enabled`
error. This issue was fixed in `reqwest` v0.13.2:
https://github.com/seanmonstar/reqwest/pull/2927
2026-04-13 18:09:50 +00:00
Tobias Koppers 74cdcde87e feat(turbopack): add LocalPathOrProjectPath PostCSS config resolution (#91338)
### What?

Adds a new `experimental.turbopackLocalPostcssConfig` option and
corresponding `PostCssConfigLocation::LocalPathOrProjectPath` variant so
Turbopack can resolve `postcss.config.js` starting from the CSS file's
directory first, falling back to the project root.

Also fixes an issue that surfaces when multiple per-directory
`postcss.config.js` files are used:

**Evaluate pool asset name collisions** — output file names for the
Node.js evaluate pool are now derived from an xxh3 hash of the full
module ident rather than the bare file name, so multiple
`postcss.config.js` files in different directories no longer collide.

### Why?

Currently Turbopack uses `ProjectPathOrLocalPath` which checks the
project root first, then falls back to the local directory. This means
per-directory `postcss.config.js` files cannot override the root config
— the root config always wins. For projects that need different PostCSS
transforms in different directories (e.g. a monorepo with multiple apps
or style directories), the more specific config should take precedence.

### How?

**Turbopack PostCSS resolution**
(`turbopack/crates/turbopack-node/src/transforms/postcss.rs`):
- Added `PostCssConfigLocation::LocalPathOrProjectPath` enum variant
with doc comments on all variants
- Refactored `find_config_in_location()` to express the search order as
a `Vec<FileSystemPath>` built from a single `match`, making each
variant's strategy immediately clear

**Evaluate pool asset file naming**
(`turbopack/crates/turbopack-node/src/evaluate.rs`):
- Replaced file-name-based output path derivation with a deterministic
xxh3 hash of the full module ident
- Adds `turbo-tasks-hash` dependency to `turbopack-node`
- Eliminates name collisions when multiple entries share the same file
name (e.g. per-directory `postcss.config.js` files)

**Next.js config flag** (`packages/next/src/server/config-shared.ts`,
`config-schema.ts`):
- Added `experimental.turbopackLocalPostcssConfig: boolean` option
(opt-in, default `false`)
- Wired through `crates/next-core/src/next_config.rs` → both client and
server contexts (`next_client/context.rs`, `next_server/context.rs`)
- When `true`, uses `LocalPathOrProjectPath`; when `false`/unset, uses
the existing `ProjectPathOrLocalPath` (no behavior change by default)

**E2E test** (`test/e2e/app-dir/turbopack-postcss-multiple-configs/`):
- Runs in both **Turbopack dev and production modes** — skips webpack
only (webpack does not support function-valued PostCSS plugins and the
`turbopackLocalPostcssConfig` flag is Turbopack-specific)
- 5 style directories, each with its own `postcss.config.js` passing a
**unique color option** (`blue`, `purple`, `orange`, `cyan`, `magenta`)
to a shared PostCSS plugin, proving that per-directory config resolution
produces distinct results rather than all sharing the same transform
- Root `postcss.config.js` is a no-op — if only the root config were
used, CSS would remain unmodified
- Test validates all 15 elements (5 dirs × 3 files) render with CSS
module classes
- Test collects all CSS (inline `<style>` tags for dev, linked `.css`
files for production) and asserts each directory's expected color is
present and `color: red` is absent

**Documentation**
(`docs/01-app/03-api-reference/05-config/01-next-config-js/turbopackLocalPostcssConfig.mdx`,
`docs/01-app/03-api-reference/08-turbopack.mdx`):
- New dedicated reference page for
`experimental.turbopackLocalPostcssConfig` covering usage, behavior
table, and a directory-tree example
- Added to the experimental options table in the Turbopack API reference

## Test plan

- [x] `cargo check -p turbo-tasks-backend` passes
- [x] `cargo check -p turbopack-node` and `cargo check -p next-core`
pass
- [x] E2E test `turbopack-postcss-multiple-configs` validates
per-directory PostCSS config resolution across 5 directories with
distinct per-directory colors (Turbopack dev and production modes)
- [x] Test correctly skips in webpack mode and deployment
- [x] Documentation page and experimental options table updated

<!-- NEXT_JS_LLM_PR -->

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Tobias Koppers <sokra@users.noreply.github.com>
2026-04-11 07:55:10 +02:00
Luke Sandberg 35b5582647 Remove a ton of turbo task functions from Issue trait items (#92623)
## What?

Remove `#[turbo_tasks::function]` from all methods on the `Issue` value_trait, following the same pattern as #92593 which did the same for `ValueDebug`.

The `Issue` trait previously had 8 methods decorated with `#[turbo_tasks::function]`, which caused the turbo-tasks machinery to generate `NativeFunction` statics, task registration, vtable entries, and scheduling/caching infrastructure for each method on each of the ~48 `impl Issue for` types in the codebase — most of which are trivially synchronous (returning a constant string, enum variant, or stored struct field).

## How?

- Change all 8 `Issue` trait methods to plain methods. Methods that are always synchronous become direct return-type methods (`severity`, `stage`, `source`, `documentation_link`). Methods that are genuinely async on at least one impl use `Pin<Box<dyn Future<Output = Result<T>> + Send + '_>>` for object safety.
- Callers that need to invoke trait methods on a `Vc<Box<dyn Issue>>` use `issue.into_trait_ref().await?` to get a `TraitRef<Box<dyn Issue>>` and call methods directly on that.
- `PlainIssue::from_issue` (the sole hot caller that materializes all fields) resolves one `trait_ref` and calls all methods on it, similar to the `ValueDebug` pattern.
- `ResolvingIssueWithLocation` (which wraps an inner `ResolvedVc<Box<dyn Issue>>`) eagerly copies `severity`, `stage`, `documentation_link`, and `source` at construction time rather than delegating async calls to the inner issue.
- Delete `OptionIssueSource` and `OptionStyledString` wrapper types, which existed solely as `Vc<T>` return type wrappers for the old turbo-tasks methods.

## Tradeoffs

**Lost per-call caching:** Previously each `Issue` method was a turbo-tasks task, so results were cached per `(Vc<Issue>, method)`. In practice this caching was useless — issues are emitted once and their fields read once by `PlainIssue::from_issue`, so there was never a second call to hit the cache. The only real cost is that `IssueFilter::matches` previously cached per `(filter, issue)` pair; that caching is preserved because `matches` itself remains a `#[turbo_tasks::function]` and now calls `into_trait_ref().await?` once at the top.

**`ResolvingIssueWithLocation` eagerly reads inner fields:** Instead of lazily delegating to the inner issue, `stage` and `documentation_link` are copied at construction. This is a minor semantic change (the values are snapshotted rather than live), but in practice these fields are always constant values so it makes no difference.

## Binary size impact (darwin-arm64, compared against #92593)

| Metric | #92593 baseline | This PR | Delta |
|---|---|---|---|
| Unstripped dylib | 124.6 MB | 124.0 MB | **-0.6 MB (-0.5%)** |
| Stripped | 84.7 MB | 84.4 MB | **-0.3 MB (-0.4%)** |
| Stripped + gzip (npm) | 29.2 MB | 29.1 MB | **-0.1 MB (-0.3%)** |

Smaller savings than `ValueDebug` (~2.5 MB stripped) because there are ~48 `Issue` impls vs. one `ValueDebug` impl per every `#[turbo_tasks::value]` type. But real and cumulative.

<!-- NEXT_JS_LLM_PR -->
2026-04-10 16:56:47 -07:00
zoomdong 21933bc80a chore(turbopack): upgrade qfiler to v0.3.0-alpha.4 (#92558)
utoopack upgrade to crate dependencies of turbopack, this dependenices
will upgrade to `0.3.0-alpha.4` and cause api build error.

it seems we can just upgrade it:
https://github.com/arthurprs/qfilter/pull/24/changes#diff-b1a35a68f14e696205874893c07fd24fdb88882b47c23cc0e0c80a30c7d53759

---------

Co-authored-by: Benjamin Woodruff <benjamin.woodruff@vercel.com>
2026-04-10 22:45:06 +00:00
Niklas Mischkulnig 0213797790 Bump swc to 63.1.1 (#92604)
To pull in https://github.com/swc-project/swc/pull/11777
2026-04-10 15:12:58 -07:00