turbopack_core/module_graph/style_groups_graph/
mod.rs1use std::{
60 sync::{
61 LazyLock,
62 atomic::{AtomicU64, Ordering},
63 },
64 time::{SystemTime, UNIX_EPOCH},
65};
66
67use anyhow::Result;
68use indexmap::map::Entry;
69use petgraph::graph::NodeIndex;
70use rustc_hash::FxHashSet;
71use tracing::{Instrument, instrument};
72use turbo_tasks::{FxIndexMap, FxIndexSet, ResolvedVc, TryJoinIterExt, Vc};
73
74use crate::{
75 chunk::{
76 ChunkItemBatchWithAsyncModuleInfo, ChunkItemWithAsyncModuleInfo, ChunkType,
77 ChunkableModule, ChunkingContext, chunk_item_batch::attach_async_info_to_chunkable_module,
78 },
79 module::{Module, StyleModule, StyleType},
80 module_graph::{
81 GraphTraversalAction, ModuleGraph,
82 module_batch::ModuleOrBatch,
83 module_batches::ModuleBatchesGraphEdge,
84 style_groups::{StyleGroups, StyleItemInfo, make_style_groups},
85 },
86};
87
88mod algorithm;
89mod subgraph_view;
90
91#[cfg(test)]
92mod tests;
93
94struct ModuleData {
96 style_type: StyleType,
97 size: u64,
99 chunk_item: ChunkItemWithAsyncModuleInfo,
100}
101
102struct StyleModuleRef {
106 chunkable: ResolvedVc<Box<dyn ChunkableModule>>,
107 style: ResolvedVc<Box<dyn StyleModule>>,
108}
109
110type ClassifiedModule = Option<(usize, ResolvedVc<Box<dyn StyleModule>>)>;
113
114#[instrument(skip(module_graph, chunking_context))]
117pub async fn compute_style_groups_graph(
118 module_graph: Vc<ModuleGraph>,
119 chunking_context: Vc<Box<dyn ChunkingContext>>,
120 request_cost: f32,
121 weight_distribution: f32,
122 max_chunk_size: u64,
123) -> Result<Vc<StyleGroups>> {
124 let (chunk_groups, modules_in_order) = collect_chunk_groups(module_graph, chunking_context)
127 .instrument(tracing::trace_span!("collect_chunk_groups"))
128 .await?;
129
130 if modules_in_order.is_empty() {
131 return Ok(make_style_groups(FxIndexMap::default()));
132 }
133
134 let module_data = resolve_module_data(module_graph, chunking_context, &modules_in_order)
136 .instrument(tracing::trace_span!("resolve_module_data"))
137 .await?;
138
139 let module_sizes: Vec<u64> = module_data.iter().map(|m| m.size).collect();
140 let module_style_types: Vec<StyleType> = module_data.iter().map(|m| m.style_type).collect();
141
142 let (mut graph, module_to_groups) = tracing::trace_span!("create_graph")
144 .in_scope(|| algorithm::create_graph(&chunk_groups, modules_in_order.len()));
145 tracing::trace_span!("make_acyclic").in_scope(|| algorithm::make_acyclic(&mut graph));
146 let global_order = tracing::trace_span!("linearize")
147 .in_scope(|| algorithm::linearize(&graph, &module_to_groups));
148 let chunks = tracing::trace_span!("split_into_chunks").in_scope(|| {
149 algorithm::split_into_chunks(
150 &global_order,
151 &chunk_groups,
152 &module_sizes,
153 &module_style_types,
154 request_cost,
155 weight_distribution,
156 max_chunk_size,
157 )
158 });
159
160 if *DEBUG_DUMP_ENABLED
163 && let Err(err) = write_debug_dump(
164 &chunk_groups,
165 &modules_in_order,
166 &module_data,
167 &global_order,
168 &chunks,
169 )
170 .instrument(tracing::trace_span!("debug_dump"))
171 .await
172 {
173 eprintln!("TURBOPACK_DEBUG_CSS_CHUNKING: failed to write debug dump: {err:?}");
174 }
175
176 assemble_style_groups(&chunks, &module_data)
179 .instrument(tracing::trace_span!("assemble"))
180 .await
181}
182
183static DEBUG_DUMP_ENABLED: LazyLock<bool> =
184 LazyLock::new(|| match std::env::var("TURBOPACK_DEBUG_CSS_CHUNKING") {
185 Ok(v) => !v.is_empty() && v != "0" && !v.eq_ignore_ascii_case("false"),
186 Err(_) => false,
187 });
188
189fn cost_to_json(cost: Option<f32>) -> serde_json::Value {
193 match cost {
194 None => serde_json::Value::Null,
195 Some(c) if c.is_finite() => serde_json::json!(c),
196 Some(c) if c == f32::NEG_INFINITY => serde_json::json!("-Infinity"),
197 Some(c) if c == f32::INFINITY => serde_json::json!("Infinity"),
198 Some(_) => serde_json::json!("NaN"),
199 }
200}
201
202async fn write_debug_dump(
206 chunk_groups: &[Vec<usize>],
207 modules: &[StyleModuleRef],
208 module_data: &[ModuleData],
209 global_order: &[NodeIndex],
210 chunks: &[(Vec<usize>, Option<f32>)],
211) -> Result<()> {
212 let ident_strings: Vec<String> = modules
215 .iter()
216 .map(async |m| -> Result<String> {
217 Ok(m.chunkable.ident_string().await?.as_str().to_owned())
218 })
219 .try_join()
220 .await?;
221
222 let ident = |id: usize| ident_strings[id].as_str();
223
224 let chunk_groups_json: Vec<Vec<&str>> = chunk_groups
225 .iter()
226 .map(|g| g.iter().map(|&id| ident(id)).collect())
227 .collect();
228
229 let mut chunks_json: Vec<serde_json::Value> = chunks
233 .iter()
234 .flat_map(|(chunk, merge_cost_to_next)| {
235 [
236 serde_json::json!(chunk.iter().map(|&id| ident(id)).collect::<Vec<_>>()),
237 serde_json::json!(cost_to_json(*merge_cost_to_next)),
238 ]
239 })
240 .collect();
241
242 chunks_json.pop();
244
245 let global_order_flat_json: Vec<&str> = global_order.iter().map(|n| ident(n.index())).collect();
246
247 let modules_json: Vec<serde_json::Value> = modules
248 .iter()
249 .enumerate()
250 .map(|(i, _)| {
251 serde_json::json!({
252 "ident": ident(i),
253 "size": module_data[i].size,
254 "style_type": match module_data[i].style_type {
255 StyleType::GlobalStyle => "GlobalStyle",
256 StyleType::IsolatedStyle => "IsolatedStyle",
257 },
258 })
259 })
260 .collect();
261
262 let dump = serde_json::json!({
263 "chunk_groups": chunk_groups_json,
264 "global_order": global_order_flat_json,
265 "chunks": chunks_json,
266 "modules": modules_json,
267 });
268
269 let now_ms = SystemTime::now()
270 .duration_since(UNIX_EPOCH)
271 .map(|d| d.as_millis())
272 .unwrap_or(0);
273 static COUNTER: AtomicU64 = AtomicU64::new(0);
274 let seq = COUNTER.fetch_add(1, Ordering::Relaxed);
275 let path =
276 std::env::current_dir()?.join(format!("turbopack-css-chunking-debug-{now_ms}-{seq}.json"));
277
278 let bytes = serde_json::to_vec_pretty(&dump)?;
279 std::fs::write(&path, &bytes)?;
280 eprintln!(
281 "TURBOPACK_DEBUG_CSS_CHUNKING: wrote {} bytes to {}",
282 bytes.len(),
283 path.display()
284 );
285 Ok(())
286}
287
288async fn assemble_style_groups(
289 chunks: &[(Vec<usize>, Option<f32>)],
290 module_data: &[ModuleData],
291) -> Result<Vc<StyleGroups>> {
292 let mut shared_chunk_items: FxIndexMap<ChunkItemWithAsyncModuleInfo, StyleItemInfo> =
293 FxIndexMap::default();
294 let mut order_counter: u32 = 0;
295 let mut push =
296 |map: &mut FxIndexMap<ChunkItemWithAsyncModuleInfo, StyleItemInfo>,
297 chunk_item: ChunkItemWithAsyncModuleInfo,
298 batch: Option<ResolvedVc<ChunkItemBatchWithAsyncModuleInfo>>| {
299 map.insert(
300 chunk_item,
301 StyleItemInfo {
302 order: Some(order_counter),
303 batch,
304 },
305 );
306 order_counter += 1;
307 };
308
309 for (chunk, _cost) in chunks {
310 if chunk.is_empty() {
311 continue;
312 }
313 if chunk.len() == 1 {
314 push(
315 &mut shared_chunk_items,
316 module_data[chunk[0]].chunk_item,
317 None,
318 );
319 continue;
320 }
321
322 let chunk_items: Vec<_> = chunk.iter().map(|&id| module_data[id].chunk_item).collect();
323 let batch = ChunkItemBatchWithAsyncModuleInfo::new(chunk_items.clone())
324 .to_resolved()
325 .await?;
326 for chunk_item in chunk_items {
327 push(&mut shared_chunk_items, chunk_item, Some(batch));
328 }
329 }
330
331 debug_assert!(
334 module_data
335 .iter()
336 .all(|data| shared_chunk_items.contains_key(&data.chunk_item)),
337 "linearize dropped a module: every module reached by the chunk-group walk must appear in \
338 the final chunk-item map",
339 );
340
341 Ok(make_style_groups(shared_chunk_items))
342}
343
344async fn collect_chunk_groups(
350 module_graph: Vc<ModuleGraph>,
351 chunking_context: Vc<Box<dyn ChunkingContext>>,
352) -> Result<(Vec<Vec<usize>>, Vec<StyleModuleRef>)> {
353 let chunk_group_info = module_graph.chunk_group_info().await?;
354 let batches_graph = module_graph
355 .module_batches(chunking_context.batching_config())
356 .await?;
357 let mut module_id_map: FxIndexMap<ResolvedVc<Box<dyn ChunkableModule>>, ClassifiedModule> =
362 FxIndexMap::default();
363 let mut next_css_id: usize = 0;
364 let mut chunk_groups: Vec<Vec<usize>> = Vec::new();
365
366 for (i, chunk_group) in chunk_group_info.chunk_groups.iter().enumerate() {
367 let ordered_entries = batches_graph.get_ordered_entries(&chunk_group_info, i);
368 let mut entries = Vec::with_capacity(chunk_group.entries_count());
369 for entry in ordered_entries {
370 entries.push(batches_graph.get_entry_index(entry).await?);
371 }
372 let mut visited = FxHashSet::default();
373 let mut items_in_postorder = FxIndexSet::default();
374 batches_graph.traverse_edges_from_entries_dfs(
375 entries.iter().copied(),
376 &mut (),
377 |parent_info, module, _| {
378 if let Some((_, ModuleBatchesGraphEdge { ty, .. })) = parent_info
379 && !ty.is_parallel()
380 {
381 return Ok(GraphTraversalAction::Exclude);
382 }
383 if visited.insert(module) {
384 Ok(GraphTraversalAction::Continue)
385 } else {
386 Ok(GraphTraversalAction::Exclude)
387 }
388 },
389 |parent_info, item, _| {
390 if let Some((_, ModuleBatchesGraphEdge { ty, .. })) = parent_info
391 && !ty.is_parallel()
392 {
393 return;
394 }
395 items_in_postorder.insert(*item);
396 },
397 )?;
398
399 let mut ids: Vec<usize> = Vec::new();
403 let mut seen: FxHashSet<usize> = FxHashSet::default();
404 let mut handle_module = async |module| -> Result<()> {
405 let id_slot = match module_id_map.entry(module) {
406 Entry::Occupied(e) => *e.get(),
407 Entry::Vacant(e) => {
408 let assigned =
409 ResolvedVc::try_sidecast::<Box<dyn StyleModule>>(module).map(|style| {
410 let id = next_css_id;
411 next_css_id += 1;
412 (id, style)
413 });
414 e.insert(assigned);
415 assigned
416 }
417 };
418 if let Some((id, _)) = id_slot
419 && seen.insert(id)
420 {
421 ids.push(id);
422 }
423 Ok(())
424 };
425
426 for item in items_in_postorder {
427 match item {
428 ModuleOrBatch::Batch(batch) => {
429 for &module in &batch.await?.modules {
430 handle_module(module).await?;
431 }
432 }
433 ModuleOrBatch::Module(module) => {
434 if let Some(chunkable_module) = ResolvedVc::try_downcast(module) {
435 handle_module(chunkable_module).await?;
436 }
437 }
438 ModuleOrBatch::None(_) => {}
439 }
440 }
441
442 if !ids.is_empty() {
443 chunk_groups.push(ids);
444 }
445 }
446
447 let modules_in_order: Vec<StyleModuleRef> = module_id_map
451 .iter()
452 .filter_map(|(&chunkable, slot)| slot.map(|(_, style)| StyleModuleRef { chunkable, style }))
453 .collect();
454 Ok((chunk_groups, modules_in_order))
455}
456
457async fn resolve_module_data(
460 module_graph: Vc<ModuleGraph>,
461 chunking_context: Vc<Box<dyn ChunkingContext>>,
462 modules: &[StyleModuleRef],
463) -> Result<Vec<ModuleData>> {
464 let async_module_info = module_graph.async_module_info();
465 modules
466 .iter()
467 .map(async |m| -> Result<ModuleData> {
468 let style_type = *m.style.style_type().await?;
469 let chunk_item = attach_async_info_to_chunkable_module(
470 m.chunkable,
471 async_module_info,
472 module_graph,
473 chunking_context,
474 )
475 .await?;
476 let size = *chunk_item
477 .chunk_type
478 .chunk_item_size(chunking_context, *chunk_item.chunk_item, None)
479 .await?;
480 Ok(ModuleData {
481 style_type,
482 size: size as u64,
483 chunk_item,
484 })
485 })
486 .try_join()
487 .await
488}