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#[derive(
131    Debug, Clone, Hash, TaskInput, PartialEq, Eq, TraceRawVcs, NonLocalValue, Encode, Decode,
132)]
133pub enum ChunkGroupEntry {
134    Entry(Vec<ResolvedVc<Box<dyn Module>>>),
135    Async(ResolvedVc<Box<dyn Module>>),
136    Isolated(ResolvedVc<Box<dyn Module>>),
137    IsolatedMerged {
138        parent: Box<ChunkGroupEntry>,
139        merge_tag: RcStr,
140        entries: Vec<ResolvedVc<Box<dyn Module>>>,
141    },
142    Shared(ResolvedVc<Box<dyn Module>>),
143    SharedMultiple(Vec<ResolvedVc<Box<dyn Module>>>),
144    SharedMerged {
145        parent: Box<ChunkGroupEntry>,
146        merge_tag: RcStr,
147        entries: Vec<ResolvedVc<Box<dyn Module>>>,
148    },
149}
150impl ChunkGroupEntry {
151    pub fn entries(&self) -> impl Iterator<Item = ResolvedVc<Box<dyn Module>>> + '_ {
152        match self {
153            Self::Async(e) | Self::Isolated(e) | Self::Shared(e) => {
154                Either::Left(std::iter::once(*e))
155            }
156            Self::Entry(entries)
157            | Self::IsolatedMerged { entries, .. }
158            | Self::SharedMultiple(entries)
159            | Self::SharedMerged { entries, .. } => Either::Right(entries.iter().copied()),
160        }
161    }
162}
163
164#[derive(Debug, Clone, Hash, TaskInput, 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
432            .graphs
433            .iter()
434            .flat_map(|g| g.entries.iter())
435            .collect::<Vec<_>>();
436
437        // First, compute the depth for each module in the graph
438        let module_depth: FxHashMap<ResolvedVc<Box<dyn Module>>, usize> = {
439            let mut module_depth =
440                FxHashMap::with_capacity_and_hasher(module_count, Default::default());
441            graph.traverse_edges_bfs(
442                entries.iter().flat_map(|e| e.entries()),
443                |parent, node| {
444                    if let Some((parent, _)) = parent {
445                        let parent_depth = *module_depth
446                            .get(&parent)
447                            .context("Module depth not found")?;
448                        module_depth.entry(node).or_insert(parent_depth + 1);
449                    } else {
450                        module_depth.insert(node, 0);
451                    };
452
453                    module_chunk_groups.insert(node, RoaringBitmapWrapper::default());
454
455                    Ok(GraphTraversalAction::Continue)
456                },
457            )?;
458            module_depth
459        };
460
461        // ----
462
463        #[allow(clippy::type_complexity)]
464        fn entry_to_chunk_group_id(
465            entry: ChunkGroupEntry,
466            chunk_groups_map: &mut FxIndexMap<
467                ChunkGroupKey,
468                (ChunkGroupId, FxIndexSet<ResolvedVc<Box<dyn Module>>>),
469            >,
470        ) -> ChunkGroupKey {
471            match entry {
472                ChunkGroupEntry::Entry(entries) => ChunkGroupKey::Entry(entries),
473                ChunkGroupEntry::Async(entry) => ChunkGroupKey::Async(entry),
474                ChunkGroupEntry::Isolated(entry) => ChunkGroupKey::Isolated(entry),
475                ChunkGroupEntry::Shared(entry) => ChunkGroupKey::Shared(entry),
476                ChunkGroupEntry::SharedMultiple(entries) => ChunkGroupKey::SharedMultiple(entries),
477                ChunkGroupEntry::IsolatedMerged {
478                    parent,
479                    merge_tag,
480                    entries: _,
481                } => {
482                    let parent = entry_to_chunk_group_id(*parent, chunk_groups_map);
483                    let len = chunk_groups_map.len();
484                    let parent = chunk_groups_map
485                        .entry(parent)
486                        .or_insert_with(|| (ChunkGroupId(len as u32), FxIndexSet::default()))
487                        .0;
488
489                    ChunkGroupKey::IsolatedMerged {
490                        parent: ChunkGroupId(*parent),
491                        merge_tag,
492                    }
493                }
494                ChunkGroupEntry::SharedMerged {
495                    parent,
496                    merge_tag,
497                    entries: _,
498                } => {
499                    let parent = entry_to_chunk_group_id(*parent, chunk_groups_map);
500                    let len = chunk_groups_map.len();
501                    let parent = chunk_groups_map
502                        .entry(parent)
503                        .or_insert_with(|| (ChunkGroupId(len as u32), FxIndexSet::default()))
504                        .0;
505
506                    ChunkGroupKey::SharedMerged {
507                        parent: ChunkGroupId(*parent),
508                        merge_tag,
509                    }
510                }
511            }
512        }
513
514        let entry_chunk_group_keys = graph
515            .graphs
516            .iter()
517            .flat_map(|g| g.entries.iter())
518            .flat_map(|chunk_group| {
519                let chunk_group_key =
520                    entry_to_chunk_group_id(chunk_group.clone(), &mut chunk_groups_map);
521                chunk_group
522                    .entries()
523                    .map(move |e| (e, chunk_group_key.clone()))
524            })
525            .collect::<FxHashMap<_, _>>();
526
527        let visit_count = graph.traverse_edges_fixed_point_with_priority(
528            entries
529                .iter()
530                .flat_map(|e| e.entries())
531                .map(|e| {
532                    Ok((
533                        e,
534                        TraversalPriority {
535                            depth: *module_depth.get(&e).context("Module depth not found")?,
536                            chunk_group_len: 0,
537                        },
538                    ))
539                })
540                .collect::<Result<Vec<_>>>()?,
541            &mut module_chunk_groups,
542            |parent_info: Option<(ResolvedVc<Box<dyn Module>>, &'_ RefData, _)>,
543             node: ResolvedVc<Box<dyn Module>>,
544             module_chunk_groups: &mut FxHashMap<
545                ResolvedVc<Box<dyn Module>>,
546                RoaringBitmapWrapper,
547            >|
548             -> Result<GraphTraversalAction> {
549                enum ChunkGroupInheritance<It: Iterator<Item = ChunkGroupKey>> {
550                    Inherit(ResolvedVc<Box<dyn Module>>),
551                    ChunkGroup(It),
552                }
553                let chunk_groups = if let Some((parent, ref_data, _)) = parent_info {
554                    match &ref_data.chunking_type {
555                        ChunkingType::Parallel { .. } => ChunkGroupInheritance::Inherit(parent),
556                        ChunkingType::Async => ChunkGroupInheritance::ChunkGroup(Either::Left(
557                            std::iter::once(ChunkGroupKey::Async(node)),
558                        )),
559                        ChunkingType::Isolated {
560                            merge_tag: None, ..
561                        } => ChunkGroupInheritance::ChunkGroup(Either::Left(std::iter::once(
562                            ChunkGroupKey::Isolated(node),
563                        ))),
564                        ChunkingType::Shared {
565                            merge_tag: None, ..
566                        } => ChunkGroupInheritance::ChunkGroup(Either::Left(std::iter::once(
567                            ChunkGroupKey::Shared(node),
568                        ))),
569                        ChunkingType::Isolated {
570                            merge_tag: Some(merge_tag),
571                            ..
572                        } => {
573                            let parents = module_chunk_groups
574                                .get(&parent)
575                                .context("Module chunk group not found")?;
576                            let chunk_groups =
577                                parents.iter().map(|parent| ChunkGroupKey::IsolatedMerged {
578                                    parent: ChunkGroupId(parent),
579                                    merge_tag: merge_tag.clone(),
580                                });
581                            ChunkGroupInheritance::ChunkGroup(Either::Right(Either::Left(
582                                chunk_groups,
583                            )))
584                        }
585                        ChunkingType::Shared {
586                            merge_tag: Some(merge_tag),
587                            ..
588                        } => {
589                            let parents = module_chunk_groups
590                                .get(&parent)
591                                .context("Module chunk group not found")?;
592                            let chunk_groups =
593                                parents.iter().map(|parent| ChunkGroupKey::SharedMerged {
594                                    parent: ChunkGroupId(parent),
595                                    merge_tag: merge_tag.clone(),
596                                });
597                            ChunkGroupInheritance::ChunkGroup(Either::Right(Either::Right(
598                                chunk_groups,
599                            )))
600                        }
601                        ChunkingType::Traced => {
602                            // Traced modules are not placed in chunk groups
603                            return Ok(GraphTraversalAction::Skip);
604                        }
605                    }
606                } else {
607                    ChunkGroupInheritance::ChunkGroup(Either::Left(std::iter::once(
608                        // TODO remove clone
609                        entry_chunk_group_keys
610                            .get(&node)
611                            .context("Module chunk group not found")?
612                            .clone(),
613                    )))
614                };
615
616                Ok(match chunk_groups {
617                    ChunkGroupInheritance::ChunkGroup(chunk_groups) => {
618                        // Start of a new chunk group, don't inherit anything from parent
619                        let chunk_group_ids = chunk_groups.map(|chunk_group| {
620                            let len = chunk_groups_map.len();
621                            let is_merged = matches!(
622                                chunk_group,
623                                ChunkGroupKey::IsolatedMerged { .. }
624                                    | ChunkGroupKey::SharedMerged { .. }
625                            );
626                            match chunk_groups_map.entry(chunk_group) {
627                                Entry::Occupied(mut e) => {
628                                    let (id, merged_entries) = e.get_mut();
629                                    if is_merged {
630                                        merged_entries.insert(node);
631                                    }
632                                    **id
633                                }
634                                Entry::Vacant(e) => {
635                                    let chunk_group_id = len as u32;
636                                    let mut set = FxIndexSet::default();
637                                    if is_merged {
638                                        set.insert(node);
639                                    }
640                                    e.insert((ChunkGroupId(chunk_group_id), set));
641                                    chunk_group_id
642                                }
643                            }
644                        });
645
646                        let chunk_groups =
647                            RoaringBitmapWrapper(RoaringBitmap::from_iter(chunk_group_ids));
648
649                        // Assign chunk group to the target node (the entry of the chunk group)
650                        let bitset = module_chunk_groups
651                            .get_mut(&node)
652                            .context("Module chunk group not found")?;
653                        if chunk_groups.is_proper_superset(bitset) {
654                            // Add bits from parent, and continue traversal because changed
655                            **bitset |= chunk_groups.into_inner();
656
657                            GraphTraversalAction::Continue
658                        } else {
659                            // Unchanged, no need to forward to children
660                            GraphTraversalAction::Skip
661                        }
662                    }
663                    ChunkGroupInheritance::Inherit(parent) => {
664                        // Inherit chunk groups from parent, merge parent chunk groups into
665                        // current
666
667                        if parent == node {
668                            // A self-reference
669                            GraphTraversalAction::Skip
670                        } else {
671                            let [Some(parent_chunk_groups), Some(current_chunk_groups)] =
672                                module_chunk_groups.get_disjoint_mut([&parent, &node])
673                            else {
674                                // All modules are inserted in the previous iteration
675                                // Technically unreachable, but could be reached due to eventual
676                                // consistency
677                                bail!("Module chunk groups not found");
678                            };
679
680                            if current_chunk_groups.is_empty() {
681                                // Initial visit, clone instead of merging
682                                *current_chunk_groups = parent_chunk_groups.clone();
683                                GraphTraversalAction::Continue
684                            } else if parent_chunk_groups.is_proper_superset(current_chunk_groups) {
685                                // Add bits from parent, and continue traversal because changed
686                                **current_chunk_groups |= &**parent_chunk_groups;
687                                GraphTraversalAction::Continue
688                            } else {
689                                // Unchanged, no need to forward to children
690                                GraphTraversalAction::Skip
691                            }
692                        }
693                    }
694                })
695            },
696            // This priority is used as a heuristic to keep the number of retraversals down, by
697            // - keeping it similar to a BFS via the depth priority
698            // - prioritizing smaller chunk groups which are expected to themselves reference
699            //   bigger chunk groups (i.e. shared code deeper down in the graph).
700            //
701            // Both try to first visit modules with a large dependency subgraph first (which
702            // would be higher in the graph and are included by few chunks themselves).
703            |successor, module_chunk_groups| {
704                Ok(TraversalPriority {
705                    depth: *module_depth
706                        .get(&successor)
707                        .context("Module depth not found")?,
708                    chunk_group_len: module_chunk_groups
709                        .get(&successor)
710                        .context("Module chunk group not found")?
711                        .len(),
712                })
713            },
714        )?;
715
716        span.record("visit_count", visit_count);
717        span.record("chunk_group_count", chunk_groups_map.len());
718
719        #[cfg(debug_assertions)]
720        {
721            use std::sync::LazyLock;
722            static PRINT_CHUNK_GROUP_INFO: LazyLock<bool> =
723                LazyLock::new(|| match std::env::var_os("TURBOPACK_PRINT_CHUNK_GROUPS") {
724                    Some(v) => v == "1",
725                    None => false,
726                });
727            if *PRINT_CHUNK_GROUP_INFO {
728                use std::{
729                    collections::{BTreeMap, BTreeSet},
730                    path::absolute,
731                };
732
733                let mut buckets = BTreeMap::default();
734                for (module, key) in &module_chunk_groups {
735                    if !key.is_empty() {
736                        buckets
737                            .entry(key.iter().collect::<Vec<_>>())
738                            .or_insert(BTreeSet::new())
739                            .insert(module.ident().to_string().await?);
740                    }
741                }
742
743                let mut result = vec![];
744                result.push("Chunk Groups:".to_string());
745                for (i, (key, _)) in chunk_groups_map.iter().enumerate() {
746                    result.push(format!(
747                        "  {:?}: {}",
748                        i,
749                        key.debug_str(chunk_groups_map.keys()).await?
750                    ));
751                }
752                result.push("# Module buckets:".to_string());
753                for (key, modules) in buckets.iter() {
754                    result.push(format!("## {:?}:", key.iter().collect::<Vec<_>>()));
755                    for module in modules {
756                        result.push(format!("  {module}"));
757                    }
758                    result.push("".to_string());
759                }
760                let f = absolute("chunk_group_info.log")?;
761                println!("written to {}", f.display());
762                std::fs::write(f, result.join("\n"))?;
763            }
764        }
765
766        Ok(ChunkGroupInfo {
767            module_chunk_groups: ResolvedVc::cell(module_chunk_groups),
768            chunk_group_keys: chunk_groups_map.keys().cloned().collect(),
769            chunk_groups: chunk_groups_map
770                .into_iter()
771                .map(|(k, (_, merged_entries))| match k {
772                    ChunkGroupKey::Entry(entries) => ChunkGroup::Entry(entries),
773                    ChunkGroupKey::Async(module) => ChunkGroup::Async(module),
774                    ChunkGroupKey::Isolated(module) => ChunkGroup::Isolated(module),
775                    ChunkGroupKey::IsolatedMerged { parent, merge_tag } => {
776                        ChunkGroup::IsolatedMerged {
777                            parent: parent.0 as usize,
778                            merge_tag,
779                            entries: merged_entries.into_iter().collect(),
780                        }
781                    }
782                    ChunkGroupKey::Shared(module) => ChunkGroup::Shared(module),
783                    ChunkGroupKey::SharedMultiple(entries) => ChunkGroup::SharedMultiple(entries),
784                    ChunkGroupKey::SharedMerged { parent, merge_tag } => ChunkGroup::SharedMerged {
785                        parent: parent.0 as usize,
786                        merge_tag,
787                        entries: merged_entries.into_iter().collect(),
788                    },
789                })
790                .collect(),
791        }
792        .cell())
793    }
794    .instrument(span_outer)
795    .await
796}