From fd0e2125c60506fea5408b34a0c2021b8ca7bf00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20W=C3=BClker?= Date: Tue, 11 Mar 2025 00:11:20 +0100 Subject: [PATCH] Keep a list of slot descendants on each shadow root (#35802) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This makes it much faster to traverse over the slot descendants of a shadow root, which is a fairly costly part of "assign slottables to a tree". This reduces the time it takes for the results to load on wpt.fyi from over 3 minutes to about 5 seconds. Signed-off-by: Simon Wülker --- components/script/dom/htmlslotelement.rs | 53 ++++++++++++++++++------ components/script/dom/node.rs | 6 ++- components/script/dom/shadowroot.rs | 43 +++++++++++++++++++ 3 files changed, 89 insertions(+), 13 deletions(-) diff --git a/components/script/dom/htmlslotelement.rs b/components/script/dom/htmlslotelement.rs index 2685179af08..6d4d03f518a 100644 --- a/components/script/dom/htmlslotelement.rs +++ b/components/script/dom/htmlslotelement.rs @@ -29,7 +29,7 @@ use crate::dom::element::{AttributeMutation, Element}; use crate::dom::globalscope::GlobalScope; use crate::dom::htmlelement::HTMLElement; use crate::dom::mutationobserver::MutationObserver; -use crate::dom::node::{Node, NodeDamage, ShadowIncluding}; +use crate::dom::node::{Node, NodeDamage, NodeTraits, ShadowIncluding}; use crate::dom::virtualmethods::VirtualMethods; use crate::script_runtime::CanGc; @@ -407,17 +407,7 @@ impl Slottable { // Step 6. Return the first slot in tree order in shadow’s descendants whose // name is slottable’s name, if any; otherwise null. - for node in shadow_root - .upcast::() - .traverse_preorder(ShadowIncluding::No) - { - if let Some(slot) = node.downcast::() { - if slot.Name() == self.name() { - return Some(DomRoot::from_ref(slot)); - } - } - } - None + shadow_root.slot_for_name(&self.name()) } /// @@ -475,11 +465,50 @@ impl VirtualMethods for HTMLSlotElement { self.super_type().unwrap().attribute_mutated(attr, mutation); if attr.local_name() == &local_name!("name") && attr.namespace() == &ns!() { + if let Some(shadow_root) = self.containing_shadow_root() { + // Shadow roots keep a list of slot descendants, so we need to tell it + // about our name change + let old_value = match mutation { + AttributeMutation::Set(old) => old + .map(|value| value.to_string().into()) + .unwrap_or_default(), + AttributeMutation::Removed => attr.value().to_string().into(), + }; + + shadow_root.unregister_slot(old_value, self); + shadow_root.register_slot(self); + } + + // Changing the name might cause slot assignments to change self.upcast::() .GetRootNode(&GetRootNodeOptions::empty()) .assign_slottables_for_a_tree() } } + + fn bind_to_tree(&self, context: &super::node::BindContext) { + if let Some(s) = self.super_type() { + s.bind_to_tree(context); + } + + if !context.tree_is_in_a_shadow_tree { + return; + } + + self.containing_shadow_root() + .expect("not in a shadow tree") + .register_slot(self); + } + + fn unbind_from_tree(&self, context: &super::node::UnbindContext) { + if let Some(s) = self.super_type() { + s.unbind_from_tree(context); + } + + if let Some(shadow_root) = self.containing_shadow_root() { + shadow_root.unregister_slot(self.Name(), self); + } + } } impl js::gc::Rootable for Slottable {} diff --git a/components/script/dom/node.rs b/components/script/dom/node.rs index d4e404dc030..4a4c9f53462 100644 --- a/components/script/dom/node.rs +++ b/components/script/dom/node.rs @@ -1362,7 +1362,11 @@ impl Node { // NOTE: This method traverses all descendants of the node and is potentially very // expensive. If the node is not a shadow root then assigning slottables to it won't // have any effect, so we take a fast path out. - if !self.is::() { + let Some(shadow_root) = self.downcast::() else { + return; + }; + + if !shadow_root.has_slot_descendants() { return; } diff --git a/components/script/dom/shadowroot.rs b/components/script/dom/shadowroot.rs index 357594a0948..c1adb60f566 100644 --- a/components/script/dom/shadowroot.rs +++ b/components/script/dom/shadowroot.rs @@ -2,6 +2,9 @@ * 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 std::collections::HashMap; +use std::collections::hash_map::Entry; + use dom_struct::dom_struct; use servo_arc::Arc; use servo_atoms::Atom; @@ -13,6 +16,7 @@ use style::stylist::{CascadeData, Stylist}; use crate::conversions::Convert; use crate::dom::bindings::cell::DomRefCell; +use crate::dom::bindings::codegen::Bindings::HTMLSlotElementBinding::HTMLSlotElement_Binding::HTMLSlotElementMethods; use crate::dom::bindings::codegen::Bindings::ShadowRootBinding::ShadowRoot_Binding::ShadowRootMethods; use crate::dom::bindings::codegen::Bindings::ShadowRootBinding::{ ShadowRootMode, SlotAssignmentMode, @@ -27,8 +31,10 @@ use crate::dom::document::Document; use crate::dom::documentfragment::DocumentFragment; use crate::dom::documentorshadowroot::{DocumentOrShadowRoot, StyleSheetInDocument}; use crate::dom::element::Element; +use crate::dom::htmlslotelement::HTMLSlotElement; use crate::dom::node::{ BindContext, Node, NodeDamage, NodeFlags, NodeTraits, ShadowIncluding, UnbindContext, + VecPreOrderInsertionHelper, }; use crate::dom::stylesheetlist::{StyleSheetList, StyleSheetListOwner}; use crate::dom::types::EventTarget; @@ -65,6 +71,8 @@ pub(crate) struct ShadowRoot { /// clonable: bool, + + slots: DomRefCell>>>, } impl ShadowRoot { @@ -95,6 +103,7 @@ impl ShadowRoot { mode, slot_assignment_mode, clonable, + slots: Default::default(), } } @@ -209,6 +218,40 @@ impl ShadowRoot { root, ); } + + pub(crate) fn register_slot(&self, slot: &HTMLSlotElement) { + debug!("Registering slot with name={:?}", slot.Name().str()); + + let mut slots = self.slots.borrow_mut(); + + let slots_with_the_same_name = slots.entry(slot.Name()).or_default(); + + // Insert the slot before the first element that comes after it in tree order + slots_with_the_same_name.insert_pre_order(slot, self.upcast::()); + } + + pub(crate) fn unregister_slot(&self, name: DOMString, slot: &HTMLSlotElement) { + debug!("Unregistering slot with name={:?}", name.str()); + + let mut slots = self.slots.borrow_mut(); + let Entry::Occupied(mut entry) = slots.entry(name) else { + panic!("slot is not registered"); + }; + entry.get_mut().retain(|s| slot != &**s); + } + + /// Find the first slot with the given name among this root's descendants in tree order + pub(crate) fn slot_for_name(&self, name: &DOMString) -> Option> { + self.slots + .borrow() + .get(name) + .and_then(|slots| slots.first()) + .map(|slot| slot.as_rooted()) + } + + pub(crate) fn has_slot_descendants(&self) -> bool { + !self.slots.borrow().is_empty() + } } impl ShadowRootMethods for ShadowRoot {