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, trace::TraceRawVcs, 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, TraceRawVcs, 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, TraceRawVcs, 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, so no need to trace this.
70    #[turbo_tasks(trace_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        )?;
359        Ok(state.items)
360    }
361}
362
363pub async fn compute_module_batches(
364    module_graph: Vc<ModuleGraph>,
365    _config: &BatchingConfig,
366) -> Result<Vc<ModuleBatchesGraph>> {
367    let outer_span = tracing::info_span!(
368        "compute module batches",
369        initial_pre_batch_items = tracing::field::Empty,
370        initial_pre_batches = tracing::field::Empty,
371        extracted_shared_items = tracing::field::Empty,
372        batches = tracing::field::Empty,
373        modules = tracing::field::Empty,
374        edges = tracing::field::Empty
375    );
376    let span = outer_span.clone();
377    async move {
378        let chunk_group_info = module_graph.chunk_group_info().await?;
379        let module_chunk_groups = chunk_group_info.module_chunk_groups.await?;
380        let collected_modules = module_graph.collected_modules().await?;
381        let module_graph = module_graph.await?;
382
383        let mut pre_batches = PreBatches::new();
384
385        // Walk the module graph and mark all modules that are boundary modules (referenced from a
386        // different chunk group bitmap)
387        module_graph.traverse_edges_unordered(|parent, node| {
388            if let Some((parent, ty)) = parent {
389                let std::collections::hash_set::Entry::Vacant(entry) =
390                    pre_batches.boundary_modules.entry(node)
391                else {
392                    // Already a boundary module, can skip check
393                    return Ok(());
394                };
395                if matches!(ty.chunking_type, ChunkingType::Emitted { .. }) {
396                    // they are handled via module_graph.collected_modules now
397                    return Ok(());
398                } else if ty.chunking_type.is_parallel() {
399                    let parent_chunk_groups = module_chunk_groups
400                        .get(&parent)
401                        .context("all modules need to have chunk group info")?;
402                    let chunk_groups = module_chunk_groups
403                        .get(&node)
404                        .context("all modules need to have chunk group info")?;
405                    if parent_chunk_groups != chunk_groups {
406                        // This is a boundary module
407                        entry.insert();
408                    }
409                } else {
410                    entry.insert();
411                }
412            }
413            Ok(())
414        })?;
415
416        // All entries are boundary modules too
417        for chunk_group in &chunk_group_info.chunk_groups {
418            pre_batches.boundary_modules.extend(chunk_group.entries());
419        }
420
421        // All collected modules are boundary modules too
422        pre_batches.boundary_modules.extend(
423            collected_modules
424                .values()
425                .flatten()
426                .flat_map(|(_, ms)| ms.iter().map(|(_, m)| *m)),
427        );
428
429        // Pre batches would be incorrect with cycles, so we need to opt-out of pre batches for
430        // cycles that include boundary modules
431        module_graph.traverse_cycles(
432            |ref_data| ref_data.chunking_type.is_parallel(),
433            |cycle| {
434                if cycle.len() > 1
435                    && cycle
436                        .iter()
437                        .any(|node| pre_batches.boundary_modules.contains(node))
438                {
439                    pre_batches
440                        .boundary_modules
441                        .extend(cycle.iter().map(|node| **node));
442                }
443                Ok(())
444            },
445        )?;
446
447        let mut queue: VecDeque<(ResolvedVc<Box<dyn Module>>, PreBatchIndex)> = VecDeque::new();
448
449        let mut chunk_group_indices_with_merged_children = FxHashSet::default();
450
451        // Start with the entries...
452        for chunk_group in &chunk_group_info.chunk_groups {
453            for entry in chunk_group.entries() {
454                pre_batches.ensure_pre_batch_for_module(entry, &module_chunk_groups, &mut queue)?;
455            }
456            if let Some(parent) = chunk_group.get_merged_parent() {
457                chunk_group_indices_with_merged_children.insert(parent);
458            }
459        }
460        // ...and all collected modules
461        for entry in collected_modules
462            .values()
463            .flatten()
464            .flat_map(|(_, ms)| ms.iter().map(|(_, m)| *m))
465        {
466            pre_batches.ensure_pre_batch_for_module(entry, &module_chunk_groups, &mut queue)?;
467        }
468
469        let mut initial_pre_batch_items = 0;
470        // Fill all pre batches
471        while let Some((chunkable_module, idx)) = queue.pop_front() {
472            let items = pre_batches
473                .get_pre_batch_items(
474                    chunkable_module,
475                    &module_chunk_groups,
476                    &module_graph,
477                    &mut queue,
478                )
479                .await?;
480            initial_pre_batch_items += items.len();
481            let batch = &mut pre_batches.batches[idx];
482            batch.items.extend(items);
483        }
484        span.record("initial_pre_batch_items", initial_pre_batch_items);
485        span.record("initial_pre_batches", pre_batches.batches.len());
486
487        // Figure out the order of all merged groups
488        let mut ordered_entries: Vec<Option<EntriesList>> =
489            vec![None; chunk_group_info.chunk_groups.len()];
490        for (i, chunk_group) in chunk_group_info.chunk_groups.iter().enumerate() {
491            if !chunk_group_indices_with_merged_children.contains(&i) {
492                continue;
493            }
494            let mut merged_modules: FxHashMap<ChunkingType, FxIndexSet<_>> = FxHashMap::default();
495            let mut stack = ordered_entries[i]
496                .as_ref()
497                .map_or_else(
498                    || Either::Left(chunk_group.entries()),
499                    |v| Either::Right(v.0.iter().copied()),
500                )
501                .map(|module| {
502                    let idx = *pre_batches
503                        .entries
504                        .get(&module)
505                        .context("could not prebatch for module")?;
506                    Ok((idx, 0))
507                })
508                .collect::<Result<Vec<_>>>()?;
509            stack.reverse();
510            let mut visited = FxHashSet::default();
511            while let Some((idx, mut pos)) = stack.pop() {
512                let batch = &pre_batches.batches[idx];
513                while let Some(item) = batch.items.get_index(pos) {
514                    match item {
515                        PreBatchItem::ParallelModule(_) => {}
516                        PreBatchItem::ParallelReference(other_idx) => {
517                            if visited.insert(*other_idx) {
518                                stack.push((idx, pos + 1));
519                                stack.push((*other_idx, 0));
520                                break;
521                            }
522                        }
523                        PreBatchItem::NonParallelEdge(chunking_type, module) => {
524                            if chunking_type.is_merged() {
525                                merged_modules
526                                    .entry(chunking_type.clone())
527                                    .or_default()
528                                    .insert(*module);
529                            }
530                        }
531                    }
532                    pos += 1;
533                }
534            }
535            if !merged_modules.is_empty() {
536                for (ty, merged_modules) in merged_modules {
537                    let chunk_group_key = match ty {
538                        ChunkingType::Isolated {
539                            merge_tag: Some(merge_tag),
540                            ..
541                        } => ChunkGroupKey::IsolatedMerged {
542                            parent: i.into(),
543                            merge_tag: merge_tag.clone(),
544                        },
545                        ChunkingType::Shared {
546                            merge_tag: Some(merge_tag),
547                            ..
548                        } => ChunkGroupKey::SharedMerged {
549                            parent: i.into(),
550                            merge_tag: merge_tag.clone(),
551                        },
552                        _ => unreachable!(),
553                    };
554                    let idx = chunk_group_info
555                        .chunk_group_keys
556                        .get_index_of(&chunk_group_key)
557                        .context("could not find chunk group key for merged chunk group")?;
558                    ordered_entries[idx] = Some(EntriesList(merged_modules));
559                }
560            }
561        }
562
563        // Create a map of parallel module to the batches they are contained in.
564        let mut parallel_module_to_pre_batch: FxIndexMap<_, Vec<PreBatchIndex>> =
565            FxIndexMap::default();
566
567        // Modules that are only referenced via `ChunkingType::PerEntry` (i.e. collecting
568        // modules). They are chunked once per entry group via
569        // `ChunkGroupContentInner::collecting_modules`, so their chunk item depends on the entry
570        // group and can't be represented in an entry-independent, shared `ModuleBatchGroup`.
571        // They still need to be in `single_module_entries` because the graph edges below
572        // reference them by index.
573        let mut per_entry_modules: FxHashSet<ResolvedVc<Box<dyn Module>>> = FxHashSet::default();
574
575        // Fill the map and also fill up the single_module_entries
576        for (idx, pre_batch) in pre_batches.batches.iter().enumerate() {
577            for item in &pre_batch.items {
578                match item {
579                    PreBatchItem::ParallelModule(module) => {
580                        parallel_module_to_pre_batch
581                            .entry(*module)
582                            .or_default()
583                            .push(idx);
584                    }
585                    PreBatchItem::NonParallelEdge(ty, module) => {
586                        if !pre_batches.entries.contains_key(module) {
587                            pre_batches.single_module_entries.insert(*module);
588                        }
589                        if matches!(ty, ChunkingType::PerEntry) {
590                            per_entry_modules.insert(*module);
591                        }
592                    }
593                    PreBatchItem::ParallelReference(_) => {}
594                }
595            }
596        }
597
598        // We never want a module to occur in multiple batches.
599
600        let mut extracted_shared_items = 0;
601        // Extract shared modules into separate batches
602        for i in 0..parallel_module_to_pre_batch.len() {
603            let (&module, batches) = parallel_module_to_pre_batch
604                .get_index(i)
605                .context("could not find parallel module to pre batch index")?;
606            if batches.len() > 1 {
607                // Create a new batch for the shared modules
608                let batches_with_item_index = batches
609                    .iter()
610                    .map(|&idx| {
611                        let batch_items = &pre_batches.batches[idx].items;
612                        let item_idx = batch_items
613                            .get_index_of(&PreBatchItem::ParallelModule(module))
614                            .context("could not find batch item index for parallel module")?;
615                        Ok((idx, item_idx))
616                    })
617                    .collect::<Result<Vec<_>>>()?;
618                let mut selected_items = 1;
619                fn get_item_at(
620                    pre_batches: &PreBatches,
621                    batch_idx: PreBatchIndex,
622                    item_idx: usize,
623                ) -> Option<&PreBatchItem> {
624                    pre_batches.batches[batch_idx].items.get_index(item_idx)
625                }
626                // Select more matching items that are equal in all batches that contain the shared
627                // module(s)
628                loop {
629                    if let Some(PreBatchItem::ParallelModule(next_module)) = get_item_at(
630                        &pre_batches,
631                        batches_with_item_index[0].0,
632                        batches_with_item_index[0].1 + selected_items,
633                    ) && parallel_module_to_pre_batch
634                        .get(next_module)
635                        .context("could not find pre batch for parallel module")?
636                        .len()
637                        == batches.len()
638                        && batches_with_item_index[1..]
639                            .iter()
640                            .all(|&(batch_idx, item_idx)| {
641                                get_item_at(&pre_batches, batch_idx, item_idx + selected_items)
642                                    == Some(&PreBatchItem::ParallelModule(*next_module))
643                            })
644                    {
645                        selected_items += 1;
646                        continue;
647                    }
648                    break;
649                }
650                extracted_shared_items += selected_items;
651
652                // Check if a batch is completely selected. In that case we can replace all other
653                // occurrences with a reference to that batch
654                let exact_match = batches_with_item_index
655                    .iter()
656                    .find(|&&(batch_idx, item_idx)| {
657                        item_idx == 0
658                            && pre_batches.batches[batch_idx].items.len() == selected_items
659                    });
660                if let Some(&(exact_match, _)) = exact_match {
661                    // Replace all other occurrences with a reference to the exact match
662                    for &(batch_index, item_start) in batches_with_item_index.iter() {
663                        if batch_index != exact_match {
664                            pre_batches.batches[batch_index].items.splice(
665                                item_start..item_start + selected_items,
666                                std::iter::once(PreBatchItem::ParallelReference(exact_match)),
667                            );
668                        }
669                    }
670                    for item in pre_batches.batches[exact_match].items.iter() {
671                        if let PreBatchItem::ParallelModule(module) = item {
672                            parallel_module_to_pre_batch
673                                .get_mut(module)
674                                .context("could not find pre batch for parallel module")?
675                                .clear();
676                        }
677                    }
678                } else {
679                    // Create a new batch of the shared part and replace all occurrences with a
680                    // reference to that batch
681                    let first_batch_index = batches_with_item_index[0].0;
682                    let first_batch_item_index = batches_with_item_index[0].1;
683                    let new_batch_index = pre_batches.batches.len();
684                    let mut new_batch =
685                        PreBatch::new(pre_batches.batches[first_batch_index].chunk_groups.clone());
686                    new_batch
687                        .items
688                        .extend(pre_batches.batches[first_batch_index].items.splice(
689                            first_batch_item_index..first_batch_item_index + selected_items,
690                            std::iter::once(PreBatchItem::ParallelReference(new_batch_index)),
691                        ));
692                    for item in new_batch.items.iter() {
693                        if let PreBatchItem::ParallelModule(module) = item {
694                            parallel_module_to_pre_batch
695                                .get_mut(module)
696                                .context("could not find pre batch for parallel module")?
697                                .clear();
698                        }
699                    }
700                    pre_batches.batches.push(new_batch);
701                    for &(batch_index, item_start) in batches_with_item_index[1..].iter() {
702                        pre_batches.batches[batch_index].items.splice(
703                            item_start..item_start + selected_items,
704                            std::iter::once(PreBatchItem::ParallelReference(new_batch_index)),
705                        );
706                    }
707                }
708            }
709        }
710        span.record("extracted_shared_items", extracted_shared_items);
711
712        // Now every module is only in one batch
713
714        let mut edges_count = 0;
715
716        // Since batches can only have references followed by a list of parallel chunkable modules,
717        // we need to split batches that have modules before references.
718        for i in 0..pre_batches.batches.len() {
719            let items = take(&mut pre_batches.batches[i].items);
720            let mut new_items =
721                FxIndexSet::with_capacity_and_hasher(items.len(), Default::default());
722            enum Mode {
723                ParallelChunkableModule,
724                Other,
725            }
726            let mut mode = Mode::Other;
727            for item in items {
728                let chunkable_module = if let PreBatchItem::ParallelModule(module) = &item {
729                    ResolvedVc::try_downcast::<Box<dyn ChunkableModule>>(*module)
730                } else {
731                    None
732                };
733                let item = if let PreBatchItem::ParallelModule(module) = item {
734                    if chunkable_module.is_some() {
735                        PreBatchItem::ParallelModule(module)
736                    } else {
737                        pre_batches.single_module_entries.insert(module);
738                        PreBatchItem::NonParallelEdge(
739                            ChunkingType::Parallel {
740                                inherit_async: false,
741                                hoisted: false,
742                            },
743                            module,
744                        )
745                    }
746                } else {
747                    item
748                };
749                match (&mode, chunkable_module) {
750                    (_, Some(_)) => {
751                        mode = Mode::ParallelChunkableModule;
752                        new_items.insert(item);
753                    }
754                    (Mode::Other, _) => {
755                        edges_count += 1;
756                        new_items.insert(item);
757                    }
758                    (Mode::ParallelChunkableModule, _) => {
759                        // Split the batch
760                        let idx = pre_batches.batches.len();
761                        let mut new_batch =
762                            PreBatch::new(pre_batches.batches[i].chunk_groups.clone());
763                        new_batch.items.extend(new_items.drain(..));
764                        pre_batches.batches.push(new_batch);
765                        edges_count += 1;
766                        new_items.insert(PreBatchItem::ParallelReference(idx));
767                        if chunkable_module.is_some() {
768                            new_items.insert(item);
769                        } else {
770                            edges_count += 1;
771                            mode = Mode::Other;
772                            new_items.insert(item);
773                        }
774                    }
775                }
776            }
777            pre_batches.batches[i].items = new_items;
778        }
779        span.record("pre_batches", pre_batches.batches.len());
780
781        // Now batches are in the correct shape. We can create the real batches and the graph.
782
783        // Create the graph
784        let mut graph: DiGraph<ModuleOrBatch, ModuleBatchesGraphEdge, u32> =
785            petgraph::graph::DiGraph::with_capacity(
786                pre_batches.batches.len() + pre_batches.single_module_entries.len(),
787                edges_count,
788            );
789
790        // Create the Vc<ModuleBatch> instances
791        let batches = pre_batches
792            .batches
793            .iter_mut()
794            .enumerate()
795            .map(async |(i, pre_batch)| {
796                let mut modules = pre_batch.items.iter().filter_map(|item| {
797                    if let PreBatchItem::ParallelModule(module) = item {
798                        ResolvedVc::try_downcast(*module)
799                    } else {
800                        None
801                    }
802                });
803                let Some(first) = modules.next() else {
804                    return Ok(ModuleOrBatch::None(i));
805                };
806                if let Some(second) = modules.next() {
807                    let batch = ModuleBatch::new(
808                        [first, second]
809                            .into_iter()
810                            .chain(modules)
811                            .map(|m| *m)
812                            .collect::<Vec<_>>(),
813                        Some(pre_batch.chunk_groups.clone()),
814                    );
815                    Ok(ModuleOrBatch::Batch(batch.to_resolved().await?))
816                } else {
817                    Ok(ModuleOrBatch::Module(ResolvedVc::upcast(first)))
818                }
819            })
820            .try_join()
821            .await?;
822
823        // Create the batch groups by grouping batches with the same chunk groups
824        let mut batch_groups: FxHashMap<_, Vec<_>> = FxHashMap::default();
825        for (i, pre_batch) in pre_batches.batches.iter().enumerate() {
826            let key = BuildHasherDefault::<FxHasher>::default().prehash(&pre_batch.chunk_groups);
827            let batch = batches[i];
828            batch_groups.entry(key).or_default().push(batch);
829        }
830        for &module in &pre_batches.single_module_entries {
831            // Modules referenced via `ChunkingType::PerEntry` are chunked per entry group and
832            // must not become part of a (shared, entry-independent) batch group. See
833            // `per_entry_modules` above.
834            if per_entry_modules.contains(&module) {
835                continue;
836            }
837            let chunk_groups = module_chunk_groups
838                .get(&module)
839                .context("all modules need to have chunk group info")?;
840            let key = BuildHasherDefault::<FxHasher>::default().prehash(chunk_groups);
841            batch_groups
842                .entry(key)
843                .or_default()
844                .push(ModuleOrBatch::Module(module));
845        }
846
847        // Create the batch group instances
848        let batch_groups = batch_groups
849            .into_iter()
850            .map(async |(key, items)| {
851                if items.len() == 1 {
852                    anyhow::Ok(Either::Left(std::iter::empty()))
853                } else {
854                    let batch_group = ModuleBatchGroup::new(items.clone(), (*key).clone())
855                        .to_resolved()
856                        .await?;
857                    Ok(Either::Right(
858                        items.into_iter().map(move |item| (item, batch_group)),
859                    ))
860                }
861            })
862            .join()
863            .await
864            .into_iter()
865            .flatten_ok()
866            .collect::<Result<FxHashMap<_, _>>>()?;
867
868        // Insert batches into the graph and store the NodeIndices
869        let mut batches_count = 0;
870        let mut modules_count = 0;
871        let batch_indices = batches
872            .into_iter()
873            .map(|batch| {
874                match &batch {
875                    ModuleOrBatch::Batch(_) => batches_count += 1,
876                    ModuleOrBatch::Module(_) => modules_count += 1,
877                    ModuleOrBatch::None(_) => {}
878                }
879                graph.add_node(batch)
880            })
881            .collect::<Vec<_>>();
882
883        // Also insert single modules into the graph and store the NodeIndices
884        let single_module_indices = pre_batches
885            .single_module_entries
886            .iter()
887            .map(|module| graph.add_node(ModuleOrBatch::Module(*module)))
888            .collect::<Vec<_>>();
889
890        span.record("batches", batches_count);
891        modules_count += pre_batches.single_module_entries.len();
892        span.record("modules", modules_count);
893        span.record("edges", edges_count);
894
895        // Build a module -> NodeIndex lookup for all modules in the graph
896        let mut module_to_node: FxHashMap<ResolvedVc<Box<dyn Module>>, NodeIndex> =
897            FxHashMap::default();
898        for (module, batches) in &parallel_module_to_pre_batch {
899            // The values in parallel_module_to_pre_batch are guaranteed to be empty or contain
900            // exactly one batch index.
901            if let Some(&first_batch) = batches.first() {
902                module_to_node.insert(*module, batch_indices[first_batch]);
903            }
904        }
905        for (idx, module) in pre_batches.single_module_entries.iter().enumerate() {
906            module_to_node.insert(*module, single_module_indices[idx]);
907        }
908
909        // Add all the edges to the graph
910        for (i, pre_batch) in pre_batches.batches.into_iter().enumerate() {
911            let index = batch_indices[i];
912            let items = pre_batch.items;
913            for item in items {
914                match item {
915                    PreBatchItem::ParallelReference(idx) => {
916                        graph.add_edge(
917                            index,
918                            batch_indices[idx],
919                            ModuleBatchesGraphEdge {
920                                ty: ChunkingType::Parallel {
921                                    inherit_async: false,
922                                    hoisted: false,
923                                },
924                                module: None,
925                                active_for_page_entry: None,
926                            },
927                        );
928                    }
929                    PreBatchItem::NonParallelEdge(ty, module) => {
930                        if matches!(ty, ChunkingType::Emitted { .. }) {
931                            // they are handled via collected_modules now
932                            continue;
933                        }
934                        if let Some(batch) = pre_batches.entries.get(&module).copied() {
935                            graph.add_edge(
936                                index,
937                                batch_indices[batch],
938                                ModuleBatchesGraphEdge {
939                                    ty,
940                                    module: Some(module),
941                                    active_for_page_entry: None,
942                                },
943                            );
944                            continue;
945                        }
946                        let idx = pre_batches
947                            .single_module_entries
948                            .get_index_of(&module)
949                            .context("could not find single module entry index")?;
950                        let idx = single_module_indices[idx];
951                        graph.add_edge(
952                            index,
953                            idx,
954                            ModuleBatchesGraphEdge {
955                                ty,
956                                module: Some(module),
957                                active_for_page_entry: None,
958                            },
959                        );
960                    }
961                    PreBatchItem::ParallelModule(_) => {}
962                }
963            }
964        }
965
966        debug_assert_eq!(graph.capacity().0, graph.node_count());
967        debug_assert_eq!(graph.capacity().1, graph.edge_count());
968
969        // Add collected reference edges (conditional on page entry)
970        for (collecting_module, refs) in &collected_modules {
971            for (entry_modules, refs) in refs {
972                let source_node = *module_to_node
973                    .get(collecting_module)
974                    .context("could not find single module entry index")?;
975                for (ref_data, target_module) in refs {
976                    if let Some(batch) = pre_batches.entries.get(target_module).copied() {
977                        for entry_module in entry_modules {
978                            graph.add_edge(
979                                source_node,
980                                batch_indices[batch],
981                                ModuleBatchesGraphEdge {
982                                    ty: ref_data.chunking_type.clone(),
983                                    module: Some(*target_module),
984                                    active_for_page_entry: Some(*entry_module),
985                                },
986                            );
987                        }
988                        continue;
989                    }
990                    let idx = pre_batches
991                        .single_module_entries
992                        .get_index_of(target_module)
993                        .context("could not find single module entry index")?;
994                    let idx = single_module_indices[idx];
995                    for entry_module in entry_modules {
996                        graph.add_edge(
997                            source_node,
998                            idx,
999                            ModuleBatchesGraphEdge {
1000                                ty: ref_data.chunking_type.clone(),
1001                                module: Some(*target_module),
1002                                active_for_page_entry: Some(*entry_module),
1003                            },
1004                        );
1005                    }
1006                }
1007            }
1008        }
1009
1010        // Find the NodeIndices for our entries of the graph
1011        let mut entries = FxHashMap::default();
1012        for chunk_group in &chunk_group_info.chunk_groups {
1013            for module in chunk_group.entries() {
1014                if let Some(batch) = pre_batches.entries.get(&module).copied() {
1015                    entries.insert(module, batch_indices[batch]);
1016                    continue;
1017                }
1018                let idx = pre_batches
1019                    .single_module_entries
1020                    .get_index_of(&module)
1021                    .context("could not find single module entry index")?;
1022                let idx = single_module_indices[idx];
1023                entries.insert(module, idx);
1024            }
1025        }
1026
1027        Ok(ModuleBatchesGraph {
1028            graph: TracedDiGraph(graph),
1029            entries,
1030            batch_groups,
1031            ordered_entries,
1032        }
1033        .cell())
1034    }
1035    .instrument(outer_span)
1036    .await
1037}