next.js
a0e452b9 - turbo-tasks: batch invalidation via Invalidator::invalidate_all (#100485)

Commit
2 days ago
turbo-tasks: batch invalidation via Invalidator::invalidate_all (#100485) ### What? Adds a batch invalidation API and uses it wherever many tasks are invalidated at once: - `Invalidator::invalidate_all`, `invalidate_all_with_reason` and `invalidate_all_with_reasons` invalidate a set of invalidators as one batch. - Duplicates are fine: task ids are deduplicated once. - Empty input does nothing, and records no reasons. - `TurboTasksApi::invalidate_many` / `invalidate_many_with_reasons` (implemented by `TurboTasks`) make one `Backend::invalidate_tasks` call, which previously had no in-tree callers. - Reasons are recorded once, before any work starts. - `TurboTasksBackend::invalidate_tasks` splits large batches into chunks and invalidates them in parallel, using the same sizing as `parallel::for_each_owned` (via the now-public `parallel::for_each_chunk`). Batched invalidations report the `Invalidator` dirty cause, like single ones. - Converted call sites: - File watcher: all invalidators of one event batch go in one call, with one `WatchChange` reason per path. There is now one `file change` span per batch instead of one per invalidator. Its `paths` field lists the changed paths, comma separated, capped at 20 with `, ... (N more paths)`. The call still happens under the existing invalidation lock. - `DiskFileSystem`: whole-filesystem, subtree and after-write invalidation. These replace the per-invalidator `parallel::for_each_owned` calls and per-invalidator reason clones. - `State::set`: all dependents in one call. - A new `state_fan_out` criterion bench in `turbo-tasks-backend/benches`. ### Why? Every multi-task producer called `Invalidator::invalidate` once per task. Each call went through `Backend::invalidate_task` → `invalidate(smallvec![id])`, which runs its own operation and scheduling. So none of these producers got the batched scheduling from #100446 (one scheduler queue lock per batch instead of per task), or the batched restores of paged-out tasks from #100465, which this PR builds on. ### How? - **Dirty priority across helper threads.** When an invalidation comes from inside a task (e.g. `State::set` in a task), the dirtied tasks get that task's priority as their parent priority, which the backend reads from the task-local task state. The parallel helper threads don't have it. So `invalidate_tasks` reads the priority once, before parallelizing, and gives every chunk an `ExecuteContext` with that priority (`execute_context_with_priority`; `ExecuteContext::get_current_task_priority` returns it instead of the task-local). Nothing else of the task state is needed, and other contexts are unchanged. - **Parallel work inside `with_turbo_tasks`.** `scope_bounded` wrapped every job, including jobs it runs on the calling thread, in `turbo_tasks_scope`, which panics while `with_turbo_tasks` holds a borrow of the instance. Jobs now only enter the scope when the thread doesn't already have that same instance. This lets `State::notify_mutated` keep using `with_turbo_tasks`. - **Unique ids at the backend.** `invalidate_many*` requires unique ids. The `Invalidator` helpers dedupe, because the watcher and the filesystem can yield the same task for several paths. Debug builds assert uniqueness for the whole batch before chunking and again in `invalidate` for each chunk; release builds pass the ids through without collecting them. ### Benchmark ``` cargo bench -p turbo-tasks-backend --bench mod -- state_fan_out ``` - **Setup:** one `State` read by N dependent tasks. Each iteration times `State::set` plus a strongly consistent read of the root sum, and asserts the result. - **Variants:** `resident`, and `evicted`, where everything is snapshotted and evicted before each iteration. That eviction step is not timed, and it asserts that at least N tasks were evicted. - **Comparison:** A/B against this PR's parent on `canary`, with the same bench copied in. Six paired runs on one 8-vCPU VM, alternating which side runs first. - **Revision:** measured on an earlier revision of this PR, before it was rebased onto #100465 (batched restores in `invalidate`), and while the chunking still lived in `TurboTasks`. The A/B wasn't re-run on the current revision, so the numbers don't include #100465's effect on either side. - **Statistics:** the change is the geometric mean of the per-pair ratios, with a 95% t-interval. | variant / N | median base | median this PR | change (95% CI) | |---|---|---|---| | resident / 1 | 0.284 ms | 0.288 ms | −7.6% [−32.0%, +25.5%] | | resident / 1,000 | 5.278 ms | 5.023 ms | −1.3% [−5.7%, +3.2%] | | resident / 10,000 | 61.045 ms | 58.029 ms | −4.7% [−8.3%, −1.0%] | | evicted / 1 | 0.258 ms | 0.265 ms | +3.3% [−0.2%, +7.0%] | | evicted / 1,000 | 8.295 ms | 6.760 ms | **−19.4%** [−21.6%, −17.2%] | | evicted / 10,000 | 91.967 ms | 69.775 ms | **−21.8%** [−25.1%, −18.4%] | - **Evicted:** a clear improvement when the dependents have to be restored, from invalidating the chunks in parallel and batching their scheduling (measured without #100465's batched restores). - **Resident:** a small win at 10k, and no reliable difference at 1k or for a single task. - **Limits:** the intervals reflect repeatability on one VM, not across machines. This is a `State` fan-out microbenchmark, not an end-to-end filesystem measurement. The OS page cache was warm. ### Tests - **`tests/invalidate_all.rs`:** - a 2,000-task batch, large enough to chunk, re-executes every task exactly once - duplicates in one batch - reasons show up in the update info - empty batches are no-ops - `State::set` inside a task with 2,000 dependents, and `parallel::for_each` inside `with_turbo_tasks` (both panic without the `scope_bounded` change) - a 1,999-task batch invalidated from inside a task leaves every task dirty with the same parent priority as one task invalidated on its own (fails without the explicit priority: tasks of the helper-thread chunks get `Initial`). Re-executions are held back so the dirty state can be inspected. - **Watcher:** the `paths` span formatting, including truncation. <!-- NEXT_JS_LLM --> <!-- fleet d21ddb86-de88-41df-82c7-aa6a29a6165e --> --------- 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>
Author
Parents
Loading