turbopack_core/module_graph/
collect.rs1use 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)]
21pub 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#[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 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 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 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 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 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 bail!("Module entry membership not found");
145 };
146
147 if current_membership.is_empty() {
148 *current_membership = parent_membership.clone();
150 Ok(GraphTraversalAction::Continue)
151 } else if parent_membership.is_proper_superset(current_membership) {
152 **current_membership |= &**parent_membership;
154 Ok(GraphTraversalAction::Continue)
155 } else {
156 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 #[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 Cow::Borrowed(&**collecting_membership)
242 } else {
243 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}