Skip to main content

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}