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]
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    // Whether this module exists in an import cycle and has been selected to break the cycle.
38    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            // There are some module that are codegened, but not referenced in the module graph,
64            let ident = module.ident_string().await?;
65            if ident.contains(".wasm_.loader.mjs") || ident.contains("/__nextjs-internal-proxy.") {
66                // Both the turbopack-wasm `ModuleChunkItem` and `EcmascriptClientReferenceModule`
67                // do `self.slightly_different_module().as_chunk_item()`, so the
68                // module that codegen sees isn't actually in the module graph.
69                // TODO fix these cases
70                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            // If the graph already has binding usage info, return it directly. This is
118            // unfortunately easy to do with
119            // ```
120            // fn get_module_graph(){
121            //   let graph = ....;
122            //   let graph = graph.without_unused_references(compute_binding_usage_info(graph));
123            //   return graph
124            // }
125            //
126            // compute_binding_usage_info(get_module_graph())
127            // ```
128            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                // Entries are always used
148                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 this is an evaluation reference and the target has no side effects
155                    // then we can drop it. NOTE: many `imports` create parallel Evaluation
156                    // and Named/All references
157                    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                    // If the current edge is an unused import, skip it
174                    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                                // all exports are unused
184                                #[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                                // Continue, add export
204                            }
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                            // Continue, has to always be included
216                        }
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                    // First visit, or the used exports changed. This can cause more imports to get
224                    // used downstream.
225                    Ok(GraphTraversalAction::Continue)
226                } else {
227                    Ok(GraphTraversalAction::Skip)
228                }
229            },
230            |_, _| Ok(0),
231        )?;
232
233        // Compute cycles and select modules to be 'circuit breakers'
234        //
235        // To break cycles we need to ensure that no importing module can observe a
236        // partially populated exports object.
237        //
238        // A circuit breaker module will need to eagerly export lazy getters for its exports to
239        // break an evaluation cycle all other modules can export values after defining them
240        //
241        // In particular, this is also needed with scope hoisting and self-imports, as in
242        // that case `__turbopack_esm__` is what initializes the exports object. (Without
243        // scope hoisting, the exports object is already populated before any executing the
244        // module factory.)
245        let mut export_circuit_breakers = FxHashSet::default();
246
247        graph_ref.traverse_cycles(
248            // No need to traverse edges that are unused.
249            |e| e.chunking_type.is_parallel() && !unused_references.contains(&e.reference),
250            |cycle| {
251                // We could compute this based on the module graph via a DFS from each entry point
252                // to the cycle.  Whatever node is hit first is an entry point to the cycle.
253                // (scope hoisting does something similar) and then we would only need to
254                // mark 'entry' modules (basically the targets of back edges in the export graph) as
255                // circuit breakers.  For now we just mark everything on the theory that cycles are
256                // rare.  For vercel-site on 8/22/2025 there were 106 cycles covering 800 modules
257                // (or 1.2% of all modules).  So with this analysis we could potentially drop 80% of
258                // the cycle breaker modules.
259                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    /// Only the side effects are needed, no exports is used.
307    #[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    /// Merge the given usage into self. Returns true if Self changed.
323    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                // Promote evaluation to something more specific
332                *self = Self::Exports(AutoSet::from_iter([name.clone()]));
333                true
334            }
335            (Self::Exports(l), ExportUsage::Named(r)) => {
336                // Merge exports
337                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}