Skip to main content

turbopack_core/chunk/
chunk_group.rs

1use std::sync::atomic::AtomicBool;
2
3use anyhow::{Context, Result};
4use bincode::{Decode, Encode};
5use rustc_hash::FxHashMap;
6use tracing::Instrument;
7use turbo_rcstr::rcstr;
8use turbo_tasks::{
9    FxIndexSet, OperationVc, ResolvedVc, TryFlatJoinIterExt, TryJoinIterExt, Vc, trace::TraceRawVcs,
10};
11
12use super::{
13    ChunkItemWithAsyncModuleInfo, ChunkingContext, availability_info::AvailabilityInfo,
14    chunking::make_chunks,
15};
16use crate::{
17    chunk::{
18        ChunkGroupContent, ChunkGroupContentInner, ChunkableModule, ChunkingType, Chunks,
19        available_modules::{AvailableModuleItem, AvailableModulesSet},
20        chunk_item_batch::{ChunkItemBatchGroup, ChunkItemOrBatchWithAsyncModuleInfo},
21    },
22    module_graph::{
23        GraphTraversalAction, ModuleGraph,
24        chunk_group_info::ChunkGroup,
25        merged_modules::MergedModuleInfo,
26        module_batch::{
27            ChunkableModuleBatchGroup, ChunkableModuleOrBatch, ModuleBatch, ModuleBatchGroup,
28            ModuleOrBatch,
29        },
30        module_batches::{BatchingConfig, ModuleBatchesGraphEdge},
31    },
32    output::OutputAssetsReference,
33};
34
35pub struct MakeChunkGroupResult {
36    pub chunks: ResolvedVc<Chunks>,
37    pub references: Vec<ResolvedVc<Box<dyn OutputAssetsReference>>>,
38    pub availability_info: AvailabilityInfo,
39}
40
41/// Creates a chunk group from a set of entries.
42pub async fn make_chunk_group(
43    chunk_group: ChunkGroup,
44    module_graph: ResolvedVc<ModuleGraph>,
45    chunking_context: ResolvedVc<Box<dyn ChunkingContext>>,
46    availability_info: AvailabilityInfo,
47) -> Result<MakeChunkGroupResult> {
48    let can_split_async = chunking_context.chunk_loading().await?.can_split_async();
49    let is_nested_async_availability_enabled = *chunking_context
50        .is_nested_async_availability_enabled()
51        .await?;
52    let should_merge_modules = *chunking_context.is_module_merging_enabled().await?;
53    let batching_config = chunking_context.batching_config().to_resolved().await?;
54
55    let ChunkGroupContent {
56        inner,
57        availability_info: new_availability_info,
58    } = chunk_group_content(
59        module_graph,
60        chunk_group,
61        ChunkGroupContentOptions {
62            availability_info,
63            can_split_async,
64            should_merge_modules,
65            batching_config,
66        },
67    )
68    .await?;
69    let ChunkGroupContentInner {
70        chunkable_items,
71        batch_groups,
72        async_modules,
73        available_modules: _,
74    } = &*inner;
75
76    let async_module_info = module_graph.async_module_info();
77
78    // Attach async info to chunkable modules
79    let mut chunk_items = chunkable_items
80        .iter()
81        .copied()
82        .map(|m| {
83            ChunkItemOrBatchWithAsyncModuleInfo::from_chunkable_module_or_batch(
84                m,
85                async_module_info,
86                *module_graph,
87                *chunking_context,
88            )
89        })
90        .try_join()
91        .await?
92        .into_iter()
93        .flatten()
94        .collect::<Vec<_>>();
95
96    let chunk_item_batch_groups = batch_groups
97        .iter()
98        .map(|&batch_group| {
99            ChunkItemBatchGroup::from_module_batch_group(
100                ChunkableModuleBatchGroup::from_module_batch_group(*batch_group),
101                *module_graph,
102                *chunking_context,
103            )
104            .to_resolved()
105        })
106        .try_join()
107        .await?;
108
109    // Insert async chunk loaders for every referenced async module
110    let async_availability_info =
111        if is_nested_async_availability_enabled || !availability_info.is_in_async_module() {
112            new_availability_info.in_async_module()
113        } else {
114            availability_info
115        };
116    let async_loaders = async_modules
117        .iter()
118        .copied()
119        .map(async |module| {
120            chunking_context
121                .async_loader_chunk_item(*module, *module_graph, async_availability_info)
122                .to_resolved()
123                .await
124        })
125        .try_join()
126        .await?;
127    let async_loader_chunk_items = async_loaders
128        .iter()
129        .map(async |&chunk_item| {
130            let chunk_type = chunk_item
131                .into_trait_ref()
132                .await?
133                .ty()
134                .to_resolved()
135                .await?;
136            Ok(ChunkItemOrBatchWithAsyncModuleInfo::ChunkItem(
137                ChunkItemWithAsyncModuleInfo {
138                    chunk_item,
139                    chunk_type,
140                    module: None,
141                    async_info: None,
142                },
143            ))
144        })
145        .try_join()
146        .await?;
147
148    chunk_items.extend(async_loader_chunk_items);
149
150    // Pass chunk items to chunking algorithm
151    let chunks = make_chunks(
152        *module_graph,
153        *chunking_context,
154        Vc::cell(chunk_items),
155        Vc::cell(chunk_item_batch_groups),
156        rcstr!(""),
157    )
158    .to_resolved()
159    .await?;
160
161    Ok(MakeChunkGroupResult {
162        chunks,
163        references: ResolvedVc::upcast_vec(async_loaders),
164        availability_info: new_availability_info,
165    })
166}
167
168#[turbo_tasks::task_input]
169#[derive(Debug, Clone, Hash, PartialEq, Eq, TraceRawVcs, Encode, Decode)]
170pub struct ChunkGroupContentOptions {
171    /// The availability info of the chunk group
172    pub availability_info: AvailabilityInfo,
173    /// Whether async modules can be split into separate chunks
174    pub can_split_async: bool,
175    /// Whether module merging is enabled
176    pub should_merge_modules: bool,
177    /// The batching config to use
178    pub batching_config: ResolvedVc<BatchingConfig>,
179}
180
181/// Computes the content of a chunk group.
182pub async fn chunk_group_content(
183    module_graph: ResolvedVc<ModuleGraph>,
184    chunk_group: ChunkGroup,
185    options: ChunkGroupContentOptions,
186) -> Result<ChunkGroupContent> {
187    let availability_info = options.availability_info;
188    let chunk_group_content = chunk_group_content_operation(module_graph, chunk_group, options);
189    let available_modules = available_modules_operation(chunk_group_content);
190    let inner = chunk_group_content.connect().await?;
191
192    Ok(ChunkGroupContent {
193        inner,
194        availability_info: availability_info.with_modules(available_modules).await?,
195    })
196}
197
198#[turbo_tasks::function(operation)]
199async fn available_modules_operation(
200    chunk_group_content: OperationVc<ChunkGroupContentInner>,
201) -> Result<Vc<AvailableModulesSet>> {
202    Ok(*chunk_group_content.connect().await?.available_modules)
203}
204
205#[turbo_tasks::function(operation)]
206async fn chunk_group_content_operation(
207    module_graph: ResolvedVc<ModuleGraph>,
208    chunk_group: ChunkGroup,
209    ChunkGroupContentOptions {
210        availability_info,
211        can_split_async,
212        should_merge_modules,
213        batching_config,
214    }: ChunkGroupContentOptions,
215) -> Result<Vc<ChunkGroupContentInner>> {
216    let module_batches_graph = module_graph.module_batches(*batching_config).await?;
217
218    type ModuleToChunkableMap = FxHashMap<ModuleOrBatch, ChunkableModuleOrBatch>;
219
220    struct TraverseState {
221        unsorted_items: ModuleToChunkableMap,
222        chunkable_items: FxIndexSet<ChunkableModuleOrBatch>,
223        async_modules: FxIndexSet<ResolvedVc<Box<dyn ChunkableModule>>>,
224    }
225
226    let mut state = TraverseState {
227        unsorted_items: FxHashMap::default(),
228        chunkable_items: FxIndexSet::default(),
229        async_modules: FxIndexSet::default(),
230    };
231
232    let available_modules = match availability_info.available_modules() {
233        Some(available_modules) => Some(available_modules.snapshot().await?),
234        None => None,
235    };
236
237    let mut entries = Vec::with_capacity(chunk_group.entries_count());
238    for entry in chunk_group.entries() {
239        entries.push(module_batches_graph.get_entry_index(entry).await?);
240    }
241
242    {
243        let _span = tracing::trace_span!("traversal").entered();
244        module_batches_graph.traverse_edges_from_entries_dfs(
245            entries,
246            &mut state,
247            |parent_info, &node, state| {
248                if matches!(node, ModuleOrBatch::None(_)) {
249                    return Ok(GraphTraversalAction::Continue);
250                }
251                // Traced modules are completely ignored during chunking
252                if let Some((
253                    _,
254                    ModuleBatchesGraphEdge {
255                        ty: ChunkingType::Traced { .. },
256                        ..
257                    },
258                )) = parent_info
259                {
260                    return Ok(GraphTraversalAction::Exclude);
261                }
262
263                let Some(chunkable_node) = ChunkableModuleOrBatch::from_module_or_batch(node)
264                else {
265                    return Ok(GraphTraversalAction::Exclude);
266                };
267
268                let is_available = available_modules
269                    .as_ref()
270                    .is_some_and(|available_modules| available_modules.get(chunkable_node.into()));
271
272                let Some((_, edge)) = parent_info else {
273                    // An entry from the entries list
274                    return Ok(if is_available {
275                        GraphTraversalAction::Exclude
276                    } else if state
277                        .unsorted_items
278                        .try_insert(node, chunkable_node)
279                        .is_ok()
280                    {
281                        GraphTraversalAction::Continue
282                    } else {
283                        GraphTraversalAction::Exclude
284                    });
285                };
286
287                Ok(match edge.ty {
288                    ChunkingType::Parallel { .. } | ChunkingType::Shared { .. } => {
289                        if is_available {
290                            GraphTraversalAction::Exclude
291                        } else if state
292                            .unsorted_items
293                            .try_insert(node, chunkable_node)
294                            .is_ok()
295                        {
296                            GraphTraversalAction::Continue
297                        } else {
298                            GraphTraversalAction::Exclude
299                        }
300                    }
301                    ChunkingType::Async => {
302                        if can_split_async {
303                            let chunkable_module =
304                                ResolvedVc::try_downcast(edge.module.unwrap())
305                                    .context("Module in async chunking edge is not chunkable")?;
306                            let is_async_loader_available =
307                                available_modules.as_ref().is_some_and(|available_modules| {
308                                    available_modules
309                                        .get(AvailableModuleItem::AsyncLoader(chunkable_module))
310                                });
311                            if !is_async_loader_available {
312                                state.async_modules.insert(chunkable_module);
313                            }
314                            GraphTraversalAction::Exclude
315                        } else if is_available {
316                            GraphTraversalAction::Exclude
317                        } else if state
318                            .unsorted_items
319                            .try_insert(node, chunkable_node)
320                            .is_ok()
321                        {
322                            GraphTraversalAction::Continue
323                        } else {
324                            GraphTraversalAction::Exclude
325                        }
326                    }
327                    ChunkingType::Traced { .. } => {
328                        // handled above before the sidecast
329                        unreachable!();
330                    }
331                    ChunkingType::Isolated { .. } => {
332                        // TODO currently not implemented
333                        GraphTraversalAction::Exclude
334                    }
335                })
336            },
337            |_, node, state| {
338                // Insert modules in topological order
339                if let Some(chunkable_module) = state.unsorted_items.get(node).copied() {
340                    state.chunkable_items.insert(chunkable_module);
341                }
342            },
343        )?;
344    }
345
346    // This needs to use the unmerged items
347    let available_modules: FxIndexSet<AvailableModuleItem> = state
348        .chunkable_items
349        .iter()
350        .copied()
351        .map(Into::into)
352        .chain(
353            state
354                .async_modules
355                .iter()
356                .copied()
357                .map(AvailableModuleItem::AsyncLoader),
358        )
359        .collect();
360    let available_modules: ResolvedVc<AvailableModulesSet> =
361        Vc::<AvailableModulesSet>::cell(available_modules)
362            .to_resolved()
363            .await?;
364
365    let should_merge_modules = if should_merge_modules {
366        let merged_modules = module_graph.merged_modules();
367        let merged_modules_ref = merged_modules.await?;
368        Some((merged_modules, merged_modules_ref))
369    } else {
370        None
371    };
372
373    let chunkable_items = if let Some((merged_modules, merged_modules_ref)) = &should_merge_modules
374    {
375        state
376            .chunkable_items
377            .into_iter()
378            .map(async |chunkable_module| match chunkable_module {
379                ChunkableModuleOrBatch::Module(module) => {
380                    let module = match merged_modules_ref
381                        .should_replace_module(ResolvedVc::upcast(module))
382                        .await?
383                    {
384                        Some(None) => return Ok(None),
385                        Some(Some(replacement)) => replacement,
386                        None => module,
387                    };
388
389                    Ok(Some(ChunkableModuleOrBatch::Module(module)))
390                }
391                ChunkableModuleOrBatch::Batch(batch) => Ok(Some(ChunkableModuleOrBatch::Batch(
392                    map_module_batch(*merged_modules, *batch)
393                        .to_resolved()
394                        .await?,
395                ))),
396                ChunkableModuleOrBatch::None(i) => Ok(Some(ChunkableModuleOrBatch::None(i))),
397            })
398            .try_flat_join()
399            .instrument(tracing::trace_span!("replace with merged modules"))
400            .await?
401    } else {
402        state.chunkable_items.into_iter().collect()
403    };
404
405    let mut batch_groups = FxIndexSet::default();
406    for &module in &chunkable_items {
407        if let Some(batch_group) = module_batches_graph.get_batch_group(&module.into()) {
408            batch_groups.insert(batch_group);
409        }
410    }
411
412    let batch_groups = if let Some((merged_modules, _)) = &should_merge_modules {
413        batch_groups
414            .into_iter()
415            .map(|group| map_module_batch_group(*merged_modules, *group).to_resolved())
416            .try_join()
417            .await?
418    } else {
419        batch_groups.into_iter().collect()
420    };
421
422    Ok(ChunkGroupContentInner {
423        chunkable_items,
424        batch_groups,
425        async_modules: state.async_modules,
426        available_modules,
427    }
428    .cell())
429}
430
431#[turbo_tasks::function]
432async fn map_module_batch(
433    merged_modules: Vc<MergedModuleInfo>,
434    batch: Vc<ModuleBatch>,
435) -> Result<Vc<ModuleBatch>> {
436    let merged_modules = merged_modules.await?;
437    let batch_ref = batch.await?;
438
439    let modified = AtomicBool::new(false);
440    let modules = batch_ref
441        .modules
442        .iter()
443        .copied()
444        .map(async |module| {
445            let module = match merged_modules
446                .should_replace_module(ResolvedVc::upcast(module))
447                .await?
448            {
449                Some(None) => {
450                    modified.store(true, std::sync::atomic::Ordering::Relaxed);
451                    return Ok(None);
452                }
453                Some(Some(replacement)) => {
454                    modified.store(true, std::sync::atomic::Ordering::Relaxed);
455                    replacement
456                }
457                None => module,
458            };
459
460            Ok(Some(module))
461        })
462        .try_flat_join()
463        .await?;
464
465    if modified.into_inner() {
466        Ok(ModuleBatch::new(
467            ResolvedVc::deref_vec(modules),
468            batch_ref.chunk_groups.clone(),
469        ))
470    } else {
471        Ok(batch)
472    }
473}
474
475#[turbo_tasks::function]
476async fn map_module_batch_group(
477    merged_modules: Vc<MergedModuleInfo>,
478    group: Vc<ModuleBatchGroup>,
479) -> Result<Vc<ModuleBatchGroup>> {
480    let merged_modules_ref = merged_modules.await?;
481    let group_ref = group.await?;
482
483    let modified = AtomicBool::new(false);
484    let items = group_ref
485        .items
486        .iter()
487        .copied()
488        .map(async |chunkable_module| match chunkable_module {
489            ModuleOrBatch::Module(module) => {
490                let module = match merged_modules_ref.should_replace_module(module).await? {
491                    Some(None) => {
492                        modified.store(true, std::sync::atomic::Ordering::Relaxed);
493                        return Ok(None);
494                    }
495                    Some(Some(replacement)) => {
496                        modified.store(true, std::sync::atomic::Ordering::Relaxed);
497                        ResolvedVc::upcast(replacement)
498                    }
499                    None => module,
500                };
501
502                Ok(Some(ModuleOrBatch::Module(module)))
503            }
504            ModuleOrBatch::Batch(batch) => {
505                let replacement = map_module_batch(merged_modules, *batch)
506                    .to_resolved()
507                    .await?;
508                if replacement != batch {
509                    modified.store(true, std::sync::atomic::Ordering::Relaxed);
510                }
511                Ok(Some(ModuleOrBatch::Batch(replacement)))
512            }
513            ModuleOrBatch::None(i) => Ok(Some(ModuleOrBatch::None(i))),
514        })
515        .try_flat_join()
516        .await?;
517
518    if modified.into_inner() {
519        Ok(ModuleBatchGroup::new(items, group_ref.chunk_groups.clone()))
520    } else {
521        Ok(group)
522    }
523}