diff --git a/Libraries/LibGfx/Font/Font.cpp b/Libraries/LibGfx/Font/Font.cpp index 8e9ed26fdce27..4247a5d006b75 100644 --- a/Libraries/LibGfx/Font/Font.cpp +++ b/Libraries/LibGfx/Font/Font.cpp @@ -31,6 +31,8 @@ float ladybird_gfx_font_glyph_width(void const*, u32); u32 ladybird_gfx_font_glyph_id(void const*, u32); bool ladybird_gfx_font_contains_glyph(void const*, u32); bool ladybird_gfx_font_is_emoji_font(void const*); +void ladybird_gfx_font_ref(void const*); +void ladybird_gfx_font_unref(void const*); } namespace Gfx { @@ -274,3 +276,15 @@ extern "C" bool ladybird_gfx_font_is_emoji_font(void const* font) VERIFY(font); return static_cast(font)->is_emoji_font(); } + +extern "C" void ladybird_gfx_font_ref(void const* font) +{ + VERIFY(font); + static_cast(font)->ref(); +} + +extern "C" void ladybird_gfx_font_unref(void const* font) +{ + VERIFY(font); + static_cast(font)->unref(); +} diff --git a/Libraries/LibGfx/Path.cpp b/Libraries/LibGfx/Path.cpp index a154df40ec9fb..5c1892d985f14 100644 --- a/Libraries/LibGfx/Path.cpp +++ b/Libraries/LibGfx/Path.cpp @@ -8,9 +8,11 @@ #include #include #include +#include extern "C" { void ladybird_gfx_path_destroy(void*); +bool ladybird_gfx_path_equals(void const*, void const*); } namespace Gfx { @@ -52,3 +54,10 @@ extern "C" void ladybird_gfx_path_destroy(void* path) { delete static_cast(path); } + +extern "C" bool ladybird_gfx_path_equals(void const* a, void const* b) +{ + auto const& path_a = *static_cast(a); + auto const& path_b = *static_cast(b); + return static_cast(path_a.impl()).sk_path() == static_cast(path_b.impl()).sk_path(); +} diff --git a/Libraries/LibGfx/Rust/src/font.rs b/Libraries/LibGfx/Rust/src/font.rs index 255d369332e26..0f916f924fc72 100644 --- a/Libraries/LibGfx/Rust/src/font.rs +++ b/Libraries/LibGfx/Rust/src/font.rs @@ -21,6 +21,8 @@ unsafe extern "C" { forced_presentation: bool, ) -> *const c_void; fn ladybird_gfx_font_cascade_list_first(list: *const c_void) -> *const c_void; + fn ladybird_gfx_font_ref(font: *const c_void); + fn ladybird_gfx_font_unref(font: *const c_void); fn ladybird_gfx_font_cascade_list_ref(list: *const c_void); fn ladybird_gfx_font_cascade_list_unref(list: *const c_void); fn ladybird_gfx_emoji_presentation_for_code_point( @@ -163,33 +165,54 @@ impl<'a> FontCascadeListRef<'a> { } } -/// A strong reference to a `Gfx::FontCascadeList`, keeping the list and every -/// font it can resolve alive until dropped. -pub struct RetainedFontCascadeList { - raw: NonNull, -} +/// Generates a strong-reference handle over a C++ ref/unref FFI pair: +/// retain-on-construct, release-on-drop. +macro_rules! retained_ffi_handle { + ($(#[$documentation:meta])* $name:ident, $ref_function:ident, $unref_function:ident, $type_name:literal) => { + $(#[$documentation])* + pub struct $name { + raw: NonNull, + } -impl RetainedFontCascadeList { - /// # Safety - /// - /// `raw` must point to a live `Gfx::FontCascadeList` at the time of the - /// call. - pub unsafe fn retain(raw: *const c_void) -> Self { - let raw = NonNull::new(raw.cast_mut()).expect("Gfx::FontCascadeList pointer must not be null"); - // SAFETY: The caller guarantees the list is live, and ref() keeps it - // that way until this reference drops. - unsafe { ladybird_gfx_font_cascade_list_ref(raw.as_ptr()) }; - Self { raw } - } + impl $name { + /// # Safety + /// + /// `raw` must point to a live object at the time of the call. + pub unsafe fn retain(raw: *const c_void) -> Self { + let raw = NonNull::new(raw.cast_mut()).expect(concat!($type_name, " pointer must not be null")); + // SAFETY: The caller guarantees the object is live, and the + // reference taken here keeps it that way until drop. + unsafe { $ref_function(raw.as_ptr()) }; + Self { raw } + } + + pub fn as_raw(&self) -> *const c_void { + self.raw.as_ptr() + } + } - pub fn as_raw(&self) -> *const c_void { - self.raw.as_ptr() - } + impl Drop for $name { + fn drop(&mut self) { + // SAFETY: retain() took a strong reference on construction. + unsafe { $unref_function(self.raw.as_ptr()) }; + } + } + }; } -impl Drop for RetainedFontCascadeList { - fn drop(&mut self) { - // SAFETY: retain() took a strong reference on construction. - unsafe { ladybird_gfx_font_cascade_list_unref(self.raw.as_ptr()) }; - } -} +retained_ffi_handle!( + /// A strong reference to a single `Gfx::Font`, keeping it alive until dropped. + RetainedFont, + ladybird_gfx_font_ref, + ladybird_gfx_font_unref, + "Gfx::Font" +); + +retained_ffi_handle!( + /// A strong reference to a `Gfx::FontCascadeList`, keeping the list and every + /// font it can resolve alive until dropped. + RetainedFontCascadeList, + ladybird_gfx_font_cascade_list_ref, + ladybird_gfx_font_cascade_list_unref, + "Gfx::FontCascadeList" +); diff --git a/Libraries/LibGfx/Rust/src/path.rs b/Libraries/LibGfx/Rust/src/path.rs index f667b1fdafebe..6a92174dc3e08 100644 --- a/Libraries/LibGfx/Rust/src/path.rs +++ b/Libraries/LibGfx/Rust/src/path.rs @@ -9,11 +9,13 @@ use std::ptr::NonNull; unsafe extern "C" { fn ladybird_gfx_path_destroy(path: *mut c_void); + fn ladybird_gfx_path_equals(a: *const c_void, b: *const c_void) -> bool; } /// The sole owner of a heap-allocated `Gfx::Path`, destroying it on drop. pub struct OwnedPath { raw: NonNull, + identity: u64, } impl OwnedPath { @@ -25,8 +27,10 @@ impl OwnedPath { /// `Gfx::Path` is owned or destroyed. #[inline] pub unsafe fn adopt(raw: *mut c_void) -> Self { + static NEXT_IDENTITY: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1); Self { raw: NonNull::new(raw).expect("Gfx::Path pointer must not be null"), + identity: NEXT_IDENTITY.fetch_add(1, std::sync::atomic::Ordering::Relaxed), } } @@ -35,6 +39,22 @@ impl OwnedPath { pub fn as_raw(&self) -> *mut c_void { self.raw.as_ptr() } + + /// A process-unique, never-reused identity for this path allocation, so + /// consumers holding a copied snapshot can recognize an unchanged path + /// without comparing contents. + #[inline] + pub fn identity(&self) -> u64 { + self.identity + } +} + +impl PartialEq for OwnedPath { + fn eq(&self, other: &Self) -> bool { + self.identity == other.identity + // SAFETY: Both sides own live heap-allocated paths for the duration of the call. + || unsafe { ladybird_gfx_path_equals(self.raw.as_ptr(), other.raw.as_ptr()) } + } } impl Drop for OwnedPath { diff --git a/Libraries/LibWeb/CSS/ComputedProperties.cpp b/Libraries/LibWeb/CSS/ComputedProperties.cpp index c5f2b3062fc9b..70a8735ccb8f4 100644 --- a/Libraries/LibWeb/CSS/ComputedProperties.cpp +++ b/Libraries/LibWeb/CSS/ComputedProperties.cpp @@ -131,6 +131,38 @@ RefPtr ComputedValues::background_color_style_value() const static_assert(to_underlying(PseudoElement::KnownPseudoElementCount) <= sizeof(u64) * 8); +static bool style_value_contains_anchor_function(StyleValue const& value) +{ + if (value.is_anchor()) + return true; + if (value.is_calculated()) + return value.as_calculated().contains_anchor_function(); + return false; +} + +bool ComputedValues::inset_properties_contain_anchor_functions() const +{ + // A bare anchor function is not stored in the inset length box at all: it lives in the + // per-side anchor inset handles kept next to it. + if (has_anchor_inset(PropertyID::Top) || has_anchor_inset(PropertyID::Right) + || has_anchor_inset(PropertyID::Bottom) || has_anchor_inset(PropertyID::Left)) + return true; + // Anchor functions inside expressions survive to used-value time as calculated values, so + // when no inset is calculated (the common case), skip reconstructing the style values. + auto const& inset_box = inset(); + if (!inset_box.top().is_calculated() && !inset_box.right().is_calculated() && !inset_box.bottom().is_calculated() && !inset_box.left().is_calculated()) + return false; + auto top = computed_style_value(PropertyID::Top); + auto right = computed_style_value(PropertyID::Right); + auto bottom = computed_style_value(PropertyID::Bottom); + auto left = computed_style_value(PropertyID::Left); + VERIFY(top && right && bottom && left); + return style_value_contains_anchor_function(*top) + || style_value_contains_anchor_function(*right) + || style_value_contains_anchor_function(*bottom) + || style_value_contains_anchor_function(*left); +} + RefPtr ComputedValues::computed_style_value(PropertyID property_id, WithAnimationsApplied with_animations_applied) const { if (with_animations_applied == WithAnimationsApplied::No && m_base_values) diff --git a/Libraries/LibWeb/CSS/ComputedValues.cpp b/Libraries/LibWeb/CSS/ComputedValues.cpp index 1b8b75267eed8..6bd61a09038c0 100644 --- a/Libraries/LibWeb/CSS/ComputedValues.cpp +++ b/Libraries/LibWeb/CSS/ComputedValues.cpp @@ -751,7 +751,8 @@ void const* style_group_default_payload(size_t group_index) static auto const default_payloads = [] { constexpr auto group_count = to_underlying(StyleGroupIndex::Count); Array vtables; -#define LIBWEB_STYLE_GROUP_VTABLE(name) vtables[to_underlying(StyleGroupIndex::name)] = make_style_group_vtable(); +#define LIBWEB_STYLE_GROUP_VTABLE(name, path, sharing_name, affects_layout) \ + vtables[to_underlying(StyleGroupIndex::name)] = make_style_group_vtable(); LIBWEB_ENUMERATE_COMPUTED_VALUE_STYLE_GROUPS(LIBWEB_STYLE_GROUP_VTABLE) #undef LIBWEB_STYLE_GROUP_VTABLE Array payloads {}; @@ -853,34 +854,27 @@ bool ComputedValues::adopt_identical_group_payloads(ComputedValues const& previo } all_shared = false; }; -#define LIBWEB_ADOPT_STYLE_GROUP(path) adopt(path, previous.path); - LIBWEB_ADOPT_STYLE_GROUP(m_inherited.table) - LIBWEB_ADOPT_STYLE_GROUP(m_inherited.list) - LIBWEB_ADOPT_STYLE_GROUP(m_inherited.ui) - LIBWEB_ADOPT_STYLE_GROUP(m_inherited.svg) - LIBWEB_ADOPT_STYLE_GROUP(m_inherited.text) - LIBWEB_ADOPT_STYLE_GROUP(m_inherited.box) - LIBWEB_ADOPT_STYLE_GROUP(m_inherited.font) - LIBWEB_ADOPT_STYLE_GROUP(m_noninherited.animation) - LIBWEB_ADOPT_STYLE_GROUP(m_noninherited.box) - LIBWEB_ADOPT_STYLE_GROUP(m_noninherited.surround) - LIBWEB_ADOPT_STYLE_GROUP(m_noninherited.sizing) - LIBWEB_ADOPT_STYLE_GROUP(m_noninherited.misc) - LIBWEB_ADOPT_STYLE_GROUP(m_noninherited.alignment) - LIBWEB_ADOPT_STYLE_GROUP(m_noninherited.border) - LIBWEB_ADOPT_STYLE_GROUP(m_noninherited.background) - LIBWEB_ADOPT_STYLE_GROUP(m_noninherited.transform) - LIBWEB_ADOPT_STYLE_GROUP(m_noninherited.effects) - LIBWEB_ADOPT_STYLE_GROUP(m_noninherited.mask_data) - LIBWEB_ADOPT_STYLE_GROUP(m_noninherited.text_reset) - LIBWEB_ADOPT_STYLE_GROUP(m_noninherited.content_data) - LIBWEB_ADOPT_STYLE_GROUP(m_noninherited.anchor) - LIBWEB_ADOPT_STYLE_GROUP(m_noninherited.grid) - LIBWEB_ADOPT_STYLE_GROUP(m_noninherited.svg_reset) +#define LIBWEB_ADOPT_STYLE_GROUP(name, path, sharing_name, affects_layout) adopt(path, previous.path); + LIBWEB_ENUMERATE_COMPUTED_VALUE_STYLE_GROUPS(LIBWEB_ADOPT_STYLE_GROUP) #undef LIBWEB_ADOPT_STYLE_GROUP return all_shared; } +bool ComputedValues::differs_in_any_layout_affecting_group_payload_from(ComputedValues const& other) const +{ + auto differs = [](StyleStructRef const& mine, StyleStructRef const& theirs) { + return !mine.ptr_equals(theirs) && !(mine == theirs); + }; +#define LIBWEB_COMPARE_STYLE_GROUP(name, path, sharing_name, affects_layout) \ + if constexpr (affects_layout) { \ + if (differs(path, other.path)) \ + return true; \ + } + LIBWEB_ENUMERATE_COMPUTED_VALUE_STYLE_GROUPS(LIBWEB_COMPARE_STYLE_GROUP) +#undef LIBWEB_COMPARE_STYLE_GROUP + return false; +} + // https://drafts.csswg.org/css-transforms-2/#grouping-property-values bool ComputedValues::has_transform_style_grouping_property() const { @@ -935,52 +929,11 @@ bool ComputedValues::has_transform_style_grouping_property() const void const* ComputedValues::style_group_payload(StyleGroupIndex group) const { switch (group) { - case StyleGroupIndex::InheritedTableValues: - return &*m_inherited.table; - case StyleGroupIndex::InheritedListValues: - return &*m_inherited.list; - case StyleGroupIndex::InheritedUIValues: - return &*m_inherited.ui; - case StyleGroupIndex::InheritedSVGValues: - return &*m_inherited.svg; - case StyleGroupIndex::InheritedTextValues: - return &*m_inherited.text; - case StyleGroupIndex::InheritedBoxValues: - return &*m_inherited.box; - case StyleGroupIndex::FontValues: - return &*m_inherited.font; - case StyleGroupIndex::AnimationValues: - return &*m_noninherited.animation; - case StyleGroupIndex::SVGResetValues: - return &*m_noninherited.svg_reset; - case StyleGroupIndex::GridValues: - return &*m_noninherited.grid; - case StyleGroupIndex::AnchorValues: - return &*m_noninherited.anchor; - case StyleGroupIndex::EffectsValues: - return &*m_noninherited.effects; - case StyleGroupIndex::MaskValues: - return &*m_noninherited.mask_data; - case StyleGroupIndex::TextResetValues: - return &*m_noninherited.text_reset; - case StyleGroupIndex::ContentValues: - return &*m_noninherited.content_data; - case StyleGroupIndex::TransformValues: - return &*m_noninherited.transform; - case StyleGroupIndex::BackgroundValues: - return &*m_noninherited.background; - case StyleGroupIndex::BorderValues: - return &*m_noninherited.border; - case StyleGroupIndex::AlignmentValues: - return &*m_noninherited.alignment; - case StyleGroupIndex::MiscResetValues: - return &*m_noninherited.misc; - case StyleGroupIndex::SizingValues: - return &*m_noninherited.sizing; - case StyleGroupIndex::SurroundValues: - return &*m_noninherited.surround; - case StyleGroupIndex::BoxValues: - return &*m_noninherited.box; +#define LIBWEB_STYLE_GROUP_PAYLOAD_CASE(name, path, sharing_name, affects_layout) \ + case StyleGroupIndex::name: \ + return &*path; + LIBWEB_ENUMERATE_COMPUTED_VALUE_STYLE_GROUPS(LIBWEB_STYLE_GROUP_PAYLOAD_CASE) +#undef LIBWEB_STYLE_GROUP_PAYLOAD_CASE case StyleGroupIndex::Count: break; } diff --git a/Libraries/LibWeb/CSS/ComputedValues.h b/Libraries/LibWeb/CSS/ComputedValues.h index 697883c3d1f78..73d0450e55e9b 100644 --- a/Libraries/LibWeb/CSS/ComputedValues.h +++ b/Libraries/LibWeb/CSS/ComputedValues.h @@ -896,34 +896,42 @@ inline Gfx::InterpolationColorSpace to_interpolation_color_space(ColorInterpolat VERIFY_NOT_REACHED(); } -// The identity of every ComputedValues style value group, in vtable registration order. -#define LIBWEB_ENUMERATE_COMPUTED_VALUE_STYLE_GROUPS(G) \ - G(InheritedTableValues) \ - G(InheritedListValues) \ - G(InheritedUIValues) \ - G(InheritedSVGValues) \ - G(InheritedTextValues) \ - G(InheritedBoxValues) \ - G(FontValues) \ - G(AnimationValues) \ - G(SVGResetValues) \ - G(GridValues) \ - G(AnchorValues) \ - G(EffectsValues) \ - G(MaskValues) \ - G(TextResetValues) \ - G(ContentValues) \ - G(TransformValues) \ - G(BackgroundValues) \ - G(BorderValues) \ - G(AlignmentValues) \ - G(MiscResetValues) \ - G(SizingValues) \ - G(SurroundValues) \ - G(BoxValues) +// Every ComputedValues style value group, in vtable registration order: +// G(enumerator, member path, sharing-info name, affects layout). +// A group may be flagged as not affecting layout only when every one of its fields is +// read exclusively at paint or display-list build time: background qualifies (paint; +// resource-observer registration runs in apply_style regardless of layout), mask +// qualifies (paint and hit-testing), and text_reset qualifies (text-decoration resolves +// at display-list build; white-space-trim is unread by layout today, so implementing it +// must revisit the flag). effects stays layout-affecting because filter/backdrop-filter +// establish fixed-positioning containing blocks and re-parent abspos descendants. +#define LIBWEB_ENUMERATE_COMPUTED_VALUE_STYLE_GROUPS(G) \ + G(InheritedTableValues, m_inherited.table, "inheritedTable", true) \ + G(InheritedListValues, m_inherited.list, "inheritedList", true) \ + G(InheritedUIValues, m_inherited.ui, "inheritedUI", true) \ + G(InheritedSVGValues, m_inherited.svg, "inheritedSVG", true) \ + G(InheritedTextValues, m_inherited.text, "inheritedText", true) \ + G(InheritedBoxValues, m_inherited.box, "inheritedBox", true) \ + G(FontValues, m_inherited.font, "font", true) \ + G(AnimationValues, m_noninherited.animation, "animation", true) \ + G(SVGResetValues, m_noninherited.svg_reset, "svgReset", true) \ + G(GridValues, m_noninherited.grid, "grid", true) \ + G(AnchorValues, m_noninherited.anchor, "anchor", true) \ + G(EffectsValues, m_noninherited.effects, "effects", true) \ + G(MaskValues, m_noninherited.mask_data, "mask", false) \ + G(TextResetValues, m_noninherited.text_reset, "textReset", false) \ + G(ContentValues, m_noninherited.content_data, "content", true) \ + G(TransformValues, m_noninherited.transform, "transform", true) \ + G(BackgroundValues, m_noninherited.background, "background", false) \ + G(BorderValues, m_noninherited.border, "border", true) \ + G(AlignmentValues, m_noninherited.alignment, "alignment", true) \ + G(MiscResetValues, m_noninherited.misc, "miscReset", true) \ + G(SizingValues, m_noninherited.sizing, "sizing", true) \ + G(SurroundValues, m_noninherited.surround, "surround", true) \ + G(BoxValues, m_noninherited.box, "box", true) enum class StyleGroupIndex : size_t { -#define LIBWEB_STYLE_GROUP_ENUMERATOR(name) name, +#define LIBWEB_STYLE_GROUP_ENUMERATOR(name, ...) name, LIBWEB_ENUMERATE_COMPUTED_VALUE_STYLE_GROUPS(LIBWEB_STYLE_GROUP_ENUMERATOR) #undef LIBWEB_STYLE_GROUP_ENUMERATOR Count, @@ -1049,6 +1057,13 @@ class WEB_API ComputedValues final : public RefCounted { AnimatedProperties const* animated_properties() const { return m_animated_properties.ptr(); } RefPtr animated_properties_snapshot() const; + // Animated values live outside the group payloads, so every group-based fast path or + // group-based diff must fall back to the slow path when either side carries them. + static bool either_carries_animated_overlay(ComputedValues const& a, ComputedValues const& b) + { + return a.has_animated_values() || b.has_animated_values() || a.animated_properties() || b.animated_properties(); + } + struct Statistics { u64 live_instance_count { 0 }; u64 total_instances_created { 0 }; @@ -1061,6 +1076,7 @@ class WEB_API ComputedValues final : public RefCounted { // restyled element keep sharing storage across style generations. Returns true when every // group ends up sharing its payload with `previous`. bool adopt_identical_group_payloads(ComputedValues const& previous) const; + bool differs_in_any_layout_affecting_group_payload_from(ComputedValues const& other) const; bool has_transform_style_grouping_property() const; @@ -1075,30 +1091,9 @@ class WEB_API ComputedValues final : public RefCounted { template void for_each_style_group_sharing_state(ComputedValues const* parent, Callback callback) const { -#define LIBWEB_VISIT_STYLE_GROUP(name, path) callback(name##sv, parent ? path.ptr_equals(parent->path) : false, path.is_default()); - LIBWEB_VISIT_STYLE_GROUP("inheritedTable", m_inherited.table) - LIBWEB_VISIT_STYLE_GROUP("inheritedList", m_inherited.list) - LIBWEB_VISIT_STYLE_GROUP("inheritedUI", m_inherited.ui) - LIBWEB_VISIT_STYLE_GROUP("inheritedSVG", m_inherited.svg) - LIBWEB_VISIT_STYLE_GROUP("inheritedText", m_inherited.text) - LIBWEB_VISIT_STYLE_GROUP("inheritedBox", m_inherited.box) - LIBWEB_VISIT_STYLE_GROUP("font", m_inherited.font) - LIBWEB_VISIT_STYLE_GROUP("animation", m_noninherited.animation) - LIBWEB_VISIT_STYLE_GROUP("box", m_noninherited.box) - LIBWEB_VISIT_STYLE_GROUP("surround", m_noninherited.surround) - LIBWEB_VISIT_STYLE_GROUP("sizing", m_noninherited.sizing) - LIBWEB_VISIT_STYLE_GROUP("miscReset", m_noninherited.misc) - LIBWEB_VISIT_STYLE_GROUP("alignment", m_noninherited.alignment) - LIBWEB_VISIT_STYLE_GROUP("border", m_noninherited.border) - LIBWEB_VISIT_STYLE_GROUP("background", m_noninherited.background) - LIBWEB_VISIT_STYLE_GROUP("transform", m_noninherited.transform) - LIBWEB_VISIT_STYLE_GROUP("effects", m_noninherited.effects) - LIBWEB_VISIT_STYLE_GROUP("mask", m_noninherited.mask_data) - LIBWEB_VISIT_STYLE_GROUP("textReset", m_noninherited.text_reset) - LIBWEB_VISIT_STYLE_GROUP("content", m_noninherited.content_data) - LIBWEB_VISIT_STYLE_GROUP("anchor", m_noninherited.anchor) - LIBWEB_VISIT_STYLE_GROUP("grid", m_noninherited.grid) - LIBWEB_VISIT_STYLE_GROUP("svgReset", m_noninherited.svg_reset) +#define LIBWEB_VISIT_STYLE_GROUP(name, path, sharing_name, affects_layout) \ + callback(sharing_name##sv, parent ? path.ptr_equals(parent->path) : false, path.is_default()); + LIBWEB_ENUMERATE_COMPUTED_VALUE_STYLE_GROUPS(LIBWEB_VISIT_STYLE_GROUP) #undef LIBWEB_VISIT_STYLE_GROUP } @@ -1371,25 +1366,17 @@ class WEB_API ComputedValues final : public RefCounted { ShapeRendering shape_rendering() const { return static_cast(m_noninherited.svg_reset->shape_rendering); } LengthBox inset() const { return length_box(m_noninherited.surround->inset); } + bool has_anchor_inset(PropertyID property_id) const + { + auto const* handle = anchor_inset_handle(property_id); + return handle && handle->pointer != nullptr; + } + bool inset_properties_contain_anchor_functions() const; RefPtr anchor_inset(PropertyID property_id) const { - ComputedValuesFFI::ComputedStyleValueHandle const* handle = nullptr; - switch (property_id) { - case PropertyID::Top: - handle = &m_noninherited.surround->top_anchor_inset; - break; - case PropertyID::Right: - handle = &m_noninherited.surround->right_anchor_inset; - break; - case PropertyID::Bottom: - handle = &m_noninherited.surround->bottom_anchor_inset; - break; - case PropertyID::Left: - handle = &m_noninherited.surround->left_anchor_inset; - break; - default: + auto const* handle = anchor_inset_handle(property_id); + if (!handle) return {}; - } static_assert(sizeof(RustStyleValueHandle) == sizeof(*handle)); return style_value_from_handle(property_id, reinterpret_cast(*handle)); } @@ -1530,6 +1517,22 @@ class WEB_API ComputedValues final : public RefCounted { RefPtr style_value_from_handle(PropertyID, RustStyleValueHandle const&) const; + ComputedValuesFFI::ComputedStyleValueHandle const* anchor_inset_handle(PropertyID property_id) const + { + switch (property_id) { + case PropertyID::Top: + return &m_noninherited.surround->top_anchor_inset; + case PropertyID::Right: + return &m_noninherited.surround->right_anchor_inset; + case PropertyID::Bottom: + return &m_noninherited.surround->bottom_anchor_inset; + case PropertyID::Left: + return &m_noninherited.surround->left_anchor_inset; + default: + return nullptr; + } + } + static LengthPercentageOrAuto length_percentage_or_auto(ComputedValuesFFI::ComputedLengthPercentageOrAuto const& value) { if (value.is_auto) @@ -3170,9 +3173,11 @@ class ComputedValues::Mutator final { } void copy_grid_placements_from(ComputedValues const& source) { - ComputedValuesFFI::rust_grid_values_copy_placements( - static_cast(source.m_noninherited.grid.operator->()), - &m_values.m_noninherited.grid.access()); + auto const* source_grid = static_cast(source.m_noninherited.grid.operator->()); + auto const* current_grid = static_cast(m_values.m_noninherited.grid.operator->()); + if (ComputedValuesFFI::rust_grid_values_placements_equal(source_grid, current_grid)) + return; + ComputedValuesFFI::rust_grid_values_copy_placements(source_grid, &m_values.m_noninherited.grid.access()); } void reset_grid_placements_to_auto() { diff --git a/Libraries/LibWeb/DOM/Document.cpp b/Libraries/LibWeb/DOM/Document.cpp index 0c26687f4772f..225830a224c73 100644 --- a/Libraries/LibWeb/DOM/Document.cpp +++ b/Libraries/LibWeb/DOM/Document.cpp @@ -1844,11 +1844,6 @@ static void propagate_scrollbar_width_to_viewport(Element& root_element, Layout: // https://drafts.csswg.org/css-overflow-3/#overflow-propagation static void propagate_overflow_to_viewport(Element& root_element, Layout::Viewport& viewport) { - viewport.modify_computed_values([](auto& values) { - values.set_overflow_x(CSS::Overflow::Auto); - values.set_overflow_y(CSS::Overflow::Auto); - }); - // https://drafts.csswg.org/css-contain-2/#contain-property // Additionally, when any containments are active on either the HTML or elements, propagation of // properties from the element to the initial containing block, the viewport, or the canvas background, is @@ -1866,19 +1861,8 @@ static void propagate_overflow_to_viewport(Element& root_element, Layout::Viewpo // when the root element’s display value is not none. auto root_element_layout_node = root_element.unsafe_layout_node(); auto const& root_element_computed_values = *root_element.computed_values(); - root_element_layout_node->modify_computed_values([&](auto& values) { - values.set_overflow_x(root_element_computed_values.overflow_x()); - values.set_overflow_y(root_element_computed_values.overflow_y()); - }); - if (body_element_can_propagate_overflow) { - auto const& body_element_computed_values = *body_element->computed_values(); - body_element->unsafe_layout_node()->modify_computed_values([&](auto& values) { - values.set_overflow_x(body_element_computed_values.overflow_x()); - values.set_overflow_y(body_element_computed_values.overflow_y()); - }); - } - auto overflow_origin_node = root_element_layout_node; + Element* overflow_origin_element = &root_element; // However, when the root element is an [HTML] html element (including XML syntax for HTML) // whose overflow value is visible (in both axes), and that element has as a child @@ -1887,12 +1871,12 @@ static void propagate_overflow_to_viewport(Element& root_element, Layout::Viewpo if (root_element.is_html_html_element() && !body_propagation_is_disabled_by_containment) { if (root_element_computed_values.overflow_x() == CSS::Overflow::Visible && root_element_computed_values.overflow_y() == CSS::Overflow::Visible) { if (body_element_can_propagate_overflow) - overflow_origin_node = body_element->unsafe_layout_node(); + overflow_origin_element = body_element; } } // If 'visible' is applied to the viewport, it must be interpreted as 'auto'. If 'clip' is applied to the viewport, it must be interpreted as 'hidden'. - auto const& overflow_origin_computed_values = overflow_origin_node->computed_values(); + auto const& overflow_origin_computed_values = *overflow_origin_element->computed_values(); auto overflow_x_to_apply = overflow_origin_computed_values.overflow_x(); if (overflow_x_to_apply == CSS::Overflow::Visible) { overflow_x_to_apply = CSS::Overflow::Auto; @@ -1905,17 +1889,30 @@ static void propagate_overflow_to_viewport(Element& root_element, Layout::Viewpo } else if (overflow_y_to_apply == CSS::Overflow::Clip) { overflow_y_to_apply = CSS::Overflow::Hidden; } + // Every node receives its final values exactly once: a steady-state pass then leaves + // every style group payload untouched instead of oscillating values within the pass. viewport.modify_computed_values([&](auto& values) { values.set_overflow_x(overflow_x_to_apply); values.set_overflow_y(overflow_y_to_apply); }); + // UAs must apply the overflow-* values set on the root element to the viewport + // when the root element's display value is not none. // The element from which the value is propagated must then have a used overflow value of visible. // FIXME: Apply this to the used values, not the computed ones. - overflow_origin_node->modify_computed_values([](auto& values) { - values.set_overflow_x(CSS::Overflow::Visible); - values.set_overflow_y(CSS::Overflow::Visible); + bool root_element_is_overflow_origin = overflow_origin_element == &root_element; + root_element_layout_node->modify_computed_values([&](auto& values) { + values.set_overflow_x(root_element_is_overflow_origin ? CSS::Overflow::Visible : root_element_computed_values.overflow_x()); + values.set_overflow_y(root_element_is_overflow_origin ? CSS::Overflow::Visible : root_element_computed_values.overflow_y()); }); + if (body_element_can_propagate_overflow) { + bool body_element_is_overflow_origin = overflow_origin_element == body_element; + auto const& body_element_computed_values = *body_element->computed_values(); + body_element->unsafe_layout_node()->modify_computed_values([&](auto& values) { + values.set_overflow_x(body_element_is_overflow_origin ? CSS::Overflow::Visible : body_element_computed_values.overflow_x()); + values.set_overflow_y(body_element_is_overflow_origin ? CSS::Overflow::Visible : body_element_computed_values.overflow_y()); + }); + } } void Document::update_layout_if_needed_for_node(Node const& node, UpdateLayoutReason reason) diff --git a/Libraries/LibWeb/DOM/Element.cpp b/Libraries/LibWeb/DOM/Element.cpp index a22ee402cacc5..b147654312f58 100644 --- a/Libraries/LibWeb/DOM/Element.cpp +++ b/Libraries/LibWeb/DOM/Element.cpp @@ -1231,8 +1231,7 @@ static CSS::RequiredInvalidationAfterStyleChange compute_required_invalidation(C // previous style generation, which future diffs turn into pure pointer compares. bool const all_group_payloads_shared = new_computed_values.adopt_identical_group_payloads(old_computed_values); bool const property_diff_can_be_skipped = all_group_payloads_shared - && !old_computed_values.has_animated_values() && !new_computed_values.has_animated_values() - && !old_computed_values.animated_properties() && !new_computed_values.animated_properties(); + && !CSS::ComputedValues::either_carries_animated_overlay(old_computed_values, new_computed_values); static bool const verify_fast_path = getenv("LIBWEB_VERIFY_STYLE_DIFF_FAST_PATH") != nullptr; if (!property_diff_can_be_skipped || verify_fast_path) { diff --git a/Libraries/LibWeb/HTML/HTMLImageElement.cpp b/Libraries/LibWeb/HTML/HTMLImageElement.cpp index b83b040bfcffd..40cd2fba44d5d 100644 --- a/Libraries/LibWeb/HTML/HTMLImageElement.cpp +++ b/Libraries/LibWeb/HTML/HTMLImageElement.cpp @@ -125,6 +125,7 @@ static bool image_element_dimensions_may_depend_on_intrinsic_size(Layout::ImageB static void reset_intrinsic_size_caches_after_image_data_change(Layout::ImageBox& image_box) { + image_box.bump_fragment_cache_epoch_of_self_and_ancestors(); image_box.reset_cached_intrinsic_sizes(); for (auto* ancestor = image_box.parent(); ancestor; ancestor = ancestor->parent()) { auto* box = as_if(ancestor); diff --git a/Libraries/LibWeb/Internals/Internals.cpp b/Libraries/LibWeb/Internals/Internals.cpp index cbde43e351696..e319dd2be2e8d 100644 --- a/Libraries/LibWeb/Internals/Internals.cpp +++ b/Libraries/LibWeb/Internals/Internals.cpp @@ -60,6 +60,7 @@ #include #include #include +#include #include #include #include @@ -644,6 +645,11 @@ WebIDL::UnsignedLongLong Internals::full_layout_count() return window().associated_document().full_layout_count(); } +WebIDL::UnsignedLongLong Internals::layout_run_cache_hit_count() +{ + return window().associated_document().layout_node_arena().formatting_context_run_cache_hit_count(); +} + WebIDL::UnsignedLongLong Internals::accumulated_visual_context_tree_build_count() { auto paintable = window().associated_document().unsafe_paintable(); diff --git a/Libraries/LibWeb/Internals/Internals.h b/Libraries/LibWeb/Internals/Internals.h index 65c098fa828ac..9a612b90c3f4a 100644 --- a/Libraries/LibWeb/Internals/Internals.h +++ b/Libraries/LibWeb/Internals/Internals.h @@ -110,6 +110,7 @@ class WEB_API Internals final : public InternalsBase { void set_content_blocking_enabled(bool enabled); WebIDL::UnsignedLongLong partial_layout_count(); WebIDL::UnsignedLongLong full_layout_count(); + WebIDL::UnsignedLongLong layout_run_cache_hit_count(); WebIDL::UnsignedLongLong accumulated_visual_context_tree_build_count(); void set_autoplay_policy(Utf16String const& policy); diff --git a/Libraries/LibWeb/Internals/Internals.idl b/Libraries/LibWeb/Internals/Internals.idl index a2403ed4ba699..e957bc6fa874e 100644 --- a/Libraries/LibWeb/Internals/Internals.idl +++ b/Libraries/LibWeb/Internals/Internals.idl @@ -89,6 +89,7 @@ interface Internals { undefined setContentBlockingEnabled(boolean enabled); unsigned long long partialLayoutCount(); unsigned long long fullLayoutCount(); + unsigned long long layoutRunCacheHitCount(); unsigned long long accumulatedVisualContextTreeBuildCount(); undefined setAutoplayPolicy(Utf16DOMString policy); diff --git a/Libraries/LibWeb/Layout/Box.cpp b/Libraries/LibWeb/Layout/Box.cpp index d0a3c806a1e2a..39df7dbfa43a5 100644 --- a/Libraries/LibWeb/Layout/Box.cpp +++ b/Libraries/LibWeb/Layout/Box.cpp @@ -59,7 +59,7 @@ bool Box::is_partial_relayout_boundary(RequireExistingPaintable require_existing // Only a full layout pass resolves anchor() functions in the inset properties to plain // values; a replay from saved inputs cannot. - if (box_inset_properties_contain_anchor_functions(*this)) + if (insets_use_anchor_functions()) return false; // NOTE: Content-dependent sizing (shrink-to-fit, intrinsic constraints, aspect-ratio) does diff --git a/Libraries/LibWeb/Layout/LayoutRustBridge.cpp b/Libraries/LibWeb/Layout/LayoutRustBridge.cpp index c3137b9faafab..d11701e22b578 100644 --- a/Libraries/LibWeb/Layout/LayoutRustBridge.cpp +++ b/Libraries/LibWeb/Layout/LayoutRustBridge.cpp @@ -648,6 +648,18 @@ LayoutRustBridge::LayoutRustBridge() = default; LayoutRustBridge::~LayoutRustBridge() = default; +// Stamps the store once per pass entry, so cache entries validate against the +// viewport the pass actually laid out with; a new bridge entry method must call +// this before entering Rust. +static void note_viewport_size_for_pass(Box& pass_root) +{ + auto viewport_rect = pass_root.document().viewport_rect(); + RustFFI::layout_arena_note_viewport_size( + pass_root.arena_handle(), + viewport_rect.width().raw_value(), + viewport_rect.height().raw_value()); +} + void LayoutRustBridge::run_root_layout(Box& viewport, CSSPixels viewport_inline_size, CSSPixels viewport_block_size, bool should_collect_devtools_layout_data) { VERIFY(!m_commit_root); @@ -657,6 +669,7 @@ void LayoutRustBridge::run_root_layout(Box& viewport, CSSPixels viewport_inline_ }; viewport.document().invalidate_stacking_context_tree(); + note_viewport_size_for_pass(viewport); auto callbacks = formatting_context_callbacks(); auto sink = commit_sink(); { @@ -681,6 +694,7 @@ void LayoutRustBridge::compute_subtree_layout(Box& root, Painting::Paintable& pa }; root.document().invalidate_stacking_context_tree(); + note_viewport_size_for_pass(root); auto viewport_rect = root.document().viewport_rect(); auto callbacks = formatting_context_callbacks(); auto sink = commit_sink(); @@ -707,6 +721,7 @@ void LayoutRustBridge::replay_saved_abspos_layout(Box& box, Painting::Paintable& }; box.document().invalidate_stacking_context_tree(); + note_viewport_size_for_pass(box); auto callbacks = formatting_context_callbacks(); auto sink = commit_sink(); { @@ -942,13 +957,15 @@ RustFFI::FfiCommitSink LayoutRustBridge::commit_sink() CSSPixels::from_raw(viewport_size.height), }); } }, - .set_computed_svg_path = [](void*, void* paintable_pointer, void* path_pointer) { + .set_computed_svg_path = [](void*, void* paintable_pointer, void* path_pointer, u64 path_identity) { VERIFY(path_pointer); auto& paintable = *static_cast(paintable_pointer); - // The path stays owned by the Rust layout state; only its contents move out. - auto* path = static_cast(path_pointer); + // The path stays owned by the Rust fragment tree, which may emit it again on a + // later commit; the identity is process-unique per path allocation, so a match + // means the preserved copy is already this exact path and the copy can be skipped. + auto const* path = static_cast(path_pointer); if (auto* svg_path_paintable = as_if(paintable)) - svg_path_paintable->set_computed_path(move(*path)); }, + svg_path_paintable->set_computed_path_if_identity_changed(*path, path_identity); }, .set_grid_layout_data = [](void*, void* paintable_pointer, RustFFI::FfiGridLayoutData const* data) { VERIFY(data); static_cast(paintable_pointer)->set_grid_layout_data(build_grid_layout_data(*data)); }, @@ -1014,41 +1031,6 @@ static Optional abstract_element_for_abspos_box(Box const& return {}; } -static bool style_value_contains_anchor(CSS::StyleValue const& value) -{ - if (value.is_anchor()) - return true; - if (value.is_calculated()) - return value.as_calculated().contains_anchor_function(); - return false; -} - -bool box_inset_properties_contain_anchor_functions(Box const& box) -{ - auto abstract_element = abstract_element_for_abspos_box(box); - if (!abstract_element.has_value()) - return false; - - auto const* computed = abstract_element->computed_values(); - if (!computed) - return false; - // Anchor functions in insets only survive to used-value time inside calculated values, so - // when no inset is calculated (the common case), skip reconstructing the style values. - auto const& inset = computed->inset(); - if (!inset.top().is_calculated() && !inset.right().is_calculated() && !inset.bottom().is_calculated() && !inset.left().is_calculated()) - return false; - - auto top = computed->computed_style_value(CSS::PropertyID::Top); - auto right = computed->computed_style_value(CSS::PropertyID::Right); - auto bottom = computed->computed_style_value(CSS::PropertyID::Bottom); - auto left = computed->computed_style_value(CSS::PropertyID::Left); - VERIFY(top && right && bottom && left); - return style_value_contains_anchor(*top) - || style_value_contains_anchor(*right) - || style_value_contains_anchor(*bottom) - || style_value_contains_anchor(*left); -} - bool can_replay_saved_abspos_layout_inputs_after_style_change(Box const& box) { if (!box.containing_block()) @@ -1167,23 +1149,6 @@ RustFFI::FfiLayoutFcCallbacks LayoutRustBridge::formatting_context_callbacks() }; } return true; }, - .read_paintable_svg_transforms = [](void*, void* node, RustFFI::FfiSvgComputedTransforms* out) { - VERIFY(out); - auto const* paintable = static_cast(node)->paintable_ptr(); - Painting::SVGGraphicsPaintable::ComputedTransforms const* transforms = nullptr; - if (auto const* svg_graphics_paintable = as_if(paintable)) - transforms = &svg_graphics_paintable->computed_transforms(); - if (auto const* svg_foreign_object_paintable = as_if(paintable)) - transforms = &svg_foreign_object_paintable->computed_transforms(); - if (auto const* svg_svg_paintable = as_if(paintable)) - transforms = &svg_svg_paintable->computed_transforms(); - if (!transforms) - return false; - *out = { - .viewbox_transform = to_ffi_affine_transform(transforms->svg_to_viewbox_transform()), - .svg_transform = to_ffi_affine_transform(transforms->svg_transform()), - }; - return true; }, .compute_svg_path = [](void*, void* node, RustFFI::FfiSvgPathRequest request) { auto const* node_with_style = as_if(*static_cast(node)); VERIFY(node_with_style); diff --git a/Libraries/LibWeb/Layout/LayoutRustBridge.h b/Libraries/LibWeb/Layout/LayoutRustBridge.h index c324a95008a6f..6beadc3e99686 100644 --- a/Libraries/LibWeb/Layout/LayoutRustBridge.h +++ b/Libraries/LibWeb/Layout/LayoutRustBridge.h @@ -50,7 +50,6 @@ class LayoutRustBridge { [[nodiscard]] Optional formatting_context_type_created_by_box(Box const&); [[nodiscard]] StringView formatting_context_type_name(RustFFI::FfiFormattingContextType); -[[nodiscard]] bool box_inset_properties_contain_anchor_functions(Box const&); [[nodiscard]] bool can_replay_saved_abspos_layout_inputs_after_style_change(Box const&); // True while a synchronous Rust layout pass (including its commit) is on the diff --git a/Libraries/LibWeb/Layout/Node.cpp b/Libraries/LibWeb/Layout/Node.cpp index e6caf2b809c22..3a14920a31ac1 100644 --- a/Libraries/LibWeb/Layout/Node.cpp +++ b/Libraries/LibWeb/Layout/Node.cpp @@ -114,6 +114,34 @@ void Node::enroll_for_arena_replaced_content_facts_sync_if_eligible() node_arena().enroll_node_for_replaced_content_facts_sync(*this); } +bool Node::fragment_cache_epochs_enabled() +{ + // The Rust cache module owns the only LADYBIRD_FC_RUN_CACHE parser; the + // bump walks follow whatever mode it resolved. + static bool const enabled = RustFFI::layout_fc_run_cache_epochs_enabled(); + return enabled; +} + +void Node::bump_fragment_cache_epoch() +{ + if (!fragment_cache_epochs_enabled()) + return; + ++node_data().fragment_cache_epoch; +} + +// NB: Bumps can legitimately run while another document's layout pass is on the stack (a +// parent pass sizing a child navigable's viewport invalidates the child document), so the +// helpers must not assert against the process-global pass flag. A bump landing between a +// run's probe and its store is handled by storing the probe-time validity, which turns it +// into a fail-safe miss. +void Node::bump_fragment_cache_epoch_of_self_and_ancestors() +{ + if (!fragment_cache_epochs_enabled()) + return; + for (auto* node = this; node; node = node->parent_ptr()) + ++node->node_data().fragment_cache_epoch; +} + void* Node::arena_handle() const { return m_arena->handle(); @@ -669,6 +697,7 @@ NodeWithStyle::NodeWithStyle(DOM::Document& document, GC::Ptr node, N set_flag(RustFFI::NodeFlag::HasStyle, true); set_flag(RustFFI::NodeFlag::IsBody, node && node == GC::Ptr { document.body() }); set_flag(RustFFI::NodeFlag::HasAnchorNames, !m_computed_values->anchor_names().is_empty()); + set_flag(RustFFI::NodeFlag::InsetsUseAnchorFunctions, m_computed_values->inset_properties_contain_anchor_functions()); publish_style_container_to_node_data(); synchronize_table_span_data(); enroll_for_arena_replaced_content_facts_sync_if_eligible(); @@ -1008,11 +1037,28 @@ NonnullRefPtr NodeWithStyle::create_anonymous_wrapper() const void NodeWithStyle::set_computed_values(NonnullRefPtr computed_values) { VERIFY(!layout_pass_currently_running()); + + // Every path that lands computed values on a layout node funnels through here — element + // restyles, inherited-style recomputation (including the animation fast path's descendant + // walk), pseudo-element application, and anonymous wrapper propagation at any depth — so + // this is the one place that can tell whether a style change can affect this box's layout. + // Style-side layout inputs are exactly the layout-affecting group payloads published to + // node data plus the animated-value overlay, which lives outside the groups and + // disqualifies pointer diffing the same way it disqualifies the style differ's group + // fast path. + bool const changes_layout_affecting_style = fragment_cache_epochs_enabled() + && (CSS::ComputedValues::either_carries_animated_overlay(*m_computed_values, *computed_values) + || computed_values->differs_in_any_layout_affecting_group_payload_from(*m_computed_values)); + m_computed_values = move(computed_values); set_flag(RustFFI::NodeFlag::HasAnchorNames, !m_computed_values->anchor_names().is_empty()); + set_flag(RustFFI::NodeFlag::InsetsUseAnchorFunctions, m_computed_values->inset_properties_contain_anchor_functions()); publish_style_container_to_node_data(); enroll_for_arena_replaced_content_facts_sync_if_eligible(); + if (changes_layout_affecting_style) + bump_fragment_cache_epoch_of_self_and_ancestors(); + for (auto* child = first_child_ptr(); child; child = child->next_sibling_ptr()) { if (auto* text_child = as_if(*child)) text_child->enroll_for_arena_text_content_sync(); @@ -1393,6 +1439,10 @@ bool NodeWithStyle::has_paint_containment() const void Node::set_needs_layout_update(DOM::SetNeedsLayoutReason reason, LayoutUpdatePropagation propagation) { + // Bumped before the already-dirty early return below: a dirty node does not imply its + // whole ancestor chain was bumped for the current epoch values, and over-bumping is free. + bump_fragment_cache_epoch_of_self_and_ancestors(); + if (needs_layout_update() && propagation == LayoutUpdatePropagation::ThroughAncestors) { // A dirty node normally implies dirty ancestors, but the walk that marked a partial // relayout boundary stopped there and left its ancestors clean, so a through-ancestors @@ -1420,6 +1470,7 @@ void Node::set_needs_layout_update(DOM::SetNeedsLayoutReason reason, LayoutUpdat // NOTE: if this node generated an anonymous parent, all ancestors are indiscriminately marked below. for_each_child_of_type([&](Box& child) { if (child.is_anonymous() && !is(child)) { + child.bump_fragment_cache_epoch(); child.set_flag(RustFFI::NodeFlag::NeedsLayoutUpdate, true); child.reset_cached_intrinsic_sizes(); } diff --git a/Libraries/LibWeb/Layout/Node.h b/Libraries/LibWeb/Layout/Node.h index a3456958675d1..b529996d794de 100644 --- a/Libraries/LibWeb/Layout/Node.h +++ b/Libraries/LibWeb/Layout/Node.h @@ -42,7 +42,7 @@ static_assert(offsetof(RustFFI::NodeData, kind) == 28); static_assert(offsetof(RustFFI::NodeData, generated_for) == 29); static_assert(offsetof(RustFFI::NodeData, intrinsic_cache_epoch) == 30); static_assert(offsetof(RustFFI::NodeData, flags) == 32); -static_assert(offsetof(RustFFI::NodeData, initial_quote_nesting_level) == 36); +static_assert(offsetof(RustFFI::NodeData, fragment_cache_epoch) == 36); static_assert(offsetof(RustFFI::NodeData, slot_generation) == 40); static_assert(offsetof(RustFFI::NodeData, table_column_span) == 42); static_assert(offsetof(RustFFI::NodeData, table_row_span) == 44); @@ -105,6 +105,7 @@ class WEB_API Node NodeArena& node_arena() const { return *m_arena; } bool is_anonymous() const { return has_flag(RustFFI::NodeFlag::Anonymous); } + bool insets_use_anchor_functions() const { return has_flag(RustFFI::NodeFlag::InsetsUseAnchorFunctions); } DOM::Node const* dom_node() const; DOM::Node* dom_node(); @@ -113,6 +114,20 @@ class WEB_API Node bool needs_layout_update() const { return has_flag(RustFFI::NodeFlag::NeedsLayoutUpdate); } + // The formatting-context run cache (LADYBIRD_FC_RUN_CACHE) validates its entries against + // these epochs; with the cache disabled nothing reads them, so the walks no-op. + static bool fragment_cache_epochs_enabled(); + + void bump_fragment_cache_epoch(); + + // Any invalidation or restructuring below a node must reach every ancestor's epoch: cached + // runs capture subtree structure, and unlike intrinsic-size invalidation there is no + // absolutely-positioned or SVG boundary — those descendants' fragments live in ancestor + // run trees. Layout tree restructuring in particular never funnels through + // set_needs_layout_update (a full pass lays out everything), so the tree mutation + // primitives call this on the parent of every structural change. + void bump_fragment_cache_epoch_of_self_and_ancestors(); + // Set when a style change altered geometry-determining properties of this node itself, so // a partial relayout must re-resolve its own size and position instead of reusing them. bool needs_own_geometry_update() const { return has_flag(RustFFI::NodeFlag::NeedsOwnGeometryUpdate); } @@ -261,9 +276,6 @@ class WEB_API Node bool is_editing_host() const { return has_flag(RustFFI::NodeFlag::IsEditingHost); } void set_is_editing_host(bool value) { set_flag(RustFFI::NodeFlag::IsEditingHost, value); } - u32 initial_quote_nesting_level() const { return m_data->initial_quote_nesting_level; } - void set_initial_quote_nesting_level(u32 value) { m_data->initial_quote_nesting_level = value; } - // https://drafts.csswg.org/css-ui/#propdef-user-select CSS::UserSelect user_select_used_value() const; diff --git a/Libraries/LibWeb/Layout/NodeArena.cpp b/Libraries/LibWeb/Layout/NodeArena.cpp index 631cb12a4618e..975afb02d691e 100644 --- a/Libraries/LibWeb/Layout/NodeArena.cpp +++ b/Libraries/LibWeb/Layout/NodeArena.cpp @@ -35,6 +35,11 @@ void NodeArena::free(RustFFI::NodeSlotId slot, u32 generation) RustFFI::layout_arena_free(m_handle, slot, generation); } +u64 NodeArena::formatting_context_run_cache_hit_count() const +{ + return RustFFI::layout_arena_fc_run_cache_hit_count(m_handle); +} + void NodeArena::enroll_text_node_for_content_sync(TextNode const& text_node) { m_text_nodes_enrolled_for_content_sync.append(text_node.make_weak_ptr()); @@ -49,14 +54,18 @@ void NodeArena::sync_enrolled_text_node_content() // by a later tree update without another enrollment trigger. Vector> still_detached_text_nodes; for (auto& weak_text_node : m_text_nodes_enrolled_for_content_sync) { - auto const* text_node = weak_text_node.ptr(); + auto* text_node = weak_text_node.ptr(); if (!text_node) continue; if (!text_node->parent()) { still_detached_text_nodes.append(move(weak_text_node)); continue; } - text_node->sync_text_content_to_arena(); + // Changed rendered text invalidates cached formatting-context runs regardless of + // which channel produced the change, including sources with no invalidation of + // their own (e.g. lang-keyed locale-sensitive casing). + if (text_node->sync_text_content_to_arena()) + text_node->bump_fragment_cache_epoch_of_self_and_ancestors(); } m_text_nodes_enrolled_for_content_sync = move(still_detached_text_nodes); } @@ -78,7 +87,7 @@ void NodeArena::sync_enrolled_replaced_content_facts() { bool any_enrolled_node_died = false; for (auto& weak_node : m_nodes_enrolled_for_replaced_content_facts_sync) { - auto const* node = weak_node.ptr(); + auto* node = weak_node.ptr(); if (!node) { any_enrolled_node_died = true; continue; @@ -86,7 +95,10 @@ void NodeArena::sync_enrolled_replaced_content_facts() RustFFI::FfiReplacedContentFacts facts {}; if (auto const* box = as_if(*node)) facts = box->build_replaced_content_facts_for_arena(); - RustFFI::layout_arena_set_replaced_content_facts(m_handle, Node::slot_id(node), facts); + // Changed facts invalidate cached formatting-context runs regardless of which + // channel produced the change, including sources with no invalidation of their own. + if (RustFFI::layout_arena_set_replaced_content_facts(m_handle, Node::slot_id(node), facts)) + node->bump_fragment_cache_epoch_of_self_and_ancestors(); } if (any_enrolled_node_died) m_nodes_enrolled_for_replaced_content_facts_sync.remove_all_matching([](auto& weak_node) { return !weak_node.ptr(); }); diff --git a/Libraries/LibWeb/Layout/NodeArena.h b/Libraries/LibWeb/Layout/NodeArena.h index 73de506ee82af..bd26ee637271d 100644 --- a/Libraries/LibWeb/Layout/NodeArena.h +++ b/Libraries/LibWeb/Layout/NodeArena.h @@ -35,6 +35,7 @@ class WEB_API NodeArena : public RefCounted { RustFFI::NodeAllocation allocate(); void free(RustFFI::NodeSlotId, u32 generation); void* handle() const { return m_handle; } + u64 formatting_context_run_cache_hit_count() const; void enroll_text_node_for_content_sync(TextNode const&); void enroll_node_for_replaced_content_facts_sync(Node const&); diff --git a/Libraries/LibWeb/Layout/TextNode.cpp b/Libraries/LibWeb/Layout/TextNode.cpp index b9002ca9b0269..73c1d96803091 100644 --- a/Libraries/LibWeb/Layout/TextNode.cpp +++ b/Libraries/LibWeb/Layout/TextNode.cpp @@ -424,14 +424,14 @@ void TextNode::enroll_for_arena_text_content_sync() const node_arena().enroll_text_node_for_content_sync(*this); } -void TextNode::sync_text_content_to_arena() const +bool TextNode::sync_text_content_to_arena() const { ensure_text_dependent_cache(); m_enrolled_for_arena_text_content_sync = false; if (m_arena_text_content_in_sync) - return; + return false; auto view = m_text_dependent_cache->text_for_rendering.utf16_view(); - RustFFI::layout_arena_set_text_content( + bool arena_text_content_changed = RustFFI::layout_arena_set_text_content( arena_handle(), slot_id(this), view.has_ascii_storage() ? reinterpret_cast(view.ascii_span().data()) : nullptr, @@ -440,6 +440,7 @@ void TextNode::sync_text_content_to_arena() const text().is_ascii_whitespace(), Unicode::may_require_bidi_processing(view)); m_arena_text_content_in_sync = true; + return arena_text_content_changed; } Utf16String TextNode::compute_text_for_rendering(TextForRenderingCacheKey const& cache_key) const diff --git a/Libraries/LibWeb/Layout/TextNode.h b/Libraries/LibWeb/Layout/TextNode.h index 0b9c75e316eeb..10e5f39a2fc18 100644 --- a/Libraries/LibWeb/Layout/TextNode.h +++ b/Libraries/LibWeb/Layout/TextNode.h @@ -42,7 +42,7 @@ class TextNode : public Node { void invalidate_text_for_rendering(); void enroll_for_arena_text_content_sync() const; - void sync_text_content_to_arena() const; + bool sync_text_content_to_arena() const; Unicode::Segmenter& grapheme_segmenter() const; diff --git a/Libraries/LibWeb/Layout/TreeBuilder.cpp b/Libraries/LibWeb/Layout/TreeBuilder.cpp index 322b35623e628..961452417cb1b 100644 --- a/Libraries/LibWeb/Layout/TreeBuilder.cpp +++ b/Libraries/LibWeb/Layout/TreeBuilder.cpp @@ -569,7 +569,7 @@ RustFFI::FfiPseudoTreeBuilderCallbacks LayoutTreeBuildBridge::make_ffi_pseudo_tr VERIFY(frame.layout_node); auto marker_style = element.document().style_computer().compute_style({ element, CSS::PseudoElement::Marker }); (void)builder.create_and_attach_list_item_marker(as(*frame.layout_node), element, css_pseudo_element(originating_pseudo), move(marker_style)); }, - .configure_layout_node = [](void* frame_pointer, void* element_pointer, RustFFI::FfiPseudoElement ffi_pseudo, u32 initial_quote_nesting_level) { + .configure_layout_node = [](void* frame_pointer, void* element_pointer, RustFFI::FfiPseudoElement ffi_pseudo) { VERIFY(frame_pointer); VERIFY(element_pointer); auto& frame = *static_cast(frame_pointer); @@ -577,7 +577,6 @@ RustFFI::FfiPseudoTreeBuilderCallbacks LayoutTreeBuildBridge::make_ffi_pseudo_tr auto pseudo_element = css_pseudo_element(ffi_pseudo); VERIFY(frame.layout_node); frame.layout_node->set_generated_for(pseudo_element, element); - frame.layout_node->set_initial_quote_nesting_level(initial_quote_nesting_level); LayoutTreeBuilderAccess::set_synthetic_pseudo_element_node(element, pseudo_element, frame.layout_node); }, .resolve_content = [](void* frame_pointer, void* element_pointer, RustFFI::FfiPseudoElement ffi_pseudo, u32 initial_quote_nesting_level) -> RustFFI::FfiResolvedPseudoContentFacts { VERIFY(frame_pointer); diff --git a/Libraries/LibWeb/Painting/SVGPathPaintable.cpp b/Libraries/LibWeb/Painting/SVGPathPaintable.cpp index e3d1055f55538..c50762925d4fa 100644 --- a/Libraries/LibWeb/Painting/SVGPathPaintable.cpp +++ b/Libraries/LibWeb/Painting/SVGPathPaintable.cpp @@ -23,12 +23,6 @@ SVGPathPaintable::SVGPathPaintable(Layout::SVGGraphicsBox const& layout_box) { } -void SVGPathPaintable::reset_for_relayout() -{ - SVGGraphicsPaintable::reset_for_relayout(); - m_computed_path.clear(); -} - Optional SVGPathPaintable::clip_path_geometry_bounds(Gfx::AffineTransform const& additional_transform) const { if (!contributes_to_clip_path() || !computed_path().has_value()) diff --git a/Libraries/LibWeb/Painting/SVGPathPaintable.h b/Libraries/LibWeb/Painting/SVGPathPaintable.h index 5fb850469bbb6..86d8a950d0f28 100644 --- a/Libraries/LibWeb/Painting/SVGPathPaintable.h +++ b/Libraries/LibWeb/Painting/SVGPathPaintable.h @@ -25,19 +25,25 @@ class WEB_API SVGPathPaintable final : public SVGGraphicsPaintable { SVG::SVGGraphicsElement const& dom_node() const { return as(*Paintable::dom_node()); } - void set_computed_path(Gfx::Path path) + // The identity is process-unique per layout-side path allocation and never + // reused, so a match proves the already-held path is byte-identical and the + // deep copy can be skipped. Every commit of a path-like fragment emits a + // path (commit-side asserted), so the held path is never stale. + void set_computed_path_if_identity_changed(Gfx::Path const& path, u64 path_identity) { - m_computed_path = move(path); + if (path_identity != 0 && path_identity == m_committed_path_identity && m_computed_path.has_value()) + return; + m_computed_path = path; + m_committed_path_identity = path_identity; } Optional const& computed_path() const { return m_computed_path; } - virtual void reset_for_relayout() override; - protected: SVGPathPaintable(Layout::SVGGraphicsBox const&); Optional m_computed_path = {}; + u64 m_committed_path_identity { 0 }; private: virtual bool is_svg_path_paintable() const final { return true; } diff --git a/Libraries/LibWeb/RefCountedTreeNode.h b/Libraries/LibWeb/RefCountedTreeNode.h index 8f44c2f037903..c2cade6b2adfc 100644 --- a/Libraries/LibWeb/RefCountedTreeNode.h +++ b/Libraries/LibWeb/RefCountedTreeNode.h @@ -282,6 +282,7 @@ class WEB_API RefCountedTreeNode { if (next_sibling) next_sibling->synchronize_topology(); } + note_structural_change_to_layout_caches(); } void replace_child(NonnullRefPtr new_child, T& old_child) @@ -621,6 +622,13 @@ class WEB_API RefCountedTreeNode { if (auto* next_sibling = node.next_sibling_ptr()) next_sibling->synchronize_topology(); } + note_structural_change_to_layout_caches(); + } + + void note_structural_change_to_layout_caches() + { + if constexpr (requires { static_cast(*this).bump_fragment_cache_epoch_of_self_and_ancestors(); }) + static_cast(*this).bump_fragment_cache_epoch_of_self_and_ancestors(); } WeakPtr m_parent; diff --git a/Libraries/LibWeb/Rust/src/css/computed_values.rs b/Libraries/LibWeb/Rust/src/css/computed_values.rs index c88eccb1dafe5..7927b3681ce6d 100644 --- a/Libraries/LibWeb/Rust/src/css/computed_values.rs +++ b/Libraries/LibWeb/Rust/src/css/computed_values.rs @@ -2056,6 +2056,52 @@ pub unsafe extern "C" fn rust_grid_values_copy_placements(source: *const GridVal }); } +/// # Safety +/// `source` and `target` must be valid grid group payloads. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn rust_grid_values_placements_equal( + source: *const GridValues, + target: *const GridValues, +) -> bool { + abort_on_panic(|| { + // SAFETY: The caller passes valid payloads and only reads them. + let (source, target) = unsafe { (&*source, &*target) }; + let name_raw = |grid: &GridValues, index: u32| { + if index == GRID_NO_INDEX { + return 0; + } + grid.names.as_slice()[index as usize].raw() + }; + // Name indices are payload-local, so a placement compares as its index-neutralized + // shape plus the raw names those indices resolve to; a field added to + // ComputedGridPlacement flows into the comparison through the struct update. + let comparable_placement = |grid: &GridValues, placement: &ComputedGridPlacement| { + ( + ComputedGridPlacement { + name_index: GRID_NO_INDEX, + implicit_start_name_index: GRID_NO_INDEX, + implicit_end_name_index: GRID_NO_INDEX, + ..*placement + }, + name_raw(grid, placement.name_index), + name_raw(grid, placement.implicit_start_name_index), + name_raw(grid, placement.implicit_end_name_index), + ) + }; + let placements_equal = |ours: &ComputedGridPlacement, theirs: &ComputedGridPlacement| { + comparable_placement(source, ours) == comparable_placement(target, theirs) + }; + placements_equal(&source.column_start, &target.column_start) + && placements_equal(&source.column_end, &target.column_end) + && placements_equal(&source.row_start, &target.row_start) + && placements_equal(&source.row_end, &target.row_end) + && source.grid_column_start_style_value == target.grid_column_start_style_value + && source.grid_column_end_style_value == target.grid_column_end_style_value + && source.grid_row_start_style_value == target.grid_row_start_style_value + && source.grid_row_end_style_value == target.grid_row_end_style_value + }) +} + /// # Safety /// `target` must be a uniquely owned grid group value. #[unsafe(no_mangle)] diff --git a/Libraries/LibWeb/Rust/src/layout/commit.rs b/Libraries/LibWeb/Rust/src/layout/commit.rs index f658439b055e0..50d8b7c490e5c 100644 --- a/Libraries/LibWeb/Rust/src/layout/commit.rs +++ b/Libraries/LibWeb/Rust/src/layout/commit.rs @@ -95,7 +95,7 @@ pub struct FfiCommitSink { pub finish_line_data: unsafe extern "C" fn(*mut c_void), pub set_computed_svg_transforms: unsafe extern "C" fn(*mut c_void, *mut c_void, crate::layout::FfiSvgComputedTransforms), pub set_svg_viewport_size: unsafe extern "C" fn(*mut c_void, *mut c_void, crate::layout::FfiCssPixelSize), - pub set_computed_svg_path: unsafe extern "C" fn(*mut c_void, *mut c_void, *mut c_void), + pub set_computed_svg_path: unsafe extern "C" fn(*mut c_void, *mut c_void, *mut c_void, u64), pub set_grid_layout_data: unsafe extern "C" fn(*mut c_void, *mut c_void, *const FfiGridLayoutData), pub set_flex_layout_data: unsafe extern "C" fn(*mut c_void, *mut c_void, *const FfiFlexLayoutData), pub set_used_grid_tracks: @@ -195,8 +195,19 @@ fn commit_subtree( if let Some(viewport_size) = fragment.svg_viewport_size { (sink.set_svg_viewport_size)(sink.context, paintable, viewport_size); } - if let Some(path) = fragment.computed_svg_path.take() { - (sink.set_computed_svg_path)(sink.context, paintable, path.as_raw()); + // The paintable keeps its committed path across relayout and only swaps it on an + // identity change, which is sound only while every committed path-like fragment + // carries a path. + debug_assert!( + fragment.computed_svg_path.is_some() + || !matches!( + callbacks.node_data(node).kind, + NodeKind::SVGGeometryBox | NodeKind::SVGTextBox | NodeKind::SVGTextPathBox + ), + "committed path-like fragment carries no computed SVG path" + ); + if let Some(path) = &fragment.computed_svg_path { + (sink.set_computed_svg_path)(sink.context, paintable, path.as_raw(), path.identity()); } } if let Some(data) = &fragment.grid_layout_data { diff --git a/Libraries/LibWeb/Rust/src/layout/fc_run_cache.rs b/Libraries/LibWeb/Rust/src/layout/fc_run_cache.rs new file mode 100644 index 0000000000000..4d17468577953 --- /dev/null +++ b/Libraries/LibWeb/Rust/src/layout/fc_run_cache.rs @@ -0,0 +1,505 @@ +/* + * Copyright (c) 2026-present, the Ladybird developers. + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum FcRunCacheMode { + Disabled, + Enabled, + /// Hits do not replay: the real layout runs and the entry is verified + /// against it, panicking on any divergence. + Shadow, +} + +fn fc_run_cache_mode_from_environment() -> FcRunCacheMode { + static MODE: std::sync::OnceLock = std::sync::OnceLock::new(); + *MODE.get_or_init(|| match std::env::var("LADYBIRD_FC_RUN_CACHE").as_deref() { + Ok("0") => FcRunCacheMode::Disabled, + Ok("1") => FcRunCacheMode::Enabled, + Ok("shadow") => FcRunCacheMode::Shadow, + Ok(unknown) => { + eprintln!("Unknown LADYBIRD_FC_RUN_CACHE value {unknown:?} (expected 0, 1, or shadow); disabling the run cache"); + FcRunCacheMode::Disabled + } + Err(_) => FcRunCacheMode::Enabled, + }) +} + +/// The complete identity of a memoizable run: the layout input plus the +/// pre-run root record state the dispatch seam captures anyway, so every +/// value a parent hands a spawned run is part of the key. +#[derive(Clone, Copy, PartialEq)] +struct FcRunCacheKey { + fc_type: FfiFormattingContextType, + input: LayoutInput, + root_cells: UsedValuesCellState, +} + +/// What must still be true for a stored entry to be replayed: the slot +/// holds the same box (generation), nothing in its subtree was invalidated +/// (the fragment cache epoch, whose bump walk has no propagation +/// boundary), and the viewport is unchanged (viewport-relative styles do +/// not necessarily funnel through per-node invalidation). +#[derive(Clone, Copy, PartialEq, Eq)] +pub(crate) struct FcRunCacheValidity { + pub(crate) slot_generation: u8, + pub(crate) fragment_cache_epoch: u32, + pub(crate) viewport: (i32, i32), +} + +struct FcRunCacheEntry { + key: FcRunCacheKey, + validity: FcRunCacheValidity, + outputs: RunOutputs, + /// Keeps every font referenced by cached line data alive: glyph runs + /// borrow raw font pointers, and a paint-only style change can drop + /// the owning computed values without touching any layout epoch. + /// + /// Cached line data also holds raw text_utf16 pointers into arena text + /// slots, deliberately unretained: replay never re-runs line building, + /// commit emits offsets rather than the pointers, and every text change + /// bumps epochs before the next probe, so nothing on the replay path + /// dereferences them. Any future consumer of cached fragment text must + /// snapshot the text alongside the fonts first. + retained_fonts: Vec, +} + +/// Per-document store of completed run results, one entry per slot, +/// surviving across layout passes on the node arena. +#[derive(Default)] +pub(crate) struct FcRunCacheArenaStore { + viewport: Cell<(i32, i32)>, + hit_count: Cell, + entries: RefCell>>>, +} + +impl FcRunCacheArenaStore { + pub(crate) fn note_viewport_size(&self, inline_size_raw: i32, block_size_raw: i32) { + self.viewport.set((inline_size_raw, block_size_raw)); + } + + pub(crate) fn viewport_size(&self) -> (i32, i32) { + self.viewport.get() + } + + pub(crate) fn hit_count(&self) -> u64 { + self.hit_count.get() + } + + pub(crate) fn remove_entry(&self, slot: u32) { + if let Some(entry) = self.entries.borrow_mut().get_mut(slot as usize) { + *entry = None; + } + } + + /// A matching entry stays stored — hits hand out shared handles — while + /// a stale entry is evicted on sight, releasing its tree and fonts. + fn matching(&self, slot: u32, validity: FcRunCacheValidity, key: &FcRunCacheKey) -> Option> { + let mut entries = self.entries.borrow_mut(); + let stored = entries.get_mut(slot as usize)?; + let entry = stored.as_ref()?; + if entry.validity != validity { + *stored = None; + return None; + } + if entry.key != *key { + return None; + } + Some(entry.clone()) + } + + fn store(&self, slot: u32, entry: std::rc::Rc) { + let mut entries = self.entries.borrow_mut(); + if entries.len() <= slot as usize { + entries.resize_with(slot as usize + 1, || None); + } + entries[slot as usize] = Some(entry); + } + + /// Drops every entry the keep predicate rejects, releasing its tree and + /// fonts. The end-of-pass sweep uses this so entries invalidated while + /// their box never probes again do not accumulate for the document's + /// lifetime. + pub(crate) fn retain_entries(&self, mut keep: impl FnMut(u32, FcRunCacheValidity) -> bool) { + let mut entries = self.entries.borrow_mut(); + for (slot, stored) in entries.iter_mut().enumerate() { + if let Some(entry) = stored + && !keep(slot as u32, entry.validity) + { + *stored = None; + } + } + } +} + +fn collect_line_data_fonts(line_data: &LineData, fonts: &mut Vec<*const c_void>) { + for line in &line_data.line_boxes { + for fragment in &line.fragments { + if let Some(glyphs) = &fragment.glyphs + && !glyphs.font.is_null() + { + fonts.push(glyphs.font); + } + } + } +} + +fn collect_fragment_tree_fonts(links: &[FragmentLink], fonts: &mut Vec<*const c_void>) { + for link in links { + if let Some(line_data) = &link.fragment.line_data { + collect_line_data_fonts(line_data, fonts); + } + collect_fragment_tree_fonts(&link.fragment.children, fonts); + } +} + +fn run_root_validity(callbacks: &FfiLayoutFcCallbacks, box_: Node) -> FcRunCacheValidity { + let data = NodeFacts::new(callbacks, box_).data(); + FcRunCacheValidity { + slot_generation: data.slot_generation, + fragment_cache_epoch: data.fragment_cache_epoch, + viewport: callbacks.arena().fc_run_cache_store().viewport.get(), + } +} + +/// A run's cache interaction, decided at probe time and concluded after +/// the run executes. Uncacheable classes bypass entirely: measurement and +/// intrinsic-sizing runs produce no fragments; subgridded grid items copy +/// the parent grid's mid-run track state, which the key cannot see; +/// anchor()-positioned roots keep their bypass while the resolved-inset +/// side effects are audited; pass entries and internal runs are not +/// spawned child runs; devtools collection emits per-run callbacks a +/// replay would skip. +enum FcRunCacheAttempt { + Bypass, + Store { + key: Box, + /// Captured at probe time and reused at store time: an invalidation + /// landing between probe and store makes the stored entry look stale + /// on its next probe (a fail-safe miss) instead of being baked into + /// a forever-valid entry. + validity: FcRunCacheValidity, + shadow_entry: Option>, + }, +} + +impl FcRunCacheAttempt { + /// Err carries the entry the caller must replay instead of running. + #[expect(clippy::too_many_arguments)] + fn probe( + purpose: LayoutPurpose, + box_: Node, + parent_grid_is_present: bool, + fc_type: FfiFormattingContextType, + layout_mode: LayoutMode, + should_collect_devtools_layout_data: bool, + callbacks: &FfiLayoutFcCallbacks, + input: &LayoutInput, + root_cells: &UsedValuesCellState, + ) -> Result> { + let mode = fc_run_cache_mode_from_environment(); + if mode == FcRunCacheMode::Disabled + || layout_mode != LayoutMode::Normal + || purpose.is_measurement() + || should_collect_devtools_layout_data + || input.participation == ParticipationInParentFormattingContext::Root + || matches!( + fc_type, + FfiFormattingContextType::InternalReplaced | FfiFormattingContextType::InternalDummy + ) + { + return Ok(Self::Bypass); + } + if fc_type == FfiFormattingContextType::Grid + && parent_grid_is_present + && grid_template_declares_a_subgrid_axis(callbacks, box_) + { + return Ok(Self::Bypass); + } + // Structural invariants behind this root-flag check, to re-verify if it is ever + // narrowed: anchor() applies only to absolutely positioned boxes and every abspos + // box is the root of its own spawned run, so the flag on the run root covers every + // anchor consumer; anchor eligibility is resolved from used values of the same + // pass under the C++ anchor_lookup containing-block guard, never from a previous + // pass; scroll compensation compares chains that converge at the run root and + // registers scroll-shift side effects a replay would skip; and anchor-size() + // resolves to None today, so inset properties are the only anchor dependency a + // run can have. + if has_flag(NodeFacts::new(callbacks, box_).data(), NodeFlag::InsetsUseAnchorFunctions) { + return Ok(Self::Bypass); + } + let key = Box::new(FcRunCacheKey { + fc_type, + input: *input, + root_cells: *root_cells, + }); + let store = callbacks.arena().fc_run_cache_store(); + let validity = run_root_validity(callbacks, box_); + match store.matching(box_.slot_index(), validity, &key) { + Some(entry) if mode == FcRunCacheMode::Shadow => { + // A shadow match is the same event a replay would be, so the + // hit counter reports it: the hit-count tests hold under the + // oracle as well. + store.hit_count.set(store.hit_count.get() + 1); + Ok(Self::Store { + key, + validity, + shadow_entry: Some(entry), + }) + } + Some(entry) => { + store.hit_count.set(store.hit_count.get() + 1); + Err(entry) + } + None => Ok(Self::Store { + key, + validity, + shadow_entry: None, + }), + } + } + + fn conclude(self, callbacks: &FfiLayoutFcCallbacks, box_: Node, outputs: &RunOutputs) { + let Self::Store { + key, + validity, + shadow_entry, + } = self + else { + return; + }; + let Some(root) = &outputs.root else { + return; + }; + let mut fonts = Vec::new(); + if let Some(line_data) = &outputs.root_outcome.line_data { + collect_line_data_fonts(line_data, &mut fonts); + } + collect_fragment_tree_fonts(&root.scoped_descendants, &mut fonts); + fonts.sort_unstable(); + fonts.dedup(); + let entry = FcRunCacheEntry { + key: *key, + validity, + outputs: outputs.clone(), + retained_fonts: fonts + .into_iter() + // SAFETY: every collected font pointer is live during the + // pass that produced the line data now being cached. + .map(|font| unsafe { libgfx_rust::font::RetainedFont::retain(font) }) + .collect(), + }; + if let Some(cached) = shadow_entry { + verify_cached_entry_against_fresh_run(box_.slot_index(), &cached, &entry); + } + callbacks + .arena() + .fc_run_cache_store() + .store(box_.slot_index(), std::rc::Rc::new(entry)); + } +} + +fn verify_cached_entry_against_fresh_run(root_slot: u32, cached: &FcRunCacheEntry, fresh: &FcRunCacheEntry) { + assert!( + cached.outputs.result == fresh.outputs.result, + "run cache shadow: child layout result diverged for slot {root_slot}" + ); + assert!( + cached.outputs.root_outcome.cells == fresh.outputs.root_outcome.cells, + "run cache shadow: body-end root record diverged for slot {root_slot}\ncached: {:?}\nfresh: {:?}", + cached.outputs.root_outcome.cells, + fresh.outputs.root_outcome.cells, + ); + assert!( + cached.outputs.root_outcome.own_metrics_sealed == fresh.outputs.root_outcome.own_metrics_sealed, + "run cache shadow: root seal state diverged for slot {root_slot}" + ); + let cached_fonts: Vec<_> = cached.retained_fonts.iter().map(|font| font.as_raw()).collect(); + let fresh_fonts: Vec<_> = fresh.retained_fonts.iter().map(|font| font.as_raw()).collect(); + assert!( + cached_fonts == fresh_fonts, + "run cache shadow: referenced fonts diverged for slot {root_slot}" + ); + assert_line_data_matches( + root_slot, + cached.outputs.root_outcome.line_data.as_deref(), + fresh.outputs.root_outcome.line_data.as_deref(), + ); + assert_rare_data_matches( + root_slot, + cached.outputs.root_outcome.rare.as_ref(), + fresh.outputs.root_outcome.rare.as_ref(), + ); + assert_unplaced_roots_match(root_slot, cached.outputs.root.as_ref(), fresh.outputs.root.as_ref()); +} + +/// The comparable view of the payloads both `UsedValuesRareData` and +/// `Fragment` carry under identical field names, so the oracle's two +/// comparison sites cannot drift apart when a payload is added. Shared +/// payloads compare by content through the Rc (paths through their +/// process-unique identity first). +macro_rules! shadow_comparable_rare_payloads { + ($carrier:expr) => { + ( + $carrier.table_cell_coordinates, + $carrier.override_borders_data, + $carrier.computed_svg_transforms, + $carrier.svg_viewport_size, + &$carrier.computed_svg_path, + &$carrier.grid_layout_data, + &$carrier.flex_layout_data, + &$carrier.used_grid_tracks, + ) + }; +} + +fn line_data_matches(cached: Option<&LineData>, fresh: Option<&LineData>) -> bool { + cached == fresh +} + +fn assert_line_data_matches(root_slot: u32, cached: Option<&LineData>, fresh: Option<&LineData>) { + assert!( + line_data_matches(cached, fresh), + "run cache shadow: root line data diverged for slot {root_slot}" + ); +} + +fn assert_rare_data_matches(root_slot: u32, cached: Option<&UsedValuesRareData>, fresh: Option<&UsedValuesRareData>) { + assert!( + cached.is_some() == fresh.is_some(), + "run cache shadow: root rare data presence diverged for slot {root_slot}" + ); + let (Some(cached), Some(fresh)) = (cached, fresh) else { + return; + }; + assert!( + shadow_comparable_rare_payloads!(cached) == shadow_comparable_rare_payloads!(fresh) + && cached.abspos_layout_inputs == fresh.abspos_layout_inputs, + "run cache shadow: root rare data diverged for slot {root_slot}" + ); +} + +fn sorted_by_key(items: &[T], key: impl Fn(&T) -> K) -> Vec { + let mut sorted = items.to_vec(); + sorted.sort_by_key(|item| key(item)); + sorted +} + +fn assert_unplaced_roots_match(root_slot: u32, cached: Option<&UnplacedRootFragment>, fresh: Option<&UnplacedRootFragment>) { + assert!( + cached.is_some() == fresh.is_some(), + "run cache shadow: unplaced root presence diverged for slot {root_slot}" + ); + let (Some(cached), Some(fresh)) = (cached, fresh) else { + return; + }; + assert!( + cached.node == fresh.node, + "run cache shadow: unplaced root node diverged for slot {root_slot}" + ); + + // Escape lists are collected partly from hash-map sweeps, whose order + // is not deterministic between runs; consumption sorts registrations + // into tree order, so the oracle compares them order-insensitively. + let pending_order = + |child: &PendingAbsposChild| (child.child_box.slot_index(), child.coordinate_space_box.slot_index()); + let cached_pending = sorted_by_key(&cached.propagated_pending_abspos, pending_order); + let fresh_pending = sorted_by_key(&fresh.propagated_pending_abspos, pending_order); + assert!( + cached_pending == fresh_pending, + "run cache shadow: propagated abspos children diverged for slot {root_slot}\ncached: {cached_pending:#?}\nfresh: {fresh_pending:#?}" + ); + let candidate_order = + |candidate: &AnchorCandidate| (candidate.node.slot_index(), candidate.coordinate_space_box.slot_index()); + let cached_candidates = sorted_by_key(&cached.propagated_anchor_candidates, candidate_order); + let fresh_candidates = sorted_by_key(&fresh.propagated_anchor_candidates, candidate_order); + assert!( + cached_candidates == fresh_candidates, + "run cache shadow: propagated anchor candidates diverged for slot {root_slot}" + ); + let inline_rect_order = + |rect: &InlineContainingBlockRect| (rect.inline_box.slot_index(), rect.coordinate_space_box.slot_index()); + let cached_inline_rects = sorted_by_key(&cached.propagated_inline_containing_block_rects, inline_rect_order); + let fresh_inline_rects = sorted_by_key(&fresh.propagated_inline_containing_block_rects, inline_rect_order); + assert!( + cached_inline_rects == fresh_inline_rects, + "run cache shadow: propagated inline containing block rects diverged for slot {root_slot}" + ); + let contribution_order = |contribution: &AbsposContainingBlockInfoContribution| contribution.child_box.slot_index(); + let cached_contributions = sorted_by_key(&cached.propagated_abspos_containing_block_info, contribution_order); + let fresh_contributions = sorted_by_key(&fresh.propagated_abspos_containing_block_info, contribution_order); + assert!( + cached_contributions == fresh_contributions, + "run cache shadow: propagated containing block info diverged for slot {root_slot}" + ); + + assert_link_lists_match(root_slot, &cached.scoped_descendants, &fresh.scoped_descendants); +} + +fn assert_link_lists_match(root_slot: u32, cached: &[FragmentLink], fresh: &[FragmentLink]) { + assert!( + cached.len() == fresh.len(), + "run cache shadow: fragment child count diverged for slot {root_slot}" + ); + for (cached_link, fresh_link) in cached.iter().zip(fresh) { + assert_links_match(root_slot, cached_link, fresh_link); + } +} + +fn assert_links_match(root_slot: u32, cached: &FragmentLink, fresh: &FragmentLink) { + let mut diverged = Vec::new(); + if cached.committed_offset != fresh.committed_offset { + diverged.push("committed_offset"); + } + if (cached.inset_left, cached.inset_right, cached.inset_top, cached.inset_bottom) + != (fresh.inset_left, fresh.inset_right, fresh.inset_top, fresh.inset_bottom) + { + diverged.push("insets"); + } + if cached.containing_line_box_index != fresh.containing_line_box_index { + diverged.push("containing_line_box_index"); + } + if cached.abspos_layout_inputs != fresh.abspos_layout_inputs { + diverged.push("abspos_layout_inputs"); + } + collect_diverged_fragment_fields(&cached.fragment, &fresh.fragment, &mut diverged); + assert!( + diverged.is_empty(), + "run cache shadow: fragment for slot {} diverged under run root slot {root_slot}: {}", + fresh.fragment.node.slot_index(), + diverged.join(", ") + ); + assert_link_lists_match(root_slot, &cached.fragment.children, &fresh.fragment.children); +} + +fn collect_diverged_fragment_fields(cached: &Fragment, fresh: &Fragment, diverged: &mut Vec<&'static str>) { + if cached.node != fresh.node { + diverged.push("node"); + } + if (cached.content_inline_size, cached.content_block_size) != (fresh.content_inline_size, fresh.content_block_size) { + diverged.push("content_size"); + } + if (cached.margin_left, cached.margin_right, cached.margin_top, cached.margin_bottom) + != (fresh.margin_left, fresh.margin_right, fresh.margin_top, fresh.margin_bottom) + { + diverged.push("margins"); + } + if (cached.border_left, cached.border_right, cached.border_top, cached.border_bottom) + != (fresh.border_left, fresh.border_right, fresh.border_top, fresh.border_bottom) + { + diverged.push("borders"); + } + if (cached.padding_left, cached.padding_right, cached.padding_top, cached.padding_bottom) + != (fresh.padding_left, fresh.padding_right, fresh.padding_top, fresh.padding_bottom) + { + diverged.push("paddings"); + } + if shadow_comparable_rare_payloads!(cached) != shadow_comparable_rare_payloads!(fresh) { + diverged.push("rare payloads"); + } + if !line_data_matches(cached.line_data.as_deref(), fresh.line_data.as_deref()) { + diverged.push("line_data"); + } +} diff --git a/Libraries/LibWeb/Rust/src/layout/flex_formatting_context.rs b/Libraries/LibWeb/Rust/src/layout/flex_formatting_context.rs index 1d5d47bf6821b..8f49b38e12155 100644 --- a/Libraries/LibWeb/Rust/src/layout/flex_formatting_context.rs +++ b/Libraries/LibWeb/Rust/src/layout/flex_formatting_context.rs @@ -2555,7 +2555,7 @@ impl<'pass> FlexFormattingContext<'pass> { as u8, lines, }; - self.container_used().rare_data_mut().flex_layout_data = Some(data); + self.container_used().rare_data_mut().flex_layout_data = Some(std::rc::Rc::new(data)); } // https://drafts.csswg.org/css-sizing-4/#aspect-ratio-automatic diff --git a/Libraries/LibWeb/Rust/src/layout/formatting_context.rs b/Libraries/LibWeb/Rust/src/layout/formatting_context.rs index 28a0819feb078..37c4866a8dda8 100644 --- a/Libraries/LibWeb/Rust/src/layout/formatting_context.rs +++ b/Libraries/LibWeb/Rust/src/layout/formatting_context.rs @@ -785,10 +785,11 @@ pub(crate) struct ChildLayoutResult { pub table_box_in_wrapper_border_box_block_size: Option, } +#[derive(Clone)] pub(crate) struct RunRootOutcome { cells: UsedValuesCellState, own_metrics_sealed: bool, - line_data: Option, + line_data: Option>, rare: Option, } @@ -807,6 +808,7 @@ impl RunRootOutcome { } } +#[derive(Clone)] pub(crate) struct RunOutputs { pub(crate) result: ChildLayoutResult, pub(crate) root: Option, @@ -866,7 +868,7 @@ pub enum FfiFlexLayoutGrowthState { Shrinking, } -#[derive(Clone, Copy, Debug)] +#[derive(Clone, Copy, Debug, PartialEq)] #[repr(C)] pub struct FfiFlexLayoutItem { pub node: *mut c_void, @@ -906,6 +908,7 @@ pub struct FfiFlexLayoutData { pub line_count: usize, } +#[derive(PartialEq)] pub(crate) struct OwnedFlexLayoutLine { pub(crate) growth_state: FfiFlexLayoutGrowthState, pub(crate) cross_start: CssPixels, @@ -913,6 +916,7 @@ pub(crate) struct OwnedFlexLayoutLine { pub(crate) items: Vec, } +#[derive(PartialEq)] pub(crate) struct OwnedFlexLayoutData { pub(crate) align_content: u8, pub(crate) align_items: u8, @@ -963,8 +967,6 @@ pub struct FfiLayoutFcCallbacks { pub build_svg_facts: unsafe extern "C" fn(*mut c_void, *mut c_void) -> FfiSvgElementFacts, pub read_paintable_geometry: unsafe extern "C" fn(*mut c_void, *mut c_void, *mut c_void, *mut crate::layout::FfiPaintableGeometry) -> bool, - pub read_paintable_svg_transforms: - unsafe extern "C" fn(*mut c_void, *mut c_void, *mut FfiSvgComputedTransforms) -> bool, pub compute_svg_path: unsafe extern "C" fn(*mut c_void, *mut c_void, FfiSvgPathRequest) -> FfiSvgPathResult, pub svg_image_bounding_box: unsafe extern "C" fn(*mut c_void, *mut c_void, CssPixels, CssPixels) -> FfiFloatRect, pub anchor_lookup: unsafe extern "C" fn(*mut c_void, *mut c_void, usize, *const *mut c_void, usize) -> NodeSlotId, @@ -1617,6 +1619,20 @@ fn run_formatting_context( parent_block: Option<&BlockFormattingContext>, ) -> ChildLayoutResult { let root_cells = UsedValuesCellState::capture(parent_used); + let cache_attempt = match FcRunCacheAttempt::probe( + purpose, + box_, + parent_grid.is_some(), + fc_type, + layout_mode, + should_collect_devtools_layout_data, + &callbacks, + &input, + &root_cells, + ) { + Ok(attempt) => attempt, + Err(entry) => return absorb_run_outputs(parent_fragments, parent_used, box_, entry.outputs.clone()), + }; let outputs = execute_formatting_context_run( purpose, root_cells, @@ -1629,6 +1645,7 @@ fn run_formatting_context( input, parent_block, ); + cache_attempt.conclude(&callbacks, box_, &outputs); absorb_run_outputs(parent_fragments, parent_used, box_, outputs) } @@ -2096,6 +2113,7 @@ pub unsafe extern "C" fn rust_layout_run_root_layout( std::ptr::null_mut(), sink, ); + callbacks.arena().sweep_stale_fc_run_cache_entries(); }); } @@ -2217,6 +2235,7 @@ pub unsafe extern "C" fn rust_layout_compute_subtree_layout( paintable_to_replace, sink, ); + callbacks.arena().sweep_stale_fc_run_cache_entries(); }); } @@ -2263,5 +2282,6 @@ pub unsafe extern "C" fn rust_layout_replay_saved_abspos_layout( paintable_to_replace, sink, ); + callbacks.arena().sweep_stale_fc_run_cache_entries(); }); } diff --git a/Libraries/LibWeb/Rust/src/layout/fragment_tree.rs b/Libraries/LibWeb/Rust/src/layout/fragment_tree.rs index ca4ce8a0cba42..1b7b9ef9e91b6 100644 --- a/Libraries/LibWeb/Rust/src/layout/fragment_tree.rs +++ b/Libraries/LibWeb/Rust/src/layout/fragment_tree.rs @@ -22,18 +22,19 @@ pub(crate) struct Fragment { pub(crate) padding_bottom: CssPixels, pub(crate) table_cell_coordinates: Option, pub(crate) override_borders_data: Option, - pub(crate) line_data: Option>, - pub(crate) grid_layout_data: Option, - pub(crate) flex_layout_data: Option, - pub(crate) used_grid_tracks: Option, + pub(crate) line_data: Option>, + pub(crate) grid_layout_data: Option>, + pub(crate) flex_layout_data: Option>, + pub(crate) used_grid_tracks: Option>, pub(crate) computed_svg_transforms: Option, pub(crate) svg_viewport_size: Option, - pub(crate) computed_svg_path: Cell>, + pub(crate) computed_svg_path: Option>, pub(crate) children: Vec, } +#[derive(Clone)] pub(crate) struct FragmentLink { - pub(crate) fragment: Box, + pub(crate) fragment: std::rc::Rc, pub(crate) committed_offset: FfiCssPixelPoint, pub(crate) inset_left: CssPixels, pub(crate) inset_right: CssPixels, @@ -43,6 +44,7 @@ pub(crate) struct FragmentLink { pub(crate) abspos_layout_inputs: Option, } +#[derive(Clone, Copy, Debug, PartialEq, Eq)] pub(crate) struct AnchorCandidate { pub(crate) node: crate::layout::node_data::NodeSlotId, pub(crate) border_box_rect: PhysicalRect, @@ -51,6 +53,7 @@ pub(crate) struct AnchorCandidate { /// The padding-box rect of an inline box that acts as an abspos containing /// block, spanning its first and last content lines. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] pub(crate) struct InlineContainingBlockRect { pub(crate) inline_box: crate::layout::node_data::NodeSlotId, pub(crate) rect: PhysicalRect, @@ -62,6 +65,7 @@ pub(crate) struct InlineContainingBlockRect { /// descendants). The rect is relative to the containing block's own content /// origin, so the contribution never rebases; it travels as-is to whichever /// run drains the child and is joined by child-box identity. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] pub(crate) struct AbsposContainingBlockInfoContribution { pub(crate) child_box: crate::layout::node_data::NodeSlotId, pub(crate) info: AbsposContainingBlockInfo, @@ -151,8 +155,8 @@ fn snapshot_fragment( node: crate::layout::node_data::NodeSlotId, children: Vec, used: &UsedValues, -) -> Box { - let line_data = used.line_data.get().map(|cell| Box::new(cell.take())); +) -> std::rc::Rc { + let line_data = used.line_data.get().map(std::cell::RefCell::take); let rare_payloads = used.rare_data.get().map(|cell| { let mut rare = cell.borrow_mut(); ( @@ -176,7 +180,7 @@ fn snapshot_fragment( svg_viewport_size, computed_svg_path, ) = rare_payloads.unwrap_or_default(); - Box::new(Fragment { + std::rc::Rc::new(Fragment { node, content_inline_size: used.content_inline_size.get(), content_block_size: used.content_block_size.get(), @@ -200,7 +204,7 @@ fn snapshot_fragment( used_grid_tracks, computed_svg_transforms, svg_viewport_size, - computed_svg_path: Cell::new(computed_svg_path), + computed_svg_path, children, }) } @@ -236,7 +240,7 @@ impl PlacementData { } } -fn link_fragment(fragment: Box, placement: PlacementData) -> FragmentLink { +fn link_fragment(fragment: std::rc::Rc, placement: PlacementData) -> FragmentLink { FragmentLink { fragment, committed_offset: placement.committed_offset, @@ -249,6 +253,7 @@ fn link_fragment(fragment: Box, placement: PlacementData) -> FragmentL } } +#[derive(Clone)] pub(crate) struct UnplacedRootFragment { pub(crate) node: crate::layout::node_data::NodeSlotId, pub(crate) scoped_descendants: Vec, diff --git a/Libraries/LibWeb/Rust/src/layout/grid_formatting_context.rs b/Libraries/LibWeb/Rust/src/layout/grid_formatting_context.rs index 0d9bb1cd34417..659b94712fc08 100644 --- a/Libraries/LibWeb/Rust/src/layout/grid_formatting_context.rs +++ b/Libraries/LibWeb/Rust/src/layout/grid_formatting_context.rs @@ -270,7 +270,7 @@ pub struct FfiGridLayoutLine { pub negative_number: i32, } -#[derive(Clone, Copy, Debug)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] #[repr(C)] pub struct FfiGridLayoutTrack { pub start: crate::layout::CssPixels, @@ -288,7 +288,7 @@ pub struct FfiGridLayoutDimension { pub track_count: usize, } -#[derive(Clone, Copy, Debug)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] #[repr(C)] pub struct FfiGridLayoutArea { pub name: usize, @@ -335,6 +335,7 @@ pub struct FfiUsedGridTrackList { pub track_count: usize, } +#[derive(PartialEq, Eq)] pub(crate) struct OwnedGridLayoutLine { pub(crate) names: Vec, pub(crate) start: crate::layout::CssPixels, @@ -358,17 +359,20 @@ impl OwnedGridLayoutLine { } } +#[derive(PartialEq, Eq)] pub(crate) struct OwnedGridLayoutDimension { pub(crate) lines: Vec, pub(crate) tracks: Vec, } +#[derive(PartialEq, Eq)] pub(crate) struct OwnedGridLayoutFragment { pub(crate) areas: Vec, pub(crate) columns: OwnedGridLayoutDimension, pub(crate) rows: OwnedGridLayoutDimension, } +#[derive(PartialEq, Eq)] pub(crate) struct OwnedGridLayoutData { pub(crate) direction: u8, pub(crate) writing_mode: u8, @@ -435,6 +439,7 @@ impl OwnedGridLayoutData { } } +#[derive(PartialEq, Eq)] pub(crate) struct OwnedUsedGridTrackList { pub(crate) is_subgrid: bool, pub(crate) lines: Vec>, @@ -453,6 +458,7 @@ impl OwnedUsedGridTrackList { } } +#[derive(PartialEq, Eq)] pub(crate) struct OwnedUsedGridTracks { pub(crate) columns: OwnedUsedGridTrackList, pub(crate) rows: OwnedUsedGridTrackList, @@ -1124,6 +1130,14 @@ impl ParentGridData { } } +/// Conservative superset of is_subgridded() for callers outside a live grid run +/// (the fc-run-cache probe): a declared subgrid axis counts regardless of the +/// parent-grid placement check only a run in progress can make. +fn grid_template_declares_a_subgrid_axis(callbacks: &FfiLayoutFcCallbacks, box_: Node) -> bool { + let grid_style = ComputedValuesView::new(&callbacks.style_payloads(box_).groups).grid_values(); + grid_style.template_columns.is_subgrid || grid_style.template_rows.is_subgrid +} + impl GridFormattingContext { pub(crate) fn new(run: &FormattingContextRun, parent_grid: Option<&GridFormattingContext>) -> Self { let grid_container = run.box_; @@ -3298,7 +3312,7 @@ impl GridFormattingContext { }; self.container_used() .rare_data_mut() - .used_grid_tracks = Some(tracks); + .used_grid_tracks = Some(std::rc::Rc::new(tracks)); } fn save_devtools_data(&self, grid_style: &GridValues) { @@ -3405,7 +3419,7 @@ impl GridFormattingContext { }; self.container_used() .rare_data_mut() - .grid_layout_data = Some(data); + .grid_layout_data = Some(std::rc::Rc::new(data)); } pub(crate) fn run(&mut self, run: &FormattingContextRun, input: LayoutInput) { diff --git a/Libraries/LibWeb/Rust/src/layout/inline_formatting_context.rs b/Libraries/LibWeb/Rust/src/layout/inline_formatting_context.rs index 20f6aef7c47ec..0eb2cd8971ed5 100644 --- a/Libraries/LibWeb/Rust/src/layout/inline_formatting_context.rs +++ b/Libraries/LibWeb/Rust/src/layout/inline_formatting_context.rs @@ -792,11 +792,11 @@ impl<'context> InlineFormattingContext<'context> { } pub(crate) fn line_data(&self) -> Ref<'_, LineData> { - self.containing_used_values.line_data_cell().borrow() + Ref::map(self.containing_used_values.line_data_cell().borrow(), |shared| &**shared) } pub(crate) fn line_data_mut(&self) -> RefMut<'_, LineData> { - self.containing_used_values.line_data_cell().borrow_mut() + RefMut::map(self.containing_used_values.line_data_cell().borrow_mut(), std::rc::Rc::make_mut) } pub(crate) fn containing_used(&self) -> std::rc::Rc { diff --git a/Libraries/LibWeb/Rust/src/layout/layout_node_arena.rs b/Libraries/LibWeb/Rust/src/layout/layout_node_arena.rs index 692583d7ce8a6..d25324e90213b 100644 --- a/Libraries/LibWeb/Rust/src/layout/layout_node_arena.rs +++ b/Libraries/LibWeb/Rust/src/layout/layout_node_arena.rs @@ -229,6 +229,7 @@ pub(crate) struct LayoutNodeArena { raw_table_column_spans: HashMap, run_used_records: RefCell>, next_run_nonce: Cell, + fc_run_cache_store: crate::layout::FcRunCacheArenaStore, owner_thread: thread::ThreadId, } @@ -249,10 +250,34 @@ impl LayoutNodeArena { raw_table_column_spans: HashMap::new(), run_used_records: RefCell::new(Vec::new()), next_run_nonce: Cell::new(1), + fc_run_cache_store: crate::layout::FcRunCacheArenaStore::default(), owner_thread: thread::current().id(), } } + pub(crate) fn fc_run_cache_store(&self) -> &crate::layout::FcRunCacheArenaStore { + &self.fc_run_cache_store + } + + /// Drops entries whose slot, epoch, or viewport stamp no longer match. + /// Runs at the end of every full pass so invalidated entries whose box + /// never probes again do not accumulate for the document's lifetime. + pub(crate) fn sweep_stale_fc_run_cache_entries(&self) { + let viewport = self.fc_run_cache_store.viewport_size(); + self.fc_run_cache_store.retain_entries(|slot, validity| { + let Some(metadata) = self.slot_metadata.get(slot as usize) else { + return false; + }; + if !metadata.occupied || metadata.generation != validity.slot_generation || viewport != validity.viewport { + return false; + } + let id = NodeSlotId::new(slot, metadata.generation); + // SAFETY: The slot is occupied at the matching generation, so the + // pointer addresses a live NodeData. + unsafe { (*self.data(id)).fragment_cache_epoch == validity.fragment_cache_epoch } + }); + } + fn assert_owner_thread(&self) { debug_assert_eq!(self.owner_thread, thread::current().id()); } @@ -356,6 +381,7 @@ impl LayoutNodeArena { ); *slot = RunRecordSlot::default(); } + self.fc_run_cache_store.remove_entry(index); self.raw_table_column_spans.remove(&id); *self.data_mut(index) = NodeData::default(); @@ -689,13 +715,26 @@ impl LayoutNodeArena { text: Vec, untransformed_text_is_ascii_whitespace: bool, may_require_bidi_processing: bool, - ) { + ) -> bool { self.assert_owner_thread(); self.data(id); let index = id.slot_index() as usize; if self.text_contents.len() <= index { self.text_contents.resize_with(index + 1, TextContentSlot::default); } + let previous = &self.text_contents[index]; + let changed = previous.generation != id.generation() + || match &previous.content { + Some(content) => { + content.text != text + || content.untransformed_text_is_ascii_whitespace != untransformed_text_is_ascii_whitespace + || content.may_require_bidi_processing != may_require_bidi_processing + } + None => true, + }; + if !changed { + return false; + } self.text_contents[index] = TextContentSlot { generation: id.generation(), content: Some(Box::new(TextContent { @@ -707,9 +746,10 @@ impl LayoutNodeArena { if let Some(slot) = self.text_chunk_caches.get_mut().get_mut(index) { *slot = TextChunkCacheSlot::default(); } + true } - pub(crate) fn set_replaced_content_facts(&mut self, id: NodeSlotId, facts: FfiReplacedContentFacts) { + pub(crate) fn set_replaced_content_facts(&mut self, id: NodeSlotId, facts: FfiReplacedContentFacts) -> bool { self.assert_owner_thread(); self.data(id); let index = id.slot_index() as usize; @@ -717,10 +757,13 @@ impl LayoutNodeArena { self.replaced_content_facts .resize_with(index + 1, ReplacedContentFactsSlot::default); } + let previous = &self.replaced_content_facts[index]; + let changed = previous.generation != id.generation() || previous.facts != Some(facts); self.replaced_content_facts[index] = ReplacedContentFactsSlot { generation: id.generation(), facts: Some(facts), }; + changed } pub(crate) fn replaced_content_facts(&self, id: NodeSlotId) -> Option { @@ -944,6 +987,23 @@ pub unsafe extern "C" fn layout_arena_free(arena: *mut c_void, id: NodeSlotId, g }); } +#[unsafe(no_mangle)] +pub extern "C" fn layout_fc_run_cache_epochs_enabled() -> bool { + crate::layout::fc_run_cache_mode_from_environment() != crate::layout::FcRunCacheMode::Disabled +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn layout_arena_fc_run_cache_hit_count(arena: *mut c_void) -> u64 { + abort_on_panic(|| { + assert!(!arena.is_null(), "layout node arena handle is null"); + // SAFETY: The C++ wrapper keeps the arena alive for this call and + // serializes all access on the document thread. + unsafe { &*arena.cast::() } + .fc_run_cache_store() + .hit_count() + }) +} + #[unsafe(no_mangle)] pub unsafe extern "C" fn layout_arena_set_text_content( arena: *mut c_void, @@ -953,7 +1013,7 @@ pub unsafe extern "C" fn layout_arena_set_text_content( length_in_code_units: usize, untransformed_text_is_ascii_whitespace: bool, may_require_bidi_processing: bool, -) { +) -> bool { abort_on_panic(|| { assert!(!arena.is_null(), "layout node arena handle is null"); let text = if length_in_code_units == 0 { @@ -978,8 +1038,8 @@ pub unsafe extern "C" fn layout_arena_set_text_content( text, untransformed_text_is_ascii_whitespace, may_require_bidi_processing, - ); - }); + ) + }) } #[unsafe(no_mangle)] @@ -987,12 +1047,28 @@ pub unsafe extern "C" fn layout_arena_set_replaced_content_facts( arena: *mut c_void, id: NodeSlotId, facts: FfiReplacedContentFacts, +) -> bool { + abort_on_panic(|| { + assert!(!arena.is_null(), "layout node arena handle is null"); + // SAFETY: The C++ wrapper keeps the arena alive for this call and + // serializes all access on the document thread. + unsafe { &mut *arena.cast::() }.set_replaced_content_facts(id, facts) + }) +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn layout_arena_note_viewport_size( + arena: *mut c_void, + viewport_inline_size_raw: i32, + viewport_block_size_raw: i32, ) { abort_on_panic(|| { assert!(!arena.is_null(), "layout node arena handle is null"); // SAFETY: The C++ wrapper keeps the arena alive for this call and // serializes all access on the document thread. - unsafe { &mut *arena.cast::() }.set_replaced_content_facts(id, facts); + unsafe { &*arena.cast::() } + .fc_run_cache_store() + .note_viewport_size(viewport_inline_size_raw, viewport_block_size_raw); }); } @@ -1033,8 +1109,8 @@ mod tests { // SAFETY: The first allocation is still live, and the comparison above // confirms that its pointer still addresses the arena slot. unsafe { - (*first_data).initial_quote_nesting_level = 42; - assert_eq!((*arena.data(first.slot)).initial_quote_nesting_level, 42); + (*first_data).table_column_span = 42; + assert_eq!((*arena.data(first.slot)).table_column_span, 42); } arena.free(first.slot, first.generation); for allocation in allocations { diff --git a/Libraries/LibWeb/Rust/src/layout/line_box.rs b/Libraries/LibWeb/Rust/src/layout/line_box.rs index 8bb25ad0efc11..5c3772a783a83 100644 --- a/Libraries/LibWeb/Rust/src/layout/line_box.rs +++ b/Libraries/LibWeb/Rust/src/layout/line_box.rs @@ -4,7 +4,7 @@ * SPDX-License-Identifier: BSD-2-Clause */ -#[derive(Clone, Copy, Debug)] +#[derive(Clone, Copy, Debug, PartialEq)] pub(crate) struct StaticPositionMarker { pub(crate) box_: Node, pub(crate) inline_offset: CssPixels, @@ -19,7 +19,7 @@ impl StaticPositionMarker { } } -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq)] pub(crate) struct LineBoxData { pub(crate) fragments: Vec, pub(crate) static_position_markers: Vec, diff --git a/Libraries/LibWeb/Rust/src/layout/line_box_fragment.rs b/Libraries/LibWeb/Rust/src/layout/line_box_fragment.rs index 44b70447b29cf..9fb063f1b8cca 100644 --- a/Libraries/LibWeb/Rust/src/layout/line_box_fragment.rs +++ b/Libraries/LibWeb/Rust/src/layout/line_box_fragment.rs @@ -14,7 +14,7 @@ pub(crate) fn is_ascii_space(code_unit: u16) -> bool { matches!(code_unit, 0x09..=0x0d | 0x20) } -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq)] pub(crate) struct GlyphData { pub(crate) glyphs: Vec, pub(crate) font: *const c_void, @@ -25,13 +25,13 @@ pub(crate) struct GlyphData { // The advance of a run's trailing whitespace, recorded at shaping time so that trimming it subtracts exactly what // shaping added. -#[derive(Clone, Copy, Debug, Default)] +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] pub(crate) struct TrailingWhitespace { pub(crate) length_in_code_units: usize, pub(crate) inline_size: CssPixels, } -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq)] pub(crate) struct LineBoxFragmentData { pub(crate) layout_node: Node, pub(crate) style_source: Node, diff --git a/Libraries/LibWeb/Rust/src/layout/mod.rs b/Libraries/LibWeb/Rust/src/layout/mod.rs index d575847031124..33a6994665306 100644 --- a/Libraries/LibWeb/Rust/src/layout/mod.rs +++ b/Libraries/LibWeb/Rust/src/layout/mod.rs @@ -36,6 +36,7 @@ include!("text_chunker.rs"); include!("replaced_with_children_formatting_context.rs"); include!("table_formatting_context.rs"); include!("geometry.rs"); +include!("fc_run_cache.rs"); mod layout_node_arena; pub mod node_data; include!("run_records.rs"); diff --git a/Libraries/LibWeb/Rust/src/layout/node_data.rs b/Libraries/LibWeb/Rust/src/layout/node_data.rs index d803ba0586664..ba3e85f117bd5 100644 --- a/Libraries/LibWeb/Rust/src/layout/node_data.rs +++ b/Libraries/LibWeb/Rust/src/layout/node_data.rs @@ -14,7 +14,7 @@ pub const GENERATED_FOR_MARKER: u8 = 6; // count so the style container array and the registered group indices line up. pub const STYLE_GROUP_COUNT: usize = 23; -#[derive(Clone, Copy, Default)] +#[derive(Clone, Copy, Default, PartialEq, Eq)] #[repr(C)] pub struct FfiReplacedContentFacts { pub has_auto_content_width: bool, @@ -159,6 +159,7 @@ pub enum NodeFlag { ProducesLineBoxFragmentWhenEmpty = 1 << 22, ListMarkerIsInside = 1 << 23, HasAnchorNames = 1 << 24, + InsetsUseAnchorFunctions = 1 << 25, } #[repr(C)] @@ -174,7 +175,13 @@ pub struct NodeData { pub generated_for: u8, pub intrinsic_cache_epoch: u16, pub flags: u32, - pub initial_quote_nesting_level: u32, + /// Advanced on every layout invalidation that reaches this node or its + /// subtree, with no propagation boundary: unlike the intrinsic epoch, + /// changes inside absolutely positioned and SVG descendants must reach + /// every ancestor, because their fragments live in ancestor run trees. + /// Wide enough that wrapping between a cache store and the next probe + /// is unreachable. + pub fragment_cache_epoch: u32, pub slot_generation: u8, pub table_column_span: u16, pub table_row_span: u16, @@ -196,10 +203,10 @@ impl Default for NodeData { generated_for: 0, intrinsic_cache_epoch: 0, flags: 0, - initial_quote_nesting_level: 0, slot_generation: 0, table_column_span: 1, table_row_span: 1, + fragment_cache_epoch: 0, style: std::ptr::null(), shell: std::ptr::null_mut(), } @@ -221,7 +228,10 @@ mod tests { assert_eq!(std::mem::size_of::(), 64); assert_eq!(std::mem::offset_of!(NodeData, intrinsic_cache_epoch), 30); assert_eq!(std::mem::offset_of!(NodeData, flags), 32); + assert_eq!(std::mem::offset_of!(NodeData, fragment_cache_epoch), 36); assert_eq!(std::mem::offset_of!(NodeData, slot_generation), 40); + assert_eq!(std::mem::offset_of!(NodeData, table_column_span), 42); + assert_eq!(std::mem::offset_of!(NodeData, table_row_span), 44); assert_eq!(std::mem::offset_of!(NodeData, style), 48); assert_eq!(std::mem::offset_of!(NodeData, shell), 56); } diff --git a/Libraries/LibWeb/Rust/src/layout/svg_formatting_context.rs b/Libraries/LibWeb/Rust/src/layout/svg_formatting_context.rs index 4a21d809e7f9f..010def9b7a77e 100644 --- a/Libraries/LibWeb/Rust/src/layout/svg_formatting_context.rs +++ b/Libraries/LibWeb/Rust/src/layout/svg_formatting_context.rs @@ -494,25 +494,10 @@ impl SvgFormattingContext { } fn computed_transforms(&self, node: Node) -> Option { - if let Some(transforms) = self - .used_values(node) + self.used_values(node) .rare_data .get() .and_then(|cell| cell.borrow().computed_svg_transforms) - { - return Some(transforms); - } - let mut transforms = FfiSvgComputedTransforms::default(); - // SAFETY: `transforms` is writable POD storage and the callback reads - // only paintable geometry retained by the C++ layout node. - let has_transforms = unsafe { - (self.callbacks.read_paintable_svg_transforms)( - self.callbacks.context, - self.callbacks.shell(node), - &raw mut transforms, - ) - }; - has_transforms.then_some(transforms) } fn set_computed_transforms(&self, node: Node, transforms: FfiSvgComputedTransforms) { @@ -967,7 +952,7 @@ impl SvgFormattingContext { let used = &used_pointer; used.set_content_inline_size(transformed_bounding_box.width); used.set_content_block_size(transformed_bounding_box.height); - self.used_values(graphics_box).rare_data_mut().computed_svg_path = Some(path); + self.used_values(graphics_box).rare_data_mut().computed_svg_path = Some(std::rc::Rc::new(path)); self.place_child(graphics_box, transformed_bounding_box.x, transformed_bounding_box.y); used.has_definite_inline_size.set(true); used.has_definite_block_size.set(true); diff --git a/Libraries/LibWeb/Rust/src/layout/tree_builder.rs b/Libraries/LibWeb/Rust/src/layout/tree_builder.rs index 62022a2fab1aa..5c90877eb8fa4 100644 --- a/Libraries/LibWeb/Rust/src/layout/tree_builder.rs +++ b/Libraries/LibWeb/Rust/src/layout/tree_builder.rs @@ -1698,7 +1698,7 @@ pub struct FfiPseudoTreeBuilderCallbacks { pub attach_style_resources: unsafe extern "C" fn(*mut c_void), pub apply_replaced_display_adjustment: unsafe extern "C" fn(*mut c_void, FfiReplacedElementDisplayAdjustment), pub create_nested_list_marker: unsafe extern "C" fn(*mut c_void, *mut c_void, *mut c_void, FfiPseudoElement), - pub configure_layout_node: unsafe extern "C" fn(*mut c_void, *mut c_void, FfiPseudoElement, u32), + pub configure_layout_node: unsafe extern "C" fn(*mut c_void, *mut c_void, FfiPseudoElement), pub resolve_content: unsafe extern "C" fn(*mut c_void, *mut c_void, FfiPseudoElement, u32) -> FfiResolvedPseudoContentFacts, pub create_content_item: unsafe extern "C" fn(*mut c_void, *mut c_void, FfiPseudoElement, usize) -> NodeSlotId, @@ -1830,7 +1830,7 @@ fn create_pseudo_element_with_frame( let initial_quote_nesting_level = state.quote_nesting_level; // SAFETY: The frame and element remain live throughout configuration. - unsafe { (callbacks.configure_layout_node)(frame, element, pseudo_element, initial_quote_nesting_level) }; + unsafe { (callbacks.configure_layout_node)(frame, element, pseudo_element) }; let layout_node_kind = host.layout().data(layout_node).kind; // https://drafts.csswg.org/css-lists-3/#list-style-position-outside // "the marker box is a block container and is placed outside the principal block box" diff --git a/Libraries/LibWeb/Rust/src/layout/used_values.rs b/Libraries/LibWeb/Rust/src/layout/used_values.rs index 15056c9896ed2..4492effac4baf 100644 --- a/Libraries/LibWeb/Rust/src/layout/used_values.rs +++ b/Libraries/LibWeb/Rust/src/layout/used_values.rs @@ -114,21 +114,21 @@ impl SealableCell { } } -#[derive(Default)] +#[derive(Clone, Default, PartialEq)] pub(crate) struct LineData { pub(crate) line_boxes: Vec, pub(crate) inline_box_pieces: Vec, } -#[derive(Default)] +#[derive(Clone, Default)] pub(crate) struct UsedValuesRareData { pub(crate) table_cell_coordinates: Option, - pub(crate) computed_svg_path: Option, + pub(crate) computed_svg_path: Option>, pub(crate) computed_svg_transforms: Option, pub(crate) svg_viewport_size: Option, - pub(crate) grid_layout_data: Option, - pub(crate) flex_layout_data: Option, - pub(crate) used_grid_tracks: Option, + pub(crate) grid_layout_data: Option>, + pub(crate) flex_layout_data: Option>, + pub(crate) used_grid_tracks: Option>, pub(crate) override_borders_data: Option, pub(crate) abspos_layout_inputs: Option, } @@ -227,7 +227,7 @@ pub(crate) struct UsedValues { pub last_baseline: Cell, - pub(crate) line_data: LazyRefCell, + pub(crate) line_data: LazyRefCell>, pub(crate) rare_data: LazyRefCell, } @@ -276,11 +276,11 @@ impl UsedValues { } pub(crate) fn line_data_ref(&self) -> Option> { - self.line_data.get().map(RefCell::borrow) + self.line_data.get().map(|cell| Ref::map(cell.borrow(), |shared| &**shared)) } - pub(crate) fn line_data_cell(&self) -> &RefCell { - self.line_data.get_or_init(LineData::default) + pub(crate) fn line_data_cell(&self) -> &RefCell> { + self.line_data.get_or_init(std::rc::Rc::default) } pub(crate) fn content_baselines_from_cells(&self) -> crate::layout::DerivedBaselines { diff --git a/Tests/LibWeb/Text/expected/layout-run-cache-animated-font-size.txt b/Tests/LibWeb/Text/expected/layout-run-cache-animated-font-size.txt new file mode 100644 index 0000000000000..995cbb1881694 --- /dev/null +++ b/Tests/LibWeb/Text/expected/layout-run-cache-animated-font-size.txt @@ -0,0 +1,3 @@ +grew: true +height before: 24 +height during: 72 diff --git a/Tests/LibWeb/Text/expected/layout-run-cache-anonymous-cell-font-size.txt b/Tests/LibWeb/Text/expected/layout-run-cache-anonymous-cell-font-size.txt new file mode 100644 index 0000000000000..d8dc450c26af4 --- /dev/null +++ b/Tests/LibWeb/Text/expected/layout-run-cache-anonymous-cell-font-size.txt @@ -0,0 +1,3 @@ +grew: true +height before: 12 +height after: 48 diff --git a/Tests/LibWeb/Text/expected/layout-run-cache-background-change-preserved.txt b/Tests/LibWeb/Text/expected/layout-run-cache-background-change-preserved.txt new file mode 100644 index 0000000000000..d0f3fef1ae8f0 --- /dev/null +++ b/Tests/LibWeb/Text/expected/layout-run-cache-background-change-preserved.txt @@ -0,0 +1 @@ +hit delta after paint-only change: 1 diff --git a/Tests/LibWeb/Text/expected/layout-run-cache-filter-change-misses.txt b/Tests/LibWeb/Text/expected/layout-run-cache-filter-change-misses.txt new file mode 100644 index 0000000000000..4870161014d4c --- /dev/null +++ b/Tests/LibWeb/Text/expected/layout-run-cache-filter-change-misses.txt @@ -0,0 +1 @@ +hit delta after filter change: 0 diff --git a/Tests/LibWeb/Text/expected/layout-run-cache-flex-baseline.txt b/Tests/LibWeb/Text/expected/layout-run-cache-flex-baseline.txt new file mode 100644 index 0000000000000..014d0474380f1 --- /dev/null +++ b/Tests/LibWeb/Text/expected/layout-run-cache-flex-baseline.txt @@ -0,0 +1,3 @@ +hit delta: >=1 +big moved by: 30 +big rect: 9.703125,80,32 diff --git a/Tests/LibWeb/Text/expected/layout-run-cache-inline-block-auto-height.txt b/Tests/LibWeb/Text/expected/layout-run-cache-inline-block-auto-height.txt new file mode 100644 index 0000000000000..5461dce57e229 --- /dev/null +++ b/Tests/LibWeb/Text/expected/layout-run-cache-inline-block-auto-height.txt @@ -0,0 +1,2 @@ +hit delta: 1 +cached rect: 60,150,45 diff --git a/Tests/LibWeb/Text/expected/layout-run-cache-inside-mutation-misses.txt b/Tests/LibWeb/Text/expected/layout-run-cache-inside-mutation-misses.txt new file mode 100644 index 0000000000000..1ba9f5b2406dc --- /dev/null +++ b/Tests/LibWeb/Text/expected/layout-run-cache-inside-mutation-misses.txt @@ -0,0 +1,2 @@ +hit delta: 0 +cached height: 70 diff --git a/Tests/LibWeb/Text/expected/layout-run-cache-lang-change-misses.txt b/Tests/LibWeb/Text/expected/layout-run-cache-lang-change-misses.txt new file mode 100644 index 0000000000000..6c37ab60981ef --- /dev/null +++ b/Tests/LibWeb/Text/expected/layout-run-cache-lang-change-misses.txt @@ -0,0 +1 @@ +hit delta after lang change: 0 diff --git a/Tests/LibWeb/Text/expected/layout-run-cache-scroll-preserved.txt b/Tests/LibWeb/Text/expected/layout-run-cache-scroll-preserved.txt new file mode 100644 index 0000000000000..6b0743e647dce --- /dev/null +++ b/Tests/LibWeb/Text/expected/layout-run-cache-scroll-preserved.txt @@ -0,0 +1,5 @@ +hit delta: 1 +scrollTop: 120 +scrollHeight: 400 +cached top: 90 +scrollTop after set: 300 diff --git a/Tests/LibWeb/Text/expected/layout-run-cache-sibling-growth.txt b/Tests/LibWeb/Text/expected/layout-run-cache-sibling-growth.txt new file mode 100644 index 0000000000000..2e65a82ce3914 --- /dev/null +++ b/Tests/LibWeb/Text/expected/layout-run-cache-sibling-growth.txt @@ -0,0 +1,3 @@ +hit delta: 1 +deep before: 0,60,60,20 +deep after: 0,90,60,20 diff --git a/Tests/LibWeb/Text/expected/layout-run-cache-width-change-misses.txt b/Tests/LibWeb/Text/expected/layout-run-cache-width-change-misses.txt new file mode 100644 index 0000000000000..c1aeb66c7532d --- /dev/null +++ b/Tests/LibWeb/Text/expected/layout-run-cache-width-change-misses.txt @@ -0,0 +1,2 @@ +hit delta: 0 +inner width: 100 diff --git a/Tests/LibWeb/Text/input/layout-run-cache-animated-font-size.html b/Tests/LibWeb/Text/input/layout-run-cache-animated-font-size.html new file mode 100644 index 0000000000000..75b1aac727c21 --- /dev/null +++ b/Tests/LibWeb/Text/input/layout-run-cache-animated-font-size.html @@ -0,0 +1,31 @@ + + + +
words words words
+ diff --git a/Tests/LibWeb/Text/input/layout-run-cache-anonymous-cell-font-size.html b/Tests/LibWeb/Text/input/layout-run-cache-anonymous-cell-font-size.html new file mode 100644 index 0000000000000..a23aa136c9b0e --- /dev/null +++ b/Tests/LibWeb/Text/input/layout-run-cache-anonymous-cell-font-size.html @@ -0,0 +1,26 @@ + + + +
words words words
+ diff --git a/Tests/LibWeb/Text/input/layout-run-cache-background-change-preserved.html b/Tests/LibWeb/Text/input/layout-run-cache-background-change-preserved.html new file mode 100644 index 0000000000000..ba017309b3817 --- /dev/null +++ b/Tests/LibWeb/Text/input/layout-run-cache-background-change-preserved.html @@ -0,0 +1,32 @@ + + + +
+
+
+
+ diff --git a/Tests/LibWeb/Text/input/layout-run-cache-filter-change-misses.html b/Tests/LibWeb/Text/input/layout-run-cache-filter-change-misses.html new file mode 100644 index 0000000000000..585bb685a33cf --- /dev/null +++ b/Tests/LibWeb/Text/input/layout-run-cache-filter-change-misses.html @@ -0,0 +1,32 @@ + + + +
+
+
+
+ diff --git a/Tests/LibWeb/Text/input/layout-run-cache-flex-baseline.html b/Tests/LibWeb/Text/input/layout-run-cache-flex-baseline.html new file mode 100644 index 0000000000000..5cd0fedb03888 --- /dev/null +++ b/Tests/LibWeb/Text/input/layout-run-cache-flex-baseline.html @@ -0,0 +1,37 @@ + + + +
+
x
A
b
+ diff --git a/Tests/LibWeb/Text/input/layout-run-cache-inline-block-auto-height.html b/Tests/LibWeb/Text/input/layout-run-cache-inline-block-auto-height.html new file mode 100644 index 0000000000000..7ab4dcfb8fd02 --- /dev/null +++ b/Tests/LibWeb/Text/input/layout-run-cache-inline-block-auto-height.html @@ -0,0 +1,33 @@ + + + +
+
+ diff --git a/Tests/LibWeb/Text/input/layout-run-cache-inside-mutation-misses.html b/Tests/LibWeb/Text/input/layout-run-cache-inside-mutation-misses.html new file mode 100644 index 0000000000000..f6cf452caaf6b --- /dev/null +++ b/Tests/LibWeb/Text/input/layout-run-cache-inside-mutation-misses.html @@ -0,0 +1,32 @@ + + + +
+
+
+ diff --git a/Tests/LibWeb/Text/input/layout-run-cache-lang-change-misses.html b/Tests/LibWeb/Text/input/layout-run-cache-lang-change-misses.html new file mode 100644 index 0000000000000..ab829c50c3bb6 --- /dev/null +++ b/Tests/LibWeb/Text/input/layout-run-cache-lang-change-misses.html @@ -0,0 +1,30 @@ + + + +
+
istanbul
+ diff --git a/Tests/LibWeb/Text/input/layout-run-cache-scroll-preserved.html b/Tests/LibWeb/Text/input/layout-run-cache-scroll-preserved.html new file mode 100644 index 0000000000000..667d037ed8c36 --- /dev/null +++ b/Tests/LibWeb/Text/input/layout-run-cache-scroll-preserved.html @@ -0,0 +1,43 @@ + + + +
+
+
+
+ diff --git a/Tests/LibWeb/Text/input/layout-run-cache-sibling-growth.html b/Tests/LibWeb/Text/input/layout-run-cache-sibling-growth.html new file mode 100644 index 0000000000000..5914c01169dfa --- /dev/null +++ b/Tests/LibWeb/Text/input/layout-run-cache-sibling-growth.html @@ -0,0 +1,42 @@ + + + +
+
+
+
+
+
+ diff --git a/Tests/LibWeb/Text/input/layout-run-cache-width-change-misses.html b/Tests/LibWeb/Text/input/layout-run-cache-width-change-misses.html new file mode 100644 index 0000000000000..c3916463a0ab1 --- /dev/null +++ b/Tests/LibWeb/Text/input/layout-run-cache-width-change-misses.html @@ -0,0 +1,34 @@ + + + +
+
+
+
+
+