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 None,
95 &mut (),
96 |parent_info, module, _| {
97 if let Some((_, ModuleBatchesGraphEdge { ty, .. })) = parent_info
98 && !ty.is_parallel()
99 {
100 return Ok(GraphTraversalAction::Exclude);
101 }
102 if visited.insert(module) {
103 Ok(GraphTraversalAction::Continue)
104 } else {
105 Ok(GraphTraversalAction::Exclude)
106 }
107 },
108 |parent_info, item, _| {
109 if let Some((_, ModuleBatchesGraphEdge { ty, .. })) = parent_info
110 && !ty.is_parallel()
111 {
112 return;
113 }
114 items_in_postorder.insert(*item);
115 },
116 )?;
117
118 let mut styles = FxIndexSet::default();
119 let mut handle_module = async |module| {
120 match module_info_map.entry(module) {
121 Entry::Occupied(mut e) => {
122 if let Some(info) = e.get_mut() {
123 info.chunk_group_indices.insert(idx, styles.len());
124 info.index_sum += styles.len();
125 styles.insert(module);
126 }
127 }
128 Entry::Vacant(e) => {
129 if let Some(style_module) =
130 ResolvedVc::try_sidecast::<Box<dyn StyleModule>>(module)
131 {
132 let style_type = *style_module.style_type().await?;
133 let mut info =
134 ModuleInfo::new(style_type, module.ident().to_string().owned().await?);
135 info.chunk_group_indices.insert(idx, styles.len());
136 info.index_sum += styles.len();
137 styles.insert(module);
138 e.insert(Some(info));
139 } else {
140 e.insert(None);
141 }
142 }
143 }
144 anyhow::Ok(())
145 };
146
147 for item in items_in_postorder {
148 match item {
149 ModuleOrBatch::Batch(batch) => {
150 for &module in &batch.await?.modules {
151 handle_module(module).await?;
152 }
153 }
154 ModuleOrBatch::Module(module) => {
155 if let Some(chunkable_module) = ResolvedVc::try_downcast(module) {
156 handle_module(chunkable_module).await?;
157 }
158 }
159 ModuleOrBatch::None(_) => {}
160 }
161 }
162
163 if !styles.is_empty() {
164 chunk_group_state.push(ChunkGroupState {
165 requests: styles.len(),
166 styles,
167 });
168 idx += 1;
169 }
170 }
171
172 module_info_map.retain(|_, info| info.is_some());
173
174 module_info_map.sort_by(|_, a, _, b| {
175 let a = a.as_ref().unwrap();
176 let b = b.as_ref().unwrap();
177 a.index_sum
178 .cmp(&b.index_sum)
179 .then_with(|| a.ident.cmp(&b.ident))
180 });
181
182 let chunk_item_and_sizes = module_info_map
184 .keys()
185 .map(async |&module| {
186 let chunk_item = attach_async_info_to_chunkable_module(
187 module,
188 async_module_info,
189 module_graph,
190 chunking_context,
191 )
192 .await?;
193 let size = *chunk_item
194 .chunk_type
195 .chunk_item_size(chunking_context, *chunk_item.chunk_item, None)
196 .await?;
197 Ok((chunk_item, size))
198 })
199 .try_join()
200 .await?;
201 module_info_map
202 .iter_mut()
203 .zip(chunk_item_and_sizes)
204 .for_each(|((_, info), (chunk_item, size))| {
205 let info = info.as_mut().unwrap();
206 info.size = size;
207 info.chunk_item = Some(chunk_item);
208 });
209
210 Ok(StyleCollection {
211 module_info_map,
212 chunk_group_state,
213 })
214}
215
216pub async fn compute_style_groups(
217 module_graph: Vc<ModuleGraph>,
218 chunking_context: Vc<Box<dyn ChunkingContext>>,
219 config: &StyleGroupsConfig,
220) -> Result<Vc<StyleGroups>> {
221 let StyleCollection {
222 module_info_map,
223 mut chunk_group_state,
224 } = collect_style_modules_per_chunk_group(module_graph, chunking_context).await?;
225
226 let mut module_dependents: FxHashMap<_, Vec<_>> = FxHashMap::default();
228 for (&module, info) in &module_info_map {
229 let info = info.as_ref().unwrap();
230 let (&idx, &start_pos) = info
232 .chunk_group_indices
233 .iter()
234 .min_by_key(|&(&idx, _)| chunk_group_state[idx].styles.len())
235 .unwrap();
236 let potential_dependents = &chunk_group_state[idx].styles[start_pos + 1..];
237
238 let dependents = potential_dependents
239 .iter()
240 .copied()
241 .filter(|dependent| {
242 let dependent_info = module_info_map.get(dependent).unwrap();
243 let dependent_info = dependent_info.as_ref().unwrap();
244
245 info.chunk_group_indices.len() >= dependent_info.chunk_group_indices.len()
248 && dependent_info
249 .chunk_group_indices
250 .iter()
251 .all(|(idx, &dependent_pos)| {
252 info.chunk_group_indices
253 .get(idx)
254 .is_some_and(|&module_pos| module_pos < dependent_pos)
255 })
256 })
257 .collect::<Vec<_>>();
258
259 if !dependents.is_empty() {
260 module_dependents.insert(module, dependents);
261 }
262 }
263
264 let mut ordered_modules_with_state = module_info_map
265 .keys()
266 .copied()
267 .map(|m| (m, false))
268 .collect::<FxIndexMap<_, _>>();
269
270 let mut shared_chunk_items = FxIndexMap::default();
271 for i in 0..ordered_modules_with_state.len() {
272 let (&module, processed) = ordered_modules_with_state.get_index_mut(i).unwrap();
273 if *processed {
274 continue;
275 }
276 *processed = true;
277
278 let info = module_info_map.get(&module).unwrap().as_ref().unwrap();
279 let mut global_mode = info.style_type == StyleType::GlobalStyle;
280
281 let mut all_chunk_states = info.chunk_group_indices.clone();
283
284 let mut new_chunk_modules = [module].into_iter().collect::<FxHashSet<_>>();
286 let mut new_chunk_items = vec![info.chunk_item.unwrap()];
287
288 let mut current_size = info.size;
290
291 let mut potential_next_modules = all_chunk_states
294 .iter()
295 .filter_map(|(&idx, pos)| {
296 let following_styles = &chunk_group_state[idx].styles[pos + 1..];
297 let i = following_styles
298 .iter()
299 .position(|m| !*ordered_modules_with_state.get(m).unwrap());
300 i.map(|i| following_styles[i])
301 })
302 .collect::<FxHashSet<_>>();
303
304 'outer: loop {
306 let mut ordered_potential_next_modules = potential_next_modules
309 .iter()
310 .copied()
311 .map(|module| {
312 let info = module_info_map.get(&module).unwrap().as_ref().unwrap();
313 let requests = info
314 .chunk_group_indices
315 .keys()
316 .filter(|idx| all_chunk_states.contains_key(idx))
317 .map(|&idx| chunk_group_state[idx].requests)
318 .max()
319 .unwrap();
320 (module, info, requests)
321 })
322 .collect::<Vec<_>>();
323 ordered_potential_next_modules
324 .sort_by_key(|(_, info, requests)| (Reverse(*requests), &info.ident));
325
326 for (module, info, _) in ordered_potential_next_modules {
328 if current_size + info.size > config.max_chunk_size {
329 continue;
331 }
332 if let Some(dependents) = module_dependents.get(&module)
334 && dependents.iter().any(|m| new_chunk_modules.contains(m))
335 {
336 continue;
339 }
340
341 let is_global = info.style_type == StyleType::GlobalStyle;
343 if is_global
344 && global_mode
345 && all_chunk_states.len() != info.chunk_group_indices.len()
346 {
347 continue;
349 }
350 if global_mode
351 && info
352 .chunk_group_indices
353 .keys()
354 .any(|idx| !all_chunk_states.contains_key(idx))
355 {
356 continue;
358 }
359 if is_global
360 && all_chunk_states
361 .keys()
362 .any(|idx| !info.chunk_group_indices.contains_key(idx))
363 {
364 continue;
366 }
367 potential_next_modules.remove(&module);
368 current_size += info.size;
369 if is_global {
370 global_mode = true;
371 }
372 for &idx in info.chunk_group_indices.keys() {
373 if all_chunk_states.contains_key(&idx) {
374 chunk_group_state[idx].requests -= 1;
376 }
377 let pos = chunk_group_state[idx].styles.get_index_of(&module).unwrap();
378 all_chunk_states.insert(idx, pos);
379 let following_styles = &chunk_group_state[idx].styles[pos + 1..];
380 if let Some(i) = following_styles.iter().position(|m| {
381 !*ordered_modules_with_state.get(m).unwrap()
382 && !new_chunk_modules.contains(m)
383 }) {
384 let module = following_styles[i];
385 potential_next_modules.insert(module);
386 }
387 }
388
389 new_chunk_items.push(info.chunk_item.unwrap());
390 new_chunk_modules.insert(module);
391 *ordered_modules_with_state.get_mut(&module).unwrap() = true;
392 continue 'outer;
393 }
394 break;
395 }
396
397 if new_chunk_items.len() > 1 {
398 let style_group = ChunkItemBatchWithAsyncModuleInfo::new(new_chunk_items.clone())
399 .to_resolved()
400 .await?;
401 for chunk_item in new_chunk_items {
402 shared_chunk_items.insert(
403 chunk_item,
404 StyleItemInfo {
405 order: None,
406 batch: Some(style_group),
407 },
408 );
409 }
410 }
411 }
412
413 Ok(StyleGroups { shared_chunk_items }.cell())
414}