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]
27#[derive(Clone, Default, Debug)]
28pub struct BindingUsageInfo {
29 unused_references: ResolvedVc<UnusedReferences>,
30 #[turbo_tasks(trace_ignore)]
31 unused_references_edges: FxHashSet<GraphEdgeIndex>,
32
33 used_exports: ResolvedVc<UsedExportsMap>,
34 export_circuit_breakers: ResolvedVc<ExportCircuitBreakers>,
35}
36
37#[turbo_tasks::value(transparent)]
38pub struct OptionBindingUsageInfo(Option<ResolvedVc<BindingUsageInfo>>);
39
40#[turbo_tasks::value]
41pub struct ModuleExportUsage {
42 pub export_usage: ResolvedVc<ModuleExportUsageInfo>,
43 pub is_circuit_breaker: bool,
45}
46#[turbo_tasks::value_impl]
47impl ModuleExportUsage {
48 #[turbo_tasks::function]
49 pub async fn all() -> Result<Vc<Self>> {
50 Ok(Self {
51 export_usage: ModuleExportUsageInfo::all().to_resolved().await?,
52 is_circuit_breaker: true,
53 }
54 .cell())
55 }
56}
57
58impl BindingUsageInfo {
59 pub fn is_reference_unused_edge(&self, edge: &GraphEdgeIndex) -> bool {
60 self.unused_references_edges.contains(edge)
61 }
62
63 pub async fn used_exports(
64 &self,
65 module: ResolvedVc<Box<dyn Module>>,
66 ) -> Result<Vc<ModuleExportUsage>> {
67 let is_circuit_breaker = self.export_circuit_breakers.contains_key(&module).await?;
68 let Some(exports) = self.used_exports.get(&module).await? else {
69 let ident = module.ident_string().await?;
71 if ident.contains(".wasm_.loader.mjs") || ident.contains("/__nextjs-internal-proxy.") {
72 return Ok(ModuleExportUsage::all());
77 }
78
79 bail!("export usage not found for module: {ident:?}");
80 };
81 Ok(ModuleExportUsage {
82 export_usage: (*exports).clone().resolved_cell(),
83 is_circuit_breaker,
84 }
85 .cell())
86 }
87}
88
89#[turbo_tasks::value_impl]
90impl BindingUsageInfo {
91 #[turbo_tasks::function]
92 pub fn unused_references(&self) -> Vc<UnusedReferences> {
93 *self.unused_references
94 }
95}
96
97#[turbo_tasks::function(operation)]
98pub async fn compute_binding_usage_info(
99 graph: OperationVc<ModuleGraph>,
100 remove_unused_imports: bool,
101) -> Result<Vc<BindingUsageInfo>> {
102 let span_outer = tracing::info_span!(
103 "compute binding usage info",
104 visit_count = tracing::field::Empty,
105 unused_reference_count = tracing::field::Empty
106 );
107 let span = span_outer.clone();
108
109 async move {
110 let mut used_exports = FxHashMap::<_, ModuleExportUsageInfo>::default();
111 #[cfg(debug_assertions)]
112 let mut debug_unused_references_name = FxHashSet::<(
113 ResolvedVc<Box<dyn Module>>,
114 ExportUsage,
115 ResolvedVc<Box<dyn Module>>,
116 )>::default();
117 let mut unused_references_edges = FxHashSet::default();
118 let mut unused_references = FxHashSet::default();
119
120 let graph = graph.connect();
121 let graph_ref = graph.await?;
122 if graph_ref.binding_usage.is_some() {
123 panic!(
135 "don't run compute_binding_usage_info on a graph after calling \
136 without_unused_references"
137 );
138 }
139 let side_effect_free_modules = if remove_unused_imports {
140 let side_effect_free_modules = compute_side_effect_free_module_info(graph).await?;
141 span.record("side_effect_free_modules", side_effect_free_modules.len());
142 Some(side_effect_free_modules)
143 } else {
144 None
145 };
146
147 let entries = graph_ref.graphs.iter().flat_map(|g| g.entry_modules());
148
149 let visit_count = graph_ref.traverse_edges_fixed_point_with_priority(
150 entries.map(|m| (m, 0)),
151 &mut (),
152 |parent, target, _| {
153 let Some((parent, ref_data, edge)) = parent else {
155 used_exports.insert(target, ModuleExportUsageInfo::All);
156 return Ok(GraphTraversalAction::Continue);
157 };
158
159 if remove_unused_imports {
160 if matches!(&ref_data.binding_usage.export, ExportUsage::Evaluation)
164 && side_effect_free_modules
165 .as_ref()
166 .expect("this must be present if `remove_unused_imports` is true")
167 .contains(&target)
168 {
169 #[cfg(debug_assertions)]
170 debug_unused_references_name.insert((
171 parent,
172 ref_data.binding_usage.export.clone(),
173 target,
174 ));
175 unused_references_edges.insert(edge);
176 unused_references.insert(ref_data.reference);
177 return Ok(GraphTraversalAction::Skip);
178 }
179 match &ref_data.binding_usage.import {
181 ImportUsage::Exports(exports) => {
182 let source_used_exports = used_exports
183 .get(&parent)
184 .context("parent module must have usage info")?;
185 if exports
186 .iter()
187 .all(|e| !source_used_exports.is_export_used(e))
188 {
189 #[cfg(debug_assertions)]
191 debug_unused_references_name.insert((
192 parent,
193 ref_data.binding_usage.export.clone(),
194 target,
195 ));
196 unused_references_edges.insert(edge);
197 unused_references.insert(ref_data.reference);
198
199 return Ok(GraphTraversalAction::Skip);
200 } else {
201 #[cfg(debug_assertions)]
202 debug_unused_references_name.remove(&(
203 parent,
204 ref_data.binding_usage.export.clone(),
205 target,
206 ));
207 unused_references_edges.remove(&edge);
208 unused_references.remove(&ref_data.reference);
209 }
211 }
212 ImportUsage::TopLevel => {
213 #[cfg(debug_assertions)]
214 debug_unused_references_name.remove(&(
215 parent,
216 ref_data.binding_usage.export.clone(),
217 target,
218 ));
219 unused_references_edges.remove(&edge);
220 unused_references.remove(&ref_data.reference);
221 }
223 }
224 }
225
226 let entry = used_exports.entry(target);
227 let is_first_visit = matches!(entry, Entry::Vacant(_));
228 if entry.or_default().add(&ref_data.binding_usage.export) || is_first_visit {
229 Ok(GraphTraversalAction::Continue)
232 } else {
233 Ok(GraphTraversalAction::Skip)
234 }
235 },
236 |_, _| Ok(0),
237 )?;
238
239 let mut export_circuit_breakers = FxHashSet::default();
252
253 graph_ref.traverse_cycles(
254 |e| e.chunking_type.is_parallel() && !unused_references.contains(&e.reference),
256 |cycle| {
257 export_circuit_breakers.extend(cycle.iter().map(|n| **n));
266 Ok(())
267 },
268 )?;
269
270 span.record("visit_count", visit_count);
271 span.record("unused_reference_count", unused_references.len());
272
273 #[cfg(debug_assertions)]
274 {
275 use std::sync::LazyLock;
276 static PRINT_UNUSED_REFERENCES: LazyLock<bool> = LazyLock::new(|| {
277 std::env::var_os("TURBOPACK_PRINT_UNUSED_REFERENCES")
278 .is_some_and(|v| v == "1" || v == "true")
279 });
280 if *PRINT_UNUSED_REFERENCES {
281 use turbo_tasks::TryJoinIterExt;
282 println!(
283 "unused references: {:#?}",
284 debug_unused_references_name
285 .iter()
286 .map(async |(s, e, t)| Ok((
287 s.ident_string().await?,
288 e,
289 t.ident_string().await?,
290 )))
291 .try_join()
292 .await?
293 );
294 }
295
296 static PRINT_USED_EXPORTS: LazyLock<bool> = LazyLock::new(|| {
297 std::env::var_os("TURBOPACK_PRINT_USED_EXPORTS")
298 .is_some_and(|v| v == "1" || v == "true")
299 });
300 if *PRINT_USED_EXPORTS {
301 use turbo_tasks::TryJoinIterExt;
302 println!(
303 "used exports: {:#?}",
304 used_exports
305 .iter()
306 .map(async |(m, v)| Ok((m.ident_string().await?, v,)))
307 .try_join()
308 .await?
309 );
310 }
311 }
312
313 Ok(BindingUsageInfo {
314 unused_references: ResolvedVc::cell(unused_references),
315 unused_references_edges,
316 used_exports: ResolvedVc::cell(used_exports),
317 export_circuit_breakers: ResolvedVc::cell(export_circuit_breakers),
318 }
319 .cell())
320 }
321 .instrument(span_outer)
322 .await
323}
324
325#[turbo_tasks::value]
326#[derive(Default, Clone, Debug)]
327pub enum ModuleExportUsageInfo {
328 #[default]
330 Evaluation,
331 Exports(AutoSet<RcStr>),
332 All,
333}
334
335#[turbo_tasks::value_impl]
336impl ModuleExportUsageInfo {
337 #[turbo_tasks::function]
338 pub fn all() -> Vc<Self> {
339 ModuleExportUsageInfo::All.cell()
340 }
341}
342
343impl ModuleExportUsageInfo {
344 pub fn add(&mut self, usage: &ExportUsage) -> bool {
346 match (&mut *self, usage) {
347 (Self::All, _) => false,
348 (_, ExportUsage::All) => {
349 *self = Self::All;
350 true
351 }
352 (Self::Evaluation, ExportUsage::Named(name)) => {
353 *self = Self::Exports(AutoSet::from_iter([name.clone()]));
355 true
356 }
357 (Self::Evaluation, ExportUsage::PartialNamespaceObject(names)) => {
358 *self = Self::Exports(AutoSet::from_iter(names.iter().cloned()));
359 true
360 }
361 (Self::Exports(l), ExportUsage::Named(r)) => {
362 l.insert(r.clone())
364 }
365 (Self::Exports(l), ExportUsage::PartialNamespaceObject(names)) => {
366 let mut changed = false;
367 for name in names {
368 changed |= l.insert(name.clone());
369 }
370 changed
371 }
372 (_, ExportUsage::Evaluation) => false,
373 }
374 }
375
376 pub fn is_export_used(&self, export: &RcStr) -> bool {
377 match self {
378 Self::All => true,
379 Self::Evaluation => false,
380 Self::Exports(exports) => exports.contains(export),
381 }
382 }
383}