Skip to main content

turbopack_core/module_graph/
chunk_group_info.rs

1use std::{
2    hash::Hash,
3    ops::{Deref, DerefMut},
4};
5
6use anyhow::{Context, Result, bail};
7use bincode::{Decode, Encode};
8use either::Either;
9use indexmap::map::Entry;
10use roaring::RoaringBitmap;
11use rustc_hash::FxHashMap;
12use tracing::Instrument;
13use turbo_rcstr::RcStr;
14use turbo_tasks::{
15    FxIndexMap, FxIndexSet, NonLocalValue, ResolvedVc, TaskInput, TryJoinIterExt, ValueToString,
16    Vc, debug::ValueDebugFormat, trace::TraceRawVcs, turbofmt,
17};
18
19use crate::{
20    chunk::ChunkingType,
21    module::Module,
22    module_graph::{GraphTraversalAction, ModuleGraph, RefData},
23};
24
25#[derive(Clone, Debug, Default, PartialEq, TraceRawVcs, ValueDebugFormat, Encode, Decode)]
26#[repr(transparent)]
27pub struct RoaringBitmapWrapper(
28    #[turbo_tasks(trace_ignore)]
29    #[bincode(with_serde)]
30    pub RoaringBitmap,
31);
32
33impl TaskInput for RoaringBitmapWrapper {
34    fn is_transient(&self) -> bool {
35        false
36    }
37}
38
39impl RoaringBitmapWrapper {
40    /// Whether `self` contains bits that are not in `other`
41    ///
42    /// The existing `is_superset` method also returns true for equal sets
43    pub fn is_proper_superset(&self, other: &Self) -> bool {
44        !self.is_subset(other)
45    }
46
47    pub fn into_inner(self) -> RoaringBitmap {
48        self.0
49    }
50}
51unsafe impl NonLocalValue for RoaringBitmapWrapper {}
52
53// RoaringBitmap doesn't impl Eq: https://github.com/RoaringBitmap/roaring-rs/issues/302
54// PartialEq can only return true if both bitmaps have the same internal representation, but two
55// bitmaps with the same content should always have the same internal representation
56impl Eq for RoaringBitmapWrapper {}
57
58impl Deref for RoaringBitmapWrapper {
59    type Target = RoaringBitmap;
60    fn deref(&self) -> &Self::Target {
61        &self.0
62    }
63}
64impl DerefMut for RoaringBitmapWrapper {
65    fn deref_mut(&mut self) -> &mut Self::Target {
66        &mut self.0
67    }
68}
69impl Hash for RoaringBitmapWrapper {
70    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
71        struct HasherWriter<'a, H: std::hash::Hasher>(&'a mut H);
72        impl<H: std::hash::Hasher> std::io::Write for HasherWriter<'_, H> {
73            fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
74                self.0.write(buf);
75                Ok(buf.len())
76            }
77            fn flush(&mut self) -> std::io::Result<()> {
78                Ok(())
79            }
80        }
81        self.0.serialize_into(HasherWriter(state)).unwrap();
82    }
83}
84
85#[turbo_tasks::value(transparent, cell = "keyed")]
86pub struct ModuleToChunkGroups(FxHashMap<ResolvedVc<Box<dyn Module>>, RoaringBitmapWrapper>);
87
88#[turbo_tasks::value]
89pub struct ChunkGroupInfo {
90    pub module_chunk_groups: ResolvedVc<ModuleToChunkGroups>,
91    #[turbo_tasks(trace_ignore)]
92    #[bincode(with = "turbo_bincode::indexset")]
93    pub chunk_groups: FxIndexSet<ChunkGroup>,
94    #[turbo_tasks(trace_ignore)]
95    #[bincode(with = "turbo_bincode::indexset")]
96    pub chunk_group_keys: FxIndexSet<ChunkGroupKey>,
97}
98
99#[turbo_tasks::value_impl]
100impl ChunkGroupInfo {
101    #[turbo_tasks::function]
102    pub fn module_chunk_groups(&self) -> Vc<ModuleToChunkGroups> {
103        *self.module_chunk_groups
104    }
105
106    #[turbo_tasks::function]
107    pub async fn get_index_of(&self, chunk_group: ChunkGroup) -> Result<Vc<usize>> {
108        if let Some(idx) = self.chunk_groups.get_index_of(&chunk_group) {
109            Ok(Vc::cell(idx))
110        } else {
111            bail!(
112                "Couldn't find chunk group index for {} in {}",
113                chunk_group.debug_str(self).await?,
114                self.chunk_groups
115                    .iter()
116                    .map(|c| c.debug_str(self))
117                    .try_join()
118                    .await?
119                    .join(", ")
120            );
121        }
122    }
123}
124
125/// See [ChunkGroup] for documentation
126#[derive(
127    Debug, Clone, Hash, TaskInput, PartialEq, Eq, TraceRawVcs, NonLocalValue, Encode, Decode,
128)]
129pub enum ChunkGroupEntry {
130    Entry(Vec<ResolvedVc<Box<dyn Module>>>),
131    Async(ResolvedVc<Box<dyn Module>>),
132    Isolated(ResolvedVc<Box<dyn Module>>),
133    IsolatedMerged {
134        parent: Box<ChunkGroupEntry>,
135        merge_tag: RcStr,
136        entries: Vec<ResolvedVc<Box<dyn Module>>>,
137    },
138    Shared(ResolvedVc<Box<dyn Module>>),
139    SharedMultiple(Vec<ResolvedVc<Box<dyn Module>>>),
140    SharedMerged {
141        parent: Box<ChunkGroupEntry>,
142        merge_tag: RcStr,
143        entries: Vec<ResolvedVc<Box<dyn Module>>>,
144    },
145}
146impl ChunkGroupEntry {
147    pub fn entries(&self) -> impl Iterator<Item = ResolvedVc<Box<dyn Module>>> + '_ {
148        match self {
149            Self::Async(e) | Self::Isolated(e) | Self::Shared(e) => {
150                Either::Left(std::iter::once(*e))
151            }
152            Self::Entry(entries)
153            | Self::IsolatedMerged { entries, .. }
154            | Self::SharedMultiple(entries)
155            | Self::SharedMerged { entries, .. } => Either::Right(entries.iter().copied()),
156        }
157    }
158}
159
160#[derive(Debug, Clone, Hash, TaskInput, PartialEq, Eq, TraceRawVcs, Encode, Decode)]
161pub enum ChunkGroup {
162    /// The entry chunk group of the compilation, e.g. src/index.js for a SPA, or app/foo/page.js
163    /// for Next.js.
164    Entry(Vec<ResolvedVc<Box<dyn Module>>>),
165    /// An async chunk group. Corresponds to an incoming [ChunkingType::Async] reference
166    Async(ResolvedVc<Box<dyn Module>>),
167    /// An isolated chunk group. Corresponds to an incoming [ChunkingType::Isolated] reference with
168    /// `merge_tag: None`
169    Isolated(ResolvedVc<Box<dyn Module>>),
170    /// An isolated chunk group. Corresponds to an incoming [ChunkingType::Isolated] reference with
171    /// `merge_tag: Some(_)`
172    IsolatedMerged {
173        parent: usize,
174        merge_tag: RcStr,
175        entries: Vec<ResolvedVc<Box<dyn Module>>>,
176    },
177    /// A shared chunk group. Corresponds to an incoming [ChunkingType::Shared] reference with
178    /// `merge_tag: None`
179    Shared(ResolvedVc<Box<dyn Module>>),
180    /// A shared chunk group with multiple entries.
181    SharedMultiple(Vec<ResolvedVc<Box<dyn Module>>>),
182    /// A shared chunk group. Corresponds to an incoming [ChunkingType::Shared] reference with
183    /// `merge_tag: Some(_)`
184    SharedMerged {
185        parent: usize,
186        merge_tag: RcStr,
187        entries: Vec<ResolvedVc<Box<dyn Module>>>,
188    },
189}
190
191impl ChunkGroup {
192    /// Returns the parent group when this chunk group is a merged group. In that case `entries()`
193    /// are in unspecified order.
194    pub fn get_merged_parent(&self) -> Option<usize> {
195        match self {
196            ChunkGroup::IsolatedMerged { parent, .. } | ChunkGroup::SharedMerged { parent, .. } => {
197                Some(*parent)
198            }
199            _ => None,
200        }
201    }
202
203    /// Iterates over the entries of the chunk group. When `get_merged_parent` is Some, the order is
204    /// unspecified.
205    pub fn entries(&self) -> impl Iterator<Item = ResolvedVc<Box<dyn Module>>> + Clone + '_ {
206        match self {
207            ChunkGroup::Async(e) | ChunkGroup::Isolated(e) | ChunkGroup::Shared(e) => {
208                Either::Left(std::iter::once(*e))
209            }
210            ChunkGroup::Entry(entries)
211            | ChunkGroup::IsolatedMerged { entries, .. }
212            | ChunkGroup::SharedMultiple(entries)
213            | ChunkGroup::SharedMerged { entries, .. } => Either::Right(entries.iter().copied()),
214        }
215    }
216
217    pub fn entries_count(&self) -> usize {
218        match self {
219            ChunkGroup::Async(_) | ChunkGroup::Isolated(_) | ChunkGroup::Shared(_) => 1,
220            ChunkGroup::Entry(entries)
221            | ChunkGroup::IsolatedMerged { entries, .. }
222            | ChunkGroup::SharedMultiple(entries)
223            | ChunkGroup::SharedMerged { entries, .. } => entries.len(),
224        }
225    }
226
227    pub async fn debug_str(&self, chunk_group_info: &ChunkGroupInfo) -> Result<String> {
228        Ok(match self {
229            ChunkGroup::Entry(entries) => format!(
230                "ChunkGroup::Entry({:?})",
231                entries
232                    .iter()
233                    .map(|m| m.ident().to_string())
234                    .try_join()
235                    .await?
236            ),
237            ChunkGroup::Async(entry) => turbofmt!("ChunkGroup::Async({:?})", entry.ident())
238                .await?
239                .to_string(),
240            ChunkGroup::Isolated(entry) => turbofmt!("ChunkGroup::Isolated({:?})", entry.ident())
241                .await?
242                .to_string(),
243            ChunkGroup::Shared(entry) => turbofmt!("ChunkGroup::Shared({:?})", entry.ident())
244                .await?
245                .to_string(),
246            ChunkGroup::SharedMultiple(entries) => format!(
247                "ChunkGroup::SharedMultiple({:?})",
248                entries
249                    .iter()
250                    .map(|m| m.ident().to_string())
251                    .try_join()
252                    .await?
253            ),
254            ChunkGroup::IsolatedMerged {
255                parent,
256                merge_tag,
257                entries,
258            } => {
259                format!(
260                    "ChunkGroup::IsolatedMerged({}, {}, {:?})",
261                    Box::pin(chunk_group_info.chunk_groups[*parent].debug_str(chunk_group_info))
262                        .await?,
263                    merge_tag,
264                    entries
265                        .iter()
266                        .map(|m| m.ident().to_string())
267                        .try_join()
268                        .await?
269                )
270            }
271            ChunkGroup::SharedMerged {
272                parent,
273                merge_tag,
274                entries,
275            } => {
276                format!(
277                    "ChunkGroup::SharedMerged({}, {}, {:?})",
278                    Box::pin(chunk_group_info.chunk_groups[*parent].debug_str(chunk_group_info))
279                        .await?,
280                    merge_tag,
281                    entries
282                        .iter()
283                        .map(|m| m.ident().to_string())
284                        .try_join()
285                        .await?
286                )
287            }
288        })
289    }
290}
291
292/// See [ChunkGroup] for documentation
293#[derive(Debug, Clone, Hash, PartialEq, Eq, Encode, Decode)]
294pub enum ChunkGroupKey {
295    Entry(Vec<ResolvedVc<Box<dyn Module>>>),
296    Async(ResolvedVc<Box<dyn Module>>),
297    Isolated(ResolvedVc<Box<dyn Module>>),
298    IsolatedMerged {
299        parent: ChunkGroupId,
300        merge_tag: RcStr,
301    },
302    Shared(ResolvedVc<Box<dyn Module>>),
303    SharedMultiple(Vec<ResolvedVc<Box<dyn Module>>>),
304    SharedMerged {
305        parent: ChunkGroupId,
306        merge_tag: RcStr,
307    },
308}
309
310impl ChunkGroupKey {
311    pub async fn debug_str(
312        &self,
313        keys: impl std::ops::Index<usize, Output = Self>,
314    ) -> Result<String> {
315        Ok(match self {
316            ChunkGroupKey::Entry(entries) => format!(
317                "Entry({:?})",
318                entries
319                    .iter()
320                    .map(|m| m.ident().to_string())
321                    .try_join()
322                    .await?
323            ),
324            ChunkGroupKey::Async(module) => {
325                turbofmt!("Async({:?})", module.ident()).await?.to_string()
326            }
327            ChunkGroupKey::Isolated(module) => turbofmt!("Isolated({:?})", module.ident())
328                .await?
329                .to_string(),
330            ChunkGroupKey::IsolatedMerged { parent, merge_tag } => {
331                format!(
332                    "IsolatedMerged {{ parent: {}, merge_tag: {:?} }}",
333                    Box::pin(keys.index(parent.0 as usize).clone().debug_str(keys)).await?,
334                    merge_tag
335                )
336            }
337            ChunkGroupKey::Shared(module) => {
338                turbofmt!("Shared({:?})", module.ident()).await?.to_string()
339            }
340            ChunkGroupKey::SharedMultiple(entries) => format!(
341                "SharedMultiple({:?})",
342                entries
343                    .iter()
344                    .map(|m| m.ident().to_string())
345                    .try_join()
346                    .await?
347            ),
348            ChunkGroupKey::SharedMerged { parent, merge_tag } => {
349                format!(
350                    "SharedMerged {{ parent: {}, merge_tag: {:?} }}",
351                    Box::pin(keys.index(parent.0 as usize).clone().debug_str(keys)).await?,
352                    merge_tag
353                )
354            }
355        })
356    }
357}
358
359#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Encode, Decode)]
360pub struct ChunkGroupId(u32);
361
362impl From<usize> for ChunkGroupId {
363    fn from(id: usize) -> Self {
364        Self(id as u32)
365    }
366}
367
368impl Deref for ChunkGroupId {
369    type Target = u32;
370    fn deref(&self) -> &Self::Target {
371        &self.0
372    }
373}
374
375#[derive(Debug, Clone, PartialEq, Eq)]
376struct TraversalPriority {
377    depth: usize,
378    chunk_group_len: u64,
379}
380impl PartialOrd for TraversalPriority {
381    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
382        Some(self.cmp(other))
383    }
384}
385impl Ord for TraversalPriority {
386    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
387        // BinaryHeap prioritizes high values
388
389        // Smaller depth has higher priority
390        let depth_order = self.depth.cmp(&other.depth).reverse();
391        // Smaller group length has higher priority
392        let chunk_group_len_order = self.chunk_group_len.cmp(&other.chunk_group_len).reverse();
393
394        depth_order.then(chunk_group_len_order)
395    }
396}
397
398pub async fn compute_chunk_group_info(graph: &ModuleGraph) -> Result<Vc<ChunkGroupInfo>> {
399    let span_outer = tracing::info_span!(
400        "compute chunk group info",
401        module_count = tracing::field::Empty,
402        visit_count = tracing::field::Empty,
403        chunk_group_count = tracing::field::Empty
404    );
405
406    let span = span_outer.clone();
407    async move {
408        #[allow(clippy::type_complexity)]
409        let mut chunk_groups_map: FxIndexMap<
410            ChunkGroupKey,
411            (ChunkGroupId, FxIndexSet<ResolvedVc<Box<dyn Module>>>),
412        > = FxIndexMap::default();
413
414        // For each module, the indices in the bitmap store which chunk groups in `chunk_groups_map`
415        // that module is part of.
416        let mut module_chunk_groups: FxHashMap<ResolvedVc<Box<dyn Module>>, RoaringBitmapWrapper> =
417            FxHashMap::default();
418
419        let module_count = graph
420            .graphs
421            .iter()
422            .map(|g| g.graph.node_count())
423            .sum::<usize>();
424        span.record("module_count", module_count);
425
426        // use all entries from all graphs
427        let entries = graph
428            .graphs
429            .iter()
430            .flat_map(|g| g.entries.iter())
431            .collect::<Vec<_>>();
432
433        // First, compute the depth for each module in the graph
434        let module_depth: FxHashMap<ResolvedVc<Box<dyn Module>>, usize> = {
435            let mut module_depth =
436                FxHashMap::with_capacity_and_hasher(module_count, Default::default());
437            graph.traverse_edges_bfs(
438                entries.iter().flat_map(|e| e.entries()),
439                |parent, node| {
440                    if let Some((parent, _)) = parent {
441                        let parent_depth = *module_depth
442                            .get(&parent)
443                            .context("Module depth not found")?;
444                        module_depth.entry(node).or_insert(parent_depth + 1);
445                    } else {
446                        module_depth.insert(node, 0);
447                    };
448
449                    module_chunk_groups.insert(node, RoaringBitmapWrapper::default());
450
451                    Ok(GraphTraversalAction::Continue)
452                },
453            )?;
454            module_depth
455        };
456
457        // ----
458
459        #[allow(clippy::type_complexity)]
460        fn entry_to_chunk_group_id(
461            entry: ChunkGroupEntry,
462            chunk_groups_map: &mut FxIndexMap<
463                ChunkGroupKey,
464                (ChunkGroupId, FxIndexSet<ResolvedVc<Box<dyn Module>>>),
465            >,
466        ) -> ChunkGroupKey {
467            match entry {
468                ChunkGroupEntry::Entry(entries) => ChunkGroupKey::Entry(entries),
469                ChunkGroupEntry::Async(entry) => ChunkGroupKey::Async(entry),
470                ChunkGroupEntry::Isolated(entry) => ChunkGroupKey::Isolated(entry),
471                ChunkGroupEntry::Shared(entry) => ChunkGroupKey::Shared(entry),
472                ChunkGroupEntry::SharedMultiple(entries) => ChunkGroupKey::SharedMultiple(entries),
473                ChunkGroupEntry::IsolatedMerged {
474                    parent,
475                    merge_tag,
476                    entries: _,
477                } => {
478                    let parent = entry_to_chunk_group_id(*parent, chunk_groups_map);
479                    let len = chunk_groups_map.len();
480                    let parent = chunk_groups_map
481                        .entry(parent)
482                        .or_insert_with(|| (ChunkGroupId(len as u32), FxIndexSet::default()))
483                        .0;
484
485                    ChunkGroupKey::IsolatedMerged {
486                        parent: ChunkGroupId(*parent as u32),
487                        merge_tag,
488                    }
489                }
490                ChunkGroupEntry::SharedMerged {
491                    parent,
492                    merge_tag,
493                    entries: _,
494                } => {
495                    let parent = entry_to_chunk_group_id(*parent, chunk_groups_map);
496                    let len = chunk_groups_map.len();
497                    let parent = chunk_groups_map
498                        .entry(parent)
499                        .or_insert_with(|| (ChunkGroupId(len as u32), FxIndexSet::default()))
500                        .0;
501
502                    ChunkGroupKey::SharedMerged {
503                        parent: ChunkGroupId(*parent as u32),
504                        merge_tag,
505                    }
506                }
507            }
508        }
509
510        let entry_chunk_group_keys = graph
511            .graphs
512            .iter()
513            .flat_map(|g| g.entries.iter())
514            .flat_map(|chunk_group| {
515                let chunk_group_key =
516                    entry_to_chunk_group_id(chunk_group.clone(), &mut chunk_groups_map);
517                chunk_group
518                    .entries()
519                    .map(move |e| (e, chunk_group_key.clone()))
520            })
521            .collect::<FxHashMap<_, _>>();
522
523        let visit_count = graph.traverse_edges_fixed_point_with_priority(
524            entries
525                .iter()
526                .flat_map(|e| e.entries())
527                .map(|e| {
528                    Ok((
529                        e,
530                        TraversalPriority {
531                            depth: *module_depth.get(&e).context("Module depth not found")?,
532                            chunk_group_len: 0,
533                        },
534                    ))
535                })
536                .collect::<Result<Vec<_>>>()?,
537            &mut module_chunk_groups,
538            |parent_info: Option<(ResolvedVc<Box<dyn Module>>, &'_ RefData, _)>,
539             node: ResolvedVc<Box<dyn Module>>,
540             module_chunk_groups: &mut FxHashMap<
541                ResolvedVc<Box<dyn Module>>,
542                RoaringBitmapWrapper,
543            >|
544             -> Result<GraphTraversalAction> {
545                enum ChunkGroupInheritance<It: Iterator<Item = ChunkGroupKey>> {
546                    Inherit(ResolvedVc<Box<dyn Module>>),
547                    ChunkGroup(It),
548                }
549                let chunk_groups = if let Some((parent, ref_data, _)) = parent_info {
550                    match &ref_data.chunking_type {
551                        ChunkingType::Parallel { .. } => ChunkGroupInheritance::Inherit(parent),
552                        ChunkingType::Async => ChunkGroupInheritance::ChunkGroup(Either::Left(
553                            std::iter::once(ChunkGroupKey::Async(node)),
554                        )),
555                        ChunkingType::Isolated {
556                            merge_tag: None, ..
557                        } => ChunkGroupInheritance::ChunkGroup(Either::Left(std::iter::once(
558                            ChunkGroupKey::Isolated(node),
559                        ))),
560                        ChunkingType::Shared {
561                            merge_tag: None, ..
562                        } => ChunkGroupInheritance::ChunkGroup(Either::Left(std::iter::once(
563                            ChunkGroupKey::Shared(node),
564                        ))),
565                        ChunkingType::Isolated {
566                            merge_tag: Some(merge_tag),
567                            ..
568                        } => {
569                            let parents = module_chunk_groups
570                                .get(&parent)
571                                .context("Module chunk group not found")?;
572                            let chunk_groups =
573                                parents.iter().map(|parent| ChunkGroupKey::IsolatedMerged {
574                                    parent: ChunkGroupId(parent),
575                                    merge_tag: merge_tag.clone(),
576                                });
577                            ChunkGroupInheritance::ChunkGroup(Either::Right(Either::Left(
578                                chunk_groups,
579                            )))
580                        }
581                        ChunkingType::Shared {
582                            merge_tag: Some(merge_tag),
583                            ..
584                        } => {
585                            let parents = module_chunk_groups
586                                .get(&parent)
587                                .context("Module chunk group not found")?;
588                            let chunk_groups =
589                                parents.iter().map(|parent| ChunkGroupKey::SharedMerged {
590                                    parent: ChunkGroupId(parent),
591                                    merge_tag: merge_tag.clone(),
592                                });
593                            ChunkGroupInheritance::ChunkGroup(Either::Right(Either::Right(
594                                chunk_groups,
595                            )))
596                        }
597                        ChunkingType::Traced => {
598                            // Traced modules are not placed in chunk groups
599                            return Ok(GraphTraversalAction::Skip);
600                        }
601                    }
602                } else {
603                    ChunkGroupInheritance::ChunkGroup(Either::Left(std::iter::once(
604                        // TODO remove clone
605                        entry_chunk_group_keys
606                            .get(&node)
607                            .context("Module chunk group not found")?
608                            .clone(),
609                    )))
610                };
611
612                Ok(match chunk_groups {
613                    ChunkGroupInheritance::ChunkGroup(chunk_groups) => {
614                        // Start of a new chunk group, don't inherit anything from parent
615                        let chunk_group_ids = chunk_groups.map(|chunk_group| {
616                            let len = chunk_groups_map.len();
617                            let is_merged = matches!(
618                                chunk_group,
619                                ChunkGroupKey::IsolatedMerged { .. }
620                                    | ChunkGroupKey::SharedMerged { .. }
621                            );
622                            match chunk_groups_map.entry(chunk_group) {
623                                Entry::Occupied(mut e) => {
624                                    let (id, merged_entries) = e.get_mut();
625                                    if is_merged {
626                                        merged_entries.insert(node);
627                                    }
628                                    **id
629                                }
630                                Entry::Vacant(e) => {
631                                    let chunk_group_id = len as u32;
632                                    let mut set = FxIndexSet::default();
633                                    if is_merged {
634                                        set.insert(node);
635                                    }
636                                    e.insert((ChunkGroupId(chunk_group_id), set));
637                                    chunk_group_id
638                                }
639                            }
640                        });
641
642                        let chunk_groups =
643                            RoaringBitmapWrapper(RoaringBitmap::from_iter(chunk_group_ids));
644
645                        // Assign chunk group to the target node (the entry of the chunk group)
646                        let bitset = module_chunk_groups
647                            .get_mut(&node)
648                            .context("Module chunk group not found")?;
649                        if chunk_groups.is_proper_superset(bitset) {
650                            // Add bits from parent, and continue traversal because changed
651                            **bitset |= chunk_groups.into_inner();
652
653                            GraphTraversalAction::Continue
654                        } else {
655                            // Unchanged, no need to forward to children
656                            GraphTraversalAction::Skip
657                        }
658                    }
659                    ChunkGroupInheritance::Inherit(parent) => {
660                        // Inherit chunk groups from parent, merge parent chunk groups into
661                        // current
662
663                        if parent == node {
664                            // A self-reference
665                            GraphTraversalAction::Skip
666                        } else {
667                            let [Some(parent_chunk_groups), Some(current_chunk_groups)] =
668                                module_chunk_groups.get_disjoint_mut([&parent, &node])
669                            else {
670                                // All modules are inserted in the previous iteration
671                                // Technically unreachable, but could be reached due to eventual
672                                // consistency
673                                bail!("Module chunk groups not found");
674                            };
675
676                            if current_chunk_groups.is_empty() {
677                                // Initial visit, clone instead of merging
678                                *current_chunk_groups = parent_chunk_groups.clone();
679                                GraphTraversalAction::Continue
680                            } else if parent_chunk_groups.is_proper_superset(current_chunk_groups) {
681                                // Add bits from parent, and continue traversal because changed
682                                **current_chunk_groups |= &**parent_chunk_groups;
683                                GraphTraversalAction::Continue
684                            } else {
685                                // Unchanged, no need to forward to children
686                                GraphTraversalAction::Skip
687                            }
688                        }
689                    }
690                })
691            },
692            // This priority is used as a heuristic to keep the number of retraversals down, by
693            // - keeping it similar to a BFS via the depth priority
694            // - prioritizing smaller chunk groups which are expected to themselves reference
695            //   bigger chunk groups (i.e. shared code deeper down in the graph).
696            //
697            // Both try to first visit modules with a large dependency subgraph first (which
698            // would be higher in the graph and are included by few chunks themselves).
699            |successor, module_chunk_groups| {
700                Ok(TraversalPriority {
701                    depth: *module_depth
702                        .get(&successor)
703                        .context("Module depth not found")?,
704                    chunk_group_len: module_chunk_groups
705                        .get(&successor)
706                        .context("Module chunk group not found")?
707                        .len(),
708                })
709            },
710        )?;
711
712        span.record("visit_count", visit_count);
713        span.record("chunk_group_count", chunk_groups_map.len());
714
715        #[cfg(debug_assertions)]
716        {
717            use once_cell::sync::Lazy;
718            static PRINT_CHUNK_GROUP_INFO: Lazy<bool> =
719                Lazy::new(|| match std::env::var_os("TURBOPACK_PRINT_CHUNK_GROUPS") {
720                    Some(v) => v == "1",
721                    None => false,
722                });
723            if *PRINT_CHUNK_GROUP_INFO {
724                use std::{
725                    collections::{BTreeMap, BTreeSet},
726                    path::absolute,
727                };
728
729                let mut buckets = BTreeMap::default();
730                for (module, key) in &module_chunk_groups {
731                    if !key.is_empty() {
732                        buckets
733                            .entry(key.iter().collect::<Vec<_>>())
734                            .or_insert(BTreeSet::new())
735                            .insert(module.ident().to_string().await?);
736                    }
737                }
738
739                let mut result = vec![];
740                result.push("Chunk Groups:".to_string());
741                for (i, (key, _)) in chunk_groups_map.iter().enumerate() {
742                    result.push(format!(
743                        "  {:?}: {}",
744                        i,
745                        key.debug_str(chunk_groups_map.keys()).await?
746                    ));
747                }
748                result.push("# Module buckets:".to_string());
749                for (key, modules) in buckets.iter() {
750                    result.push(format!("## {:?}:", key.iter().collect::<Vec<_>>()));
751                    for module in modules {
752                        result.push(format!("  {module}"));
753                    }
754                    result.push("".to_string());
755                }
756                let f = absolute("chunk_group_info.log")?;
757                println!("written to {}", f.display());
758                std::fs::write(f, result.join("\n"))?;
759            }
760        }
761
762        Ok(ChunkGroupInfo {
763            module_chunk_groups: ResolvedVc::cell(module_chunk_groups),
764            chunk_group_keys: chunk_groups_map.keys().cloned().collect(),
765            chunk_groups: chunk_groups_map
766                .into_iter()
767                .map(|(k, (_, merged_entries))| match k {
768                    ChunkGroupKey::Entry(entries) => ChunkGroup::Entry(entries),
769                    ChunkGroupKey::Async(module) => ChunkGroup::Async(module),
770                    ChunkGroupKey::Isolated(module) => ChunkGroup::Isolated(module),
771                    ChunkGroupKey::IsolatedMerged { parent, merge_tag } => {
772                        ChunkGroup::IsolatedMerged {
773                            parent: parent.0 as usize,
774                            merge_tag,
775                            entries: merged_entries.into_iter().collect(),
776                        }
777                    }
778                    ChunkGroupKey::Shared(module) => ChunkGroup::Shared(module),
779                    ChunkGroupKey::SharedMultiple(entries) => ChunkGroup::SharedMultiple(entries),
780                    ChunkGroupKey::SharedMerged { parent, merge_tag } => ChunkGroup::SharedMerged {
781                        parent: parent.0 as usize,
782                        merge_tag,
783                        entries: merged_entries.into_iter().collect(),
784                    },
785                })
786                .collect(),
787        }
788        .cell())
789    }
790    .instrument(span_outer)
791    .await
792}