turbopack_core/module_graph/
merged_modules.rs1use std::collections::hash_map::Entry;
2
3use anyhow::{Context, Result, bail};
4use rustc_hash::{FxHashMap, FxHashSet};
5use tracing::Instrument;
6use turbo_tasks::{
7 FxIndexMap, FxIndexSet, ResolvedVc, TryFlatJoinIterExt, TryJoinIterExt, ValueToString, Vc,
8};
9
10use crate::{
11 chunk::{
12 ChunkableModule, ChunkingType, MergeableModule, MergeableModuleExposure, MergeableModules,
13 MergeableModulesExposed,
14 },
15 module::Module,
16 module_graph::{
17 GraphTraversalAction, ModuleGraph, RefData, SingleModuleGraphModuleNode,
18 chunk_group_info::RoaringBitmapWrapper,
19 },
20 resolve::ExportUsage,
21};
22
23#[turbo_tasks::value]
24pub struct MergedModuleInfo {
25 #[allow(clippy::type_complexity)]
27 pub replacements: FxHashMap<ResolvedVc<Box<dyn Module>>, ResolvedVc<Box<dyn ChunkableModule>>>,
28 #[allow(clippy::type_complexity)]
31 pub replacements_to_original:
32 FxHashMap<ResolvedVc<Box<dyn Module>>, ResolvedVc<Box<dyn Module>>>,
33 pub included: FxHashSet<ResolvedVc<Box<dyn Module>>>,
35}
36
37impl MergedModuleInfo {
38 pub fn should_replace_module(
40 &self,
41 module: ResolvedVc<Box<dyn Module>>,
42 ) -> Option<ResolvedVc<Box<dyn ChunkableModule>>> {
43 self.replacements.get(&module).copied()
44 }
45
46 pub fn get_original_module(
49 &self,
50 module: ResolvedVc<Box<dyn Module>>,
51 ) -> Option<ResolvedVc<Box<dyn Module>>> {
52 self.replacements_to_original.get(&module).copied()
53 }
54
55 pub fn should_create_chunk_item_for(&self, module: ResolvedVc<Box<dyn Module>>) -> bool {
58 !self.included.contains(&module)
59 }
60}
61
62pub async fn compute_merged_modules(module_graph: Vc<ModuleGraph>) -> Result<Vc<MergedModuleInfo>> {
68 let span_outer = tracing::info_span!(
69 "compute merged modules",
70 module_count = tracing::field::Empty,
71 visit_count = tracing::field::Empty,
72 merged_groups = tracing::field::Empty,
73 included_modules = tracing::field::Empty
74 );
75
76 let span = span_outer.clone();
77 async move {
78 let async_module_info = module_graph.async_module_info().await?;
79 let chunk_group_info = module_graph.chunk_group_info().await?;
80 let module_graph = module_graph.await?;
81
82 let graphs = module_graph.graphs.iter().try_join().await?;
83 let module_count = graphs.iter().map(|g| g.graph.node_count()).sum::<usize>();
84 span.record("module_count", module_count);
85
86 let entries = graphs
88 .iter()
89 .flat_map(|g| g.entries.iter())
90 .flat_map(|g| g.entries())
91 .collect::<Vec<_>>();
92
93 let mut module_merged_groups: FxHashMap<ResolvedVc<Box<dyn Module>>, RoaringBitmapWrapper> =
97 FxHashMap::with_capacity_and_hasher(module_count, Default::default());
98 let mut entry_modules =
100 FxHashSet::with_capacity_and_hasher(module_count, Default::default());
101
102 let mergeable = graphs
103 .iter()
104 .flat_map(|g| g.iter_nodes())
105 .map(async |n| {
106 let module = n.module;
107 if let Some(mergeable) =
108 ResolvedVc::try_downcast::<Box<dyn MergeableModule>>(module)
109 && *mergeable.is_mergeable().await?
110 {
111 return Ok(Some(module));
112 }
113 Ok(None)
114 })
115 .try_flat_join()
116 .await?
117 .into_iter()
118 .collect::<FxHashSet<_>>();
119
120 let mut next_index = 0u32;
121 let visit_count = module_graph
122 .traverse_edges_fixed_point_with_priority(
123 entries.iter().map(|e| (*e, 0)),
124 &mut (),
125 |parent_info: Option<(&'_ SingleModuleGraphModuleNode, &'_ RefData)>,
126 node: &'_ SingleModuleGraphModuleNode,
127 _|
128 -> Result<GraphTraversalAction> {
129 let (parent_module, hoisted) =
132 parent_info.map_or((None, false), |(node, ty)| {
133 (
134 Some(node.module),
135 match &ty.chunking_type {
136 ChunkingType::Parallel { hoisted, .. } => *hoisted,
137 _ => false,
138 },
139 )
140 });
141 let module = node.module;
142
143 Ok(if parent_module.is_some_and(|p| p == module) {
144 GraphTraversalAction::Skip
146 } else if hoisted
147 && let Some(parent_module) = parent_module
148 && mergeable.contains(&parent_module)
149 && mergeable.contains(&module)
150 && !async_module_info.contains(&parent_module)
151 && !async_module_info.contains(&module)
152 {
153 module_merged_groups.entry(node.module).or_default();
158 let [Some(parent_merged_groups), Some(current_merged_groups)] =
159 module_merged_groups.get_disjoint_mut([&parent_module, &node.module])
160 else {
161 bail!("unreachable except for eventual consistency");
163 };
164
165 if current_merged_groups.is_empty() {
166 *current_merged_groups = parent_merged_groups.clone();
168 GraphTraversalAction::Continue
169 } else if parent_merged_groups.is_proper_superset(current_merged_groups) {
170 **current_merged_groups |= &**parent_merged_groups;
172 GraphTraversalAction::Continue
173 } else {
174 GraphTraversalAction::Skip
176 }
177 } else {
178 if entry_modules.insert(module) {
181 let idx = next_index;
183 next_index += 1;
184
185 if module_merged_groups.entry(module).or_default().insert(idx) {
186 GraphTraversalAction::Continue
188 } else {
189 GraphTraversalAction::Skip
191 }
192 } else {
193 GraphTraversalAction::Skip
196 }
197 })
198 },
199 |_, _| Ok(0),
200 )
201 .await?;
202
203 span.record("visit_count", visit_count);
204
205 #[derive(Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
206 struct ListOccurrence {
207 entry: usize,
210 list: usize,
211 }
212
213 let mut lists = vec![];
217 let mut lists_reverse_indices: FxIndexMap<
218 ResolvedVc<Box<dyn MergeableModule>>,
219 FxIndexSet<ListOccurrence>,
220 > = FxIndexMap::default();
221
222 #[allow(clippy::type_complexity)]
226 let mut intra_group_references: FxIndexMap<
227 ResolvedVc<Box<dyn Module>>,
228 FxIndexSet<ResolvedVc<Box<dyn Module>>>,
229 > = FxIndexMap::default();
230 #[allow(clippy::type_complexity)]
233 let mut intra_group_references_rev: FxIndexMap<
234 ResolvedVc<Box<dyn Module>>,
235 FxIndexSet<ResolvedVc<Box<dyn Module>>>,
236 > = FxIndexMap::default();
237
238 for chunk_group in &chunk_group_info.chunk_groups {
240 let mut chunk_lists: FxHashMap<&RoaringBitmapWrapper, usize> =
243 FxHashMap::with_capacity_and_hasher(
244 module_merged_groups.len() / chunk_group_info.chunk_groups.len(),
245 Default::default(),
246 );
247
248 let mut visited = FxHashSet::default();
252
253 module_graph
254 .traverse_edges_from_entries_topological(
255 chunk_group.entries(),
256 &mut (),
257 |parent_info, node, _| {
258 if parent_info.is_none_or(|(_, r)| r.chunking_type.is_parallel())
259 && visited.insert(node.module)
260 {
261 Ok(GraphTraversalAction::Continue)
262 } else {
263 Ok(GraphTraversalAction::Exclude)
264 }
265 },
266 |parent_info, node, _| {
267 let module = node.module;
268 let bitmap = module_merged_groups
269 .get(&module)
270 .context("every module should have a bitmap at this point")?;
271
272 if mergeable.contains(&module) {
273 let mergeable_module =
274 ResolvedVc::try_downcast::<Box<dyn MergeableModule>>(module)
275 .unwrap();
276 match chunk_lists.entry(bitmap) {
277 Entry::Vacant(e) => {
278 let idx = lists.len();
280 e.insert(idx);
281 lists.push(vec![mergeable_module]);
282 lists_reverse_indices
283 .entry(mergeable_module)
284 .or_default()
285 .insert(ListOccurrence {
286 list: idx,
287 entry: 0,
288 });
289 }
290 Entry::Occupied(e) => {
291 let list_idx = *e.get();
292 let list = &mut lists[list_idx];
293 list.push(mergeable_module);
294 lists_reverse_indices
295 .entry(mergeable_module)
296 .or_default()
297 .insert(ListOccurrence {
298 list: list_idx,
299 entry: list.len() - 1,
300 });
301 }
302 }
303 }
304
305 if let Some((parent, _)) = parent_info {
306 let same_bitmap = module_merged_groups.get(&parent.module).unwrap()
307 == module_merged_groups.get(&module).unwrap();
308
309 if same_bitmap {
310 intra_group_references_rev
311 .entry(module)
312 .or_default()
313 .insert(parent.module);
314 }
315 }
316 Ok(())
317 },
318 )
319 .await?;
320 }
321
322 lists_reverse_indices
324 .sort_by_cached_key(|_, b| b.iter().map(|o| o.entry).min().map(|v| -(v as i64)));
325
326 let mut exposed_modules_imported: FxHashSet<ResolvedVc<Box<dyn Module>>> =
330 FxHashSet::with_capacity_and_hasher(module_merged_groups.len(), Default::default());
331 let mut exposed_modules_namespace: FxHashSet<ResolvedVc<Box<dyn Module>>> =
332 FxHashSet::with_capacity_and_hasher(module_merged_groups.len(), Default::default());
333
334 module_graph
335 .traverse_edges_from_entries_topological(
336 entries,
337 &mut (),
338 |_, _, _| Ok(GraphTraversalAction::Continue),
339 |parent_info, node, _| {
340 let module = node.module;
341
342 if let Some((parent, _)) = parent_info {
343 let same_bitmap = module_merged_groups.get(&parent.module).unwrap()
344 == module_merged_groups.get(&module).unwrap();
345
346 if same_bitmap {
347 intra_group_references
348 .entry(parent.module)
349 .or_default()
350 .insert(module);
351 }
352 }
353
354 if parent_info.is_none_or(|(parent, _)| {
355 module_merged_groups.get(&parent.module).unwrap()
356 != module_merged_groups.get(&module).unwrap()
357 }) {
358 exposed_modules_imported.insert(module);
363 }
364 if parent_info.is_some_and(|(_, r)| matches!(r.export, ExportUsage::All)) {
365 exposed_modules_namespace.insert(module);
368 }
369 Ok(())
370 },
371 )
372 .await?;
373
374 while let Some((_, common_occurrences)) = lists_reverse_indices.pop() {
375 if common_occurrences.len() < 2 {
376 continue;
378 }
379 let first_occurrence = &common_occurrences[0];
383
384 let mut common_length = 2;
386 loop {
387 let m =
388 lists[first_occurrence.list].get(first_occurrence.entry + common_length - 1);
389 if m.is_some()
390 && common_occurrences
391 .iter()
392 .skip(1)
393 .all(|ListOccurrence { list, entry }| {
394 lists[*list].get(*entry + common_length - 1) == m
395 })
396 {
397 common_length += 1;
398 continue;
399 }
400
401 common_length -= 1;
403 break;
404 }
405
406 let common_list = lists[first_occurrence.list]
411 [first_occurrence.entry..first_occurrence.entry + common_length]
412 .to_vec();
413
414 let common_list_index = lists.len();
415 lists.push(common_list.clone());
416
417 for (i, &m) in common_list.iter().enumerate().skip(1) {
420 let occurrences = lists_reverse_indices.get_mut(&m).unwrap();
421 for common_occurrence in &common_occurrences {
422 let removed = occurrences.swap_remove(&ListOccurrence {
423 list: common_occurrence.list,
424 entry: common_occurrence.entry + i,
425 });
426 debug_assert!(removed);
427 }
428 occurrences.insert(ListOccurrence {
429 list: common_list_index,
430 entry: i,
431 });
432 }
433
434 for common_occurrence in &common_occurrences {
435 let list = &mut lists[common_occurrence.list];
436 let after_list = list.split_off(common_occurrence.entry + common_length);
437 list.truncate(common_occurrence.entry);
438 let before_list = &*list;
439
440 {
447 let before_list =
448 FxHashSet::from_iter(before_list.iter().map(|m| ResolvedVc::upcast(*m)));
449 let common_list =
450 FxHashSet::from_iter(common_list.iter().map(|m| ResolvedVc::upcast(*m)));
451 let after_list =
452 FxHashSet::from_iter(after_list.iter().map(|m| ResolvedVc::upcast(*m)));
453
454 let references_from_before = before_list
455 .iter()
456 .filter_map(|m| intra_group_references.get(m))
457 .flatten()
458 .copied()
459 .filter(|m| common_list.contains(m) || after_list.contains(m))
460 .collect::<FxHashSet<_>>();
461 let references_from_common = common_list
462 .iter()
463 .filter_map(|m| intra_group_references.get(m))
464 .flatten()
465 .filter(|m| before_list.contains(m) || after_list.contains(m))
466 .collect::<FxHashSet<_>>();
467 let references_from_after = after_list
468 .iter()
469 .filter_map(|m| intra_group_references.get(m))
470 .flatten()
471 .copied()
472 .filter(|m| before_list.contains(m) || common_list.contains(m))
473 .collect::<FxHashSet<_>>();
474
475 let modules_to_expose = before_list
476 .iter()
477 .chain(common_list.iter())
478 .chain(after_list.iter())
479 .copied()
480 .filter(|m| {
481 references_from_before.contains(m)
482 || references_from_common.contains(m)
483 || references_from_after.contains(m)
484 });
485
486 exposed_modules_imported.extend(modules_to_expose);
487 }
488
489 if !after_list.is_empty() {
492 let after_index = lists.len();
493 lists.push(after_list.clone());
494 for (i, &m) in after_list.iter().enumerate() {
495 let occurrences = lists_reverse_indices
496 .get_mut(&m)
497 .context(format!("{:?}", m.ident().to_string().await?))?;
498
499 let removed = occurrences.swap_remove(&ListOccurrence {
500 list: common_occurrence.list,
501 entry: common_occurrence.entry + common_length + i,
502 });
503 debug_assert!(removed);
504
505 occurrences.insert(ListOccurrence {
506 list: after_index,
507 entry: i,
508 });
509 }
510 }
511 }
512 }
513
514 let lists = lists.into_iter().collect::<FxHashSet<_>>();
516
517 let result = lists
519 .into_iter()
520 .map(async |list| {
521 if list.len() < 2 {
522 return Ok(None);
524 }
525
526 let list_set = list
527 .iter()
528 .map(|&m| ResolvedVc::upcast::<Box<dyn Module>>(m))
529 .collect::<FxIndexSet<_>>();
530
531 let entry_points = list
532 .iter()
533 .filter(|m| {
534 intra_group_references_rev
535 .get(&ResolvedVc::upcast(**m))
536 .is_none_or(|refs| refs.is_disjoint(&list_set))
537 })
538 .map(|m| **m)
539 .collect::<Vec<_>>();
540 debug_assert_ne!(entry_points.len(), 0);
541
542 let list_exposed = list
543 .iter()
544 .map(|&m| {
545 (
546 m,
547 if exposed_modules_imported.contains(&ResolvedVc::upcast(m)) {
548 MergeableModuleExposure::External
549 } else if exposed_modules_namespace.contains(&ResolvedVc::upcast(m)) {
550 MergeableModuleExposure::Internal
551 } else {
552 MergeableModuleExposure::None
553 },
554 )
555 })
556 .collect::<Vec<_>>();
557
558 let entry = *list.last().unwrap();
559 let result = entry
560 .merge(
561 MergeableModulesExposed::interned(list_exposed),
562 MergeableModules::interned(entry_points),
563 )
564 .to_resolved()
565 .await?;
566
567 let list_len = list.len();
568 Ok(Some((
569 ResolvedVc::upcast::<Box<dyn Module>>(entry),
570 result,
571 list.into_iter()
572 .take(list_len - 1)
573 .map(ResolvedVc::upcast::<Box<dyn Module>>)
574 .collect::<Vec<_>>(),
575 )))
576 })
577 .try_join()
578 .await?;
579
580 #[allow(clippy::type_complexity)]
581 let mut replacements: FxHashMap<
582 ResolvedVc<Box<dyn Module>>,
583 ResolvedVc<Box<dyn ChunkableModule>>,
584 > = Default::default();
585 #[allow(clippy::type_complexity)]
586 let mut replacements_to_original: FxHashMap<
587 ResolvedVc<Box<dyn Module>>,
588 ResolvedVc<Box<dyn Module>>,
589 > = Default::default();
590 let mut included: FxHashSet<ResolvedVc<Box<dyn Module>>> = FxHashSet::default();
591
592 for (original, replacement, replacement_included) in result.into_iter().flatten() {
593 replacements.insert(original, replacement);
594 replacements_to_original.insert(ResolvedVc::upcast(replacement), original);
595 included.extend(replacement_included);
596 }
597
598 span.record("merged_groups", replacements.len());
599 span.record("included_modules", included.len());
600
601 Ok(MergedModuleInfo {
602 replacements,
603 replacements_to_original,
604 included,
605 }
606 .cell())
607 }
608 .instrument(span_outer)
609 .await
610}