layout: Store most anonymous pseudo-elements in box slots (#37941)

Previously, anonymous boxes, such for anonymous table parts were not
associated with their non-pseudo ancestor DOM nodes. This presents a
problem when it comes time to clear layout data during incremental
layouts. This change reworks the way that pseudo-elements in general are
stored in their non-pseudo ancestor DOM nodes, allowing for any number
to be placed there.

This trades a bit of performance for space, as just adding a vector to
the node would add something like 24 bytes of storage to every node.
This change should have a neutral runtime memory usage.

Testing: This shouldn't change observable behavior and is thus covered
by
existing WPT tests. It will allow tests to pass in a subsequent PR.

Signed-off-by: Martin Robinson <mrobinson@igalia.com>
Co-authored-by: Oriol Brufau <obrufau@igalia.com>
This commit is contained in:
Martin Robinson 2025-07-08 17:22:09 +02:00 committed by GitHub
parent 4054f9a5a0
commit 51367c22a6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 106 additions and 95 deletions

1
Cargo.lock generated
View file

@ -4725,6 +4725,7 @@ dependencies = [
"servo_geometry",
"servo_malloc_size_of",
"servo_url",
"smallvec",
"stylo",
"stylo_atoms",
"stylo_traits",

View file

@ -52,6 +52,7 @@ servo_arc = { workspace = true }
servo_config = { path = "../config" }
servo_geometry = { path = "../geometry" }
servo_url = { path = "../url" }
smallvec = { workspace = true }
stylo = { workspace = true }
stylo_atoms = { workspace = true }
stylo_traits = { workspace = true }

View file

@ -18,6 +18,7 @@ use malloc_size_of_derive::MallocSizeOf;
use net_traits::image_cache::Image;
use script::layout_dom::ServoLayoutNode;
use servo_arc::Arc as ServoArc;
use smallvec::SmallVec;
use style::context::SharedStyleContext;
use style::properties::ComputedValues;
use style::selector_parser::{PseudoElement, RestyleDamage};
@ -32,27 +33,36 @@ use crate::replaced::CanvasInfo;
use crate::table::TableLevelBox;
use crate::taffy::TaffyItemBox;
#[derive(MallocSizeOf)]
pub struct PseudoLayoutData {
pseudo: PseudoElement,
box_slot: ArcRefCell<Option<LayoutBox>>,
}
/// The data that is stored in each DOM node that is used by layout.
#[derive(Default, MallocSizeOf)]
pub struct InnerDOMLayoutData {
pub(super) self_box: ArcRefCell<Option<LayoutBox>>,
pub(super) pseudo_before_box: ArcRefCell<Option<LayoutBox>>,
pub(super) pseudo_after_box: ArcRefCell<Option<LayoutBox>>,
pub(super) pseudo_marker_box: ArcRefCell<Option<LayoutBox>>,
pub(super) pseudo_boxes: SmallVec<[PseudoLayoutData; 2]>,
}
impl InnerDOMLayoutData {
pub(crate) fn for_pseudo(
&self,
pseudo_element: Option<PseudoElement>,
) -> AtomicRef<Option<LayoutBox>> {
match pseudo_element {
Some(PseudoElement::Before) => self.pseudo_before_box.borrow(),
Some(PseudoElement::After) => self.pseudo_after_box.borrow(),
Some(PseudoElement::Marker) => self.pseudo_marker_box.borrow(),
_ => self.self_box.borrow(),
) -> Option<AtomicRef<Option<LayoutBox>>> {
let Some(pseudo_element) = pseudo_element else {
return Some(self.self_box.borrow());
};
for pseudo_layout_data in self.pseudo_boxes.iter() {
if pseudo_element == pseudo_layout_data.pseudo {
return Some(pseudo_layout_data.box_slot.borrow());
}
}
None
}
}
/// A box that is stored in one of the `DOMLayoutData` slots.
@ -171,16 +181,18 @@ pub struct BoxSlot<'dom> {
pub(crate) marker: PhantomData<&'dom ()>,
}
/// A mutable reference to a `LayoutBox` stored in a DOM element.
impl BoxSlot<'_> {
pub(crate) fn new(slot: ArcRefCell<Option<LayoutBox>>) -> Self {
let slot = Some(slot);
impl From<ArcRefCell<Option<LayoutBox>>> for BoxSlot<'_> {
fn from(layout_box_slot: ArcRefCell<Option<LayoutBox>>) -> Self {
let slot = Some(layout_box_slot);
Self {
slot,
marker: PhantomData,
}
}
}
/// A mutable reference to a `LayoutBox` stored in a DOM element.
impl BoxSlot<'_> {
pub(crate) fn dummy() -> Self {
let slot = None;
Self {
@ -226,12 +238,14 @@ pub(crate) trait NodeExt<'dom> {
fn layout_data_mut(&self) -> AtomicRefMut<'dom, InnerDOMLayoutData>;
fn layout_data(&self) -> Option<AtomicRef<'dom, InnerDOMLayoutData>>;
fn element_box_slot(&self) -> BoxSlot<'dom>;
fn pseudo_element_box_slot(&self, which: PseudoElement) -> BoxSlot<'dom>;
fn unset_pseudo_element_box(&self, which: PseudoElement);
fn pseudo_element_box_slot(&self, pseudo_element: PseudoElement) -> BoxSlot<'dom>;
/// Remove boxes for the element itself, and its `:before` and `:after` if any.
/// Remove boxes for the element itself, and all of its pseudo-element boxes.
fn unset_all_boxes(&self);
/// Remove all pseudo-element boxes for this element.
fn unset_all_pseudo_boxes(&self);
fn fragments_for_pseudo(&self, pseudo_element: Option<PseudoElement>) -> Vec<Fragment>;
fn invalidate_cached_fragment(&self);
@ -341,62 +355,55 @@ impl<'dom> NodeExt<'dom> for ServoLayoutNode<'dom> {
}
fn element_box_slot(&self) -> BoxSlot<'dom> {
BoxSlot::new(self.layout_data_mut().self_box.clone())
self.layout_data_mut().self_box.clone().into()
}
fn pseudo_element_box_slot(&self, pseudo_element_type: PseudoElement) -> BoxSlot<'dom> {
let data = self.layout_data_mut();
let cell = match pseudo_element_type {
PseudoElement::Before => &data.pseudo_before_box,
PseudoElement::After => &data.pseudo_after_box,
PseudoElement::Marker => &data.pseudo_marker_box,
_ => unreachable!(
"Asked for box slot for unsupported pseudo-element: {:?}",
pseudo_element_type
),
};
BoxSlot::new(cell.clone())
}
fn unset_pseudo_element_box(&self, pseudo_element_type: PseudoElement) {
let data = self.layout_data_mut();
let cell = match pseudo_element_type {
PseudoElement::Before => &data.pseudo_before_box,
PseudoElement::After => &data.pseudo_after_box,
PseudoElement::Marker => &data.pseudo_marker_box,
_ => unreachable!(
"Asked for box slot for unsupported pseudo-element: {:?}",
pseudo_element_type
),
};
*cell.borrow_mut() = None;
fn pseudo_element_box_slot(&self, pseudo_element: PseudoElement) -> BoxSlot<'dom> {
let mut layout_data = self.layout_data_mut();
let box_slot = ArcRefCell::new(None);
layout_data.pseudo_boxes.push(PseudoLayoutData {
pseudo: pseudo_element,
box_slot: box_slot.clone(),
});
box_slot.into()
}
fn unset_all_boxes(&self) {
let data = self.layout_data_mut();
*data.self_box.borrow_mut() = None;
*data.pseudo_before_box.borrow_mut() = None;
*data.pseudo_after_box.borrow_mut() = None;
*data.pseudo_marker_box.borrow_mut() = None;
let mut layout_data = self.layout_data_mut();
*layout_data.self_box.borrow_mut() = None;
layout_data.pseudo_boxes.clear();
// Stylo already takes care of removing all layout data
// for DOM descendants of elements with `display: none`.
}
fn unset_all_pseudo_boxes(&self) {
self.layout_data_mut().pseudo_boxes.clear();
}
fn invalidate_cached_fragment(&self) {
let data = self.layout_data_mut();
if let Some(data) = data.self_box.borrow_mut().as_mut() {
if let Some(data) = data.self_box.borrow_mut().as_ref() {
data.invalidate_cached_fragment();
}
for pseudo_layout_data in data.pseudo_boxes.iter() {
if let Some(layout_box) = pseudo_layout_data.box_slot.borrow().as_ref() {
layout_box.invalidate_cached_fragment();
}
}
}
fn fragments_for_pseudo(&self, pseudo_element: Option<PseudoElement>) -> Vec<Fragment> {
NodeExt::layout_data(self)
.and_then(|layout_data| {
let Some(layout_data) = NodeExt::layout_data(self) else {
return vec![];
};
let Some(layout_data) = layout_data.for_pseudo(pseudo_element) else {
return vec![];
};
layout_data
.for_pseudo(pseudo_element)
.as_ref()
.map(LayoutBox::fragments)
})
.unwrap_or_default()
}
@ -407,22 +414,12 @@ impl<'dom> NodeExt<'dom> for ServoLayoutNode<'dom> {
layout_object.repair_style(context, self, &style);
}
if let Some(layout_object) = &*data.pseudo_before_box.borrow() {
if let Some(node) = self.to_threadsafe().with_pseudo(PseudoElement::Before) {
layout_object.repair_style(context, self, &node.style(context));
for pseudo_layout_data in data.pseudo_boxes.iter() {
if let Some(layout_box) = pseudo_layout_data.box_slot.borrow().as_ref() {
if let Some(node) = self.to_threadsafe().with_pseudo(pseudo_layout_data.pseudo) {
layout_box.repair_style(context, self, &node.style(context));
}
}
if let Some(layout_object) = &*data.pseudo_after_box.borrow() {
if let Some(node) = self.to_threadsafe().with_pseudo(PseudoElement::After) {
layout_object.repair_style(context, self, &node.style(context));
}
}
if let Some(layout_object) = &*data.pseudo_marker_box.borrow() {
if let Some(node) = self.to_threadsafe().with_pseudo(PseudoElement::Marker) {
layout_object.repair_style(context, self, &node.style(context));
}
}
}

View file

@ -231,9 +231,7 @@ fn traverse_element<'dom>(
context: &LayoutContext,
handler: &mut impl TraversalHandler<'dom>,
) {
// Clear any existing pseudo-element box slot, because markers are not handled like
// `::before`` and `::after`. They are processed during box tree creation.
element.unset_pseudo_element_box(PseudoElement::Marker);
element.unset_all_pseudo_boxes();
let replaced = ReplacedContents::for_element(element, context);
let style = element.style(&context.style_context);
@ -286,9 +284,6 @@ fn traverse_eager_pseudo_element<'dom>(
) {
assert!(pseudo_element_type.is_eager());
// First clear any old contents from the node.
node_info.node.unset_pseudo_element_box(pseudo_element_type);
// If this node doesn't have this eager pseudo-element, exit early. This depends on
// the style applied to the element.
let Some(pseudo_element_info) = node_info.pseudo(context, pseudo_element_type) else {
@ -353,8 +348,8 @@ fn traverse_pseudo_element_contents<'dom>(
anonymous_info,
display_inline,
Contents::Replaced(contents),
// We dont keep pointers to boxes generated by contents of pseudo-elements
BoxSlot::dummy(),
info.node
.pseudo_element_box_slot(PseudoElement::ServoAnonymousBox),
)
},
}

View file

@ -299,9 +299,12 @@ impl<'dom, 'style> BlockContainerBuilder<'dom, 'style> {
self.push_block_level_job_for_inline_formatting_context(inline_formatting_context);
}
let box_slot = table_info
.node
.pseudo_element_box_slot(PseudoElement::ServoAnonymousTable);
self.block_level_boxes.push(BlockLevelJob {
info: table_info,
box_slot: BoxSlot::dummy(),
box_slot,
kind: BlockLevelCreator::AnonymousTable { table_block },
propagated_data: self.propagated_data,
});
@ -683,10 +686,13 @@ impl<'dom> BlockContainerBuilder<'dom, '_> {
})
.clone();
let box_slot = self
.info
.node
.pseudo_element_box_slot(PseudoElement::ServoAnonymousBox);
self.block_level_boxes.push(BlockLevelJob {
info,
// FIXME(nox): We should be storing this somewhere.
box_slot: BoxSlot::dummy(),
box_slot,
kind: BlockLevelCreator::SameFormattingContextBlock(
IntermediateBlockContainer::InlineFormattingContext(
BlockContainer::InlineFormattingContext(inline_formatting_context),

View file

@ -302,10 +302,7 @@ impl<'dom> IncrementalBoxTreeUpdate<'dom> {
}
let layout_data = NodeExt::layout_data(&potential_dirty_root_node)?;
if layout_data.pseudo_before_box.borrow().is_some() {
return None;
}
if layout_data.pseudo_after_box.borrow().is_some() {
if !layout_data.pseudo_boxes.is_empty() {
return None;
}

View file

@ -20,7 +20,7 @@ use super::{
};
use crate::cell::ArcRefCell;
use crate::context::LayoutContext;
use crate::dom::{BoxSlot, LayoutBox};
use crate::dom::{BoxSlot, LayoutBox, NodeExt};
use crate::dom_traversal::{Contents, NodeAndStyleInfo, NonReplacedContents, TraversalHandler};
use crate::flow::{BlockContainerBuilder, BlockFormattingContext};
use crate::formatting_contexts::{
@ -715,12 +715,18 @@ impl<'style, 'dom> TableBuilderTraversal<'style, 'dom> {
row_builder.finish();
let style = anonymous_info.style.clone();
self.push_table_row(ArcRefCell::new(TableTrack {
let table_row = ArcRefCell::new(TableTrack {
base: LayoutBoxBase::new((&anonymous_info).into(), style.clone()),
group_index: self.current_row_group_index,
is_anonymous: true,
shared_background_style: SharedStyle::new(style),
}));
});
self.push_table_row(table_row.clone());
self.info
.node
.pseudo_element_box_slot(PseudoElement::ServoAnonymousTableRow)
.set(LayoutBox::TableLevelBox(TableLevelBox::Track(table_row)))
}
fn push_table_row(&mut self, table_track: ArcRefCell<TableTrack>) {
@ -981,14 +987,22 @@ impl<'style, 'builder, 'dom, 'a> TableRowBuilder<'style, 'builder, 'dom, 'a> {
}
let block_container = builder.finish();
self.table_traversal
.builder
.add_cell(ArcRefCell::new(TableSlotCell {
let new_table_cell = ArcRefCell::new(TableSlotCell {
base: LayoutBoxBase::new(BaseFragmentInfo::anonymous(), anonymous_info.style),
contents: BlockFormattingContext::from_block_container(block_container),
colspan: 1,
rowspan: 1,
}));
});
self.table_traversal
.builder
.add_cell(new_table_cell.clone());
self.info
.node
.pseudo_element_box_slot(PseudoElement::ServoAnonymousTableCell)
.set(LayoutBox::TableLevelBox(TableLevelBox::Cell(
new_table_cell,
)));
}
}