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::{ResolvedVc, Vc};
9
10use crate::{
11    module::Module,
12    module_graph::{GraphEdgeIndex, GraphTraversalAction, ModuleGraph},
13    reference::ModuleReference,
14    resolve::{ExportUsage, ImportUsage},
15};
16
17#[turbo_tasks::value]
18#[derive(Clone, Default, Debug)]
19pub struct BindingUsageInfo {
20    unused_references: FxHashSet<ResolvedVc<Box<dyn ModuleReference>>>,
21    #[turbo_tasks(trace_ignore)]
22    unused_references_edges: FxHashSet<GraphEdgeIndex>,
23
24    used_exports: FxHashMap<ResolvedVc<Box<dyn Module>>, ModuleExportUsageInfo>,
25    export_circuit_breakers: FxHashSet<ResolvedVc<Box<dyn Module>>>,
26}
27
28#[turbo_tasks::value(transparent)]
29pub struct OptionBindingUsageInfo(Option<ResolvedVc<BindingUsageInfo>>);
30
31#[turbo_tasks::value]
32pub struct ModuleExportUsage {
33    pub export_usage: ResolvedVc<ModuleExportUsageInfo>,
34    // Whether this module exists in an import cycle and has been selected to break the cycle.
35    pub is_circuit_breaker: bool,
36}
37#[turbo_tasks::value_impl]
38impl ModuleExportUsage {
39    #[turbo_tasks::function]
40    pub async fn all() -> Result<Vc<Self>> {
41        Ok(Self {
42            export_usage: ModuleExportUsageInfo::all().to_resolved().await?,
43            is_circuit_breaker: true,
44        }
45        .cell())
46    }
47}
48
49impl BindingUsageInfo {
50    pub fn is_reference_unused_edge(&self, edge: &GraphEdgeIndex) -> bool {
51        self.unused_references_edges.contains(edge)
52    }
53
54    pub fn is_reference_unused(&self, reference: &ResolvedVc<Box<dyn ModuleReference>>) -> bool {
55        self.unused_references.contains(reference)
56    }
57
58    pub async fn used_exports(
59        &self,
60        module: ResolvedVc<Box<dyn Module>>,
61    ) -> Result<Vc<ModuleExportUsage>> {
62        let is_circuit_breaker = self.export_circuit_breakers.contains(&module);
63        let Some(exports) = self.used_exports.get(&module) else {
64            // There are some module that are codegened, but not referenced in the module graph,
65            let ident = module.ident_string().await?;
66            if ident.contains(".wasm_.loader.mjs") || ident.contains("/__nextjs-internal-proxy.") {
67                // Both the turbopack-wasm `ModuleChunkItem` and `EcmascriptClientReferenceModule`
68                // do `self.slightly_different_module().as_chunk_item()`, so the
69                // module that codegen sees isn't actually in the module graph.
70                // TODO fix these cases
71                return Ok(ModuleExportUsage::all());
72            }
73
74            bail!("export usage not found for module: {ident:?}");
75        };
76        Ok(ModuleExportUsage {
77            export_usage: exports.clone().resolved_cell(),
78            is_circuit_breaker,
79        }
80        .cell())
81    }
82}
83
84#[turbo_tasks::function(operation)]
85pub async fn compute_binding_usage_info(
86    graph: ResolvedVc<ModuleGraph>,
87    remove_unused_imports: bool,
88) -> Result<Vc<BindingUsageInfo>> {
89    let span_outer = tracing::info_span!(
90        "compute bindung usage info",
91        visit_count = tracing::field::Empty,
92        unused_reference_count = tracing::field::Empty
93    );
94    let span = span_outer.clone();
95
96    async move {
97        let mut used_exports = FxHashMap::<_, ModuleExportUsageInfo>::default();
98        #[cfg(debug_assertions)]
99        let mut debug_unused_references_name = FxHashSet::<(
100            ResolvedVc<Box<dyn Module>>,
101            ExportUsage,
102            ResolvedVc<Box<dyn Module>>,
103        )>::default();
104        let mut unused_references_edges = FxHashSet::default();
105        let mut unused_references = FxHashSet::default();
106
107        if graph.await?.binding_usage.is_some() {
108            // If the graph already has binding usage info, return it directly. This is
109            // unfortunately easy to do with
110            // ```
111            // fn get_module_graph(){
112            //   let graph = ....;
113            //   let graph = graph.without_unused_references(compute_binding_usage_info(graph));
114            //   return graph
115            // }
116            //
117            // compute_binding_usage_info(get_module_graph())
118            // ```
119            panic!(
120                "don't run compute_binding_usage_info on a graph after calling \
121                 without_unused_references"
122            );
123        }
124
125        let graph = graph.read_graphs().await?;
126
127        let entries = graph.graphs.iter().flat_map(|g| g.entry_modules());
128
129        let visit_count = graph.traverse_edges_fixed_point_with_priority(
130            entries.map(|m| (m, 0)),
131            &mut (),
132            |parent, target, _| {
133                // Entries are always used
134                let Some((parent, ref_data, edge)) = parent else {
135                    used_exports.insert(target, ModuleExportUsageInfo::All);
136                    return Ok(GraphTraversalAction::Continue);
137                };
138
139                if remove_unused_imports {
140                    // If the current edge is an unused import, skip it
141                    match &ref_data.binding_usage.import {
142                        ImportUsage::Exports(exports) => {
143                            let source_used_exports = used_exports
144                                .get(&parent)
145                                .context("parent module must have usage info")?;
146                            if exports
147                                .iter()
148                                .all(|e| !source_used_exports.is_export_used(e))
149                            {
150                                #[cfg(debug_assertions)]
151                                debug_unused_references_name.insert((
152                                    parent,
153                                    ref_data.binding_usage.export.clone(),
154                                    target,
155                                ));
156                                unused_references_edges.insert(edge);
157                                unused_references.insert(ref_data.reference);
158
159                                return Ok(GraphTraversalAction::Skip);
160                            } else {
161                                #[cfg(debug_assertions)]
162                                debug_unused_references_name.remove(&(
163                                    parent,
164                                    ref_data.binding_usage.export.clone(),
165                                    target,
166                                ));
167                                unused_references_edges.remove(&edge);
168                                unused_references.remove(&ref_data.reference);
169                                // Continue, add export
170                            }
171                        }
172                        ImportUsage::SideEffects => {
173                            #[cfg(debug_assertions)]
174                            debug_unused_references_name.remove(&(
175                                parent,
176                                ref_data.binding_usage.export.clone(),
177                                target,
178                            ));
179                            unused_references_edges.remove(&edge);
180                            unused_references.remove(&ref_data.reference);
181                            // Continue, has to always be included
182                        }
183                    }
184                }
185
186                let entry = used_exports.entry(target);
187                let is_first_visit = matches!(entry, Entry::Vacant(_));
188                if entry.or_default().add(&ref_data.binding_usage.export) || is_first_visit {
189                    // First visit, or the used exports changed. This can cause more imports to get
190                    // used downstream.
191                    Ok(GraphTraversalAction::Continue)
192                } else {
193                    Ok(GraphTraversalAction::Skip)
194                }
195            },
196            |_, _| Ok(0),
197        )?;
198
199        // Compute cycles and select modules to be 'circuit breakers'
200        // A circuit breaker module will need to eagerly export lazy getters for its exports to
201        // break an evaluation cycle all other modules can export values after defining them
202        let mut export_circuit_breakers = FxHashSet::default();
203        graph.traverse_cycles(
204            |e| e.chunking_type.is_parallel(),
205            |cycle| {
206                // To break cycles we need to ensure that no importing module can observe a
207                // partially populated exports object.
208
209                // We could compute this based on the module graph via a DFS from each entry point
210                // to the cycle.  Whatever node is hit first is an entry point to the cycle.
211                // (scope hoisting does something similar) and then we would only need to
212                // mark 'entry' modules (basically the targets of back edges in the export graph) as
213                // circuit breakers.  For now we just mark everything on the theory that cycles are
214                // rare.  For vercel-site on 8/22/2025 there were 106 cycles covering 800 modules
215                // (or 1.2% of all modules).  So with this analysis we could potentially drop 80% of
216                // the cycle breaker modules.
217                export_circuit_breakers.extend(cycle.iter().map(|n| **n));
218                Ok(())
219            },
220        )?;
221
222        span.record("visit_count", visit_count);
223        span.record("unused_reference_count", unused_references.len());
224
225        #[cfg(debug_assertions)]
226        {
227            use once_cell::sync::Lazy;
228            static PRINT_UNUSED_REFERENCES: Lazy<bool> = Lazy::new(|| {
229                std::env::var_os("TURBOPACK_PRINT_UNUSED_REFERENCES")
230                    .is_some_and(|v| v == "1" || v == "true")
231            });
232            if *PRINT_UNUSED_REFERENCES {
233                use turbo_tasks::TryJoinIterExt;
234                println!(
235                    "unused references: {:#?}",
236                    debug_unused_references_name
237                        .iter()
238                        .map(async |(s, e, t)| Ok((
239                            s.ident_string().await?,
240                            e,
241                            t.ident_string().await?,
242                        )))
243                        .try_join()
244                        .await?
245                );
246            }
247        }
248
249        Ok(BindingUsageInfo {
250            unused_references,
251            unused_references_edges,
252            used_exports,
253            export_circuit_breakers,
254        }
255        .cell())
256    }
257    .instrument(span_outer)
258    .await
259}
260
261#[turbo_tasks::value]
262#[derive(Default, Clone, Debug)]
263pub enum ModuleExportUsageInfo {
264    /// Only the side effects are needed, no exports is used.
265    #[default]
266    Evaluation,
267    Exports(AutoSet<RcStr>),
268    All,
269}
270
271#[turbo_tasks::value_impl]
272impl ModuleExportUsageInfo {
273    #[turbo_tasks::function]
274    pub fn all() -> Vc<Self> {
275        ModuleExportUsageInfo::All.cell()
276    }
277}
278
279impl ModuleExportUsageInfo {
280    /// Merge the given usage into self. Returns true if Self changed.
281    pub fn add(&mut self, usage: &ExportUsage) -> bool {
282        match (&mut *self, usage) {
283            (Self::All, _) => false,
284            (_, ExportUsage::All) => {
285                *self = Self::All;
286                true
287            }
288            (Self::Evaluation, ExportUsage::Named(name)) => {
289                // Promote evaluation to something more specific
290                *self = Self::Exports(AutoSet::from_iter([name.clone()]));
291                true
292            }
293            (Self::Exports(l), ExportUsage::Named(r)) => {
294                // Merge exports
295                l.insert(r.clone())
296            }
297            (_, ExportUsage::Evaluation) => false,
298        }
299    }
300
301    pub fn is_export_used(&self, export: &RcStr) -> bool {
302        match self {
303            Self::All => true,
304            Self::Evaluation => false,
305            Self::Exports(exports) => exports.contains(export),
306        }
307    }
308}