turbopack_core/chunk/
chunk_group.rs

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