turbopack_core/module_graph/
binding_usage_info.rs1use std::collections::hash_map::Entry;
2
3use anyhow::{Context, Result, bail};
4use auto_hash_map::AutoSet;
5use rustc_hash::{FxHashMap, FxHashSet};
6use tracing::Instrument;
7use turbo_rcstr::RcStr;
8use turbo_tasks::{OperationVc, ResolvedVc, Vc};
9
10use crate::{
11 chunk::chunking_context::UnusedReferences,
12 module::Module,
13 module_graph::{
14 GraphEdgeIndex, GraphTraversalAction, ModuleGraph,
15 side_effect_module_info::compute_side_effect_free_module_info,
16 },
17 resolve::{ExportUsage, ImportUsage},
18};
19
20#[turbo_tasks::value(transparent, cell = "keyed")]
21pub struct UsedExportsMap(FxHashMap<ResolvedVc<Box<dyn Module>>, ModuleExportUsageInfo>);
22
23#[turbo_tasks::value(transparent, cell = "keyed")]
24pub struct ExportCircuitBreakers(FxHashSet<ResolvedVc<Box<dyn Module>>>);
25
26#[turbo_tasks::value(transparent, cell = "keyed")]
36pub struct PartialNamespaceModules(FxHashSet<ResolvedVc<Box<dyn Module>>>);
37
38#[turbo_tasks::value]
39#[derive(Clone, Default, Debug)]
40pub struct BindingUsageInfo {
41 unused_references: ResolvedVc<UnusedReferences>,
42 #[turbo_tasks(trace_ignore)]
43 unused_references_edges: FxHashSet<GraphEdgeIndex>,
44
45 used_exports: ResolvedVc<UsedExportsMap>,
46 export_circuit_breakers: ResolvedVc<ExportCircuitBreakers>,
47 partial_namespace_modules: ResolvedVc<PartialNamespaceModules>,
48}
49
50#[turbo_tasks::value(transparent)]
51pub struct OptionBindingUsageInfo(Option<ResolvedVc<BindingUsageInfo>>);
52
53#[turbo_tasks::value]
54pub struct ModuleExportUsage {
55 pub export_usage: ResolvedVc<ModuleExportUsageInfo>,
56 pub is_circuit_breaker: bool,
58 pub namespace_object_may_escape: bool,
61}
62#[turbo_tasks::value_impl]
63impl ModuleExportUsage {
64 #[turbo_tasks::function]
65 pub async fn all() -> Result<Vc<Self>> {
66 Ok(Self {
67 export_usage: ModuleExportUsageInfo::all().to_resolved().await?,
68 is_circuit_breaker: true,
69 namespace_object_may_escape: true,
70 }
71 .cell())
72 }
73}
74
75impl BindingUsageInfo {
76 pub fn is_reference_unused_edge(&self, edge: &GraphEdgeIndex) -> bool {
77 self.unused_references_edges.contains(edge)
78 }
79
80 pub async fn used_exports(
81 &self,
82 module: ResolvedVc<Box<dyn Module>>,
83 ) -> Result<Vc<ModuleExportUsage>> {
84 let is_circuit_breaker = self.export_circuit_breakers.contains_key(&module).await?;
85 let Some(exports) = self.used_exports.get(&module).await? else {
86 let ident = module.ident_string().await?;
88 if ident.contains(".wasm_.loader.mjs") || ident.contains("/__nextjs-internal-proxy.") {
89 return Ok(ModuleExportUsage::all());
94 }
95
96 bail!("export usage not found for module: {ident:?}");
97 };
98 let namespace_object_may_escape =
99 self.partial_namespace_modules.contains_key(&module).await?;
100 Ok(ModuleExportUsage {
101 export_usage: (*exports).clone().resolved_cell(),
102 is_circuit_breaker,
103 namespace_object_may_escape,
104 }
105 .cell())
106 }
107}
108
109#[turbo_tasks::value_impl]
110impl BindingUsageInfo {
111 #[turbo_tasks::function]
112 pub fn unused_references(&self) -> Vc<UnusedReferences> {
113 *self.unused_references
114 }
115}
116
117#[turbo_tasks::function(operation)]
118pub async fn compute_binding_usage_info(
119 graph: OperationVc<ModuleGraph>,
120 remove_unused_imports: bool,
121) -> Result<Vc<BindingUsageInfo>> {
122 let span_outer = tracing::info_span!(
123 "compute binding usage info",
124 visit_count = tracing::field::Empty,
125 unused_reference_count = tracing::field::Empty
126 );
127 let span = span_outer.clone();
128
129 async move {
130 let mut used_exports = FxHashMap::<_, ModuleExportUsageInfo>::default();
131 let mut partial_namespace_modules = FxHashSet::default();
132 #[cfg(debug_assertions)]
133 let mut debug_unused_references_name = FxHashSet::<(
134 ResolvedVc<Box<dyn Module>>,
135 ExportUsage,
136 ResolvedVc<Box<dyn Module>>,
137 )>::default();
138 let mut unused_references_edges = FxHashSet::default();
139 let mut unused_references =
140 FxHashMap::<_, FxHashSet<ResolvedVc<Box<dyn Module>>>>::default();
141
142 let graph = graph.connect();
143 let graph_ref = graph.await?;
144 if graph_ref.binding_usage.is_some() {
145 panic!(
157 "don't run compute_binding_usage_info on a graph after calling \
158 without_unused_references"
159 );
160 }
161 let side_effect_free_modules = if remove_unused_imports {
162 let side_effect_free_modules = compute_side_effect_free_module_info(graph).await?;
163 span.record("side_effect_free_modules", side_effect_free_modules.len());
164 Some(side_effect_free_modules)
165 } else {
166 None
167 };
168
169 let entries = graph_ref.all_chunk_group_entry_modules();
170
171 let visit_count = graph_ref.traverse_edges_fixed_point_with_priority(
172 entries.map(|m| (m, 0)),
173 &mut (),
174 |parent, target, _, _| {
175 let Some((parent, ref_data, edge)) = parent else {
177 used_exports.insert(target, ModuleExportUsageInfo::All);
178 return Ok(GraphTraversalAction::Continue);
179 };
180
181 if remove_unused_imports {
182 if matches!(&ref_data.binding_usage.export, ExportUsage::Evaluation)
186 && side_effect_free_modules
187 .as_ref()
188 .expect("this must be present if `remove_unused_imports` is true")
189 .contains(&target)
190 {
191 #[cfg(debug_assertions)]
192 debug_unused_references_name.insert((
193 parent,
194 ref_data.binding_usage.export.clone(),
195 target,
196 ));
197 unused_references_edges.insert(edge);
198 unused_references
199 .entry(ref_data.reference)
200 .or_default()
201 .insert(target);
202 return Ok(GraphTraversalAction::Skip);
203 }
204 match &ref_data.binding_usage.import {
206 ImportUsage::Exports(exports) => {
207 let source_used_exports = used_exports
208 .get(&parent)
209 .context("parent module must have usage info")?;
210 if exports
211 .iter()
212 .all(|e| !source_used_exports.is_export_used(e))
213 {
214 #[cfg(debug_assertions)]
216 debug_unused_references_name.insert((
217 parent,
218 ref_data.binding_usage.export.clone(),
219 target,
220 ));
221 unused_references_edges.insert(edge);
222 unused_references
223 .entry(ref_data.reference)
224 .or_default()
225 .insert(target);
226
227 return Ok(GraphTraversalAction::Skip);
228 } else {
229 #[cfg(debug_assertions)]
230 debug_unused_references_name.remove(&(
231 parent,
232 ref_data.binding_usage.export.clone(),
233 target,
234 ));
235 unused_references_edges.remove(&edge);
236 if let Entry::Occupied(mut e) =
237 unused_references.entry(ref_data.reference)
238 {
239 e.get_mut().remove(&target);
240 if e.get().is_empty() {
241 e.remove();
242 }
243 }
244 }
246 }
247 ImportUsage::TopLevel => {
248 #[cfg(debug_assertions)]
249 debug_unused_references_name.remove(&(
250 parent,
251 ref_data.binding_usage.export.clone(),
252 target,
253 ));
254 unused_references_edges.remove(&edge);
255 if let Entry::Occupied(mut e) =
256 unused_references.entry(ref_data.reference)
257 {
258 e.get_mut().remove(&target);
259 if e.get().is_empty() {
260 e.remove();
261 }
262 }
263 }
265 }
266 }
267
268 let entry = used_exports.entry(target);
269 let is_first_visit = matches!(entry, Entry::Vacant(_));
270 if matches!(
271 &ref_data.binding_usage.export,
272 ExportUsage::PartialNamespaceObject(_)
273 ) {
274 partial_namespace_modules.insert(target);
277 }
278 if entry.or_default().add(&ref_data.binding_usage.export) || is_first_visit {
279 Ok(GraphTraversalAction::Continue)
282 } else {
283 Ok(GraphTraversalAction::Skip)
284 }
285 },
286 |_, _| Ok(0),
287 )?;
288
289 let mut export_circuit_breakers = FxHashSet::default();
302
303 graph_ref.traverse_cycles(
304 |e| e.chunking_type.is_parallel() && !unused_references.contains_key(&e.reference),
306 |cycle| {
307 export_circuit_breakers.extend(cycle.iter().map(|n| **n));
316 Ok(())
317 },
318 )?;
319
320 span.record("visit_count", visit_count);
321 span.record("unused_reference_count", unused_references.len());
322
323 #[cfg(debug_assertions)]
324 {
325 use std::sync::LazyLock;
326 static PRINT_UNUSED_REFERENCES: LazyLock<bool> = LazyLock::new(|| {
327 std::env::var_os("TURBOPACK_PRINT_UNUSED_REFERENCES")
328 .is_some_and(|v| v == "1" || v == "true")
329 });
330 if *PRINT_UNUSED_REFERENCES {
331 use turbo_tasks::TryJoinIterExt;
332 println!(
333 "unused references: {:#?}",
334 debug_unused_references_name
335 .iter()
336 .map(async |(s, e, t)| Ok((
337 s.ident_string().await?,
338 e,
339 t.ident_string().await?,
340 )))
341 .try_join()
342 .await?
343 );
344 }
345
346 static PRINT_USED_EXPORTS: LazyLock<bool> = LazyLock::new(|| {
347 std::env::var_os("TURBOPACK_PRINT_USED_EXPORTS")
348 .is_some_and(|v| v == "1" || v == "true")
349 });
350 if *PRINT_USED_EXPORTS {
351 use turbo_tasks::TryJoinIterExt;
352 println!(
353 "used exports: {:#?}",
354 used_exports
355 .iter()
356 .map(async |(m, v)| Ok((m.ident_string().await?, v,)))
357 .try_join()
358 .await?
359 );
360 }
361 }
362
363 Ok(BindingUsageInfo {
364 unused_references: ResolvedVc::cell(unused_references),
365 unused_references_edges,
366 used_exports: ResolvedVc::cell(used_exports),
367 export_circuit_breakers: ResolvedVc::cell(export_circuit_breakers),
368 partial_namespace_modules: ResolvedVc::cell(partial_namespace_modules),
369 }
370 .cell())
371 }
372 .instrument(span_outer)
373 .await
374}
375
376#[turbo_tasks::value]
377#[derive(Default, Clone, Debug)]
378pub enum ModuleExportUsageInfo {
379 #[default]
381 Evaluation,
382 Exports(AutoSet<RcStr>),
383 All,
384}
385
386#[turbo_tasks::value_impl]
387impl ModuleExportUsageInfo {
388 #[turbo_tasks::function]
389 pub fn all() -> Vc<Self> {
390 ModuleExportUsageInfo::All.cell()
391 }
392}
393
394impl ModuleExportUsageInfo {
395 pub fn add(&mut self, usage: &ExportUsage) -> bool {
397 match (&mut *self, usage) {
398 (Self::All, _) => false,
399 (_, ExportUsage::All) => {
400 *self = Self::All;
401 true
402 }
403 (Self::Evaluation, ExportUsage::Named(name)) => {
404 *self = Self::Exports(AutoSet::from_iter([name.clone()]));
406 true
407 }
408 (Self::Evaluation, ExportUsage::PartialNamespaceObject(names)) => {
409 *self = Self::Exports(AutoSet::from_iter(names.iter().cloned()));
410 true
411 }
412 (Self::Exports(l), ExportUsage::Named(r)) => {
413 l.insert(r.clone())
415 }
416 (Self::Exports(l), ExportUsage::PartialNamespaceObject(names)) => {
417 let mut changed = false;
418 for name in names {
419 changed |= l.insert(name.clone());
420 }
421 changed
422 }
423 (_, ExportUsage::Evaluation) => false,
424 }
425 }
426
427 pub fn is_export_used(&self, export: &RcStr) -> bool {
428 match self {
429 Self::All => true,
430 Self::Evaluation => false,
431 Self::Exports(exports) => exports.contains(export),
432 }
433 }
434}