Skip to main content

turbopack_core/module_graph/
module_batches.rs

1use std::{
2    collections::{VecDeque, hash_map::Entry},
3    hash::BuildHasherDefault,
4    mem::take,
5};
6
7use anyhow::{Context, Result, bail};
8use bincode::{Decode, Encode};
9use either::Either;
10use itertools::Itertools;
11use petgraph::graph::{DiGraph, EdgeIndex, NodeIndex};
12use rustc_hash::{FxHashMap, FxHashSet, FxHasher};
13use serde::{Deserialize, Serialize};
14use tracing::Instrument;
15use turbo_prehash::BuildHasherExt;
16use turbo_tasks::{
17    FxIndexMap, FxIndexSet, JoinIterExt, NonLocalValue, ResolvedVc, TryJoinIterExt, ValueToString,
18    Vc, turbobail,
19};
20
21use crate::{
22    chunk::{ChunkableModule, ChunkingType},
23    module::Module,
24    module_graph::{
25        GraphTraversalAction, ModuleGraph,
26        chunk_group_info::{ChunkGroupInfo, ChunkGroupKey, RoaringBitmapWrapper},
27        module_batch::{ModuleBatch, ModuleBatchGroup, ModuleOrBatch},
28        traced_di_graph::{TracedDiGraph, iter_neighbors_rev},
29    },
30};
31#[turbo_tasks::value(task_input)]
32#[derive(Debug, Clone, Default, Hash)]
33pub struct BatchingConfig {
34    /// Use a heuristic based on the module path to create batches. It aims for batches of a good
35    /// size.
36    pub use_heuristic: bool,
37}
38
39#[turbo_tasks::value_impl]
40impl BatchingConfig {
41    #[turbo_tasks::function]
42    pub fn new(config: BatchingConfig) -> Vc<Self> {
43        config.cell()
44    }
45}
46
47#[derive(Debug, Clone, Serialize, Deserialize, NonLocalValue)]
48pub struct ModuleBatchesGraphEdge {
49    pub ty: ChunkingType,
50    pub module: Option<ResolvedVc<Box<dyn Module>>>,
51    /// If set, this edge is only active when chunking for this page entry.
52    pub active_for_page_entry: Option<ResolvedVc<Box<dyn Module>>>,
53}
54
55#[derive(Debug, Clone, NonLocalValue, Encode, Decode)]
56struct EntriesList(
57    #[bincode(with = "turbo_bincode::indexset")] pub FxIndexSet<ResolvedVc<Box<dyn Module>>>,
58);
59
60#[turbo_tasks::value(cell = "new", eq = "manual")]
61pub struct ModuleBatchesGraph {
62    graph: TracedDiGraph<ModuleOrBatch, ModuleBatchesGraphEdge>,
63
64    // NodeIndex isn't necessarily stable (because of swap_remove), but we never remove nodes.
65    //
66    // HashMaps have nondeterministic order, but this map is only used for lookups and not
67    // iteration.
68    //
69    // This contains Vcs, but they are already contained in the graph.
70    #[turbo_tasks(unsafe_ignore)]
71    #[bincode(with_serde)]
72    entries: FxHashMap<ResolvedVc<Box<dyn Module>>, NodeIndex>,
73    batch_groups: FxHashMap<ModuleOrBatch, ResolvedVc<ModuleBatchGroup>>,
74
75    /// For chunk groups where the postorder of entries is different than the order of the
76    /// `ChunkGroup::entries()` this contains Some with the postorder list of entries of that chunk
77    /// group. The index in this list corresponds to the index in the
78    /// chunk_group_info.chunk_groups.
79    ordered_entries: Vec<Option<EntriesList>>,
80}
81
82impl ModuleBatchesGraph {
83    pub async fn get_entry_index(&self, entry: ResolvedVc<Box<dyn Module>>) -> Result<NodeIndex> {
84        let Some(entry) = self.entries.get(&entry) else {
85            if cfg!(debug_assertions) {
86                let possible_entries = format!(
87                    "{:#?}",
88                    self.entries
89                        .keys()
90                        .map(|e| e.ident().to_string())
91                        .try_join()
92                        .await?
93                );
94                turbobail!(
95                    "Entry {} is not in graph (possible entries: {})",
96                    entry.ident(),
97                    possible_entries
98                );
99            } else {
100                bail!("Entry is not in graph");
101            }
102        };
103        Ok(*entry)
104    }
105
106    pub fn get_ordered_entries<'l>(
107        &'l self,
108        chunk_group_info: &'l ChunkGroupInfo,
109        idx: usize,
110    ) -> impl Iterator<Item = ResolvedVc<Box<dyn Module>>> + 'l {
111        if let Some(EntriesList(ordered_entries)) = self
112            .ordered_entries
113            .get(idx)
114            .as_ref()
115            .and_then(|o| o.as_ref())
116        {
117            if let Some(chunk_group) = chunk_group_info.chunk_groups.get_index(idx) {
118                debug_assert_eq!(ordered_entries.len(), chunk_group.entries_count());
119            }
120            Either::Left(Either::Left(ordered_entries.iter().copied()))
121        } else if let Some(chunk_group) = chunk_group_info.chunk_groups.get_index(idx) {
122            Either::Right(chunk_group.entries())
123        } else {
124            Either::Left(Either::Right(std::iter::empty()))
125        }
126    }
127
128    pub fn get_batch_group(
129        &self,
130        module_or_batch: &ModuleOrBatch,
131    ) -> Option<ResolvedVc<ModuleBatchGroup>> {
132        self.batch_groups.get(module_or_batch).copied()
133    }
134
135    pub async fn get_entry(&self, entry: ResolvedVc<Box<dyn Module>>) -> Result<ModuleOrBatch> {
136        let entry = self.get_entry_index(entry).await?;
137        Ok(*self.graph.node_weight(entry).unwrap())
138    }
139
140    // Clippy complains but there's a type error without the bound
141    #[allow(clippy::implied_bounds_in_impls)]
142    /// Traverses all reachable edges in dfs order. The preorder visitor can be used to
143    /// forward state down the graph, and to skip subgraphs
144    ///
145    /// Use this to collect batches/modules in evaluation order.
146    ///
147    /// Target nodes can be revisited (once per incoming edge).
148    /// Edges are traversed in normal order, so should correspond to reference order.
149    ///
150    /// * `entries` - The entry modules to start the traversal from
151    /// * `state` - The state to be passed to the visitors
152    /// * `visit_preorder` - Called before visiting the children of a node.
153    ///    - Receives: (originating &ModuleBatchesGraphNode, edge &ChunkingType), target
154    ///      &ModuleBatchesGraphNode, state &S
155    ///    - Can return [GraphTraversalAction]s to control the traversal
156    /// * `visit_postorder` - Called after visiting the children of a node. Return
157    ///    - Receives: (originating &ModuleBatchesGraphNode, edge &ChunkingType), target
158    ///      &ModuleBatchesGraphNode, state &S
159    pub fn traverse_edges_from_entries_dfs<'a, S>(
160        &'a self,
161        entries: impl IntoIterator<
162            Item = NodeIndex,
163            IntoIter = impl Iterator<Item = NodeIndex> + DoubleEndedIterator,
164        >,
165        active_page_entries: Option<&FxHashSet<ResolvedVc<Box<dyn Module>>>>,
166        state: &mut S,
167        mut visit_preorder: impl FnMut(
168            Option<(&'a ModuleOrBatch, &'a ModuleBatchesGraphEdge)>,
169            &'a ModuleOrBatch,
170            &mut S,
171        ) -> Result<GraphTraversalAction>,
172        mut visit_postorder: impl FnMut(
173            Option<(&'a ModuleOrBatch, &'a ModuleBatchesGraphEdge)>,
174            &'a ModuleOrBatch,
175            &mut S,
176        ),
177    ) -> Result<()> {
178        let graph = &self.graph;
179
180        enum ReverseDFSPass {
181            Visit,
182            ExpandAndVisit,
183        }
184
185        let entries = entries.into_iter();
186        #[allow(clippy::type_complexity)] // This is a temporary internal structure
187        let mut stack: Vec<(ReverseDFSPass, Option<(NodeIndex, EdgeIndex)>, NodeIndex)> = entries
188            .rev()
189            .map(|e| (ReverseDFSPass::ExpandAndVisit, None, e))
190            .collect();
191        let mut expanded = FxHashSet::default();
192        while let Some((pass, parent, current)) = stack.pop() {
193            let parent_arg = parent.map(|(node, edge)| {
194                (
195                    graph.node_weight(node).unwrap(),
196                    graph.edge_weight(edge).unwrap(),
197                )
198            });
199            match pass {
200                ReverseDFSPass::Visit => {
201                    let current_node = graph.node_weight(current).unwrap();
202                    visit_postorder(parent_arg, current_node, state);
203                }
204                ReverseDFSPass::ExpandAndVisit => {
205                    let current_node = graph.node_weight(current).unwrap();
206                    let action = visit_preorder(parent_arg, current_node, state)?;
207                    if action == GraphTraversalAction::Exclude {
208                        continue;
209                    }
210                    stack.push((ReverseDFSPass::Visit, parent, current));
211                    if action == GraphTraversalAction::Continue && expanded.insert(current) {
212                        stack.extend(
213                            iter_neighbors_rev(graph, current)
214                                .filter(|&(edge_idx, _)| {
215                                    let edge = graph.edge_weight(edge_idx).unwrap();
216                                    match &edge.active_for_page_entry {
217                                        None => true,
218                                        Some(entry) => active_page_entries
219                                            .is_some_and(|entries| entries.contains(entry)),
220                                    }
221                                })
222                                .map(|(edge, child)| {
223                                    (ReverseDFSPass::ExpandAndVisit, Some((current, edge)), child)
224                                }),
225                        );
226                    }
227                }
228            }
229        }
230
231        Ok(())
232    }
233}
234
235type PreBatchIndex = usize;
236
237#[derive(Hash, PartialEq, Eq, Clone, Debug)]
238enum PreBatchItem {
239    ParallelModule(ResolvedVc<Box<dyn Module>>),
240    ParallelReference(PreBatchIndex),
241    NonParallelEdge(ChunkingType, ResolvedVc<Box<dyn Module>>),
242}
243
244struct PreBatch {
245    items: FxIndexSet<PreBatchItem>,
246    chunk_groups: RoaringBitmapWrapper,
247}
248
249impl PreBatch {
250    fn new(chunk_groups: RoaringBitmapWrapper) -> Self {
251        Self {
252            items: FxIndexSet::default(),
253            chunk_groups,
254        }
255    }
256}
257
258struct TraversalState<'l> {
259    items: Vec<PreBatchItem>,
260    this: &'l mut PreBatches,
261}
262
263struct PreBatches {
264    boundary_modules: FxHashSet<ResolvedVc<Box<dyn Module>>>,
265    batches: Vec<PreBatch>,
266    entries: FxHashMap<ResolvedVc<Box<dyn Module>>, PreBatchIndex>,
267    single_module_entries: FxIndexSet<ResolvedVc<Box<dyn Module>>>,
268}
269
270impl PreBatches {
271    fn new() -> Self {
272        Self {
273            boundary_modules: FxHashSet::default(),
274            batches: Vec::new(),
275            entries: FxHashMap::default(),
276            single_module_entries: FxIndexSet::default(),
277        }
278    }
279
280    fn ensure_pre_batch_for_module(
281        &mut self,
282        module: ResolvedVc<Box<dyn Module>>,
283        module_chunk_groups: &FxHashMap<ResolvedVc<Box<dyn Module>>, RoaringBitmapWrapper>,
284        queue: &mut VecDeque<(ResolvedVc<Box<dyn Module>>, PreBatchIndex)>,
285    ) -> Result<PreBatchIndex> {
286        Ok(match self.entries.entry(module) {
287            Entry::Vacant(e) => {
288                let index = self.batches.len();
289                queue.push_back((module, index));
290                let chunk_groups = module_chunk_groups
291                    .get(&module)
292                    .context("all modules need to have chunk group info")?;
293                let batch = PreBatch::new((*chunk_groups).clone());
294                self.batches.push(batch);
295                e.insert(index);
296                index
297            }
298            Entry::Occupied(e) => *e.get(),
299        })
300    }
301
302    async fn get_pre_batch_items(
303        &mut self,
304        entry: ResolvedVc<Box<dyn Module>>,
305        module_chunk_groups: &FxHashMap<ResolvedVc<Box<dyn Module>>, RoaringBitmapWrapper>,
306        module_graph: &ModuleGraph,
307        queue: &mut VecDeque<(ResolvedVc<Box<dyn Module>>, PreBatchIndex)>,
308    ) -> Result<Vec<PreBatchItem>> {
309        let mut state = TraversalState {
310            items: Vec::new(),
311            this: self,
312        };
313        let mut visited = FxHashSet::default();
314        module_graph.traverse_edges_dfs(
315            std::iter::once(entry),
316            &mut state,
317            |parent_info, node, state| {
318                let ty = parent_info.map_or(
319                    &ChunkingType::Parallel {
320                        inherit_async: false,
321                        hoisted: false,
322                    },
323                    |(_, ty)| &ty.chunking_type,
324                );
325                let module = node;
326                if matches!(ty, ChunkingType::Emitted { .. }) {
327                    // they are handled via module_graph.collected_modules now
328                    return Ok(GraphTraversalAction::Exclude);
329                }
330                if !ty.is_parallel() {
331                    state.items.push(PreBatchItem::NonParallelEdge(
332                        ty.without_inherit_async(),
333                        module,
334                    ));
335                    return Ok(GraphTraversalAction::Exclude);
336                }
337                if visited.insert(module) {
338                    if parent_info.is_some() && state.this.boundary_modules.contains(&module) {
339                        let idx = state.this.ensure_pre_batch_for_module(
340                            module,
341                            module_chunk_groups,
342                            queue,
343                        )?;
344                        state.items.push(PreBatchItem::ParallelReference(idx));
345                        return Ok(GraphTraversalAction::Exclude);
346                    }
347                    Ok(GraphTraversalAction::Continue)
348                } else {
349                    Ok(GraphTraversalAction::Exclude)
350                }
351            },
352            |_, node, state| {
353                let item = PreBatchItem::ParallelModule(node);
354                state.items.push(item);
355                Ok(())
356            },
357            false,
358            false,
359        )?;
360        Ok(state.items)
361    }
362}
363
364pub async fn compute_module_batches(
365    module_graph: Vc<ModuleGraph>,
366    _config: &BatchingConfig,
367) -> Result<Vc<ModuleBatchesGraph>> {
368    let outer_span = tracing::info_span!(
369        "compute module batches",
370        initial_pre_batch_items = tracing::field::Empty,
371        initial_pre_batches = tracing::field::Empty,
372        extracted_shared_items = tracing::field::Empty,
373        batches = tracing::field::Empty,
374        modules = tracing::field::Empty,
375        edges = tracing::field::Empty
376    );
377    let span = outer_span.clone();
378    async move {
379        let chunk_group_info = module_graph.chunk_group_info().await?;
380        let module_chunk_groups = chunk_group_info.module_chunk_groups.await?;
381        let collected_modules = module_graph.collected_modules().await?;
382        let module_graph = module_graph.await?;
383
384        let mut pre_batches = PreBatches::new();
385
386        // Walk the module graph and mark all modules that are boundary modules (referenced from a
387        // different chunk group bitmap)
388        module_graph.traverse_edges_unordered(|parent, node| {
389            if let Some((parent, ty)) = parent {
390                let std::collections::hash_set::Entry::Vacant(entry) =
391                    pre_batches.boundary_modules.entry(node)
392                else {
393                    // Already a boundary module, can skip check
394                    return Ok(());
395                };
396                if matches!(ty.chunking_type, ChunkingType::Emitted { .. }) {
397                    // they are handled via module_graph.collected_modules now
398                    return Ok(());
399                } else if ty.chunking_type.is_parallel() {
400                    let parent_chunk_groups = module_chunk_groups
401                        .get(&parent)
402                        .context("all modules need to have chunk group info")?;
403                    let chunk_groups = module_chunk_groups
404                        .get(&node)
405                        .context("all modules need to have chunk group info")?;
406                    if parent_chunk_groups != chunk_groups {
407                        // This is a boundary module
408                        entry.insert();
409                    }
410                } else {
411                    entry.insert();
412                }
413            }
414            Ok(())
415        })?;
416
417        // All entries are boundary modules too
418        for chunk_group in &chunk_group_info.chunk_groups {
419            pre_batches.boundary_modules.extend(chunk_group.entries());
420        }
421
422        // All collected modules are boundary modules too
423        pre_batches.boundary_modules.extend(
424            collected_modules
425                .values()
426                .flatten()
427                .flat_map(|(_, ms)| ms.iter().map(|(_, m)| *m)),
428        );
429
430        // Pre batches would be incorrect with cycles, so we need to opt-out of pre batches for
431        // cycles that include boundary modules
432        module_graph.traverse_cycles(
433            |ref_data| ref_data.chunking_type.is_parallel(),
434            |cycle| {
435                if cycle.len() > 1
436                    && cycle
437                        .iter()
438                        .any(|node| pre_batches.boundary_modules.contains(node))
439                {
440                    pre_batches
441                        .boundary_modules
442                        .extend(cycle.iter().map(|node| **node));
443                }
444                Ok(())
445            },
446        )?;
447
448        let mut queue: VecDeque<(ResolvedVc<Box<dyn Module>>, PreBatchIndex)> = VecDeque::new();
449
450        let mut chunk_group_indices_with_merged_children = FxHashSet::default();
451
452        // Start with the entries...
453        for chunk_group in &chunk_group_info.chunk_groups {
454            for entry in chunk_group.entries() {
455                pre_batches.ensure_pre_batch_for_module(entry, &module_chunk_groups, &mut queue)?;
456            }
457            if let Some(parent) = chunk_group.get_merged_parent() {
458                chunk_group_indices_with_merged_children.insert(parent);
459            }
460        }
461        // ...and all collected modules
462        for entry in collected_modules
463            .values()
464            .flatten()
465            .flat_map(|(_, ms)| ms.iter().map(|(_, m)| *m))
466        {
467            pre_batches.ensure_pre_batch_for_module(entry, &module_chunk_groups, &mut queue)?;
468        }
469
470        let mut initial_pre_batch_items = 0;
471        // Fill all pre batches
472        while let Some((chunkable_module, idx)) = queue.pop_front() {
473            let items = pre_batches
474                .get_pre_batch_items(
475                    chunkable_module,
476                    &module_chunk_groups,
477                    &module_graph,
478                    &mut queue,
479                )
480                .await?;
481            initial_pre_batch_items += items.len();
482            let batch = &mut pre_batches.batches[idx];
483            batch.items.extend(items);
484        }
485        span.record("initial_pre_batch_items", initial_pre_batch_items);
486        span.record("initial_pre_batches", pre_batches.batches.len());
487
488        // Figure out the order of all merged groups
489        let mut ordered_entries: Vec<Option<EntriesList>> =
490            vec![None; chunk_group_info.chunk_groups.len()];
491        for (i, chunk_group) in chunk_group_info.chunk_groups.iter().enumerate() {
492            if !chunk_group_indices_with_merged_children.contains(&i) {
493                continue;
494            }
495            let mut merged_modules: FxHashMap<ChunkingType, FxIndexSet<_>> = FxHashMap::default();
496            let mut stack = ordered_entries[i]
497                .as_ref()
498                .map_or_else(
499                    || Either::Left(chunk_group.entries()),
500                    |v| Either::Right(v.0.iter().copied()),
501                )
502                .map(|module| {
503                    let idx = *pre_batches
504                        .entries
505                        .get(&module)
506                        .context("could not prebatch for module")?;
507                    Ok((idx, 0))
508                })
509                .collect::<Result<Vec<_>>>()?;
510            stack.reverse();
511            let mut visited = FxHashSet::default();
512            while let Some((idx, mut pos)) = stack.pop() {
513                let batch = &pre_batches.batches[idx];
514                while let Some(item) = batch.items.get_index(pos) {
515                    match item {
516                        PreBatchItem::ParallelModule(_) => {}
517                        PreBatchItem::ParallelReference(other_idx) => {
518                            if visited.insert(*other_idx) {
519                                stack.push((idx, pos + 1));
520                                stack.push((*other_idx, 0));
521                                break;
522                            }
523                        }
524                        PreBatchItem::NonParallelEdge(chunking_type, module) => {
525                            if chunking_type.is_merged() {
526                                merged_modules
527                                    .entry(chunking_type.clone())
528                                    .or_default()
529                                    .insert(*module);
530                            }
531                        }
532                    }
533                    pos += 1;
534                }
535            }
536            if !merged_modules.is_empty() {
537                for (ty, merged_modules) in merged_modules {
538                    let chunk_group_key = match ty {
539                        ChunkingType::Isolated {
540                            merge_tag: Some(merge_tag),
541                            ..
542                        } => ChunkGroupKey::IsolatedMerged {
543                            parent: i.into(),
544                            merge_tag: merge_tag.clone(),
545                        },
546                        ChunkingType::Shared {
547                            merge_tag: Some(merge_tag),
548                            ..
549                        } => ChunkGroupKey::SharedMerged {
550                            parent: i.into(),
551                            merge_tag: merge_tag.clone(),
552                        },
553                        _ => unreachable!(),
554                    };
555                    let idx = chunk_group_info
556                        .chunk_group_keys
557                        .get_index_of(&chunk_group_key)
558                        .context("could not find chunk group key for merged chunk group")?;
559                    ordered_entries[idx] = Some(EntriesList(merged_modules));
560                }
561            }
562        }
563
564        // Create a map of parallel module to the batches they are contained in.
565        let mut parallel_module_to_pre_batch: FxIndexMap<_, Vec<PreBatchIndex>> =
566            FxIndexMap::default();
567
568        // Modules that are only referenced via `ChunkingType::PerEntry` (i.e. collecting
569        // modules). They are chunked once per entry group via
570        // `ChunkGroupContentInner::collecting_modules`, so their chunk item depends on the entry
571        // group and can't be represented in an entry-independent, shared `ModuleBatchGroup`.
572        // They still need to be in `single_module_entries` because the graph edges below
573        // reference them by index.
574        let mut per_entry_modules: FxHashSet<ResolvedVc<Box<dyn Module>>> = FxHashSet::default();
575
576        // Fill the map and also fill up the single_module_entries
577        for (idx, pre_batch) in pre_batches.batches.iter().enumerate() {
578            for item in &pre_batch.items {
579                match item {
580                    PreBatchItem::ParallelModule(module) => {
581                        parallel_module_to_pre_batch
582                            .entry(*module)
583                            .or_default()
584                            .push(idx);
585                    }
586                    PreBatchItem::NonParallelEdge(ty, module) => {
587                        if !pre_batches.entries.contains_key(module) {
588                            pre_batches.single_module_entries.insert(*module);
589                        }
590                        if matches!(ty, ChunkingType::PerEntry) {
591                            per_entry_modules.insert(*module);
592                        }
593                    }
594                    PreBatchItem::ParallelReference(_) => {}
595                }
596            }
597        }
598
599        // We never want a module to occur in multiple batches.
600
601        let mut extracted_shared_items = 0;
602        // Extract shared modules into separate batches
603        for i in 0..parallel_module_to_pre_batch.len() {
604            let (&module, batches) = parallel_module_to_pre_batch
605                .get_index(i)
606                .context("could not find parallel module to pre batch index")?;
607            if batches.len() > 1 {
608                // Create a new batch for the shared modules
609                let batches_with_item_index = batches
610                    .iter()
611                    .map(|&idx| {
612                        let batch_items = &pre_batches.batches[idx].items;
613                        let item_idx = batch_items
614                            .get_index_of(&PreBatchItem::ParallelModule(module))
615                            .context("could not find batch item index for parallel module")?;
616                        Ok((idx, item_idx))
617                    })
618                    .collect::<Result<Vec<_>>>()?;
619                let mut selected_items = 1;
620                fn get_item_at(
621                    pre_batches: &PreBatches,
622                    batch_idx: PreBatchIndex,
623                    item_idx: usize,
624                ) -> Option<&PreBatchItem> {
625                    pre_batches.batches[batch_idx].items.get_index(item_idx)
626                }
627                // Select more matching items that are equal in all batches that contain the shared
628                // module(s)
629                loop {
630                    if let Some(PreBatchItem::ParallelModule(next_module)) = get_item_at(
631                        &pre_batches,
632                        batches_with_item_index[0].0,
633                        batches_with_item_index[0].1 + selected_items,
634                    ) && parallel_module_to_pre_batch
635                        .get(next_module)
636                        .context("could not find pre batch for parallel module")?
637                        .len()
638                        == batches.len()
639                        && batches_with_item_index[1..]
640                            .iter()
641                            .all(|&(batch_idx, item_idx)| {
642                                get_item_at(&pre_batches, batch_idx, item_idx + selected_items)
643                                    == Some(&PreBatchItem::ParallelModule(*next_module))
644                            })
645                    {
646                        selected_items += 1;
647                        continue;
648                    }
649                    break;
650                }
651                extracted_shared_items += selected_items;
652
653                // Check if a batch is completely selected. In that case we can replace all other
654                // occurrences with a reference to that batch
655                let exact_match = batches_with_item_index
656                    .iter()
657                    .find(|&&(batch_idx, item_idx)| {
658                        item_idx == 0
659                            && pre_batches.batches[batch_idx].items.len() == selected_items
660                    });
661                if let Some(&(exact_match, _)) = exact_match {
662                    // Replace all other occurrences with a reference to the exact match
663                    for &(batch_index, item_start) in batches_with_item_index.iter() {
664                        if batch_index != exact_match {
665                            pre_batches.batches[batch_index].items.splice(
666                                item_start..item_start + selected_items,
667                                std::iter::once(PreBatchItem::ParallelReference(exact_match)),
668                            );
669                        }
670                    }
671                    for item in pre_batches.batches[exact_match].items.iter() {
672                        if let PreBatchItem::ParallelModule(module) = item {
673                            parallel_module_to_pre_batch
674                                .get_mut(module)
675                                .context("could not find pre batch for parallel module")?
676                                .clear();
677                        }
678                    }
679                } else {
680                    // Create a new batch of the shared part and replace all occurrences with a
681                    // reference to that batch
682                    let first_batch_index = batches_with_item_index[0].0;
683                    let first_batch_item_index = batches_with_item_index[0].1;
684                    let new_batch_index = pre_batches.batches.len();
685                    let mut new_batch =
686                        PreBatch::new(pre_batches.batches[first_batch_index].chunk_groups.clone());
687                    new_batch
688                        .items
689                        .extend(pre_batches.batches[first_batch_index].items.splice(
690                            first_batch_item_index..first_batch_item_index + selected_items,
691                            std::iter::once(PreBatchItem::ParallelReference(new_batch_index)),
692                        ));
693                    for item in new_batch.items.iter() {
694                        if let PreBatchItem::ParallelModule(module) = item {
695                            parallel_module_to_pre_batch
696                                .get_mut(module)
697                                .context("could not find pre batch for parallel module")?
698                                .clear();
699                        }
700                    }
701                    pre_batches.batches.push(new_batch);
702                    for &(batch_index, item_start) in batches_with_item_index[1..].iter() {
703                        pre_batches.batches[batch_index].items.splice(
704                            item_start..item_start + selected_items,
705                            std::iter::once(PreBatchItem::ParallelReference(new_batch_index)),
706                        );
707                    }
708                }
709            }
710        }
711        span.record("extracted_shared_items", extracted_shared_items);
712
713        // Now every module is only in one batch
714
715        let mut edges_count = 0;
716
717        // Since batches can only have references followed by a list of parallel chunkable modules,
718        // we need to split batches that have modules before references.
719        for i in 0..pre_batches.batches.len() {
720            let items = take(&mut pre_batches.batches[i].items);
721            let mut new_items =
722                FxIndexSet::with_capacity_and_hasher(items.len(), Default::default());
723            enum Mode {
724                ParallelChunkableModule,
725                Other,
726            }
727            let mut mode = Mode::Other;
728            for item in items {
729                let chunkable_module = if let PreBatchItem::ParallelModule(module) = &item {
730                    ResolvedVc::try_downcast::<Box<dyn ChunkableModule>>(*module)
731                } else {
732                    None
733                };
734                let item = if let PreBatchItem::ParallelModule(module) = item {
735                    if chunkable_module.is_some() {
736                        PreBatchItem::ParallelModule(module)
737                    } else {
738                        pre_batches.single_module_entries.insert(module);
739                        PreBatchItem::NonParallelEdge(
740                            ChunkingType::Parallel {
741                                inherit_async: false,
742                                hoisted: false,
743                            },
744                            module,
745                        )
746                    }
747                } else {
748                    item
749                };
750                match (&mode, chunkable_module) {
751                    (_, Some(_)) => {
752                        mode = Mode::ParallelChunkableModule;
753                        new_items.insert(item);
754                    }
755                    (Mode::Other, _) => {
756                        edges_count += 1;
757                        new_items.insert(item);
758                    }
759                    (Mode::ParallelChunkableModule, _) => {
760                        // Split the batch
761                        let idx = pre_batches.batches.len();
762                        let mut new_batch =
763                            PreBatch::new(pre_batches.batches[i].chunk_groups.clone());
764                        new_batch.items.extend(new_items.drain(..));
765                        pre_batches.batches.push(new_batch);
766                        edges_count += 1;
767                        new_items.insert(PreBatchItem::ParallelReference(idx));
768                        if chunkable_module.is_some() {
769                            new_items.insert(item);
770                        } else {
771                            edges_count += 1;
772                            mode = Mode::Other;
773                            new_items.insert(item);
774                        }
775                    }
776                }
777            }
778            pre_batches.batches[i].items = new_items;
779        }
780        span.record("pre_batches", pre_batches.batches.len());
781
782        // Now batches are in the correct shape. We can create the real batches and the graph.
783
784        // Create the graph
785        let mut graph: DiGraph<ModuleOrBatch, ModuleBatchesGraphEdge, u32> =
786            petgraph::graph::DiGraph::with_capacity(
787                pre_batches.batches.len() + pre_batches.single_module_entries.len(),
788                edges_count,
789            );
790
791        // Create the Vc<ModuleBatch> instances
792        let batches = pre_batches
793            .batches
794            .iter_mut()
795            .enumerate()
796            .map(async |(i, pre_batch)| {
797                let mut modules = pre_batch.items.iter().filter_map(|item| {
798                    if let PreBatchItem::ParallelModule(module) = item {
799                        ResolvedVc::try_downcast(*module)
800                    } else {
801                        None
802                    }
803                });
804                let Some(first) = modules.next() else {
805                    return Ok(ModuleOrBatch::None(i));
806                };
807                if let Some(second) = modules.next() {
808                    let batch = ModuleBatch::new(
809                        [first, second]
810                            .into_iter()
811                            .chain(modules)
812                            .map(|m| *m)
813                            .collect::<Vec<_>>(),
814                        Some(pre_batch.chunk_groups.clone()),
815                    );
816                    Ok(ModuleOrBatch::Batch(batch.to_resolved().await?))
817                } else {
818                    Ok(ModuleOrBatch::Module(ResolvedVc::upcast(first)))
819                }
820            })
821            .try_join()
822            .await?;
823
824        // Create the batch groups by grouping batches with the same chunk groups
825        let mut batch_groups: FxHashMap<_, Vec<_>> = FxHashMap::default();
826        for (i, pre_batch) in pre_batches.batches.iter().enumerate() {
827            let key = BuildHasherDefault::<FxHasher>::default().prehash(&pre_batch.chunk_groups);
828            let batch = batches[i];
829            batch_groups.entry(key).or_default().push(batch);
830        }
831        for &module in &pre_batches.single_module_entries {
832            // Modules referenced via `ChunkingType::PerEntry` are chunked per entry group and
833            // must not become part of a (shared, entry-independent) batch group. See
834            // `per_entry_modules` above.
835            if per_entry_modules.contains(&module) {
836                continue;
837            }
838            let chunk_groups = module_chunk_groups
839                .get(&module)
840                .context("all modules need to have chunk group info")?;
841            let key = BuildHasherDefault::<FxHasher>::default().prehash(chunk_groups);
842            batch_groups
843                .entry(key)
844                .or_default()
845                .push(ModuleOrBatch::Module(module));
846        }
847
848        // Create the batch group instances
849        let batch_groups = batch_groups
850            .into_iter()
851            .map(async |(key, items)| {
852                if items.len() == 1 {
853                    anyhow::Ok(Either::Left(std::iter::empty()))
854                } else {
855                    let batch_group = ModuleBatchGroup::new(items.clone(), (*key).clone())
856                        .to_resolved()
857                        .await?;
858                    Ok(Either::Right(
859                        items.into_iter().map(move |item| (item, batch_group)),
860                    ))
861                }
862            })
863            .join()
864            .await
865            .into_iter()
866            .flatten_ok()
867            .collect::<Result<FxHashMap<_, _>>>()?;
868
869        // Insert batches into the graph and store the NodeIndices
870        let mut batches_count = 0;
871        let mut modules_count = 0;
872        let batch_indices = batches
873            .into_iter()
874            .map(|batch| {
875                match &batch {
876                    ModuleOrBatch::Batch(_) => batches_count += 1,
877                    ModuleOrBatch::Module(_) => modules_count += 1,
878                    ModuleOrBatch::None(_) => {}
879                }
880                graph.add_node(batch)
881            })
882            .collect::<Vec<_>>();
883
884        // Also insert single modules into the graph and store the NodeIndices
885        let single_module_indices = pre_batches
886            .single_module_entries
887            .iter()
888            .map(|module| graph.add_node(ModuleOrBatch::Module(*module)))
889            .collect::<Vec<_>>();
890
891        span.record("batches", batches_count);
892        modules_count += pre_batches.single_module_entries.len();
893        span.record("modules", modules_count);
894        span.record("edges", edges_count);
895
896        // Build a module -> NodeIndex lookup for all modules in the graph
897        let mut module_to_node: FxHashMap<ResolvedVc<Box<dyn Module>>, NodeIndex> =
898            FxHashMap::default();
899        for (module, batches) in &parallel_module_to_pre_batch {
900            // The values in parallel_module_to_pre_batch are guaranteed to be empty or contain
901            // exactly one batch index.
902            if let Some(&first_batch) = batches.first() {
903                module_to_node.insert(*module, batch_indices[first_batch]);
904            }
905        }
906        for (idx, module) in pre_batches.single_module_entries.iter().enumerate() {
907            module_to_node.insert(*module, single_module_indices[idx]);
908        }
909
910        // Add all the edges to the graph
911        for (i, pre_batch) in pre_batches.batches.into_iter().enumerate() {
912            let index = batch_indices[i];
913            let items = pre_batch.items;
914            for item in items {
915                match item {
916                    PreBatchItem::ParallelReference(idx) => {
917                        graph.add_edge(
918                            index,
919                            batch_indices[idx],
920                            ModuleBatchesGraphEdge {
921                                ty: ChunkingType::Parallel {
922                                    inherit_async: false,
923                                    hoisted: false,
924                                },
925                                module: None,
926                                active_for_page_entry: None,
927                            },
928                        );
929                    }
930                    PreBatchItem::NonParallelEdge(ty, module) => {
931                        if matches!(ty, ChunkingType::Emitted { .. }) {
932                            // they are handled via collected_modules now
933                            continue;
934                        }
935                        if let Some(batch) = pre_batches.entries.get(&module).copied() {
936                            graph.add_edge(
937                                index,
938                                batch_indices[batch],
939                                ModuleBatchesGraphEdge {
940                                    ty,
941                                    module: Some(module),
942                                    active_for_page_entry: None,
943                                },
944                            );
945                            continue;
946                        }
947                        let idx = pre_batches
948                            .single_module_entries
949                            .get_index_of(&module)
950                            .context("could not find single module entry index")?;
951                        let idx = single_module_indices[idx];
952                        graph.add_edge(
953                            index,
954                            idx,
955                            ModuleBatchesGraphEdge {
956                                ty,
957                                module: Some(module),
958                                active_for_page_entry: None,
959                            },
960                        );
961                    }
962                    PreBatchItem::ParallelModule(_) => {}
963                }
964            }
965        }
966
967        debug_assert_eq!(graph.capacity().0, graph.node_count());
968        debug_assert_eq!(graph.capacity().1, graph.edge_count());
969
970        // Add collected reference edges (conditional on page entry)
971        for (collecting_module, refs) in &collected_modules {
972            for (entry_modules, refs) in refs {
973                let source_node = *module_to_node
974                    .get(collecting_module)
975                    .context("could not find single module entry index")?;
976                for (ref_data, target_module) in refs {
977                    if let Some(batch) = pre_batches.entries.get(target_module).copied() {
978                        for entry_module in entry_modules {
979                            graph.add_edge(
980                                source_node,
981                                batch_indices[batch],
982                                ModuleBatchesGraphEdge {
983                                    ty: ref_data.chunking_type.clone(),
984                                    module: Some(*target_module),
985                                    active_for_page_entry: Some(*entry_module),
986                                },
987                            );
988                        }
989                        continue;
990                    }
991                    let idx = pre_batches
992                        .single_module_entries
993                        .get_index_of(target_module)
994                        .context("could not find single module entry index")?;
995                    let idx = single_module_indices[idx];
996                    for entry_module in entry_modules {
997                        graph.add_edge(
998                            source_node,
999                            idx,
1000                            ModuleBatchesGraphEdge {
1001                                ty: ref_data.chunking_type.clone(),
1002                                module: Some(*target_module),
1003                                active_for_page_entry: Some(*entry_module),
1004                            },
1005                        );
1006                    }
1007                }
1008            }
1009        }
1010
1011        // Find the NodeIndices for our entries of the graph
1012        let mut entries = FxHashMap::default();
1013        for chunk_group in &chunk_group_info.chunk_groups {
1014            for module in chunk_group.entries() {
1015                if let Some(batch) = pre_batches.entries.get(&module).copied() {
1016                    entries.insert(module, batch_indices[batch]);
1017                    continue;
1018                }
1019                let idx = pre_batches
1020                    .single_module_entries
1021                    .get_index_of(&module)
1022                    .context("could not find single module entry index")?;
1023                let idx = single_module_indices[idx];
1024                entries.insert(module, idx);
1025            }
1026        }
1027
1028        Ok(ModuleBatchesGraph {
1029            graph: TracedDiGraph(graph),
1030            entries,
1031            batch_groups,
1032            ordered_entries,
1033        }
1034        .cell())
1035    }
1036    .instrument(outer_span)
1037    .await
1038}