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            if cfg!(debug_assertions) {
112                bail!(
113                    "Couldn't find chunk group index for {} in {}",
114                    chunk_group.debug_str(self).await?,
115                    self.chunk_groups
116                        .iter()
117                        .map(|c| c.debug_str(self))
118                        .try_join()
119                        .await?
120                        .join(", ")
121                );
122            } else {
123                bail!("Couldn't find chunk group index")
124            }
125        }
126    }
127}
128
129/// See [ChunkGroup] for documentation
130#[turbo_tasks::task_input]
131#[derive(Debug, Clone, Hash, PartialEq, Eq, TraceRawVcs, Encode, Decode)]
132pub enum ChunkGroupEntry {
133    Entry(Vec<ResolvedVc<Box<dyn Module>>>),
134    Async(ResolvedVc<Box<dyn Module>>),
135    Isolated(ResolvedVc<Box<dyn Module>>),
136    IsolatedMerged {
137        parent: Box<ChunkGroupEntry>,
138        merge_tag: RcStr,
139        entries: Vec<ResolvedVc<Box<dyn Module>>>,
140    },
141    Shared(ResolvedVc<Box<dyn Module>>),
142    SharedMultiple(Vec<ResolvedVc<Box<dyn Module>>>),
143    SharedMerged {
144        parent: Box<ChunkGroupEntry>,
145        merge_tag: RcStr,
146        entries: Vec<ResolvedVc<Box<dyn Module>>>,
147    },
148}
149impl ChunkGroupEntry {
150    pub fn entries(&self) -> impl Iterator<Item = ResolvedVc<Box<dyn Module>>> + '_ {
151        match self {
152            Self::Async(e) | Self::Isolated(e) | Self::Shared(e) => {
153                Either::Left(std::iter::once(*e))
154            }
155            Self::Entry(entries)
156            | Self::IsolatedMerged { entries, .. }
157            | Self::SharedMultiple(entries)
158            | Self::SharedMerged { entries, .. } => Either::Right(entries.iter().copied()),
159        }
160    }
161}
162
163#[turbo_tasks::task_input]
164#[derive(Debug, Clone, Hash, PartialEq, Eq, TraceRawVcs, Encode, Decode)]
165pub enum ChunkGroup {
166    /// The entry chunk group of the compilation, e.g. src/index.js for a SPA, or app/foo/page.js
167    /// for Next.js.
168    Entry(Vec<ResolvedVc<Box<dyn Module>>>),
169    /// An async chunk group. Corresponds to an incoming [ChunkingType::Async] reference
170    Async(ResolvedVc<Box<dyn Module>>),
171    /// An isolated chunk group. Corresponds to an incoming [ChunkingType::Isolated] reference with
172    /// `merge_tag: None`
173    Isolated(ResolvedVc<Box<dyn Module>>),
174    /// An isolated chunk group. Corresponds to an incoming [ChunkingType::Isolated] reference with
175    /// `merge_tag: Some(_)`
176    IsolatedMerged {
177        parent: usize,
178        merge_tag: RcStr,
179        entries: Vec<ResolvedVc<Box<dyn Module>>>,
180    },
181    /// A shared chunk group. Corresponds to an incoming [ChunkingType::Shared] reference with
182    /// `merge_tag: None`
183    Shared(ResolvedVc<Box<dyn Module>>),
184    /// A shared chunk group with multiple entries.
185    SharedMultiple(Vec<ResolvedVc<Box<dyn Module>>>),
186    /// A shared chunk group. Corresponds to an incoming [ChunkingType::Shared] reference with
187    /// `merge_tag: Some(_)`
188    SharedMerged {
189        parent: usize,
190        merge_tag: RcStr,
191        entries: Vec<ResolvedVc<Box<dyn Module>>>,
192    },
193}
194
195impl ChunkGroup {
196    /// Returns the parent group when this chunk group is a merged group. In that case `entries()`
197    /// are in unspecified order.
198    pub fn get_merged_parent(&self) -> Option<usize> {
199        match self {
200            ChunkGroup::IsolatedMerged { parent, .. } | ChunkGroup::SharedMerged { parent, .. } => {
201                Some(*parent)
202            }
203            _ => None,
204        }
205    }
206
207    /// Iterates over the entries of the chunk group. When `get_merged_parent` is Some, the order is
208    /// unspecified.
209    pub fn entries(&self) -> impl Iterator<Item = ResolvedVc<Box<dyn Module>>> + Clone + '_ {
210        match self {
211            ChunkGroup::Async(e) | ChunkGroup::Isolated(e) | ChunkGroup::Shared(e) => {
212                Either::Left(std::iter::once(*e))
213            }
214            ChunkGroup::Entry(entries)
215            | ChunkGroup::IsolatedMerged { entries, .. }
216            | ChunkGroup::SharedMultiple(entries)
217            | ChunkGroup::SharedMerged { entries, .. } => Either::Right(entries.iter().copied()),
218        }
219    }
220
221    pub fn entries_count(&self) -> usize {
222        match self {
223            ChunkGroup::Async(_) | ChunkGroup::Isolated(_) | ChunkGroup::Shared(_) => 1,
224            ChunkGroup::Entry(entries)
225            | ChunkGroup::IsolatedMerged { entries, .. }
226            | ChunkGroup::SharedMultiple(entries)
227            | ChunkGroup::SharedMerged { entries, .. } => entries.len(),
228        }
229    }
230
231    pub async fn debug_str(&self, chunk_group_info: &ChunkGroupInfo) -> Result<String> {
232        Ok(match self {
233            ChunkGroup::Entry(entries) => format!(
234                "ChunkGroup::Entry({:?})",
235                entries
236                    .iter()
237                    .map(|m| m.ident().to_string())
238                    .try_join()
239                    .await?
240            ),
241            ChunkGroup::Async(entry) => turbofmt!("ChunkGroup::Async({:?})", entry.ident())
242                .await?
243                .to_string(),
244            ChunkGroup::Isolated(entry) => turbofmt!("ChunkGroup::Isolated({:?})", entry.ident())
245                .await?
246                .to_string(),
247            ChunkGroup::Shared(entry) => turbofmt!("ChunkGroup::Shared({:?})", entry.ident())
248                .await?
249                .to_string(),
250            ChunkGroup::SharedMultiple(entries) => format!(
251                "ChunkGroup::SharedMultiple({:?})",
252                entries
253                    .iter()
254                    .map(|m| m.ident().to_string())
255                    .try_join()
256                    .await?
257            ),
258            ChunkGroup::IsolatedMerged {
259                parent,
260                merge_tag,
261                entries,
262            } => {
263                format!(
264                    "ChunkGroup::IsolatedMerged({}, {}, {:?})",
265                    Box::pin(chunk_group_info.chunk_groups[*parent].debug_str(chunk_group_info))
266                        .await?,
267                    merge_tag,
268                    entries
269                        .iter()
270                        .map(|m| m.ident().to_string())
271                        .try_join()
272                        .await?
273                )
274            }
275            ChunkGroup::SharedMerged {
276                parent,
277                merge_tag,
278                entries,
279            } => {
280                format!(
281                    "ChunkGroup::SharedMerged({}, {}, {:?})",
282                    Box::pin(chunk_group_info.chunk_groups[*parent].debug_str(chunk_group_info))
283                        .await?,
284                    merge_tag,
285                    entries
286                        .iter()
287                        .map(|m| m.ident().to_string())
288                        .try_join()
289                        .await?
290                )
291            }
292        })
293    }
294}
295
296/// See [ChunkGroup] for documentation
297#[derive(Debug, Clone, Hash, PartialEq, Eq, Encode, Decode)]
298pub enum ChunkGroupKey {
299    Entry(Vec<ResolvedVc<Box<dyn Module>>>),
300    Async(ResolvedVc<Box<dyn Module>>),
301    Isolated(ResolvedVc<Box<dyn Module>>),
302    IsolatedMerged {
303        parent: ChunkGroupId,
304        merge_tag: RcStr,
305    },
306    Shared(ResolvedVc<Box<dyn Module>>),
307    SharedMultiple(Vec<ResolvedVc<Box<dyn Module>>>),
308    SharedMerged {
309        parent: ChunkGroupId,
310        merge_tag: RcStr,
311    },
312}
313
314impl ChunkGroupKey {
315    pub async fn debug_str(
316        &self,
317        keys: impl std::ops::Index<usize, Output = Self>,
318    ) -> Result<String> {
319        Ok(match self {
320            ChunkGroupKey::Entry(entries) => format!(
321                "Entry({:?})",
322                entries
323                    .iter()
324                    .map(|m| m.ident().to_string())
325                    .try_join()
326                    .await?
327            ),
328            ChunkGroupKey::Async(module) => {
329                turbofmt!("Async({:?})", module.ident()).await?.to_string()
330            }
331            ChunkGroupKey::Isolated(module) => turbofmt!("Isolated({:?})", module.ident())
332                .await?
333                .to_string(),
334            ChunkGroupKey::IsolatedMerged { parent, merge_tag } => {
335                format!(
336                    "IsolatedMerged {{ parent: {}, merge_tag: {:?} }}",
337                    Box::pin(keys.index(parent.0 as usize).clone().debug_str(keys)).await?,
338                    merge_tag
339                )
340            }
341            ChunkGroupKey::Shared(module) => {
342                turbofmt!("Shared({:?})", module.ident()).await?.to_string()
343            }
344            ChunkGroupKey::SharedMultiple(entries) => format!(
345                "SharedMultiple({:?})",
346                entries
347                    .iter()
348                    .map(|m| m.ident().to_string())
349                    .try_join()
350                    .await?
351            ),
352            ChunkGroupKey::SharedMerged { parent, merge_tag } => {
353                format!(
354                    "SharedMerged {{ parent: {}, merge_tag: {:?} }}",
355                    Box::pin(keys.index(parent.0 as usize).clone().debug_str(keys)).await?,
356                    merge_tag
357                )
358            }
359        })
360    }
361}
362
363#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Encode, Decode)]
364pub struct ChunkGroupId(u32);
365
366impl From<usize> for ChunkGroupId {
367    fn from(id: usize) -> Self {
368        Self(id as u32)
369    }
370}
371
372impl Deref for ChunkGroupId {
373    type Target = u32;
374    fn deref(&self) -> &Self::Target {
375        &self.0
376    }
377}
378
379#[derive(Debug, Clone, PartialEq, Eq)]
380struct TraversalPriority {
381    depth: usize,
382    chunk_group_len: u64,
383}
384impl PartialOrd for TraversalPriority {
385    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
386        Some(self.cmp(other))
387    }
388}
389impl Ord for TraversalPriority {
390    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
391        // BinaryHeap prioritizes high values
392
393        // Smaller depth has higher priority
394        let depth_order = self.depth.cmp(&other.depth).reverse();
395        // Smaller group length has higher priority
396        let chunk_group_len_order = self.chunk_group_len.cmp(&other.chunk_group_len).reverse();
397
398        depth_order.then(chunk_group_len_order)
399    }
400}
401
402pub async fn compute_chunk_group_info(graph: &ModuleGraph) -> Result<Vc<ChunkGroupInfo>> {
403    let span_outer = tracing::info_span!(
404        "compute chunk group info",
405        module_count = tracing::field::Empty,
406        visit_count = tracing::field::Empty,
407        chunk_group_count = tracing::field::Empty
408    );
409
410    let span = span_outer.clone();
411    async move {
412        #[allow(clippy::type_complexity)]
413        let mut chunk_groups_map: FxIndexMap<
414            ChunkGroupKey,
415            (ChunkGroupId, FxIndexSet<ResolvedVc<Box<dyn Module>>>),
416        > = FxIndexMap::default();
417
418        // For each module, the indices in the bitmap store which chunk groups in `chunk_groups_map`
419        // that module is part of.
420        let mut module_chunk_groups: FxHashMap<ResolvedVc<Box<dyn Module>>, RoaringBitmapWrapper> =
421            FxHashMap::default();
422
423        let module_count = graph
424            .graphs
425            .iter()
426            .map(|g| g.graph.node_count())
427            .sum::<usize>();
428        span.record("module_count", module_count);
429
430        // use all entries from all graphs
431        let entries = graph.all_chunk_group_entries().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),
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),
504                        merge_tag,
505                    }
506                }
507            }
508        }
509
510        let entry_chunk_group_keys = entries
511            .iter()
512            .flat_map(|&chunk_group| {
513                let chunk_group_key =
514                    entry_to_chunk_group_id(chunk_group.clone(), &mut chunk_groups_map);
515                chunk_group
516                    .entries()
517                    .map(move |e| (e, chunk_group_key.clone()))
518            })
519            .collect::<FxHashMap<_, _>>();
520
521        let visit_count = graph.traverse_edges_fixed_point_with_priority(
522            entries
523                .iter()
524                .flat_map(|e| e.entries())
525                .map(|e| {
526                    Ok((
527                        e,
528                        TraversalPriority {
529                            depth: *module_depth.get(&e).context("Module depth not found")?,
530                            chunk_group_len: 0,
531                        },
532                    ))
533                })
534                .collect::<Result<Vec<_>>>()?,
535            &mut module_chunk_groups,
536            |parent_info: Option<(ResolvedVc<Box<dyn Module>>, &'_ RefData, _)>,
537             node: ResolvedVc<Box<dyn Module>>,
538             module_chunk_groups: &mut FxHashMap<
539                ResolvedVc<Box<dyn Module>>,
540                RoaringBitmapWrapper,
541            >|
542             -> Result<GraphTraversalAction> {
543                enum ChunkGroupInheritance<It: Iterator<Item = ChunkGroupKey>> {
544                    Inherit(ResolvedVc<Box<dyn Module>>),
545                    ChunkGroup(It),
546                }
547                let chunk_groups = if let Some((parent, ref_data, _)) = parent_info {
548                    match &ref_data.chunking_type {
549                        ChunkingType::Parallel { .. } => ChunkGroupInheritance::Inherit(parent),
550                        ChunkingType::Async => ChunkGroupInheritance::ChunkGroup(Either::Left(
551                            std::iter::once(ChunkGroupKey::Async(node)),
552                        )),
553                        ChunkingType::Isolated {
554                            merge_tag: None, ..
555                        } => ChunkGroupInheritance::ChunkGroup(Either::Left(std::iter::once(
556                            ChunkGroupKey::Isolated(node),
557                        ))),
558                        ChunkingType::Shared {
559                            merge_tag: None, ..
560                        } => ChunkGroupInheritance::ChunkGroup(Either::Left(std::iter::once(
561                            ChunkGroupKey::Shared(node),
562                        ))),
563                        ChunkingType::Isolated {
564                            merge_tag: Some(merge_tag),
565                            ..
566                        } => {
567                            let parents = module_chunk_groups
568                                .get(&parent)
569                                .context("Module chunk group not found")?;
570                            let chunk_groups =
571                                parents.iter().map(|parent| ChunkGroupKey::IsolatedMerged {
572                                    parent: ChunkGroupId(parent),
573                                    merge_tag: merge_tag.clone(),
574                                });
575                            ChunkGroupInheritance::ChunkGroup(Either::Right(Either::Left(
576                                chunk_groups,
577                            )))
578                        }
579                        ChunkingType::Shared {
580                            merge_tag: Some(merge_tag),
581                            ..
582                        } => {
583                            let parents = module_chunk_groups
584                                .get(&parent)
585                                .context("Module chunk group not found")?;
586                            let chunk_groups =
587                                parents.iter().map(|parent| ChunkGroupKey::SharedMerged {
588                                    parent: ChunkGroupId(parent),
589                                    merge_tag: merge_tag.clone(),
590                                });
591                            ChunkGroupInheritance::ChunkGroup(Either::Right(Either::Right(
592                                chunk_groups,
593                            )))
594                        }
595                        ChunkingType::Traced { .. } => {
596                            // Traced modules are not placed in chunk groups
597                            return Ok(GraphTraversalAction::Skip);
598                        }
599                    }
600                } else {
601                    ChunkGroupInheritance::ChunkGroup(Either::Left(std::iter::once(
602                        // TODO remove clone
603                        entry_chunk_group_keys
604                            .get(&node)
605                            .context("Module chunk group not found")?
606                            .clone(),
607                    )))
608                };
609
610                Ok(match chunk_groups {
611                    ChunkGroupInheritance::ChunkGroup(chunk_groups) => {
612                        // Start of a new chunk group, don't inherit anything from parent
613                        let chunk_group_ids = chunk_groups.map(|chunk_group| {
614                            let len = chunk_groups_map.len();
615                            let is_merged = matches!(
616                                chunk_group,
617                                ChunkGroupKey::IsolatedMerged { .. }
618                                    | ChunkGroupKey::SharedMerged { .. }
619                            );
620                            match chunk_groups_map.entry(chunk_group) {
621                                Entry::Occupied(mut e) => {
622                                    let (id, merged_entries) = e.get_mut();
623                                    if is_merged {
624                                        merged_entries.insert(node);
625                                    }
626                                    **id
627                                }
628                                Entry::Vacant(e) => {
629                                    let chunk_group_id = len as u32;
630                                    let mut set = FxIndexSet::default();
631                                    if is_merged {
632                                        set.insert(node);
633                                    }
634                                    e.insert((ChunkGroupId(chunk_group_id), set));
635                                    chunk_group_id
636                                }
637                            }
638                        });
639
640                        let chunk_groups =
641                            RoaringBitmapWrapper(RoaringBitmap::from_iter(chunk_group_ids));
642
643                        // Assign chunk group to the target node (the entry of the chunk group)
644                        let bitset = module_chunk_groups
645                            .get_mut(&node)
646                            .context("Module chunk group not found")?;
647                        if chunk_groups.is_proper_superset(bitset) {
648                            // Add bits from parent, and continue traversal because changed
649                            **bitset |= chunk_groups.into_inner();
650
651                            GraphTraversalAction::Continue
652                        } else {
653                            // Unchanged, no need to forward to children
654                            GraphTraversalAction::Skip
655                        }
656                    }
657                    ChunkGroupInheritance::Inherit(parent) => {
658                        // Inherit chunk groups from parent, merge parent chunk groups into
659                        // current
660
661                        if parent == node {
662                            // A self-reference
663                            GraphTraversalAction::Skip
664                        } else {
665                            let [Some(parent_chunk_groups), Some(current_chunk_groups)] =
666                                module_chunk_groups.get_disjoint_mut([&parent, &node])
667                            else {
668                                // All modules are inserted in the previous iteration
669                                // Technically unreachable, but could be reached due to eventual
670                                // consistency
671                                bail!("Module chunk groups not found");
672                            };
673
674                            if current_chunk_groups.is_empty() {
675                                // Initial visit, clone instead of merging
676                                *current_chunk_groups = parent_chunk_groups.clone();
677                                GraphTraversalAction::Continue
678                            } else if parent_chunk_groups.is_proper_superset(current_chunk_groups) {
679                                // Add bits from parent, and continue traversal because changed
680                                **current_chunk_groups |= &**parent_chunk_groups;
681                                GraphTraversalAction::Continue
682                            } else {
683                                // Unchanged, no need to forward to children
684                                GraphTraversalAction::Skip
685                            }
686                        }
687                    }
688                })
689            },
690            // This priority is used as a heuristic to keep the number of retraversals down, by
691            // - keeping it similar to a BFS via the depth priority
692            // - prioritizing smaller chunk groups which are expected to themselves reference
693            //   bigger chunk groups (i.e. shared code deeper down in the graph).
694            //
695            // Both try to first visit modules with a large dependency subgraph first (which
696            // would be higher in the graph and are included by few chunks themselves).
697            |successor, module_chunk_groups| {
698                Ok(TraversalPriority {
699                    depth: *module_depth
700                        .get(&successor)
701                        .context("Module depth not found")?,
702                    chunk_group_len: module_chunk_groups
703                        .get(&successor)
704                        .context("Module chunk group not found")?
705                        .len(),
706                })
707            },
708        )?;
709
710        span.record("visit_count", visit_count);
711        span.record("chunk_group_count", chunk_groups_map.len());
712
713        #[cfg(debug_assertions)]
714        {
715            use std::sync::LazyLock;
716            static PRINT_CHUNK_GROUP_INFO: LazyLock<bool> =
717                LazyLock::new(|| match std::env::var_os("TURBOPACK_PRINT_CHUNK_GROUPS") {
718                    Some(v) => v == "1",
719                    None => false,
720                });
721            if *PRINT_CHUNK_GROUP_INFO {
722                use std::{
723                    collections::{BTreeMap, BTreeSet},
724                    path::absolute,
725                };
726
727                let mut buckets = BTreeMap::default();
728                for (module, key) in &module_chunk_groups {
729                    if !key.is_empty() {
730                        buckets
731                            .entry(key.iter().collect::<Vec<_>>())
732                            .or_insert(BTreeSet::new())
733                            .insert(module.ident().to_string().await?);
734                    }
735                }
736
737                let mut result = vec![];
738                result.push("Chunk Groups:".to_string());
739                for (i, (key, _)) in chunk_groups_map.iter().enumerate() {
740                    result.push(format!(
741                        "  {:?}: {}",
742                        i,
743                        key.debug_str(chunk_groups_map.keys()).await?
744                    ));
745                }
746                result.push("# Module buckets:".to_string());
747                for (key, modules) in buckets.iter() {
748                    result.push(format!("## {:?}:", key.iter().collect::<Vec<_>>()));
749                    for module in modules {
750                        result.push(format!("  {module}"));
751                    }
752                    result.push("".to_string());
753                }
754                let f = absolute("chunk_group_info.log")?;
755                println!("written to {}", f.display());
756                std::fs::write(f, result.join("\n"))?;
757            }
758        }
759
760        Ok(ChunkGroupInfo {
761            module_chunk_groups: ResolvedVc::cell(module_chunk_groups),
762            chunk_group_keys: chunk_groups_map.keys().cloned().collect(),
763            chunk_groups: chunk_groups_map
764                .into_iter()
765                .map(|(k, (_, merged_entries))| match k {
766                    ChunkGroupKey::Entry(entries) => ChunkGroup::Entry(entries),
767                    ChunkGroupKey::Async(module) => ChunkGroup::Async(module),
768                    ChunkGroupKey::Isolated(module) => ChunkGroup::Isolated(module),
769                    ChunkGroupKey::IsolatedMerged { parent, merge_tag } => {
770                        ChunkGroup::IsolatedMerged {
771                            parent: parent.0 as usize,
772                            merge_tag,
773                            entries: merged_entries.into_iter().collect(),
774                        }
775                    }
776                    ChunkGroupKey::Shared(module) => ChunkGroup::Shared(module),
777                    ChunkGroupKey::SharedMultiple(entries) => ChunkGroup::SharedMultiple(entries),
778                    ChunkGroupKey::SharedMerged { parent, merge_tag } => ChunkGroup::SharedMerged {
779                        parent: parent.0 as usize,
780                        merge_tag,
781                        entries: merged_entries.into_iter().collect(),
782                    },
783                })
784                .collect(),
785        }
786        .cell())
787    }
788    .instrument(span_outer)
789    .await
790}