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]
21#[derive(Clone, Default, Debug)]
22pub struct BindingUsageInfo {
23 unused_references: ResolvedVc<UnusedReferences>,
24 #[turbo_tasks(trace_ignore)]
25 unused_references_edges: FxHashSet<GraphEdgeIndex>,
26
27 used_exports: FxHashMap<ResolvedVc<Box<dyn Module>>, ModuleExportUsageInfo>,
28 export_circuit_breakers: FxHashSet<ResolvedVc<Box<dyn Module>>>,
29}
30
31#[turbo_tasks::value(transparent)]
32pub struct OptionBindingUsageInfo(Option<ResolvedVc<BindingUsageInfo>>);
33
34#[turbo_tasks::value]
35pub struct ModuleExportUsage {
36 pub export_usage: ResolvedVc<ModuleExportUsageInfo>,
37 pub is_circuit_breaker: bool,
39}
40#[turbo_tasks::value_impl]
41impl ModuleExportUsage {
42 #[turbo_tasks::function]
43 pub async fn all() -> Result<Vc<Self>> {
44 Ok(Self {
45 export_usage: ModuleExportUsageInfo::all().to_resolved().await?,
46 is_circuit_breaker: true,
47 }
48 .cell())
49 }
50}
51
52impl BindingUsageInfo {
53 pub fn is_reference_unused_edge(&self, edge: &GraphEdgeIndex) -> bool {
54 self.unused_references_edges.contains(edge)
55 }
56
57 pub async fn used_exports(
58 &self,
59 module: ResolvedVc<Box<dyn Module>>,
60 ) -> Result<Vc<ModuleExportUsage>> {
61 let is_circuit_breaker = self.export_circuit_breakers.contains(&module);
62 let Some(exports) = self.used_exports.get(&module) else {
63 let ident = module.ident_string().await?;
65 if ident.contains(".wasm_.loader.mjs") || ident.contains("/__nextjs-internal-proxy.") {
66 return Ok(ModuleExportUsage::all());
71 }
72
73 bail!("export usage not found for module: {ident:?}");
74 };
75 Ok(ModuleExportUsage {
76 export_usage: exports.clone().resolved_cell(),
77 is_circuit_breaker,
78 }
79 .cell())
80 }
81}
82
83#[turbo_tasks::value_impl]
84impl BindingUsageInfo {
85 #[turbo_tasks::function]
86 pub fn unused_references(&self) -> Vc<UnusedReferences> {
87 *self.unused_references
88 }
89}
90
91#[turbo_tasks::function(operation)]
92pub async fn compute_binding_usage_info(
93 graph: OperationVc<ModuleGraph>,
94 remove_unused_imports: bool,
95) -> Result<Vc<BindingUsageInfo>> {
96 let span_outer = tracing::info_span!(
97 "compute binding usage info",
98 visit_count = tracing::field::Empty,
99 unused_reference_count = tracing::field::Empty
100 );
101 let span = span_outer.clone();
102
103 async move {
104 let mut used_exports = FxHashMap::<_, ModuleExportUsageInfo>::default();
105 #[cfg(debug_assertions)]
106 let mut debug_unused_references_name = FxHashSet::<(
107 ResolvedVc<Box<dyn Module>>,
108 ExportUsage,
109 ResolvedVc<Box<dyn Module>>,
110 )>::default();
111 let mut unused_references_edges = FxHashSet::default();
112 let mut unused_references = FxHashSet::default();
113
114 let graph = graph.connect();
115 let graph_ref = graph.await?;
116 if graph_ref.binding_usage.is_some() {
117 panic!(
129 "don't run compute_binding_usage_info on a graph after calling \
130 without_unused_references"
131 );
132 }
133 let side_effect_free_modules = if remove_unused_imports {
134 let side_effect_free_modules = compute_side_effect_free_module_info(graph).await?;
135 span.record("side_effect_free_modules", side_effect_free_modules.len());
136 Some(side_effect_free_modules)
137 } else {
138 None
139 };
140
141 let entries = graph_ref.graphs.iter().flat_map(|g| g.entry_modules());
142
143 let visit_count = graph_ref.traverse_edges_fixed_point_with_priority(
144 entries.map(|m| (m, 0)),
145 &mut (),
146 |parent, target, _| {
147 let Some((parent, ref_data, edge)) = parent else {
149 used_exports.insert(target, ModuleExportUsageInfo::All);
150 return Ok(GraphTraversalAction::Continue);
151 };
152
153 if remove_unused_imports {
154 if matches!(&ref_data.binding_usage.export, ExportUsage::Evaluation)
158 && side_effect_free_modules
159 .as_ref()
160 .expect("this must be present if `remove_unused_imports` is true")
161 .contains(&target)
162 {
163 #[cfg(debug_assertions)]
164 debug_unused_references_name.insert((
165 parent,
166 ref_data.binding_usage.export.clone(),
167 target,
168 ));
169 unused_references_edges.insert(edge);
170 unused_references.insert(ref_data.reference);
171 return Ok(GraphTraversalAction::Skip);
172 }
173 match &ref_data.binding_usage.import {
175 ImportUsage::Exports(exports) => {
176 let source_used_exports = used_exports
177 .get(&parent)
178 .context("parent module must have usage info")?;
179 if exports
180 .iter()
181 .all(|e| !source_used_exports.is_export_used(e))
182 {
183 #[cfg(debug_assertions)]
185 debug_unused_references_name.insert((
186 parent,
187 ref_data.binding_usage.export.clone(),
188 target,
189 ));
190 unused_references_edges.insert(edge);
191 unused_references.insert(ref_data.reference);
192
193 return Ok(GraphTraversalAction::Skip);
194 } else {
195 #[cfg(debug_assertions)]
196 debug_unused_references_name.remove(&(
197 parent,
198 ref_data.binding_usage.export.clone(),
199 target,
200 ));
201 unused_references_edges.remove(&edge);
202 unused_references.remove(&ref_data.reference);
203 }
205 }
206 ImportUsage::TopLevel => {
207 #[cfg(debug_assertions)]
208 debug_unused_references_name.remove(&(
209 parent,
210 ref_data.binding_usage.export.clone(),
211 target,
212 ));
213 unused_references_edges.remove(&edge);
214 unused_references.remove(&ref_data.reference);
215 }
217 }
218 }
219
220 let entry = used_exports.entry(target);
221 let is_first_visit = matches!(entry, Entry::Vacant(_));
222 if entry.or_default().add(&ref_data.binding_usage.export) || is_first_visit {
223 Ok(GraphTraversalAction::Continue)
226 } else {
227 Ok(GraphTraversalAction::Skip)
228 }
229 },
230 |_, _| Ok(0),
231 )?;
232
233 let mut export_circuit_breakers = FxHashSet::default();
246
247 graph_ref.traverse_cycles(
248 |e| e.chunking_type.is_parallel() && !unused_references.contains(&e.reference),
250 |cycle| {
251 export_circuit_breakers.extend(cycle.iter().map(|n| **n));
260 Ok(())
261 },
262 )?;
263
264 span.record("visit_count", visit_count);
265 span.record("unused_reference_count", unused_references.len());
266
267 #[cfg(debug_assertions)]
268 {
269 use once_cell::sync::Lazy;
270 static PRINT_UNUSED_REFERENCES: Lazy<bool> = Lazy::new(|| {
271 std::env::var_os("TURBOPACK_PRINT_UNUSED_REFERENCES")
272 .is_some_and(|v| v == "1" || v == "true")
273 });
274 if *PRINT_UNUSED_REFERENCES {
275 use turbo_tasks::TryJoinIterExt;
276 println!(
277 "unused references: {:#?}",
278 debug_unused_references_name
279 .iter()
280 .map(async |(s, e, t)| Ok((
281 s.ident_string().await?,
282 e,
283 t.ident_string().await?,
284 )))
285 .try_join()
286 .await?
287 );
288 }
289 }
290
291 Ok(BindingUsageInfo {
292 unused_references: ResolvedVc::cell(unused_references),
293 unused_references_edges,
294 used_exports,
295 export_circuit_breakers,
296 }
297 .cell())
298 }
299 .instrument(span_outer)
300 .await
301}
302
303#[turbo_tasks::value]
304#[derive(Default, Clone, Debug)]
305pub enum ModuleExportUsageInfo {
306 #[default]
308 Evaluation,
309 Exports(AutoSet<RcStr>),
310 All,
311}
312
313#[turbo_tasks::value_impl]
314impl ModuleExportUsageInfo {
315 #[turbo_tasks::function]
316 pub fn all() -> Vc<Self> {
317 ModuleExportUsageInfo::All.cell()
318 }
319}
320
321impl ModuleExportUsageInfo {
322 pub fn add(&mut self, usage: &ExportUsage) -> bool {
324 match (&mut *self, usage) {
325 (Self::All, _) => false,
326 (_, ExportUsage::All) => {
327 *self = Self::All;
328 true
329 }
330 (Self::Evaluation, ExportUsage::Named(name)) => {
331 *self = Self::Exports(AutoSet::from_iter([name.clone()]));
333 true
334 }
335 (Self::Exports(l), ExportUsage::Named(r)) => {
336 l.insert(r.clone())
338 }
339 (_, ExportUsage::Evaluation) => false,
340 }
341 }
342
343 pub fn is_export_used(&self, export: &RcStr) -> bool {
344 match self {
345 Self::All => true,
346 Self::Evaluation => false,
347 Self::Exports(exports) => exports.contains(export),
348 }
349 }
350}