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