Skip to main content

turbopack_core/chunk/
available_chunk_groups.rs

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/// Chunk groups whose content is already available.
12///
13/// Whether an individual module is available is derived by intersecting this bitmap with the
14/// module's chunk group membership bitmap.
15///
16/// The bitmap holds indices into the [`crate::module_graph::chunk_group_info::ChunkGroupInfo`] of a
17/// single [`ModuleGraph`]. The same index means something entirely different in another graph, so
18/// the graph is stored alongside the bitmap and every operation asserts that it matches. Use
19/// [`AvailableChunkGroups::translate`] to move availability to a different graph.
20#[turbo_tasks::value]
21#[derive(Clone, Debug)]
22pub struct AvailableChunkGroups {
23    /// The module graph whose `ChunkGroupInfo` the indices in `chunk_groups` refer to.
24    module_graph: ResolvedVc<ModuleGraph>,
25    chunk_groups: RoaringBitmapWrapper,
26}
27
28impl AvailableChunkGroups {
29    /// Panics in debug builds when `module_graph` is not the graph these chunk group indices
30    /// belong to. Chunk group indices of a different graph would silently resolve to unrelated
31    /// chunk groups, which drops modules from the output.
32    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    /// The chunk groups that are available, for `module_graph`.
42    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    /// Adds `chunk_group` to the set. Adding a chunk group that is already available is a no-op:
62    /// a chunk group can be reached again further down a chunking chain, and availability is a
63    /// set, not a count.
64    #[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    /// Re-expresses this availability in terms of `module_graph`'s
81    /// [`crate::module_graph::chunk_group_info::ChunkGroupInfo`].
82    ///
83    /// Every index is mapped to its [`ChunkGroupKey`] in the source graph and back to the index of
84    /// that key in the target graph. A chunk group that the target graph doesn't know is dropped,
85    /// which is the conservative direction: it can only lead to a module being emitted again,
86    /// never to one being omitted.
87    #[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    /// A hash identifying the available chunk groups, used to distinguish assets that are
117    /// generated for different availability.
118    ///
119    /// This value reaches asset idents and therefore output file names (see
120    /// [`crate::chunk::availability_info::AvailabilityInfo::ident`]), so it must mean the same
121    /// thing everywhere. It is derived from the *identity* of each available chunk group -- module
122    /// idents and merge tags -- and never from the chunk group indices themselves: an index is
123    /// only meaningful within one [`ModuleGraph`]'s `ChunkGroupInfo` and denotes an unrelated chunk
124    /// group in any other graph. Hashing indices would let two different availabilities, coming
125    /// from two graphs, claim the same output path with different content.
126    #[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
150/// Hashes the identities of the available chunk groups.
151///
152/// The identities are sorted first: chunk groups are iterated in index order, which differs
153/// between graphs, so the set has to be put into an order that only depends on the identities
154/// themselves.
155fn 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
166/// Maps a chunk group index of `from` to the index of the same chunk group in `to`, or `None` when
167/// `to` doesn't contain that chunk group.
168///
169/// Merged chunk group keys identify their parent by index, so the parent is translated first and a
170/// merged group whose parent is unknown to `to` is dropped along with it.
171fn 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    /// The hash `AvailableChunkGroups::hash` produces for the chunk groups `ids` of `keys`.
209    ///
210    /// Renders the keys the way `hash` does. Every key here is module-free, so rendering resolves
211    /// no module idents and needs no turbo-tasks context.
212    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        // The same chunk groups, but discovered in a different order, so the indices differ.
228        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        // The merged group's parent moved from index 0 to index 1, so the key only matches when
251        // the parent is translated first.
252        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        // Unknown group.
266        assert_eq!(translate_chunk_group(&from, &to, 1), None);
267        // Known group with a known parent.
268        assert_eq!(translate_chunk_group(&from, &to, 2), Some(1));
269        // Merged group whose parent the target graph doesn't have.
270        assert_eq!(translate_chunk_group(&from, &to, 3), None);
271        // Out of range.
272        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        // The same chunk groups, discovered in a different order by each graph, so the same
289        // availability is a *different* set of indices in each: {0,2} here, {2,1} there.
290        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        // Hashing the bitmaps would compare {0,2} against {1,2} and call the same availability
302        // different.
303        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        // This is the collision that motivated hashing identities: when a consumer builds more
313        // than one module graph, the same index denotes an unrelated chunk group in each, so
314        // hashing the bitmap made two different availabilities claim one output path.
315        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        // Both are the bitmap {0}, but they mean different chunk groups.
325        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        // Different variants, different merge tags and different set sizes must all stay apart.
342        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        // A merged group identifies its parent by index. Two graphs that agree on the groups but
350        // not on their order must still render the parent identically.
351        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}