turbopack_core/chunk/
chunk_group.rs

1use std::{cell::RefCell, 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    Chunk, ChunkGroupContent, ChunkItemWithAsyncModuleInfo, ChunkingContext,
10    availability_info::AvailabilityInfo, chunking::make_chunks,
11};
12use crate::{
13    chunk::{
14        ChunkableModule, ChunkingType,
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: Vec<ResolvedVc<Box<dyn Chunk>>>,
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        chunk_items,
159        chunk_item_batch_groups,
160        rcstr!(""),
161    )
162    .await?;
163
164    Ok(MakeChunkGroupResult {
165        chunks,
166        referenced_output_assets,
167        references: ResolvedVc::upcast_vec(async_loaders),
168        availability_info: new_availability_info,
169    })
170}
171
172pub async fn references_to_output_assets(
173    references: impl IntoIterator<Item = &ResolvedVc<Box<dyn ModuleReference>>>,
174) -> Result<Vc<OutputAssetsWithReferenced>> {
175    let output_assets = references
176        .into_iter()
177        .map(|reference| reference.resolve_reference().primary_output_assets())
178        .try_join()
179        .await?;
180    let mut set = HashSet::new();
181    let output_assets = output_assets
182        .iter()
183        .flatten()
184        .copied()
185        .filter(|&asset| set.insert(asset))
186        .collect::<Vec<_>>();
187    Ok(OutputAssetsWithReferenced {
188        assets: ResolvedVc::cell(output_assets),
189        referenced_assets: OutputAssets::empty_resolved(),
190        references: OutputAssetsReferences::empty_resolved(),
191    }
192    .cell())
193}
194
195pub struct ChunkGroupContentOptions {
196    /// The availability info of the chunk group
197    pub availability_info: AvailabilityInfo,
198    /// Whether async modules can be split into separate chunks
199    pub can_split_async: bool,
200    /// Whether traced modules should be collected
201    pub should_trace: bool,
202    /// Whether module merging is enabled
203    pub should_merge_modules: bool,
204    /// The batching config to use
205    pub batching_config: Vc<BatchingConfig>,
206}
207
208/// Computes the content of a chunk group.
209pub async fn chunk_group_content(
210    module_graph: Vc<ModuleGraph>,
211    chunk_group_entries: impl IntoIterator<
212        IntoIter = impl Iterator<Item = ResolvedVc<Box<dyn Module>>> + Send,
213    > + Send,
214    ChunkGroupContentOptions {
215        availability_info,
216        can_split_async,
217        should_trace,
218        should_merge_modules,
219        batching_config,
220    }: ChunkGroupContentOptions,
221) -> Result<ChunkGroupContent> {
222    let module_batches_graph = module_graph.module_batches(batching_config).await?;
223
224    type ModuleToChunkableMap = FxHashMap<ModuleOrBatch, ChunkableModuleOrBatch>;
225
226    struct TraverseState {
227        unsorted_items: ModuleToChunkableMap,
228        chunkable_items: FxIndexSet<ChunkableModuleOrBatch>,
229        async_modules: FxIndexSet<ResolvedVc<Box<dyn ChunkableModule>>>,
230        traced_modules: FxIndexSet<ResolvedVc<Box<dyn Module>>>,
231    }
232
233    let mut state = TraverseState {
234        unsorted_items: FxHashMap::default(),
235        chunkable_items: FxIndexSet::default(),
236        async_modules: FxIndexSet::default(),
237        traced_modules: FxIndexSet::default(),
238    };
239
240    let available_modules = match availability_info.available_modules() {
241        Some(available_modules) => Some(available_modules.snapshot().await?),
242        None => None,
243    };
244
245    let chunk_group_entries = chunk_group_entries.into_iter();
246    let mut entries = Vec::with_capacity(chunk_group_entries.size_hint().0);
247    for entry in chunk_group_entries {
248        entries.push(module_batches_graph.get_entry_index(entry).await?);
249    }
250
251    module_batches_graph.traverse_edges_from_entries_dfs(
252        entries,
253        &mut state,
254        |parent_info, &node, state| {
255            if matches!(node, ModuleOrBatch::None(_)) {
256                return Ok(GraphTraversalAction::Continue);
257            }
258            // Traced modules need to have a special handling
259            if let Some((
260                _,
261                ModuleBatchesGraphEdge {
262                    ty: ChunkingType::Traced,
263                    ..
264                },
265            )) = parent_info
266            {
267                if should_trace {
268                    let ModuleOrBatch::Module(module) = node else {
269                        unreachable!();
270                    };
271                    state.traced_modules.insert(module);
272                }
273                return Ok(GraphTraversalAction::Exclude);
274            }
275
276            let Some(chunkable_node) = ChunkableModuleOrBatch::from_module_or_batch(node) else {
277                return Ok(GraphTraversalAction::Exclude);
278            };
279
280            let is_available = available_modules
281                .as_ref()
282                .is_some_and(|available_modules| available_modules.get(chunkable_node.into()));
283
284            let Some((_, edge)) = parent_info else {
285                // An entry from the entries list
286                return Ok(if is_available {
287                    GraphTraversalAction::Exclude
288                } else if state
289                    .unsorted_items
290                    .try_insert(node, chunkable_node)
291                    .is_ok()
292                {
293                    GraphTraversalAction::Continue
294                } else {
295                    GraphTraversalAction::Exclude
296                });
297            };
298
299            Ok(match edge.ty {
300                ChunkingType::Parallel { .. } | ChunkingType::Shared { .. } => {
301                    if is_available {
302                        GraphTraversalAction::Exclude
303                    } else if state
304                        .unsorted_items
305                        .try_insert(node, chunkable_node)
306                        .is_ok()
307                    {
308                        GraphTraversalAction::Continue
309                    } else {
310                        GraphTraversalAction::Exclude
311                    }
312                }
313                ChunkingType::Async => {
314                    if can_split_async {
315                        let chunkable_module = ResolvedVc::try_downcast(edge.module.unwrap())
316                            .context("Module in async chunking edge is not chunkable")?;
317                        let is_async_loader_available =
318                            available_modules.as_ref().is_some_and(|available_modules| {
319                                available_modules
320                                    .get(AvailableModuleItem::AsyncLoader(chunkable_module))
321                            });
322                        if !is_async_loader_available {
323                            state.async_modules.insert(chunkable_module);
324                        }
325                        GraphTraversalAction::Exclude
326                    } else if is_available {
327                        GraphTraversalAction::Exclude
328                    } else if state
329                        .unsorted_items
330                        .try_insert(node, chunkable_node)
331                        .is_ok()
332                    {
333                        GraphTraversalAction::Continue
334                    } else {
335                        GraphTraversalAction::Exclude
336                    }
337                }
338                ChunkingType::Traced => {
339                    // handled above before the sidecast
340                    unreachable!();
341                }
342                ChunkingType::Isolated { .. } => {
343                    // TODO currently not implemented
344                    GraphTraversalAction::Exclude
345                }
346            })
347        },
348        |_, node, state| {
349            // Insert modules in topological order
350            if let Some(chunkable_module) = state.unsorted_items.get(node).copied() {
351                state.chunkable_items.insert(chunkable_module);
352            }
353        },
354    )?;
355
356    // This needs to use the unmerged items
357    let available_modules = state
358        .chunkable_items
359        .iter()
360        .copied()
361        .map(Into::into)
362        .chain(
363            state
364                .async_modules
365                .iter()
366                .copied()
367                .map(AvailableModuleItem::AsyncLoader),
368        )
369        .collect();
370    let availability_info = availability_info
371        .with_modules(Vc::cell(available_modules))
372        .await?;
373
374    let should_merge_modules = if should_merge_modules {
375        let merged_modules = module_graph.merged_modules();
376        let merged_modules_ref = merged_modules.await?;
377        Some((merged_modules, merged_modules_ref))
378    } else {
379        None
380    };
381
382    let chunkable_items = if let Some((merged_modules, merged_modules_ref)) = &should_merge_modules
383    {
384        state
385            .chunkable_items
386            .into_iter()
387            .map(async |chunkable_module| match chunkable_module {
388                ChunkableModuleOrBatch::Module(module) => {
389                    if !merged_modules_ref.should_create_chunk_item_for(ResolvedVc::upcast(module))
390                    {
391                        return Ok(None);
392                    }
393
394                    let module = if let Some(replacement) =
395                        merged_modules_ref.should_replace_module(ResolvedVc::upcast(module))
396                    {
397                        replacement
398                    } else {
399                        module
400                    };
401
402                    Ok(Some(ChunkableModuleOrBatch::Module(module)))
403                }
404                ChunkableModuleOrBatch::Batch(batch) => Ok(Some(ChunkableModuleOrBatch::Batch(
405                    map_module_batch(*merged_modules, *batch)
406                        .to_resolved()
407                        .await?,
408                ))),
409                ChunkableModuleOrBatch::None(i) => Ok(Some(ChunkableModuleOrBatch::None(i))),
410            })
411            .try_flat_join()
412            .await?
413    } else {
414        state.chunkable_items.into_iter().collect()
415    };
416
417    let mut batch_groups = FxIndexSet::default();
418    for &module in &chunkable_items {
419        if let Some(batch_group) = module_batches_graph.get_batch_group(&module.into()) {
420            batch_groups.insert(batch_group);
421        }
422    }
423
424    let batch_groups = if let Some((merged_modules, _)) = &should_merge_modules {
425        batch_groups
426            .into_iter()
427            .map(|group| map_module_batch_group(*merged_modules, *group).to_resolved())
428            .try_join()
429            .await?
430    } else {
431        batch_groups.into_iter().collect()
432    };
433
434    Ok(ChunkGroupContent {
435        chunkable_items,
436        batch_groups,
437        async_modules: state.async_modules,
438        traced_modules: state.traced_modules,
439        availability_info,
440    })
441}
442
443#[turbo_tasks::function]
444async fn map_module_batch(
445    merged_modules: Vc<MergedModuleInfo>,
446    batch: Vc<ModuleBatch>,
447) -> Result<Vc<ModuleBatch>> {
448    let merged_modules = merged_modules.await?;
449    let batch_ref = batch.await?;
450
451    let modified = RefCell::new(false);
452    let modules = batch_ref
453        .modules
454        .iter()
455        .flat_map(|&module| {
456            if !merged_modules.should_create_chunk_item_for(ResolvedVc::upcast(module)) {
457                *modified.borrow_mut() = true;
458                return None;
459            }
460
461            let module = if let Some(replacement) =
462                merged_modules.should_replace_module(ResolvedVc::upcast(module))
463            {
464                *modified.borrow_mut() = true;
465                replacement
466            } else {
467                module
468            };
469
470            Some(module)
471        })
472        .collect::<Vec<_>>();
473
474    if modified.into_inner() {
475        Ok(ModuleBatch::new(
476            ResolvedVc::deref_vec(modules),
477            batch_ref.chunk_groups.clone(),
478        ))
479    } else {
480        Ok(batch)
481    }
482}
483
484#[turbo_tasks::function]
485async fn map_module_batch_group(
486    merged_modules: Vc<MergedModuleInfo>,
487    group: Vc<ModuleBatchGroup>,
488) -> Result<Vc<ModuleBatchGroup>> {
489    let merged_modules_ref = merged_modules.await?;
490    let group_ref = group.await?;
491
492    let modified = AtomicBool::new(false);
493    let items = group_ref
494        .items
495        .iter()
496        .copied()
497        .map(async |chunkable_module| match chunkable_module {
498            ModuleOrBatch::Module(module) => {
499                if !merged_modules_ref.should_create_chunk_item_for(module) {
500                    modified.store(true, std::sync::atomic::Ordering::Relaxed);
501                    return Ok(None);
502                }
503
504                let module =
505                    if let Some(replacement) = merged_modules_ref.should_replace_module(module) {
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}