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