Skip to main content

turbopack_core/module_graph/
collect.rs

1use std::borrow::Cow;
2
3use anyhow::{Context, Result, bail};
4use rustc_hash::FxHashMap;
5use turbo_rcstr::{RcStr, rcstr};
6use turbo_tasks::{FxIndexMap, FxIndexSet, ResolvedVc, TryJoinIterExt, Vc};
7
8use crate::{
9    chunk::ChunkingType,
10    emit_collect::CollectingModule,
11    issue::{IssueExt, module::ModuleIssue},
12    module::Module,
13    module_graph::{
14        GraphTraversalAction, ModuleGraph, RefData,
15        chunk_group_info::{ChunkGroupEntry, RoaringBitmapWrapper, TraversalPriority},
16    },
17};
18
19#[turbo_tasks::value(transparent, cell = "keyed")]
20#[allow(clippy::type_complexity)]
21/// Additional references that need to be added to the graph due to collecting modules. They
22/// are conditional based on the current page being chunked.
23///
24/// (Collecting Module) -> Vec<(ChunkGroup::Entry Modules, Vec<(Reference, Collected Module)>)>
25pub struct CollectedModules(
26    #[bincode(with = "turbo_bincode::indexmap")]
27    FxIndexMap<
28        ResolvedVc<Box<dyn Module>>,
29        Vec<(
30            Vec<ResolvedVc<Box<dyn Module>>>,
31            Vec<(RefData, ResolvedVc<Box<dyn Module>>)>,
32        )>,
33    >,
34);
35
36// The goal is:
37// 1. Find all ChunkingType::Emitted references
38// 2. Find all CollectingModules
39// 3. For each CollectingModule, collect all emitted references within the same ChunkGroup::Entry as
40//    the CollectingModule where the .namespace() matches.
41#[tracing::instrument(level = "info", name = "compute emit-collect", skip_all)]
42pub async fn collect_graph(graph: Vc<ModuleGraph>) -> Result<Vc<CollectedModules>> {
43    let graph = graph.await?;
44    let graphs = &graph.graphs;
45
46    let module_count = graphs.iter().map(|g| g.graph.node_count()).sum::<usize>();
47
48    let entry_groups = graph
49        .all_chunk_group_entries()
50        .flat_map(|g| match g {
51            ChunkGroupEntry::Entry {
52                modules: entries, ..
53            } => Some(entries),
54            _ => None,
55        })
56        .collect::<Vec<_>>();
57    let entry_group_modules = entry_groups
58        .iter()
59        .flat_map(|entries| entries.iter().copied())
60        .collect::<Vec<_>>();
61
62    // Create a mapping of module -> ChunkGroupEntry::Entry that import it
63    let mut module_entry_membership: FxHashMap<ResolvedVc<Box<dyn Module>>, RoaringBitmapWrapper> =
64        FxHashMap::with_capacity_and_hasher(module_count, Default::default());
65    for (i, entries) in entry_groups.iter().enumerate() {
66        for entry in *entries {
67            module_entry_membership
68                .entry(*entry)
69                .or_default()
70                .insert(i as u32);
71        }
72    }
73
74    // First, compute the depth for each module in the graph
75    let module_depth: FxHashMap<ResolvedVc<Box<dyn Module>>, usize> = {
76        let mut module_depth =
77            FxHashMap::with_capacity_and_hasher(module_count, Default::default());
78        graph.traverse_edges_bfs(entry_group_modules.iter().copied(), |parent, node| {
79            if let Some((parent, _)) = parent {
80                let parent_depth = *module_depth
81                    .get(&parent)
82                    .context("Module depth not found")?;
83                module_depth.entry(node).or_insert(parent_depth + 1);
84            } else {
85                module_depth.insert(node, 0);
86            };
87
88            module_entry_membership.entry(node).or_default();
89
90            Ok(GraphTraversalAction::Continue)
91        })?;
92        module_depth
93    };
94
95    // - Discover all collecting module
96    // - Discover all emitted references
97    // - Set module_entry_membership
98    let mut collecting_modules: FxIndexSet<ResolvedVc<Box<dyn CollectingModule>>> =
99        FxIndexSet::default();
100    let mut emitted_references: FxIndexSet<(&RefData, ResolvedVc<Box<dyn Module>>)> =
101        FxIndexSet::default();
102    graph.traverse_edges_fixed_point_with_priority(
103        entry_group_modules
104            .iter()
105            .map(|e| {
106                Ok((
107                    *e,
108                    TraversalPriority {
109                        depth: *module_depth.get(e).context("Module depth not found")?,
110                        chunk_group_len: 0,
111                    },
112                ))
113            })
114            .collect::<Result<Vec<_>>>()?,
115        &mut (&mut module_entry_membership, &mut emitted_references),
116        |parent_info: Option<(ResolvedVc<Box<dyn Module>>, &'_ RefData, _)>,
117         node: ResolvedVc<Box<dyn Module>>,
118         _,
119         (module_entry_membership, emitted_references)|
120         -> Result<GraphTraversalAction> {
121            if let Some(node) = ResolvedVc::try_downcast::<Box<dyn CollectingModule>>(node) {
122                collecting_modules.insert(node);
123            }
124
125            let Some((parent, ref_data, _)) = parent_info else {
126                // An entry module
127                return Ok(GraphTraversalAction::Continue);
128            };
129
130            if let ChunkingType::Emitted { .. } = ref_data.chunking_type {
131                emitted_references.insert((ref_data, node));
132            }
133
134            if parent == node {
135                // A self-reference
136                Ok(GraphTraversalAction::Skip)
137            } else {
138                let [Some(parent_membership), Some(current_membership)] =
139                    module_entry_membership.get_disjoint_mut([&parent, &node])
140                else {
141                    // All modules are inserted in the previous iteration
142                    // Technically unreachable, but could be reached due to eventual
143                    // consistency
144                    bail!("Module entry membership not found");
145                };
146
147                if current_membership.is_empty() {
148                    // Initial visit, clone instead of merging
149                    *current_membership = parent_membership.clone();
150                    Ok(GraphTraversalAction::Continue)
151                } else if parent_membership.is_proper_superset(current_membership) {
152                    // Add bits from parent, and continue traversal because changed
153                    **current_membership |= &**parent_membership;
154                    Ok(GraphTraversalAction::Continue)
155                } else {
156                    // Unchanged, no need to forward to children
157                    Ok(GraphTraversalAction::Skip)
158                }
159            }
160        },
161        |successor, (module_entry_membership, _)| {
162            Ok(TraversalPriority {
163                depth: *module_depth
164                    .get(&successor)
165                    .context("Module depth not found")?,
166                chunk_group_len: module_entry_membership
167                    .get(&successor)
168                    .context("Module entry membership not found")?
169                    .len(),
170            })
171        },
172    )?;
173
174    for collecting_module in &collecting_modules {
175        let collecting_module = ResolvedVc::upcast(*collecting_module);
176        let collecting_membership = module_entry_membership
177            .get(&collecting_module)
178            .context("Module entry membership not found")?;
179
180        if collecting_membership.len() > 1 {
181            ModuleIssue::new(
182                *collecting_module.ident().to_resolved().await?,
183                rcstr!("Invalid use of __turbopack_collect__"),
184                rcstr!(
185                    "A module containing __turbopack_collect__ must not be reachable from \
186                     multiple entry chunk groups. Move the call into an entry-specific module."
187                ),
188                None,
189            )
190            .to_resolved()
191            .await?
192            .emit();
193        }
194    }
195
196    let collecting_modules = {
197        let mut map: FxHashMap<RcStr, Vec<ResolvedVc<Box<dyn CollectingModule>>>> =
198            FxHashMap::default();
199        for (m, namespace) in collecting_modules
200            .iter()
201            .map(async |target| Ok((*target, target.namespace().owned().await?)))
202            .try_join()
203            .await?
204        {
205            map.entry(namespace).or_default().push(m);
206        }
207        map
208    };
209
210    // Now we have all necessary information. List out all collected references for each (Entry
211    // Module, Collecting Module) pair they are contained in.
212
213    // Same type as `struct CollectedModules`:
214    // (Collecting Module) -> Vec<(entry_groups index, Vec<(Reference, Collected Module)>)>
215    #[allow(clippy::type_complexity)]
216    let mut collected_references: FxIndexMap<
217        ResolvedVc<Box<dyn Module>>,
218        FxIndexMap<u32, Vec<(RefData, ResolvedVc<Box<dyn Module>>)>>,
219    > = FxIndexMap::default();
220
221    for (ref_data, emitted_module) in emitted_references {
222        let emitted_membership = module_entry_membership
223            .get(&emitted_module)
224            .context("Module entry membership not found")?;
225
226        let ChunkingType::Emitted {
227            namespace,
228            emit_to_all_entries,
229        } = &ref_data.chunking_type
230        else {
231            bail!("unreachable: expected emitted reference");
232        };
233
234        for collecting_module in collecting_modules.get(namespace).into_iter().flatten() {
235            let collecting_membership = module_entry_membership
236                .get(&ResolvedVc::upcast(*collecting_module))
237                .context("Module entry membership not found")?;
238
239            let matching_chunk_groups = if *emit_to_all_entries {
240                // Add to all entry groups the collecting module is in.
241                Cow::Borrowed(&**collecting_membership)
242            } else {
243                // Add to all entry groups the collecting module is in that also contain the emitted
244                // module.
245                Cow::Owned((**collecting_membership).clone() & (&**emitted_membership))
246            };
247            if !matching_chunk_groups.is_empty() {
248                let refs = collected_references
249                    .entry(ResolvedVc::upcast(*collecting_module))
250                    .or_default();
251                for entry in matching_chunk_groups.iter() {
252                    refs.entry(entry).or_default().push((
253                        RefData {
254                            chunking_type: ChunkingType::Collected {
255                                namespace: namespace.clone(),
256                            },
257                            ..ref_data.clone()
258                        },
259                        emitted_module,
260                    ));
261                }
262            }
263        }
264    }
265
266    Ok(CollectedModules(
267        collected_references
268            .into_iter()
269            .map(|(k, v)| {
270                (
271                    k,
272                    v.into_iter()
273                        .map(|(k, v)| (entry_groups[k as usize].clone(), v))
274                        .collect(),
275                )
276            })
277            .collect(),
278    )
279    .cell())
280}