turbopack_core/module_graph/style_groups_graph/
mod.rs1use anyhow::Result;
58use indexmap::map::Entry;
59use rustc_hash::FxHashSet;
60use tracing::{Instrument, instrument};
61use turbo_tasks::{FxIndexMap, FxIndexSet, ResolvedVc, TryJoinIterExt, Vc};
62
63use crate::{
64 chunk::{
65 ChunkItemBatchWithAsyncModuleInfo, ChunkItemWithAsyncModuleInfo, ChunkType,
66 ChunkableModule, ChunkingContext, chunk_item_batch::attach_async_info_to_chunkable_module,
67 },
68 module::{StyleModule, StyleType},
69 module_graph::{
70 GraphTraversalAction, ModuleGraph,
71 module_batch::ModuleOrBatch,
72 module_batches::ModuleBatchesGraphEdge,
73 style_groups::{StyleGroups, StyleItemInfo, make_style_groups},
74 },
75};
76
77mod algorithm;
78mod subgraph_view;
79
80#[cfg(test)]
81mod tests;
82
83struct ModuleData {
85 style_type: StyleType,
86 size: u64,
88 chunk_item: ChunkItemWithAsyncModuleInfo,
89}
90
91struct StyleModuleRef {
95 chunkable: ResolvedVc<Box<dyn ChunkableModule>>,
96 style: ResolvedVc<Box<dyn StyleModule>>,
97}
98
99type ClassifiedModule = Option<(usize, ResolvedVc<Box<dyn StyleModule>>)>;
102
103#[instrument(skip(module_graph, chunking_context))]
106pub async fn compute_style_groups_graph(
107 module_graph: Vc<ModuleGraph>,
108 chunking_context: Vc<Box<dyn ChunkingContext>>,
109 request_cost: f32,
110 module_factor_cost: f32,
111 max_chunk_size: u64,
112) -> Result<Vc<StyleGroups>> {
113 let (chunk_groups, modules_in_order) = collect_chunk_groups(module_graph, chunking_context)
116 .instrument(tracing::trace_span!("collect_chunk_groups"))
117 .await?;
118
119 if modules_in_order.is_empty() {
120 return Ok(make_style_groups(FxIndexMap::default()));
121 }
122
123 let module_data = resolve_module_data(module_graph, chunking_context, &modules_in_order)
125 .instrument(tracing::trace_span!("resolve_module_data"))
126 .await?;
127
128 let module_sizes: Vec<u64> = module_data.iter().map(|m| m.size).collect();
129 let module_style_types: Vec<StyleType> = module_data.iter().map(|m| m.style_type).collect();
130
131 let mut graph = tracing::trace_span!("create_graph")
133 .in_scope(|| algorithm::create_graph(&chunk_groups, modules_in_order.len()));
134 tracing::trace_span!("make_acyclic").in_scope(|| algorithm::make_acyclic(&mut graph));
135 let global_order = tracing::trace_span!("linearize").in_scope(|| algorithm::linearize(&graph));
136 let chunks = tracing::trace_span!("split_into_chunks").in_scope(|| {
137 algorithm::split_into_chunks(
138 &global_order,
139 &chunk_groups,
140 &module_sizes,
141 &module_style_types,
142 request_cost,
143 module_factor_cost,
144 max_chunk_size,
145 )
146 });
147
148 assemble_style_groups(&chunks, &module_data)
151 .instrument(tracing::trace_span!("assemble"))
152 .await
153}
154
155async fn assemble_style_groups(
156 chunks: &[Vec<usize>],
157 module_data: &[ModuleData],
158) -> Result<Vc<StyleGroups>> {
159 let mut shared_chunk_items: FxIndexMap<ChunkItemWithAsyncModuleInfo, StyleItemInfo> =
160 FxIndexMap::default();
161 let mut order_counter: u32 = 0;
162 let mut push =
163 |map: &mut FxIndexMap<ChunkItemWithAsyncModuleInfo, StyleItemInfo>,
164 chunk_item: ChunkItemWithAsyncModuleInfo,
165 batch: Option<ResolvedVc<ChunkItemBatchWithAsyncModuleInfo>>| {
166 map.insert(
167 chunk_item,
168 StyleItemInfo {
169 order: Some(order_counter),
170 batch,
171 },
172 );
173 order_counter += 1;
174 };
175
176 for chunk in chunks {
177 if chunk.is_empty() {
178 continue;
179 }
180 if chunk.len() == 1 {
181 push(
182 &mut shared_chunk_items,
183 module_data[chunk[0]].chunk_item,
184 None,
185 );
186 continue;
187 }
188
189 let chunk_items: Vec<_> = chunk.iter().map(|&id| module_data[id].chunk_item).collect();
190 let batch = ChunkItemBatchWithAsyncModuleInfo::new(chunk_items.clone())
191 .to_resolved()
192 .await?;
193 for chunk_item in chunk_items {
194 push(&mut shared_chunk_items, chunk_item, Some(batch));
195 }
196 }
197
198 debug_assert!(
201 module_data
202 .iter()
203 .all(|data| shared_chunk_items.contains_key(&data.chunk_item)),
204 "linearize dropped a module: every module reached by the chunk-group walk must appear in \
205 the final chunk-item map",
206 );
207
208 Ok(make_style_groups(shared_chunk_items))
209}
210
211async fn collect_chunk_groups(
217 module_graph: Vc<ModuleGraph>,
218 chunking_context: Vc<Box<dyn ChunkingContext>>,
219) -> Result<(Vec<Vec<usize>>, Vec<StyleModuleRef>)> {
220 let chunk_group_info = module_graph.chunk_group_info().await?;
221 let batches_graph = module_graph
222 .module_batches(chunking_context.batching_config())
223 .await?;
224 let mut module_id_map: FxIndexMap<ResolvedVc<Box<dyn ChunkableModule>>, ClassifiedModule> =
229 FxIndexMap::default();
230 let mut next_css_id: usize = 0;
231 let mut chunk_groups: Vec<Vec<usize>> = Vec::new();
232
233 for (i, chunk_group) in chunk_group_info.chunk_groups.iter().enumerate() {
234 let ordered_entries = batches_graph.get_ordered_entries(&chunk_group_info, i);
235 let mut entries = Vec::with_capacity(chunk_group.entries_count());
236 for entry in ordered_entries {
237 entries.push(batches_graph.get_entry_index(entry).await?);
238 }
239 let mut visited = FxHashSet::default();
240 let mut items_in_postorder = FxIndexSet::default();
241 batches_graph.traverse_edges_from_entries_dfs(
242 entries.iter().copied(),
243 &mut (),
244 |parent_info, module, _| {
245 if let Some((_, ModuleBatchesGraphEdge { ty, .. })) = parent_info
246 && !ty.is_parallel()
247 {
248 return Ok(GraphTraversalAction::Exclude);
249 }
250 if visited.insert(module) {
251 Ok(GraphTraversalAction::Continue)
252 } else {
253 Ok(GraphTraversalAction::Exclude)
254 }
255 },
256 |parent_info, item, _| {
257 if let Some((_, ModuleBatchesGraphEdge { ty, .. })) = parent_info
258 && !ty.is_parallel()
259 {
260 return;
261 }
262 items_in_postorder.insert(*item);
263 },
264 )?;
265
266 let mut ids: Vec<usize> = Vec::new();
270 let mut seen: FxHashSet<usize> = FxHashSet::default();
271 let mut handle_module = async |module| -> Result<()> {
272 let id_slot = match module_id_map.entry(module) {
273 Entry::Occupied(e) => *e.get(),
274 Entry::Vacant(e) => {
275 let assigned =
276 ResolvedVc::try_sidecast::<Box<dyn StyleModule>>(module).map(|style| {
277 let id = next_css_id;
278 next_css_id += 1;
279 (id, style)
280 });
281 e.insert(assigned);
282 assigned
283 }
284 };
285 if let Some((id, _)) = id_slot
286 && seen.insert(id)
287 {
288 ids.push(id);
289 }
290 Ok(())
291 };
292
293 for item in items_in_postorder {
294 match item {
295 ModuleOrBatch::Batch(batch) => {
296 for &module in &batch.await?.modules {
297 handle_module(module).await?;
298 }
299 }
300 ModuleOrBatch::Module(module) => {
301 if let Some(chunkable_module) = ResolvedVc::try_downcast(module) {
302 handle_module(chunkable_module).await?;
303 }
304 }
305 ModuleOrBatch::None(_) => {}
306 }
307 }
308
309 if !ids.is_empty() {
310 chunk_groups.push(ids);
311 }
312 }
313
314 let modules_in_order: Vec<StyleModuleRef> = module_id_map
318 .iter()
319 .filter_map(|(&chunkable, slot)| slot.map(|(_, style)| StyleModuleRef { chunkable, style }))
320 .collect();
321 Ok((chunk_groups, modules_in_order))
322}
323
324async fn resolve_module_data(
327 module_graph: Vc<ModuleGraph>,
328 chunking_context: Vc<Box<dyn ChunkingContext>>,
329 modules: &[StyleModuleRef],
330) -> Result<Vec<ModuleData>> {
331 let async_module_info = module_graph.async_module_info();
332 modules
333 .iter()
334 .map(async |m| -> Result<ModuleData> {
335 let style_type = *m.style.style_type().await?;
336 let chunk_item = attach_async_info_to_chunkable_module(
337 m.chunkable,
338 async_module_info,
339 module_graph,
340 chunking_context,
341 )
342 .await?;
343 let size = *chunk_item
344 .chunk_type
345 .chunk_item_size(chunking_context, *chunk_item.chunk_item, None)
346 .await?;
347 Ok(ModuleData {
348 style_type,
349 size: size as u64,
350 chunk_item,
351 })
352 })
353 .try_join()
354 .await
355}