Repository: vercel/next.js
Pull request: Turbopack: hierarchical output asset trace

- hierarchical output asset trace
- only create spans and await idents if tracing is actually enabled. We don't benefit from using inline `tracing::enabled!(` macro calls to do compile-time DCE, because these spans are always enabled in release builds anyway.

`asset [output]/...` are the output chunks
`asset [project]/...` are the NFT assets
<img width="1743" height="438" alt="Bildschirmfoto 2025-08-26 um 10 20 55" src="https://github.com/user-attachments/assets/2cea1ad4-2ed7-4a82-9b91-fcba2a0836cb" />

What this unfortunately doesn't exactly tell you immediately is which import pulled in a given NFT asset.
But you can search for e.g. `packages/next/dist/compiled/next-server/pages-turbo.runtime.prod.js` and then that will show up in the module graph as well (because it's a reference). At least in some cases

<img width="1262" height="243" alt="Bildschirmfoto 2025-08-26 um 10 22 38" src="https://github.com/user-attachments/assets/4b652ae5-9f81-4ac8-92e9-60926d10a90e" />

Closes PACK-5347

Changed files:
filename: crates/next-api/src/nft_json.rs
patch: @@ -2,10 +2,11 @@ use std::collections::{BTreeSet, VecDeque};
 
 use anyhow::{Result, bail};
 use serde_json::json;
+use tracing::{Level, Span};
 use turbo_rcstr::RcStr;
 use turbo_tasks::{
-    FxIndexMap, ReadRef, ResolvedVc, TryFlatJoinIterExt, TryJoinIterExt, Vc,
-    graph::{AdjacencyMap, GraphTraversal},
+    FxIndexMap, ReadRef, ResolvedVc, TryFlatJoinIterExt, TryJoinIterExt, ValueToString, Vc,
+    graph::{AdjacencyMap, GraphTraversal, Visit, VisitControlFlow},
 };
 use turbo_tasks_fs::{
     DirectoryEntry, File, FileSystem, FileSystemPath,
@@ -229,7 +230,8 @@ impl Asset for NftJsonAsset {
 
         // Collect base assets first
         for referenced_chunk in
-            all_assets_from_entries_filtered(Vc::cell(entries), client_root, exclude_glob).await?
+            all_assets_from_entries_filtered(Vc::cell(entries), Some(client_root), exclude_glob)
+                .await?
         {
             if chunk.eq(referenced_chunk) {
                 continue;
@@ -373,45 +375,105 @@ fn relativize_glob(glob: &str, relative_to: FileSystemPath) -> Result<(&str, Fil
 /// Walks the asset graph from multiple assets and collect all referenced
 /// assets, but filters out all client assets and glob matches.
 #[turbo_tasks::function]
-async fn all_assets_from_entries_filtered(
+pub async fn all_assets_from_entries_filtered(
     entries: Vc<OutputAssets>,
-    client_root: FileSystemPath,
+    client_root: Option<FileSystemPath>,
     exclude_glob: Option<Vc<Glob>>,
 ) -> Result<Vc<OutputAssets>> {
     let exclude_glob = if let Some(exclude_glob) = exclude_glob {
         Some(exclude_glob.await?)
     } else {
         None
     };
+    let emit_spans = tracing::enabled!(Level::INFO);
     Ok(Vc::cell(
         AdjacencyMap::new()
             .skip_duplicates()
             .visit(
-                entries.await?.iter().copied().map(ResolvedVc::upcast),
-                |asset| get_referenced_server_assets(asset, &client_root, &exclude_glob),
+                entries
+                    .await?
+                    .iter()
+                    .map(async |asset| {
+                        Ok((
+                            ResolvedVc::upcast(*asset),
+                            if emit_spans {
+                                Some(asset.path().to_string().await?)
+                            } else {
+                                None
+                            },
+                        ))
+                    })
+                    .try_join()
+                    .await?,
+                OutputAssetFilteredVisit {
+                    client_root,
+                    exclude_glob,
+                    emit_spans,
+                },
             )
             .await
             .completed()?
             .into_inner()
             .into_postorder_topological()
+            .map(|n| n.0)
             .collect(),
     ))
 }
 
+struct OutputAssetFilteredVisit {
+    client_root: Option<FileSystemPath>,
+    exclude_glob: Option<ReadRef<Glob>>,
+    emit_spans: bool,
+}
+impl Visit<(ResolvedVc<Box<dyn OutputAsset>>, Option<ReadRef<RcStr>>)>
+    for OutputAssetFilteredVisit
+{
+    type Edge = (ResolvedVc<Box<dyn OutputAsset>>, Option<ReadRef<RcStr>>);
+    type EdgesIntoIter = Vec<Self::Edge>;
+    type EdgesFuture = impl Future<Output = Result<Self::EdgesIntoIter>>;
+
+    fn visit(&mut self, edge: Self::Edge) -> VisitControlFlow<Self::Edge> {
+        VisitControlFlow::Continue(edge)
+    }
+
+    fn edges(
+        &mut self,
+        node: &(ResolvedVc<Box<dyn OutputAsset>>, Option<ReadRef<RcStr>>),
+    ) -> Self::EdgesFuture {
+        let client_root = self.client_root.clone();
+        let exclude_glob = self.exclude_glob.clone();
+        get_referenced_server_assets(self.emit_spans, node.0, client_root, exclude_glob)
+    }
+
+    fn span(
+        &mut self,
+        node: &(ResolvedVc<Box<dyn OutputAsset>>, Option<ReadRef<RcStr>>),
+    ) -> tracing::Span {
+        if let Some(ident) = &node.1 {
+            tracing::info_span!("asset", name = display(ident))
+        } else {
+            Span::current()
+        }
+    }
+}
+
 /// Computes the list of all chunk children of a given chunk, but filters out all client assets and
 /// glob matches.
 async fn get_referenced_server_assets(
+    emit_spans: bool,
     asset: ResolvedVc<Box<dyn OutputAsset>>,
-    client_root: &FileSystemPath,
-    exclude_glob: &Option<ReadRef<Glob>>,
-) -> Result<Vec<ResolvedVc<Box<dyn OutputAsset>>>> {
+    client_root: Option<FileSystemPath>,
+    exclude_glob: Option<ReadRef<Glob>>,
+) -> Result<Vec<(ResolvedVc<Box<dyn OutputAsset>>, Option<ReadRef<RcStr>>)>> {
     asset
         .references()
         .await?
         .iter()
         .map(async |asset| {
             let asset_path = asset.path().await?;
-            if asset_path.is_inside_ref(client_root) {
+            if let Some(client_root) = &client_root
+                && asset_path.is_inside_ref(client_root)
+            {
                 return Ok(None);
             }
 
@@ -422,7 +484,14 @@ async fn get_referenced_server_assets(
                 return Ok(None);
             }
 
-            Ok(Some(*asset))
+            Ok(Some((
+                *asset,
+                if emit_spans {
+                    Some(asset.path().to_string().await?)
+                } else {
+                    None
+                },
+            )))
         })
         .try_flat_join()
         .await

filename: crates/next-core/src/emit.rs
patch: @@ -1,8 +1,9 @@
 use anyhow::Result;
-use tracing::Instrument;
+use tracing::{Instrument, Level, Span};
+use turbo_rcstr::RcStr;
 use turbo_tasks::{
-    FxIndexSet, ResolvedVc, TryFlatJoinIterExt, Vc,
-    graph::{AdjacencyMap, GraphTraversal},
+    FxIndexSet, ReadRef, ResolvedVc, TryFlatJoinIterExt, TryJoinIterExt, ValueToString, Vc,
+    graph::{AdjacencyMap, GraphTraversal, Visit, VisitControlFlow},
 };
 use turbo_tasks_fs::{FileSystemPath, rebase};
 use turbopack_core::{
@@ -113,18 +114,68 @@ async fn emit_rebase(
     Ok(())
 }
 
+struct OutputAssetVisit {
+    emit_spans: bool,
+}
+impl Visit<(ResolvedVc<Box<dyn OutputAsset>>, Option<ReadRef<RcStr>>)> for OutputAssetVisit {
+    type Edge = (ResolvedVc<Box<dyn OutputAsset>>, Option<ReadRef<RcStr>>);
+    type EdgesIntoIter = Vec<Self::Edge>;
+    type EdgesFuture = impl Future<Output = Result<Self::EdgesIntoIter>>;
+
+    fn visit(&mut self, edge: Self::Edge) -> VisitControlFlow<Self::Edge> {
+        VisitControlFlow::Continue(edge)
+    }
+
+    fn edges(
+        &mut self,
+        node: &(ResolvedVc<Box<dyn OutputAsset>>, Option<ReadRef<RcStr>>),
+    ) -> Self::EdgesFuture {
+        get_referenced_assets(self.emit_spans, node.0)
+    }
+
+    fn span(
+        &mut self,
+        node: &(ResolvedVc<Box<dyn OutputAsset>>, Option<ReadRef<RcStr>>),
+    ) -> tracing::Span {
+        if let Some(ident) = &node.1 {
+            tracing::info_span!("asset", name = display(ident))
+        } else {
+            Span::current()
+        }
+    }
+}
+
 /// Walks the asset graph from multiple assets and collect all referenced
 /// assets.
 #[turbo_tasks::function]
 pub async fn all_assets_from_entries(entries: Vc<OutputAssets>) -> Result<Vc<OutputAssets>> {
+    let emit_spans = tracing::enabled!(Level::INFO);
     Ok(Vc::cell(
         AdjacencyMap::new()
             .skip_duplicates()
-            .visit(entries.await?.iter().copied(), get_referenced_assets)
+            .visit(
+                entries
+                    .await?
+                    .iter()
+                    .map(async |asset| {
+                        Ok((
+                            ResolvedVc::upcast(*asset),
+                            if emit_spans {
+                                Some(asset.path().to_string().await?)
+                            } else {
+                                None
+                            },
+                        ))
+                    })
+                    .try_join()
+                    .await?,
+                OutputAssetVisit { emit_spans },
+            )
             .await
             .completed()?
             .into_inner()
             .into_postorder_topological()
+            .map(|(asset, _)| asset)
             .collect::<FxIndexSet<_>>()
             .into_iter()
             .collect(),
@@ -133,13 +184,23 @@ pub async fn all_assets_from_entries(entries: Vc<OutputAssets>) -> Result<Vc<Out
 
 /// Computes the list of all chunk children of a given chunk.
 async fn get_referenced_assets(
+    emit_spans: bool,
     asset: ResolvedVc<Box<dyn OutputAsset>>,
-) -> Result<impl Iterator<Item = ResolvedVc<Box<dyn OutputAsset>>> + Send> {
-    Ok(asset
+) -> Result<Vec<(ResolvedVc<Box<dyn OutputAsset>>, Option<ReadRef<RcStr>>)>> {
+    asset
         .references()
         .await?
         .iter()
-        .copied()
-        .collect::<Vec<_>>()
-        .into_iter())
+        .map(async |asset| {
+            Ok((
+                *asset,
+                if emit_spans {
+                    Some(asset.path().to_string().await?)
+                } else {
+                    None
+                },
+            ))
+        })
+        .try_join()
+        .await
 }

filename: turbopack/crates/turbopack-core/src/module_graph/mod.rs
patch: @@ -14,7 +14,7 @@ use petgraph::{
 };
 use rustc_hash::{FxHashMap, FxHashSet};
 use serde::{Deserialize, Serialize};
-use tracing::{Instrument, Span};
+use tracing::{Instrument, Level, Span};
 use turbo_rcstr::RcStr;
 use turbo_tasks::{
     CollectiblesSource, FxIndexMap, NonLocalValue, ReadRef, ResolvedVc, TryJoinIterExt,
@@ -223,12 +223,13 @@ impl SingleModuleGraph {
         visited_modules: &FxIndexMap<ResolvedVc<Box<dyn Module>>, GraphNodeIndex>,
         include_traced: bool,
     ) -> Result<Vc<Self>> {
+        let emit_spans = tracing::enabled!(Level::INFO);
         let root_edges = entries
             .iter()
             .flat_map(|e| e.entries())
             .map(|e| async move {
                 Ok(SingleModuleGraphBuilderEdge {
-                    to: SingleModuleGraphBuilderNode::new_module(e).await?,
+                    to: SingleModuleGraphBuilderNode::new_module(emit_spans, e).await?,
                     export: ExportUsage::All,
                 })
             })
@@ -241,6 +242,7 @@ impl SingleModuleGraph {
                 root_edges,
                 SingleModuleGraphBuilder {
                     visited_modules,
+                    emit_spans,
                     include_traced,
                 },
             )
@@ -1667,14 +1669,14 @@ enum SingleModuleGraphBuilderNode {
         target: ResolvedVc<Box<dyn Module>>,
         // These two fields are only used for tracing. Derived from `source.ident()` and
         // `target.ident()`
-        source_ident: ReadRef<RcStr>,
-        target_ident: ReadRef<RcStr>,
+        source_ident: Option<ReadRef<RcStr>>,
+        target_ident: Option<ReadRef<RcStr>>,
     },
     /// A regular module
     Module {
         module: ResolvedVc<Box<dyn Module>>,
         // module.ident().to_string(), eagerly computed for tracing
-        ident: ReadRef<RcStr>,
+        ident: Option<ReadRef<RcStr>>,
     },
     /// A reference to a module that is already listed in visited_modules
     VisitedModule {
@@ -1684,24 +1686,37 @@ enum SingleModuleGraphBuilderNode {
 }
 
 impl SingleModuleGraphBuilderNode {
-    async fn new_module(module: ResolvedVc<Box<dyn Module>>) -> Result<Self> {
+    async fn new_module(emit_spans: bool, module: ResolvedVc<Box<dyn Module>>) -> Result<Self> {
         let ident = module.ident();
         Ok(Self::Module {
             module,
-            ident: ident.to_string().await?,
+            ident: if emit_spans {
+                Some(ident.to_string().await?)
+            } else {
+                None
+            },
         })
     }
     async fn new_chunkable_ref(
+        emit_spans: bool,
         source: ResolvedVc<Box<dyn Module>>,
         target: ResolvedVc<Box<dyn Module>>,
         ref_data: RefData,
     ) -> Result<Self> {
         Ok(Self::ChunkableReference {
             ref_data,
             source,
-            source_ident: source.ident().to_string().await?,
+            source_ident: if emit_spans {
+                Some(source.ident().to_string().await?)
+            } else {
+                None
+            },
             target,
-            target_ident: target.ident().to_string().await?,
+            target_ident: if emit_spans {
+                Some(target.ident().to_string().await?)
+            } else {
+                None
+            },
         })
     }
     fn new_visited_module(module: ResolvedVc<Box<dyn Module>>, idx: GraphNodeIndex) -> Self {
@@ -1722,6 +1737,9 @@ const COMMON_CHUNKING_TYPE: ChunkingType = ChunkingType::Parallel {
 
 struct SingleModuleGraphBuilder<'a> {
     visited_modules: &'a FxIndexMap<ResolvedVc<Box<dyn Module>>, GraphNodeIndex>,
+
+    emit_spans: bool,
+
     /// Whether to walk ChunkingType::Traced references
     include_traced: bool,
 }
@@ -1767,6 +1785,7 @@ impl Visit<(SingleModuleGraphBuilderNode, ExportUsage)> for SingleModuleGraphBui
             SingleModuleGraphBuilderNode::VisitedModule { .. } => unreachable!(),
         };
         let visited_modules = self.visited_modules;
+        let emit_spans = self.emit_spans;
         let include_traced = self.include_traced;
         async move {
             Ok(match (module, chunkable_ref_target) {
@@ -1788,10 +1807,12 @@ impl Visit<(SingleModuleGraphBuilderNode, ExportUsage)> for SingleModuleGraphBui
                                 if let Some(idx) = visited_modules.get(&target) {
                                     SingleModuleGraphBuilderNode::new_visited_module(target, *idx)
                                 } else {
-                                    SingleModuleGraphBuilderNode::new_module(target).await?
+                                    SingleModuleGraphBuilderNode::new_module(emit_spans, target)
+                                        .await?
                                 }
                             } else {
                                 SingleModuleGraphBuilderNode::new_chunkable_ref(
+                                    emit_spans,
                                     module,
                                     target,
                                     RefData {
@@ -1814,7 +1835,11 @@ impl Visit<(SingleModuleGraphBuilderNode, ExportUsage)> for SingleModuleGraphBui
                                 *idx,
                             )
                         } else {
-                            SingleModuleGraphBuilderNode::new_module(chunkable_ref_target).await?
+                            SingleModuleGraphBuilderNode::new_module(
+                                emit_spans,
+                                chunkable_ref_target,
+                            )
+                            .await?
                         },
                         export,
                     }]
@@ -1825,15 +1850,20 @@ impl Visit<(SingleModuleGraphBuilderNode, ExportUsage)> for SingleModuleGraphBui
     }
 
     fn span(&mut self, (node, _): &(SingleModuleGraphBuilderNode, ExportUsage)) -> tracing::Span {
+        if !self.emit_spans {
+            return Span::current();
+        }
+
         match node {
-            SingleModuleGraphBuilderNode::Module { ident, .. } => {
+            SingleModuleGraphBuilderNode::Module {
+                ident: Some(ident), ..
+            } => {
                 tracing::info_span!("module", name = display(ident))
             }
-
             SingleModuleGraphBuilderNode::ChunkableReference {
                 ref_data,
-                source_ident,
-                target_ident,
+                source_ident: Some(source_ident),
+                target_ident: Some(target_ident),
                 ..
             } => match &ref_data.chunking_type {
                 ChunkingType::Parallel {
@@ -1852,6 +1882,7 @@ impl Visit<(SingleModuleGraphBuilderNode, ExportUsage)> for SingleModuleGraphBui
             SingleModuleGraphBuilderNode::VisitedModule { .. } => {
                 tracing::info_span!("visited module")
             }
+            _ => Span::current(),
         }
     }
 }