turbo-tasks-backend: parallelize decoding of large batch lookups (#100453)
### What?
`TurboBackingStorage::batch_lookup_data` now decodes the `TaskStorage`
entries of a batch in parallel when the batch has at least
`PARALLEL_DECODE_THRESHOLD` (2048) items. Smaller batches still decode
sequentially. This PR also adds an opt-in Criterion benchmark that
measures where parallel decoding starts to pay off.
### Why?
Batched task restores read all entries with a single `batch_get` and
then decoded every entry one after another on the calling thread. With
large restore batches, that single-threaded decode is a bottleneck.
### How?
- `batch_lookup_data` is now split into two steps:
- a raw fetch (`batch_lookup_raw`), unchanged I/O;
- `decode_task_storage_batch`, which takes the parallel cutoff as a
parameter.
- Both paths share the same decode closure, so ordering, `None` for
missing keys, and failing the batch on a decode error behave exactly as
before.
- The parallel path uses `turbo_tasks::parallel::map_collect`. It
borrows the raw bytes, keeps result order and the turbo-tasks context,
and doesn't add another thread pool.
- `turbo_tasks::scope_bounded` (behind `parallel::*`) now enters the
turbo-tasks context once per helper instead of once per job:
- #100485 fixed re-entering the context while the caller holds a borrow
(restores can run inside `with_turbo_tasks`). Its fix is
`ensure_turbo_tasks_scope`, which compares the current task-local on
every job.
- Now, matching `scope_unbounded`, each helper task enters the captured
context once around its drain loop. Jobs drained inline by the calling
thread run directly in the context that is already there. This drops the
per-job `Arc` clone and task-local check.
- `ensure_turbo_tasks_scope` is removed, since nothing else uses it.
- The benchmark internals are exposed through a `#[doc(hidden)] pub mod
private` in `turbo-tasks-backend`. No public method is added to
`TurboBackingStorage`.
- New benchmark `benches/batch_lookup.rs`, gated behind an env var like
the GC bench:
- Setup builds and persists a real task graph, then reads the raw
Meta/Data bytes once.
- It times only the decode call, on a Tokio worker thread, for 2–8192
items.
- It compares sequential, parallel, and the default threshold.
```bash
TURBOPACK_BENCH_BATCH_LOOKUP=1 cargo bench -p turbo-tasks-backend --bench mod -- batch_lookup
```
Results on an 8 vCPU machine (decode time, µs):
| Items | Meta seq | Meta par | Data seq | Data par |
|---:|---:|---:|---:|---:|
| 64 | 7.8 | 25.4 | 10.8 | 29.3 |
| 512 | 62.2 | 81.6 | 78.9 | 73.4 |
| 1024 | 123.9 | 136.4 | 159.2 | 118.5 |
| 2048 | 255.5 | 180.6 | 338.0 | 220.8 |
| 4096 | 524.2 | 331.9 | 711.1 | 401.0 |
| 8192 | 1169.1 | 626.9 | 1410.7 | 728.6 |
- Parallel decoding is 2–8× slower below roughly 256 items.
- Data breaks even around 512–1024 items, while Meta is still slower at
1024.
- At 2048 both win (1.42× for Meta, 1.53× for Data), so the shared
threshold is set conservatively at 2048.
These numbers come from a synthetic graph, so the crossover will vary
with core count, task contents, and how busy the runtime is. They
measure decode time only, not end-to-end build time.
### Tests
- New unit test: sequential and forced-parallel decoding produce
identical results for Meta and Data, including missing entries and empty
input. Both modes fail on malformed bytes.
- New round-trip test: calls `batch_lookup_data` against a real database
at 2047 and 2048 items, with reversed order, duplicate ids, and missing
ids.
- The existing regression test `parallel_inside_with_turbo_tasks` (from
#100485, in `turbo-tasks-backend/tests/invalidate_all.rs`) passes with
the per-helper context. So do the 7 `node_file_trace_persistent` cases
that panicked when large restore batches first started decoding in
parallel.
- Ran `cargo test -p turbo-tasks-backend --lib --tests`, `cargo test -p
turbo-tasks --lib`, the WASI lib tests (`--target wasm32-wasip1-threads`
with the Node WASI host), rustdoc with warnings denied, `cargo clippy -p
turbo-tasks -p turbo-tasks-backend --tests --benches -- -D warnings`,
and `cargo fmt --check`.
<!-- NEXT_JS_LLM -->
<!-- fleet 0b5d5738-9eb7-40fb-953e-c8e74f8635dc -->
---------
Co-authored-by: vercel-fleet-prod[bot] <318278635+vercel-fleet-prod[bot]@users.noreply.github.com>
Co-authored-by: Tobias Koppers <1365881+sokra@users.noreply.github.com>