From 042d95220ac268a9fd77a945079bdb68ecf2b653 Mon Sep 17 00:00:00 2001 From: Martin Robinson Date: Thu, 19 Feb 2026 15:31:13 +0100 Subject: [PATCH] layout: Replace dirty root approach with flexible box tree layout (#42700) Replace the old dirty root box tree layout approach with one that works based on independent formatting contexts. Instead of just allowing a single dirty root to be the source of box tree reconstruction, allow box tree reconstruction to happen anywhere in the tree that can isolate box tree damage from ancestors. This essentially combines damage propagation and box tree construction into a single step. There is currently one downside to this approach compared to the dirty root approach which is that we currently cannot detect and start box tree layout when the dirty has a compatible `display` and `position` value. This can mean the scope of box tree layout extends further up the tree. We will address this in a followup -- but have not noticed any major performance implications (currently fragment tree layout is much more expensive than box tree layout). Benefits: 1. Damage propagation now only happens under the dirty root. 2. Future changes can limit the scope of damage up the tree and perhaps preserve the inline content size cache between box tree rebuilds. Testing: This should not change behavior in a testable way (we currently do not have robust performance tests), so WPT test results should not change. --------- Signed-off-by: Martin Robinson Co-authored-by: Oriol Brufau Co-authored-by: Luke Warlow --- components/layout/construct_modern.rs | 3 + components/layout/dom.rs | 104 ++++++++++ components/layout/flexbox/mod.rs | 2 +- components/layout/flow/root.rs | 236 +--------------------- components/layout/formatting_contexts.rs | 80 ++++++-- components/layout/layout_box_base.rs | 8 +- components/layout/layout_impl.rs | 52 +++-- components/layout/lib.rs | 2 +- components/layout/table/construct.rs | 7 +- components/layout/traversal.rs | 137 ++++++++++--- components/script/dom/element.rs | 2 +- components/script/layout_dom/element.rs | 4 +- components/shared/layout/layout_damage.rs | 37 ++-- 13 files changed, 349 insertions(+), 325 deletions(-) diff --git a/components/layout/construct_modern.rs b/components/layout/construct_modern.rs index 805ce3015c5..d24480d9da4 100644 --- a/components/layout/construct_modern.rs +++ b/components/layout/construct_modern.rs @@ -99,6 +99,9 @@ impl<'dom> ModernContainerJob<'dom> { let formatting_context = IndependentFormattingContext::new( LayoutBoxBase::new(info.into(), info.style.clone()), IndependentFormattingContextContents::Flow(block_formatting_context), + // This is just a series of anonymous text runs, so we don't need to worry + // about what kind of PropagatedBoxTreeData is used here. + Default::default(), ); Some(ModernItem { diff --git a/components/layout/dom.rs b/components/layout/dom.rs index c568438b808..21bbc124460 100644 --- a/components/layout/dom.rs +++ b/components/layout/dom.rs @@ -22,6 +22,8 @@ use style::properties::ComputedValues; use style::selector_parser::PseudoElement; use crate::cell::{ArcRefCell, WeakRefCell}; +use crate::context::LayoutContext; +use crate::dom_traversal::NodeAndStyleInfo; use crate::flexbox::FlexLevelBox; use crate::flow::BlockLevelBox; use crate::flow::inline::{InlineItem, SharedInlineStyles, WeakInlineItem}; @@ -312,6 +314,7 @@ pub(crate) trait NodeExt<'dom> { fn ensure_inner_layout_data(&self) -> AtomicRefMut<'dom, InnerDOMLayoutData>; fn inner_layout_data(&self) -> Option>; + fn inner_layout_data_mut(&self) -> Option>; fn box_slot(&self) -> BoxSlot<'dom>; /// Remove boxes for the element itself, and all of its pseudo-element boxes. @@ -333,6 +336,17 @@ pub(crate) trait NodeExt<'dom> { /// /// When this node has no box yet, `false` is returned. fn isolates_damage_for_damage_propagation(&self) -> bool; + + /// Try to re-run box tree reconstruction from this point. This can succeed if the + /// node itself is still valid and isolates box tree damage from ancestors (for + /// instance, if it starts an independent formatting context). **Note:** This assumes + /// that no ancestors have box damage. + /// + /// Returns `true` if box tree reconstruction was sucessful and `false` otherwise. + fn rebuild_box_tree_from_independent_formatting_context( + &self, + layout_context: &LayoutContext, + ) -> bool; } impl<'dom> NodeExt<'dom> for ServoThreadSafeLayoutNode<'dom> { @@ -428,6 +442,16 @@ impl<'dom> NodeExt<'dom> for ServoThreadSafeLayoutNode<'dom> { }) } + fn inner_layout_data_mut(&self) -> Option> { + self.layout_data().map(|data| { + data.as_any() + .downcast_ref::() + .unwrap() + .0 + .borrow_mut() + }) + } + fn box_slot(&self) -> BoxSlot<'dom> { let pseudo_element_chain = self.pseudo_element_chain(); let Some(primary) = pseudo_element_chain.primary else { @@ -517,4 +541,84 @@ impl<'dom> NodeExt<'dom> for ServoThreadSafeLayoutNode<'dom> { LayoutBox::TaffyItemBox(..) => true, } } + + fn rebuild_box_tree_from_independent_formatting_context( + &self, + layout_context: &LayoutContext, + ) -> bool { + // Do not run incremental box tree layout at the `` or root element as there + // is some special processing that must happen for these elements and it currently + // only happens when doing a full box tree construction traversal. + if self.as_element().is_some_and(|element| { + element.is_body_element_of_html_element_root() || element.is_root() + }) { + return false; + } + + let layout_box = { + let Some(mut inner_layout_data) = self.inner_layout_data_mut() else { + return false; + }; + inner_layout_data.pseudo_boxes.clear(); + inner_layout_data.self_box.clone() + }; + + let layout_box = layout_box.borrow(); + let Some(layout_box) = &*layout_box else { + return false; + }; + + let info = NodeAndStyleInfo::new(*self, self.style(&layout_context.style_context)); + match layout_box { + LayoutBox::DisplayContents(..) => false, + LayoutBox::BlockLevel(block_level) => { + let mut block_level = block_level.borrow_mut(); + match &mut *block_level { + BlockLevelBox::Independent(independent_formatting_context) => { + independent_formatting_context.rebuild(layout_context, &info); + true + }, + BlockLevelBox::OutOfFlowAbsolutelyPositionedBox(positioned_box) => { + positioned_box + .borrow_mut() + .context + .rebuild(layout_context, &info); + true + }, + _ => false, + } + }, + LayoutBox::InlineLevel(inline_level) => match inline_level { + InlineItem::OutOfFlowAbsolutelyPositionedBox(positioned_box, ..) => { + positioned_box + .borrow_mut() + .context + .rebuild(layout_context, &info); + true + }, + InlineItem::Atomic(atomic_box, _, _) => { + atomic_box.borrow_mut().rebuild(layout_context, &info); + true + }, + _ => false, + }, + LayoutBox::FlexLevel(flex_level_box) => { + let mut flex_level_box = flex_level_box.borrow_mut(); + match &mut *flex_level_box { + FlexLevelBox::FlexItem(flex_item_box) => flex_item_box + .independent_formatting_context + .rebuild(layout_context, &info), + FlexLevelBox::OutOfFlowAbsolutelyPositionedBox(positioned_box) => { + positioned_box + .borrow_mut() + .context + .rebuild(layout_context, &info); + }, + } + true + }, + LayoutBox::TableLevelBox(..) => false, + LayoutBox::TaffyItemBox(..) => false, + } + } } diff --git a/components/layout/flexbox/mod.rs b/components/layout/flexbox/mod.rs index c6b102c6ed4..0fc52687135 100644 --- a/components/layout/flexbox/mod.rs +++ b/components/layout/flexbox/mod.rs @@ -212,7 +212,7 @@ impl FlexLevelBox { #[derive(MallocSizeOf)] pub(crate) struct FlexItemBox { - independent_formatting_context: IndependentFormattingContext, + pub(crate) independent_formatting_context: IndependentFormattingContext, } impl std::fmt::Debug for FlexItemBox { diff --git a/components/layout/flow/root.rs b/components/layout/flow/root.rs index cc763ad3865..6ef1dbacca4 100644 --- a/components/layout/flow/root.rs +++ b/components/layout/flow/root.rs @@ -3,35 +3,27 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ use app_units::Au; -use atomic_refcell::AtomicRef; use euclid::Rect; use euclid::default::Size2D as UntypedSize2D; +use layout_api::AxesOverflow; use layout_api::wrapper_traits::{LayoutNode, ThreadSafeLayoutElement, ThreadSafeLayoutNode}; -use layout_api::{AxesOverflow, LayoutElementType, LayoutNodeType}; use malloc_size_of_derive::MallocSizeOf; use paint_api::display_list::AxesScrollSensitivity; use script::layout_dom::{ServoLayoutNode, ServoThreadSafeLayoutNode}; -use servo_arc::Arc; -use style::dom::{NodeInfo, TNode}; -use style::properties::ComputedValues; use style::values::computed::Overflow; -use style::values::specified::box_::DisplayOutside; use style_traits::CSSPixel; use crate::cell::ArcRefCell; use crate::context::LayoutContext; use crate::dom::{LayoutBox, NodeExt}; use crate::dom_traversal::{Contents, NodeAndStyleInfo}; -use crate::flexbox::FlexLevelBox; use crate::flow::float::FloatBox; -use crate::flow::inline::InlineItem; use crate::flow::{BlockContainer, BlockFormattingContext, BlockLevelBox}; use crate::formatting_contexts::IndependentFormattingContext; use crate::fragment_tree::{FragmentFlags, FragmentTree}; use crate::geom::{LogicalVec2, PhysicalSize}; use crate::positioned::{AbsolutelyPositionedBox, PositioningContext}; -use crate::style_ext::{Display, DisplayGeneratingBox, DisplayInside}; -use crate::taffy::{TaffyItemBox, TaffyItemBoxInner}; +use crate::style_ext::Display; use crate::{DefiniteContainingBlock, PropagatedBoxTreeData}; #[derive(MallocSizeOf)] @@ -124,37 +116,6 @@ impl BoxTree { root_overflow }) } - - /// This method attempts to incrementally update the box tree from an - /// arbitrary node that is not necessarily the document's root element. - /// - /// If the node is not a valid candidate for incremental update, the method - /// loops over its parent. The only valid candidates for now are absolutely - /// positioned boxes which don't change their outside display mode (i.e. it - /// will not attempt to update from an absolutely positioned inline element - /// which became an absolutely positioned block element). The value `true` - /// is returned if an incremental update could be done, and `false` - /// otherwise. - /// - /// There are various pain points that need to be taken care of to extend - /// the set of valid candidates: - /// * it is not obvious how to incrementally check whether a block - /// formatting context still contains floats or not; - /// * the propagation of text decorations towards node descendants is - /// hard to do incrementally with our current representation of boxes - /// * how intrinsic content sizes are computed eagerly makes it hard - /// to update those sizes for ancestors of the node from which we - /// made an incremental update. - pub(crate) fn update( - context: &LayoutContext, - dirty_root_from_script: ServoLayoutNode<'_>, - ) -> bool { - let Some(box_tree_update) = IncrementalBoxTreeUpdate::find(dirty_root_from_script) else { - return false; - }; - box_tree_update.update_from_dirty_root(context); - true - } } fn construct_for_root_element( @@ -267,196 +228,3 @@ impl BoxTree { ) } } - -#[expect(clippy::enum_variant_names)] -enum DirtyRootBoxTreeNode { - AbsolutelyPositionedBlockLevelBox(ArcRefCell), - AbsolutelyPositionedInlineLevelBox(InlineItem, usize), - AbsolutelyPositionedFlexLevelBox(ArcRefCell), - AbsolutelyPositionedTaffyLevelBox(ArcRefCell), -} - -struct IncrementalBoxTreeUpdate<'dom> { - node: ServoLayoutNode<'dom>, - box_tree_node: DirtyRootBoxTreeNode, - primary_style: Arc, - display_inside: DisplayInside, -} - -impl<'dom> IncrementalBoxTreeUpdate<'dom> { - fn find(dirty_root_from_script: ServoLayoutNode<'dom>) -> Option { - let mut maybe_dirty_root_node = Some(dirty_root_from_script); - while let Some(dirty_root_node) = maybe_dirty_root_node { - if let Some(dirty_root) = Self::new_if_valid(dirty_root_node) { - return Some(dirty_root); - } - - maybe_dirty_root_node = dirty_root_node.parent_node(); - } - - None - } - - fn new_if_valid(potential_dirty_root_node: ServoLayoutNode<'dom>) -> Option { - if !potential_dirty_root_node.is_element() { - return None; - } - - if potential_dirty_root_node.type_id() == - LayoutNodeType::Element(LayoutElementType::HTMLBodyElement) - { - // This can require changes to the canvas background. - return None; - } - - // Don't update unstyled nodes or nodes that have pseudo-elements. - let potential_thread_safe_dirty_root_node = potential_dirty_root_node.to_threadsafe(); - let element_data = potential_thread_safe_dirty_root_node - .style_data()? - .element_data - .borrow(); - if !element_data.styles.pseudos.is_empty() { - return None; - } - - let layout_data = NodeExt::inner_layout_data(&potential_thread_safe_dirty_root_node)?; - if !layout_data.pseudo_boxes.is_empty() { - return None; - } - - let primary_style = element_data.styles.primary(); - let box_style = primary_style.get_box(); - - if !box_style.position.is_absolutely_positioned() { - return None; - } - - let display_inside = match Display::from(box_style.display) { - Display::GeneratingBox(DisplayGeneratingBox::OutsideInside { inside, .. }) => inside, - _ => return None, - }; - - let box_tree_node = - match &*AtomicRef::filter_map(layout_data.self_box.borrow(), Option::as_ref)? { - LayoutBox::DisplayContents(..) => return None, - LayoutBox::BlockLevel(block_level_box) => match &*block_level_box.borrow() { - BlockLevelBox::OutOfFlowAbsolutelyPositionedBox(_) - if box_style.position.is_absolutely_positioned() => - { - // If the outer type of its original display changed from block to inline, - // a block-level abspos needs to be placed in an inline formatting context, - // see [`BlockContainerBuilder::handle_absolutely_positioned_element()`]. - if box_style.original_display.outside() == DisplayOutside::Inline { - return None; - } - DirtyRootBoxTreeNode::AbsolutelyPositionedBlockLevelBox( - block_level_box.clone(), - ) - }, - _ => return None, - }, - LayoutBox::InlineLevel(inline_level_box) => { - let InlineItem::OutOfFlowAbsolutelyPositionedBox(_, text_offset_index) = - inline_level_box - else { - return None; - }; - DirtyRootBoxTreeNode::AbsolutelyPositionedInlineLevelBox( - inline_level_box.clone(), - *text_offset_index, - ) - }, - LayoutBox::FlexLevel(flex_level_box) => match &*flex_level_box.borrow() { - FlexLevelBox::OutOfFlowAbsolutelyPositionedBox(_) - if box_style.position.is_absolutely_positioned() => - { - DirtyRootBoxTreeNode::AbsolutelyPositionedFlexLevelBox( - flex_level_box.clone(), - ) - }, - _ => return None, - }, - LayoutBox::TableLevelBox(..) => return None, - LayoutBox::TaffyItemBox(taffy_level_box) => { - match &taffy_level_box.borrow().taffy_level_box { - TaffyItemBoxInner::OutOfFlowAbsolutelyPositionedBox(_) - if box_style.position.is_absolutely_positioned() => - { - DirtyRootBoxTreeNode::AbsolutelyPositionedTaffyLevelBox( - taffy_level_box.clone(), - ) - }, - _ => return None, - } - }, - }; - - Some(Self { - node: potential_dirty_root_node, - box_tree_node, - primary_style: primary_style.clone(), - display_inside, - }) - } - - #[servo_tracing::instrument(name = "Box Tree Update From Dirty Root", skip_all)] - fn update_from_dirty_root(&self, context: &LayoutContext) { - let node = self.node.to_threadsafe(); - let contents = Contents::for_element(node, context); - - let info = NodeAndStyleInfo::new(node, self.primary_style.clone()); - - let build_new_box = |old_parent| { - let mut out_of_flow_absolutely_positioned_box = - AbsolutelyPositionedBox::construct(context, &info, self.display_inside, contents); - if let Some(old_parent) = old_parent { - out_of_flow_absolutely_positioned_box - .context - .base - .parent_box - .replace(old_parent); - } - out_of_flow_absolutely_positioned_box - }; - - match &self.box_tree_node { - DirtyRootBoxTreeNode::AbsolutelyPositionedBlockLevelBox(block_level_box) => { - let mut block_level_box = block_level_box.borrow_mut(); - let old_parent = block_level_box.with_base(|base| base.parent_box.clone()); - *block_level_box = BlockLevelBox::OutOfFlowAbsolutelyPositionedBox( - ArcRefCell::new(build_new_box(old_parent)), - ); - }, - DirtyRootBoxTreeNode::AbsolutelyPositionedInlineLevelBox( - inline_level_box, - text_offset_index, - ) => match inline_level_box { - InlineItem::OutOfFlowAbsolutelyPositionedBox(positioned_box, offset_in_text) => { - let mut positioned_box = positioned_box.borrow_mut(); - let old_parent = positioned_box.context.base.parent_box.clone(); - *positioned_box = build_new_box(old_parent); - assert_eq!( - *offset_in_text, *text_offset_index, - "The offset of the dirty root shouldn't have changed" - ); - }, - _ => unreachable!("The dirty root should be absolutely positioned"), - }, - DirtyRootBoxTreeNode::AbsolutelyPositionedFlexLevelBox(flex_level_box) => { - let mut flex_level_box = flex_level_box.borrow_mut(); - let old_parent = flex_level_box.with_base(|base| base.parent_box.clone()); - *flex_level_box = FlexLevelBox::OutOfFlowAbsolutelyPositionedBox(ArcRefCell::new( - build_new_box(old_parent), - )); - }, - DirtyRootBoxTreeNode::AbsolutelyPositionedTaffyLevelBox(taffy_level_box) => { - let mut taffy_level_box = taffy_level_box.borrow_mut(); - let old_parent = taffy_level_box.with_base(|base| base.parent_box.clone()); - taffy_level_box.taffy_level_box = - TaffyItemBoxInner::OutOfFlowAbsolutelyPositionedBox(ArcRefCell::new( - build_new_box(old_parent), - )); - }, - } - } -} diff --git a/components/layout/formatting_contexts.rs b/components/layout/formatting_contexts.rs index be3bb84786e..a2edd73cba1 100644 --- a/components/layout/formatting_contexts.rs +++ b/components/layout/formatting_contexts.rs @@ -26,7 +26,7 @@ use crate::replaced::ReplacedContents; use crate::sizing::{ self, ComputeInlineContentSizes, ContentSizes, InlineContentSizesResult, LazySize, }; -use crate::style_ext::{AspectRatio, DisplayInside, LayoutStyle}; +use crate::style_ext::{AspectRatio, Display, DisplayInside, LayoutStyle}; use crate::table::Table; use crate::taffy::TaffyContainer; use crate::{ @@ -41,6 +41,9 @@ pub(crate) struct IndependentFormattingContext { // Private so that code outside of this module cannot match variants. // It should go through methods instead. contents: IndependentFormattingContextContents, + /// Data that was originally propagated down to this [`IndependentFormattingContext`] + /// during creation. This is used during incremental layout. + propagated_data: PropagatedBoxTreeData, } #[derive(Debug, MallocSizeOf)] @@ -75,11 +78,45 @@ impl Baselines { } impl IndependentFormattingContext { - pub(crate) fn new(base: LayoutBoxBase, contents: IndependentFormattingContextContents) -> Self { - Self { base, contents } + pub(crate) fn new( + base: LayoutBoxBase, + contents: IndependentFormattingContextContents, + propagated_data: PropagatedBoxTreeData, + ) -> Self { + Self { + base, + contents, + propagated_data, + } } - pub fn construct( + pub(crate) fn rebuild( + &mut self, + layout_context: &LayoutContext, + node_and_style_info: &NodeAndStyleInfo, + ) { + let contents = Contents::for_element(node_and_style_info.node, layout_context); + let display = match Display::from(node_and_style_info.style.get_box().display) { + Display::None | Display::Contents => { + unreachable!("Should never try to rebuild IndependentFormattingContext with no box") + }, + Display::GeneratingBox(display) => display.used_value_for_contents(&contents), + }; + self.contents = Self::construct_contents( + layout_context, + node_and_style_info, + &mut self.base.base_fragment_info, + display.display_inside(), + contents, + self.propagated_data, + ); + + self.base.clear_fragments_and_fragment_cache(); + *self.base.cached_inline_content_size.borrow_mut() = None; + self.base.repair_style(&node_and_style_info.style); + } + + pub(crate) fn construct( context: &LayoutContext, node_and_style_info: &NodeAndStyleInfo, display_inside: DisplayInside, @@ -87,7 +124,29 @@ impl IndependentFormattingContext { propagated_data: PropagatedBoxTreeData, ) -> Self { let mut base_fragment_info: BaseFragmentInfo = node_and_style_info.into(); + let contents = Self::construct_contents( + context, + node_and_style_info, + &mut base_fragment_info, + display_inside, + contents, + propagated_data, + ); + Self { + base: LayoutBoxBase::new(base_fragment_info, node_and_style_info.style.clone()), + contents, + propagated_data, + } + } + fn construct_contents( + context: &LayoutContext, + node_and_style_info: &NodeAndStyleInfo, + base_fragment_info: &mut BaseFragmentInfo, + display_inside: DisplayInside, + contents: Contents, + propagated_data: PropagatedBoxTreeData, + ) -> IndependentFormattingContextContents { let non_replaced_contents = match contents { Contents::Replaced(contents) => { base_fragment_info.flags.insert(FragmentFlags::IS_REPLACED); @@ -116,12 +175,10 @@ impl IndependentFormattingContext { ArcRefCell::new(IndependentFormattingContext::new( widget_base, widget_contents, + propagated_data, )) }); - return Self { - base: LayoutBoxBase::new(base_fragment_info, node_and_style_info.style.clone()), - contents: IndependentFormattingContextContents::Replaced(contents, widget), - }; + return IndependentFormattingContextContents::Replaced(contents, widget); }, Contents::Widget(non_replaced_contents) => { base_fragment_info.flags.insert(FragmentFlags::IS_WIDGET); @@ -129,7 +186,8 @@ impl IndependentFormattingContext { }, Contents::NonReplaced(non_replaced_contents) => non_replaced_contents, }; - let contents = match display_inside { + + match display_inside { DisplayInside::Flow { is_list_item } | DisplayInside::FlowRoot { is_list_item } => { IndependentFormattingContextContents::Flow(BlockFormattingContext::construct( context, @@ -173,10 +231,6 @@ impl IndependentFormattingContext { propagated_data, )) }, - }; - Self { - base: LayoutBoxBase::new(base_fragment_info, node_and_style_info.style.clone()), - contents, } } diff --git a/components/layout/layout_box_base.rs b/components/layout/layout_box_base.rs index 123c3f04385..aab9ebd0fb8 100644 --- a/components/layout/layout_box_base.rs +++ b/components/layout/layout_box_base.rs @@ -92,6 +92,11 @@ impl LayoutBoxBase { self.fragments.borrow_mut().clear(); } + pub(crate) fn clear_fragments_and_fragment_cache(&self) { + self.fragments.borrow_mut().clear(); + *self.cached_layout_result.borrow_mut() = None; + } + pub(crate) fn repair_style(&mut self, new_style: &Arc) { self.style = new_style.clone(); for fragment in self.fragments.borrow_mut().iter_mut() { @@ -111,8 +116,7 @@ impl LayoutBoxBase { element_damage: LayoutDamage, damage_from_children: LayoutDamage, ) -> LayoutDamage { - self.clear_fragments(); - *self.cached_layout_result.borrow_mut() = None; + self.clear_fragments_and_fragment_cache(); if !element_damage.is_empty() || damage_from_children.contains(LayoutDamage::RECOMPUTE_INLINE_CONTENT_SIZES) diff --git a/components/layout/layout_impl.rs b/components/layout/layout_impl.rs index 4ba66b4b385..3d862058d83 100644 --- a/components/layout/layout_impl.rs +++ b/components/layout/layout_impl.rs @@ -22,8 +22,8 @@ use fonts::{FontContext, FontContextWebFontMethods, WebFontDocumentContext}; use fonts_traits::StylesheetWebFontLoadFinishedCallback; use layout_api::wrapper_traits::LayoutNode; use layout_api::{ - BoxAreaType, CSSPixelRectIterator, IFrameSizes, Layout, LayoutConfig, LayoutDamage, - LayoutFactory, OffsetParentResponse, PhysicalSides, PropertyRegistration, QueryMsg, ReflowGoal, + BoxAreaType, CSSPixelRectIterator, IFrameSizes, Layout, LayoutConfig, LayoutFactory, + OffsetParentResponse, PhysicalSides, PropertyRegistration, QueryMsg, ReflowGoal, ReflowPhasesRun, ReflowRequest, ReflowRequestRestyle, ReflowResult, RegisterPropertyError, ScrollContainerQueryFlags, ScrollContainerResponse, TrustedNodeAddress, }; @@ -93,7 +93,7 @@ use crate::query::{ process_resolved_font_style_query, process_resolved_style_request, process_scroll_container_query, }; -use crate::traversal::{RecalcStyle, compute_damage_and_repair_style}; +use crate::traversal::{RecalcStyle, compute_damage_and_rebuild_box_tree}; use crate::{BoxTree, FragmentTree}; // This mutex is necessary due to syncronisation issues between two different types of thread-local storage @@ -1106,11 +1106,26 @@ impl LayoutThread { Default::default() }; - let damage = compute_damage_and_repair_style( - &layout_context.style_context, - root_node.to_threadsafe(), - damage_from_environment, - ); + let mut box_tree = self.box_tree.borrow_mut(); + let damage = { + let box_tree = &mut *box_tree; + let mut compute_damage_and_build_box_tree = || { + compute_damage_and_rebuild_box_tree( + box_tree, + &layout_context, + dirty_root, + root_node, + damage_from_environment, + ) + }; + + if let Some(pool) = rayon_pool { + pool.install(compute_damage_and_build_box_tree) + } else { + compute_damage_and_build_box_tree() + } + }; + if damage.contains(RestyleDamage::RECALCULATE_OVERFLOW) { self.need_overflow_calculation.set(true); } @@ -1120,31 +1135,12 @@ impl LayoutThread { if damage.contains(RestyleDamage::REPAINT) { self.need_new_display_list.set(true); } - if !damage.contains(RestyleDamage::RELAYOUT) { layout_context.style_context.stylist.rule_tree().maybe_gc(); return (ReflowPhasesRun::empty(), IFrameSizes::default()); } - let mut box_tree = self.box_tree.borrow_mut(); - let box_tree = &mut *box_tree; - let layout_damage: LayoutDamage = damage.into(); - if box_tree.is_none() || layout_damage.has_box_damage() { - let mut build_box_tree = || { - if !BoxTree::update(recalc_style_traversal.context(), dirty_root) { - *box_tree = Some(Arc::new(BoxTree::construct( - recalc_style_traversal.context(), - root_node, - ))); - } - }; - if let Some(pool) = rayon_pool { - pool.install(build_box_tree) - } else { - build_box_tree() - }; - } - + let box_tree = &*box_tree; let viewport_size = self.stylist.device().au_viewport_size(); let run_layout = || { box_tree diff --git a/components/layout/lib.rs b/components/layout/lib.rs index 9d7c45e1e1c..ddf5f09e327 100644 --- a/components/layout/lib.rs +++ b/components/layout/lib.rs @@ -157,7 +157,7 @@ impl<'a> From<&'_ DefiniteContainingBlock<'a>> for ContainingBlock<'a> { /// Data that is propagated from ancestors to descendants during [`crate::flow::BoxTree`] /// construction. This allows data to flow in the reverse direction of the typical layout /// propoagation, but only during `BoxTree` construction. -#[derive(Clone, Copy, Debug)] +#[derive(Clone, Copy, Debug, MallocSizeOf)] struct PropagatedBoxTreeData { allow_percentage_column_in_tables: bool, } diff --git a/components/layout/table/construct.rs b/components/layout/table/construct.rs index e7e36e63d98..4b2fc959a19 100644 --- a/components/layout/table/construct.rs +++ b/components/layout/table/construct.rs @@ -121,6 +121,7 @@ impl Table { let ifc = IndependentFormattingContext::new( LayoutBoxBase::new((&table_info).into(), table_style), IndependentFormattingContextContents::Table(table), + propagated_data, ); (table_info, ifc) @@ -882,7 +883,11 @@ impl<'dom> TraversalHandler<'dom> for TableBuilderTraversal<'_, 'dom> { ); let base = LayoutBoxBase::new(info.into(), info.style.clone()); ArcRefCell::new(TableCaption { - context: IndependentFormattingContext::new(base, contents), + context: IndependentFormattingContext::new( + base, + contents, + self.current_propagated_data, + ), }) }); diff --git a/components/layout/traversal.rs b/components/layout/traversal.rs index db19f33c24d..383a0db75c4 100644 --- a/components/layout/traversal.rs +++ b/components/layout/traversal.rs @@ -3,17 +3,19 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ use std::cell::Cell; +use std::sync::Arc; use bitflags::Flags; use layout_api::LayoutDamage; use layout_api::wrapper_traits::{LayoutNode, ThreadSafeLayoutNode}; -use script::layout_dom::ServoThreadSafeLayoutNode; +use script::layout_dom::{ServoLayoutNode, ServoThreadSafeLayoutNode}; use style::context::{SharedStyleContext, StyleContext}; use style::data::ElementData; use style::dom::{NodeInfo, TElement, TNode}; use style::selector_parser::RestyleDamage; use style::traversal::{DomTraversal, PerLevelTraversalData, recalc_style_at}; +use crate::BoxTree; use crate::context::LayoutContext; use crate::dom::{DOMLayoutData, NodeExt}; @@ -95,16 +97,85 @@ where } #[servo_tracing::instrument(skip_all)] -pub(crate) fn compute_damage_and_repair_style( - context: &SharedStyleContext, - node: ServoThreadSafeLayoutNode<'_>, +pub(crate) fn compute_damage_and_rebuild_box_tree( + box_tree: &mut Option>, + layout_context: &LayoutContext, + dirty_root: ServoLayoutNode<'_>, + root_node: ServoLayoutNode<'_>, damage_from_environment: RestyleDamage, ) -> RestyleDamage { - compute_damage_and_repair_style_inner(context, node, damage_from_environment) + let restyle_damage = compute_damage_and_rebuild_box_tree_inner( + layout_context, + dirty_root.to_threadsafe(), + damage_from_environment, + ); + + let layout_damage: LayoutDamage = restyle_damage.into(); + if box_tree.is_none() { + *box_tree = Some(Arc::new(BoxTree::construct(layout_context, root_node))); + return restyle_damage; + } + + // There are two cases where we need to do more work: + // + // 1. Fragment tree layout needs to run again, in which case we should invalidate all + // fragments to the root of the DOM. + // 2. Box tree reconstruction needs to run at the dirty root, in which case we need to + // find an appropriate place to run box tree reconstruction and *also* invalidate all + // fragments to the root of the DOM. + if !restyle_damage.contains(RestyleDamage::RELAYOUT) { + return restyle_damage; + } + + // If the damage traversal indicated that the dirty root needs a new box, walk up the + // tree to find an appropriate place to run box tree reconstruction. + let mut needs_box_tree_rebuild = layout_damage.needs_new_box(); + + let mut damage_for_ancestors = LayoutDamage::RECOMPUTE_INLINE_CONTENT_SIZES; + let mut maybe_parent_node = dirty_root.traversal_parent(); + while let Some(parent_node) = maybe_parent_node { + let threadsafe_parent_node = parent_node.as_node().to_threadsafe(); + + // If we need box tree reconstruction, try it here. + if needs_box_tree_rebuild && + threadsafe_parent_node + .rebuild_box_tree_from_independent_formatting_context(layout_context) + { + needs_box_tree_rebuild = false; + } + + if needs_box_tree_rebuild { + // We have not yet found a place to run box tree reconstruction, so clear this + // node's boxes to ensure that they are invalidated for the reconstruction we + // will run later. + threadsafe_parent_node.unset_all_boxes(); + } else { + // Reconstruction has already run or was not necessary, so we just need to + // ensure that fragment tree layout does not reuse any cached fragments. + let new_damage_for_ancestors = Cell::new(LayoutDamage::empty()); + threadsafe_parent_node.with_layout_box_base_including_pseudos(|base| { + new_damage_for_ancestors.set( + new_damage_for_ancestors.get() | + base.add_damage(Default::default(), damage_for_ancestors), + ); + }); + damage_for_ancestors = new_damage_for_ancestors.get(); + } + + maybe_parent_node = parent_node.traversal_parent(); + } + + // We could not find a place in the middle of the tree to run box tree reconstruction, + // so just rebuild the whole tree. + if needs_box_tree_rebuild { + *box_tree = Some(Arc::new(BoxTree::construct(layout_context, root_node))); + } + + restyle_damage } -pub(crate) fn compute_damage_and_repair_style_inner( - context: &SharedStyleContext, +pub(crate) fn compute_damage_and_rebuild_box_tree_inner( + layout_context: &LayoutContext, node: ServoThreadSafeLayoutNode<'_>, damage_from_parent: RestyleDamage, ) -> RestyleDamage { @@ -132,11 +203,11 @@ pub(crate) fn compute_damage_and_repair_style_inner( // box damage. let mut damage_for_children = element_and_parent_damage; damage_for_children.truncate(); - let rebuild_children = element_damage.contains(LayoutDamage::rebuild_box_tree()) || - (damage_from_parent.contains(LayoutDamage::rebuild_box_tree()) && + let rebuild_children = element_damage.contains(LayoutDamage::box_damage()) || + (damage_from_parent.contains(LayoutDamage::box_damage()) && !node.isolates_damage_for_damage_propagation()); if rebuild_children { - damage_for_children.insert(LayoutDamage::rebuild_box_tree()); + damage_for_children.insert(LayoutDamage::box_damage()); } else if damage_for_children.contains(RestyleDamage::RELAYOUT) && !element_damage.contains(RestyleDamage::RELAYOUT) && node.isolates_damage_for_damage_propagation() @@ -150,8 +221,11 @@ pub(crate) fn compute_damage_and_repair_style_inner( let mut damage_from_children = RestyleDamage::empty(); for child in node.children() { if child.is_element() { - damage_from_children |= - compute_damage_and_repair_style_inner(context, child, damage_for_children); + damage_from_children |= compute_damage_and_rebuild_box_tree_inner( + layout_context, + child, + damage_for_children, + ); } } @@ -160,17 +234,32 @@ pub(crate) fn compute_damage_and_repair_style_inner( let mut layout_damage_for_parent = element_and_parent_damage | (damage_from_children & RestyleDamage::RELAYOUT); - if damage_from_children.contains(LayoutDamage::recollect_box_tree_children()) || - element_and_parent_damage.contains(LayoutDamage::recollect_box_tree_children()) - { - // In this case, this node, an ancestor, or a descendant needs to be completely - // rebuilt. That means that this box is no longer valid and also needs to be rebuilt - // (perhaps some of its children do not though). In this case, unset all existing - // boxes for the node and ensure that the appropriate rebuild-type damage propagates - // up the tree. - node.unset_all_boxes(); - layout_damage_for_parent - .insert(LayoutDamage::recollect_box_tree_children() | RestyleDamage::RELAYOUT); + let element_or_ancestors_need_rebuild = + element_and_parent_damage.contains(LayoutDamage::descendant_has_box_damage()); + let descendant_needs_rebuild = + damage_from_children.contains(LayoutDamage::descendant_has_box_damage()); + if element_or_ancestors_need_rebuild || descendant_needs_rebuild { + if element_or_ancestors_need_rebuild || + !node.rebuild_box_tree_from_independent_formatting_context(layout_context) + { + // In this case: + // - this node or an ancestor needs to be completely rebuilt. + // - a descendant needs to be rebuilt, but we are still propagating the rebuild + // damage to an independent formatting context. + // + // This means that this box is no longer valid and also needs to be rebuilt + // (perhaps some of its descendants do not though). In this case, unset all existing + // boxes for the node and ensure that the appropriate rebuild-type damage + // propagates up the tree. + node.unset_all_boxes(); + layout_damage_for_parent + .insert(LayoutDamage::descendant_has_box_damage() | RestyleDamage::RELAYOUT); + } else { + // In this case, we have rebuilt the box tree from this point and we do not + // have to propagate rebuild box tree damage up the tree any further. + layout_damage_for_parent + .insert(RestyleDamage::RELAYOUT | LayoutDamage::recompute_inline_content_sizes()); + } } else { // In this case, this node's boxes are preserved! It's possible that we still need // to run fragment tree layout in this subtree due to an ancestor, this node, or a @@ -191,7 +280,7 @@ pub(crate) fn compute_damage_and_repair_style_inner( // update any preserved layout data structures' style references, if *this* // element's style has changed. if !element_damage.is_empty() { - node.repair_style(context); + node.repair_style(&layout_context.style_context); } } diff --git a/components/script/dom/element.rs b/components/script/dom/element.rs index bcba2c010cc..ead85cf5d81 100644 --- a/components/script/dom/element.rs +++ b/components/script/dom/element.rs @@ -379,7 +379,7 @@ impl Element { doc.note_node_with_dirty_descendants(self.upcast()); restyle .damage - .insert(LayoutDamage::recollect_box_tree_children()); + .insert(LayoutDamage::descendant_has_box_damage()); }, NodeDamage::Other => { doc.note_node_with_dirty_descendants(self.upcast()); diff --git a/components/script/layout_dom/element.rs b/components/script/layout_dom/element.rs index c9c0a971b57..142ae8b6a82 100644 --- a/components/script/layout_dom/element.rs +++ b/components/script/layout_dom/element.rs @@ -731,9 +731,9 @@ impl<'dom> style::dom::TElement for ServoLayoutElement<'dom> { }; if box_tree_needs_rebuild() { - RestyleDamage::from_bits_retain(LayoutDamage::REBUILD_BOX.bits()) + RestyleDamage::from_bits_retain(LayoutDamage::BOX_DAMAGE.bits()) } else if text_shaping_needs_recollect() { - RestyleDamage::from_bits_retain(LayoutDamage::RECOLLECT_BOX_TREE_CHILDREN.bits()) + RestyleDamage::from_bits_retain(LayoutDamage::DESCENDANT_HAS_BOX_DAMAGE.bits()) } else { // This element needs to be laid out again, but does not have any damage to // its box. In the future, we will distinguish between types of damage to the diff --git a/components/shared/layout/layout_damage.rs b/components/shared/layout/layout_damage.rs index ef03c176a26..c54bef02382 100644 --- a/components/shared/layout/layout_damage.rs +++ b/components/shared/layout/layout_damage.rs @@ -10,33 +10,34 @@ bitflags! { /// of `RestyleDamage` from stylo, which only uses the 4 lower bits. #[derive(Clone, Copy, Default, Eq, PartialEq)] pub struct LayoutDamage: u16 { - /// Recollect the box children for this element, because some of the them will be - /// rebuilt. - const RECOLLECT_BOX_TREE_CHILDREN = 0b0111_1111_1111 << 4; /// Clear the cached inline content sizes and recompute them during the next layout. const RECOMPUTE_INLINE_CONTENT_SIZES = 0b1000_0000_0000 << 4; - /// Rebuild the entire box for this element, which means that every part of layout - /// needs to happen again. - const REBUILD_BOX = 0b1111_1111_1111 << 4; + /// Rebuild this box and all of its ancestors. Do not rebuild any children. This + /// is used when a box's content (such as text content) changes or a descendant + /// has box damage ([`Self::BOX_DAMAGE`]). + const DESCENDANT_HAS_BOX_DAMAGE = 0b0111_1111_1111 << 4; + /// Rebuild this box, all of its ancestors and all of its descendants. This is the + /// most a box can be damaged. + const BOX_DAMAGE = 0b1111_1111_1111 << 4; } } impl LayoutDamage { - pub fn recollect_box_tree_children() -> RestyleDamage { - RestyleDamage::from_bits_retain(LayoutDamage::RECOLLECT_BOX_TREE_CHILDREN.bits()) + pub fn descendant_has_box_damage() -> RestyleDamage { + RestyleDamage::from_bits_retain(LayoutDamage::DESCENDANT_HAS_BOX_DAMAGE.bits()) + } + + pub fn box_damage() -> RestyleDamage { + RestyleDamage::from_bits_retain(LayoutDamage::BOX_DAMAGE.bits()) + } + + pub fn needs_new_box(&self) -> bool { + self.contains(Self::DESCENDANT_HAS_BOX_DAMAGE) } pub fn recompute_inline_content_sizes() -> RestyleDamage { RestyleDamage::from_bits_retain(LayoutDamage::RECOMPUTE_INLINE_CONTENT_SIZES.bits()) } - - pub fn rebuild_box_tree() -> RestyleDamage { - RestyleDamage::from_bits_retain(LayoutDamage::REBUILD_BOX.bits()) - } - - pub fn has_box_damage(&self) -> bool { - self.intersects(Self::REBUILD_BOX) - } } impl From for LayoutDamage { @@ -53,9 +54,9 @@ impl From for RestyleDamage { impl std::fmt::Debug for LayoutDamage { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - if self.contains(Self::REBUILD_BOX) { + if self.contains(Self::BOX_DAMAGE) { f.write_str("REBUILD_BOX") - } else if self.contains(Self::RECOLLECT_BOX_TREE_CHILDREN) { + } else if self.contains(Self::DESCENDANT_HAS_BOX_DAMAGE) { f.write_str("RECOLLECT_BOX_TREE_CHILDREN") } else { f.write_str("EMPTY")