Skip to main content

turbopack_core/chunk/
chunk_group.rs

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