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