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}