mirror of
https://github.com/servo/servo
synced 2026-04-26 01:25:32 +02:00
Add a cache for `SameFormattingContextBlock`s: A new kind of cache is added which allows reusing fragments for `SameFormattingContextBlock` (the majority of block level elements). This can reduce the amount of fragment tree construction during incremental layout (although with a 40 byte overhead per `SameFormattingContextBlock`). Testing: This change adds a new Servo-specific WPT test that verifies that the minimal number of `Fragment`s are rebuilt after an incremental layout. Previously, when running `flexbox-deeply-nested-column-flow.html` from `tests/blink_perf_tests/perf_tests/layout/`, performance would degrade with successive test runs. This is due to how slow inline layout is. This change removes that progressive degradation, though we may be able to remove this cache with improvements to inline layout. Signed-off-by: Martin Robinson <mrobinson@igalia.com> Co-authored-by: Luke Warlow <lwarlow@igalia.com> Co-authored-by: Oriol Brufau <obrufau@igalia.com>
328 lines
14 KiB
Rust
328 lines
14 KiB
Rust
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* 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::fmt::{Debug, Formatter};
|
|
use std::sync::atomic::{AtomicBool, Ordering};
|
|
|
|
use app_units::Au;
|
|
use atomic_refcell::AtomicRefCell;
|
|
use euclid::Point2D;
|
|
use layout_api::LayoutDamage;
|
|
use malloc_size_of_derive::MallocSizeOf;
|
|
use servo_arc::Arc;
|
|
use style::computed_values::position::T as Position;
|
|
use style::logical_geometry::WritingMode;
|
|
use style::properties::ComputedValues;
|
|
use style::values::specified::align::AlignFlags;
|
|
use style_traits::CSSPixel;
|
|
|
|
use crate::context::LayoutContext;
|
|
use crate::dom::{LayoutBox, WeakLayoutBox};
|
|
use crate::flow::CollapsibleWithParentStartMargin;
|
|
use crate::formatting_contexts::Baselines;
|
|
use crate::fragment_tree::{
|
|
BaseFragmentInfo, BoxFragment, CollapsedBlockMargins, Fragment, FragmentStatus,
|
|
SpecificLayoutInfo,
|
|
};
|
|
use crate::geom::LogicalSides1D;
|
|
use crate::positioned::{PositioningContext, relative_adjustement};
|
|
use crate::sizing::{ComputeInlineContentSizes, InlineContentSizesResult, SizeConstraint};
|
|
use crate::{ArcRefCell, ConstraintSpace, ContainingBlock, ContainingBlockSize};
|
|
|
|
/// A box tree node that handles containing information about style and the original DOM
|
|
/// node or pseudo-element that it is based on. This also handles caching of layout values
|
|
/// such as the inline content sizes to avoid recalculating these values during layout
|
|
/// passes.
|
|
///
|
|
/// In the future, this will hold layout results to support incremental layout.
|
|
#[derive(MallocSizeOf)]
|
|
pub(crate) struct LayoutBoxBase {
|
|
pub base_fragment_info: BaseFragmentInfo,
|
|
pub style: Arc<ComputedValues>,
|
|
pub cached_inline_content_size:
|
|
AtomicRefCell<Option<Box<(SizeConstraint, InlineContentSizesResult)>>>,
|
|
pub outer_inline_content_sizes_depend_on_content: AtomicBool,
|
|
pub cached_layout_result: AtomicRefCell<Option<LayoutResultAndInputs>>,
|
|
pub fragments: AtomicRefCell<Vec<Fragment>>,
|
|
pub parent_box: Option<WeakLayoutBox>,
|
|
}
|
|
|
|
impl LayoutBoxBase {
|
|
pub(crate) fn new(base_fragment_info: BaseFragmentInfo, style: Arc<ComputedValues>) -> Self {
|
|
Self {
|
|
base_fragment_info,
|
|
style,
|
|
cached_inline_content_size: AtomicRefCell::default(),
|
|
outer_inline_content_sizes_depend_on_content: AtomicBool::new(true),
|
|
cached_layout_result: AtomicRefCell::default(),
|
|
fragments: AtomicRefCell::default(),
|
|
parent_box: None,
|
|
}
|
|
}
|
|
|
|
/// Get the inline content sizes of a box tree node that extends this [`LayoutBoxBase`], fetch
|
|
/// the result from a cache when possible.
|
|
pub(crate) fn inline_content_sizes(
|
|
&self,
|
|
layout_context: &LayoutContext,
|
|
constraint_space: &ConstraintSpace,
|
|
layout_box: &impl ComputeInlineContentSizes,
|
|
) -> InlineContentSizesResult {
|
|
let mut cache = self.cached_inline_content_size.borrow_mut();
|
|
if let Some(cached_inline_content_size) = cache.as_ref() {
|
|
let (previous_cb_block_size, result) = **cached_inline_content_size;
|
|
if !result.depends_on_block_constraints ||
|
|
previous_cb_block_size == constraint_space.block_size
|
|
{
|
|
return result;
|
|
}
|
|
// TODO: Should we keep multiple caches for various block sizes?
|
|
}
|
|
|
|
let result =
|
|
layout_box.compute_inline_content_sizes_with_fixup(layout_context, constraint_space);
|
|
*cache = Some(Box::new((constraint_space.block_size, result)));
|
|
result
|
|
}
|
|
|
|
pub(crate) fn fragments(&self) -> Vec<Fragment> {
|
|
self.fragments.borrow().clone()
|
|
}
|
|
|
|
pub(crate) fn add_fragment(&self, fragment: Fragment) {
|
|
self.fragments.borrow_mut().push(fragment);
|
|
}
|
|
|
|
pub(crate) fn set_fragment(&self, fragment: Fragment) {
|
|
*self.fragments.borrow_mut() = vec![fragment];
|
|
}
|
|
|
|
pub(crate) fn clear_fragments(&self) {
|
|
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<ComputedValues>) {
|
|
self.style = new_style.clone();
|
|
for fragment in self.fragments.borrow_mut().iter_mut() {
|
|
if let Some(mut base) = fragment.base_mut() {
|
|
base.repair_style(new_style);
|
|
}
|
|
}
|
|
}
|
|
|
|
#[expect(unused)]
|
|
pub(crate) fn parent_box(&self) -> Option<LayoutBox> {
|
|
self.parent_box.as_ref().and_then(WeakLayoutBox::upgrade)
|
|
}
|
|
|
|
pub(crate) fn add_damage(
|
|
&self,
|
|
element_damage: LayoutDamage,
|
|
damage_from_children: LayoutDamage,
|
|
) -> LayoutDamage {
|
|
self.clear_fragments_and_fragment_cache();
|
|
|
|
if !element_damage.is_empty() ||
|
|
damage_from_children.contains(LayoutDamage::RECOMPUTE_INLINE_CONTENT_SIZES)
|
|
{
|
|
*self.cached_inline_content_size.borrow_mut() = None;
|
|
}
|
|
|
|
let mut damage_for_parent = element_damage | damage_from_children;
|
|
|
|
// When a block container has a mix of inline-level and block-level contents, the
|
|
// inline-level ones are wrapped inside an anonymous block associated with the
|
|
// block container. The anonymous block has an `auto` size, so its intrinsic
|
|
// contribution depends on content, but it can't affect the intrinsic size of
|
|
// ancestors if the block container is sized extrinsically.
|
|
//
|
|
// If the intrinsic contributions of this node depend on content, we will need to
|
|
// clear the cached intrinsic sizes of the parent. But if the contributions are
|
|
// purely extrinsic, then the intrinsic sizes of the ancestors won't be affected,
|
|
// and we can keep the cache.
|
|
damage_for_parent.set(
|
|
LayoutDamage::RECOMPUTE_INLINE_CONTENT_SIZES,
|
|
!element_damage.is_empty() ||
|
|
(!self.base_fragment_info.is_anonymous() &&
|
|
self.outer_inline_content_sizes_depend_on_content
|
|
.load(Ordering::Relaxed)),
|
|
);
|
|
|
|
damage_for_parent
|
|
}
|
|
|
|
pub(crate) fn cached_same_formatting_context_block_if_applicable(
|
|
&self,
|
|
containing_block: &ContainingBlock,
|
|
collapsible_with_parent_start_margin: Option<CollapsibleWithParentStartMargin>,
|
|
ignore_block_margins_for_stretch: LogicalSides1D<bool>,
|
|
has_inline_parent: bool,
|
|
) -> Option<ArcRefCell<BoxFragment>> {
|
|
let mut cached_layout_result = self.cached_layout_result.borrow_mut();
|
|
let Some(LayoutResultAndInputs::SameFormattingContextBlock(result)) =
|
|
&mut *cached_layout_result
|
|
else {
|
|
return None;
|
|
};
|
|
|
|
if result.containing_block_size != containing_block.size ||
|
|
result.containing_block_writing_mode != containing_block.style.writing_mode ||
|
|
result.containing_block_justify_items !=
|
|
containing_block.style.clone_justify_items().computed.0.0 ||
|
|
result.collapsible_with_parent_start_margin != collapsible_with_parent_start_margin ||
|
|
result.ignore_block_margins_for_stretch != ignore_block_margins_for_stretch ||
|
|
result.has_inline_parent != has_inline_parent
|
|
{
|
|
return None;
|
|
}
|
|
|
|
let fragment = result.result.fragment.clone();
|
|
{
|
|
let mut borrowed_fragment = fragment.borrow_mut();
|
|
|
|
// Ideally when the final position doesn't change, this wouldn't be set, but we have
|
|
// no way currently to track whether the final position wil differ from the one set in
|
|
// the cached fragment. Final positioning is done in the containing block and depends
|
|
// on things like margins and the size of siblings.
|
|
borrowed_fragment.base.status = FragmentStatus::PositionMaybeChanged;
|
|
|
|
borrowed_fragment.base.rect.origin = result.result.original_offset;
|
|
if self.style.clone_position() == Position::Relative {
|
|
borrowed_fragment.base.rect.origin +=
|
|
relative_adjustement(&self.style, containing_block)
|
|
.to_physical_vector(containing_block.style.writing_mode)
|
|
}
|
|
}
|
|
|
|
Some(fragment)
|
|
}
|
|
|
|
pub(crate) fn cache_same_formatting_context_block_layout(
|
|
&self,
|
|
containing_block: &ContainingBlock,
|
|
collapsible_with_parent_start_margin: Option<CollapsibleWithParentStartMargin>,
|
|
ignore_block_margins_for_stretch: LogicalSides1D<bool>,
|
|
has_inline_parent: bool,
|
|
fragment: ArcRefCell<BoxFragment>,
|
|
) {
|
|
let mut original_offset;
|
|
{
|
|
let borrowed_fragment = fragment.borrow();
|
|
original_offset = borrowed_fragment.content_rect().origin;
|
|
if self.style.clone_position() == Position::Relative {
|
|
original_offset -= relative_adjustement(&self.style, containing_block)
|
|
.to_physical_vector(containing_block.style.writing_mode)
|
|
}
|
|
}
|
|
|
|
*self.cached_layout_result.borrow_mut() =
|
|
Some(LayoutResultAndInputs::SameFormattingContextBlock(Box::new(
|
|
SameFormattingContextBlockLayoutResultAndInputs {
|
|
result: SameFormattingContextBlockLayoutResult {
|
|
fragment,
|
|
original_offset,
|
|
},
|
|
containing_block_size: containing_block.size.clone(),
|
|
containing_block_writing_mode: containing_block.style.writing_mode,
|
|
containing_block_justify_items: containing_block
|
|
.style
|
|
.clone_justify_items()
|
|
.computed
|
|
.0
|
|
.0,
|
|
collapsible_with_parent_start_margin,
|
|
ignore_block_margins_for_stretch,
|
|
has_inline_parent,
|
|
},
|
|
)));
|
|
}
|
|
}
|
|
|
|
impl Debug for LayoutBoxBase {
|
|
fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> {
|
|
f.debug_struct("LayoutBoxBase").finish()
|
|
}
|
|
}
|
|
|
|
#[derive(MallocSizeOf)]
|
|
pub(crate) enum LayoutResultAndInputs {
|
|
IndependentFormattingContext(Box<IndependentFormattingContextLayoutResultAndInputs>),
|
|
SameFormattingContextBlock(Box<SameFormattingContextBlockLayoutResultAndInputs>),
|
|
}
|
|
|
|
#[derive(Clone, MallocSizeOf)]
|
|
pub(crate) struct IndependentFormattingContextLayoutResult {
|
|
pub fragments: Vec<Fragment>,
|
|
|
|
/// <https://drafts.csswg.org/css2/visudet.html#root-height>
|
|
pub content_block_size: Au,
|
|
|
|
/// If this layout is for a block container, this tracks the collapsable size
|
|
/// of start and end margins and whether or not the block container collapsed through.
|
|
pub collapsible_margins_in_children: CollapsedBlockMargins,
|
|
|
|
/// The contents of a table may force it to become wider than what we would expect
|
|
/// from 'width' and 'min-width'. This is the resulting inline content size,
|
|
/// or None for non-table layouts.
|
|
pub content_inline_size_for_table: Option<Au>,
|
|
|
|
/// The offset of the last inflow baseline of this layout in the content area, if
|
|
/// there was one. This is used to propagate baselines to the ancestors of `display:
|
|
/// inline-block`.
|
|
pub baselines: Baselines,
|
|
|
|
/// Whether or not this layout depends on the containing block size.
|
|
pub depends_on_block_constraints: bool,
|
|
|
|
/// Additional information of this layout that could be used by Javascripts and devtools.
|
|
pub specific_layout_info: Option<SpecificLayoutInfo>,
|
|
}
|
|
|
|
/// A collection of layout inputs and a cached layout result for an IndependentFormattingContext for
|
|
/// use in [`LayoutBoxBase`].
|
|
#[derive(MallocSizeOf)]
|
|
pub(crate) struct IndependentFormattingContextLayoutResultAndInputs {
|
|
/// The [`IndependentFormattingContextLayoutResult`] for this layout.
|
|
pub result: IndependentFormattingContextLayoutResult,
|
|
|
|
/// The [`ContainingBlockSize`] to use for this box's contents, but not
|
|
/// for the box itself.
|
|
pub containing_block_for_children_size: ContainingBlockSize,
|
|
|
|
/// A [`PositioningContext`] holding absolutely-positioned descendants
|
|
/// collected during the layout of this box.
|
|
pub positioning_context: PositioningContext,
|
|
}
|
|
|
|
#[derive(Clone, MallocSizeOf)]
|
|
pub(crate) struct SameFormattingContextBlockLayoutResult {
|
|
pub fragment: ArcRefCell<BoxFragment>,
|
|
original_offset: Point2D<Au, CSSPixel>,
|
|
}
|
|
|
|
/// A collection of layout inputs and a cached layout result for a SameFormattingContextBlock for
|
|
/// use in [`LayoutBoxBase`].
|
|
#[derive(MallocSizeOf)]
|
|
pub(crate) struct SameFormattingContextBlockLayoutResultAndInputs {
|
|
pub result: SameFormattingContextBlockLayoutResult,
|
|
/// The [`ContainingBlockSize`] used when this block was laid out.
|
|
pub containing_block_size: ContainingBlockSize,
|
|
/// The containing block's [`WritingMode`] used when this block was laid out.
|
|
pub containing_block_writing_mode: WritingMode,
|
|
/// The containing block's `justify-items` [`AlignFlags`] used when this block was laid out.
|
|
pub containing_block_justify_items: AlignFlags,
|
|
/// Whether or not the margin in this block was collapsible with the parent's start margin
|
|
/// when this block was laid out.
|
|
collapsible_with_parent_start_margin: Option<CollapsibleWithParentStartMargin>,
|
|
/// Whether or not block margins were ignored for stretch when this block was laid out.
|
|
ignore_block_margins_for_stretch: LogicalSides1D<bool>,
|
|
/// Whether or not this block had an inline parent.
|
|
has_inline_parent: bool,
|
|
}
|