script/dom/document/accessibility_data.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 js::context::NoGC;
6use rustc_hash::FxHashSet;
7use script_bindings::root::Dom;
8use servo_config::pref;
9use style::dom::OpaqueNode;
10
11use crate::dom::Node;
12
13#[derive(Clone, Default, JSTraceable, MallocSizeOf)]
14#[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)]
15pub(crate) struct AccessibilityData {
16 /// Nodes which have been unbound from the DOM but may not yet have been removed from the
17 /// accessibility tree. This is cleared after each reflow.
18 rooted_nodes: FxHashSet<Dom<Node>>,
19}
20
21impl AccessibilityData {
22 /// Root a node which has been removed from the DOM but which may still have an associated
23 /// accessibility tree node. It will be unrooted after the next reflow, since the accessibility
24 /// tree is updated as part of the reflow process.
25 ///
26 /// Longer explanation:
27 /// - The accessibility tree doesn't hold strong references to DOM nodes, but uses
28 /// [`OpaqueNode`]s as a way of mapping from an incoming DOM node to an existing accessibility
29 /// tree node. This allows us to cache previously computed accessibility data, and update it
30 /// based on the current DOM node state, which is passed in to the update function.
31 /// - If a DOM node is garbage collected before its corresponding node is removed from the
32 /// accessibility tree, there is a risk that another new DOM node may be created at the same
33 /// memory address, causing it to have an identical `OpaqueNode`. If this `OpaqueNode` was
34 /// used to look up a node in the accessibility tree, we would get the stale accessibility
35 /// node corresponding to the node which was removed.
36 /// - A DOM node is prevented from being garbage collected while it's connected to the document;
37 /// it's kept alive by strong references in its parent, child and/or sibling [`Node`]s (and in
38 /// the case of the document itself, by a strong reference in the [`Window`]). See
39 /// [`Node::first_child`], [`Node::next_sibling`], etc.
40 /// - After a node is removed from the tree, those strong references are removed, and it _may_
41 /// become a candidate for GC if its DOM object isn't held (directly or indirectly) in script
42 /// and it isn't immediately inserted elsewhere in the DOM.
43 /// - To make sure the node isn't GCed before the next accessibility update occurs, we
44 /// temporarily root it here in between its removal from the tree and the subsequent reflow.
45 /// - During reflow, the accessibility tree is updated, and all stale accessibility nodes are
46 /// removed.
47 /// - Once reflow has begun, no further DOM mutations can occur, and we can safely un-root these
48 /// nodes by dropping all the strong references being held here. This will allow them to be
49 /// potential candidates for GC after reflow has finished.
50 /// See [`Self::unroot_all_removed_nodes()`] and
51 /// [`Self::unroot_and_drain_all_removed_nodes()`].
52 pub(crate) fn root_removed_node(&mut self, _no_gc: &NoGC, node_to_root: &Node) {
53 debug_assert!(pref!(accessibility_enabled));
54
55 self.rooted_nodes.insert(Dom::from_ref(node_to_root));
56 }
57
58 /// Clear all nodes which were rooted using [`Self::root_removed_node()`], and return the nodes
59 /// which are still disconnected from the tree.
60 /// This should be called instead of [`Self::unroot_all_removed_nodes()`] during reflow
61 /// if [`pref::expensive_accessibility_test_assertions_enabled`] set.
62 pub(crate) fn unroot_and_drain_all_removed_nodes(&mut self) -> FxHashSet<OpaqueNode> {
63 self.rooted_nodes
64 .drain()
65 .filter_map(|node| {
66 if node.is_connected() {
67 return None;
68 }
69 Some(node.to_opaque())
70 })
71 .collect()
72 }
73
74 /// Clear all nodes which were rooted using [`Self::root_removed_node()`].
75 /// This should only be called during reflow.
76 pub(crate) fn unroot_all_removed_nodes(&mut self) {
77 self.rooted_nodes.clear();
78 }
79}