Repository navigation
turbo-tasks-backend: keep recorded GC roots until their TTL expires - #100279
Merged
Merged
Conversation
Contributor
Tests PassedCommit: 1f2e702 |
lukesandberg
commented
Oct 15, 2026
lukesandberg
commented
Oct 17, 2026
lukesandberg
commented
Oct 18, 2026
lukesandberg
marked this pull request as ready for review
October 19, 2026 20:41
jimmyhmiller
approved these changes
Nov 3, 2026
Co-authored-by: Luke Sandberg <210140+lukesandberg@users.noreply.github.com>
Co-authored-by: Luke Sandberg <210140+lukesandberg@users.noreply.github.com>
Co-authored-by: Luke Sandberg <210140+lukesandberg@users.noreply.github.com>
Co-authored-by: Luke Sandberg <210140+lukesandberg@users.noreply.github.com>
lukesandberg
force-pushed
the
fleet/gc-root-ttl-resident-roots
branch
from
November 3, 2026 18:30
96e0ca6 to
1f2e702
Compare
lukesandberg
added a commit
that referenced
this pull request
Nov 6, 2026
β¦100279) With Turbopack GC enabled, a task recorded in the persisted GC roots map now stays protected from collection until its TTL expires. Before, a recorded root was dropped from the map as soon as it became resident again. This also fixes pinned roots falling out of the roots map after their `Meta` was evicted. At refresh time, `gc_roots_refresh_and_age_out` dropped every *resident* root from the persisted roots map, assuming the pass's root scan would judge it. But the judgement would ~always judge them as dead. A task becomes a root because it had only transient pins from a prior session, if we read it back to 'refresh' then it doens't have those transient pins and will always be collected. On a warm `next dev` start, startup invalidation propagates through the aggregation graph and restores almost every prior root before anything requests it. So every root that hadn't been re-pinned yet was collected in the first GC pass, along with its whole exclusive subtree. The 3-day TTL never applied, and the next session had to recompute and re-persist everything. On `bench/nested-deps-app-router-many-pages` with GC enabled: - a warm session that idled before its first request collected about 4.3M tasks; - the next session then ran as slowly as a cold one. A related bug: the root scan can't see a task whose `Meta` has been evicted, so the refresh also dropped roots that were still pinned. **Once a root, always a root, until it ages out.** In theory a prior session root could gain a 'persistent pin' and then lose it which might mean we could collect it, this case is currently uncovered (and completely theoretical). - `cargo nextest run -p turbo-tasks-backend`: all tests pass. - `cargo clippy -p turbo-tasks-backend --all-targets` and `cargo check -p turbo-tasks-backend --release` are clean. - The end-to-end numbers above were measured with an earlier revision of this PR. In that run, an idle warm session collected 0 tasks and kept all 246 roots, and the next session ran at warm speed. I haven't re-run them on this revision. The cold-to-warm path they measured still keeps prior roots protected, and it now has fewer special cases. Not covered here: retention cost (memory and cache size) and persisted-byte amplification. <!-- NEXT_JS_LLM --> <!-- fleet 96d8c261-a703-457c-b145-0fb472af31a7 --> --------- Co-authored-by: vercel-fleet-prod[bot] <318278635+vercel-fleet-prod[bot]@users.noreply.github.com> Co-authored-by: Luke Sandberg <210140+lukesandberg@users.noreply.github.com> (cherry picked from commit dd54647) Co-authored-by: Luke Sandberg <lukesandberg@users.noreply.github.com>
lukesandberg
added a commit
that referenced
this pull request
Nov 7, 2026
Backports these two Turbopack GC fixes to `releases/lts/active`: - [turbo-tasks-backend: keep recorded GC roots until their TTL expires (#100279)](#100279) - [[turbopack] Fix GC/eviction race by dropping task resurrection (#100370)](#100370) It builds on #100209 (the #99902 backport) and #100002 (snapshot consistency fixes), both already on this branch. GC is opt-in on 16.4 (`experimental.turbopackGc`), so this only affects apps that enable it. ## Conflict resolution **#100279** - `gc.rs` / `mod.rs` doc comments: took canary's text, which uses plain backticks instead of intra-doc links. - `snapshot_and_persist`: the `process` closure keeps this branch's 3-argument signature. #99747, which added a `category` parameter, isn't on this branch. The PR's `#[cfg(debug_assertions)] gc_protected_roots` set and the extended `debug_assert!` were added unchanged. **#100370** - `aggregation_update.rs` `increase_active_count`: replaced `resurrect_deleted(...)` with `task.assert_not_deleted(...)`. Kept the `self.check_optimization_pending(&task)` call that follows it on this branch; canary removed it in #100146/#100147, which aren't here. - `aggregation_update.rs` `optimize_queue.pop_front()` arm: kept the `optimization_pending_flag_already_set` binding. Canary renames it to `_`, but this branch still reads it in the per-job budget-exhausted path (`lock_and_mark_optimization_pending`). Canary replaced that path with a bulk drain. Everything else applied as-is. ## Verification - `cargo nextest run -p turbo-tasks-backend`: 289/289 passed. This includes the new `gc_cross_session` TTL tests and the `task_by_type_tests` lookup tests. - `cargo clippy -p turbo-tasks-backend --tests` is clean. - `cargo check -p turbo-tasks-backend` passes with `--release` and with `--features task_dirty_cause`. - `cargo fmt --check` is clean. CI may show the Tailwind failures currently affecting all `releases/lts/active` PRs until the Tailwind pin backport (#101307) lands. <!-- NEXT_JS_LLM --> <!-- fleet 91f3474f-6748-4c06-8329-d8d3673533dd --> --------- Co-authored-by: vercel-fleet-prod[bot] <318278635+vercel-fleet-prod[bot]@users.noreply.github.com> Co-authored-by: Luke Sandberg <210140+lukesandberg@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What?
With Turbopack GC enabled, a task recorded in the persisted GC roots map now stays protected from collection until its TTL expires. Before, a recorded root was dropped from the map as soon as it became resident again. This also fixes pinned roots falling out of the roots map after their
Metawas evicted.Why?
At refresh time,
gc_roots_refresh_and_age_outdropped every resident root from the persisted roots map, assuming the pass's root scan would judge it. But the judgement would ~always judge them as dead. A task becomes a root because it had only transient pins from a prior session, if we read it back to 'refresh' then it doens't have those transient pins and will always be collected. On a warmnext devstart, startup invalidation propagates through the aggregation graph and restores almost every prior root before anything requests it. So every root that hadn't been re-pinned yet was collected in the first GC pass, along with its whole exclusive subtree. The 3-day TTL never applied, and the next session had to recompute and re-persist everything.On
bench/nested-deps-app-router-many-pageswith GC enabled:A related bug: the root scan can't see a task whose
Metahas been evicted, so the refresh also dropped roots that were still pinned.How?
Once a root, always a root, until it ages out.
In theory a prior session root could gain a 'persistent pin' and then lose it which might mean we could collect it, this case is currently uncovered (and completely theoretical).
Verification
cargo nextest run -p turbo-tasks-backend: all tests pass.cargo clippy -p turbo-tasks-backend --all-targetsandcargo check -p turbo-tasks-backend --releaseare clean.Not covered here: retention cost (memory and cache size) and persisted-byte amplification.