From d7a327bac404fc68ad579b7999304ed5ef5ca108 Mon Sep 17 00:00:00 2001 From: Tobias Koppers Date: Tue, 2 Dec 2025 14:50:44 +0100 Subject: [PATCH 1/3] Turbopack: parallelize making dependent tasks dirty --- .../turbo-tasks-backend/src/backend/mod.rs | 55 +++++++++++++++---- turbopack/crates/turbo-tasks/src/util.rs | 18 ++++++ 2 files changed, 62 insertions(+), 11 deletions(-) diff --git a/turbopack/crates/turbo-tasks-backend/src/backend/mod.rs b/turbopack/crates/turbo-tasks-backend/src/backend/mod.rs index 765df76264fd6..21abe711cab82 100644 --- a/turbopack/crates/turbo-tasks-backend/src/backend/mod.rs +++ b/turbopack/crates/turbo-tasks-backend/src/backend/mod.rs @@ -36,10 +36,11 @@ use turbo_tasks::{ event::{Event, EventListener}, message_queue::TimingEvent, registry::get_value_type, + scope::scope_and_block, task_statistics::TaskStatisticsApi, trace::TraceRawVcs, turbo_tasks, - util::{IdFactoryWithReuse, good_chunk_size}, + util::{IdFactoryWithReuse, good_chunk_size, into_chunks}, }; pub use self::{operation::AnyOperation, storage::TaskDataCategory}; @@ -48,10 +49,11 @@ use crate::backend::operation::TaskDirtyCause; use crate::{ backend::{ operation::{ - AggregationUpdateJob, AggregationUpdateQueue, CleanupOldEdgesOperation, - ComputeDirtyAndCleanUpdate, ConnectChildOperation, ExecuteContext, ExecuteContextImpl, - Operation, OutdatedEdge, TaskGuard, connect_children, get_aggregation_number, - get_uppers, is_root_node, make_task_dirty_internal, prepare_new_children, + AggregationUpdateJob, AggregationUpdateQueue, ChildExecuteContext, + CleanupOldEdgesOperation, ComputeDirtyAndCleanUpdate, ConnectChildOperation, + ExecuteContext, ExecuteContextImpl, Operation, OutdatedEdge, TaskGuard, + connect_children, get_aggregation_number, get_uppers, is_root_node, + make_task_dirty_internal, prepare_new_children, }, storage::{ InnerStorageSnapshot, Storage, count, get, get_many, get_mut, get_mut_or_insert_with, @@ -2160,8 +2162,12 @@ impl TurboTasksBackendInner { ) { debug_assert!(!output_dependent_tasks.is_empty()); - let mut queue = AggregationUpdateQueue::new(); - for dependent_task_id in output_dependent_tasks { + fn process_output_dependents( + ctx: &mut impl ExecuteContext<'_>, + task_id: TaskId, + dependent_task_id: TaskId, + queue: &mut AggregationUpdateQueue, + ) { #[cfg(feature = "trace_task_output_dependencies")] let span = tracing::trace_span!( "invalidate output dependency", @@ -2174,7 +2180,7 @@ impl TurboTasksBackendInner { // once tasks are never invalidated #[cfg(feature = "trace_task_output_dependencies")] span.record("result", "once task"); - continue; + return; } let mut make_stale = true; let dependent = ctx.task(dependent_task_id, TaskDataCategory::All); @@ -2191,7 +2197,7 @@ impl TurboTasksBackendInner { // output anymore and doesn't need to be invalidated #[cfg(feature = "trace_task_output_dependencies")] span.record("result", "no backward dependency"); - continue; + return; } make_task_dirty_internal( dependent, @@ -2199,14 +2205,41 @@ impl TurboTasksBackendInner { make_stale, #[cfg(feature = "trace_task_dirty")] TaskDirtyCause::OutputChange { task_id }, - &mut queue, + queue, ctx, ); #[cfg(feature = "trace_task_output_dependencies")] span.record("result", "marked dirty"); } - queue.execute(ctx); + if output_dependent_tasks.len() > 128 { + let chunk_size = good_chunk_size(output_dependent_tasks.len()); + let chunks = into_chunks(output_dependent_tasks.to_vec(), chunk_size); + let _ = scope_and_block(chunks.len(), |scope| { + for chunk in chunks { + let child_ctx = ctx.child_context(); + scope.spawn(move || { + let mut ctx = child_ctx.create(); + let mut queue = AggregationUpdateQueue::new(); + for dependent_task_id in chunk { + process_output_dependents( + &mut ctx, + task_id, + dependent_task_id, + &mut queue, + ) + } + queue.execute(&mut ctx); + }); + } + }); + } else { + let mut queue = AggregationUpdateQueue::new(); + for dependent_task_id in output_dependent_tasks { + process_output_dependents(ctx, task_id, dependent_task_id, &mut queue); + } + queue.execute(ctx); + } } fn task_execution_completed_unfinished_children_dirty( diff --git a/turbopack/crates/turbo-tasks/src/util.rs b/turbopack/crates/turbo-tasks/src/util.rs index 260a8797f8e7e..b2d87723588a9 100644 --- a/turbopack/crates/turbo-tasks/src/util.rs +++ b/turbopack/crates/turbo-tasks/src/util.rs @@ -330,6 +330,12 @@ pub struct IntoChunks { chunk_size: usize, } +impl IntoChunks { + pub fn len(&self) -> usize { + (self.data.len() - self.index + self.chunk_size - 1) / self.chunk_size + } +} + impl Iterator for IntoChunks { type Item = Chunk; @@ -414,6 +420,18 @@ impl Drop for Chunk { mod tests { use super::*; + #[test] + fn test_into_chunks_len() { + assert_eq!(into_chunks::(vec![], 2).len(), 0); + assert_eq!(into_chunks(vec![1], 2).len(), 1); + assert_eq!(into_chunks(vec![1, 2], 2).len(), 1); + assert_eq!(into_chunks(vec![1, 2, 3], 2).len(), 2); + assert_eq!(into_chunks(vec![1, 2, 3, 4], 2).len(), 2); + assert_eq!(into_chunks(vec![1, 2, 3, 4, 5], 2).len(), 3); + assert_eq!(into_chunks(vec![1, 2, 3, 4, 5, 6], 2).len(), 3); + assert_eq!(into_chunks(vec![1, 2, 3, 4, 5, 6, 7], 2).len(), 4); + } + #[test] fn test_chunk_iterator() { let data = [(); 10] From ec4fd3d8572c0c0788932c8ddc64c4ae691735ef Mon Sep 17 00:00:00 2001 From: Tobias Koppers Date: Thu, 4 Dec 2025 08:52:39 +0100 Subject: [PATCH 2/3] clippy --- turbopack/crates/turbo-tasks/src/util.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/turbopack/crates/turbo-tasks/src/util.rs b/turbopack/crates/turbo-tasks/src/util.rs index b2d87723588a9..7cf5645bb4dc5 100644 --- a/turbopack/crates/turbo-tasks/src/util.rs +++ b/turbopack/crates/turbo-tasks/src/util.rs @@ -332,7 +332,11 @@ pub struct IntoChunks { impl IntoChunks { pub fn len(&self) -> usize { - (self.data.len() - self.index + self.chunk_size - 1) / self.chunk_size + (self.data.len() - self.index).div_ceil(self.chunk_size) + } + + pub fn is_empty(&self) -> bool { + self.index >= self.data.len() } } From cb3e78e16e4e69f1a2a522a256e512ec5f2d0749 Mon Sep 17 00:00:00 2001 From: Tobias Koppers Date: Tue, 9 Dec 2025 14:23:11 +0100 Subject: [PATCH 3/3] use constants --- .../crates/turbo-tasks-backend/src/backend/mod.rs | 14 ++++++++++++-- .../src/backend/operation/connect_children.rs | 4 ++-- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/turbopack/crates/turbo-tasks-backend/src/backend/mod.rs b/turbopack/crates/turbo-tasks-backend/src/backend/mod.rs index 21abe711cab82..3d622de8f06c6 100644 --- a/turbopack/crates/turbo-tasks-backend/src/backend/mod.rs +++ b/turbopack/crates/turbo-tasks-backend/src/backend/mod.rs @@ -72,6 +72,16 @@ use crate::{ }, }; +/// Threshold for parallelizing making dependent tasks dirty. +/// If the number of dependent tasks exceeds this threshold, +/// the operation will be parallelized. +const DEPENDENT_TASKS_DIRTY_PARALLIZATION_THRESHOLD: usize = 128; + +/// Threshold for parallelizing prefetching tasks. +/// If the number of tasks to prefetch exceeds this threshold, +/// the operation will be parallelized. +const PREFETCH_TASKS_PARALLIZATION_THRESHOLD: usize = 128; + const SNAPSHOT_REQUESTED_BIT: usize = 1 << (usize::BITS - 1); /// Configurable idle timeout for snapshot persistence. @@ -2212,7 +2222,7 @@ impl TurboTasksBackendInner { span.record("result", "marked dirty"); } - if output_dependent_tasks.len() > 128 { + if output_dependent_tasks.len() > DEPENDENT_TASKS_DIRTY_PARALLIZATION_THRESHOLD { let chunk_size = good_chunk_size(output_dependent_tasks.len()); let chunks = into_chunks(output_dependent_tasks.to_vec(), chunk_size); let _ = scope_and_block(chunks.len(), |scope| { @@ -2605,7 +2615,7 @@ impl TurboTasksBackendInner { let range: Range = if let Some(range) = range { range } else { - if data.len() > 128 { + if data.len() > PREFETCH_TASKS_PARALLIZATION_THRESHOLD { let chunk_size = good_chunk_size(data.len()); let chunks = data.len().div_ceil(chunk_size); for i in 0..chunks { diff --git a/turbopack/crates/turbo-tasks-backend/src/backend/operation/connect_children.rs b/turbopack/crates/turbo-tasks-backend/src/backend/operation/connect_children.rs index 36ab4b042ecfa..3009a06f29bd2 100644 --- a/turbopack/crates/turbo-tasks-backend/src/backend/operation/connect_children.rs +++ b/turbopack/crates/turbo-tasks-backend/src/backend/operation/connect_children.rs @@ -125,10 +125,10 @@ pub fn connect_children( // This avoids long pauses of more than 30µs * 10k = 300ms. // We don't want to parallelize too eagerly as spawning tasks and the temporary allocations have // a cost as well. - const MIN_CHILDREN_FOR_PARALLEL: usize = 10000; + const CONNECT_CHILDREN_PARALLIZATION_THRESHOLD: usize = 10000; let len = new_follower_ids.len(); - if len >= MIN_CHILDREN_FOR_PARALLEL { + if len >= CONNECT_CHILDREN_PARALLIZATION_THRESHOLD { let new_follower_ids = new_follower_ids.into_vec(); let chunk_size = good_chunk_size(len); let _ = scope_and_block(len.div_ceil(chunk_size), |scope| {