1use std::sync::atomic::AtomicBool;
2
3use anyhow::{Context, Result, bail};
4use bincode::{Decode, Encode};
5use rustc_hash::{FxHashMap, FxHashSet};
6use smallvec::SmallVec;
7use tracing::Instrument;
8use turbo_rcstr::rcstr;
9use turbo_tasks::{
10 FxIndexSet, JoinIterExt, OperationVc, ResolvedVc, TryFlatJoinIterExt, TryJoinIterExt, Vc,
11};
12
13use super::{
14 ChunkItemWithAsyncModuleInfo, ChunkingContext, availability_info::AvailabilityInfo,
15 chunking::make_chunks,
16};
17use crate::{
18 chunk::{
19 ChunkGroupContent, ChunkGroupContentInner, ChunkableModule, ChunkingType, Chunks,
20 available_modules::{AvailableModuleItem, AvailableModulesSet},
21 chunk_item_batch::{ChunkItemBatchGroup, ChunkItemOrBatchWithAsyncModuleInfo},
22 },
23 emit_collect::CollectingModule,
24 module::{Module, Modules},
25 module_graph::{
26 GraphTraversalAction, ModuleGraph,
27 chunk_group_info::ChunkGroup,
28 merged_modules::MergedModuleInfo,
29 module_batch::{
30 ChunkableModuleBatchGroup, ChunkableModuleOrBatch, ModuleBatch, ModuleBatchGroup,
31 ModuleOrBatch,
32 },
33 module_batches::{BatchingConfig, ModuleBatchesGraphEdge},
34 },
35 output::OutputAssetsReference,
36};
37
38pub struct MakeChunkGroupResult {
39 pub chunks: ResolvedVc<Chunks>,
40 pub references: Vec<ResolvedVc<Box<dyn OutputAssetsReference>>>,
41 pub availability_info: AvailabilityInfo,
42}
43
44pub async fn make_chunk_group(
48 chunk_groups: SmallVec<[ChunkGroup; 1]>,
49 module_graph: ResolvedVc<ModuleGraph>,
50 chunking_context: ResolvedVc<Box<dyn ChunkingContext>>,
51 availability_info: AvailabilityInfo,
52) -> Result<MakeChunkGroupResult> {
53 let can_split_async = chunking_context.chunk_loading().await?.can_split_async();
54 let is_nested_async_availability_enabled = *chunking_context
55 .is_nested_async_availability_enabled()
56 .await?;
57 let should_merge_modules = *chunking_context.is_module_merging_enabled().await?;
58 let batching_config = chunking_context.batching_config().to_resolved().await?;
59
60 let ChunkGroupContent {
61 inner,
62 availability_info: new_availability_info,
63 } = chunk_group_content(
64 module_graph,
65 chunk_groups,
66 ChunkGroupContentOptions {
67 availability_info,
68 can_split_async,
69 should_merge_modules,
70 batching_config,
71 },
72 )
73 .await?;
74 let ChunkGroupContentInner {
75 chunkable_items,
76 batch_groups,
77 async_modules,
78 collecting_modules,
79 available_modules: _,
80 } = &*inner;
81
82 let async_module_info = module_graph.async_module_info();
83
84 let mut chunk_items = chunkable_items
86 .iter()
87 .copied()
88 .map(|m| {
89 ChunkItemOrBatchWithAsyncModuleInfo::from_chunkable_module_or_batch(
90 m,
91 async_module_info,
92 *module_graph,
93 *chunking_context,
94 )
95 })
96 .join()
97 .await
98 .into_iter()
99 .filter_map(Result::transpose)
100 .collect::<Result<Vec<_>>>()?;
101
102 let chunk_item_batch_groups = batch_groups
103 .iter()
104 .map(|&batch_group| {
105 ChunkItemBatchGroup::from_module_batch_group(
106 ChunkableModuleBatchGroup::from_module_batch_group(*batch_group),
107 *module_graph,
108 *chunking_context,
109 )
110 .to_resolved()
111 })
112 .try_join()
113 .await?;
114
115 chunk_items.extend(
116 collecting_modules
117 .into_iter()
118 .map(async |module| {
119 let Some(entry_chunk_group) = new_availability_info.entry_group() else {
120 bail!("unexpected collect module in non-entry chunk group",);
121 };
122 let chunk_item = module
123 .as_chunk_item(*module_graph, *chunking_context, *entry_chunk_group)
124 .to_resolved()
125 .await?;
126 let chunk_type = chunk_item
127 .into_trait_ref()
128 .await?
129 .ty()
130 .to_resolved()
131 .await?;
132 Ok(ChunkItemOrBatchWithAsyncModuleInfo::ChunkItem(
133 ChunkItemWithAsyncModuleInfo {
134 chunk_item,
135 chunk_type,
136 module: Some(ResolvedVc::upcast(*module)),
137 async_info: None,
138 },
139 ))
140 })
141 .try_join()
142 .await?,
143 );
144
145 let async_availability_info =
147 if is_nested_async_availability_enabled || !availability_info.is_in_async_module() {
148 new_availability_info.in_async_module()
149 } else {
150 availability_info
151 };
152 let async_loaders = async_modules
153 .iter()
154 .copied()
155 .map(|module| {
156 chunking_context
157 .async_loader_chunk_item(*module, *module_graph, async_availability_info)
158 .to_resolved()
159 })
160 .try_join()
161 .await?;
162 let async_loader_chunk_items = async_loaders
163 .iter()
164 .map(async |&chunk_item| {
165 let chunk_type = chunk_item
166 .into_trait_ref()
167 .await?
168 .ty()
169 .to_resolved()
170 .await?;
171 Ok(ChunkItemOrBatchWithAsyncModuleInfo::ChunkItem(
172 ChunkItemWithAsyncModuleInfo {
173 chunk_item,
174 chunk_type,
175 module: None,
176 async_info: None,
177 },
178 ))
179 })
180 .try_join()
181 .await?;
182
183 chunk_items.extend(async_loader_chunk_items);
184
185 let chunks = make_chunks(
187 *module_graph,
188 *chunking_context,
189 Vc::cell(chunk_items),
190 Vc::cell(chunk_item_batch_groups),
191 rcstr!(""),
192 )
193 .to_resolved()
194 .await?;
195
196 Ok(MakeChunkGroupResult {
197 chunks,
198 references: ResolvedVc::upcast_vec(async_loaders),
199 availability_info: new_availability_info,
200 })
201}
202
203#[turbo_tasks::task_input]
204#[derive(Debug, Clone, Hash, PartialEq, Eq, Encode, Decode)]
205pub struct ChunkGroupContentOptions {
206 pub availability_info: AvailabilityInfo,
208 pub can_split_async: bool,
210 pub should_merge_modules: bool,
212 pub batching_config: ResolvedVc<BatchingConfig>,
214}
215
216pub async fn chunk_group_content(
224 module_graph: ResolvedVc<ModuleGraph>,
225 chunk_groups: SmallVec<[ChunkGroup; 1]>,
226 options: ChunkGroupContentOptions,
227) -> Result<ChunkGroupContent> {
228 let availability_info = options.availability_info;
229 let chunk_groups = dedup_chunk_groups(chunk_groups)?;
230
231 let entry_group: Option<ResolvedVc<Modules>> =
232 entry_group_modules(&chunk_groups).map(ResolvedVc::cell);
233
234 let chunk_group_content = chunk_group_content_operation(module_graph, chunk_groups, options);
235 let available_modules = available_modules_operation(chunk_group_content);
236 let availability_info = availability_info.with_modules(available_modules).await?;
237
238 let availability_info = if let Some(entry_group) = entry_group {
239 availability_info.with_entry_group(entry_group)
240 } else {
241 availability_info
242 };
243 let inner = chunk_group_content.connect().await?;
244
245 Ok(ChunkGroupContent {
246 inner,
247 availability_info,
248 })
249}
250
251fn dedup_chunk_groups(
254 chunk_groups: SmallVec<[ChunkGroup; 1]>,
255) -> Result<SmallVec<[ChunkGroup; 1]>> {
256 if chunk_groups.is_empty() {
257 bail!("Cannot chunk an empty list of chunk groups");
258 }
259 let mut keys = FxHashSet::default();
260 Ok(chunk_groups
261 .into_iter()
262 .filter(|chunk_group| keys.insert(chunk_group.key()))
263 .collect())
264}
265
266fn entry_group_modules(chunk_groups: &[ChunkGroup]) -> Option<Vec<ResolvedVc<Box<dyn Module>>>> {
269 let mut modules = FxIndexSet::default();
270 let mut has_entry_group = false;
271 for chunk_group in chunk_groups {
272 if let ChunkGroup::Entry(entries) = chunk_group {
273 has_entry_group = true;
274 modules.extend(entries.iter().copied());
275 }
276 }
277 has_entry_group.then(|| modules.into_iter().collect())
278}
279
280#[turbo_tasks::function(operation)]
281async fn available_modules_operation(
282 chunk_group_content: OperationVc<ChunkGroupContentInner>,
283) -> Result<Vc<AvailableModulesSet>> {
284 Ok(*chunk_group_content.connect().await?.available_modules)
285}
286
287#[turbo_tasks::function(operation)]
288async fn chunk_group_content_operation(
289 module_graph: ResolvedVc<ModuleGraph>,
290 chunk_groups: SmallVec<[ChunkGroup; 1]>,
291 ChunkGroupContentOptions {
292 availability_info,
293 can_split_async,
294 should_merge_modules,
295 batching_config,
296 }: ChunkGroupContentOptions,
297) -> Result<Vc<ChunkGroupContentInner>> {
298 let module_batches_graph = module_graph.module_batches(*batching_config).await?;
299
300 type ModuleToChunkableMap = FxHashMap<ModuleOrBatch, ChunkableModuleOrBatch>;
301
302 struct TraverseState {
303 unsorted_items: ModuleToChunkableMap,
304 chunkable_items: FxIndexSet<ChunkableModuleOrBatch>,
305 async_modules: FxIndexSet<ResolvedVc<Box<dyn ChunkableModule>>>,
306 collecting_modules: FxIndexSet<ResolvedVc<Box<dyn CollectingModule>>>,
307 }
308
309 let mut state = TraverseState {
310 unsorted_items: FxHashMap::default(),
311 chunkable_items: FxIndexSet::default(),
312 async_modules: FxIndexSet::default(),
313 collecting_modules: FxIndexSet::default(),
314 };
315
316 let available_modules = match availability_info.available_modules() {
317 Some(available_modules) => Some(available_modules.snapshot().await?),
318 None => None,
319 };
320
321 let entries = chunk_groups
324 .iter()
325 .flat_map(|chunk_group| chunk_group.entries())
326 .collect::<FxIndexSet<_>>()
327 .into_iter()
328 .map(|entry| module_batches_graph.get_entry_index(entry))
329 .try_join()
330 .await?;
331
332 let active_page_entries: Option<FxHashSet<ResolvedVc<Box<dyn Module>>>> =
333 if let Some(entry_modules) = entry_group_modules(&chunk_groups) {
334 Some(entry_modules.into_iter().collect())
335 } else if let Some(entry_group) = availability_info.entry_group() {
336 Some(entry_group.await?.iter().copied().collect())
337 } else {
338 None
339 };
340
341 {
342 let _span = tracing::trace_span!("traversal").entered();
343 module_batches_graph.traverse_edges_from_entries_dfs(
344 entries,
345 active_page_entries.as_ref(),
346 &mut state,
347 |parent_info, &node, state| {
348 if matches!(node, ModuleOrBatch::None(_)) {
349 return Ok(GraphTraversalAction::Continue);
350 }
351 if let Some((
353 _,
354 ModuleBatchesGraphEdge {
355 ty: ChunkingType::Traced { .. },
356 ..
357 },
358 )) = parent_info
359 {
360 return Ok(GraphTraversalAction::Exclude);
361 }
362
363 if let Some(collecting_module) = parent_info
364 .and_then(|(_, edge)| edge.module)
365 .and_then(ResolvedVc::try_downcast::<Box<dyn CollectingModule>>)
366 {
367 state.collecting_modules.insert(collecting_module);
368 return Ok(GraphTraversalAction::Continue);
369 }
370
371 let Some(chunkable_node) = ChunkableModuleOrBatch::from_module_or_batch(node)
372 else {
373 return Ok(GraphTraversalAction::Exclude);
374 };
375
376 let is_available = available_modules
377 .as_ref()
378 .is_some_and(|available_modules| available_modules.get(chunkable_node.into()));
379
380 let Some((_, edge)) = parent_info else {
381 return Ok(if is_available {
383 GraphTraversalAction::Exclude
384 } else if state
385 .unsorted_items
386 .try_insert(node, chunkable_node)
387 .is_ok()
388 {
389 GraphTraversalAction::Continue
390 } else {
391 GraphTraversalAction::Exclude
392 });
393 };
394
395 Ok(match edge.ty {
396 ChunkingType::Parallel { .. }
397 | ChunkingType::Shared { .. }
398 | ChunkingType::Collected { .. } => {
399 if is_available {
400 GraphTraversalAction::Exclude
401 } else if state
402 .unsorted_items
403 .try_insert(node, chunkable_node)
404 .is_ok()
405 {
406 GraphTraversalAction::Continue
407 } else {
408 GraphTraversalAction::Exclude
409 }
410 }
411 ChunkingType::Async => {
412 if can_split_async {
413 let chunkable_module =
414 ResolvedVc::try_downcast(edge.module.unwrap())
415 .context("Module in async chunking edge is not chunkable")?;
416 let is_async_loader_available =
417 available_modules.as_ref().is_some_and(|available_modules| {
418 available_modules
419 .get(AvailableModuleItem::AsyncLoader(chunkable_module))
420 });
421 if !is_async_loader_available {
422 state.async_modules.insert(chunkable_module);
423 }
424 GraphTraversalAction::Exclude
425 } else if is_available {
426 GraphTraversalAction::Exclude
427 } else if state
428 .unsorted_items
429 .try_insert(node, chunkable_node)
430 .is_ok()
431 {
432 GraphTraversalAction::Continue
433 } else {
434 GraphTraversalAction::Exclude
435 }
436 }
437 ChunkingType::Traced { .. } => {
438 unreachable!();
440 }
441 ChunkingType::PerEntry => {
442 bail!(
444 "ChunkingType::PerEntry is currently only supported for \
445 CollectingModule"
446 );
447 }
448 ChunkingType::Emitted { .. } => {
449 GraphTraversalAction::Exclude
451 }
452 ChunkingType::Isolated { .. } => {
453 GraphTraversalAction::Exclude
455 }
456 })
457 },
458 |_, node, state| {
459 if let Some(chunkable_module) = state.unsorted_items.get(node).copied() {
461 state.chunkable_items.insert(chunkable_module);
462 }
463 },
464 )?;
465 }
466
467 let available_modules: FxIndexSet<AvailableModuleItem> = state
469 .chunkable_items
470 .iter()
471 .copied()
472 .map(Into::into)
473 .chain(
474 state
475 .async_modules
476 .iter()
477 .copied()
478 .map(AvailableModuleItem::AsyncLoader),
479 )
480 .collect();
481 let available_modules: ResolvedVc<AvailableModulesSet> =
482 Vc::<AvailableModulesSet>::cell(available_modules)
483 .to_resolved()
484 .await?;
485
486 let should_merge_modules = if should_merge_modules {
487 let merged_modules = module_graph.merged_modules();
488 let merged_modules_ref = merged_modules.await?;
489 Some((merged_modules, merged_modules_ref))
490 } else {
491 None
492 };
493
494 let chunkable_items = if let Some((merged_modules, merged_modules_ref)) = &should_merge_modules
495 {
496 state
497 .chunkable_items
498 .into_iter()
499 .map(async |chunkable_module| match chunkable_module {
500 ChunkableModuleOrBatch::Module(module) => {
501 let module = match merged_modules_ref
502 .should_replace_module(ResolvedVc::upcast(module))
503 .await?
504 {
505 Some(None) => return Ok(None),
506 Some(Some(replacement)) => replacement,
507 None => module,
508 };
509
510 Ok(Some(ChunkableModuleOrBatch::Module(module)))
511 }
512 ChunkableModuleOrBatch::Batch(batch) => Ok(Some(ChunkableModuleOrBatch::Batch(
513 map_module_batch(*merged_modules, *batch)
514 .to_resolved()
515 .await?,
516 ))),
517 ChunkableModuleOrBatch::None(i) => Ok(Some(ChunkableModuleOrBatch::None(i))),
518 })
519 .try_flat_join()
520 .instrument(tracing::trace_span!("replace with merged modules"))
521 .await?
522 } else {
523 state.chunkable_items.into_iter().collect()
524 };
525
526 let mut batch_groups = FxIndexSet::default();
527 for &module in &chunkable_items {
528 if let Some(batch_group) = module_batches_graph.get_batch_group(&module.into()) {
529 batch_groups.insert(batch_group);
530 }
531 }
532
533 let batch_groups = if let Some((merged_modules, _)) = &should_merge_modules {
534 batch_groups
535 .into_iter()
536 .map(|group| map_module_batch_group(*merged_modules, *group).to_resolved())
537 .try_join()
538 .await?
539 } else {
540 batch_groups.into_iter().collect()
541 };
542
543 Ok(ChunkGroupContentInner {
544 chunkable_items,
545 batch_groups,
546 async_modules: state.async_modules,
547 collecting_modules: state.collecting_modules,
548 available_modules,
549 }
550 .cell())
551}
552
553#[turbo_tasks::function]
554async fn map_module_batch(
555 merged_modules: Vc<MergedModuleInfo>,
556 batch: Vc<ModuleBatch>,
557) -> Result<Vc<ModuleBatch>> {
558 let merged_modules = merged_modules.await?;
559 let batch_ref = batch.await?;
560
561 let modified = AtomicBool::new(false);
562 let modules = batch_ref
563 .modules
564 .iter()
565 .copied()
566 .map(async |module| {
567 let module = match merged_modules
568 .should_replace_module(ResolvedVc::upcast(module))
569 .await?
570 {
571 Some(None) => {
572 modified.store(true, std::sync::atomic::Ordering::Relaxed);
573 return Ok(None);
574 }
575 Some(Some(replacement)) => {
576 modified.store(true, std::sync::atomic::Ordering::Relaxed);
577 replacement
578 }
579 None => module,
580 };
581
582 Ok(Some(module))
583 })
584 .try_flat_join()
585 .await?;
586
587 if modified.into_inner() {
588 Ok(ModuleBatch::new(
589 ResolvedVc::deref_vec(modules),
590 batch_ref.chunk_groups.clone(),
591 ))
592 } else {
593 Ok(batch)
594 }
595}
596
597#[turbo_tasks::function]
598async fn map_module_batch_group(
599 merged_modules: Vc<MergedModuleInfo>,
600 group: Vc<ModuleBatchGroup>,
601) -> Result<Vc<ModuleBatchGroup>> {
602 let merged_modules_ref = merged_modules.await?;
603 let group_ref = group.await?;
604
605 let modified = AtomicBool::new(false);
606 let items = group_ref
607 .items
608 .iter()
609 .copied()
610 .map(async |chunkable_module| match chunkable_module {
611 ModuleOrBatch::Module(module) => {
612 let module = match merged_modules_ref.should_replace_module(module).await? {
613 Some(None) => {
614 modified.store(true, std::sync::atomic::Ordering::Relaxed);
615 return Ok(None);
616 }
617 Some(Some(replacement)) => {
618 modified.store(true, std::sync::atomic::Ordering::Relaxed);
619 ResolvedVc::upcast(replacement)
620 }
621 None => module,
622 };
623
624 Ok(Some(ModuleOrBatch::Module(module)))
625 }
626 ModuleOrBatch::Batch(batch) => {
627 let replacement = map_module_batch(merged_modules, *batch)
628 .to_resolved()
629 .await?;
630 if replacement != batch {
631 modified.store(true, std::sync::atomic::Ordering::Relaxed);
632 }
633 Ok(Some(ModuleOrBatch::Batch(replacement)))
634 }
635 ModuleOrBatch::None(i) => Ok(Some(ModuleOrBatch::None(i))),
636 })
637 .try_flat_join()
638 .await?;
639
640 if modified.into_inner() {
641 Ok(ModuleBatchGroup::new(items, group_ref.chunk_groups.clone()))
642 } else {
643 Ok(group)
644 }
645}
646
647#[cfg(test)]
648mod tests {
649 use anyhow::Result;
650 use smallvec::smallvec;
651 use turbo_rcstr::rcstr;
652 use turbo_tasks::{Completion, ResolvedVc, Vc};
653 use turbo_tasks_backend::{BackendOptions, TurboTasksBackend, noop_backing_storage};
654 use turbo_tasks_fs::{File, FileContent, FileSystem, VirtualFileSystem};
655
656 use super::{dedup_chunk_groups, entry_group_modules};
657 use crate::{
658 asset::AssetContent, module::Module, module_graph::chunk_group_info::ChunkGroup,
659 raw_module::RawModule, source::Source, virtual_source::VirtualSource,
660 };
661
662 async fn test_module(name: &str) -> Result<ResolvedVc<Box<dyn Module>>> {
663 let root = VirtualFileSystem::new_with_name(rcstr!("chunk-group-test"))
664 .root()
665 .await?;
666 let source = Vc::upcast::<Box<dyn Source>>(VirtualSource::new(
667 root.join(name)?,
668 AssetContent::file(FileContent::Content(File::from("")).cell()),
669 ));
670 Vc::upcast::<Box<dyn Module>>(RawModule::new(source))
671 .to_resolved()
672 .await
673 }
674
675 #[turbo_tasks::function(operation, root)]
676 async fn combines_chunk_group_lists_operation() -> Result<Vc<Completion>> {
677 let error = dedup_chunk_groups(smallvec![]).unwrap_err().to_string();
678 assert_eq!(error, "Cannot chunk an empty list of chunk groups");
679
680 let a = test_module("a.js").await?;
681 let b = test_module("b.js").await?;
682 let c = test_module("c.js").await?;
683
684 let shared_a = ChunkGroup::Shared(a);
685 let shared_b = ChunkGroup::Shared(b);
686 let groups = dedup_chunk_groups(smallvec![
687 shared_a.clone(),
688 shared_b.clone(),
689 shared_a.clone()
690 ])?;
691 assert_eq!(
692 groups.iter().map(ChunkGroup::key).collect::<Vec<_>>(),
693 vec![shared_a.key(), shared_b.key()]
694 );
695
696 assert_eq!(entry_group_modules(&[ChunkGroup::Shared(a)]), None);
697 assert_eq!(
698 entry_group_modules(&[
699 ChunkGroup::Entry(vec![a, b]),
700 ChunkGroup::Shared(c),
701 ChunkGroup::Entry(vec![b, c]),
702 ]),
703 Some(vec![a, b, c])
704 );
705
706 Ok(Completion::new())
707 }
708
709 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
710 async fn combines_chunk_group_lists() {
711 let tt = turbo_tasks::TurboTasks::new(TurboTasksBackend::new(
712 BackendOptions::default(),
713 noop_backing_storage(),
714 ));
715 tt.run_once(async {
716 combines_chunk_group_lists_operation()
717 .read_strongly_consistent()
718 .await?;
719 Ok(())
720 })
721 .await
722 .unwrap();
723 }
724}