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/// Modules that are read through a *partial namespace object* — an
27/// [`ExportUsage::PartialNamespaceObject`] edge points at them.
28///
29/// The used export names are still known individually (that is why the usage stays
30/// [`ModuleExportUsageInfo::Exports`]), but the reads went through a namespace value: a namespace
31/// binding's member reads or destructuring, or the object a dynamic `import()` resolves to. Some of
32/// those reads are lowered to direct named accesses and some are not, and this set does not
33/// distinguish them — so a consumer that wants to rename the module's export keys has to assume the
34/// original names may still be read somewhere, and leave them alone.
35#[turbo_tasks::value(transparent, cell = "keyed")]
36pub struct PartialNamespaceModules(FxHashSet<ResolvedVc<Box<dyn Module>>>);
37
38#[turbo_tasks::value]
39#[derive(Clone, Default, Debug)]
40pub struct BindingUsageInfo {
41    unused_references: ResolvedVc<UnusedReferences>,
42    #[turbo_tasks(trace_ignore)]
43    unused_references_edges: FxHashSet<GraphEdgeIndex>,
44
45    used_exports: ResolvedVc<UsedExportsMap>,
46    export_circuit_breakers: ResolvedVc<ExportCircuitBreakers>,
47    partial_namespace_modules: ResolvedVc<PartialNamespaceModules>,
48}
49
50#[turbo_tasks::value(transparent)]
51pub struct OptionBindingUsageInfo(Option<ResolvedVc<BindingUsageInfo>>);
52
53#[turbo_tasks::value]
54pub struct ModuleExportUsage {
55    pub export_usage: ResolvedVc<ModuleExportUsageInfo>,
56    // Whether this module exists in an import cycle and has been selected to break the cycle.
57    pub is_circuit_breaker: bool,
58    /// Whether this module is read through a namespace value somewhere, which means one of those
59    /// reads may still use an original export name. See [`PartialNamespaceModules`].
60    pub namespace_object_may_escape: bool,
61}
62#[turbo_tasks::value_impl]
63impl ModuleExportUsage {
64    #[turbo_tasks::function]
65    pub async fn all() -> Result<Vc<Self>> {
66        Ok(Self {
67            export_usage: ModuleExportUsageInfo::all().to_resolved().await?,
68            is_circuit_breaker: true,
69            namespace_object_may_escape: true,
70        }
71        .cell())
72    }
73}
74
75impl BindingUsageInfo {
76    pub fn is_reference_unused_edge(&self, edge: &GraphEdgeIndex) -> bool {
77        self.unused_references_edges.contains(edge)
78    }
79
80    pub async fn used_exports(
81        &self,
82        module: ResolvedVc<Box<dyn Module>>,
83    ) -> Result<Vc<ModuleExportUsage>> {
84        let is_circuit_breaker = self.export_circuit_breakers.contains_key(&module).await?;
85        let Some(exports) = self.used_exports.get(&module).await? else {
86            // There are some module that are codegened, but not referenced in the module graph,
87            let ident = module.ident_string().await?;
88            if ident.contains(".wasm_.loader.mjs") || ident.contains("/__nextjs-internal-proxy.") {
89                // Both the turbopack-wasm `ModuleChunkItem` and `EcmascriptClientReferenceModule`
90                // do `self.slightly_different_module().as_chunk_item()`, so the
91                // module that codegen sees isn't actually in the module graph.
92                // TODO fix these cases
93                return Ok(ModuleExportUsage::all());
94            }
95
96            bail!("export usage not found for module: {ident:?}");
97        };
98        let namespace_object_may_escape =
99            self.partial_namespace_modules.contains_key(&module).await?;
100        Ok(ModuleExportUsage {
101            export_usage: (*exports).clone().resolved_cell(),
102            is_circuit_breaker,
103            namespace_object_may_escape,
104        }
105        .cell())
106    }
107}
108
109#[turbo_tasks::value_impl]
110impl BindingUsageInfo {
111    #[turbo_tasks::function]
112    pub fn unused_references(&self) -> Vc<UnusedReferences> {
113        *self.unused_references
114    }
115}
116
117#[turbo_tasks::function(operation)]
118pub async fn compute_binding_usage_info(
119    graph: OperationVc<ModuleGraph>,
120    remove_unused_imports: bool,
121) -> Result<Vc<BindingUsageInfo>> {
122    let span_outer = tracing::info_span!(
123        "compute binding usage info",
124        visit_count = tracing::field::Empty,
125        unused_reference_count = tracing::field::Empty
126    );
127    let span = span_outer.clone();
128
129    async move {
130        let mut used_exports = FxHashMap::<_, ModuleExportUsageInfo>::default();
131        let mut partial_namespace_modules = FxHashSet::default();
132        #[cfg(debug_assertions)]
133        let mut debug_unused_references_name = FxHashSet::<(
134            ResolvedVc<Box<dyn Module>>,
135            ExportUsage,
136            ResolvedVc<Box<dyn Module>>,
137        )>::default();
138        let mut unused_references_edges = FxHashSet::default();
139        let mut unused_references =
140            FxHashMap::<_, FxHashSet<ResolvedVc<Box<dyn Module>>>>::default();
141
142        let graph = graph.connect();
143        let graph_ref = graph.await?;
144        if graph_ref.binding_usage.is_some() {
145            // If the graph already has binding usage info, return it directly. This is
146            // unfortunately easy to do with
147            // ```
148            // fn get_module_graph(){
149            //   let graph = ....;
150            //   let graph = graph.without_unused_references(compute_binding_usage_info(graph));
151            //   return graph
152            // }
153            //
154            // compute_binding_usage_info(get_module_graph())
155            // ```
156            panic!(
157                "don't run compute_binding_usage_info on a graph after calling \
158                 without_unused_references"
159            );
160        }
161        let side_effect_free_modules = if remove_unused_imports {
162            let side_effect_free_modules = compute_side_effect_free_module_info(graph).await?;
163            span.record("side_effect_free_modules", side_effect_free_modules.len());
164            Some(side_effect_free_modules)
165        } else {
166            None
167        };
168
169        let entries = graph_ref.all_chunk_group_entry_modules();
170
171        let visit_count = graph_ref.traverse_edges_fixed_point_with_priority(
172            entries.map(|m| (m, 0)),
173            &mut (),
174            |parent, target, _, _| {
175                // Entries are always used
176                let Some((parent, ref_data, edge)) = parent else {
177                    used_exports.insert(target, ModuleExportUsageInfo::All);
178                    return Ok(GraphTraversalAction::Continue);
179                };
180
181                if remove_unused_imports {
182                    // If this is an evaluation reference and the target has no side effects
183                    // then we can drop it. NOTE: many `imports` create parallel Evaluation
184                    // and Named/All references
185                    if matches!(&ref_data.binding_usage.export, ExportUsage::Evaluation)
186                        && side_effect_free_modules
187                            .as_ref()
188                            .expect("this must be present if `remove_unused_imports` is true")
189                            .contains(&target)
190                    {
191                        #[cfg(debug_assertions)]
192                        debug_unused_references_name.insert((
193                            parent,
194                            ref_data.binding_usage.export.clone(),
195                            target,
196                        ));
197                        unused_references_edges.insert(edge);
198                        unused_references
199                            .entry(ref_data.reference)
200                            .or_default()
201                            .insert(target);
202                        return Ok(GraphTraversalAction::Skip);
203                    }
204                    // If the current edge is an unused import, skip it
205                    match &ref_data.binding_usage.import {
206                        ImportUsage::Exports(exports) => {
207                            let source_used_exports = used_exports
208                                .get(&parent)
209                                .context("parent module must have usage info")?;
210                            if exports
211                                .iter()
212                                .all(|e| !source_used_exports.is_export_used(e))
213                            {
214                                // all exports are unused
215                                #[cfg(debug_assertions)]
216                                debug_unused_references_name.insert((
217                                    parent,
218                                    ref_data.binding_usage.export.clone(),
219                                    target,
220                                ));
221                                unused_references_edges.insert(edge);
222                                unused_references
223                                    .entry(ref_data.reference)
224                                    .or_default()
225                                    .insert(target);
226
227                                return Ok(GraphTraversalAction::Skip);
228                            } else {
229                                #[cfg(debug_assertions)]
230                                debug_unused_references_name.remove(&(
231                                    parent,
232                                    ref_data.binding_usage.export.clone(),
233                                    target,
234                                ));
235                                unused_references_edges.remove(&edge);
236                                if let Entry::Occupied(mut e) =
237                                    unused_references.entry(ref_data.reference)
238                                {
239                                    e.get_mut().remove(&target);
240                                    if e.get().is_empty() {
241                                        e.remove();
242                                    }
243                                }
244                                // Continue, add export
245                            }
246                        }
247                        ImportUsage::TopLevel => {
248                            #[cfg(debug_assertions)]
249                            debug_unused_references_name.remove(&(
250                                parent,
251                                ref_data.binding_usage.export.clone(),
252                                target,
253                            ));
254                            unused_references_edges.remove(&edge);
255                            if let Entry::Occupied(mut e) =
256                                unused_references.entry(ref_data.reference)
257                            {
258                                e.get_mut().remove(&target);
259                                if e.get().is_empty() {
260                                    e.remove();
261                                }
262                            }
263                            // Continue, has to always be included
264                        }
265                    }
266                }
267
268                let entry = used_exports.entry(target);
269                let is_first_visit = matches!(entry, Entry::Vacant(_));
270                if matches!(
271                    &ref_data.binding_usage.export,
272                    ExportUsage::PartialNamespaceObject(_)
273                ) {
274                    // `target` is read through a namespace value. We know which names are used, but
275                    // not that every read of them was lowered to a direct named access.
276                    partial_namespace_modules.insert(target);
277                }
278                if entry.or_default().add(&ref_data.binding_usage.export) || is_first_visit {
279                    // First visit, or the used exports changed. This can cause more imports to get
280                    // used downstream.
281                    Ok(GraphTraversalAction::Continue)
282                } else {
283                    Ok(GraphTraversalAction::Skip)
284                }
285            },
286            |_, _| Ok(0),
287        )?;
288
289        // Compute cycles and select modules to be 'circuit breakers'
290        //
291        // To break cycles we need to ensure that no importing module can observe a
292        // partially populated exports object.
293        //
294        // A circuit breaker module will need to eagerly export lazy getters for its exports to
295        // break an evaluation cycle all other modules can export values after defining them
296        //
297        // In particular, this is also needed with scope hoisting and self-imports, as in
298        // that case `__turbopack_esm__` is what initializes the exports object. (Without
299        // scope hoisting, the exports object is already populated before any executing the
300        // module factory.)
301        let mut export_circuit_breakers = FxHashSet::default();
302
303        graph_ref.traverse_cycles(
304            // No need to traverse edges that are unused.
305            |e| e.chunking_type.is_parallel() && !unused_references.contains_key(&e.reference),
306            |cycle| {
307                // We could compute this based on the module graph via a DFS from each entry point
308                // to the cycle.  Whatever node is hit first is an entry point to the cycle.
309                // (scope hoisting does something similar) and then we would only need to
310                // mark 'entry' modules (basically the targets of back edges in the export graph) as
311                // circuit breakers.  For now we just mark everything on the theory that cycles are
312                // rare.  For vercel-site on 8/22/2025 there were 106 cycles covering 800 modules
313                // (or 1.2% of all modules).  So with this analysis we could potentially drop 80% of
314                // the cycle breaker modules.
315                export_circuit_breakers.extend(cycle.iter().map(|n| **n));
316                Ok(())
317            },
318        )?;
319
320        span.record("visit_count", visit_count);
321        span.record("unused_reference_count", unused_references.len());
322
323        #[cfg(debug_assertions)]
324        {
325            use std::sync::LazyLock;
326            static PRINT_UNUSED_REFERENCES: LazyLock<bool> = LazyLock::new(|| {
327                std::env::var_os("TURBOPACK_PRINT_UNUSED_REFERENCES")
328                    .is_some_and(|v| v == "1" || v == "true")
329            });
330            if *PRINT_UNUSED_REFERENCES {
331                use turbo_tasks::TryJoinIterExt;
332                println!(
333                    "unused references: {:#?}",
334                    debug_unused_references_name
335                        .iter()
336                        .map(async |(s, e, t)| Ok((
337                            s.ident_string().await?,
338                            e,
339                            t.ident_string().await?,
340                        )))
341                        .try_join()
342                        .await?
343                );
344            }
345
346            static PRINT_USED_EXPORTS: LazyLock<bool> = LazyLock::new(|| {
347                std::env::var_os("TURBOPACK_PRINT_USED_EXPORTS")
348                    .is_some_and(|v| v == "1" || v == "true")
349            });
350            if *PRINT_USED_EXPORTS {
351                use turbo_tasks::TryJoinIterExt;
352                println!(
353                    "used exports: {:#?}",
354                    used_exports
355                        .iter()
356                        .map(async |(m, v)| Ok((m.ident_string().await?, v,)))
357                        .try_join()
358                        .await?
359                );
360            }
361        }
362
363        Ok(BindingUsageInfo {
364            unused_references: ResolvedVc::cell(unused_references),
365            unused_references_edges,
366            used_exports: ResolvedVc::cell(used_exports),
367            export_circuit_breakers: ResolvedVc::cell(export_circuit_breakers),
368            partial_namespace_modules: ResolvedVc::cell(partial_namespace_modules),
369        }
370        .cell())
371    }
372    .instrument(span_outer)
373    .await
374}
375
376#[turbo_tasks::value]
377#[derive(Default, Clone, Debug)]
378pub enum ModuleExportUsageInfo {
379    /// Only the side effects are needed, no exports is used.
380    #[default]
381    Evaluation,
382    Exports(AutoSet<RcStr>),
383    All,
384}
385
386#[turbo_tasks::value_impl]
387impl ModuleExportUsageInfo {
388    #[turbo_tasks::function]
389    pub fn all() -> Vc<Self> {
390        ModuleExportUsageInfo::All.cell()
391    }
392}
393
394impl ModuleExportUsageInfo {
395    /// Merge the given usage into self. Returns true if Self changed.
396    pub fn add(&mut self, usage: &ExportUsage) -> bool {
397        match (&mut *self, usage) {
398            (Self::All, _) => false,
399            (_, ExportUsage::All) => {
400                *self = Self::All;
401                true
402            }
403            (Self::Evaluation, ExportUsage::Named(name)) => {
404                // Promote evaluation to something more specific
405                *self = Self::Exports(AutoSet::from_iter([name.clone()]));
406                true
407            }
408            (Self::Evaluation, ExportUsage::PartialNamespaceObject(names)) => {
409                *self = Self::Exports(AutoSet::from_iter(names.iter().cloned()));
410                true
411            }
412            (Self::Exports(l), ExportUsage::Named(r)) => {
413                // Merge exports
414                l.insert(r.clone())
415            }
416            (Self::Exports(l), ExportUsage::PartialNamespaceObject(names)) => {
417                let mut changed = false;
418                for name in names {
419                    changed |= l.insert(name.clone());
420                }
421                changed
422            }
423            (_, ExportUsage::Evaluation) => false,
424        }
425    }
426
427    pub fn is_export_used(&self, export: &RcStr) -> bool {
428        match self {
429            Self::All => true,
430            Self::Evaluation => false,
431            Self::Exports(exports) => exports.contains(export),
432        }
433    }
434}