perf(dom): classify insertion document transitions once

This commit is contained in:
ldm0
2026-09-01 02:30:32 +08:00
committed by Donough Liu
parent f720f0af40
commit 8a0610a014
9 changed files with 142 additions and 139 deletions
@@ -56,21 +56,6 @@ pub(crate) struct CustomElementAdoptionPlan {
pub(crate) registry_retargets: Vec<RegistryAssociationRetarget>,
}
impl CustomElementAdoptionPlan {
pub(crate) fn has_targets(&self) -> bool {
!self.targets.is_empty()
}
pub(crate) fn has_registry_retargets_without_adoption(&self) -> bool {
self.targets.is_empty() && !self.registry_retargets.is_empty()
}
pub(crate) fn extend(&mut self, other: Self) {
self.targets.extend(other.targets);
self.registry_retargets.extend(other.registry_retargets);
}
}
#[cfg(test)]
mod tests {
use super::{CustomElementRegistryAssociation, CustomElementRegistryKey};
@@ -8,32 +8,37 @@ use super::{
};
use crate::{document_runtime::DomHandle, dom::native::Node, native_bridge::JsContextHost};
fn adoption_plan_before_adoption(
host_ptr: *mut JsContextHost,
root: DomHandle,
new_document: DomHandle,
) -> CustomElementAdoptionPlan {
let host = unsafe { &*host_ptr };
CustomElementAdoptionPlan {
targets: adoption_callback_targets(host.dom_host(), root, new_document),
registry_retargets: registry_association_retargets_before_adoption(
host_ptr,
root,
new_document,
),
}
}
pub(crate) fn adoption_plan_for_roots_before_adoption(
host_ptr: *mut JsContextHost,
roots: &[DomHandle],
new_document: DomHandle,
crosses_documents: bool,
) -> CustomElementAdoptionPlan {
let mut plan = CustomElementAdoptionPlan::default();
let host = unsafe { &*host_ptr };
if !crosses_documents && !host.has_explicit_custom_element_registry_associations() {
return CustomElementAdoptionPlan::default();
}
let mut targets = Vec::new();
let mut registry_retargets = Vec::new();
for &root in roots {
plan.extend(adoption_plan_before_adoption(host_ptr, root, new_document));
if crosses_documents {
targets.extend(adoption_callback_targets(
host.dom_host(),
root,
new_document,
));
}
registry_retargets.extend(registry_association_retargets_before_adoption(
host_ptr,
root,
new_document,
));
}
CustomElementAdoptionPlan {
targets,
registry_retargets,
}
plan
}
fn registry_association_retargets_before_adoption(
@@ -712,33 +712,23 @@ impl DocumentRuntime {
document_handle: DomHandle,
handle: DomHandle,
) -> Option<DomHandle> {
let plan = self.tree_adoption_plan(host_ptr, document_handle, handle);
self.apply_native_adoption_plan(scope, host_ptr, &plan)
}
fn tree_adoption_plan(
&self,
host_ptr: *mut JsContextHost,
document_handle: DomHandle,
handle: DomHandle,
) -> TreeAdoptionPlan {
TreeAdoptionPlan::before_adoption(
let plan = TreeAdoptionPlan::before_standalone_adoption(
&self.dom_host,
host_ptr,
std::slice::from_ref(&handle),
handle,
document_handle,
true,
)
);
self.apply_native_adoption_plan(scope, host_ptr, handle, &plan)
}
fn apply_native_adoption_plan(
&mut self,
scope: &mut v8::PinScope<'_, '_>,
host_ptr: *mut JsContextHost,
root: DomHandle,
plan: &TreeAdoptionPlan,
) -> Option<DomHandle> {
let (root, previous_owner_document) = plan.root_with_previous_owner_document()?;
let new_document = plan.new_document()?;
let (previous_owner_document, new_document) = plan.documents()?;
let (adopted, stylesheet_owner_changes) = self
.dom_host
.adopt_node_with_stylesheet_owner_changes(new_document, root)?;
@@ -746,7 +736,7 @@ impl DocumentRuntime {
host_ptr,
&plan.custom_elements().registry_retargets,
);
if previous_owner_document.is_some_and(|owner| owner != new_document) {
if previous_owner_document != new_document {
self.queue_image_loads_after_owner_document_change(scope, host_ptr, root);
}
custom_elements::enqueue_adopted_callbacks(
@@ -770,11 +760,11 @@ impl DocumentRuntime {
scope: &mut v8::PinScope<'_, '_>,
host_ptr: *mut JsContextHost,
handle: DomHandle,
previous_owner_document: Option<DomHandle>,
previous_owner_document: DomHandle,
document_handle: DomHandle,
stylesheet_owner_changes: &[crate::dom::native::DomStylesheetOwnerChange],
) {
if !previous_owner_document.is_some_and(|owner| owner != document_handle) {
if previous_owner_document == document_handle {
return;
}
let runtime = unsafe { &mut *host_ptr };
@@ -2,67 +2,95 @@ use super::insertion_plan::TreeInsertionPlan;
use crate::{
custom_elements,
document_runtime::{DocumentRuntime, DomHandle},
dom::native::{DomHost, Node},
dom::native::DomHost,
native_bridge::JsContextHost,
};
#[derive(Clone, Debug, Default)]
pub(in crate::document_runtime) struct TreeAdoptionPlan {
root: Option<DomHandle>,
previous_owner_document: Option<DomHandle>,
new_document: Option<DomHandle>,
roots_with_owner_document_change: Vec<DomHandle>,
transition: Option<TreeDocumentTransition>,
custom_elements: custom_elements::CustomElementAdoptionPlan,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
struct TreeDocumentTransition {
previous_document: DomHandle,
new_document: DomHandle,
}
impl TreeDocumentTransition {
fn documents(self) -> (DomHandle, DomHandle) {
(self.previous_document, self.new_document)
}
fn crosses_documents(self) -> bool {
self.previous_document != self.new_document
}
fn cross_document(self) -> Option<(DomHandle, DomHandle)> {
self.crosses_documents().then_some(self.documents())
}
fn for_root(dom_host: &DomHost, root: DomHandle, new_document: DomHandle) -> Option<Self> {
Some(Self {
previous_document: dom_host.owner_document_handle(root)?,
new_document,
})
}
}
impl TreeAdoptionPlan {
pub(in crate::document_runtime) fn before_adoption(
pub(in crate::document_runtime) fn before_standalone_adoption(
dom_host: &DomHost,
host_ptr: *mut JsContextHost,
root: DomHandle,
new_document: DomHandle,
) -> Self {
Self::before_adoption(
dom_host,
host_ptr,
std::slice::from_ref(&root),
new_document,
)
}
fn before_adoption(
dom_host: &DomHost,
host_ptr: *mut JsContextHost,
roots: &[DomHandle],
new_document: DomHandle,
collect_custom_elements: bool,
) -> Self {
let root = roots.first().copied();
let previous_owner_document =
root.and_then(|root| dom_host.node(root).and_then(Node::owner_document));
let roots_with_owner_document_change = roots
.iter()
.copied()
.filter(|root| {
dom_host
.node(*root)
.and_then(Node::owner_document)
.is_some_and(|previous| previous != new_document)
})
.collect();
let custom_elements = if collect_custom_elements {
custom_elements::adoption_plan_for_roots_before_adoption(host_ptr, roots, new_document)
} else {
custom_elements::CustomElementAdoptionPlan::default()
};
// The roots of one DOM insertion share a node document, including
// children hoisted from a DocumentFragment. One lookup classifies the
// whole batch.
let transition = roots
.first()
.and_then(|root| TreeDocumentTransition::for_root(dom_host, *root, new_document));
let custom_elements = transition.map_or_else(Default::default, |transition| {
custom_elements::adoption_plan_for_roots_before_adoption(
host_ptr,
roots,
new_document,
transition.crosses_documents(),
)
});
Self {
root,
previous_owner_document,
new_document: Some(new_document),
roots_with_owner_document_change,
transition,
custom_elements,
}
}
pub(in crate::document_runtime) fn root_with_previous_owner_document(
&self,
) -> Option<(DomHandle, Option<DomHandle>)> {
self.root.map(|root| (root, self.previous_owner_document))
pub(in crate::document_runtime) fn documents(&self) -> Option<(DomHandle, DomHandle)> {
self.transition.map(TreeDocumentTransition::documents)
}
pub(super) fn has_targets(&self) -> bool {
self.custom_elements.has_targets()
pub(super) fn crosses_documents(&self) -> bool {
self.transition
.is_some_and(TreeDocumentTransition::crosses_documents)
}
pub(super) fn has_registry_retargets_without_adoption(&self) -> bool {
self.custom_elements
.has_registry_retargets_without_adoption()
fn cross_document(&self) -> Option<(DomHandle, DomHandle)> {
self.transition?.cross_document()
}
pub(in crate::document_runtime) fn custom_elements(
@@ -70,14 +98,6 @@ impl TreeAdoptionPlan {
) -> &custom_elements::CustomElementAdoptionPlan {
&self.custom_elements
}
pub(in crate::document_runtime) fn roots_with_owner_document_change(&self) -> &[DomHandle] {
&self.roots_with_owner_document_change
}
pub(in crate::document_runtime) fn new_document(&self) -> Option<DomHandle> {
self.new_document
}
}
impl DocumentRuntime {
@@ -87,17 +107,10 @@ impl DocumentRuntime {
roots: &[DomHandle],
parent: DomHandle,
) -> TreeAdoptionPlan {
let Some(new_document) = self.document_for_insertion_parent(parent) else {
let Some(new_document) = self.dom_host.owner_document_handle(parent) else {
return TreeAdoptionPlan::default();
};
TreeAdoptionPlan::before_adoption(&self.dom_host, host_ptr, roots, new_document, true)
}
fn document_for_insertion_parent(&self, parent: DomHandle) -> Option<DomHandle> {
if self.dom_host.node(parent).is_some_and(Node::is_document) {
return Some(parent);
}
self.dom_host.owner_document_handle(parent)
TreeAdoptionPlan::before_adoption(&self.dom_host, host_ptr, roots, new_document)
}
pub(super) fn sync_shadow_root_adopted_style_sheets_after_insertion_adoption(
@@ -106,7 +119,7 @@ impl DocumentRuntime {
host_ptr: *mut JsContextHost,
insertion_plan: &TreeInsertionPlan<'_>,
) {
let Some(new_document) = insertion_plan.adoption.new_document() else {
let Some((_, new_document)) = insertion_plan.adoption.cross_document() else {
return;
};
if unsafe { &*host_ptr }
@@ -116,7 +129,7 @@ impl DocumentRuntime {
return;
}
let runtime = unsafe { &mut *host_ptr };
for &root in insertion_plan.adoption.roots_with_owner_document_change() {
for &root in insertion_plan.insertion_roots {
for shadow_root in runtime.shadow_roots_in_subtree(root) {
crate::native_bridge::element::clear_shadow_root_adopted_style_sheets(
scope,
@@ -136,40 +149,51 @@ mod tests {
use url::Url;
#[test]
fn adoption_plan_records_only_roots_that_change_owner_document() {
fn fragment_insertion_roots_share_one_document_transition() {
let mut dom_host = DomHost::from_dom(NativeDom::new(
Url::parse("https://example.test/").expect("test URL parses"),
));
let target_document = dom_host.document_handle();
let other_document = dom_host.create_detached_html_document();
let same_document_root = dom_host.create_parser_element_without_attributes_for_document(
target_document,
let foreign_fragment = dom_host.create_document_fragment_for_document(other_document);
let first_root = dom_host.create_parser_element_without_attributes_for_document(
other_document,
"div".to_owned(),
"http://www.w3.org/1999/xhtml".to_owned(),
None,
);
let other_document_root = dom_host.create_parser_element_without_attributes_for_document(
let second_root = dom_host.create_parser_element_without_attributes_for_document(
other_document,
"span".to_owned(),
"http://www.w3.org/1999/xhtml".to_owned(),
None,
);
let plan = TreeAdoptionPlan::before_adoption(
&dom_host,
std::ptr::null_mut(),
&[same_document_root, other_document_root],
target_document,
false,
);
assert!(dom_host.append_child(foreign_fragment, first_root));
assert!(dom_host.append_child(foreign_fragment, second_root));
assert_eq!(
plan.root_with_previous_owner_document(),
Some((same_document_root, Some(target_document)))
dom_host.owner_document_handle(first_root),
Some(other_document)
);
assert_eq!(
plan.roots_with_owner_document_change(),
&[other_document_root]
dom_host.owner_document_handle(second_root),
Some(other_document)
);
// A DocumentFragment adopts each appended child into its own node
// document, so either root classifies the whole insertion batch.
assert_eq!(
TreeDocumentTransition::for_root(&dom_host, first_root, target_document),
Some(TreeDocumentTransition {
previous_document: other_document,
new_document: target_document,
})
);
assert_eq!(
TreeDocumentTransition::for_root(&dom_host, first_root, other_document),
Some(TreeDocumentTransition {
previous_document: other_document,
new_document: other_document,
})
);
}
}
@@ -12,8 +12,10 @@ impl DocumentRuntime {
insertion_plan: &TreeInsertionPlan<'_>,
) {
let runtime = unsafe { &mut *host_ptr };
for &root in insertion_plan.adoption.roots_with_owner_document_change() {
runtime.migrate_inline_style_metadata_in_subtree(root);
if insertion_plan.adoption.crosses_documents() {
for &root in insertion_plan.insertion_roots {
runtime.migrate_inline_style_metadata_in_subtree(root);
}
}
for &root in insertion_plan.insertion_roots {
runtime.clear_disconnected_shadow_roots_in_subtree(root);
@@ -152,7 +152,7 @@ impl DocumentRuntime {
sync_upgrade_connected_subtrees: bool,
) {
let was_connected = insertion_plan.was_lifecycle_connected_before_insert();
let adopted_across_documents = insertion_plan.adopted_across_documents();
let adopted_across_documents = insertion_plan.adoption.crosses_documents();
if was_connected && adopted_across_documents && !insertion_plan.inserting_fragment_children
{
self.enqueue_adoption_disconnected_callbacks_in_subtrees_unless_pending(
@@ -48,10 +48,6 @@ impl TreeInsertionPlan<'_> {
pub(super) fn was_lifecycle_connected_before_insert(&self) -> bool {
!self.lifecycle_connected_roots_before_insert.is_empty()
}
pub(super) fn adopted_across_documents(&self) -> bool {
self.adoption.has_targets()
}
}
impl TreeInsertionLiveRangeMode {
@@ -195,10 +195,7 @@ impl DocumentRuntime {
insertion_plan,
profile.subresource_request_initiator_type(),
);
if insertion_plan
.adoption
.has_registry_retargets_without_adoption()
{
if !insertion_plan.adoption.crosses_documents() {
custom_elements::apply_registry_association_retargets(
host_ptr,
&insertion_plan.adoption.custom_elements().registry_retargets,
@@ -227,7 +224,7 @@ impl DocumentRuntime {
) {
let lifecycle_quiescent =
unsafe { &*host_ptr }.custom_elements_subtree_lifecycle_quiescent();
let adopted_across_documents = insertion_plan.adoption.has_targets();
let adopted_across_documents = insertion_plan.adoption.crosses_documents();
let mut connected_roots = Vec::new();
let mut removed_from_lifecycle_roots = Vec::new();
let mut form_state_roots = Vec::new();
@@ -1034,6 +1034,10 @@ impl JsContextHost {
.message_port_delivery()
}
pub(crate) fn has_explicit_custom_element_registry_associations(&self) -> bool {
!self.custom_element_registry_associations.is_empty()
}
#[cfg(test)]
pub(crate) fn bridge_ref_count_for_test(&self) -> usize {
self.bridge_ref_count.get()