turbopack_core/module_graph/style_groups_loose/
mod.rs1use std::cmp::Reverse;
2
3use anyhow::Result;
4use indexmap::map::Entry;
5use rustc_hash::{FxHashMap, FxHashSet};
6use turbo_rcstr::RcStr;
7use turbo_tasks::{FxIndexMap, FxIndexSet, ResolvedVc, TryJoinIterExt, ValueToString, Vc};
8
9use crate::{
10 chunk::{
11 ChunkItemBatchWithAsyncModuleInfo, ChunkItemWithAsyncModuleInfo, ChunkType,
12 ChunkableModule, ChunkingContext, chunk_item_batch::attach_async_info_to_chunkable_module,
13 },
14 module::{Module, StyleModule, StyleType},
15 module_graph::{
16 GraphTraversalAction, ModuleGraph,
17 module_batch::ModuleOrBatch,
18 module_batches::ModuleBatchesGraphEdge,
19 style_groups::{StyleGroups, StyleGroupsConfig, StyleItemInfo},
20 },
21};
22
23#[derive(Debug)]
24struct ModuleInfo {
25 style_type: StyleType,
26 ident: RcStr,
27 chunk_group_indices: FxHashMap<usize, usize>,
28 index_sum: usize,
29 size: usize,
30 chunk_item: Option<ChunkItemWithAsyncModuleInfo>,
31}
32
33impl ModuleInfo {
34 fn new(style_type: StyleType, ident: RcStr) -> Self {
35 Self {
36 style_type,
37 ident,
38 chunk_group_indices: Default::default(),
39 index_sum: 0,
40 size: 0,
41 chunk_item: None,
42 }
43 }
44}
45
46struct ChunkGroupState {
47 styles: FxIndexSet<ResolvedVc<Box<dyn ChunkableModule>>>,
48 requests: usize,
51}
52
53struct StyleCollection {
56 module_info_map: FxIndexMap<ResolvedVc<Box<dyn ChunkableModule>>, Option<ModuleInfo>>,
60 chunk_group_state: Vec<ChunkGroupState>,
63}
64
65async fn collect_style_modules_per_chunk_group(
69 module_graph: Vc<ModuleGraph>,
70 chunking_context: Vc<Box<dyn ChunkingContext>>,
71) -> Result<StyleCollection> {
72 let chunk_group_info = module_graph.chunk_group_info().await?;
73 let batches_graph = module_graph
74 .module_batches(chunking_context.batching_config())
75 .await?;
76 let async_module_info = module_graph.async_module_info();
77 let mut module_info_map: FxIndexMap<ResolvedVc<Box<dyn ChunkableModule>>, Option<ModuleInfo>> =
78 FxIndexMap::default();
79
80 let mut chunk_group_state: Vec<ChunkGroupState> = Vec::new();
82 let mut idx = 0;
83 for (i, chunk_group) in chunk_group_info.chunk_groups.iter().enumerate() {
84 let ordered_entries = batches_graph.get_ordered_entries(&chunk_group_info, i);
85 let mut entries = Vec::with_capacity(chunk_group.entries_count());
86 for entry in ordered_entries {
87 entries.push(batches_graph.get_entry_index(entry).await?);
88 }
89 let mut visited = FxHashSet::default();
90 let mut items_in_postorder = FxIndexSet::default();
91 batches_graph.traverse_edges_from_entries_dfs(
92 entries.iter().copied(),
93 &mut (),
94 |parent_info, module, _| {
95 if let Some((_, ModuleBatchesGraphEdge { ty, .. })) = parent_info
96 && !ty.is_parallel()
97 {
98 return Ok(GraphTraversalAction::Exclude);
99 }
100 if visited.insert(module) {
101 Ok(GraphTraversalAction::Continue)
102 } else {
103 Ok(GraphTraversalAction::Exclude)
104 }
105 },
106 |parent_info, item, _| {
107 if let Some((_, ModuleBatchesGraphEdge { ty, .. })) = parent_info
108 && !ty.is_parallel()
109 {
110 return;
111 }
112 items_in_postorder.insert(*item);
113 },
114 )?;
115
116 let mut styles = FxIndexSet::default();
117 let mut handle_module = async |module| {
118 match module_info_map.entry(module) {
119 Entry::Occupied(mut e) => {
120 if let Some(info) = e.get_mut() {
121 info.chunk_group_indices.insert(idx, styles.len());
122 info.index_sum += styles.len();
123 styles.insert(module);
124 }
125 }
126 Entry::Vacant(e) => {
127 if let Some(style_module) =
128 ResolvedVc::try_sidecast::<Box<dyn StyleModule>>(module)
129 {
130 let style_type = *style_module.style_type().await?;
131 let mut info =
132 ModuleInfo::new(style_type, module.ident().to_string().owned().await?);
133 info.chunk_group_indices.insert(idx, styles.len());
134 info.index_sum += styles.len();
135 styles.insert(module);
136 e.insert(Some(info));
137 } else {
138 e.insert(None);
139 }
140 }
141 }
142 anyhow::Ok(())
143 };
144
145 for item in items_in_postorder {
146 match item {
147 ModuleOrBatch::Batch(batch) => {
148 for &module in &batch.await?.modules {
149 handle_module(module).await?;
150 }
151 }
152 ModuleOrBatch::Module(module) => {
153 if let Some(chunkable_module) = ResolvedVc::try_downcast(module) {
154 handle_module(chunkable_module).await?;
155 }
156 }
157 ModuleOrBatch::None(_) => {}
158 }
159 }
160
161 if !styles.is_empty() {
162 chunk_group_state.push(ChunkGroupState {
163 requests: styles.len(),
164 styles,
165 });
166 idx += 1;
167 }
168 }
169
170 module_info_map.retain(|_, info| info.is_some());
171
172 module_info_map.sort_by(|_, a, _, b| {
173 let a = a.as_ref().unwrap();
174 let b = b.as_ref().unwrap();
175 a.index_sum
176 .cmp(&b.index_sum)
177 .then_with(|| a.ident.cmp(&b.ident))
178 });
179
180 let chunk_item_and_sizes = module_info_map
182 .keys()
183 .map(async |&module| {
184 let chunk_item = attach_async_info_to_chunkable_module(
185 module,
186 async_module_info,
187 module_graph,
188 chunking_context,
189 )
190 .await?;
191 let size = *chunk_item
192 .chunk_type
193 .chunk_item_size(chunking_context, *chunk_item.chunk_item, None)
194 .await?;
195 Ok((chunk_item, size))
196 })
197 .try_join()
198 .await?;
199 module_info_map
200 .iter_mut()
201 .zip(chunk_item_and_sizes)
202 .for_each(|((_, info), (chunk_item, size))| {
203 let info = info.as_mut().unwrap();
204 info.size = size;
205 info.chunk_item = Some(chunk_item);
206 });
207
208 Ok(StyleCollection {
209 module_info_map,
210 chunk_group_state,
211 })
212}
213
214pub async fn compute_style_groups(
215 module_graph: Vc<ModuleGraph>,
216 chunking_context: Vc<Box<dyn ChunkingContext>>,
217 config: &StyleGroupsConfig,
218) -> Result<Vc<StyleGroups>> {
219 let StyleCollection {
220 module_info_map,
221 mut chunk_group_state,
222 } = collect_style_modules_per_chunk_group(module_graph, chunking_context).await?;
223
224 let mut module_dependents: FxHashMap<_, Vec<_>> = FxHashMap::default();
226 for (&module, info) in &module_info_map {
227 let info = info.as_ref().unwrap();
228 let (&idx, &start_pos) = info
230 .chunk_group_indices
231 .iter()
232 .min_by_key(|&(&idx, _)| chunk_group_state[idx].styles.len())
233 .unwrap();
234 let potential_dependents = &chunk_group_state[idx].styles[start_pos + 1..];
235
236 let dependents = potential_dependents
237 .iter()
238 .copied()
239 .filter(|dependent| {
240 let dependent_info = module_info_map.get(dependent).unwrap();
241 let dependent_info = dependent_info.as_ref().unwrap();
242
243 info.chunk_group_indices.len() >= dependent_info.chunk_group_indices.len()
246 && dependent_info
247 .chunk_group_indices
248 .iter()
249 .all(|(idx, &dependent_pos)| {
250 info.chunk_group_indices
251 .get(idx)
252 .is_some_and(|&module_pos| module_pos < dependent_pos)
253 })
254 })
255 .collect::<Vec<_>>();
256
257 if !dependents.is_empty() {
258 module_dependents.insert(module, dependents);
259 }
260 }
261
262 let mut ordered_modules_with_state = module_info_map
263 .keys()
264 .copied()
265 .map(|m| (m, false))
266 .collect::<FxIndexMap<_, _>>();
267
268 let mut shared_chunk_items = FxIndexMap::default();
269 for i in 0..ordered_modules_with_state.len() {
270 let (&module, processed) = ordered_modules_with_state.get_index_mut(i).unwrap();
271 if *processed {
272 continue;
273 }
274 *processed = true;
275
276 let info = module_info_map.get(&module).unwrap().as_ref().unwrap();
277 let mut global_mode = info.style_type == StyleType::GlobalStyle;
278
279 let mut all_chunk_states = info.chunk_group_indices.clone();
281
282 let mut new_chunk_modules = [module].into_iter().collect::<FxHashSet<_>>();
284 let mut new_chunk_items = vec![info.chunk_item.unwrap()];
285
286 let mut current_size = info.size;
288
289 let mut potential_next_modules = all_chunk_states
292 .iter()
293 .filter_map(|(&idx, pos)| {
294 let following_styles = &chunk_group_state[idx].styles[pos + 1..];
295 let i = following_styles
296 .iter()
297 .position(|m| !*ordered_modules_with_state.get(m).unwrap());
298 i.map(|i| following_styles[i])
299 })
300 .collect::<FxHashSet<_>>();
301
302 'outer: loop {
304 let mut ordered_potential_next_modules = potential_next_modules
307 .iter()
308 .copied()
309 .map(|module| {
310 let info = module_info_map.get(&module).unwrap().as_ref().unwrap();
311 let requests = info
312 .chunk_group_indices
313 .keys()
314 .filter(|idx| all_chunk_states.contains_key(idx))
315 .map(|&idx| chunk_group_state[idx].requests)
316 .max()
317 .unwrap();
318 (module, info, requests)
319 })
320 .collect::<Vec<_>>();
321 ordered_potential_next_modules
322 .sort_by_key(|(_, info, requests)| (Reverse(*requests), &info.ident));
323
324 for (module, info, _) in ordered_potential_next_modules {
326 if current_size + info.size > config.max_chunk_size {
327 continue;
329 }
330 if let Some(dependents) = module_dependents.get(&module)
332 && dependents.iter().any(|m| new_chunk_modules.contains(m))
333 {
334 continue;
337 }
338
339 let is_global = info.style_type == StyleType::GlobalStyle;
341 if is_global
342 && global_mode
343 && all_chunk_states.len() != info.chunk_group_indices.len()
344 {
345 continue;
347 }
348 if global_mode
349 && info
350 .chunk_group_indices
351 .keys()
352 .any(|idx| !all_chunk_states.contains_key(idx))
353 {
354 continue;
356 }
357 if is_global
358 && all_chunk_states
359 .keys()
360 .any(|idx| !info.chunk_group_indices.contains_key(idx))
361 {
362 continue;
364 }
365 potential_next_modules.remove(&module);
366 current_size += info.size;
367 if is_global {
368 global_mode = true;
369 }
370 for &idx in info.chunk_group_indices.keys() {
371 if all_chunk_states.contains_key(&idx) {
372 chunk_group_state[idx].requests -= 1;
374 }
375 let pos = chunk_group_state[idx].styles.get_index_of(&module).unwrap();
376 all_chunk_states.insert(idx, pos);
377 let following_styles = &chunk_group_state[idx].styles[pos + 1..];
378 if let Some(i) = following_styles.iter().position(|m| {
379 !*ordered_modules_with_state.get(m).unwrap()
380 && !new_chunk_modules.contains(m)
381 }) {
382 let module = following_styles[i];
383 potential_next_modules.insert(module);
384 }
385 }
386
387 new_chunk_items.push(info.chunk_item.unwrap());
388 new_chunk_modules.insert(module);
389 *ordered_modules_with_state.get_mut(&module).unwrap() = true;
390 continue 'outer;
391 }
392 break;
393 }
394
395 if new_chunk_items.len() > 1 {
396 let style_group = ChunkItemBatchWithAsyncModuleInfo::new(new_chunk_items.clone())
397 .to_resolved()
398 .await?;
399 for chunk_item in new_chunk_items {
400 shared_chunk_items.insert(
401 chunk_item,
402 StyleItemInfo {
403 order: None,
404 batch: Some(style_group),
405 },
406 );
407 }
408 }
409 }
410
411 Ok(StyleGroups { shared_chunk_items }.cell())
412}