Skip to main content

turbopack_core/module_graph/
chunk_group_info.rs

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