Skip to main content

turbopack_core/module_graph/
binding_usage_info.rs

1use 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    // Whether this module exists in an import cycle and has been selected to break the cycle.
44    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            // There are some module that are codegened, but not referenced in the module graph,
70            let ident = module.ident_string().await?;
71            if ident.contains(".wasm_.loader.mjs") || ident.contains("/__nextjs-internal-proxy.") {
72                // Both the turbopack-wasm `ModuleChunkItem` and `EcmascriptClientReferenceModule`
73                // do `self.slightly_different_module().as_chunk_item()`, so the
74                // module that codegen sees isn't actually in the module graph.
75                // TODO fix these cases
76                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            // If the graph already has binding usage info, return it directly. This is
124            // unfortunately easy to do with
125            // ```
126            // fn get_module_graph(){
127            //   let graph = ....;
128            //   let graph = graph.without_unused_references(compute_binding_usage_info(graph));
129            //   return graph
130            // }
131            //
132            // compute_binding_usage_info(get_module_graph())
133            // ```
134            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                // Entries are always used
154                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 this is an evaluation reference and the target has no side effects
161                    // then we can drop it. NOTE: many `imports` create parallel Evaluation
162                    // and Named/All references
163                    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                    // If the current edge is an unused import, skip it
180                    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                                // all exports are unused
190                                #[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                                // Continue, add export
210                            }
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                            // Continue, has to always be included
222                        }
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                    // First visit, or the used exports changed. This can cause more imports to get
230                    // used downstream.
231                    Ok(GraphTraversalAction::Continue)
232                } else {
233                    Ok(GraphTraversalAction::Skip)
234                }
235            },
236            |_, _| Ok(0),
237        )?;
238
239        // Compute cycles and select modules to be 'circuit breakers'
240        //
241        // To break cycles we need to ensure that no importing module can observe a
242        // partially populated exports object.
243        //
244        // A circuit breaker module will need to eagerly export lazy getters for its exports to
245        // break an evaluation cycle all other modules can export values after defining them
246        //
247        // In particular, this is also needed with scope hoisting and self-imports, as in
248        // that case `__turbopack_esm__` is what initializes the exports object. (Without
249        // scope hoisting, the exports object is already populated before any executing the
250        // module factory.)
251        let mut export_circuit_breakers = FxHashSet::default();
252
253        graph_ref.traverse_cycles(
254            // No need to traverse edges that are unused.
255            |e| e.chunking_type.is_parallel() && !unused_references.contains(&e.reference),
256            |cycle| {
257                // We could compute this based on the module graph via a DFS from each entry point
258                // to the cycle.  Whatever node is hit first is an entry point to the cycle.
259                // (scope hoisting does something similar) and then we would only need to
260                // mark 'entry' modules (basically the targets of back edges in the export graph) as
261                // circuit breakers.  For now we just mark everything on the theory that cycles are
262                // rare.  For vercel-site on 8/22/2025 there were 106 cycles covering 800 modules
263                // (or 1.2% of all modules).  So with this analysis we could potentially drop 80% of
264                // the cycle breaker modules.
265                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    /// Only the side effects are needed, no exports is used.
313    #[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    /// Merge the given usage into self. Returns true if Self changed.
329    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                // Promote evaluation to something more specific
338                *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                // Merge exports
347                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}