feat(pack): support turbopack bundle analyzer#2761
Conversation
There was a problem hiding this comment.
Code Review
This pull request introduces a native analysis capability to Turbopack, including a new turbopack-analyze crate and logic to generate detailed data on output assets and module graphs. It integrates this feature into the CLI via an --analyze flag and provides the necessary NAPI bindings. The review feedback highlights a potential logic error where root modules might be missed during graph traversal if they lack incoming edges. Additionally, there is an opportunity to improve performance by parallelizing the processing of output assets and their compressed sizes.
| if let Some((parent_node, reference)) = parent { | ||
| all_modules.insert(parent_node); | ||
| all_modules.insert(node); | ||
| match reference.chunking_type { | ||
| ChunkingType::Async => { | ||
| all_async_edges.insert((parent_node, node)); | ||
| } | ||
| _ => { | ||
| all_edges.insert((parent_node, node)); | ||
| } | ||
| } | ||
| } | ||
| Ok(()) | ||
| })?; |
There was a problem hiding this comment.
In analyze_module_graphs, modules are only added to all_modules if they are part of an edge where they are either a dependency or have a parent. Root modules (entry points) that have no incoming edges might be missing from the all_modules set if they are not explicitly inserted. The node should be inserted into all_modules regardless of whether parent is Some to ensure all nodes in the graph are captured.
References
- Ensure all relevant nodes in a graph traversal are captured, including roots.
| for &asset in output_assets.await? { | ||
| let filename = asset.path().to_string().owned().await?; | ||
| if filename.ends_with(".map") || filename.ends_with(".nft.json") { | ||
| continue; | ||
| } | ||
|
|
||
| let output_file_index = builder.add_output_file(AnalyzeOutputFile { filename }); | ||
| let chunk_parts = split_output_asset_into_parts(*asset).await?; | ||
| for chunk_part in chunk_parts { | ||
| let decoded_source = urlencoding::decode(&chunk_part.source)?; | ||
| let source = if let Some(stripped) = decoded_source.strip_prefix(&prefix) { | ||
| Cow::Borrowed(stripped) | ||
| } else { | ||
| Cow::Owned(format!( | ||
| "[project]/{}", | ||
| decoded_source.trim_start_matches("../") | ||
| )) | ||
| }; | ||
| let source_index = builder.ensure_source(&source).1; | ||
| let chunk_part_index = builder.add_chunk_part(AnalyzeChunkPart { | ||
| source_index, | ||
| output_file_index, | ||
| size: chunk_part.real_size + chunk_part.unaccounted_size, | ||
| compressed_size: chunk_part.get_compressed_size().await?, | ||
| }); | ||
| builder.add_chunk_part_to_output_file(output_file_index, chunk_part_index); | ||
| builder.add_chunk_part_to_source(source_index, chunk_part_index); | ||
| } | ||
| } |
There was a problem hiding this comment.
The processing of output assets and their chunk parts is currently sequential. Since get_compressed_size() can be a computationally expensive operation (involving compression), this can become a significant performance bottleneck for large applications. Consider using try_join or try_flat_join to process assets and their chunk parts in parallel before populating the builder.
📊 Performance Benchmark Report (with-antd)Utoopack Performance ReportReport ID: Executive Summary
Build Phase TimelineShows when each build phase is active and how much CPU it consumes.
Workload Distribution by Diagnostic Tier
Top 20 Tasks by Self-TimeSelf-time is the exclusive duration: time spent in the task itself, not in sub-tasks.
Critical Path AnalysisThe longest sequential dependency chains that determine wall-clock time.
Batching CandidatesHigh-volume tasks dominated by a single parent. If the parent can batch them,
Duration Distribution
Action Items
Report generated by Utoopack Performance Analysis Agent |
Summary
ANALYZE=1 up buildto enable turbopack's native analyzeTest Plan