Skip to content

turbo-tasks: fix hashed cell mode crash on task error (re-land #91576)#92108

Merged
sokra merged 4 commits into
canaryfrom
sokra/store-hash-only
Apr 7, 2026
Merged

turbo-tasks: fix hashed cell mode crash on task error (re-land #91576)#92108
sokra merged 4 commits into
canaryfrom
sokra/store-hash-only

Conversation

@sokra

@sokra sokra commented Mar 30, 2026

Copy link
Copy Markdown
Member

What?

Re-lands #91576 ("turbo-tasks: add hashed cell mode for hash-based change detection without cell data"), which was reverted in #92103 due to a FATAL crash in the filesystem-cache test suite.

Includes a bug fix on top: in task_execution_completed_prepare, skip updating cell_type_max_index when the task completed with an error.

Also adds a CellHash = [u8; 16] type alias (requested in review) used throughout the hash pipeline.

Why?

The original feature (serialization = "hash" on FileContent and Code) stores a hash of the cell data instead of the full serialized value. On session restore, the hash is used to detect whether cell content has changed without needing the full data in memory. This avoids a large persistent cache size increase.

The bug that caused the revert: When a task fails partway through re-execution (before recreating all the cells from its previous run), cell_counters only reflects the partially-executed state. The old code used those partial counters to update cell_type_max_index, removing entries for cell types that were not yet created at the point of failure. This caused downstream tasks that still held cell dependencies from the previous successful run to hit a hard "Cell no longer exists" error.

Concrete failure path in filesystem-cache rename app page test:

  1. get_app_page_entry runs for /remove-me/page, creating two FileContent cells (indices 0 and 1). cell_type_max_index[FileContent] = 2 is persisted.
  2. The folder is renamed (app/remove-meapp/add-me), dirtying the task.
  3. On re-execution, get_app_page_entry fails at config.await? (the loader tree errors because the directory is gone) — before any FileContent::cell() calls.
  4. cell_counters has no FileContent entry → old code removed cell_type_max_index[FileContent].
  5. The parse task tries to read FileContent cell 1 from get_app_page_entrycell_type_max_index is None"Cell no longer exists" panic → FATAL error.

Why it didn't crash before serialization = "hash": FileContent was previously serializable, so parse read stale cell data directly from persistent_cell_data, which task_execution_completed_cleanup already preserves on error. With serialization = "hash", data is transient — readers fall back to cell_type_max_index for range validation, where a stale None caused the crash.

How?

Core feature: serialization = "hash" cell mode

  • New SerializationMode::Hash variant in turbo-tasks-macros — marks a value type as non-serializable but stores a DeterministicHash of the cell data for change detection.
  • VcCellHashedCompareMode<T> cell mode: compares values via PartialEq when available, falls back to hash comparison when transient data has been evicted.
  • hashed_compare_and_update / hashed_compare_and_update_with_shared_reference on CurrentCellRef compute and pass content hashes through the update pipeline.
  • Backend update_cell uses hash-based comparison to skip invalidation when the old cell data is unavailable but the hash matches.
  • cell_data_hash: AutoMap<CellId, CellHash> field in task storage persists hashes across sessions.
  • Stale cell_data_hash entries are cleaned up in task_execution_completed_cleanup alongside cell data removal.
  • CellHash = [u8; 16] type alias keeps alignment at 1 byte to avoid padding growth in AutoMap/LazyField enum variants.
  • Hash bytes use little-endian encoding (to_le_bytes) for cross-platform cache portability.

Bug fix: preserve cell_type_max_index on task error

In task_execution_completed_prepare, guard the cell_type_max_index update block with if result.is_ok(). This mirrors the existing task_execution_completed_cleanup behavior that already skips cell data removal when is_error is true, keeping cell_type_max_index consistent with the preserved transient cell data.

Applied to FileContent and Code

  • FileContent uses serialization = "hash" — full content is persisted via a separate PersistedFileContent type when needed (e.g., in DiskFileSystem::write).
  • Code uses serialization = "hash" with Arc<Vec<Mapping>> for cheap cloning. Code::cell_persisted() creates a PersistedCode cell directly and returns Vc<Code> via PersistedCode::to_code(), avoiding an intermediate hash-mode cell.

Other improvements

  • DeterministicHash impls for SmallVec and ().
  • Xxh3Hash128Hasher::finish_bytes() method returning [u8; 16].
  • hash = "manual" option on #[turbo_tasks::value] to opt out of auto-deriving DeterministicHash.

Note: The shutdown hang and cache poisoning fixes that were previously on this branch have been merged separately via #92254.

Test plan

  • test/e2e/filesystem-cache/filesystem-cache.test.ts passes (all 17 tests)
  • New turbopack/crates/turbo-tasks-backend/tests/hashed_cell_mode.rs integration test verifies hash-based change detection: value changes trigger invalidation, equal values (same hash) do not
  • cargo check passes for turbo-tasks, turbo-tasks-backend, turbo-tasks-fs, turbopack-core, turbopack-ecmascript
  • CI green (attempt 2)

@nextjs-bot nextjs-bot added created-by: Turbopack team PRs by the Turbopack team. Turbopack Related to Turbopack with Next.js. labels Mar 30, 2026
@nextjs-bot

nextjs-bot commented Mar 30, 2026

Copy link
Copy Markdown
Contributor

Tests Passed

@codspeed-hq

codspeed-hq Bot commented Mar 30, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 17 untouched benchmarks
⏩ 3 skipped benchmarks1


Comparing sokra/store-hash-only (9d592de) with canary (9853944)

Open in CodSpeed

Footnotes

  1. 3 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

@sokra sokra requested a review from lukesandberg March 31, 2026 16:12
@sokra sokra marked this pull request as ready for review March 31, 2026 16:15
Comment thread turbopack/crates/turbo-tasks/src/manager.rs Outdated
@nextjs-bot

nextjs-bot commented Apr 1, 2026

Copy link
Copy Markdown
Contributor

Stats from current PR

✅ No significant changes detected

📊 All Metrics
📖 Metrics Glossary

Dev Server Metrics:

  • Listen = TCP port starts accepting connections
  • First Request = HTTP server returns successful response
  • Cold = Fresh build (no cache)
  • Warm = With cached build artifacts

Build Metrics:

  • Fresh = Clean build (no .next directory)
  • Cached = With existing .next directory

Change Thresholds:

  • Time: Changes < 50ms AND < 10%, OR < 2% are insignificant
  • Size: Changes < 1KB AND < 1% are insignificant
  • All other changes are flagged to catch regressions

⚡ Dev Server

Metric Canary PR Change Trend
Cold (Listen) 456ms 455ms ▁█▅▅▅
Cold (Ready in log) 442ms 443ms ▁▂▅▇▄
Cold (First Request) 1.150s 1.139s ▆▁▁▂▂
Warm (Listen) 457ms 456ms █▁██▁
Warm (Ready in log) 443ms 442ms ▂▂▇█▁
Warm (First Request) 339ms 345ms ▃▄▇█▄
📦 Dev Server (Webpack) (Legacy)

📦 Dev Server (Webpack)

Metric Canary PR Change Trend
Cold (Listen) 456ms 456ms ▁▁▅▁▅
Cold (Ready in log) 435ms 435ms ▁▃▆▂▂
Cold (First Request) 1.950s 1.942s ▇▇▇▆▁
Warm (Listen) 455ms 456ms ▁▁▁▁▁
Warm (Ready in log) 436ms 436ms ▁▂▅▁▂
Warm (First Request) 1.968s 1.958s ▆▇█▆▁

⚡ Production Builds

Metric Canary PR Change Trend
Fresh Build 3.872s 3.872s ▇▆██▁
Cached Build 3.956s 3.927s ▄███▄
📦 Production Builds (Webpack) (Legacy)

📦 Production Builds (Webpack)

Metric Canary PR Change Trend
Fresh Build 14.387s 14.411s ▁▂▅▂▂
Cached Build 14.477s 14.603s ▁▂▇▃▄
node_modules Size 488 MB 488 MB █████
📦 Bundle Sizes

Bundle Sizes

⚡ Turbopack

Client

Main Bundles
Canary PR Change
02fkg8wfh0iju.js gzip 9.19 kB N/A -
050zwt5xh_0tx.js gzip 10.4 kB N/A -
0803-3r6mifdx.js gzip 157 B N/A -
087fzjd-gvlzv.js gzip 450 B N/A -
0cz1d0mv5g_q7.js gzip 39.4 kB 39.4 kB
0d0pcwea0l749.js gzip 151 B N/A -
0d34flh8j9r6u.js gzip 156 B N/A -
0p0khj57rq_v_.js gzip 162 B N/A -
0ppxcl_z43mad.js gzip 8.52 kB N/A -
13mnpc17btogu.js gzip 157 B N/A -
19oha6-znmkcv.js gzip 8.55 kB N/A -
1elt1qium-r2m.css gzip 115 B 115 B
1ppe_gkx_dsny.js gzip 159 B N/A -
2_5rjb7lqxntf.js gzip 221 B 221 B
219prxwxgaalc.js gzip 7.61 kB N/A -
26elcgxnn9zjd.js gzip 8.52 kB N/A -
28s7tbll9vbjr.js gzip 156 B N/A -
2900hudr6gvm0.js gzip 2.28 kB N/A -
2bbl2qamyvimj.js gzip 65.7 kB N/A -
2lv2js3kmdeho.js gzip 8.48 kB N/A -
2rehygrd36hqv.js gzip 8.58 kB N/A -
2scbv16he964r.js gzip 158 B N/A -
2srwswih0m9_h.js gzip 13.3 kB N/A -
3-jz00s4w-r6h.js gzip 13 kB N/A -
3-p9p9mheqhzx.js gzip 8.55 kB N/A -
31030bryqpolg.js gzip 8.53 kB N/A -
31dx5nmrzzuy7.js gzip 225 B N/A -
37r23u64aoktk.js gzip 155 B N/A -
3925v09gtu-5k.js gzip 49 kB N/A -
39x4zj5mjb4d_.js gzip 9.77 kB N/A -
3at2ovgizp8r6.js gzip 158 B N/A -
3bknr2e7m9s7z.js gzip 155 B N/A -
3k-48b78ys_vy.js gzip 10.1 kB N/A -
3m7-5rfj0avoz.js gzip 12.9 kB N/A -
3t39n05ky9z08.js gzip 70.8 kB N/A -
3uqce_6sa526g.js gzip 8.47 kB N/A -
3yurjqk-sjs3y.js gzip 1.46 kB N/A -
3znov3m90-kab.js gzip 168 B N/A -
40ybjx9c192n0.js gzip 13.8 kB N/A -
421vzwdt9j1b_.js gzip 5.62 kB N/A -
44jj5q-kk1jan.js gzip 157 B N/A -
turbopack-03..e7c0.js gzip 4.18 kB N/A -
turbopack-0k..9h2a.js gzip 4.18 kB N/A -
turbopack-0m..6r79.js gzip 4.18 kB N/A -
turbopack-0v..v8st.js gzip 4.18 kB N/A -
turbopack-1s..tsd2.js gzip 4.16 kB N/A -
turbopack-3-..bo-6.js gzip 4.18 kB N/A -
turbopack-31..l4gh.js gzip 4.18 kB N/A -
turbopack-36..rum2.js gzip 4.18 kB N/A -
turbopack-3i..3636.js gzip 4.18 kB N/A -
turbopack-3v..wfxq.js gzip 4.18 kB N/A -
turbopack-3v..1qwv.js gzip 4.19 kB N/A -
turbopack-3z..kcbv.js gzip 4.18 kB N/A -
turbopack-40..j2ay.js gzip 4.18 kB N/A -
turbopack-42..qz47.js gzip 4.17 kB N/A -
03dgzoo-qf3sm.js gzip N/A 9.19 kB -
03i0taczqebbx.js gzip N/A 70.8 kB -
05tx5f25dlivn.js gzip N/A 8.53 kB -
0c7ez6p2qc57f.js gzip N/A 5.62 kB -
0duvj3qk5pvgn.js gzip N/A 13.8 kB -
0ifxao1ktkgwg.js gzip N/A 156 B -
0m-34rm9w_wpm.js gzip N/A 7.6 kB -
0qnwuk92m8i7o.js gzip N/A 10.4 kB -
0r4wrn6n0ue2m.js gzip N/A 8.55 kB -
0rp0fodtbt_6m.js gzip N/A 8.52 kB -
0sfck-km4dl1k.js gzip N/A 8.47 kB -
0x0xuhmxzwkp8.js gzip N/A 8.47 kB -
1-wdvgxnzicj7.js gzip N/A 1.46 kB -
11u6nxujb2eg4.js gzip N/A 450 B -
19uunh8umr1a1.js gzip N/A 157 B -
1el9fuakpgh8m.js gzip N/A 155 B -
1jv-o1_s-zmua.js gzip N/A 49 kB -
1mifo-hcc4vf6.js gzip N/A 154 B -
1o5x2xlfw7x62.js gzip N/A 156 B -
1sk7rrnby7fjt.js gzip N/A 157 B -
2-j7jrt35v955.js gzip N/A 160 B -
27kwgyklbqvcl.js gzip N/A 152 B -
2e2z-03lx4fjc.js gzip N/A 13 kB -
2irxuxkr23i0g.js gzip N/A 160 B -
2k9ax08cjl2id.js gzip N/A 12.9 kB -
2lms6k76q5-6m.js gzip N/A 13.3 kB -
2qx4twi9i3xus.js gzip N/A 2.28 kB -
2srnqic6tvxxd.js gzip N/A 8.52 kB -
2zkc9u4375pyw.js gzip N/A 157 B -
30l7m4nayp73a.js gzip N/A 8.55 kB -
34v1uamxoz09s.js gzip N/A 170 B -
34wde90lr4zme.js gzip N/A 157 B -
3h_ecpiaatwgc.js gzip N/A 10.1 kB -
3hxw-cpxtvy_3.js gzip N/A 156 B -
3ity0aahajapd.js gzip N/A 225 B -
3wrhpuc-j1aw9.js gzip N/A 9.77 kB -
3xlti3rufjlyg.js gzip N/A 65.7 kB -
43mlw9dy_8f02.js gzip N/A 8.58 kB -
turbopack-02..6_tq.js gzip N/A 4.18 kB -
turbopack-0h..r50b.js gzip N/A 4.18 kB -
turbopack-17..z-3u.js gzip N/A 4.19 kB -
turbopack-18..evlj.js gzip N/A 4.17 kB -
turbopack-1c..a07c.js gzip N/A 4.18 kB -
turbopack-1h..a606.js gzip N/A 4.18 kB -
turbopack-1o.._bpf.js gzip N/A 4.18 kB -
turbopack-1w..e9r6.js gzip N/A 4.18 kB -
turbopack-22..wdmr.js gzip N/A 4.18 kB -
turbopack-2c..zde7.js gzip N/A 4.18 kB -
turbopack-31..4lzd.js gzip N/A 4.18 kB -
turbopack-3g..9wtz.js gzip N/A 4.18 kB -
turbopack-3l..q89n.js gzip N/A 4.18 kB -
turbopack-40..aa11.js gzip N/A 4.16 kB -
Total 464 kB 464 kB ✅ -22 B

Server

Middleware
Canary PR Change
middleware-b..fest.js gzip 722 B 714 B 🟢 8 B (-1%)
Total 722 B 714 B ✅ -8 B
Build Details
Build Manifests
Canary PR Change
_buildManifest.js gzip 434 B 435 B
Total 434 B 435 B ⚠️ +1 B

📦 Webpack

Client

Main Bundles
Canary PR Change
5528-HASH.js gzip 5.54 kB N/A -
6280-HASH.js gzip 60.7 kB N/A -
6335.HASH.js gzip 169 B N/A -
912-HASH.js gzip 4.59 kB N/A -
e8aec2e4-HASH.js gzip 62.8 kB N/A -
framework-HASH.js gzip 59.7 kB 59.7 kB
main-app-HASH.js gzip 256 B 254 B
main-HASH.js gzip 39.4 kB 39.3 kB
webpack-HASH.js gzip 1.68 kB 1.68 kB
262-HASH.js gzip N/A 4.59 kB -
2889.HASH.js gzip N/A 169 B -
5602-HASH.js gzip N/A 5.55 kB -
6948ada0-HASH.js gzip N/A 62.8 kB -
9544-HASH.js gzip N/A 61.4 kB -
Total 235 kB 235 kB ⚠️ +579 B
Polyfills
Canary PR Change
polyfills-HASH.js gzip 39.4 kB 39.4 kB
Total 39.4 kB 39.4 kB
Pages
Canary PR Change
_app-HASH.js gzip 194 B 194 B
_error-HASH.js gzip 183 B 180 B 🟢 3 B (-2%)
css-HASH.js gzip 331 B 330 B
dynamic-HASH.js gzip 1.81 kB 1.81 kB
edge-ssr-HASH.js gzip 256 B 256 B
head-HASH.js gzip 351 B 352 B
hooks-HASH.js gzip 384 B 383 B
image-HASH.js gzip 580 B 581 B
index-HASH.js gzip 260 B 260 B
link-HASH.js gzip 2.51 kB 2.51 kB
routerDirect..HASH.js gzip 320 B 319 B
script-HASH.js gzip 386 B 386 B
withRouter-HASH.js gzip 315 B 315 B
1afbb74e6ecf..834.css gzip 106 B 106 B
Total 7.98 kB 7.98 kB ✅ -1 B

Server

Edge SSR
Canary PR Change
edge-ssr.js gzip 125 kB 126 kB
page.js gzip 273 kB 273 kB
Total 398 kB 398 kB ⚠️ +159 B
Middleware
Canary PR Change
middleware-b..fest.js gzip 617 B 616 B
middleware-r..fest.js gzip 156 B 155 B
middleware.js gzip 44 kB 44.5 kB 🔴 +464 B (+1%)
edge-runtime..pack.js gzip 842 B 842 B
Total 45.6 kB 46.1 kB ⚠️ +462 B
Build Details
Build Manifests
Canary PR Change
_buildManifest.js gzip 715 B 718 B
Total 715 B 718 B ⚠️ +3 B
Build Cache
Canary PR Change
0.pack gzip 4.38 MB 4.37 MB 🟢 9.61 kB (0%)
index.pack gzip 116 kB 113 kB 🟢 3.12 kB (-3%)
index.pack.old gzip 115 kB 116 kB 🔴 +1.26 kB (+1%)
Total 4.61 MB 4.6 MB ✅ -11.5 kB

🔄 Shared (bundler-independent)

Runtimes
Canary PR Change
app-page-exp...dev.js gzip 342 kB 342 kB
app-page-exp..prod.js gzip 189 kB 189 kB
app-page-tur...dev.js gzip 341 kB 341 kB
app-page-tur..prod.js gzip 189 kB 189 kB
app-page-tur...dev.js gzip 338 kB 338 kB
app-page-tur..prod.js gzip 187 kB 187 kB
app-page.run...dev.js gzip 338 kB 338 kB
app-page.run..prod.js gzip 187 kB 187 kB
app-route-ex...dev.js gzip 76.6 kB 76.6 kB
app-route-ex..prod.js gzip 52.2 kB 52.2 kB
app-route-tu...dev.js gzip 76.6 kB 76.6 kB
app-route-tu..prod.js gzip 52.2 kB 52.2 kB
app-route-tu...dev.js gzip 76.2 kB 76.2 kB
app-route-tu..prod.js gzip 52 kB 52 kB
app-route.ru...dev.js gzip 76.2 kB 76.2 kB
app-route.ru..prod.js gzip 52 kB 52 kB
dist_client_...dev.js gzip 324 B 324 B
dist_client_...dev.js gzip 326 B 326 B
dist_client_...dev.js gzip 318 B 318 B
dist_client_...dev.js gzip 317 B 317 B
pages-api-tu...dev.js gzip 43.8 kB 43.8 kB
pages-api-tu..prod.js gzip 33.4 kB 33.4 kB
pages-api.ru...dev.js gzip 43.8 kB 43.8 kB
pages-api.ru..prod.js gzip 33.4 kB 33.4 kB
pages-turbo....dev.js gzip 53.2 kB 53.2 kB
pages-turbo...prod.js gzip 39 kB 39 kB
pages.runtim...dev.js gzip 53.2 kB 53.2 kB
pages.runtim..prod.js gzip 39 kB 39 kB
server.runti..prod.js gzip 62.8 kB 62.8 kB
Total 3.03 MB 3.03 MB ✅ -3 B
📎 Tarball URL
https://vercel-packages.vercel.app/next/commits/9d592de8e0f9cbd38751b802b3442f04caaab0af/next

@sokra sokra force-pushed the sokra/store-hash-only branch from af0fbdf to fc6911e Compare April 1, 2026 20:45
@sokra sokra force-pushed the sokra/store-hash-only branch 3 times, most recently from 505df5f to 9ec8687 Compare April 3, 2026 20:42
@nextjs-bot nextjs-bot added create-next-app Related to our CLI tool for quickly starting a new Next.js application. type: next labels Apr 3, 2026
sokra and others added 2 commits April 7, 2026 05:25
@sokra sokra force-pushed the sokra/store-hash-only branch from 9ec8687 to 85ab7e0 Compare April 7, 2026 05:26
…d of removed resolve_strongly_consistent()

Co-Authored-By: Claude <noreply@anthropic.com>
Comment thread turbopack/crates/turbopack-ecmascript/src/chunk/item.rs Outdated
Comment thread turbopack/crates/turbopack-core/src/code_builder.rs Outdated
…dianness, improve Code persistence

- Clean up cell_data_hash entries in task_execution_completed_cleanup for removed cells
- Extract duplicated hash-update logic into update_cell_data_hash() helper
- Use to_le_bytes() instead of to_ne_bytes() for cross-platform hash consistency
- Add cell_persisted() to Code to avoid triple-cell indirection
- Change Code.mappings to Arc<Vec<Mapping>> to reduce clone cost
- Replace rand::random with AtomicU32 counter in hashed_cell_mode test

Co-Authored-By: Claude <noreply@anthropic.com>
@sokra sokra merged commit 8db9a75 into canary Apr 7, 2026
314 of 316 checks passed
@sokra sokra deleted the sokra/store-hash-only branch April 7, 2026 15:59
@github-actions github-actions Bot locked as resolved and limited conversation to collaborators Apr 22, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

create-next-app Related to our CLI tool for quickly starting a new Next.js application. created-by: Turbopack team PRs by the Turbopack team. locked Turbopack Related to Turbopack with Next.js. type: next

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants