1use anyhow::{Context, Result};
2use roaring::RoaringBitmap;
3use turbo_tasks::{FxIndexSet, ResolvedVc, TryJoinIterExt, Vc};
4use turbo_tasks_hash::Xxh3Hash64Hasher;
5
6use crate::module_graph::{
7 ModuleGraph,
8 chunk_group_info::{ChunkGroupId, ChunkGroupKey, RoaringBitmapWrapper},
9};
10
11#[turbo_tasks::value]
21#[derive(Clone, Debug)]
22pub struct AvailableChunkGroups {
23 module_graph: ResolvedVc<ModuleGraph>,
25 chunk_groups: RoaringBitmapWrapper,
26}
27
28impl AvailableChunkGroups {
29 pub fn assert_module_graph(&self, module_graph: ResolvedVc<ModuleGraph>) {
33 debug_assert_eq!(
34 self.module_graph, module_graph,
35 "AvailableChunkGroups was created for a different ModuleGraph. Chunk group indices \
36 are only valid within the ChunkGroupInfo of the graph that produced them; use \
37 AvailableChunkGroups::translate to move them to another graph."
38 );
39 }
40
41 pub fn chunk_groups(&self, module_graph: ResolvedVc<ModuleGraph>) -> &RoaringBitmapWrapper {
43 self.assert_module_graph(module_graph);
44 &self.chunk_groups
45 }
46}
47
48#[turbo_tasks::value_impl]
49impl AvailableChunkGroups {
50 #[turbo_tasks::function]
51 pub fn new(module_graph: ResolvedVc<ModuleGraph>, chunk_group: u32) -> Vc<Self> {
52 let mut chunk_groups = RoaringBitmapWrapper::default();
53 chunk_groups.insert(chunk_group);
54 Self {
55 module_graph,
56 chunk_groups,
57 }
58 .cell()
59 }
60
61 #[turbo_tasks::function]
65 pub fn with_chunk_group(
66 &self,
67 module_graph: ResolvedVc<ModuleGraph>,
68 chunk_group: u32,
69 ) -> Vc<Self> {
70 self.assert_module_graph(module_graph);
71 let mut chunk_groups = self.chunk_groups.clone();
72 chunk_groups.insert(chunk_group);
73 Self {
74 module_graph: self.module_graph,
75 chunk_groups,
76 }
77 .cell()
78 }
79
80 #[turbo_tasks::function]
88 pub async fn translate(&self, module_graph: ResolvedVc<ModuleGraph>) -> Result<Vc<Self>> {
89 if self.module_graph == module_graph {
90 return Ok(Self {
91 module_graph,
92 chunk_groups: self.chunk_groups.clone(),
93 }
94 .cell());
95 }
96
97 let from = self.module_graph.chunk_group_info().await?;
98 let to = module_graph.chunk_group_info().await?;
99
100 let mut translated = RoaringBitmap::new();
101 for id in self.chunk_groups.iter() {
102 if let Some(id) =
103 translate_chunk_group(&from.chunk_group_keys, &to.chunk_group_keys, id)
104 {
105 translated.insert(id);
106 }
107 }
108
109 Ok(Self {
110 module_graph,
111 chunk_groups: RoaringBitmapWrapper(translated),
112 }
113 .cell())
114 }
115
116 #[turbo_tasks::function]
127 pub async fn hash(&self) -> Result<Vc<u64>> {
128 let chunk_group_info = self.module_graph.chunk_group_info().await?;
129 let keys = &chunk_group_info.chunk_group_keys;
130
131 let idents = self
132 .chunk_groups
133 .iter()
134 .map(async |id| {
135 let key = keys.get_index(id as usize).with_context(|| {
136 format!(
137 "available chunk group {id} is not a chunk group of the module graph it \
138 belongs to"
139 )
140 })?;
141 key.ident_str(keys).await
142 })
143 .try_join()
144 .await?;
145
146 Ok(Vc::cell(hash_chunk_group_idents(idents)))
147 }
148}
149
150fn hash_chunk_group_idents(mut idents: Vec<String>) -> u64 {
156 idents.sort_unstable();
157
158 let mut hasher = Xxh3Hash64Hasher::new();
159 hasher.write_value(idents.len());
160 for ident in &idents {
161 hasher.write_value(ident);
162 }
163 hasher.finish()
164}
165
166fn translate_chunk_group(
172 from: &FxIndexSet<ChunkGroupKey>,
173 to: &FxIndexSet<ChunkGroupKey>,
174 id: u32,
175) -> Option<u32> {
176 let key = from.get_index(id as usize)?;
177 let key = match key {
178 ChunkGroupKey::IsolatedMerged { parent, merge_tag } => ChunkGroupKey::IsolatedMerged {
179 parent: ChunkGroupId::from(translate_chunk_group(from, to, **parent)? as usize),
180 merge_tag: merge_tag.clone(),
181 },
182 ChunkGroupKey::SharedMerged { parent, merge_tag } => ChunkGroupKey::SharedMerged {
183 parent: ChunkGroupId::from(translate_chunk_group(from, to, **parent)? as usize),
184 merge_tag: merge_tag.clone(),
185 },
186 key => key.clone(),
187 };
188 to.get_index_of(&key).map(|id| id as u32)
189}
190
191#[cfg(test)]
192mod tests {
193 use turbo_rcstr::rcstr;
194 use turbo_tasks::FxIndexSet;
195
196 use crate::{
197 chunk::available_chunk_groups::{hash_chunk_group_idents, translate_chunk_group},
198 module_graph::chunk_group_info::{ChunkGroupId, ChunkGroupKey},
199 };
200
201 fn isolated_merged(parent: usize, merge_tag: &str) -> ChunkGroupKey {
202 ChunkGroupKey::IsolatedMerged {
203 parent: ChunkGroupId::from(parent),
204 merge_tag: merge_tag.into(),
205 }
206 }
207
208 async fn hash_of(keys: &FxIndexSet<ChunkGroupKey>, ids: &[u32]) -> u64 {
213 let mut idents = Vec::new();
214 for &id in ids {
215 idents.push(
216 keys[id as usize]
217 .ident_str(keys)
218 .await
219 .expect("rendering a chunk group without modules cannot fail"),
220 );
221 }
222 hash_chunk_group_idents(idents)
223 }
224
225 #[test]
226 fn translates_chunk_groups_by_key_not_by_index() {
227 let from = FxIndexSet::from_iter([
229 ChunkGroupKey::Entry(vec![]),
230 ChunkGroupKey::SharedMultiple(vec![]),
231 ]);
232 let to = FxIndexSet::from_iter([
233 ChunkGroupKey::SharedMultiple(vec![]),
234 ChunkGroupKey::Entry(vec![]),
235 ]);
236
237 assert_eq!(translate_chunk_group(&from, &to, 0), Some(1));
238 assert_eq!(translate_chunk_group(&from, &to, 1), Some(0));
239 }
240
241 #[test]
242 fn translates_a_merged_group_parent_too() {
243 let from = FxIndexSet::from_iter([ChunkGroupKey::Entry(vec![]), isolated_merged(0, "x")]);
244 let to = FxIndexSet::from_iter([
245 ChunkGroupKey::SharedMultiple(vec![]),
246 ChunkGroupKey::Entry(vec![]),
247 isolated_merged(1, "x"),
248 ]);
249
250 assert_eq!(translate_chunk_group(&from, &to, 1), Some(2));
253 }
254
255 #[test]
256 fn drops_chunk_groups_the_target_graph_does_not_have() {
257 let from = FxIndexSet::from_iter([
258 ChunkGroupKey::Entry(vec![]),
259 ChunkGroupKey::SharedMultiple(vec![]),
260 isolated_merged(0, "x"),
261 isolated_merged(1, "y"),
262 ]);
263 let to = FxIndexSet::from_iter([ChunkGroupKey::Entry(vec![]), isolated_merged(0, "x")]);
264
265 assert_eq!(translate_chunk_group(&from, &to, 1), None);
267 assert_eq!(translate_chunk_group(&from, &to, 2), Some(1));
269 assert_eq!(translate_chunk_group(&from, &to, 3), None);
271 assert_eq!(translate_chunk_group(&from, &to, 4), None);
273 }
274
275 #[test]
276 fn rcstr_merge_tags_compare_by_value() {
277 let from = FxIndexSet::from_iter([
278 ChunkGroupKey::Entry(vec![]),
279 isolated_merged(0, rcstr!("x").as_str()),
280 ]);
281 let to = FxIndexSet::from_iter([ChunkGroupKey::Entry(vec![]), isolated_merged(0, "x")]);
282
283 assert_eq!(translate_chunk_group(&from, &to, 1), Some(1));
284 }
285
286 #[tokio::test]
287 async fn hash_does_not_depend_on_chunk_group_order() {
288 let one = FxIndexSet::from_iter([
291 ChunkGroupKey::Entry(vec![]),
292 ChunkGroupKey::SharedMultiple(vec![]),
293 isolated_merged(0, "x"),
294 ]);
295 let other = FxIndexSet::from_iter([
296 ChunkGroupKey::SharedMultiple(vec![]),
297 isolated_merged(2, "x"),
298 ChunkGroupKey::Entry(vec![]),
299 ]);
300
301 assert_eq!(
304 hash_of(&one, &[0, 2]).await,
305 hash_of(&other, &[2, 1]).await,
306 "the same chunk groups must hash equally regardless of their index"
307 );
308 }
309
310 #[tokio::test]
311 async fn hash_distinguishes_equal_indices_in_different_graphs() {
312 let one = FxIndexSet::from_iter([
316 ChunkGroupKey::Entry(vec![]),
317 ChunkGroupKey::SharedMultiple(vec![]),
318 ]);
319 let other = FxIndexSet::from_iter([
320 ChunkGroupKey::SharedMultiple(vec![]),
321 ChunkGroupKey::Entry(vec![]),
322 ]);
323
324 assert_ne!(
326 hash_of(&one, &[0]).await,
327 hash_of(&other, &[0]).await,
328 "different chunk groups must not share a hash just because they share an index"
329 );
330 }
331
332 #[tokio::test]
333 async fn hash_distinguishes_different_chunk_groups() {
334 let keys = FxIndexSet::from_iter([
335 ChunkGroupKey::Entry(vec![]),
336 ChunkGroupKey::SharedMultiple(vec![]),
337 isolated_merged(0, "x"),
338 isolated_merged(0, "y"),
339 ]);
340
341 assert_ne!(hash_of(&keys, &[0]).await, hash_of(&keys, &[1]).await);
343 assert_ne!(hash_of(&keys, &[2]).await, hash_of(&keys, &[3]).await);
344 assert_ne!(hash_of(&keys, &[0]).await, hash_of(&keys, &[0, 1]).await);
345 }
346
347 #[tokio::test]
348 async fn hash_never_contains_a_chunk_group_index() {
349 let one = FxIndexSet::from_iter([ChunkGroupKey::Entry(vec![]), isolated_merged(0, "x")]);
352 let other = FxIndexSet::from_iter([
353 ChunkGroupKey::SharedMultiple(vec![]),
354 ChunkGroupKey::Entry(vec![]),
355 isolated_merged(1, "x"),
356 ]);
357
358 assert_eq!(hash_of(&one, &[1]).await, hash_of(&other, &[2]).await);
359 }
360}