Skip to main content

turbopack_core/module_graph/
merged_modules.rs

1use std::{cmp::Reverse, collections::hash_map::Entry, hash::Hash};
2
3use anyhow::{Context, Result, bail};
4use rustc_hash::{FxHashMap, FxHashSet};
5use tracing::Instrument;
6use turbo_tasks::{FxIndexMap, FxIndexSet, ResolvedVc, TryFlatJoinIterExt, TryJoinIterExt, Vc};
7
8use crate::{
9    chunk::{
10        ChunkableModule, ChunkingType, MergeableModule, MergeableModuleExposure, MergeableModules,
11        MergeableModulesExposed,
12    },
13    module::Module,
14    module_graph::{
15        GraphTraversalAction, ModuleGraph, RefData, chunk_group_info::RoaringBitmapWrapper,
16    },
17    resolve::ExportUsage,
18};
19
20#[turbo_tasks::value(transparent, cell = "keyed")]
21#[allow(clippy::type_complexity)]
22pub struct MergedModulesReplacements(
23    FxHashMap<ResolvedVc<Box<dyn Module>>, Option<ResolvedVc<Box<dyn ChunkableModule>>>>,
24);
25
26#[turbo_tasks::value(transparent, cell = "keyed")]
27#[allow(clippy::type_complexity)]
28pub struct MergedModulesOriginalModules(
29    FxHashMap<ResolvedVc<Box<dyn Module>>, ResolvedVc<Box<dyn Module>>>,
30);
31
32#[turbo_tasks::value]
33pub struct MergedModuleInfo {
34    /// A map of modules describing how they participate in module merging:
35    /// - Not present: the module is not affected by merging and a regular chunk item should be
36    ///   created for it.
37    /// - Present with `Some(replacement)`: the module should be replaced with the given merged
38    ///   module when creating a chunk item.
39    /// - Present with `None`: the module is already included in some other merged module returned
40    ///   by a `Some(replacement)` entry and no chunk item should be created for it.
41    pub replacements: ResolvedVc<MergedModulesReplacements>,
42    /// A map of replacement modules to their corresponding chunk group info (which is the same as
43    /// the chunk group info of the original module it replaced).
44    pub replacements_to_original: ResolvedVc<MergedModulesOriginalModules>,
45}
46
47impl MergedModuleInfo {
48    /// Returns the merging decision for the given module:
49    /// - `None`: the module is not affected by merging, keep it as-is.
50    /// - `Some(None)`: the module is already included in another merged module, skip it.
51    /// - `Some(Some(replacement))`: the module should be replaced with `replacement`.
52    pub async fn should_replace_module(
53        &self,
54        module: ResolvedVc<Box<dyn Module>>,
55    ) -> Result<Option<Option<ResolvedVc<Box<dyn ChunkableModule>>>>> {
56        Ok(self.replacements.get(&module).await?.as_deref().copied())
57    }
58
59    /// Returns the original module for the given replacement module (useful for retrieving the
60    /// chunk group info).
61    pub async fn get_original_module(
62        &self,
63        module: ResolvedVc<Box<dyn Module>>,
64    ) -> Result<Option<ResolvedVc<Box<dyn Module>>>> {
65        Ok(self
66            .replacements_to_original
67            .get(&module)
68            .await?
69            .as_deref()
70            .copied())
71    }
72}
73
74/// Assigns each distinct bitmap a dense id, starting at 0, so that callers can hash and compare a
75/// `u32` instead of a whole bitmap (hashing a [`RoaringBitmapWrapper`] serializes it). Equal
76/// bitmaps get the same id. Returns the id of every key and the number of distinct bitmaps.
77fn intern_bitmaps<'a, K: Eq + Hash>(
78    entries: impl IntoIterator<Item = (K, &'a RoaringBitmapWrapper)>,
79    capacity: usize,
80) -> (FxHashMap<K, u32>, usize) {
81    let mut ids: FxHashMap<&'a RoaringBitmapWrapper, u32> = FxHashMap::default();
82    let mut result = FxHashMap::with_capacity_and_hasher(capacity, Default::default());
83    for (key, bitmap) in entries {
84        let next_id = ids.len() as u32;
85        result.insert(key, *ids.entry(bitmap).or_insert(next_id));
86    }
87    (result, ids.len())
88}
89
90/// Determine which modules can be merged together:
91/// - if all chunks execute a sequence of modules in the same order, they can be merged together and
92///   treated as one.
93/// - if a merged module has an incoming edge not contained in the group, it has to expose its
94///   exports into the module cache.
95pub async fn compute_merged_modules(module_graph: Vc<ModuleGraph>) -> Result<Vc<MergedModuleInfo>> {
96    let span_outer = tracing::info_span!(
97        "compute merged modules",
98        module_count = tracing::field::Empty,
99        visit_count = tracing::field::Empty,
100        merged_groups = tracing::field::Empty,
101        included_modules = tracing::field::Empty
102    );
103
104    let span = span_outer.clone();
105    async move {
106        let async_module_info = module_graph.async_module_info();
107        let chunk_group_info = module_graph.chunk_group_info().await?;
108        let module_graph = module_graph.await?;
109
110        let graphs = &module_graph.graphs;
111        let module_count = graphs.iter().map(|g| g.graph.node_count()).sum::<usize>();
112        span.record("module_count", module_count);
113
114        // Use all entries from all graphs
115        let entries = module_graph
116            .all_chunk_group_entry_modules()
117            .collect::<Vec<_>>();
118
119        // First, rank every module in reverse postorder, so that a module is visited after the
120        // modules importing it.
121        let module_rank = {
122            let _inner_span = tracing::info_span!("compute reverse postorder").entered();
123            module_graph.reverse_postorder_ranks(entries.iter().copied())?
124        };
125
126        // For each module, the indices in the bitmap store which merge group entry modules
127        // transitively import that module. The bitmap can be treated as an opaque value, merging
128        // all modules with the same bitmap.
129        let mut module_merged_groups: FxHashMap<ResolvedVc<Box<dyn Module>>, RoaringBitmapWrapper> =
130            FxHashMap::with_capacity_and_hasher(module_count, Default::default());
131        // Entries that started a new merge group for some deopt reason
132        let mut entry_modules =
133            FxHashSet::with_capacity_and_hasher(module_count, Default::default());
134
135        let inner_span = tracing::info_span!("collect mergeable modules");
136        let mergeable = module_graph
137            .iter_reachable_modules()?
138            .map(async |module| {
139                if let Some(mergeable) =
140                    ResolvedVc::try_downcast::<Box<dyn MergeableModule>>(module)
141                    && *mergeable.is_mergeable().await?
142                {
143                    return Ok(Some(module));
144                }
145                Ok(None)
146            })
147            .try_flat_join()
148            .instrument(inner_span)
149            .await?
150            .into_iter()
151            .collect::<FxHashSet<_>>();
152
153        // Pre-fetch async status for all mergeable modules using keyed access to avoid
154        // reading the full AsyncModulesInfo set during the synchronous traversal below.
155        let inner_span = tracing::info_span!("pre-fetch async module status");
156        let async_modules: FxHashSet<_> = mergeable
157            .iter()
158            .map(async |&module| Ok(async_module_info.is_async(module).await?.then_some(module)))
159            .try_flat_join()
160            .instrument(inner_span)
161            .await?
162            .into_iter()
163            .collect();
164
165        let inner_span = tracing::info_span!("fixed point traversal").entered();
166
167        let mut next_index = 0u32;
168        let visit_count = module_graph.traverse_edges_fixed_point_with_priority(
169            entries
170                .iter()
171                .map(|e| {
172                    Ok((
173                        *e,
174                        Reverse(*module_rank.get(e).context("Module rank not found")?),
175                    ))
176                })
177                .collect::<Result<Vec<_>>>()?,
178            &mut (),
179            |parent_info: Option<(ResolvedVc<Box<dyn Module>>, &'_ RefData, _)>,
180             node: ResolvedVc<Box<dyn Module>>,
181             _,
182             _|
183             -> Result<GraphTraversalAction> {
184                // On the down traversal, establish which edges are mergeable and set the list
185                // indices.
186                let (parent_module, hoisted) =
187                    parent_info.map_or((None, false), |(node, ty, _)| {
188                        (
189                            Some(node),
190                            match &ty.chunking_type {
191                                ChunkingType::Parallel { hoisted, .. } => *hoisted,
192                                _ => false,
193                            },
194                        )
195                    });
196                let module = node;
197
198                Ok(if parent_module.is_some_and(|p| p == module) {
199                    // A self-reference
200                    GraphTraversalAction::Skip
201                } else if hoisted
202                    && let Some(parent_module) = parent_module
203                    && mergeable.contains(&parent_module)
204                    && mergeable.contains(&module)
205                    && !async_modules.contains(&parent_module)
206                    && !async_modules.contains(&module)
207                {
208                    // ^ TODO technically we could merge a sync child into an async parent
209
210                    // A hoisted reference from a mergeable module to a non-async mergeable
211                    // module, inherit bitmaps from parent.
212                    module_merged_groups.entry(node).or_default();
213                    let [Some(parent_merged_groups), Some(current_merged_groups)] =
214                        module_merged_groups.get_disjoint_mut([&parent_module, &node])
215                    else {
216                        // All modules are inserted in the previous iteration
217                        bail!("unreachable except for eventual consistency");
218                    };
219
220                    if current_merged_groups.is_empty() {
221                        // Initial visit, clone instead of merging
222                        *current_merged_groups = parent_merged_groups.clone();
223                        GraphTraversalAction::Continue
224                    } else if parent_merged_groups.is_proper_superset(current_merged_groups) {
225                        // Add bits from parent, and continue traversal because changed
226                        **current_merged_groups |= &**parent_merged_groups;
227                        GraphTraversalAction::Continue
228                    } else {
229                        // Unchanged, no need to forward to children
230                        GraphTraversalAction::Skip
231                    }
232                } else {
233                    // Either a non-hoisted reference or an incompatible parent or child module
234
235                    if entry_modules.insert(module) {
236                        // Not assigned a new group before, create a new one.
237                        let idx = next_index;
238                        next_index += 1;
239
240                        if module_merged_groups.entry(module).or_default().insert(idx) {
241                            // Mark and continue traversal because modified (or first visit)
242                            GraphTraversalAction::Continue
243                        } else {
244                            // Unchanged, no need to forward to children
245                            GraphTraversalAction::Skip
246                        }
247                    } else {
248                        // Already visited and assigned a new group, no need to forward to
249                        // children.
250                        GraphTraversalAction::Skip
251                    }
252                })
253            },
254            |successor, _| {
255                // Visit modules in reverse postorder (lowest rank first, the traversal visits the
256                // highest priority first), as we are propagating bitmaps downwards.
257                Ok(Reverse(
258                    *module_rank
259                        .get(&successor)
260                        .context("Module rank not found")?,
261                ))
262            },
263        )?;
264
265        drop(inner_span);
266
267        // The traversals below look up and compare the bitmaps of many modules (once per visit),
268        // so use a dense id per distinct bitmap instead of the bitmaps themselves.
269        let module_bitmap_ids = {
270            let _inner_span = tracing::info_span!("intern bitmaps").entered();
271            intern_bitmaps(
272                module_merged_groups
273                    .iter()
274                    .map(|(&module, bitmap)| (module, bitmap)),
275                module_merged_groups.len(),
276            )
277            .0
278        };
279        let bitmap_id = |module: ResolvedVc<Box<dyn Module>>| {
280            module_bitmap_ids
281                .get(&module)
282                .copied()
283                .context("every module should have a bitmap")
284        };
285
286        let inner_span = tracing::info_span!("chunk group collection").entered();
287
288        span.record("visit_count", visit_count);
289
290        #[derive(Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
291        struct ListOccurrence {
292            // The field order here is important, these structs will get ordered by the entry
293            // index.
294            entry: usize,
295            list: usize,
296            chunk_group: usize,
297        }
298
299        // A list of all different execution traces (orderings) of all modules, initially a union of
300        // the partition of each chunk's modules (one for each ESM subtree in each chunks), but
301        // further split up later on.
302        // This is a list (one per chunk group, initially) of lists (one per ESM subtree) of modules
303        let mut lists;
304        let mut lists_reverse_indices: FxIndexMap<
305            ResolvedVc<Box<dyn MergeableModule>>,
306            FxIndexSet<ListOccurrence>,
307        > = FxIndexMap::default();
308
309        // Once we do the reconciliation below, we need to insert new lists, but the lists are per
310        // chunk group, so we put them into this one.
311        #[allow(non_snake_case)]
312        let LISTS_COMMON_IDX: usize;
313
314        // A map of all references between modules with the same bitmap. These are all references,
315        // including reexecution edges and cycles. Used to expose additional modules if the
316        // bitmap-groups are split up further.
317        #[allow(clippy::type_complexity)]
318        let mut intra_group_references: FxIndexMap<
319            ResolvedVc<Box<dyn Module>>,
320            FxIndexSet<ResolvedVc<Box<dyn Module>>>,
321        > = FxIndexMap::default();
322        // A map of all references between modules with the same bitmap. These are only the
323        // references relevant for execution (ignoring cycles), to find the entries of a group.
324        #[allow(clippy::type_complexity)]
325        let mut intra_group_references_rev: FxIndexMap<
326            ResolvedVc<Box<dyn Module>>,
327            FxIndexSet<ResolvedVc<Box<dyn Module>>>,
328        > = FxIndexMap::default();
329
330        {
331            struct ChunkGroupResult {
332                first_chunk_group_idx: usize,
333                #[allow(clippy::type_complexity)]
334                list_lists: Vec<Vec<Vec<ResolvedVc<Box<dyn MergeableModule>>>>>,
335                lists_reverse_indices:
336                    FxIndexMap<ResolvedVc<Box<dyn MergeableModule>>, FxIndexSet<ListOccurrence>>,
337                #[allow(clippy::type_complexity)]
338                intra_group_references_rev: FxIndexMap<
339                    ResolvedVc<Box<dyn Module>>,
340                    FxIndexSet<ResolvedVc<Box<dyn Module>>>,
341                >,
342            }
343            let span = tracing::info_span!("map chunk groups").entered();
344
345            let result = turbo_tasks::parallel::map_collect_chunked_owned::<_, _, Result<Vec<_>>>(
346                // TODO without collect
347                chunk_group_info.chunk_groups.iter().enumerate().collect(),
348                |chunk| {
349                    let mut list_lists = vec![];
350                    let mut lists_reverse_indices: FxIndexMap<
351                        ResolvedVc<Box<dyn MergeableModule>>,
352                        FxIndexSet<ListOccurrence>,
353                    > = FxIndexMap::default();
354                    #[allow(clippy::type_complexity)]
355                    let mut intra_group_references_rev: FxIndexMap<
356                        ResolvedVc<Box<dyn Module>>,
357                        FxIndexSet<ResolvedVc<Box<dyn Module>>>,
358                    > = FxIndexMap::default();
359
360                    let mut chunk = chunk.peekable();
361                    let first_chunk_group_idx = chunk.peek().unwrap().0;
362
363                    for (chunk_group_idx, chunk_group) in chunk {
364                        let mut lists = vec![];
365
366                        // A partition of all modules in the chunk into several execution traces
367                        // (orderings), stored in the top-level lists and referenced here by
368                        // index.
369                        // Keyed by bitmap id.
370                        let mut chunk_lists: FxHashMap<u32, usize> =
371                            FxHashMap::with_capacity_and_hasher(
372                                module_merged_groups.len() / chunk_group_info.chunk_groups.len(),
373                                Default::default(),
374                            );
375
376                        // This is necessary to have the correct order with cycles: a `a -> b -> a`
377                        // graph would otherwise be visited as `b->a`, `a->b`,
378                        // leading to the list `a, b` which is not execution order.
379                        let mut visited = FxHashSet::default();
380
381                        module_graph.traverse_edges_dfs(
382                            chunk_group.entries(),
383                            &mut (),
384                            |parent_info, node, _| {
385                                if parent_info.is_none_or(|(_, r)| r.chunking_type.is_parallel())
386                                    && visited.insert(node)
387                                {
388                                    Ok(GraphTraversalAction::Continue)
389                                } else {
390                                    Ok(GraphTraversalAction::Exclude)
391                                }
392                            },
393                            |parent_info, node, _| {
394                                let module = node;
395                                let bitmap = bitmap_id(module)?;
396
397                                if mergeable.contains(&module) {
398                                    let mergeable_module =
399                                        ResolvedVc::try_downcast::<Box<dyn MergeableModule>>(
400                                            module,
401                                        )
402                                        .context(
403                                            "found mergeable module which is not a MergeableModule",
404                                        )?;
405                                    match chunk_lists.entry(bitmap) {
406                                        Entry::Vacant(e) => {
407                                            // New list, insert the module
408                                            let idx = lists.len();
409                                            e.insert(idx);
410                                            lists.push(vec![mergeable_module]);
411                                            lists_reverse_indices
412                                                .entry(mergeable_module)
413                                                .or_default()
414                                                .insert(ListOccurrence {
415                                                    chunk_group: chunk_group_idx,
416                                                    list: idx,
417                                                    entry: 0,
418                                                });
419                                        }
420                                        Entry::Occupied(e) => {
421                                            let list_idx = *e.get();
422                                            let list = &mut lists[list_idx];
423                                            list.push(mergeable_module);
424                                            lists_reverse_indices
425                                                .entry(mergeable_module)
426                                                .or_default()
427                                                .insert(ListOccurrence {
428                                                    chunk_group: chunk_group_idx,
429                                                    list: list_idx,
430                                                    entry: list.len() - 1,
431                                                });
432                                        }
433                                    }
434                                }
435
436                                if let Some((parent, _)) = parent_info {
437                                    let same_bitmap = bitmap_id(parent)? == bitmap;
438
439                                    if same_bitmap {
440                                        intra_group_references_rev
441                                            .entry(module)
442                                            .or_default()
443                                            .insert(parent);
444                                    }
445                                }
446                                Ok(())
447                            },
448                            false,
449                            false,
450                        )?;
451
452                        list_lists.push(lists);
453                    }
454                    Ok(ChunkGroupResult {
455                        first_chunk_group_idx,
456                        list_lists,
457                        lists_reverse_indices,
458                        intra_group_references_rev,
459                    })
460                },
461            )?;
462
463            drop(span);
464            let _span = tracing::info_span!("merging chunk group lists").entered();
465
466            lists_reverse_indices
467                .reserve_exact(result.iter().map(|r| r.lists_reverse_indices.len()).sum());
468            intra_group_references_rev.reserve_exact(
469                result
470                    .iter()
471                    .map(|r| r.intra_group_references_rev.len())
472                    .sum(),
473            );
474
475            lists = vec![Default::default(); chunk_group_info.chunk_groups.len() + 1];
476            LISTS_COMMON_IDX = result.len();
477            for ChunkGroupResult {
478                first_chunk_group_idx,
479                list_lists: result_lists,
480                lists_reverse_indices: result_lists_reverse_indices,
481                intra_group_references_rev: result_intra_group_references_rev,
482            } in result
483            {
484                lists.splice(
485                    first_chunk_group_idx..(first_chunk_group_idx + result_lists.len()),
486                    result_lists,
487                );
488                for (module, occurrences) in result_lists_reverse_indices {
489                    lists_reverse_indices
490                        .entry(module)
491                        .or_default()
492                        .extend(occurrences);
493                }
494                for (module, occurrences) in result_intra_group_references_rev {
495                    intra_group_references_rev
496                        .entry(module)
497                        .or_default()
498                        .extend(occurrences);
499                }
500            }
501        }
502
503        drop(inner_span);
504        let inner_span = tracing::info_span!("exposed computation").entered();
505
506        // We use list.pop() below, so reverse order using negation
507        lists_reverse_indices
508            .sort_by_cached_key(|_, b| b.iter().map(|o| o.entry).min().map(|v| -(v as i64)));
509
510        // Modules that are referenced from outside the group, so their exports need to be exposed.
511        // Initially these are set based on the bitmaps (and namespace imports), but more modules
512        // might need to be exposed if the lists are split up further below.
513        let mut exposed_modules_imported: FxHashSet<ResolvedVc<Box<dyn Module>>> =
514            FxHashSet::with_capacity_and_hasher(module_merged_groups.len(), Default::default());
515        let mut exposed_modules_namespace: FxHashSet<ResolvedVc<Box<dyn Module>>> =
516            FxHashSet::with_capacity_and_hasher(module_merged_groups.len(), Default::default());
517
518        module_graph.traverse_edges_dfs(
519            entries,
520            &mut (),
521            |_, _, _| Ok(GraphTraversalAction::Continue),
522            |parent_info, node, _| {
523                let module = node;
524
525                if let Some((parent, _)) = parent_info {
526                    let same_bitmap = bitmap_id(parent)? == bitmap_id(module)?;
527
528                    if same_bitmap {
529                        intra_group_references
530                            .entry(parent)
531                            .or_default()
532                            .insert(module);
533                    }
534                }
535
536                if match parent_info {
537                    None => true,
538                    Some((parent, _)) => bitmap_id(parent)? != bitmap_id(module)?,
539                } {
540                    // This module needs to be exposed:
541                    // - referenced from another group or
542                    // - an entry module (TODO assume it will be required for Node/Edge, but not
543                    // necessarily needed for browser),
544                    exposed_modules_imported.insert(module);
545                }
546                if parent_info.is_some_and(|(_, r)| {
547                    matches!(
548                        r.binding_usage.export,
549                        ExportUsage::All
550                            | ExportUsage::PartialNamespaceObject(_)
551                            | ExportUsage::Passthrough { .. }
552                    )
553                }) {
554                    // This module needs to be exposed:
555                    // - namespace import from another group
556                    exposed_modules_namespace.insert(module);
557                }
558                Ok(())
559            },
560            false,
561            false,
562        )?;
563
564        drop(inner_span);
565        let inner_span = tracing::info_span!("reconciliation").entered();
566        while let Some((_, common_occurrences)) = lists_reverse_indices.pop() {
567            if common_occurrences.len() < 2 {
568                // Module exists only in one list, no need to split
569                continue;
570            }
571            // The module occurs in multiple lists, which need to split up so that there is exactly
572            // one list containing the module.
573
574            let first_occurrence = &common_occurrences[0];
575
576            // Find the longest common sequence in the lists, starting from the given module.
577            let mut common_length = 2;
578            loop {
579                let m = lists[first_occurrence.chunk_group][first_occurrence.list]
580                    .get(first_occurrence.entry + common_length - 1);
581                if m.is_some()
582                    && common_occurrences.iter().skip(1).all(
583                        |ListOccurrence {
584                             chunk_group,
585                             list,
586                             entry,
587                         }| {
588                            lists[*chunk_group][*list].get(*entry + common_length - 1) == m
589                        },
590                    )
591                {
592                    common_length += 1;
593                    continue;
594                }
595
596                // Went one too far, the common length is what the previous iteration verified
597                common_length -= 1;
598                break;
599            }
600
601            // Split into three lists:
602            // - "common" [occurrence.entry .. occurrence.entry + common_length) -- same for all
603            // - "before" [0 .. occurrence.entry)
604            // - "after"  [occurrence.entry + common_length .. ]
605            let common_list = lists[first_occurrence.chunk_group][first_occurrence.list]
606                [first_occurrence.entry..first_occurrence.entry + common_length]
607                .to_vec();
608
609            let common_list_index = lists[LISTS_COMMON_IDX].len();
610            lists[LISTS_COMMON_IDX].push(common_list.clone());
611
612            // Insert occurrences for the "common" list, skip the first because that is now
613            // guaranteed to exist only once
614            for (i, &m) in common_list.iter().enumerate().skip(1) {
615                let occurrences = lists_reverse_indices
616                    .get_mut(&m)
617                    .context("every module should have occurrences")?;
618                for common_occurrence in &common_occurrences {
619                    let removed = occurrences.swap_remove(&ListOccurrence {
620                        chunk_group: common_occurrence.chunk_group,
621                        list: common_occurrence.list,
622                        entry: common_occurrence.entry + i,
623                    });
624                    debug_assert!(removed);
625                }
626                occurrences.insert(ListOccurrence {
627                    chunk_group: LISTS_COMMON_IDX,
628                    list: common_list_index,
629                    entry: i,
630                });
631            }
632
633            for common_occurrence in &common_occurrences {
634                let list = &mut lists[common_occurrence.chunk_group][common_occurrence.list];
635                let after_list = list.split_off(common_occurrence.entry + common_length);
636                list.truncate(common_occurrence.entry);
637                let before_list = &*list;
638
639                // For all previously merged references (intra_group_references) that now cross
640                // "before", "common" and "after", mark the referenced modules as
641                // exposed.
642                // Note that due to circular dependencies, there can be
643                // references that go against execution order (e.g. from "before" to
644                // "common").
645                {
646                    let before_list =
647                        FxHashSet::from_iter(before_list.iter().map(|m| ResolvedVc::upcast(*m)));
648                    let common_list =
649                        FxHashSet::from_iter(common_list.iter().map(|m| ResolvedVc::upcast(*m)));
650                    let after_list =
651                        FxHashSet::from_iter(after_list.iter().map(|m| ResolvedVc::upcast(*m)));
652
653                    let references_from_before = before_list
654                        .iter()
655                        .filter_map(|m| intra_group_references.get(m))
656                        .flatten()
657                        .copied()
658                        .filter(|m| common_list.contains(m) || after_list.contains(m))
659                        .collect::<FxHashSet<_>>();
660                    let references_from_common = common_list
661                        .iter()
662                        .filter_map(|m| intra_group_references.get(m))
663                        .flatten()
664                        .filter(|m| before_list.contains(m) || after_list.contains(m))
665                        .collect::<FxHashSet<_>>();
666                    let references_from_after = after_list
667                        .iter()
668                        .filter_map(|m| intra_group_references.get(m))
669                        .flatten()
670                        .copied()
671                        .filter(|m| before_list.contains(m) || common_list.contains(m))
672                        .collect::<FxHashSet<_>>();
673
674                    let modules_to_expose = before_list
675                        .iter()
676                        .chain(common_list.iter())
677                        .chain(after_list.iter())
678                        .copied()
679                        .filter(|m| {
680                            references_from_before.contains(m)
681                                || references_from_common.contains(m)
682                                || references_from_after.contains(m)
683                        });
684
685                    exposed_modules_imported.extend(modules_to_expose);
686                }
687
688                // The occurrences for the "before" list (`list`) are still valid, need to update
689                // the occurrences for the "after" list
690                if !after_list.is_empty() {
691                    let after_index = lists[LISTS_COMMON_IDX].len();
692                    lists[LISTS_COMMON_IDX].push(after_list.clone());
693                    for (i, &m) in after_list.iter().enumerate() {
694                        let Some(occurrences) = lists_reverse_indices.get_mut(&m) else {
695                            bail!("Couldn't find module in reverse list");
696                        };
697
698                        let removed = occurrences.swap_remove(&ListOccurrence {
699                            chunk_group: common_occurrence.chunk_group,
700                            list: common_occurrence.list,
701                            entry: common_occurrence.entry + common_length + i,
702                        });
703                        debug_assert!(removed);
704
705                        occurrences.insert(ListOccurrence {
706                            chunk_group: LISTS_COMMON_IDX,
707                            list: after_index,
708                            entry: i,
709                        });
710                    }
711                }
712            }
713        }
714
715        // Dedupe the lists
716        let lists = lists.into_iter().flatten().collect::<FxHashSet<_>>();
717
718        drop(inner_span);
719        let inner_span = tracing::info_span!("merging");
720        // Call MergeableModule impl to merge the modules.
721        let result = lists
722            .into_iter()
723            .map(async |list| {
724                if list.len() < 2 {
725                    // Nothing to merge
726                    return Ok(None);
727                }
728
729                let list_set = list
730                    .iter()
731                    .map(|&m| ResolvedVc::upcast::<Box<dyn Module>>(m))
732                    .collect::<FxIndexSet<_>>();
733
734                let entry_points = list
735                    .iter()
736                    .filter(|m| {
737                        intra_group_references_rev
738                            .get(&ResolvedVc::upcast(**m))
739                            .is_none_or(|refs| refs.is_disjoint(&list_set))
740                    })
741                    .map(|m| **m)
742                    .collect::<Vec<_>>();
743                debug_assert_ne!(entry_points.len(), 0);
744
745                let list_exposed = list
746                    .iter()
747                    .map(|&m| {
748                        (
749                            m,
750                            if exposed_modules_imported.contains(&ResolvedVc::upcast(m)) {
751                                MergeableModuleExposure::External
752                            } else if exposed_modules_namespace.contains(&ResolvedVc::upcast(m)) {
753                                MergeableModuleExposure::Internal
754                            } else {
755                                MergeableModuleExposure::None
756                            },
757                        )
758                    })
759                    .collect::<Vec<_>>();
760
761                let entry = *list.last().unwrap();
762                let result = entry
763                    .merge(
764                        MergeableModulesExposed::interned(list_exposed),
765                        MergeableModules::interned(entry_points),
766                    )
767                    .to_resolved()
768                    .await?;
769
770                let list_len = list.len();
771                Ok(Some((
772                    ResolvedVc::upcast::<Box<dyn Module>>(entry),
773                    result,
774                    list.into_iter()
775                        .take(list_len - 1)
776                        .map(ResolvedVc::upcast::<Box<dyn Module>>)
777                        .collect::<Vec<_>>(),
778                )))
779            })
780            .try_join()
781            .instrument(inner_span)
782            .await?;
783
784        #[allow(clippy::type_complexity)]
785        let mut replacements: FxHashMap<
786            ResolvedVc<Box<dyn Module>>,
787            Option<ResolvedVc<Box<dyn ChunkableModule>>>,
788        > = Default::default();
789        #[allow(clippy::type_complexity)]
790        let mut replacements_to_original: FxHashMap<
791            ResolvedVc<Box<dyn Module>>,
792            ResolvedVc<Box<dyn Module>>,
793        > = Default::default();
794        let mut merged_groups = 0;
795        let mut included_modules = 0;
796
797        for (original, replacement, replacement_included) in result.into_iter().flatten() {
798            replacements.insert(original, Some(replacement));
799            replacements_to_original.insert(ResolvedVc::upcast(replacement), original);
800            merged_groups += 1;
801            included_modules += replacement_included.len();
802            for included in replacement_included {
803                replacements.insert(included, None);
804            }
805        }
806
807        span.record("merged_groups", merged_groups);
808        span.record("included_modules", included_modules);
809
810        Ok(MergedModuleInfo {
811            replacements: ResolvedVc::cell(replacements),
812            replacements_to_original: ResolvedVc::cell(replacements_to_original),
813        }
814        .cell())
815    }
816    .instrument(span_outer)
817    .await
818}
819
820#[cfg(test)]
821mod tests {
822    use roaring::RoaringBitmap;
823
824    use super::*;
825
826    fn bitmap(bits: &[u32]) -> RoaringBitmapWrapper {
827        RoaringBitmapWrapper(bits.iter().copied().collect::<RoaringBitmap>())
828    }
829
830    #[test]
831    fn intern_bitmaps_gives_equal_bitmaps_equal_ids() {
832        let a = bitmap(&[1, 5, 100_000]);
833        let a_reordered = bitmap(&[100_000, 1, 5]);
834        let b = bitmap(&[1, 5]);
835        let empty = bitmap(&[]);
836        let empty2 = bitmap(&[]);
837        let entries = [
838            ("a", &a),
839            ("b", &b),
840            ("a_reordered", &a_reordered),
841            ("empty", &empty),
842            ("empty2", &empty2),
843        ];
844
845        let (ids, distinct) = intern_bitmaps(entries.iter().copied(), entries.len());
846
847        assert_eq!(distinct, 3);
848        assert_eq!(ids.len(), entries.len());
849        assert_eq!(ids["a"], ids["a_reordered"]);
850        assert_eq!(ids["empty"], ids["empty2"]);
851        assert_ne!(ids["a"], ids["b"]);
852        assert_ne!(ids["a"], ids["empty"]);
853        assert_ne!(ids["b"], ids["empty"]);
854        // Ids are dense and start at zero.
855        let mut all = ids.values().copied().collect::<Vec<_>>();
856        all.sort_unstable();
857        all.dedup();
858        assert_eq!(all, vec![0, 1, 2]);
859    }
860}