Skip to main content

script/dom/
tree_ordered_index_map.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
5use std::cell::{Cell, Ref};
6use std::collections::hash_map::Entry;
7
8use js::context::NoGC;
9use rustc_hash::{FxBuildHasher, FxHashMap};
10use script_bindings::assert::assert_in_script;
11use script_bindings::cell::DomRefCell;
12use script_bindings::inheritance::Castable;
13use script_bindings::root::{Dom, DomRoot};
14use style::Atom;
15
16use crate::dom::Node;
17use crate::dom::bindings::root::{LayoutDom, MutNullableDom};
18use crate::dom::bindings::trace::HashMapTracedValues;
19use crate::dom::iterators::ShadowIncluding;
20use crate::dom::types::Element;
21
22/// An entry in the [`TreeOrderedIndexMap`].
23///
24/// This entry has two states: resolved and unresolved. When an entry in the map refers to a
25/// single element, it is resolved. When any additional entries are added or removed, the entry
26/// becomes unresolved. At that point, in order to know what element an `id` or `name`
27/// attribute refer to, the [`TreeOrderedIndexMap`] walks the DOM and fills out the `elements`
28/// and `element` field of the entry.
29#[derive(JSTraceable, MallocSizeOf)]
30#[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)]
31struct TreeOrderedIndexMapEntry {
32    /// The first element in the tree that has this key. If this is null,
33    /// then it means the list needs to be regenerated after a modification
34    /// of the map.
35    element: MutNullableDom<Element>,
36    /// An ordered list of [`Element`]s that have this key. When this is empty,
37    /// it means the list needs to be regenerated after a modification of the map.
38    elements: Vec<Dom<Element>>,
39    /// The number of [`Element`]s that have this key. This is used in order to
40    /// do an early return during generation of [`Self::elements`].
41    count: usize,
42}
43
44impl TreeOrderedIndexMapEntry {
45    fn new(element: &Element) -> Self {
46        Self {
47            element: MutNullableDom::new(Some(element)),
48            elements: vec![Dom::from_ref(element)],
49            count: 1,
50        }
51    }
52
53    fn add(&mut self) {
54        assert!(self.count >= 1);
55        self.count += 1;
56        self.element.clear();
57        self.elements.clear()
58    }
59
60    fn remove(&mut self) -> bool {
61        self.count -= 1;
62        self.element.clear();
63        self.elements.clear();
64        self.count > 0
65    }
66
67    /// Returns true if this entry needs to be resolved. An entry will need to be
68    /// resolved if it ever gains more than one element associated with it or has
69    /// more than one element associated and `Self::remove()` is called.
70    fn needs_resolution(&self) -> bool {
71        // An empty elements array implicitly means that an entry needs resolution, because
72        // `Self::add()` and `Self::remove()` will clear the array.
73        self.elements.is_empty()
74    }
75}
76
77#[derive(Clone, Copy, JSTraceable, MallocSizeOf)]
78enum IndexType {
79    Id,
80    Name,
81}
82
83impl IndexType {
84    fn get(&self, element: &Element) -> Option<Atom> {
85        match self {
86            IndexType::Id => element.get_id(),
87            IndexType::Name => element.get_name(),
88        }
89    }
90}
91
92/// A data structure that tracks the use of an identifier type in the DOM. Currently this is
93/// meant for tracking elements that share an `id` or `name` attribute.
94///
95/// The core problem this structure is trying to solve is that elements with the same `id` and
96/// 'name' often need to be accessed in document order, yet calculating the order of elements
97/// is expensive. This structure makes calculation of that ordering as cheap as possible by
98/// delaying it until the last moment before it is needed.
99///
100/// The goal here is to mitigate the situation where many elements on a page share the same
101/// identifier. Maintaining an in-order map in that case has very negative performance
102/// characteristics. This map solves that problem, making the optimal case still O(1). It's
103/// actually not very common that a normal page needs the elements in order during script
104/// execution.
105///
106/// Each entry in the map holds a cached ordering, initially valid if there is only a single
107/// element that has a particular `id` or `name`. When another element is added to the map
108/// which shares that identifier, the cache is invalidated and count of elements is maintained.
109/// Only when needing to access the ordered representation of the elements with the identifier
110/// do we walk the DOM, collecting the various elements in order.
111///
112/// There is one unfortunate penalty we have to pay due to Servo's rooting design: layout that
113/// might match elements by id *does* need elements in order. Before layout or running any
114/// query selectors, we need to resolve all unresolved entries in the map. In the end though,
115/// this is still just a single walk over the DOM. This is *only* run when a map entry becomes
116/// invalid due to to an element sharing an id being added or removed.
117#[derive(JSTraceable, MallocSizeOf)]
118#[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)]
119pub(crate) struct TreeOrderedIndexMap {
120    /// It is safe to use FxHash for these maps as Atoms are `string_cache` items that will
121    /// have the hash computed from a u32.
122    map: DomRefCell<HashMapTracedValues<Atom, TreeOrderedIndexMapEntry, FxBuildHasher>>,
123    /// Whether or not this map might need to be resolved for layout or for calls to
124    /// [`Self::for_each`].
125    might_need_rebuild_for_layout: Cell<bool>,
126    /// The [`IndexType`] that this [`TreeOrderedIndexMap`] uses, which is either
127    /// the `id` or `name` attribute.
128    index_type: IndexType,
129}
130
131impl TreeOrderedIndexMap {
132    pub(crate) fn name() -> Self {
133        Self {
134            map: Default::default(),
135            might_need_rebuild_for_layout: Default::default(),
136            index_type: IndexType::Name,
137        }
138    }
139
140    pub(crate) fn id() -> Self {
141        Self {
142            map: Default::default(),
143            might_need_rebuild_for_layout: Default::default(),
144            index_type: IndexType::Id,
145        }
146    }
147
148    /// Add an entry to this map from the `key` (an `id` or `name` attribute value) to
149    /// `Element`. If this is the first time this key is used, then the map will be able to map
150    /// to the entry without having to resolve ordering.
151    pub(crate) fn add(&self, key: &Atom, element: &Element) {
152        debug_assert!(
153            element.upcast::<Node>().is_in_a_document_tree() ||
154                element.upcast::<Node>().is_in_a_shadow_tree()
155        );
156
157        // Gracefully handle empty keys. The calling code should not try to
158        // add empty keys to the map, but this guards a bit against logic errors.
159        if key.is_empty() {
160            return;
161        }
162
163        let mut map = self.map.borrow_mut();
164        match map.entry(key.clone()) {
165            Entry::Vacant(entry) => {
166                entry.insert(TreeOrderedIndexMapEntry::new(element));
167            },
168            Entry::Occupied(mut entry) => {
169                entry.get_mut().add();
170                self.might_need_rebuild_for_layout.set(true);
171            },
172        }
173    }
174
175    /// Remove an entry from the map with `key` (an `id` or `name` attribute value). If there
176    /// are more elements that share the same key, the entry will need resoluton before use.
177    /// Removal of the last element for an entry will remove it from the map.
178    pub(crate) fn remove(&self, key: &Atom) {
179        // Gracefully handle empty keys. The calling code should not try to
180        // add empty keys to the map, but this guards a bit against logic errors.
181        if key.is_empty() {
182            return;
183        }
184
185        let mut map = self.map.borrow_mut();
186        let Entry::Occupied(mut occupied_entry) = map.entry(key.clone()) else {
187            unreachable!("Tried to remove unknown id or name entry: {key}");
188        };
189        if !occupied_entry.get_mut().remove() {
190            occupied_entry.remove();
191        } else {
192            self.might_need_rebuild_for_layout.set(true);
193        }
194    }
195
196    /// Get the first element that has this `key` from the DOM, possibly resolving the ordering
197    /// of unresolved entries.
198    ///
199    /// Note: This should *never* be used during layout as it does rooting and unrooting.
200    pub(crate) fn get(&self, no_gc: &NoGC, scope: &Node, key: &Atom) -> Option<DomRoot<Element>> {
201        assert_in_script();
202
203        let mut map = self.map.borrow_mut();
204        let Entry::Occupied(mut occupied_entry) = map.entry(key.clone()) else {
205            return None;
206        };
207        if let Some(element) = occupied_entry.get().element.get() {
208            return Some(element);
209        }
210
211        Self::resolve_one(no_gc, scope, key, occupied_entry.get_mut(), self.index_type);
212        if let Some(element) = occupied_entry.get().element.get() {
213            return Some(element);
214        }
215
216        occupied_entry.remove();
217        None
218    }
219
220    /// Get all of the entries in the map, in DOM order that share a particular `key`. If the
221    /// entry for this key is unresolved, it will be resolved.
222    ///
223    /// Note: This should *never* be used during layout as it does rooting and unrooting.
224    pub(crate) fn get_all(
225        &self,
226        no_gc: &NoGC,
227        scope: &Node,
228        key: &Atom,
229    ) -> Ref<'_, [Dom<Element>]> {
230        assert_in_script();
231
232        {
233            let mut map = self.map.borrow_mut();
234            if let Entry::Occupied(mut occupied_entry) = map.entry(key.clone()) {
235                if occupied_entry.get().needs_resolution() {
236                    Self::resolve_one(no_gc, scope, key, occupied_entry.get_mut(), self.index_type);
237                }
238                if occupied_entry.get().elements.is_empty() {
239                    occupied_entry.remove();
240                }
241            }
242        }
243        Ref::map(self.map.borrow(), |map| {
244            map.get(key)
245                .map(|entry| &*entry.elements)
246                .unwrap_or_default()
247        })
248    }
249
250    /// Run the given callback against every (key, elements) tuple in this map. This will
251    /// resolve all unresolved entries in the map.
252    ///
253    /// Note: This should *never* be used during layout as it does rooting and unrooting.
254    pub(crate) fn for_each(
255        &self,
256        no_gc: &NoGC,
257        scope: &Node,
258        mut callback: impl FnMut(&Atom, &[Dom<Element>]),
259    ) {
260        self.resolve_all(no_gc, scope);
261        for (key, entry) in self.map.borrow().iter() {
262            callback(key, &entry.elements);
263        }
264    }
265
266    /// Get all of the entries in the map, in DOM order that share a particular `key`. This
267    /// will not resolve any entries. It is an error to call this before an entry is resolved.
268    #[expect(unsafe_code)]
269    pub(crate) fn get_all_for_layout(&self, key: &Atom) -> &[LayoutDom<'_, Element>] {
270        // # Safety: `Dom<Element>` should have the exact same memory layout as
271        // `LayoutDom<'_, Element>` so this should be safe.
272        unsafe {
273            self.map
274                .borrow_for_layout()
275                .get(key)
276                .map_or(&[], |entry| LayoutDom::to_layout_slice(&entry.elements))
277        }
278    }
279
280    /// Resolve a single entry in the map that has more than one element associated with it.
281    /// This will walk the DOM and gather all of the elements that have this id/name associated
282    /// with them in DOM order.
283    fn resolve_one(
284        no_gc: &NoGC,
285        scope: &Node,
286        key: &Atom,
287        entry: &mut TreeOrderedIndexMapEntry,
288        index_type: IndexType,
289    ) {
290        for node in scope.traverse_preorder_non_rooting(no_gc, ShadowIncluding::No) {
291            let Some(element) = node.downcast::<Element>() else {
292                continue;
293            };
294            match index_type.get(element) {
295                Some(id) if id == *key => {
296                    entry.elements.push(Dom::from_ref(element));
297                    entry.element.or_init(|| DomRoot::from_ref(element));
298
299                    if entry.count == entry.elements.len() {
300                        return;
301                    }
302                },
303                _ => {},
304            }
305        }
306
307        entry.count = entry.elements.len();
308    }
309
310    /// Resolve all entries in the map, meaning the next access will not need any resolution.
311    /// This should be called before any layout activity that accesses the map.
312    pub(crate) fn resolve_all(&self, no_gc: &NoGC, scope: &Node) {
313        if !self.might_need_rebuild_for_layout.take() {
314            return;
315        }
316
317        let mut map = self.map.borrow_mut();
318        let mut entries_needing_rebuild: FxHashMap<_, _> = map
319            .iter_mut()
320            .filter(|(_, entry)| entry.needs_resolution())
321            .collect();
322
323        for node in scope.traverse_preorder_non_rooting(no_gc, ShadowIncluding::No) {
324            let Some(element) = node.downcast::<Element>() else {
325                continue;
326            };
327
328            let Some(index) = self.index_type.get(element) else {
329                continue;
330            };
331
332            let mut complete = false;
333            if let Some(entry) = entries_needing_rebuild.get_mut(&index) {
334                entry.elements.push(Dom::from_ref(element));
335                entry.element.or_init(|| DomRoot::from_ref(element));
336                complete = entry.count == entry.elements.len();
337            }
338
339            // Stop tracking entries that are complete.
340            if complete {
341                entries_needing_rebuild.remove(&index);
342
343                // When we have completed all entries, exit this method entirely.
344                if entries_needing_rebuild.is_empty() {
345                    return;
346                }
347            }
348        }
349
350        // If no elements were found for any of the entries needing resolution,
351        // remove them from the map mirroring what `resolve_one` does. For all
352        // other entries, update their final `count`.
353        let mut keys_needing_removal = Vec::new();
354        for (key, entry) in entries_needing_rebuild {
355            if entry.elements.is_empty() {
356                keys_needing_removal.push(key.clone());
357            } else {
358                entry.count = entry.elements.len();
359            }
360        }
361
362        for key in keys_needing_removal {
363            map.remove(&key);
364        }
365    }
366}