turbopack_core/module_graph/
merged_modules.rs

1use 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    /// A map of modules to the merged module containing the module plus additional modules.
26    #[allow(clippy::type_complexity)]
27    pub replacements: FxHashMap<ResolvedVc<Box<dyn Module>>, ResolvedVc<Box<dyn ChunkableModule>>>,
28    /// A map of replacement modules to their corresponding chunk group info (which is the same as
29    /// the chunk group info of the original module it replaced).
30    #[allow(clippy::type_complexity)]
31    pub replacements_to_original:
32        FxHashMap<ResolvedVc<Box<dyn Module>>, ResolvedVc<Box<dyn Module>>>,
33    /// A map of modules that are already contained as values in replacements.
34    pub included: FxHashSet<ResolvedVc<Box<dyn Module>>>,
35}
36
37impl MergedModuleInfo {
38    /// Whether the given module should be replaced with a merged module.
39    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    /// Returns the original module for the given replacement module (useful for retrieving the
47    /// chunk group info).
48    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    // Whether the given module should be skipped during chunking, as it is already included in a
56    // module returned by some `should_replace_module` call.
57    pub fn should_create_chunk_item_for(&self, module: ResolvedVc<Box<dyn Module>>) -> bool {
58        !self.included.contains(&module)
59    }
60}
61
62/// Determine which modules can be merged together:
63/// - if all chunks execute a sequence of modules in the same order, they can be merged together and
64///   treated as one.
65/// - if a merged module has an incoming edge not contained in the group, it has to expose its
66///   exports into the module cache.
67pub 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        // Use all entries from all graphs
87        let entries = graphs
88            .iter()
89            .flat_map(|g| g.entries.iter())
90            .flat_map(|g| g.entries())
91            .collect::<Vec<_>>();
92
93        // For each module, the indices in the bitmap store which merge group entry modules
94        // transitively import that module. The bitmap can be treated as an opaque value, merging
95        // all modules with the same bitmap.
96        let mut module_merged_groups: FxHashMap<ResolvedVc<Box<dyn Module>>, RoaringBitmapWrapper> =
97            FxHashMap::with_capacity_and_hasher(module_count, Default::default());
98        // Entries that started a new merge group for some deopt reason
99        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                    // On the down traversal, establish which edges are mergeable and set the list
130                    // indices.
131                    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                        // A self-reference
145                        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                        // ^ TODO technically we could merge a sync child into an async parent
154
155                        // A hoisted reference from a mergeable module to a non-async mergeable
156                        // module, inherit bitmaps from parent.
157                        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                            // All modules are inserted in the previous iteration
162                            bail!("unreachable except for eventual consistency");
163                        };
164
165                        if current_merged_groups.is_empty() {
166                            // Initial visit, clone instead of merging
167                            *current_merged_groups = parent_merged_groups.clone();
168                            GraphTraversalAction::Continue
169                        } else if parent_merged_groups.is_proper_superset(current_merged_groups) {
170                            // Add bits from parent, and continue traversal because changed
171                            **current_merged_groups |= &**parent_merged_groups;
172                            GraphTraversalAction::Continue
173                        } else {
174                            // Unchanged, no need to forward to children
175                            GraphTraversalAction::Skip
176                        }
177                    } else {
178                        // Either a non-hoisted reference or an incompatible parent or child module
179
180                        if entry_modules.insert(module) {
181                            // Not assigned a new group before, create a new one.
182                            let idx = next_index;
183                            next_index += 1;
184
185                            if module_merged_groups.entry(module).or_default().insert(idx) {
186                                // Mark and continue traversal because modified (or first visit)
187                                GraphTraversalAction::Continue
188                            } else {
189                                // Unchanged, no need to forward to children
190                                GraphTraversalAction::Skip
191                            }
192                        } else {
193                            // Already visited and assigned a new group, no need to forward to
194                            // children.
195                            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            // The field order here is important, these structs will get ordered by the entry
208            // index.
209            entry: usize,
210            list: usize,
211        }
212
213        // A list of all different execution traces (orderings) of all modules, initially a union of
214        // the partition of each chunk's modules (one for each ESM subtree in each chunks), but
215        // further split up later on.
216        let mut lists = vec![];
217        let mut lists_reverse_indices: FxIndexMap<
218            ResolvedVc<Box<dyn MergeableModule>>,
219            FxIndexSet<ListOccurrence>,
220        > = FxIndexMap::default();
221
222        // A map of all references between modules with the same bitmap. These are all references,
223        // including reexecution edges and cycles. Used to expose additional modules if the
224        // bitmap-groups are split up further.
225        #[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        // A map of all references between modules with the same bitmap. These are only the
231        // references relevant for execution (ignoring cycles), to find the entries of a group.
232        #[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        // TODO try to parallelize this loop somehow
239        for chunk_group in &chunk_group_info.chunk_groups {
240            // A partition of all modules in the chunk into several execution traces (orderings),
241            // stored in the top-level lists and referenced here by index.
242            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            // This is necessary to have the correct order with cycles: a `a -> b -> a` graph would
249            // otherwise be visited as `b->a`, `a->b`, leading to the list `a, b` which is not
250            // execution order.
251            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                                    // New list, insert the module
279                                    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        // We use list.pop() below, so reverse order using negation
323        lists_reverse_indices
324            .sort_by_cached_key(|_, b| b.iter().map(|o| o.entry).min().map(|v| -(v as i64)));
325
326        // Modules that are referenced from outside the group, so their exports need to be exposed.
327        // Initially these are set based on the bitmaps (and namespace imports), but more modules
328        // might need to be exposed if the lists are split up further below.
329        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                        // This module needs to be exposed:
359                        // - referenced from another group or
360                        // - an entry module (TODO assume it will be required for Node/Edge, but not
361                        // necessarily needed for browser),
362                        exposed_modules_imported.insert(module);
363                    }
364                    if parent_info.is_some_and(|(_, r)| matches!(r.export, ExportUsage::All)) {
365                        // This module needs to be exposed:
366                        // - namespace import from another group
367                        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                // Module exists only in one list, no need to split
377                continue;
378            }
379            // The module occurs in multiple lists, which need to split up so that there is exactly
380            // one list containing the module.
381
382            let first_occurrence = &common_occurrences[0];
383
384            // Find the longest common sequence in the lists, starting from the given module.
385            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                // Went one too far, the common length is what the previous iteration verified
402                common_length -= 1;
403                break;
404            }
405
406            // Split into three lists:
407            // - "common" [occurrence.entry .. occurrence.entry + common_length) -- same for all
408            // - "before" [0 .. occurrence.entry)
409            // - "after"  [occurrence.entry + common_length .. ]
410            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            // Insert occurrences for the "common" list, skip the first because that is now
418            // guaranteed to exist only once
419            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                // For all previously merged references (intra_group_references) that now cross
441                // "before", "common" and "after", mark the referenced modules as
442                // exposed.
443                // Note that due to circular dependencies, there can be
444                // references that go against execution order (e.g. from "before" to
445                // "common").
446                {
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                // The occurrences for the "before" list (`list`) are still valid, need to update
490                // the occurrences for the "after" list
491                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        // Dedupe the lists
515        let lists = lists.into_iter().collect::<FxHashSet<_>>();
516
517        // Call MergeableModule impl to merge the modules.
518        let result = lists
519            .into_iter()
520            .map(async |list| {
521                if list.len() < 2 {
522                    // Nothing to merge
523                    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}