turbo-tasks: add hashed cell mode for hash-based change detection without cell data (#91576)
## Summary
### Core: hash-based cell change detection
- Adds `cell_data_hash: AutoMap<CellId, [u8; 16]>` to the storage schema
(`category = "data"`) so the backend can persist a hash of transient
cell data across evictions
- Stored as `[u8; 16]` (little-endian bytes of a u128) rather than
`u128` to keep the 1-byte alignment out of `AutoMap` and therefore out
of the `LazyField` enum — a bare `u128` would grow the enum from 56 to
64 bytes due to its 16-byte alignment requirement
- Adds `content_hash: Option<u128>` to `UpdateCellOperation::run` and
threads it through the full call chain: `CurrentCellRef` →
`TurboTasksCallApi::update_own_task_cell` → `Backend::update_task_cell`
→ operation
- **New invalidation logic** in `UpdateCellOperation::run` for the
`assume_unchanged = false` path:
- Old content available → real equality compare (unchanged)
- Old content evicted, hashes match → write new content but **skip
dependent invalidation**
- Old content evicted, hashes differ (or missing) → write + invalidate
as before
- `cell_data_hash` is updated whenever content is written (skipped if
hash is unchanged; always updated for non-serializable cells regardless
of `assume_unchanged`)
- Adds `hashed_compare_and_update` /
`hashed_compare_and_update_with_shared_reference` methods to
`CurrentCellRef` (require `T: PartialEq + DeterministicHash`); hash is
computed lazily (only after equality check fails when old value is
available)
- Adds `VcCellHashedCompareMode<T>` in `cell_mode.rs` which implements
`VcCellMode<T>` for `T: VcValueType + PartialEq + DeterministicHash`
### Macro: `serialization = "hash"` and `hash = "manual"`
- **`serialization = "hash"`** — behaves like `serialization = "none"`
(no disk serialization, transient) but uses `VcCellHashedCompareMode` so
the stored hash prevents spurious downstream invalidation when transient
data is evicted and re-executed
- Valid only with `cell = "compare"` (the default); combining with `cell
= "new"` or `cell = "keyed"` is a compile error
- Automatically derives `DeterministicHash` on the annotated type
- **`hash = "manual"`** — opt-out of the auto-derive when a custom
`DeterministicHash` impl is needed (analogous to `eq = "manual"`); using
`hash = "manual"` without `serialization = "hash"` is a compile error
### turbo-tasks-fs: `PersistedFileContent`
- Adds `PersistedFileContent` — a mirror of `FileContent` that is
returned by `Vc<FileContent>::persist()`, storing the same data but
obtained through the persistent cache path
- `FileContent` is switched to `serialization = "hash"` so the macro
auto-derives `DeterministicHash`
- `DiskFileSystem::write()` now calls `.persist().await?` before
emitting the write effect, using `PersistedFileContent` for the file
comparison and write — this ensures full content is in the persistent
cache on restore, avoiding spurious downstream invalidation
- `WriteContent::File` in `invalidator_map.rs` is updated to hold
`ReadRef<PersistedFileContent>`
### turbopack-core: `Code` with hash-based serialization
- Adds `DeterministicHash` impls for `SmallVec<[T; N]>` and `()` in
`turbo-tasks-hash`
- Switches `Code` to `serialization = "hash"` so module factory code
cells use hash-based change detection
- Calls `.persist()` on module factory code cells in
`turbopack-ecmascript`
### next-core: duplicate asset detection and parallel emit
- `emit_assets` now emits node and client assets concurrently via
`try_join!`
- Detects duplicate assets (same output path, different content) and
returns a hard error with a diff summary — previously the last writer
silently won
## Macro syntax
```rust
#[turbo_tasks::value(serialization = "hash")]
struct MyTransientType { ... }
// opt out of auto-derived DeterministicHash when you need a custom impl:
#[turbo_tasks::value(serialization = "hash", hash = "manual")]
struct MyTransientType { ... }
```
## Test plan
- [x] Integration tests in
`turbopack/crates/turbo-tasks-backend/tests/hashed_cell_mode.rs`:
- `test_hashed_cell_mode_change_triggers_invalidation` — value change
triggers consumer re-execution
- `test_hashed_cell_mode_equal_value_no_invalidation` — same hash
prevents consumer re-execution
- [x] `cargo test -p turbo-tasks-macros-tests` passes
- [x] `cargo test -p turbo-tasks-backend --test hashed_cell_mode` passes
- [x] `cargo check --workspace` passes
---------
Co-authored-by: Tobias Koppers <sokra@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Vercel <vercel[bot]@users.noreply.github.com>