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 once_cell::sync::Lazy;
276 static PRINT_UNUSED_REFERENCES: Lazy<bool> = Lazy::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
297 Ok(BindingUsageInfo {
298 unused_references: ResolvedVc::cell(unused_references),
299 unused_references_edges,
300 used_exports: ResolvedVc::cell(used_exports),
301 export_circuit_breakers: ResolvedVc::cell(export_circuit_breakers),
302 }
303 .cell())
304 }
305 .instrument(span_outer)
306 .await
307}
308
309#[turbo_tasks::value]
310#[derive(Default, Clone, Debug)]
311pub enum ModuleExportUsageInfo {
312 #[default]
314 Evaluation,
315 Exports(AutoSet<RcStr>),
316 All,
317}
318
319#[turbo_tasks::value_impl]
320impl ModuleExportUsageInfo {
321 #[turbo_tasks::function]
322 pub fn all() -> Vc<Self> {
323 ModuleExportUsageInfo::All.cell()
324 }
325}
326
327impl ModuleExportUsageInfo {
328 pub fn add(&mut self, usage: &ExportUsage) -> bool {
330 match (&mut *self, usage) {
331 (Self::All, _) => false,
332 (_, ExportUsage::All) => {
333 *self = Self::All;
334 true
335 }
336 (Self::Evaluation, ExportUsage::Named(name)) => {
337 *self = Self::Exports(AutoSet::from_iter([name.clone()]));
339 true
340 }
341 (Self::Evaluation, ExportUsage::PartialNamespaceObject(names)) => {
342 *self = Self::Exports(AutoSet::from_iter(names.iter().cloned()));
343 true
344 }
345 (Self::Exports(l), ExportUsage::Named(r)) => {
346 l.insert(r.clone())
348 }
349 (Self::Exports(l), ExportUsage::PartialNamespaceObject(names)) => {
350 let mut changed = false;
351 for name in names {
352 changed |= l.insert(name.clone());
353 }
354 changed
355 }
356 (_, ExportUsage::Evaluation) => false,
357 }
358 }
359
360 pub fn is_export_used(&self, export: &RcStr) -> bool {
361 match self {
362 Self::All => true,
363 Self::Evaluation => false,
364 Self::Exports(exports) => exports.contains(export),
365 }
366 }
367}