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