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