mirror of
https://github.com/hanzoai/next.js.git
synced 2026-09-24 17:00:51 +00:00
Turbopack: add TURBOPACK_DEBUG_CSS_CHUNKING env var (#95080)
### What?
Adds a `TURBOPACK_DEBUG_CSS_CHUNKING` environment variable. When set to
a truthy value, the graph-based CSS chunker (`experimental.cssChunking:
"graph"`) writes a JSON snapshot of its inputs and outputs to the
current working directory on each invocation.
### Why?
We are investigating problems with the new graph-based CSS chunking
algorithm. The pipeline (`create_graph` → `make_acyclic` → `linearize` →
`split_into_chunks`) is internal to `turbopack-core` and doesn't surface
enough information through normal build output to debug bad chunking
decisions post-hoc. A side-channel dump lets us reproduce and reason
about a problematic build without instrumenting Rust on a user's
machine.
### How?
In
`turbopack/crates/turbopack-core/src/module_graph/style_groups_graph/mod.rs`,
between `split_into_chunks` and the result assembly:
- Whether the dump is enabled is cached in a `static DEBUG_DUMP_ENABLED:
LazyLock<bool>` so the env var is read exactly once per process. Truthy
= anything other than unset, empty, `0`, or `false` (case-insensitive).
- When enabled, every call to `compute_style_groups_graph` resolves
`ident_string()` for every CSS module in parallel via `try_join` and
writes a pretty-printed JSON file
`turbopack-css-chunking-debug-<unix_ms>-<seq>.json` in the current
working directory. The timestamp + atomic counter suffix ensures
concurrent or repeated calls don't clobber each other.
- Failures while writing the dump are logged to stderr and otherwise
swallowed — a debug toggle must never fail a build.
The JSON document contains:
- `chunk_groups`: `string[][]` — module idents per chunk group, in the
order the algorithm sees them.
- `global_order`: `string[]` — the flat global order produced by
`linearize`.
- `global_order_chunks`: `string[][]` — the same modules grouped by the
merged segments produced by `split_into_chunks` (e.g. `[["a","b"],
["c"], ["d","e","f"]]`).
- `modules`: `[{ ident, size, style_type }]` — per-module metadata where
`size` is the chunk item size in bytes (the same value used by the cost
model) and `style_type` is `"GlobalStyle"` or `"IsolatedStyle"`.
Verification: `cargo clippy -p turbopack-core --no-deps` is clean and
the existing `style_groups_graph` test suite passes (53 tests).
No user-facing config change, no docs change — the env var is
intentionally undocumented and only meant for triage.
Closes NEXT-
Fixes #
---------
Co-authored-by: v-work-app[bot] <262237222+v-work-app[bot]@users.noreply.github.com>
Co-authored-by: Claude <[email protected]>
Co-authored-by: Tobias Koppers <[email protected]>
This commit is contained in:
co-authored by
v-work-app[bot] <262237222+v-work-app[bot]@users.noreply.github.com>
Claude
Tobias Koppers
parent
b0d33e9afb
commit
16db9d0544
@@ -538,6 +538,13 @@ impl PartialOrd for OrdF32 {
|
||||
/// * `module_factor_cost` — see module-level docs.
|
||||
/// * `max_chunk_size` — bytes; merges that produce a multi-item chunk above this are forbidden
|
||||
/// (`+infinity`). `0` disables the cap.
|
||||
///
|
||||
/// Returns one `(chunk, merge_cost_to_next)` tuple per chunk, in global order. `merge_cost_to_next`
|
||||
/// is the surviving split's metric — the cost-delta of merging this chunk with the following one
|
||||
/// (`cost(merged) - cost(this) - cost(next)`, always `>= 0` since every negative metric was merged
|
||||
/// away). It is `None` for the last chunk, which has no following neighbour. These metrics are
|
||||
/// surfaced by the `TURBOPACK_DEBUG_CSS_CHUNKING` dump so a boundary that was close to merging can
|
||||
/// be told apart from one held back by a hard constraint (`+infinity`).
|
||||
pub(super) fn split_into_chunks(
|
||||
global_order: &[NodeIndex],
|
||||
chunk_groups: &[Vec<usize>],
|
||||
@@ -546,7 +553,7 @@ pub(super) fn split_into_chunks(
|
||||
request_cost: f32,
|
||||
module_factor_cost: f32,
|
||||
max_chunk_size: u64,
|
||||
) -> Vec<Vec<usize>> {
|
||||
) -> Vec<(Vec<usize>, Option<f32>)> {
|
||||
if global_order.is_empty() {
|
||||
return Vec::new();
|
||||
}
|
||||
@@ -641,12 +648,14 @@ pub(super) fn split_into_chunks(
|
||||
}
|
||||
|
||||
// Materialize chunks by walking `order` and starting a new chunk on each true split point.
|
||||
let mut result: Vec<Vec<usize>> = vec![vec![order[0]]];
|
||||
// When a split closes the current chunk, record its metric as that chunk's cost-to-next.
|
||||
let mut result: Vec<(Vec<usize>, Option<f32>)> = vec![(vec![order[0]], None)];
|
||||
for i in 1..n {
|
||||
if split_points[i - 1] {
|
||||
result.push(vec![order[i]]);
|
||||
result.last_mut().unwrap().1 = metrics[i - 1];
|
||||
result.push((vec![order[i]], None));
|
||||
} else {
|
||||
result.last_mut().unwrap().push(order[i]);
|
||||
result.last_mut().unwrap().0.push(order[i]);
|
||||
}
|
||||
}
|
||||
result
|
||||
|
||||
@@ -54,8 +54,17 @@
|
||||
//! that would put a global item into a chunk loaded by a chunk group not currently loading that
|
||||
//! item is treated as `+infinity` cost.
|
||||
|
||||
use std::{
|
||||
sync::{
|
||||
LazyLock,
|
||||
atomic::{AtomicU64, Ordering},
|
||||
},
|
||||
time::{SystemTime, UNIX_EPOCH},
|
||||
};
|
||||
|
||||
use anyhow::Result;
|
||||
use indexmap::map::Entry;
|
||||
use petgraph::graph::NodeIndex;
|
||||
use rustc_hash::FxHashSet;
|
||||
use tracing::{Instrument, instrument};
|
||||
use turbo_tasks::{FxIndexMap, FxIndexSet, ResolvedVc, TryJoinIterExt, Vc};
|
||||
@@ -65,7 +74,7 @@ use crate::{
|
||||
ChunkItemBatchWithAsyncModuleInfo, ChunkItemWithAsyncModuleInfo, ChunkType,
|
||||
ChunkableModule, ChunkingContext, chunk_item_batch::attach_async_info_to_chunkable_module,
|
||||
},
|
||||
module::{StyleModule, StyleType},
|
||||
module::{Module, StyleModule, StyleType},
|
||||
module_graph::{
|
||||
GraphTraversalAction, ModuleGraph,
|
||||
module_batch::ModuleOrBatch,
|
||||
@@ -145,6 +154,22 @@ pub async fn compute_style_groups_graph(
|
||||
)
|
||||
});
|
||||
|
||||
// Optional debug dump controlled by `TURBOPACK_DEBUG_CSS_CHUNKING`. Failures here are
|
||||
// logged and otherwise swallowed so a debug toggle never breaks the build.
|
||||
if *DEBUG_DUMP_ENABLED
|
||||
&& let Err(err) = write_debug_dump(
|
||||
&chunk_groups,
|
||||
&modules_in_order,
|
||||
&module_data,
|
||||
&global_order,
|
||||
&chunks,
|
||||
)
|
||||
.instrument(tracing::trace_span!("debug_dump"))
|
||||
.await
|
||||
{
|
||||
eprintln!("TURBOPACK_DEBUG_CSS_CHUNKING: failed to write debug dump: {err:?}");
|
||||
}
|
||||
|
||||
// 4. Assemble the result. Each multi-item chunk becomes a `ChunkItemBatch`; singletons get a
|
||||
// `batch = None` entry so the production sort still places them at the right `order`.
|
||||
assemble_style_groups(&chunks, &module_data)
|
||||
@@ -152,8 +177,113 @@ pub async fn compute_style_groups_graph(
|
||||
.await
|
||||
}
|
||||
|
||||
static DEBUG_DUMP_ENABLED: LazyLock<bool> =
|
||||
LazyLock::new(|| match std::env::var("TURBOPACK_DEBUG_CSS_CHUNKING") {
|
||||
Ok(v) => !v.is_empty() && v != "0" && !v.eq_ignore_ascii_case("false"),
|
||||
Err(_) => false,
|
||||
});
|
||||
|
||||
/// Serialize a split cost metric for the debug dump. `serde_json` renders non-finite floats as
|
||||
/// `null`, which would be indistinguishable from the genuine `None` of the last chunk, so the
|
||||
/// non-finite cases are emitted as strings instead.
|
||||
fn cost_to_json(cost: Option<f32>) -> serde_json::Value {
|
||||
match cost {
|
||||
None => serde_json::Value::Null,
|
||||
Some(c) if c.is_finite() => serde_json::json!(c),
|
||||
Some(c) if c == f32::NEG_INFINITY => serde_json::json!("-Infinity"),
|
||||
Some(c) if c == f32::INFINITY => serde_json::json!("Infinity"),
|
||||
Some(_) => serde_json::json!("NaN"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Write a JSON snapshot of the inputs and outputs of the graph-based CSS chunker to the
|
||||
/// current working directory. Each invocation produces a uniquely named file so concurrent or
|
||||
/// repeated computations don't overwrite each other.
|
||||
async fn write_debug_dump(
|
||||
chunk_groups: &[Vec<usize>],
|
||||
modules: &[StyleModuleRef],
|
||||
module_data: &[ModuleData],
|
||||
global_order: &[NodeIndex],
|
||||
chunks: &[(Vec<usize>, Option<f32>)],
|
||||
) -> Result<()> {
|
||||
// Resolve `ident_string()` for every module up front. Done in parallel to keep this off the
|
||||
// critical path even on graphs with thousands of CSS modules.
|
||||
let ident_strings: Vec<String> = modules
|
||||
.iter()
|
||||
.map(async |m| -> Result<String> {
|
||||
Ok(m.chunkable.ident_string().await?.as_str().to_owned())
|
||||
})
|
||||
.try_join()
|
||||
.await?;
|
||||
|
||||
let ident = |id: usize| ident_strings[id].as_str();
|
||||
|
||||
let chunk_groups_json: Vec<Vec<&str>> = chunk_groups
|
||||
.iter()
|
||||
.map(|g| g.iter().map(|&id| ident(id)).collect())
|
||||
.collect();
|
||||
|
||||
// Each chunk carries the cost-delta of merging it with the following chunk (`None`/`null` for
|
||||
// the last chunk). A finite value is the surviving split metric; `"Infinity"` marks a boundary
|
||||
// a hard constraint forbade merging across.
|
||||
let mut chunks_json: Vec<serde_json::Value> = chunks
|
||||
.iter()
|
||||
.flat_map(|(chunk, merge_cost_to_next)| {
|
||||
[
|
||||
serde_json::json!(chunk.iter().map(|&id| ident(id)).collect::<Vec<_>>()),
|
||||
serde_json::json!(cost_to_json(*merge_cost_to_next)),
|
||||
]
|
||||
})
|
||||
.collect();
|
||||
|
||||
// last chunk has no merge cost
|
||||
chunks_json.pop();
|
||||
|
||||
let global_order_flat_json: Vec<&str> = global_order.iter().map(|n| ident(n.index())).collect();
|
||||
|
||||
let modules_json: Vec<serde_json::Value> = modules
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, _)| {
|
||||
serde_json::json!({
|
||||
"ident": ident(i),
|
||||
"size": module_data[i].size,
|
||||
"style_type": match module_data[i].style_type {
|
||||
StyleType::GlobalStyle => "GlobalStyle",
|
||||
StyleType::IsolatedStyle => "IsolatedStyle",
|
||||
},
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
let dump = serde_json::json!({
|
||||
"chunk_groups": chunk_groups_json,
|
||||
"global_order": global_order_flat_json,
|
||||
"chunks": chunks_json,
|
||||
"modules": modules_json,
|
||||
});
|
||||
|
||||
let now_ms = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.map(|d| d.as_millis())
|
||||
.unwrap_or(0);
|
||||
static COUNTER: AtomicU64 = AtomicU64::new(0);
|
||||
let seq = COUNTER.fetch_add(1, Ordering::Relaxed);
|
||||
let path =
|
||||
std::env::current_dir()?.join(format!("turbopack-css-chunking-debug-{now_ms}-{seq}.json"));
|
||||
|
||||
let bytes = serde_json::to_vec_pretty(&dump)?;
|
||||
std::fs::write(&path, &bytes)?;
|
||||
eprintln!(
|
||||
"TURBOPACK_DEBUG_CSS_CHUNKING: wrote {} bytes to {}",
|
||||
bytes.len(),
|
||||
path.display()
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn assemble_style_groups(
|
||||
chunks: &[Vec<usize>],
|
||||
chunks: &[(Vec<usize>, Option<f32>)],
|
||||
module_data: &[ModuleData],
|
||||
) -> Result<Vc<StyleGroups>> {
|
||||
let mut shared_chunk_items: FxIndexMap<ChunkItemWithAsyncModuleInfo, StyleItemInfo> =
|
||||
@@ -173,7 +303,7 @@ async fn assemble_style_groups(
|
||||
order_counter += 1;
|
||||
};
|
||||
|
||||
for chunk in chunks {
|
||||
for (chunk, _cost) in chunks {
|
||||
if chunk.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -95,6 +95,8 @@ fn split_simple(
|
||||
request_cost: f32,
|
||||
) -> Vec<Vec<usize>> {
|
||||
let count = global_order.len();
|
||||
// The cost-to-next metric on each tuple is exercised by the production dump, not these
|
||||
// structural assertions, so drop it and keep the bare chunk lists.
|
||||
split_into_chunks(
|
||||
global_order,
|
||||
chunk_groups,
|
||||
@@ -105,6 +107,9 @@ fn split_simple(
|
||||
0.0,
|
||||
0,
|
||||
)
|
||||
.into_iter()
|
||||
.map(|(chunk, _cost)| chunk)
|
||||
.collect()
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user