mirror of
https://github.com/lexmount/moli.git
synced 2026-10-04 16:00:47 +00:00
perf(dom): classify insertion document transitions once
This commit is contained in:
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user