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