turbopack_core/module_graph/
collect.rs1use std::{borrow::Cow, cmp::Reverse};
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},
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_rank = graph.reverse_postorder_ranks(entry_group_modules.iter().copied())?;
77 for module in module_rank.keys() {
78 module_entry_membership.entry(*module).or_default();
79 }
80
81 let mut collecting_modules: FxIndexSet<ResolvedVc<Box<dyn CollectingModule>>> =
85 FxIndexSet::default();
86 let mut emitted_references: FxIndexSet<(&RefData, ResolvedVc<Box<dyn Module>>)> =
87 FxIndexSet::default();
88 graph.traverse_edges_fixed_point_with_priority(
89 entry_group_modules
90 .iter()
91 .map(|e| {
92 Ok((
93 *e,
94 Reverse(*module_rank.get(e).context("Module rank not found")?),
95 ))
96 })
97 .collect::<Result<Vec<_>>>()?,
98 &mut (&mut module_entry_membership, &mut emitted_references),
99 |parent_info: Option<(ResolvedVc<Box<dyn Module>>, &'_ RefData, _)>,
100 node: ResolvedVc<Box<dyn Module>>,
101 _,
102 (module_entry_membership, emitted_references)|
103 -> Result<GraphTraversalAction> {
104 if let Some(node) = ResolvedVc::try_downcast::<Box<dyn CollectingModule>>(node) {
105 collecting_modules.insert(node);
106 }
107
108 let Some((parent, ref_data, _)) = parent_info else {
109 return Ok(GraphTraversalAction::Continue);
111 };
112
113 if let ChunkingType::Emitted { .. } = ref_data.chunking_type {
114 emitted_references.insert((ref_data, node));
115 }
116
117 if parent == node {
118 Ok(GraphTraversalAction::Skip)
120 } else {
121 let [Some(parent_membership), Some(current_membership)] =
122 module_entry_membership.get_disjoint_mut([&parent, &node])
123 else {
124 bail!("Module entry membership not found");
128 };
129
130 if current_membership.is_empty() {
131 *current_membership = parent_membership.clone();
133 Ok(GraphTraversalAction::Continue)
134 } else if parent_membership.is_proper_superset(current_membership) {
135 **current_membership |= &**parent_membership;
137 Ok(GraphTraversalAction::Continue)
138 } else {
139 Ok(GraphTraversalAction::Skip)
141 }
142 }
143 },
144 |successor, _| {
147 Ok(Reverse(
148 *module_rank
149 .get(&successor)
150 .context("Module rank not found")?,
151 ))
152 },
153 )?;
154
155 for collecting_module in &collecting_modules {
156 let collecting_module = ResolvedVc::upcast(*collecting_module);
157 let collecting_membership = module_entry_membership
158 .get(&collecting_module)
159 .context("Module entry membership not found")?;
160
161 if collecting_membership.len() > 1 {
162 ModuleIssue::new(
163 *collecting_module.ident().to_resolved().await?,
164 rcstr!("Invalid use of __turbopack_collect__"),
165 rcstr!(
166 "A module containing __turbopack_collect__ must not be reachable from \
167 multiple entry chunk groups. Move the call into an entry-specific module."
168 ),
169 None,
170 )
171 .to_resolved()
172 .await?
173 .emit();
174 }
175 }
176
177 let collecting_modules = {
178 let mut map: FxHashMap<RcStr, Vec<ResolvedVc<Box<dyn CollectingModule>>>> =
179 FxHashMap::default();
180 for (m, namespace) in collecting_modules
181 .iter()
182 .map(async |target| Ok((*target, target.namespace().owned().await?)))
183 .try_join()
184 .await?
185 {
186 map.entry(namespace).or_default().push(m);
187 }
188 map
189 };
190
191 #[allow(clippy::type_complexity)]
197 let mut collected_references: FxIndexMap<
198 ResolvedVc<Box<dyn Module>>,
199 FxIndexMap<u32, Vec<(RefData, ResolvedVc<Box<dyn Module>>)>>,
200 > = FxIndexMap::default();
201
202 for (ref_data, emitted_module) in emitted_references {
203 let emitted_membership = module_entry_membership
204 .get(&emitted_module)
205 .context("Module entry membership not found")?;
206
207 let ChunkingType::Emitted {
208 namespace,
209 emit_to_all_entries,
210 } = &ref_data.chunking_type
211 else {
212 bail!("unreachable: expected emitted reference");
213 };
214
215 for collecting_module in collecting_modules.get(namespace).into_iter().flatten() {
216 let collecting_membership = module_entry_membership
217 .get(&ResolvedVc::upcast(*collecting_module))
218 .context("Module entry membership not found")?;
219
220 let matching_chunk_groups = if *emit_to_all_entries {
221 Cow::Borrowed(&**collecting_membership)
223 } else {
224 Cow::Owned((**collecting_membership).clone() & (&**emitted_membership))
227 };
228 if !matching_chunk_groups.is_empty() {
229 let refs = collected_references
230 .entry(ResolvedVc::upcast(*collecting_module))
231 .or_default();
232 for entry in matching_chunk_groups.iter() {
233 refs.entry(entry).or_default().push((
234 RefData {
235 chunking_type: ChunkingType::Collected {
236 namespace: namespace.clone(),
237 },
238 ..ref_data.clone()
239 },
240 emitted_module,
241 ));
242 }
243 }
244 }
245 }
246
247 Ok(CollectedModules(
248 collected_references
249 .into_iter()
250 .map(|(k, v)| {
251 (
252 k,
253 v.into_iter()
254 .map(|(k, v)| (entry_groups[k as usize].clone(), v))
255 .collect(),
256 )
257 })
258 .collect(),
259 )
260 .cell())
261}