Repository navigation
turbo-tasks-backend: track early aggregation edge removals as follower debt instead of retrying - #100873
Merged
Conversation
Contributor
Tests PassedCommit: 75538d5 |
Contributor
Reproduction resultsWe checked this PR against the stress reproduction that intermittently panicked ( Setup
Caveats
|
sokra
added a commit
that referenced
this pull request
Oct 29, 2026
### What?
When an aggregation update queue holds at least 4096 `jobs`, it now runs all
of them in parallel instead of one by one.
- Each chunk runs on a child execution context and pushes the work its jobs
generate into a private buffer queue. Buffers are never processed.
- After all chunks finish, every buffer is merged back into the queue:
- aggregation number updates go through the same done-map-aware merging as
regular pushes;
- balance and find-and-schedule work stays deduplicated;
- scheduled tasks keep their highest priority;
- optimizations respect the queue's cap and budget. Refused optimizations
set `optimization_pending`, as before.
- GC queues, fan-out buffers, and contexts with a GC collectible callback
never fan out.
Stacked on #100873.
### Why?
One queue runs on one thread, but a single update can cascade into a very
large number of jobs, e.g. when a task with many followers becomes an
aggregating node. This spreads such a cascade over the available threads.
This relies on #100873. There, an edge removal that is processed before its
addition records follower debt instead of retrying. So no job waits for other
work: every chunk, including edge removals, can complete on its own, as
`scope_bounded` requires. The generated work, including additions that pay
debt, is merged back into the queue and runs to completion with it.
This is a parallelization change. It is not a fix for the aggregation edge
removal panic or memory runaways; that is #100873.
### How?
- `JOB_FAN_OUT_THRESHOLD = 4096`, checked in `AggregationUpdateQueue::process`.
- `fan_out` runs the jobs via `turbo_tasks::parallel::map_collect_chunked_owned`.
- `ChildExecuteContext` is now `Copy`. Its priority is still resolved on the
calling thread.
- New: `ExecuteContext::has_gc_collectible`.
- Aggregation number merging moved into `push_aggregation_number_update`, so
`push` and the buffer merge share it.
- Tests cover:
- the threshold and the exclusions;
- every buffer merge rule;
- activation with a test-only threshold: a graph with interleaved edge
additions and removals (some removals overtake their additions), then an
invalidation. They check that fan-out ran and compare against sequential
processing, with 1 and 4 runtime workers.
### Verification
On top of #100873 `23eb92d7`:
- `cargo test -p turbo-tasks-backend` passes in default,
`verify_aggregation_graph` and `aggregation_update_no_batch` modes (382
tests each).
- clippy `--all-targets -D warnings` (default and
`verify_aggregation_graph`), `cargo fmt --check`, and a `cargo check` with
`trace_aggregation_update_stats,trace_aggregation_update_queue` pass.
- Locally forced threshold of 4 (not committed): the full suite passes in
default and `verify_aggregation_graph` modes, with about 28.7k / 28.9k
fan-outs, including about 27k in `aggregation_churn`.
- `turbo_tasks_backend_priority_contention/realistic_warm/128x512` (the
realistic contention bench from #100561, added locally, not part of this
PR; Criterion defaults; 32 CPUs), 6 interleaved runs on one boot, #100873
vs this PR:
| | Runs | ok | Median ms per run |
|---|---:|---:|---|
| #100873 | 3 | 3 | 40.69, 58.34, 58.13 |
| This PR | 3 | 3 | 55.25, 58.09, 57.40 |
No clear difference; this bench shows no measured speed-up.
<!-- NEXT_JS_LLM -->
Co-authored-by: Tobias Koppers <1365881+sokra@users.noreply.github.com>
This was referenced Oct 29, 2026
sokra
added a commit
that referenced
this pull request
Oct 30, 2026
… batches ### What? An aggregation update queue now runs large batches in parallel instead of one by one: - **Jobs:** when it holds at least 4096 `jobs`, all of them run in parallel. - **Edge removals:** when at least 4096 edge removals are due, all of them run in parallel. Removals are due when no job, aggregation number update or balancing is pending; this is the same rule #100892 uses to process removals one by one. How the parallel batch runs: - Each chunk runs on a child execution context and pushes the work its jobs generate into a private buffer queue. Buffers are never processed. - After all chunks finish, every buffer is merged back into the queue: - generated jobs are appended to `jobs`; - generated removals are appended to the deferred removals, FIFO; - aggregation number updates go through the same done-map-aware merging as regular pushes; - balance and find-and-schedule work stays deduplicated; - scheduled tasks keep their highest priority; - optimizations respect the queue's cap and budget. Refused optimizations set `optimization_pending`, as before. - GC queues, fan-out buffers and contexts with a GC collectible callback never fan out. Stacked on #100889 (on #100892 and #100873). ### Why? One queue runs on one thread, but a single update can cascade into a very large number of jobs, e.g. when a task with many followers becomes an aggregating node, or loses many followers. This spreads such a cascade over the available threads. Every job of a batch completes on its own, which is what `scope_bounded` requires. A removal that runs before its addition records follower debt (#100873) instead of waiting. Additions that pay that debt come back through the merge and run to completion with the queue. This changes one ordering rule from #100892, and only for removal batches at or above the threshold. Processed one by one, the work a removal generates runs before the next removal. In a parallel batch, it runs after the whole batch, with the normal priorities. A batch still only starts when no job, aggregation number update or balancing is pending, so removals still run after the additions queued before them. Below the threshold, nothing changes. This is a parallelization change. It is not a fix for the aggregation edge removal panic or memory runaways; that is #100873. ### How? - `JOB_FAN_OUT_THRESHOLD = 4096`. `AggregationUpdateQueue::fan_out_kind` picks `FanOutKind::Jobs`, `FanOutKind::Removals` or none. - `fan_out` takes all queued jobs of that kind and runs them via `turbo_tasks::parallel::map_collect_chunked_owned`. Jobs and removals share one implementation. - `ChildExecuteContext` is now `Copy`. Its priority is still resolved on the calling thread. - New: `ExecuteContext::has_gc_collectible`. - Aggregation number merging moved into `push_aggregation_number_update`, so `push` and the buffer merge share it. - The test hook type is now `Send + Sync`. Buffers never inherit hooks. - Tests cover: - the threshold and the exclusions, for both batch kinds; - that no removal runs, and no removal batch starts, while other work is pending; - every buffer merge rule, including removals; - work generated by a removal batch, with the final edges and collectible counts; - activation with a test-only threshold: additions, deferred removals and then an invalidation. They check that both batch kinds fanned out, and compare the graph and data against sequential processing, with 1 and 4 runtime workers. ### Verification On top of #100889 `e44ad7f0`: - `cargo test -p turbo-tasks-backend` passes in default, `verify_aggregation_graph` and `aggregation_update_no_batch` modes (400 tests each). This includes the deferred removal, follower debt and held dirty state tests from the PRs below, unchanged. - clippy `--all-targets -D warnings` (default and `verify_aggregation_graph`), `cargo fmt --check`, and a `cargo check` with `trace_aggregation_update_stats,trace_aggregation_update_queue` pass. - Locally forced threshold of 4 (not committed): the full suite passes in default and `verify_aggregation_graph` modes with no tests excluded, with about 24k jobs fan-outs and 10k removal fan-outs per mode. - `turbo_tasks_backend_priority_contention/realistic_warm/128x512` (the realistic contention bench from #100561, added locally, not part of this PR; Criterion defaults; 32 CPUs), 6 interleaved runs on one boot. All runs completed, with peak RSS around 0.26 GB: | | Runs | ok | Median ms per run | |---|---:|---:|---| | #100889 | 3 | 3 | 67.05, 67.05, 69.79 | | This PR | 3 | 3 | 67.81, 67.49, 66.97 | No clear difference; this bench shows no measured speed-up. <!-- NEXT_JS_LLM --> Co-authored-by: Tobias Koppers <1365881+sokra@users.noreply.github.com>
sokra
added a commit
that referenced
this pull request
Oct 30, 2026
… batches ### What? An aggregation update queue now runs large batches in parallel instead of one by one: - **Jobs:** when it holds at least 4096 `jobs`, all of them run in parallel. - **Edge removals:** when at least 4096 edge removals are due, all of them run in parallel. Removals are due when no job, aggregation number update or balancing is pending; this is the same rule #100892 uses to process removals one by one. How the parallel batch runs: - Each chunk runs on a child execution context and pushes the work its jobs generate into a private buffer queue. Buffers are never processed. - After all chunks finish, every buffer is merged back into the queue: - generated jobs are appended to `jobs`; - generated removals are appended to the deferred removals, FIFO; - aggregation number updates go through the same done-map-aware merging as regular pushes; - balance and find-and-schedule work stays deduplicated; - scheduled tasks keep their highest priority; - optimizations respect the queue's cap and budget. Refused optimizations set `optimization_pending`, as before. - GC queues, fan-out buffers and contexts with a GC collectible callback never fan out. Stacked on #100889 (on #100892 and #100873). ### Why? One queue runs on one thread, but a single update can cascade into a very large number of jobs, e.g. when a task with many followers becomes an aggregating node, or loses many followers. This spreads such a cascade over the available threads. Every job of a batch completes on its own, which is what `scope_bounded` requires. A removal that runs before its addition records follower debt (#100873) instead of waiting. Additions that pay that debt come back through the merge and run to completion with the queue. This changes one ordering rule from #100892, and only for removal batches at or above the threshold. Processed one by one, the work a removal generates runs before the next removal. In a parallel batch, it runs after the whole batch, with the normal priorities. A batch still only starts when no job, aggregation number update or balancing is pending, so removals still run after the additions queued before them. Below the threshold, nothing changes. This is a parallelization change. It is not a fix for the aggregation edge removal panic or memory runaways; that is #100873. ### How? - `JOB_FAN_OUT_THRESHOLD = 4096`. `AggregationUpdateQueue::fan_out_kind` picks `FanOutKind::Jobs`, `FanOutKind::Removals` or none. - `fan_out` takes all queued jobs of that kind and runs them via `turbo_tasks::parallel::map_collect_chunked_owned`. Jobs and removals share one implementation. - `ChildExecuteContext` is now `Copy`. Its priority is still resolved on the calling thread. - New: `ExecuteContext::has_gc_collectible`. - Aggregation number merging moved into `push_aggregation_number_update`, so `push` and the buffer merge share it. - The test hook type is now `Send + Sync`. Buffers never inherit hooks. - Tests cover: - the threshold and the exclusions, for both batch kinds; - that no removal runs, and no removal batch starts, while other work is pending; - every buffer merge rule, including removals; - work generated by a removal batch, with the final edges and collectible counts; - activation with a test-only threshold: additions, deferred removals and then an invalidation. They check that both batch kinds fanned out, and compare the graph and data against sequential processing, with 1 and 4 runtime workers. ### Verification On top of #100889 `e44ad7f0`: - `cargo test -p turbo-tasks-backend` passes in default, `verify_aggregation_graph` and `aggregation_update_no_batch` modes (400 tests each). This includes the deferred removal, follower debt and held dirty state tests from the PRs below, unchanged. - clippy `--all-targets -D warnings` (default and `verify_aggregation_graph`), `cargo fmt --check`, and a `cargo check` with `trace_aggregation_update_stats,trace_aggregation_update_queue` pass. - Locally forced threshold of 4 (not committed): the full suite passes in default and `verify_aggregation_graph` modes with no tests excluded, with about 24k jobs fan-outs and 10k removal fan-outs per mode. - `turbo_tasks_backend_priority_contention/realistic_warm/128x512` (the realistic contention bench from #100561, added locally, not part of this PR; Criterion defaults; 32 CPUs), 6 interleaved runs on one boot. All runs completed, with peak RSS around 0.26 GB: | | Runs | ok | Median ms per run | |---|---:|---:|---| | #100889 | 3 | 3 | 67.05, 67.05, 69.79 | | This PR | 3 | 3 | 67.81, 67.49, 66.97 | No clear difference; this bench shows no measured speed-up. <!-- NEXT_JS_LLM --> Co-authored-by: Tobias Koppers <1365881+sokra@users.noreply.github.com>
sokra
force-pushed
the
fleet/aggregation-negative-edges
branch
2 times, most recently
from
October 30, 2026 10:29
9dff90e to
c9d5d79
Compare
sokra
added a commit
that referenced
this pull request
Oct 30, 2026
… batches ### What? An aggregation update queue now runs large batches in parallel instead of one by one: - **Jobs:** when it holds at least 4096 `jobs`, all of them run in parallel. - **Edge removals:** when at least 4096 edge removals are due, all of them run in parallel. Removals are due when no job, aggregation number update or balancing is pending; this is the same rule #100892 uses to process removals one by one. How the parallel batch runs: - Each chunk runs on a child execution context and pushes the work its jobs generate into a private buffer queue. Buffers are never processed. - After all chunks finish, every buffer is merged back into the queue: - generated jobs are appended to `jobs`; - generated removals are appended to the deferred removals, FIFO; - aggregation number updates go through the same done-map-aware merging as regular pushes; - balance and find-and-schedule work stays deduplicated; - scheduled tasks keep their highest priority; - optimizations respect the queue's cap and budget. Refused optimizations set `optimization_pending`, as before. - GC queues, fan-out buffers and contexts with a GC collectible callback never fan out. Stacked on #100889 (on #100892 and #100873). ### Why? One queue runs on one thread, but a single update can cascade into a very large number of jobs, e.g. when a task with many followers becomes an aggregating node, or loses many followers. This spreads such a cascade over the available threads. Every job of a batch completes on its own, which is what `scope_bounded` requires. A removal that runs before its addition records follower debt (#100873) instead of waiting. Additions that pay that debt come back through the merge and run to completion with the queue. This changes one ordering rule from #100892, and only for removal batches at or above the threshold. Processed one by one, the work a removal generates runs before the next removal. In a parallel batch, it runs after the whole batch, with the normal priorities. A batch still only starts when no job, aggregation number update or balancing is pending, so removals still run after the additions queued before them. Below the threshold, nothing changes. This is a parallelization change. It is not a fix for the aggregation edge removal panic or memory runaways; that is #100873. ### How? - `JOB_FAN_OUT_THRESHOLD = 4096`. `AggregationUpdateQueue::fan_out_kind` picks `FanOutKind::Jobs`, `FanOutKind::Removals` or none. - `fan_out` takes all queued jobs of that kind and runs them via `turbo_tasks::parallel::map_collect_chunked_owned`. Jobs and removals share one implementation. - `ChildExecuteContext` is now `Copy`. Its priority is still resolved on the calling thread. - New: `ExecuteContext::has_gc_collectible`. - Aggregation number merging moved into `push_aggregation_number_update`, so `push` and the buffer merge share it. - The test hook type is now `Send + Sync`. Buffers never inherit hooks. - Tests cover: - the threshold and the exclusions, for both batch kinds; - that no removal runs, and no removal batch starts, while other work is pending; - every buffer merge rule, including removals; - work generated by a removal batch, with the final edges and collectible counts; - activation with a test-only threshold: additions, deferred removals and then an invalidation. They check that both batch kinds fanned out, and compare the graph and data against sequential processing, with 1 and 4 runtime workers. ### Verification On top of #100889 `e44ad7f0`: - `cargo test -p turbo-tasks-backend` passes in default, `verify_aggregation_graph` and `aggregation_update_no_batch` modes (400 tests each). This includes the deferred removal, follower debt and held dirty state tests from the PRs below, unchanged. - clippy `--all-targets -D warnings` (default and `verify_aggregation_graph`), `cargo fmt --check`, and a `cargo check` with `trace_aggregation_update_stats,trace_aggregation_update_queue` pass. - Locally forced threshold of 4 (not committed): the full suite passes in default and `verify_aggregation_graph` modes with no tests excluded, with about 24k jobs fan-outs and 10k removal fan-outs per mode. - `turbo_tasks_backend_priority_contention/realistic_warm/128x512` (the realistic contention bench from #100561, added locally, not part of this PR; Criterion defaults; 32 CPUs), 6 interleaved runs on one boot. All runs completed, with peak RSS around 0.26 GB: | | Runs | ok | Median ms per run | |---|---:|---:|---| | #100889 | 3 | 3 | 67.05, 67.05, 69.79 | | This PR | 3 | 3 | 67.81, 67.49, 66.97 | No clear difference; this bench shows no measured speed-up. <!-- NEXT_JS_LLM --> Co-authored-by: Tobias Koppers <1365881+sokra@users.noreply.github.com>
sokra
commented
Oct 30, 2026
sokra
commented
Oct 30, 2026
sokra
commented
Oct 30, 2026
sokra
commented
Oct 30, 2026
sokra
commented
Oct 30, 2026
sokra
commented
Oct 30, 2026
sokra
commented
Oct 30, 2026
sokra
commented
Oct 30, 2026
sokra
force-pushed
the
fleet/aggregation-negative-edges
branch
from
November 4, 2026 08:21
50e19c3 to
cc9ae70
Compare
sokra
added a commit
that referenced
this pull request
Nov 5, 2026
… batches ### What? An aggregation update queue now runs large batches in parallel instead of one by one: - **Jobs:** when it holds at least 4096 `jobs`, all of them run in parallel. - **Edge removals:** when at least 4096 edge removals are due, all of them run in parallel. Removals are due when no job, aggregation number update or balancing is pending; this is the same rule #100892 uses to process removals one by one. How the parallel batch runs: - Each chunk runs on a child execution context and pushes the work its jobs generate into a private buffer queue. Buffers are never processed. - After all chunks finish, every buffer is merged back into the queue: - generated jobs are appended to `jobs`; - generated removals are appended to the deferred removals, FIFO; - aggregation number updates go through the same done-map-aware merging as regular pushes; - balance and find-and-schedule work stays deduplicated; - scheduled tasks keep their highest priority; - optimizations respect the queue's cap and budget. Refused optimizations set `optimization_pending`, as before. - GC queues, fan-out buffers and contexts with a GC collectible callback never fan out. Stacked on #100889 (on #100892 and #100873). ### Why? One queue runs on one thread, but a single update can cascade into a very large number of jobs, e.g. when a task with many followers becomes an aggregating node, or loses many followers. This spreads such a cascade over the available threads. Every job of a batch completes on its own, which is what `scope_bounded` requires. A removal that runs before its addition records follower debt (#100873) instead of waiting. Additions that pay that debt come back through the merge and run to completion with the queue. This changes one ordering rule from #100892, and only for removal batches at or above the threshold. Processed one by one, the work a removal generates runs before the next removal. In a parallel batch, it runs after the whole batch, with the normal priorities. A batch still only starts when no job, aggregation number update or balancing is pending, so removals still run after the additions queued before them. Below the threshold, nothing changes. This is a parallelization change. It is not a fix for the aggregation edge removal panic or memory runaways; that is #100873. ### How? - `JOB_FAN_OUT_THRESHOLD = 4096`. `AggregationUpdateQueue::fan_out_kind` picks `FanOutKind::Jobs`, `FanOutKind::Removals` or none. - `fan_out` takes all queued jobs of that kind and runs them via `turbo_tasks::parallel::map_collect_chunked_owned`. Jobs and removals share one implementation. - `ChildExecuteContext` is now `Copy`. Its priority is still resolved on the calling thread. - New: `ExecuteContext::has_gc_collectible`. - Aggregation number merging moved into `push_aggregation_number_update`, so `push` and the buffer merge share it. - The test hook type is now `Send + Sync`. Buffers never inherit hooks. - Tests cover: - the threshold and the exclusions, for both batch kinds; - that no removal runs, and no removal batch starts, while other work is pending; - every buffer merge rule, including removals; - work generated by a removal batch, with the final edges and collectible counts; - activation with a test-only threshold: additions, deferred removals and then an invalidation. They check that both batch kinds fanned out, and compare the graph and data against sequential processing, with 1 and 4 runtime workers. ### Verification On top of #100889 `e44ad7f0`: - `cargo test -p turbo-tasks-backend` passes in default, `verify_aggregation_graph` and `aggregation_update_no_batch` modes (400 tests each). This includes the deferred removal, follower debt and held dirty state tests from the PRs below, unchanged. - clippy `--all-targets -D warnings` (default and `verify_aggregation_graph`), `cargo fmt --check`, and a `cargo check` with `trace_aggregation_update_stats,trace_aggregation_update_queue` pass. - Locally forced threshold of 4 (not committed): the full suite passes in default and `verify_aggregation_graph` modes with no tests excluded, with about 24k jobs fan-outs and 10k removal fan-outs per mode. - `turbo_tasks_backend_priority_contention/realistic_warm/128x512` (the realistic contention bench from #100561, added locally, not part of this PR; Criterion defaults; 32 CPUs), 6 interleaved runs on one boot. All runs completed, with peak RSS around 0.26 GB: | | Runs | ok | Median ms per run | |---|---:|---:|---| | #100889 | 3 | 3 | 67.05, 67.05, 69.79 | | This PR | 3 | 3 | 67.81, 67.49, 66.97 | No clear difference; this bench shows no measured speed-up. <!-- NEXT_JS_LLM --> Co-authored-by: Tobias Koppers <1365881+sokra@users.noreply.github.com>
Co-authored-by: Tobias Koppers <1365881+sokra@users.noreply.github.com>
Co-authored-by: Tobias Koppers <1365881+sokra@users.noreply.github.com>
Co-authored-by: Tobias Koppers <1365881+sokra@users.noreply.github.com>
Use plain i32 arithmetic in net_debt, avoid cloning follower ids when no uppers need them, share the removal steps between single and multiple edges via a TaskIdList trait (TaskIdVec or Option<TaskId>), check pending optimizations on the single upper, and rename the test hook type. Co-authored-by: Tobias Koppers <1365881+sokra@users.noreply.github.com>
Extract the addition steps of the three add handlers into helpers, like the removal steps: follower_add_side, add_follower_edge, add_inner_edge, on_inner_edges_appeared and on_inner_edges_appeared_on_upper. Handlers keep their lock order, debt checks, job variants and batching. Co-authored-by: Tobias Koppers <1365881+sokra@users.noreply.github.com>
Co-authored-by: Tobias Koppers <1365881+sokra@users.noreply.github.com>
Co-authored-by: Tobias Koppers <1365881+sokra@users.noreply.github.com>
sokra
force-pushed
the
fleet/aggregation-negative-edges
branch
from
November 6, 2026 15:30
cc9ae70 to
c2d937c
Compare
sokra
added a commit
that referenced
this pull request
Nov 6, 2026
… batches ### What? An aggregation update queue now runs large batches in parallel instead of one by one: - **Jobs:** when it holds at least 4096 `jobs`, all of them run in parallel. - **Edge removals:** when at least 4096 edge removals are due, all of them run in parallel. Removals are due when no job, aggregation number update or balancing is pending; this is the same rule #100892 uses to process removals one by one. How the parallel batch runs: - Each chunk runs on a child execution context and pushes the work its jobs generate into a private buffer queue. Buffers are never processed. - After all chunks finish, every buffer is merged back into the queue: - generated jobs are appended to `jobs`; - generated removals are appended to the deferred removals, FIFO; - aggregation number updates go through the same done-map-aware merging as regular pushes; - balance and find-and-schedule work stays deduplicated; - scheduled tasks keep their highest priority; - optimizations respect the queue's cap and budget. Refused optimizations set `optimization_pending`, as before. - GC queues, fan-out buffers and contexts with a GC collectible callback never fan out. Stacked on #100889 (on #100892 and #100873). ### Why? One queue runs on one thread, but a single update can cascade into a very large number of jobs, e.g. when a task with many followers becomes an aggregating node, or loses many followers. This spreads such a cascade over the available threads. Every job of a batch completes on its own, which is what `scope_bounded` requires. A removal that runs before its addition records follower debt (#100873) instead of waiting. Additions that pay that debt come back through the merge and run to completion with the queue. This changes one ordering rule from #100892, and only for removal batches at or above the threshold. Processed one by one, the work a removal generates runs before the next removal. In a parallel batch, it runs after the whole batch, with the normal priorities. A batch still only starts when no job, aggregation number update or balancing is pending, so removals still run after the additions queued before them. Below the threshold, nothing changes. This is a parallelization change. It is not a fix for the aggregation edge removal panic or memory runaways; that is #100873. ### How? - `JOB_FAN_OUT_THRESHOLD = 4096`. `AggregationUpdateQueue::fan_out_kind` picks `FanOutKind::Jobs`, `FanOutKind::Removals` or none. - `fan_out` takes all queued jobs of that kind and runs them via `turbo_tasks::parallel::map_collect_chunked_owned`. Jobs and removals share one implementation. - `ChildExecuteContext` is now `Copy`. Its priority is still resolved on the calling thread. - New: `ExecuteContext::has_gc_collectible`. - Aggregation number merging moved into `push_aggregation_number_update`, so `push` and the buffer merge share it. - The test hook type is now `Send + Sync`. Buffers never inherit hooks. - Tests cover: - the threshold and the exclusions, for both batch kinds; - that no removal runs, and no removal batch starts, while other work is pending; - every buffer merge rule, including removals; - work generated by a removal batch, with the final edges and collectible counts; - activation with a test-only threshold: additions, deferred removals and then an invalidation. They check that both batch kinds fanned out, and compare the graph and data against sequential processing, with 1 and 4 runtime workers. ### Verification On top of #100889 `e44ad7f0`: - `cargo test -p turbo-tasks-backend` passes in default, `verify_aggregation_graph` and `aggregation_update_no_batch` modes (400 tests each). This includes the deferred removal, follower debt and held dirty state tests from the PRs below, unchanged. - clippy `--all-targets -D warnings` (default and `verify_aggregation_graph`), `cargo fmt --check`, and a `cargo check` with `trace_aggregation_update_stats,trace_aggregation_update_queue` pass. - Locally forced threshold of 4 (not committed): the full suite passes in default and `verify_aggregation_graph` modes with no tests excluded, with about 24k jobs fan-outs and 10k removal fan-outs per mode. - `turbo_tasks_backend_priority_contention/realistic_warm/128x512` (the realistic contention bench from #100561, added locally, not part of this PR; Criterion defaults; 32 CPUs), 6 interleaved runs on one boot. All runs completed, with peak RSS around 0.26 GB: | | Runs | ok | Median ms per run | |---|---:|---:|---| | #100889 | 3 | 3 | 67.05, 67.05, 69.79 | | This PR | 3 | 3 | 67.81, 67.49, 66.97 | No clear difference; this bench shows no measured speed-up. <!-- NEXT_JS_LLM --> Co-authored-by: Tobias Koppers <1365881+sokra@users.noreply.github.com>
lukesandberg
approved these changes
Nov 8, 2026
Co-authored-by: Tobias Koppers <1365881+sokra@users.noreply.github.com>
…tics Co-authored-by: Tobias Koppers <1365881+sokra@users.noreply.github.com>
sokra
marked this pull request as ready for review
November 10, 2026 22:22
sokra
added a commit
that referenced
this pull request
Nov 10, 2026
…s and balancing (#100892) Stacked on #100873 (negative follower debt). Only the commits on top of #100873 belong to this PR. ### What? Within one `AggregationUpdateQueue`, aggregation edge removals (`InnerOf{Upper,Uppers}LostFollower(s)`) now wait in a separate removal queue until all ordinary jobs, aggregation number updates and balancing in that queue are done. ### Why? Previously additions and removals were interleaved in one FIFO. When an edge (or a task reachable through another edge) was removed and re-added in the same queue, its aggregated data went `1 → 0 → 1`: data was removed and re-added, collectibles dependents were invalidated, dirty containers flipped and readers could briefly miss collectibles. With removals last it goes `1 → 2 → 1`, so none of that temporary work happens. ### How? - `push` routes the three lost-follower jobs into `removal_jobs`; everything else (data updates, active/parent counts, invalidations, additions) keeps its queue and order. - `process` priority: `jobs` → aggregation number updates → balancing → **removals** → optimize → find dirty → schedule. A small `pop_job` helper picks a removal only when the higher-priority queues are empty, so work generated by a removal runs before the next removal, and generated removals append FIFO. The existing job dispatch is unchanged. - GC: `execute_until_rebalance` must drain removals while balancing stays deferred. It moves balance jobs out of the queue after every step and returns both previously pending and newly generated ones; the caller's deleted-endpoint filtering is unchanged. - Safety: a removal processed later is the ordinary case. A removal that still overtakes its addition (another queue, or an addition generated later) records follower debt as in #100873. The PR relies on the existing debt contract and doesn't weaken it. No new locking, no schema change. ### Scope / limitations - Queue-local only: helps when the keeping addition/balance is already pending in the same queue. No atomicity across separate queues or concurrent operations. - Task completion still removes old children (`cleanup_old_edges`) and connects new children (`connect_children`) in separate queues, so a task switching between children that share a descendant still churns. Combining those is planned as a separate, later stacked PR after measuring this one. ### Tests New `deferred_removal_tests` (real backend, step-by-step queue processing): - same-edge replacement and a moved shared descendant: the upper's collectible never drops to zero at any step, and a collectibles reader is not invalidated; - genuine loss still removes data and invalidates the reader; - removals only run when no ordinary job, aggregation number update or balance is pending; - work generated by a removal runs before the next removal, generated removals append FIFO; - `execute_until_rebalance` drains removals but executes no balancing and returns pending and newly generated balance edges. An existing debt test that mixed remove/remove/add in one queue now uses separate queues so it still forces a real overtake. Mutations: routing removals back into the ordinary FIFO fails 4 of the new tests; not collecting newly generated balances in the GC path fails the GC test. ### Verification - `cargo test -p turbo-tasks-backend` in default, `verify_aggregation_graph`, and `aggregation_update_no_batch`: 56/56 test groups each (including the existing stress tests). - `aggregation_churn` stress: 20/20 serial runs normal and 20/20 with graph verification. - `fuzz_graph`: 8,543 runs in 181 s, no crashes. - clippy (`--all-targets`, default and `verify_aggregation_graph,trace_aggregation_update_stats`), fuzz crate clippy, fmt, private rustdoc with `-D warnings`. ### Work counts and performance Exact counts (`replace_work`: 32 existing edges removed and re-added in one queue, a reader on the upper; feature `trace_aggregation_update_stats`), vs #100873: | | aggregated data update jobs | collectibles invalidation jobs | |---|---:|---:| | #100873 | 2 | 2 | | this PR | 0 | 0 | Queue microbench (release unit-test binary, 3 interleaved same-machine rounds, Criterion central estimates): | case | #100873 | this PR | |---|---|---| | replace (remove + re-add in one queue) | 12.7–12.9 µs | 8.5–10.2 µs (−20…−35%) | | overtake (removals in a separate earlier queue) | 10.2–10.4 µs | 11.0–12.3 µs (+6…+21%, noisy) | | ordered / ordered additions / ordered removals | — | within noise, removals possibly slightly slower | The microbench binary includes inert test-only hooks on this branch, so it does not isolate production overhead. End-to-end benches (`aggregation_churn`, `stress/fibonacci/500`) drifted too much across rounds to draw a conclusion either way. This is not claimed as a general speedup. ### Rebase onto removal benchmark/traversal follow-up Rebased the existing two logical commits onto #100873 at `50e19c34`, retaining the deferred-removal change and separate balance-queue drain refinement. The parent's permanent removal fixture/benchmarks and swap-order traversal are inherited, not additional child scope. Only the new parent's test observer needed adapting to inspect `removal_jobs` rather than the ordinary queue; production priority, GC balance collection and drain behavior are unchanged by the rebase. Local verification before the canary rebase: full default, `verify_aggregation_graph` and `aggregation_update_no_batch` suites each pass **56/56 groups** (210 unit tests plus one ignored benchmark). This includes the parent's 756-configuration fixture test and traversal/multiplicity/ancestor regressions, the actual-handler race tests, and this child's priority, genuine-loss, no-invalidation and GC-drain tests. Formatting, all-target clippy, private rustdoc with warnings denied and fuzz manifest check pass. The older stress/fuzz/performance numbers above are historical, not newly measured on this rebased child. Parent normal-library profiling explicitly reports a 32-edge benefit and a larger-map tradeoff; it is not evidence of an end-to-end child speedup. Post-push CI is green on `c26cd62d`, including native cargo units/benches, miri, WASI/wasm, Rust checks/rustdoc, and Windows tests; no reruns were needed. ### Canary restack (`e9607690`) Replayed the existing two child commits onto #100873 at `cc9ae702` (canary base `cc65c414`). Both child patches are unchanged by range-diff. Queue priority, GC balance collection and separate drain refinement are preserved. The final source tree exactly matches the locally verified integration tree. On this child, full default, `verify_aggregation_graph` and `aggregation_update_no_batch` suites each pass **59 test groups**, including **222 unit tests** (one ignored benchmark). Formatting, all-target clippy, private rustdoc with warnings denied and fuzz manifest checks pass. Prior stress/fuzz/performance results above remain historical; no new-base performance or end-to-end speedup is claimed. Post-push CI is green on `e9607690`, including cargo units/benches, miri, WASI/wasm, Rust checks/rustdoc and Windows tests; no reruns were needed. Base relationship and ready status are unchanged; higher layers were not modified. ### GC/no-resurrection canary restack (`d91b5264`) Restacked the same two child commits onto #100873 at `c2d937c1` (canary base `4a0609e1`, #100370). Both child patches remain unchanged by range-diff: removal priority, deferred GC balance collection and the separate drain refinement are preserved. Upstream no-resurrection behavior and deleted-endpoint filtering are retained. The final source exactly matches the verified integration tree. Latest local verification: full default, `verify_aggregation_graph` and `aggregation_update_no_batch` suites pass **59 groups each**, including **226 unit tests** (one ignored benchmark), updated GC reconnect/replacement and `task_by_type` tests, plus this child's GC/priority regressions. Formatting, all-target clippy, private rustdoc with warnings denied and fuzz manifest check pass. Initial default `recompute_dependency` hit an existing 16-bit random-tag collision: both functions logged re-execution and value correctly changed 1→2, but tags were equal. A targeted rerun and final full runs pass; a deterministic-oracle follow-up is recorded, without changing test or production behavior here. Older stress/fuzz/performance figures remain historical; no new-base speedup is claimed. Post-push CI is green on `d91b5264`, including cargo units/benches, miri, WASI/wasm, Rust checks/rustdoc and Windows tests; no CI reruns were needed. Base, ready status and higher stack layers are unchanged by this update. ### Restack onto reviewer follow-up (`fd4345e9`) Replayed only the existing two logical child commits onto #100873 at `75538d53`. The GC drain keeps its removal-job predicate, deferred balance collection and per-step `drain(..)` inside the parent's new fatal-panic boundary. Final source matches the verified child integration exactly. Queue priority, debt pre/postchecks, deleted-endpoint filtering, PR base/readiness and higher stack branches are unchanged. Local verification: default, `verify_aggregation_graph` and `aggregation_update_no_batch` full suites each pass **59 groups**, including **229 unit tests** (one ignored benchmark); stats library passes (two ignored benchmarks), including ordinary/stats/GC fatal-panic subprocess tests. Formatting, all-target clippy, private rustdoc with warnings denied and fuzz manifest checks pass. Opt-in queue benchmark smoke passes **10 routine + 756 extended cases**, and default omission is checked. Older timing/stress/fuzz figures above remain historical; no new speedup is claimed. Pushed-tip CI is green on `fd4345e9`, including cargo units/benches, miri, WASI/wasm, Rust checks/rustdoc and Windows tests. The first cargo-unit run had one `turbopack-go::live::versioned_dependencies_use_the_module_cache_without_downloads` failure with `Permission denied (os error 13)`; one rerun passed without source changes. That test's error-context/permission investigation is tracked separately. <!-- NEXT_JS_LLM --> <!-- fleet e31fbff5-6610-4d8e-9172-b2528e8725ad --> --------- 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>
sokra
added a commit
that referenced
this pull request
Nov 11, 2026
… batches ### What? An aggregation update queue now runs large batches in parallel instead of one by one: - **Jobs:** when it holds at least 4096 `jobs`, all of them run in parallel. - **Edge removals:** when at least 4096 edge removals are due, all of them run in parallel. Removals are due when no job, aggregation number update or balancing is pending; this is the same rule #100892 uses to process removals one by one. How the parallel batch runs: - Each chunk runs on a child execution context and pushes the work its jobs generate into a private buffer queue. Buffers are never processed. - After all chunks finish, every buffer is merged back into the queue: - generated jobs are appended to `jobs`; - generated removals are appended to the deferred removals, FIFO; - aggregation number updates go through the same done-map-aware merging as regular pushes; - balance and find-and-schedule work stays deduplicated; - scheduled tasks keep their highest priority; - optimizations respect the queue's cap and budget. Refused optimizations set `optimization_pending`, as before. - GC queues, fan-out buffers and contexts with a GC collectible callback never fan out. Stacked on #100889 (on #100892 and #100873). ### Why? One queue runs on one thread, but a single update can cascade into a very large number of jobs, e.g. when a task with many followers becomes an aggregating node, or loses many followers. This spreads such a cascade over the available threads. Every job of a batch completes on its own, which is what `scope_bounded` requires. A removal that runs before its addition records follower debt (#100873) instead of waiting. Additions that pay that debt come back through the merge and run to completion with the queue. This changes one ordering rule from #100892, and only for removal batches at or above the threshold. Processed one by one, the work a removal generates runs before the next removal. In a parallel batch, it runs after the whole batch, with the normal priorities. A batch still only starts when no job, aggregation number update or balancing is pending, so removals still run after the additions queued before them. Below the threshold, nothing changes. This is a parallelization change. It is not a fix for the aggregation edge removal panic or memory runaways; that is #100873. ### How? - `JOB_FAN_OUT_THRESHOLD = 4096`. `AggregationUpdateQueue::fan_out_kind` picks `FanOutKind::Jobs`, `FanOutKind::Removals` or none. - `fan_out` takes all queued jobs of that kind and runs them via `turbo_tasks::parallel::map_collect_chunked_owned`. Jobs and removals share one implementation. - `ChildExecuteContext` is now `Copy`. Its priority is still resolved on the calling thread. - New: `ExecuteContext::has_gc_collectible`. - Aggregation number merging moved into `push_aggregation_number_update`, so `push` and the buffer merge share it. - The test hook type is now `Send + Sync`. Buffers never inherit hooks. - Tests cover: - the threshold and the exclusions, for both batch kinds; - that no removal runs, and no removal batch starts, while other work is pending; - every buffer merge rule, including removals; - work generated by a removal batch, with the final edges and collectible counts; - activation with a test-only threshold: additions, deferred removals and then an invalidation. They check that both batch kinds fanned out, and compare the graph and data against sequential processing, with 1 and 4 runtime workers. ### Verification On top of #100889 `e44ad7f0`: - `cargo test -p turbo-tasks-backend` passes in default, `verify_aggregation_graph` and `aggregation_update_no_batch` modes (400 tests each). This includes the deferred removal, follower debt and held dirty state tests from the PRs below, unchanged. - clippy `--all-targets -D warnings` (default and `verify_aggregation_graph`), `cargo fmt --check`, and a `cargo check` with `trace_aggregation_update_stats,trace_aggregation_update_queue` pass. - Locally forced threshold of 4 (not committed): the full suite passes in default and `verify_aggregation_graph` modes with no tests excluded, with about 24k jobs fan-outs and 10k removal fan-outs per mode. - `turbo_tasks_backend_priority_contention/realistic_warm/128x512` (the realistic contention bench from #100561, added locally, not part of this PR; Criterion defaults; 32 CPUs), 6 interleaved runs on one boot. All runs completed, with peak RSS around 0.26 GB: | | Runs | ok | Median ms per run | |---|---:|---:|---| | #100889 | 3 | 3 | 67.05, 67.05, 69.79 | | This PR | 3 | 3 | 67.81, 67.49, 66.97 | No clear difference; this bench shows no measured speed-up. <!-- NEXT_JS_LLM --> Co-authored-by: Tobias Koppers <1365881+sokra@users.noreply.github.com>
sokra
added a commit
that referenced
this pull request
Nov 13, 2026
… batches ### What? An aggregation update queue now runs large batches in parallel instead of one by one: - **Jobs:** when it holds at least 4096 `jobs`, all of them run in parallel. - **Edge removals:** when at least 4096 edge removals are due, all of them run in parallel. Removals are due when no job, aggregation number update or balancing is pending; this is the same rule #100892 uses to process removals one by one. How the parallel batch runs: - Each chunk runs on a child execution context and pushes the work its jobs generate into a private buffer queue. Buffers are never processed. - After all chunks finish, every buffer is merged back into the queue: - generated jobs are appended to `jobs`; - generated removals are appended to the deferred removals, FIFO; - aggregation number updates go through the same done-map-aware merging as regular pushes; - balance and find-and-schedule work stays deduplicated; - scheduled tasks keep their highest priority; - optimizations respect the queue's cap and budget. Refused optimizations set `optimization_pending`, as before. - GC queues, fan-out buffers and contexts with a GC collectible callback never fan out. Stacked on #100889 (on #100892 and #100873). ### Why? One queue runs on one thread, but a single update can cascade into a very large number of jobs, e.g. when a task with many followers becomes an aggregating node, or loses many followers. This spreads such a cascade over the available threads. Every job of a batch completes on its own, which is what `scope_bounded` requires. A removal that runs before its addition records follower debt (#100873) instead of waiting. Additions that pay that debt come back through the merge and run to completion with the queue. This changes one ordering rule from #100892, and only for removal batches at or above the threshold. Processed one by one, the work a removal generates runs before the next removal. In a parallel batch, it runs after the whole batch, with the normal priorities. A batch still only starts when no job, aggregation number update or balancing is pending, so removals still run after the additions queued before them. Below the threshold, nothing changes. This is a parallelization change. It is not a fix for the aggregation edge removal panic or memory runaways; that is #100873. ### How? - `JOB_FAN_OUT_THRESHOLD = 4096`. `AggregationUpdateQueue::fan_out_kind` picks `FanOutKind::Jobs`, `FanOutKind::Removals` or none. - `fan_out` takes all queued jobs of that kind and runs them via `turbo_tasks::parallel::map_collect_chunked_owned`. Jobs and removals share one implementation. - `ChildExecuteContext` is now `Copy`. Its priority is still resolved on the calling thread. - New: `ExecuteContext::has_gc_collectible`. - Aggregation number merging moved into `push_aggregation_number_update`, so `push` and the buffer merge share it. - The test hook type is now `Send + Sync`. Buffers never inherit hooks. - Tests cover: - the threshold and the exclusions, for both batch kinds; - that no removal runs, and no removal batch starts, while other work is pending; - every buffer merge rule, including removals; - work generated by a removal batch, with the final edges and collectible counts; - activation with a test-only threshold: additions, deferred removals and then an invalidation. They check that both batch kinds fanned out, and compare the graph and data against sequential processing, with 1 and 4 runtime workers. ### Verification On top of #100889 `e44ad7f0`: - `cargo test -p turbo-tasks-backend` passes in default, `verify_aggregation_graph` and `aggregation_update_no_batch` modes (400 tests each). This includes the deferred removal, follower debt and held dirty state tests from the PRs below, unchanged. - clippy `--all-targets -D warnings` (default and `verify_aggregation_graph`), `cargo fmt --check`, and a `cargo check` with `trace_aggregation_update_stats,trace_aggregation_update_queue` pass. - Locally forced threshold of 4 (not committed): the full suite passes in default and `verify_aggregation_graph` modes with no tests excluded, with about 24k jobs fan-outs and 10k removal fan-outs per mode. - `turbo_tasks_backend_priority_contention/realistic_warm/128x512` (the realistic contention bench from #100561, added locally, not part of this PR; Criterion defaults; 32 CPUs), 6 interleaved runs on one boot. All runs completed, with peak RSS around 0.26 GB: | | Runs | ok | Median ms per run | |---|---:|---:|---| | #100889 | 3 | 3 | 67.05, 67.05, 69.79 | | This PR | 3 | 3 | 67.81, 67.49, 66.97 | No clear difference; this bench shows no measured speed-up. <!-- NEXT_JS_LLM --> Co-authored-by: Tobias Koppers <1365881+sokra@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What?
Replace aggregation-edge removal retries with negative follower counts ("debt"). A removal that overtakes its addition records debt; the late addition cancels it instead of spinning, requeuing, or eventually panicking. Remove
retry_loop, the retry job fields, andMAX_RETRIES.Also add deterministic regression tests, concurrent graph-churn stress tests, fuzz-driver improvements, and benchmarks.
How?
An edge
U → Flives either inF.upper[U](unsigned) or inU.followers[F](nowi32). Only followers can go negative. Only counts greater than zero are real edges.BalanceEdgeif an inner edge appeared meanwhile.<= 0↔> 0crossings. Debt creation/payment alone has no semantic edge effect.aggregation_edge.rscontains pure checked arithmetic, placement, and netting. In the queue, the handlers for one or several uppers or followers share step helpers: removalremove_inner_edges→on_inner_edges_disappearedandremove_follower_edges; additionfollower_add_side/add_follower_edgeandadd_inner_edge→on_inner_edges_appeared(on_inner_edges_appeared_on_upperfor the multi-follower handler). Their id lists use a smallTaskIdListtrait (TaskIdVecorOption<TaskId>), so single-edge handlers build no vectors.Review follow-up
A separate-model correctness review identified four issues, addressed by the follow-up:
Additional focused tests seed dirty/current-session-clean state and nonzero active counters: pure debt has no propagation effects; overpayment creates one appearance per edge; repeated positive references do not duplicate effects; split cancellation reverses dirty and clean contributions once; leaf promotion preserves data, active counts, and ancestor state while renesting dirty containers.
Review order: pure helpers → queue module docs and handlers → snapshot/verifier → deterministic tests → churn/fuzz driver → benchmarks.
Maintainer review round (0f405c3, 7b61001)
0f405c3e: plaini32arithmetic innet_debt; nofollower_idsclone when there are no uppers; removal steps shared between single and multiple edges viaTaskIdList;check_optimization_pendingon the single upper; test hook type renamed toTestHook.7b61001b: the three add handlers share step helpers like the removal path. Lock order, debt pre-check and postcheck, job variants, batching and optimize heuristics are unchanged.verify_aggregation_graphandaggregation_update_no_batch; churn 10/10 normal + 10/10 verify (serial); fuzz 10,449 runs / 181 s without crashes; clippy and rustdoc-D warnings. Removing either shared postcheck, or the debt pre-check infollower_add_side, fails the race/debt tests. CI green on7b61001b.c9d5d79f(3 interleaved rounds, median): ordered −3%, ordered removals −10%, overtake −6%, ordered additions +4–5% (~6 ns per single-edge addition, attributed to the add-path extraction; not the addedcheck_optimization_pendingand not recovered by inline hints).Verification
Follow-up local results:
Full crate suites in default,
verify_aggregation_graph, andaggregation_update_no_batch: 56/56 test groups pass in each mode, including 198 unit tests (one ignored benchmark).aggregation_churn: 20/20 serial runs in normal mode and 20/20 with graph verification.The known WASI failure on the original tip was deterministic (
catch_unwindon panic-abort), not a flaky job. Target guards are fixed; post-push WASI CI passes one82c325d(job123330618672). The full build-and-test suite is green, including native units, miri, Rust checks, benches, and Windows tests.Four mutation probes: each of the three actual add-handler postcheck callsites, plus the verifier's edgeless-endpoint omission, is independently detected by the new tests; all mutations restored.
Follow-up fuzz rerun: 6,929 runs in 181 seconds, no crashes, with graph verification enabled (no sanitizer).
Follow-up formatting, clippy (backend all targets, default and graph-verification modes; fuzz crate), and private rustdoc with
-D warnings: pass.Historical pre-follow-up evidence (not newly measured coverage/performance):
verify_aggregation_graph, andaggregation_update_no_batchmodes passed.Benchmarks
Historical same-machine A/B against canary, three interleaved rounds, criterion medians:
scope_stressstress/fibonacci/500This is a tradeoff, not a blanket performance win. The single-threaded microbench does not measure the old multi-threaded spin-wait. Part of the Fibonacci cost comes from the extra upper lock required by the race argument; reducing that cost without weakening correctness is a separate follow-up. No new performance claim is made for the review follow-up.
Limitations / status
verify_aggregation_graphstop-time check reports residual debt (on restored resident tasks).Removal benchmark and traversal follow-up
Two separate commits add permanent coverage and then the narrow traversal change:
private::aggregation_queueis an explicitly unstable native fixture shared by benches and normal-library profiling. The opt-inTURBOPACK_BENCH_AGGREGATION_QUEUE=1Criterion group runs ten routine cases;extendedexposes 756 filterable configurations: inner/follower/mixed placement, true single-edge/one-ID batch/full-batch jobs, references 1→0 or 2→1, data/no-data, aligned/reversed/seeded-permuted IDs, and total sizes 1/8/16/32/33/64/66. Setup/restoration/checks are untimed. Raw edge counts and aggregate contributions are checked after every phase. Aggregation-number optimizations are deliberately disabled: this is isolated queue work, not an end-to-end workload.remove_follower_edgesconsumes its input with existingTaskIdList::retainswap-removal semantics. For aligned small linear maps, visiting the swapped replacement avoids repeated scans. Both result lists, edge multiplicity, positive-crossing effects, lock order, debt prechecks and postchecks remain intact. Emitted batch order intentionally changes; new real-handler tests cover Option/32/33 inputs, ancestor propagation, retained references, duplicate IDs and mixed positive/debt entries.Normal-dependency profiling (not a backend test build) uses the same frozen fixture, system allocator, one runtime/backend worker, no backing storage, and no dev-only inline statistics. Release settings are explicit (opt3/thin LTO/cgu1 plus the applicable regex-syntax size override). Callgrind/DHAT cover 18 configurations; Criterion uses 30 samples, 1 s warmup, 3 s measurement and three interleaved parent/patch rounds. Payload/context/queue construction and removal drain are timed; restoration/assertions are not. Guest instructions and simulated events are not hardware cycles.
The 64-edge case had non-overlapping slower 95% intervals in all three rounds. Its extra confirmation pair was 3454.881 → 3569.930 ns (+3.33%), still with non-overlapping intervals: the ≥5% magnitude did not repeat, but a smaller slowdown persists. This tradeoff is retained and disclosed, not described as regression-free. Full33 is +4.78% elapsed/+5.99% instructions; mixed one-ID32 is +4.25% elapsed. Mixed/permuted/retained cases can do more instruction work. Allocation totals were equal across the three revisions in all 18 sampled cases. Remaining normal-library helper/guard/accessor overhead relative to canary is not fully diagnosed; symbol self costs do not prove extra semantic locks. No blanket speedup claim.
Local verification before the canary rebase: all three full feature modes pass 56/56 groups (204 unit tests plus one ignored benchmark); 10/10 normal + 10/10 graph-verified serial churn; fuzz seed 2019458061 completed 14,271 runs in 181 s without failure; both shared addition-postcheck mutations fail the corresponding real-handler race tests and were restored. Formatting, all-target clippy, private rustdoc (warnings denied), fuzz manifest checks, 756-case extended smoke, ten routine cases, disabled gating and the original ignored benchmark all pass. Separate-model correctness and profiling-report reviews found no remaining blocker; these are not human approval. Post-push CI is green on
50e19c34, including native cargo units/benches, miri, WASI/wasm, Rust checks/rustdoc, and Windows tests; no reruns were needed.Canary rebase (
cc9ae702)Rebased the seven existing commits onto canary
cc65c414. The only manual integration resolvesTurboTasksBackend::stop: keep canary'sActivityMonitorshutdown handling, then run both graph and aggregation-edge verification before dropping storage. All debt, race, GC, helper and traversal behavior is unchanged; the benchmark and traversal commits remain separate. Range-diff shows only this stop-hook adaptation.Both rebased layers were checked against exact verified source trees. On this parent, full default,
verify_aggregation_graphandaggregation_update_no_batchsuites each pass 59 test groups, including 216 unit tests (one ignored benchmark). Formatting, all-target clippy, private rustdoc with warnings denied and fuzz manifest checks pass. The historical stress/fuzz/mutation and benchmark results above were not rerun on the new canary base, and their performance percentages are not new-base measurements. Post-push CI is green oncc9ae702, including cargo units/benches, miri, WASI/wasm, Rust checks/rustdoc and Windows tests; no reruns were needed.GC/no-resurrection canary rebase (
c2d937c1)Rebased the same seven commits onto canary
4a0609e1(#100370). The only manual resolution removes the obsoleteresurrect_deletedimport while keeping aggregation-edge helpers. Upstream's live-task assertions, deleted-candidate filtering, GC task-cache removal and optimization changes remain intact. The previousActivityMonitorstop-verification integration is retained. No debt, race, balancing or traversal logic was weakened; final source exactly matches the verified integration tree.Latest local verification: full default,
verify_aggregation_graphandaggregation_update_no_batchsuites pass 59 groups each, including 220 unit tests (one ignored benchmark), the updated GC reconnect/replacement tests and all fourtask_by_typetests. Formatting, all-target clippy, private rustdoc with warnings denied and fuzz manifest check pass. On the child an existing 16-bit random-tag re-execution oracle collided once despite correct 1→2 propagation and logged re-execution; targeted and final full runs passed, with a deterministic-oracle follow-up recorded. No test assertions were removed. Older performance, fuzz and stress numbers above are historical, not measurements on this base. Post-push CI is green onc2d937c1, including cargo units/benches, miri, WASI/wasm, Rust checks/rustdoc and Windows tests; no CI reruns were needed.Reviewer follow-up (
614290c4,75538d53)filter_map; clarified the outgoing-diff merge TODO without implementing merging.Local checks on both layers: three full feature modes pass 59 groups each (parent 223 units, child 229, one ignored benchmark); stats libraries, fmt, all-target clippy, private rustdoc with warnings denied, and fuzz manifest checks pass. Both layers pass opt-in benchmark smoke: 10 routine + 756 extended cases, with default omission verified. These are correctness smoke tests, not fresh performance measurements. The historical timings/stress/fuzz results above were not rerun for this update. Pushed-tip CI is green on
75538d53, including cargo units/benches, miri, WASI/wasm, Rust checks/rustdoc and Windows tests; this parent's CI needed no reruns. Review threads remain unresolved for the reviewer.