Skip to content

[pull] canary from vercel:canary - #1360

Merged
pull[bot] merged 2 commits into
code:canaryfrom
vercel:canary
Sep 2, 2026
Merged

[pull] canary from vercel:canary#1360
pull[bot] merged 2 commits into
code:canaryfrom
vercel:canary

Conversation

@pull

@pull pull Bot commented Sep 2, 2026

Copy link
Copy Markdown

See Commits and Changes for more details.


Created by pull[bot] (v2.0.0-alpha.4)

Can you help keep this open source service alive? 💖 Please sponsor : )

Avoid spawning a worker thread to run turbopack

The typical arguments for worker threads are that they isolate the JS
heap and enable parallelism, but turbopack does both these things by
default (native heap is isolated, the js deps are minor and have a lot
of overlap with existing next server deps).

There is also no substantial parallelism opportunity from worker threads
that aren't immediately delivered by turbopacks execution model

## Benchmarks

Measured with a paired A/B harness that alternates `head` and `base`
builds so thermal drift and background load cancel out, discarding a
warmup build per variant and clearing `.next` before each run. Both
variants are the same commit except for this change. 35 paired
iterations on a default `create-next-app` (App Router, TS), Node
20.11.1, Apple silicon.

| Metric | this PR | canary | Diff | 95% CI | Wins |
| --- | --- | --- | --- | --- | --- |
| `next-build` total | 2478 ms | 2595 ms | **-117 ms (4.5%)** | [50,
183] | 30/35 |
| `run-turbopack` | 1228 ms | 1335 ms | -107 ms (8.0%) | [62, 152] |
31/35 |
| overhead (total - turbopack) | 1250 ms | 1260 ms | -10 ms (0.8%) |
[-27, 46] | 20/35 |
| wall clock | 2775 ms | 2888 ms | -112 ms (3.9%) | [45, 180] | 30/35 |
| peak RSS | 1175 MiB | 1195 MiB | **-19 MiB (1.6%)** | [3.4, 35.3] |
22/35 |

Significant by both a paired t-test and a Wilcoxon signed-rank test (p =
0.0001 for total build time, p = 0.025 for RSS). The Wilcoxon is
included because a couple of slow builds per run would otherwise
dominate the variance.

Nearly all of the saving lands inside the `run-turbopack` span rather
than around it, which is expected: that span wraps the worker spawn, the
IPC to marshal the build across, and the teardown, so deleting the
worker removes work from inside it.

The win is proportionally smaller on larger apps, where compilation
dominates. On `bench/basic-app` (~400 source modules, ~19s builds) the
same ~115 ms is well inside the noise -- 12 paired iterations there
showed no significant difference on any metric. So this is a real but
small improvement that is only user-visible on small builds:

| Metric | this PR | canary | Diff | 95% CI |
| --- | --- | --- | --- | --- |
| `next-build` total | 19313 ms | 19436 ms | -123 ms (0.6%) | [-256,
502] |
| overhead (total - turbopack) | 1424 ms | 1415 ms | +9 ms (0.6% slower)
| [-52, 70] |

Caveats: single machine and two fixture shapes. Peak RSS is the build
process's own footprint (`/usr/bin/time -l`), which includes the worker
thread's isolate but not the separately spawned static-generation
workers. A CI runner with fewer cores would plausibly show a larger
effect.
Adds garbage collection, built on the `parent_count` reference counting

### How collection works

* we scan all storage shards for collectible tasks to seed the sweep
* For each collectible task we
    1. mark it as deleted
    2. remove all outgoing edges
    3. queue new tasks for deletion if the edge removals triggered it

For removing edges we rely on the existing `CleanupOldEdges` operation,
though it is enhanced to collect the tasks that become collectible as
edges are removed

Deletion markers are transformed into tombstones by persistence and then
dropped from memory by eviction.

### Coordinator changes

GC mutates the graph, so we exclude other tasks from running while it
works. When gc completes we hand off to persistence which switches to
the existing copy on write mode.

The subtle piece is `GcPhase::into_snapshot`: GC has to hand its
exclusion directly to the snapshot that tombstones what it collected.
Swapping the flags under one lock leaves no window where an operation
could start and resurrect a just-collected task.

### Enabling it

Off by default; `TURBO_ENGINE_GC` turns it on, also requires eviction to
be enabled (otherwise deleted tasks persist in RAM)

`prevent_gc()` becomes real, pinning the calling task through
`transient_ref_count`.
@pull pull Bot locked and limited conversation to collaborators Sep 2, 2026
@pull pull Bot added the ⤵️ pull label Sep 2, 2026
@pull
pull Bot merged commit 8ea76d6 into code:canary Sep 2, 2026
9 of 11 checks passed
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant