[turbopack] Simplify local task tracking (#93478)
### What?
Replace the `FuturesUnordered<Either<JoinHandle, ...>>` that tracks a
global task's in-flight local tasks with a small in-place
counter+`Event` bundled into a new `LocalTaskTracker` that lives
directly on `CurrentTaskState`.
### Why?
`wait_for_local_tasks` only needs a barrier — "have all in-flight local
tasks completed?" — not a stream of outputs. `FuturesUnordered` was
overkill for that, and its supporting machinery cost an allocation per
parent task plus an allocation per local task, plus repeated lock
acquisitions per registration and per completion.
Per parent task execution, this PR removes:
- **One `FuturesUnordered` allocation.** The lazy
`Option<FuturesUnordered<…>>` is gone; the counter+event is inline on
`CurrentTaskState`.
- **One `tokio::sync::oneshot::channel()` allocation per local task.**
`priority_runner::schedule_with_join_handle` (and `JoinHandle`) is
deleted; local tasks now use plain `schedule`.
- **One intrusive `FuturesUnordered::Task` node allocation per local
task.**
- **One `RwLock<CurrentTaskState>` write-lock acquisition per local task
spawn.** The pre-existing `create_local_task` write lock now also bumps
the in-flight counter, so we no longer take a separate lock to push into
the tracker.
- **One `RwLock<CurrentTaskState>` write-lock acquisition per local task
completion.** The pre-existing `Scheduled → Done` write lock now also
decrements the counter, notifies per-task waiters, and (if the count hit
zero) notifies the collective wait event — all under one lock.
Net effect on hot paths:
- **Spawn**: was 2 allocations + 2 locks; now 0 allocations + 1 lock
(the existing `create_local_task` lock).
- **Wait when nothing was spawned**: a single read-lock that finds
`in_flight == 0` and returns. No allocation, no listener, no await.
### How?
- New `LocalTaskTracker`
([`turbopack/crates/turbo-tasks/src/local_task_tracker.rs`](turbopack/crates/turbo-tasks/src/local_task_tracker.rs))
bundles:
- `tasks: Vec<LocalTask>` (was a separate field on `CurrentTaskState`),
- `in_flight: u32` (plain integer; the surrounding `RwLock` provides
synchronization),
- `done: Event` (notified on transitions to zero).
- `CurrentTaskState::local_tasks` is now `LocalTaskTracker` instead of
`Vec<LocalTask>`. The wait-group is part of the same struct, so a parent
task that doesn't spawn any local tasks pays nothing beyond the inline
fields.
- `LocalTaskTracker::create` pushes a new `Scheduled` entry and
increments the counter.
- `LocalTaskTracker::complete` does the `Scheduled → Done` swap, fires
the per-task `done_event` (waking `try_read_local_output` waiters),
decrements the counter, and notifies the collective `done` event if the
count hit zero — all while holding one write lock.
- `wait_for_local_tasks` is the standard double-check pattern (snapshot
the counter, register a listener, re-check, await) against the tracker.
No loop after the await — the codebase contract is that no new local
tasks (or detached test futures) can be registered after the parent's
user body returns, except from inside an already-in-flight scope, so a
single notify is sufficient.
- `spawn_detached_for_testing` uses a tiny `register_detached` /
`dec_in_flight` pair (test path; no RAII guard, panics abort upstream).
- `priority_runner::schedule_with_join_handle`, `JoinHandle`, the
`Option<Sender<()>>` field on `HeapItem`, and the matching `tx.send(())`
in `WorkerFuture::poll` are all deleted; nothing else used them.
Unit tests for the tracker cover balanced create/complete, balanced
detached register/dec, and that `complete` wakes per-task listeners. The
existing detached integration tests (including the nested
`spawn_detached_for_testing` case in `detached.rs`) still pass.
<!-- NEXT_JS_LLM_PR -->