diff --git a/Libraries/LibWeb/Layout/LayoutRustBridge.cpp b/Libraries/LibWeb/Layout/LayoutRustBridge.cpp index 757b45cce300a..1b512a9fa585e 100644 --- a/Libraries/LibWeb/Layout/LayoutRustBridge.cpp +++ b/Libraries/LibWeb/Layout/LayoutRustBridge.cpp @@ -642,18 +642,6 @@ 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); @@ -663,7 +651,6 @@ 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(); { @@ -688,7 +675,6 @@ void LayoutRustBridge::compute_subtree_layout(Box& root) }; 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(); @@ -714,7 +700,6 @@ void LayoutRustBridge::replay_saved_abspos_layout(Box& box) }; box.document().invalidate_stacking_context_tree(); - note_viewport_size_for_pass(box); auto callbacks = formatting_context_callbacks(); auto sink = commit_sink(); { @@ -755,6 +740,7 @@ RustFFI::FfiCommitSink LayoutRustBridge::commit_sink() VERIFY(!bridge.m_replaced_paintable); VERIFY(!bridge.m_commit_parent_paintable); VERIFY(!bridge.m_commit_insert_before_paintable); + VERIFY(bridge.m_reused_paintables.is_empty()); if (!root.is_viewport()) { bridge.m_replaced_paintable = root.paintable(); @@ -780,10 +766,17 @@ RustFFI::FfiCommitSink LayoutRustBridge::commit_sink() }; }, .finish_commit = [](void* context) { auto& bridge = *static_cast(context); + for (auto& reused : bridge.m_reused_paintables) { + auto new_absolute_position = reused.paintable->absolute_position(); + if (new_absolute_position != reused.old_absolute_position) + reused.paintable->translate_reused_subtree_absolute_geometry(new_absolute_position - reused.old_absolute_position); + } + bridge.m_reused_paintables.clear(); bridge.m_commit_insert_before_paintable = nullptr; bridge.m_commit_parent_paintable = nullptr; bridge.m_replaced_paintable = nullptr; }, - .prepare_node = [](void*, void* node_pointer, bool has_used_values) -> void* { + .prepare_node = [](void* context, void* node_pointer, bool has_used_values, bool reuses_committed_subtree) -> void* { + auto& bridge = *static_cast(context); auto& node = *static_cast(node_pointer); RefPtr paintable; @@ -791,10 +784,17 @@ RustFFI::FfiCommitSink LayoutRustBridge::commit_sink() // Inline boxes that never went through inline layout (so they have no used values) still // need a paintable so DOM geometry queries have something to answer from. paintable = node.paintable(); - if (paintable) + if (reuses_committed_subtree) { + VERIFY(paintable); + bridge.m_reused_paintables.append({ *paintable, paintable->absolute_position() }); + if (paintable->parent()) + paintable->remove(); + paintable->set_containing_block(nullptr); + } else if (paintable) { paintable->reset_for_relayout(); - else + } else { paintable = node.create_paintable(); + } node.set_paintable(paintable); } else if (node.paintable_ptr()) { // A paintable surviving from a previous layout on a node this pass did not lay out is @@ -831,9 +831,14 @@ RustFFI::FfiCommitSink LayoutRustBridge::commit_sink() CSSPixels::from_raw(metrics.margin_bottom), CSSPixels::from_raw(metrics.margin_left), }; - paintable.set_content_size( + CSSPixelSize content_size { CSSPixels::from_raw(metrics.content_inline_size), - CSSPixels::from_raw(metrics.content_block_size)); + CSSPixels::from_raw(metrics.content_block_size) + }; + if (metrics.reuses_committed_subtree) + VERIFY(paintable.content_size() == content_size); + else + paintable.set_content_size(content_size); paintable.set_offset({ CSSPixels::from_raw(metrics.content_offset.x), CSSPixels::from_raw(metrics.content_offset.y), diff --git a/Libraries/LibWeb/Layout/LayoutRustBridge.h b/Libraries/LibWeb/Layout/LayoutRustBridge.h index 9070bfd034266..f61d895125f05 100644 --- a/Libraries/LibWeb/Layout/LayoutRustBridge.h +++ b/Libraries/LibWeb/Layout/LayoutRustBridge.h @@ -10,6 +10,7 @@ #include #include #include +#include #include #include #include @@ -41,11 +42,16 @@ class LayoutRustBridge { [[nodiscard]] RustFFI::FfiCommitSink commit_sink(); struct LineCommitContext; + struct ReusedPaintable { + NonnullRefPtr paintable; + CSSPixelPoint old_absolute_position; + }; Box const* m_commit_root { nullptr }; OwnPtr m_line_commit_context; RefPtr m_replaced_paintable; RefPtr m_commit_parent_paintable; RefPtr m_commit_insert_before_paintable; + Vector m_reused_paintables; }; [[nodiscard]] Optional formatting_context_type_created_by_box(Box const&); diff --git a/Libraries/LibWeb/Layout/Node.cpp b/Libraries/LibWeb/Layout/Node.cpp index 072769533f5df..a29f2af2c6553 100644 --- a/Libraries/LibWeb/Layout/Node.cpp +++ b/Libraries/LibWeb/Layout/Node.cpp @@ -153,7 +153,15 @@ void* Node::arena_handle() const void Node::synchronize_topology() { - m_data->parent = slot_id(Base::parent_ptr()); + auto old_parent = m_data->parent; + auto new_parent = slot_id(Base::parent_ptr()); + if (old_parent.index != new_parent.index) { + if (old_parent.index != RustFFI::NodeSlotId_INVALID.index) + node_arena().note_inline_layout_damage(old_parent); + if (new_parent.index != RustFFI::NodeSlotId_INVALID.index) + node_arena().note_inline_layout_damage(new_parent); + } + m_data->parent = new_parent; m_data->first_child = slot_id(Base::first_child_ptr()); m_data->last_child = slot_id(Base::last_child_ptr()); m_data->previous_sibling = slot_id(Base::previous_sibling_ptr()); diff --git a/Libraries/LibWeb/Layout/NodeArena.cpp b/Libraries/LibWeb/Layout/NodeArena.cpp index 975afb02d691e..f4601bda9d9e9 100644 --- a/Libraries/LibWeb/Layout/NodeArena.cpp +++ b/Libraries/LibWeb/Layout/NodeArena.cpp @@ -40,6 +40,11 @@ u64 NodeArena::formatting_context_run_cache_hit_count() const return RustFFI::layout_arena_fc_run_cache_hit_count(m_handle); } +void NodeArena::note_inline_layout_damage(RustFFI::NodeSlotId box) +{ + RustFFI::layout_arena_note_inline_layout_damage(m_handle, box); +} + 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()); diff --git a/Libraries/LibWeb/Layout/NodeArena.h b/Libraries/LibWeb/Layout/NodeArena.h index bd26ee637271d..4fef3a7b798dd 100644 --- a/Libraries/LibWeb/Layout/NodeArena.h +++ b/Libraries/LibWeb/Layout/NodeArena.h @@ -36,6 +36,7 @@ class WEB_API NodeArena : public RefCounted { void free(RustFFI::NodeSlotId, u32 generation); void* handle() const { return m_handle; } u64 formatting_context_run_cache_hit_count() const; + void note_inline_layout_damage(RustFFI::NodeSlotId); void enroll_text_node_for_content_sync(TextNode const&); void enroll_node_for_replaced_content_facts_sync(Node const&); diff --git a/Libraries/LibWeb/Painting/Paintable.cpp b/Libraries/LibWeb/Painting/Paintable.cpp index 9b5d977164e30..0c7d3634a3510 100644 --- a/Libraries/LibWeb/Painting/Paintable.cpp +++ b/Libraries/LibWeb/Painting/Paintable.cpp @@ -1424,6 +1424,18 @@ void Paintable::invalidate_absolute_geometry_cache(InvalidateDescendantGeometry }); } +void Paintable::translate_reused_subtree_absolute_geometry(CSSPixelPoint delta) +{ + for_each_in_inclusive_subtree([&](Paintable& paintable) { + paintable.invalidate_absolute_geometry_cache(InvalidateDescendantGeometry::No); + if (paintable.m_overflow_data.has_value()) + paintable.m_overflow_data->scrollable_overflow_rect.translate_by(delta); + // Recorded paint commands bake absolute coordinates. + paintable.invalidate_paint_cache(); + return TraversalDecision::Continue; + }); +} + CSSPixelPoint Paintable::offset() const { return m_offset; diff --git a/Libraries/LibWeb/Painting/Paintable.h b/Libraries/LibWeb/Painting/Paintable.h index 93a16a4a0277e..2ae5357c34fa1 100644 --- a/Libraries/LibWeb/Painting/Paintable.h +++ b/Libraries/LibWeb/Painting/Paintable.h @@ -531,6 +531,7 @@ class WEB_API Paintable void paint_middle_button_scroll_indicator(DisplayListRecordingContext&) const; void invalidate_absolute_geometry_cache(InvalidateDescendantGeometry); + void translate_reused_subtree_absolute_geometry(CSSPixelPoint); GC::Weak m_dom_node; WeakPtr m_layout_node; diff --git a/Libraries/LibWeb/Rust/src/layout/abspos_engine.rs b/Libraries/LibWeb/Rust/src/layout/abspos_engine.rs index 22a642a8169e1..6e5d91b09fe01 100644 --- a/Libraries/LibWeb/Rust/src/layout/abspos_engine.rs +++ b/Libraries/LibWeb/Rust/src/layout/abspos_engine.rs @@ -1921,6 +1921,7 @@ pub(crate) fn drain_abspos_with_placed_containing_blocks( should_collect_devtools_layout_data, treat_block_axis_percentage_insets_as_auto_beyond_root: false, fragments: Some(entry_fragments.clone()), + previous_line_data: None, }; loop { let batch = entry_fragments.take_drainable_abspos(accumulator_root, records, &callbacks); diff --git a/Libraries/LibWeb/Rust/src/layout/block_formatting_context.rs b/Libraries/LibWeb/Rust/src/layout/block_formatting_context.rs index f3a53d65c576d..2d8f2c14d2e4e 100644 --- a/Libraries/LibWeb/Rust/src/layout/block_formatting_context.rs +++ b/Libraries/LibWeb/Rust/src/layout/block_formatting_context.rs @@ -197,9 +197,11 @@ pub(crate) struct BlockFormattingContext { derived_baselines_of_root_box: Cell, trailing_collapsed_margin: Cell>, table_box_in_wrapper_border_box_block_size: Cell>, + min_content_inline_size_from_max_content_layout: Cell>, fragments: Option>, should_collect_devtools_layout_data: bool, treat_block_axis_percentage_insets_as_auto_beyond_root: bool, + previous_line_data: Option>, } impl BlockFormattingContext { @@ -222,8 +224,10 @@ impl BlockFormattingContext { derived_baselines_of_root_box: Cell::new(DerivedBaselines::default()), trailing_collapsed_margin: Cell::new(None), table_box_in_wrapper_border_box_block_size: Cell::new(None), + min_content_inline_size_from_max_content_layout: Cell::new(None), should_collect_devtools_layout_data: run.should_collect_devtools_layout_data, treat_block_axis_percentage_insets_as_auto_beyond_root: run.treat_block_axis_percentage_insets_as_auto_beyond_root, + previous_line_data: run.previous_line_data.clone(), } } @@ -237,6 +241,7 @@ impl BlockFormattingContext { should_collect_devtools_layout_data: self.should_collect_devtools_layout_data, treat_block_axis_percentage_insets_as_auto_beyond_root: self.treat_block_axis_percentage_insets_as_auto_beyond_root, fragments: self.fragments.clone(), + previous_line_data: self.previous_line_data.clone(), } } @@ -293,7 +298,7 @@ impl BlockFormattingContext { ancestor == node || self.is_ancestor_of(ancestor, node) } - fn sizing(&self) -> SizingContext { + pub(crate) fn sizing(&self) -> SizingContext { SizingContext::new(self.purpose, self.records.clone(), self.callbacks) } @@ -2550,6 +2555,10 @@ impl BlockFormattingContext { context.run(); let automatic_inline_size = context.automatic_content_inline_size; let automatic_block_size = context.automatic_content_block_size; + if block_container == self.root { + self.min_content_inline_size_from_max_content_layout + .set(context.min_content_inline_size_from_max_content_layout); + } if !self.used(block_container).has_definite_inline_size() { // NOTE: min-width or max-width for boxes with inline children can only be applied after inside layout // is done and the inline size of the box content is known @@ -2843,6 +2852,10 @@ impl BlockFormattingContext { self.greatest_child_inline_size_including_floats(self.root) } + pub(crate) fn min_content_inline_size_from_max_content_layout(&self) -> Option { + self.min_content_inline_size_from_max_content_layout.get() + } + pub(crate) fn automatic_content_block_size(&self) -> CssPixels { automatic_block_size_for_bfc_root( &self.records, diff --git a/Libraries/LibWeb/Rust/src/layout/commit.rs b/Libraries/LibWeb/Rust/src/layout/commit.rs index 83b16c52eb484..433fb9cb0fab3 100644 --- a/Libraries/LibWeb/Rust/src/layout/commit.rs +++ b/Libraries/LibWeb/Rust/src/layout/commit.rs @@ -17,6 +17,7 @@ pub struct FfiTableCellCoordinates { #[repr(C)] pub struct FfiCommittedBoxMetrics { pub fragment_identity: u64, + pub reuses_committed_subtree: bool, pub content_offset: crate::layout::FfiCssPixelPoint, pub content_inline_size: crate::layout::CssPixels, pub content_block_size: crate::layout::CssPixels, @@ -85,7 +86,7 @@ pub struct FfiCommitSink { pub context: *mut c_void, pub begin_commit: unsafe extern "C" fn(*mut c_void, *mut c_void) -> FfiCommitPosition, pub finish_commit: unsafe extern "C" fn(*mut c_void), - pub prepare_node: unsafe extern "C" fn(*mut c_void, *mut c_void, bool) -> *mut c_void, + pub prepare_node: unsafe extern "C" fn(*mut c_void, *mut c_void, bool, bool) -> *mut c_void, pub set_box_metrics: unsafe extern "C" fn(*mut c_void, *mut c_void, FfiCommittedBoxMetrics), pub set_override_borders: unsafe extern "C" fn(*mut c_void, *mut c_void, FfiBordersData), pub set_table_cell_coordinates: unsafe extern "C" fn(*mut c_void, *mut c_void, FfiTableCellCoordinates), @@ -116,6 +117,8 @@ fn commit_subtree( ) { let slot_index = callbacks.slot_index(node); let entry = scopes.link_for_slot(slot_index); + let reuses_committed_subtree = scopes.subtree_was_reused(slot_index); + debug_assert!(!reuses_committed_subtree || entry.is_some()); if let Some(link) = entry { callbacks.set_saved_abspos_layout_inputs(node, link.abspos_layout_inputs); // SVG roots are the only non-abspos partial relayout boundaries; save their committed @@ -156,7 +159,14 @@ fn commit_subtree( // SAFETY: The C++ sink owns paintables and copies every plain-data // input synchronously. let node_shell = callbacks.shell(node); - let paintable = unsafe { (sink.prepare_node)(sink.context, node_shell, entry.is_some()) }; + let paintable = unsafe { + (sink.prepare_node)( + sink.context, + node_shell, + entry.is_some(), + reuses_committed_subtree, + ) + }; let mut has_pending_inline_box_geometry = false; if let Some(link) = entry @@ -171,6 +181,7 @@ fn commit_subtree( paintable, FfiCommittedBoxMetrics { fragment_identity: fragment.identity, + reuses_committed_subtree, content_offset: link.committed_offset, content_inline_size: fragment.content_inline_size, content_block_size: fragment.content_block_size, @@ -196,16 +207,18 @@ fn commit_subtree( ); } - unsafe { + if !reuses_committed_subtree { + unsafe { if let Some(borders) = fragment.override_borders_data { (sink.set_override_borders)(sink.context, paintable, borders); } if let Some(coordinates) = fragment.table_cell_coordinates { (sink.set_table_cell_coordinates)(sink.context, paintable, coordinates); } + } } - if let Some(line_data) = &fragment.line_data { + if !reuses_committed_subtree && let Some(line_data) = &fragment.line_data { // SAFETY: The sink keeps one line accumulator live between // begin_line_data() and finish_line_data(). let accepts_lines = unsafe { (sink.begin_line_data)(sink.context, paintable) }; @@ -224,7 +237,8 @@ fn commit_subtree( } } - unsafe { + if !reuses_committed_subtree { + unsafe { if let Some(transform) = fragment.svg_viewport_transform { (sink.set_svg_viewport_transform)(sink.context, paintable, transform); } @@ -245,18 +259,19 @@ fn commit_subtree( 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 { + if !reuses_committed_subtree && let Some(data) = &fragment.grid_layout_data { data.with_ffi_view(|view| { unsafe { (sink.set_grid_layout_data)(sink.context, paintable, view) }; }); } - if let Some(data) = &fragment.flex_layout_data { + if !reuses_committed_subtree && let Some(data) = &fragment.flex_layout_data { data.with_ffi_view(|view| { unsafe { (sink.set_flex_layout_data)(sink.context, paintable, view) }; }); } - if let Some(tracks) = &fragment.used_grid_tracks { + if !reuses_committed_subtree && let Some(tracks) = &fragment.used_grid_tracks { tracks.with_ffi_views(|columns, rows| { unsafe { (sink.set_used_grid_tracks)(sink.context, paintable, columns, rows) }; }); @@ -276,6 +291,10 @@ fn commit_subtree( }; assert_eq!(result.paintable, paintable); + if reuses_committed_subtree { + return; + } + if let Some(link) = entry { scopes.open_scope(&link.fragment.children); } diff --git a/Libraries/LibWeb/Rust/src/layout/fc_run_cache.rs b/Libraries/LibWeb/Rust/src/layout/fc_run_cache.rs index 7aeb9084fa4f0..f1dfbb5c770c3 100644 --- a/Libraries/LibWeb/Rust/src/layout/fc_run_cache.rs +++ b/Libraries/LibWeb/Rust/src/layout/fc_run_cache.rs @@ -29,7 +29,9 @@ fn fc_run_cache_mode_from_environment() -> FcRunCacheMode { /// 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. +/// value a parent hands a spawned run is part of the key. Viewport changes +/// reach a run through that input or through the normal style/layout epoch +/// invalidation for viewport-dependent computed values. #[derive(Clone, Copy, PartialEq)] struct FcRunCacheKey { fc_type: FfiFormattingContextType, @@ -39,14 +41,11 @@ struct FcRunCacheKey { /// 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). +/// (the fragment cache epoch, whose bump walk has no propagation boundary). #[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 { @@ -66,24 +65,51 @@ struct FcRunCacheEntry { retained_fonts: Vec, } +impl FcRunCacheEntry { + fn can_reuse_committed_subtree(&self) -> bool { + self.outputs + .root + .as_ref() + .is_none_or(|root| root.propagated_pending_abspos.is_empty()) + } + + fn outputs_for_reused_subtree(&self) -> RunOutputs { + debug_assert!(self.can_reuse_committed_subtree()); + let root = self.outputs.root.as_ref().map(|root| UnplacedRootFragment { + node: root.node, + // Commit stops at the reused root, so descendant fragments and nested reuse markers never + // enter its scopes. Only payloads that escape the run still have to reach the parent. + scoped_descendants: Vec::new(), + reused_subtree_roots: std::collections::HashSet::new(), + propagated_pending_abspos: root.propagated_pending_abspos.clone(), + propagated_anchor_candidates: root.propagated_anchor_candidates.clone(), + propagated_inline_containing_block_rects: root.propagated_inline_containing_block_rects.clone(), + propagated_abspos_containing_block_info: root.propagated_abspos_containing_block_info.clone(), + }); + RunOutputs { + result: self.outputs.result, + root, + root_outcome: self.outputs.root_outcome.clone(), + } + } +} + +#[derive(Clone, Copy, Default)] +struct InlineLayoutDamage { + generation: u8, + structural_epoch_bumps: u32, +} + /// 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>>>, + inline_layout_damage: 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() } @@ -92,16 +118,65 @@ impl FcRunCacheArenaStore { if let Some(entry) = self.entries.borrow_mut().get_mut(slot as usize) { *entry = None; } + if let Some(damage) = self.inline_layout_damage.borrow_mut().get_mut(slot as usize) { + *damage = InlineLayoutDamage::default(); + } } - /// 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)?; + pub(crate) fn note_inline_layout_damage(&self, box_: Node) { + if self + .entries + .borrow() + .get(box_.slot_index() as usize) + .is_none_or(Option::is_none) + { + return; + } + let mut damage = self.inline_layout_damage.borrow_mut(); + if damage.len() <= box_.slot_index() as usize { + damage.resize(box_.slot_index() as usize + 1, InlineLayoutDamage::default()); + } + let entry = &mut damage[box_.slot_index() as usize]; + if entry.generation != box_.generation() { + *entry = InlineLayoutDamage { + generation: box_.generation(), + structural_epoch_bumps: 1, + }; + } else { + entry.structural_epoch_bumps = entry + .structural_epoch_bumps + .checked_add(1) + .expect("inline layout damage counter overflowed"); + } + } + + fn take_inline_layout_damage(&self, box_: Node) -> u32 { + let mut damage = self.inline_layout_damage.borrow_mut(); + let Some(entry) = damage.get_mut(box_.slot_index() as usize) else { + return 0; + }; + let result = if entry.generation == box_.generation() { + entry.structural_epoch_bumps + } else { + 0 + }; + *entry = InlineLayoutDamage::default(); + result + } + + /// A matching entry stays stored and hands out a shared handle. A stale + /// entry survives until the fresh run replaces it, allowing structural + /// inline damage to use its line data during that run. + fn matching( + &self, + slot: u32, + validity: FcRunCacheValidity, + key: &FcRunCacheKey, + ) -> Option> { + let entries = self.entries.borrow(); + let stored = entries.get(slot as usize)?; let entry = stored.as_ref()?; if entry.validity != validity { - *stored = None; return None; } if entry.key != *key { @@ -110,6 +185,34 @@ impl FcRunCacheArenaStore { Some(entry.clone()) } + fn structurally_damaged_entry( + &self, + slot: u32, + validity: FcRunCacheValidity, + key: &FcRunCacheKey, + structural_epoch_bumps: u32, + ) -> Option> { + if structural_epoch_bumps == 0 { + return None; + } + let entries = self.entries.borrow(); + let entry = entries.get(slot as usize)?.as_ref()?; + if entry.validity.slot_generation != validity.slot_generation + || entry.key != *key + // Each child-list edit bumps once when topology changes and once + // when the parent is marked for layout-tree-update layout. + || validity + .fragment_cache_epoch + .wrapping_sub(entry.validity.fragment_cache_epoch) + != structural_epoch_bumps + .checked_mul(2) + .expect("inline layout damage epoch delta overflowed") + { + 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 { @@ -160,7 +263,6 @@ fn run_root_validity(callbacks: &FfiLayoutFcCallbacks, box_: Node) -> FcRunCache FcRunCacheValidity { slot_generation: data.slot_generation, fragment_cache_epoch: data.fragment_cache_epoch, - viewport: callbacks.arena().fc_run_cache_store().viewport.get(), } } @@ -182,6 +284,7 @@ enum FcRunCacheAttempt { /// a forever-valid entry. validity: FcRunCacheValidity, shadow_entry: Option>, + structurally_damaged_entry: Option>, }, } @@ -209,6 +312,11 @@ impl FcRunCacheAttempt { fc_type, FfiFormattingContextType::InternalReplaced | FfiFormattingContextType::InternalDummy ) + // The direct normal-layout path for an empty atomic block only sizes and snapshots its root. + // Replaying a stored output costs more than rebuilding it and retains an entry needlessly. + || (fc_type == FfiFormattingContextType::Block + && input.participation == ParticipationInParentFormattingContext::AtomicInline + && callbacks.first_child(box_).is_invalid()) { return Ok(Self::Bypass); } @@ -237,6 +345,7 @@ impl FcRunCacheAttempt { }); let store = callbacks.arena().fc_run_cache_store(); let validity = run_root_validity(callbacks, box_); + let structural_epoch_bumps = store.take_inline_layout_damage(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 @@ -247,25 +356,43 @@ impl FcRunCacheAttempt { key, validity, shadow_entry: Some(entry), + structurally_damaged_entry: None, }) } Some(entry) => { store.hit_count.set(store.hit_count.get() + 1); Err(entry) } - None => Ok(Self::Store { - key, - validity, - shadow_entry: None, - }), + None => { + let structurally_damaged_entry = + store.structurally_damaged_entry(box_.slot_index(), validity, &key, structural_epoch_bumps); + Ok(Self::Store { + key, + validity, + shadow_entry: None, + structurally_damaged_entry, + }) + } } } + fn previous_line_data(&self) -> Option> { + let Self::Store { + structurally_damaged_entry: Some(entry), + .. + } = self + else { + return None; + }; + entry.outputs.root_outcome.line_data.clone() + } + fn conclude(self, callbacks: &FfiLayoutFcCallbacks, box_: Node, outputs: &RunOutputs) { let Self::Store { key, validity, shadow_entry, + structurally_damaged_entry: _, } = self else { return; @@ -273,6 +400,12 @@ impl FcRunCacheAttempt { let Some(root) = &outputs.root else { return; }; + // A committed-subtree hit omits that subtree's descendant fragments. Do not embed such a + // skeletal result in an entry that exports an out-of-flow descendant: replaying the parent + // has to rebuild its paintable subtree so the freshly laid-out descendant can commit. + if !root.propagated_pending_abspos.is_empty() && !root.reused_subtree_roots.is_empty() { + return; + } let mut fonts = Vec::new(); if let Some(line_data) = &outputs.root_outcome.line_data { collect_line_data_fonts(line_data, &mut fonts); diff --git a/Libraries/LibWeb/Rust/src/layout/flex_formatting_context.rs b/Libraries/LibWeb/Rust/src/layout/flex_formatting_context.rs index 8f49b38e12155..ec9a2d0453bce 100644 --- a/Libraries/LibWeb/Rust/src/layout/flex_formatting_context.rs +++ b/Libraries/LibWeb/Rust/src/layout/flex_formatting_context.rs @@ -221,6 +221,7 @@ impl<'pass> FlexFormattingContext<'pass> { should_collect_devtools_layout_data: self.should_collect_devtools_layout_data, treat_block_axis_percentage_insets_as_auto_beyond_root: self.treat_block_axis_percentage_insets_as_auto_beyond_root, fragments: self.fragments.clone(), + previous_line_data: None, } } diff --git a/Libraries/LibWeb/Rust/src/layout/formatting_context.rs b/Libraries/LibWeb/Rust/src/layout/formatting_context.rs index 99f4aeb32d51d..3a257701a3372 100644 --- a/Libraries/LibWeb/Rust/src/layout/formatting_context.rs +++ b/Libraries/LibWeb/Rust/src/layout/formatting_context.rs @@ -780,6 +780,7 @@ pub struct FfiBordersData { #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] pub(crate) struct ChildLayoutResult { pub automatic_content_inline_size: CssPixels, + pub min_content_inline_size_from_max_content_layout: Option, pub automatic_content_block_size: CssPixels, pub baselines: DerivedBaselines, pub table_box_in_wrapper_border_box_block_size: Option, @@ -1128,6 +1129,7 @@ pub(crate) struct FormattingContextRun { pub(crate) should_collect_devtools_layout_data: bool, pub(crate) treat_block_axis_percentage_insets_as_auto_beyond_root: bool, pub(crate) fragments: Option>, + pub(crate) previous_line_data: Option>, } impl FormattingContextRun { @@ -1637,8 +1639,21 @@ fn run_formatting_context( &root_cells, ) { Ok(attempt) => attempt, - Err(entry) => return absorb_run_outputs(parent_fragments, parent_used, box_, entry.outputs.clone()), + Err(entry) => { + let reuses_committed_subtree = entry.can_reuse_committed_subtree(); + let outputs = if reuses_committed_subtree { + entry.outputs_for_reused_subtree() + } else { + entry.outputs.clone() + }; + return absorb_run_outputs(parent_fragments, parent_used, box_, outputs, reuses_committed_subtree); + } }; + if let Some(parent_fragments) = parent_fragments { + // A later fresh run for this root supersedes a hit recorded earlier in the same pass. + parent_fragments.clear_reused_subtree_root(box_); + } + let previous_line_data = cache_attempt.previous_line_data(); let outputs = execute_formatting_context_run( purpose, root_cells, @@ -1650,9 +1665,10 @@ fn run_formatting_context( callbacks, input, parent_block, + previous_line_data, ); cache_attempt.conclude(&callbacks, box_, &outputs); - absorb_run_outputs(parent_fragments, parent_used, box_, outputs) + absorb_run_outputs(parent_fragments, parent_used, box_, outputs, false) } #[expect(clippy::too_many_arguments)] @@ -1667,6 +1683,7 @@ fn execute_formatting_context_run( callbacks: FfiLayoutFcCallbacks, input: LayoutInput, parent_block: Option<&BlockFormattingContext>, + previous_line_data: Option>, ) -> RunOutputs { assert!(!box_.is_invalid()); let root_used = std::rc::Rc::new(root_cells.materialize_record()); @@ -1685,6 +1702,7 @@ fn execute_formatting_context_run( (!root_containing_block.is_invalid()).then_some(root_containing_block), )) }), + previous_line_data, }; let run = &run; let body_input = apply_root_sizing_directives(run, &input); @@ -1706,6 +1724,15 @@ fn execute_formatting_context_run( baselines: cached_baselines, ..ChildLayoutResult::default() } + } else if layout_mode == LayoutMode::Normal + && !purpose.is_measurement() + && matches!(input.participation, ParticipationInParentFormattingContext::AtomicInline) + && fc_type == FfiFormattingContextType::Block + && callbacks.first_child(box_).is_invalid() + { + // An empty atomic block context has no body output. Root sizing and finalization still + // run through the shared paths around this branch. + ChildLayoutResult::default() } else { let mut context_implementation = create_formatting_context_implementation(run, parent_grid, fc_type); let result = match &mut context_implementation { @@ -1715,6 +1742,8 @@ fn execute_formatting_context_run( store_derived_baselines(&run.records.used_values(run.box_), baselines); ChildLayoutResult { automatic_content_inline_size: context.automatic_content_inline_size(), + min_content_inline_size_from_max_content_layout: context + .min_content_inline_size_from_max_content_layout(), automatic_content_block_size: context.automatic_content_block_size(), baselines, table_box_in_wrapper_border_box_block_size: context.table_box_in_wrapper_border_box_block_size(), @@ -1809,22 +1838,23 @@ fn execute_formatting_context_run( if registered_abspos_children_could_never_be_laid_out { return run.outputs(result, take_run_fragments()); } - let implementation = implementation.expect("cached measurement replay only occurs on measurement states"); - match &implementation { - FormattingContextImplementation::Block(_) => {} - FormattingContextImplementation::Table(_) => { - let box_ = run.box_; - register_table_abspos_descendants(run, box_); - } - FormattingContextImplementation::Flex(context) => { - context.parent_did_dimension(); - } - FormattingContextImplementation::Grid(context) => { - context.parent_did_dimension(); - } - FormattingContextImplementation::Svg(_) | FormattingContextImplementation::ReplacedWithChildren => {} - FormattingContextImplementation::InternalReplaced | FormattingContextImplementation::InternalDummy => { - return run.outputs(result, take_run_fragments()); + if let Some(implementation) = implementation { + match &implementation { + FormattingContextImplementation::Block(_) => {} + FormattingContextImplementation::Table(_) => { + let box_ = run.box_; + register_table_abspos_descendants(run, box_); + } + FormattingContextImplementation::Flex(context) => { + context.parent_did_dimension(); + } + FormattingContextImplementation::Grid(context) => { + context.parent_did_dimension(); + } + FormattingContextImplementation::Svg(_) | FormattingContextImplementation::ReplacedWithChildren => {} + FormattingContextImplementation::InternalReplaced | FormattingContextImplementation::InternalDummy => { + return run.outputs(result, take_run_fragments()); + } } } run.records.used_values(run.box_).seal_own_metrics(); @@ -1960,6 +1990,7 @@ fn absorb_run_outputs( parent_used: &UsedValues, child: Node, outputs: RunOutputs, + reuses_committed_subtree: bool, ) -> ChildLayoutResult { let RunOutputs { result, @@ -1969,6 +2000,9 @@ fn absorb_run_outputs( root_outcome.apply_to_record(parent_used); if let (Some(fragments), Some(root)) = (parent_fragments, root) { debug_assert!(root.node == child, "a child run returned a root for a different box"); + if reuses_committed_subtree { + fragments.note_reused_subtree_root(child); + } fragments.hold_unplaced_root(root); } result @@ -2074,6 +2108,7 @@ pub unsafe extern "C" fn rust_layout_run_root_layout( should_collect_devtools_layout_data, treat_block_axis_percentage_insets_as_auto_beyond_root: false, fragments: Some(entry_fragments.clone()), + previous_line_data: None, }; let mut root_for_layout = root; @@ -2176,6 +2211,7 @@ pub unsafe extern "C" fn rust_layout_compute_subtree_layout( should_collect_devtools_layout_data: false, treat_block_axis_percentage_insets_as_auto_beyond_root: false, fragments: Some(entry_fragments.clone()), + previous_line_data: None, }; if !viewport.is_invalid() && viewport != root { let viewport_inline_size = CssPixels::from_raw(viewport_inline_size_raw); @@ -2270,6 +2306,7 @@ pub unsafe extern "C" fn rust_layout_replay_saved_abspos_layout( should_collect_devtools_layout_data: false, treat_block_axis_percentage_insets_as_auto_beyond_root: false, fragments: Some(entry_fragments.clone()), + previous_line_data: None, }; AbsposEngine::for_run(&run).replay(&run, box_); drain_and_commit_entry_pass( diff --git a/Libraries/LibWeb/Rust/src/layout/fragment_tree.rs b/Libraries/LibWeb/Rust/src/layout/fragment_tree.rs index fda04718ce98d..8e4e447fba45a 100644 --- a/Libraries/LibWeb/Rust/src/layout/fragment_tree.rs +++ b/Libraries/LibWeb/Rust/src/layout/fragment_tree.rs @@ -260,6 +260,7 @@ fn link_fragment(fragment: std::rc::Rc, placement: PlacementData) -> F pub(crate) struct UnplacedRootFragment { pub(crate) node: crate::layout::node_data::NodeSlotId, pub(crate) scoped_descendants: Vec, + pub(crate) reused_subtree_roots: std::collections::HashSet, pub(crate) propagated_pending_abspos: Vec, pub(crate) propagated_anchor_candidates: Vec, pub(crate) propagated_inline_containing_block_rects: Vec, @@ -268,11 +269,13 @@ pub(crate) struct UnplacedRootFragment { pub(crate) struct CompletedPassFragments { pub(crate) roots: Vec, + pub(crate) reused_subtree_roots: std::collections::HashSet, } pub(crate) struct CommitScopes<'tree> { links_by_slot: std::collections::HashMap, open_scopes: Vec<&'tree [FragmentLink]>, + reused_subtree_roots: &'tree std::collections::HashSet, } impl<'tree> CommitScopes<'tree> { @@ -282,6 +285,7 @@ impl<'tree> CommitScopes<'tree> { let mut scopes = Self { links_by_slot: std::collections::HashMap::new(), open_scopes: Vec::new(), + reused_subtree_roots: &fragments.reused_subtree_roots, }; scopes.open_scope(&fragments.roots); scopes @@ -291,6 +295,10 @@ impl<'tree> CommitScopes<'tree> { self.links_by_slot.get(&slot).copied() } + pub(crate) fn subtree_was_reused(&self, slot: u32) -> bool { + self.reused_subtree_roots.contains(&slot) + } + pub(crate) fn open_scope(&mut self, links: &'tree [FragmentLink]) { for link in links { let previous = self.links_by_slot.insert(link.fragment.node.slot_index(), link); @@ -364,6 +372,7 @@ struct RunFragmentBuilderInner { inline_containing_block_rects_at_root: Vec, abspos_containing_block_info_contributions: Vec, top_scope_links: Vec, + reused_subtree_roots: std::collections::HashSet, } impl RunFragmentBuilderInner { @@ -630,13 +639,23 @@ impl RunFragmentBuilder { pub(crate) fn hold_unplaced_root(&self, root: UnplacedRootFragment) { let slot = root.node.slot_index(); - let previous = self.inner.borrow_mut().child_roots_awaiting_placement.insert(slot, root); + let mut inner = self.inner.borrow_mut(); + inner.reused_subtree_roots.extend(root.reused_subtree_roots.iter().copied()); + let previous = inner.child_roots_awaiting_placement.insert(slot, root); debug_assert!( previous.is_none(), "a child run's root was handed over twice before placement" ); } + pub(crate) fn note_reused_subtree_root(&self, node: crate::layout::node_data::NodeSlotId) { + self.inner.borrow_mut().reused_subtree_roots.insert(node.slot_index()); + } + + pub(crate) fn clear_reused_subtree_root(&self, node: crate::layout::node_data::NodeSlotId) { + self.inner.borrow_mut().reused_subtree_roots.remove(&node.slot_index()); + } + pub(crate) fn normalize_arrivals_for_placement(&self, node: crate::layout::node_data::NodeSlotId) { let mut inner = self.inner.borrow_mut(); let slot = node.slot_index(); @@ -770,8 +789,10 @@ impl RunFragmentBuilder { callbacks: &FfiLayoutFcCallbacks, ) -> CompletedPassFragments { debug_assert!(self.is_entry_accumulator, "an ordinary run closes as a singular unplaced root"); + let root = self.close(records, callbacks); CompletedPassFragments { - roots: self.close(records, callbacks).scoped_descendants, + roots: root.scoped_descendants, + reused_subtree_roots: root.reused_subtree_roots, } } @@ -828,6 +849,7 @@ impl RunFragmentBuilder { UnplacedRootFragment { node: self.root_node, scoped_descendants: inner.top_scope_links, + reused_subtree_roots: inner.reused_subtree_roots, propagated_pending_abspos, propagated_anchor_candidates, propagated_inline_containing_block_rects, diff --git a/Libraries/LibWeb/Rust/src/layout/grid_formatting_context.rs b/Libraries/LibWeb/Rust/src/layout/grid_formatting_context.rs index 659b94712fc08..d41bb3b0ac498 100644 --- a/Libraries/LibWeb/Rust/src/layout/grid_formatting_context.rs +++ b/Libraries/LibWeb/Rust/src/layout/grid_formatting_context.rs @@ -1183,6 +1183,7 @@ impl GridFormattingContext { should_collect_devtools_layout_data: self.should_collect_devtools_layout_data, treat_block_axis_percentage_insets_as_auto_beyond_root: self.treat_block_axis_percentage_insets_as_auto_beyond_root, fragments: self.fragments.clone(), + previous_line_data: None, } } @@ -2509,6 +2510,7 @@ impl GridFormattingContext { should_collect_devtools_layout_data: false, treat_block_axis_percentage_insets_as_auto_beyond_root: false, fragments: None, + previous_line_data: None, }; let mut context = GridFormattingContext::new(&scratch_run, Some(self)); let mut available = self.available_space.unwrap(); diff --git a/Libraries/LibWeb/Rust/src/layout/inline_formatting_context.rs b/Libraries/LibWeb/Rust/src/layout/inline_formatting_context.rs index ae8cb1d95e05a..3175b747f7e84 100644 --- a/Libraries/LibWeb/Rust/src/layout/inline_formatting_context.rs +++ b/Libraries/LibWeb/Rust/src/layout/inline_formatting_context.rs @@ -755,6 +755,7 @@ pub(crate) struct InlineFormattingContext<'context> { pub(crate) containing_used_values: std::rc::Rc, pub(crate) fragmented_inlines_in_pre_order: Vec, pub(crate) automatic_content_inline_size: CssPixels, + pub(crate) min_content_inline_size_from_max_content_layout: Option, pub(crate) automatic_content_block_size: CssPixels, block_axis_float_clearance: Cell, } @@ -780,6 +781,7 @@ impl<'context> InlineFormattingContext<'context> { containing_used_values, fragmented_inlines_in_pre_order: Vec::new(), automatic_content_inline_size: CssPixels::default(), + min_content_inline_size_from_max_content_layout: None, automatic_content_block_size: CssPixels::default(), block_axis_float_clearance: Cell::new(CssPixels::default()), } @@ -992,6 +994,14 @@ impl<'context> InlineFormattingContext<'context> { content_baselines } + pub(crate) fn paired_min_content_inline_size_for_atomic_root(&self, node: Node) -> Option { + self.parent.sizing().paired_min_content_inline_size_for_atomic_root( + node, + self.input.available_space, + self.input.containing_block_constraints, + ) + } + fn clear_floating_boxes(&self, node: Node) -> bool { self.parent.clear_floating_boxes( node, @@ -1018,12 +1028,185 @@ impl<'context> InlineFormattingContext<'context> { style.text_overflow() == text_overflow::ELLIPSIS && style.overflow_x() != overflow::VISIBLE } + fn reusable_atomic_line_prefix( + &self, + previous: &LineData, + iterator: &InlineLevelIterator, + ) -> (Vec, usize) { + if self.containing_block != self.run.box_ + || !previous.inline_box_pieces.is_empty() + || previous + .line_boxes + .iter() + .any(|line| line.writing_mode != writing_mode::HORIZONTAL_TB) + || iterator.items().iter().any(|item| item.type_ != ItemType::Element) + { + return (Vec::new(), 0); + } + + let mut item_index = 0usize; + let mut reused_lines = Vec::new(); + let mut item_count_before_last_line = 0usize; + for line in &previous.line_boxes { + if line.fragments.is_empty() + || line.has_block_level_box + || !line.static_position_markers.is_empty() + || !line.inline_box_baselines.is_empty() + { + break; + } + let line_item_start = item_index; + let mut running_inline_length = CssPixels::default(); + let mut matched = true; + for fragment in &line.fragments { + let Some(item) = iterator.items().get(item_index) else { + matched = false; + break; + }; + let used = self.used(item.node); + let expected_inline_offset = + running_inline_length + item.margin_start + item.border_start + item.padding_start; + if !fragment.is_atomic_inline + || fragment.layout_node != item.node + || fragment.inline_offset != expected_inline_offset + || fragment.inline_length != item.inline_size + || fragment.block_length != used.content_block_size.get() + || fragment.border_box_block_start != used.border_box_top(false) + { + matched = false; + break; + } + running_inline_length += item.margin_start + + item.border_start + + item.padding_start + + item.inline_size + + item.padding_end + + item.border_end + + item.margin_end; + item_index += 1; + } + if !matched || running_inline_length != line.inline_length { + item_index = line_item_start; + break; + } + item_count_before_last_line = line_item_start; + reused_lines.push(line.clone()); + } + + // Additional content can fit on the old final line, so that line is + // damaged even when every old fragment before the insertion matches. + if item_index < iterator.items().len() && reused_lines.len() == previous.line_boxes.len() { + reused_lines.pop(); + item_index = item_count_before_last_line; + } + (reused_lines, item_index) + } + + fn min_content_inline_size_from_max_content_items(&self, items: &[Item]) -> Option { + if self.input.available_space.inline_size != AvailableSize::MaxContent { + return None; + } + if self.facts(self.containing_block).is_scroll_container() { + return None; + } + if self.style(self.containing_block).writing_mode() != writing_mode::HORIZONTAL_TB { + return None; + } + + let containing_style = self.style(self.containing_block); + let containing_inline_size = self.input.containing_block_constraints.inline_basis(); + if containing_style.text_indent().to_px(containing_inline_size) != CssPixels::default() { + return None; + } + + let wraps = containing_style.text_wrap_mode() == text_wrap_mode::WRAP; + let mut maximum = CssPixels::default(); + let mut current = CssPixels::default(); + let mut line_has_content = false; + let finish_line = + |maximum: &mut CssPixels, current: &mut CssPixels, line_has_content: &mut bool| { + *maximum = (*maximum).max(*current); + *current = CssPixels::default(); + *line_has_content = false; + }; + for item in items { + match item.type_ { + ItemType::Element => { + if item.has_box_model_metrics() { + return None; + } + if wraps && line_has_content { + finish_line(&mut maximum, &mut current, &mut line_has_content); + } + current += item.min_content_inline_size?; + line_has_content = true; + } + ItemType::Text => { + if item.has_box_model_metrics() || item.contains_tab(self) { + return None; + } + if item.length_in_node == 0 && item.inline_size == CssPixels::default() { + continue; + } + let wraps = self.style(self.parent_node(item.node)).text_wrap_mode() == text_wrap_mode::WRAP; + if !wraps { + current += item.inline_size; + line_has_content = true; + continue; + } + if item.is_ascii_whitespace(self) { + if !item.is_collapsible_whitespace { + return None; + } + if line_has_content { + finish_line(&mut maximum, &mut current, &mut line_has_content); + } + continue; + } + if item.trailing_whitespace.inline_size != CssPixels::default() { + return None; + } + if item.can_break_before && line_has_content { + finish_line(&mut maximum, &mut current, &mut line_has_content); + } + current += item.inline_size; + line_has_content = true; + } + ItemType::ForcedBreak => { + finish_line(&mut maximum, &mut current, &mut line_has_content); + } + ItemType::BlockLevelBox | ItemType::AbsolutelyPositionedElement | ItemType::FloatingElement => { + return None; + } + } + } + finish_line(&mut maximum, &mut current, &mut line_has_content); + Some(maximum) + } + pub(crate) fn generate_line_boxes(&mut self) { - self.line_data_mut().line_boxes.clear(); - self.line_data_mut().inline_box_pieces.clear(); let mut iterator = InlineLevelIterator::new(self); + self.min_content_inline_size_from_max_content_layout = + self.min_content_inline_size_from_max_content_items(iterator.items()); self.fragmented_inlines_in_pre_order = iterator.take_visited_fragmented_inlines(); - let mut line_builder = LineBuilder::new(self); + let (reused_lines, reused_item_count) = self + .run + .previous_line_data + .as_deref() + .map(|previous| self.reusable_atomic_line_prefix(previous, &iterator)) + .unwrap_or_default(); + { + let mut data = self.line_data_mut(); + data.line_boxes = reused_lines; + data.inline_box_pieces.clear(); + } + iterator.skip_items(reused_item_count); + let reused_line_count = self.line_data().line_boxes.len(); + let mut line_builder = if reused_line_count == 0 { + LineBuilder::new(self) + } else { + LineBuilder::new_after_reused_lines(self) + }; let mut leading_margin = CssPixels::default(); let mut leading_border = CssPixels::default(); @@ -1129,8 +1312,7 @@ impl<'context> InlineFormattingContext<'context> { ItemType::Text => { line_builder.prepare_to_append_inline_content(); if self.style(self.parent_node(item.node)).text_wrap_mode() == text_wrap_mode::WRAP { - let is_whitespace = - item.is_collapsible_whitespace || iterator.item_is_ascii_whitespace(self, &item); + let is_whitespace = item.is_collapsible_whitespace || item.is_ascii_whitespace(self); let next_inline_size = if is_whitespace { iterator.next_non_whitespace_sequence_inline_size(self) } else { @@ -1167,7 +1349,7 @@ impl<'context> InlineFormattingContext<'context> { } let line_count = self.line_data().line_boxes.len(); - for line_index in 0..line_count { + for line_index in reused_line_count..line_count { self.line_data_mut().line_boxes[line_index].trim_trailing_whitespace(); } if self.text_overflow_applies() { diff --git a/Libraries/LibWeb/Rust/src/layout/inline_level_iterator.rs b/Libraries/LibWeb/Rust/src/layout/inline_level_iterator.rs index ed2835111c2a8..f0ba8300077b9 100644 --- a/Libraries/LibWeb/Rust/src/layout/inline_level_iterator.rs +++ b/Libraries/LibWeb/Rust/src/layout/inline_level_iterator.rs @@ -22,6 +22,7 @@ pub(crate) struct Item { pub(crate) offset_in_node: usize, pub(crate) length_in_node: usize, pub(crate) inline_size: CssPixels, + pub(crate) min_content_inline_size: Option, pub(crate) padding_start: CssPixels, pub(crate) padding_end: CssPixels, pub(crate) border_start: CssPixels, @@ -44,6 +45,7 @@ impl Item { offset_in_node: 0, length_in_node: 0, inline_size: CssPixels::default(), + min_content_inline_size: None, padding_start: CssPixels::default(), padding_end: CssPixels::default(), border_start: CssPixels::default(), @@ -61,6 +63,29 @@ impl Item { pub(crate) fn border_box_inline_size(&self) -> CssPixels { self.border_start + self.padding_start + self.inline_size + self.padding_end + self.border_end } + + pub(crate) fn has_box_model_metrics(&self) -> bool { + self.margin_start != CssPixels::default() + || self.border_start != CssPixels::default() + || self.padding_start != CssPixels::default() + || self.padding_end != CssPixels::default() + || self.border_end != CssPixels::default() + || self.margin_end != CssPixels::default() + } + + pub(crate) fn is_ascii_whitespace(&self, context: &InlineFormattingContext<'_>) -> bool { + assert_eq!(self.type_, ItemType::Text); + let text = &context.callbacks.text_content(self.node).text; + text[self.offset_in_node..self.offset_in_node + self.length_in_node] + .iter() + .all(|unit| *unit <= 0x7f && (*unit as u8).is_ascii_whitespace()) + } + + pub(crate) fn contains_tab(&self, context: &InlineFormattingContext<'_>) -> bool { + assert_eq!(self.type_, ItemType::Text); + let text = &context.callbacks.text_content(self.node).text; + text[self.offset_in_node..self.offset_in_node + self.length_in_node].contains(&(b'\t' as u16)) + } } #[derive(Clone, Copy, Default)] @@ -575,9 +600,11 @@ impl<'iterator, 'context> InlineLevelIteratorGenerator<'iterator, 'context> { .create_used_values(node, self.context().input.containing_block_constraints) }; let content_baselines = self.context_mut().dimension_box_on_line(node); + let min_content_inline_size = self.context().paired_min_content_inline_size_for_atomic_root(node); let mut item = Item::new(ItemType::Element, node); item.content_baselines = content_baselines; item.inline_size = used.content_inline_size.get(); + item.min_content_inline_size = min_content_inline_size; item.padding_start = used.padding_left.get(); item.padding_end = used.padding_right.get(); item.border_start = used.border_left.get(); @@ -613,6 +640,15 @@ impl InlineLevelIterator { )) } + pub(crate) fn items(&self) -> &[Item] { + &self.items + } + + pub(crate) fn skip_items(&mut self, count: usize) { + assert!(self.next_item_index + count <= self.items.len()); + self.next_item_index += count; + } + pub(crate) fn next_non_whitespace_sequence_inline_size( &self, context: &InlineFormattingContext<'_>, @@ -627,11 +663,7 @@ impl InlineLevelIterator { if item.type_ != ItemType::Text || item.is_collapsible_whitespace { break; } - let text = &context.callbacks.text_content(item.node).text; - if text[item.offset_in_node..item.offset_in_node + item.length_in_node] - .iter() - .all(|unit| *unit <= 0x7f && (*unit as u8).is_ascii_whitespace()) - { + if item.is_ascii_whitespace(context) { break; } } @@ -640,14 +672,6 @@ impl InlineLevelIterator { size } - pub(crate) fn item_is_ascii_whitespace(&self, context: &InlineFormattingContext<'_>, item: &Item) -> bool { - assert_eq!(item.type_, ItemType::Text); - let text = &context.callbacks.text_content(item.node).text; - text[item.offset_in_node..item.offset_in_node + item.length_in_node] - .iter() - .all(|unit| *unit <= 0x7f && (*unit as u8).is_ascii_whitespace()) - } - pub(crate) fn take_visited_fragmented_inlines(&mut self) -> Vec { std::mem::take(&mut self.visited_fragmented_inlines) } diff --git a/Libraries/LibWeb/Rust/src/layout/layout_node_arena.rs b/Libraries/LibWeb/Rust/src/layout/layout_node_arena.rs index c639f7592b7ec..b48e97e5085d7 100644 --- a/Libraries/LibWeb/Rust/src/layout/layout_node_arena.rs +++ b/Libraries/LibWeb/Rust/src/layout/layout_node_arena.rs @@ -59,6 +59,7 @@ pub(crate) enum IntrinsicSizeCacheKind { #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub(crate) struct IntrinsicInlineSizeMeasurement { pub(crate) automatic_content_inline_size: CssPixels, + pub(crate) min_content_inline_size_from_max_content_layout: Option, pub(crate) available_block_size: AvailableSize, pub(crate) content_inline_size: CssPixels, pub(crate) content_block_size: CssPixels, @@ -267,16 +268,15 @@ impl LayoutNodeArena { &self.fc_run_cache_store } - /// Drops entries whose slot, epoch, or viewport stamp no longer match. + /// Drops entries whose slot or epoch no longer matches. /// 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 { + if !metadata.occupied || metadata.generation != validity.slot_generation { return false; } let id = NodeSlotId::new(slot, metadata.generation); @@ -1069,6 +1069,25 @@ pub unsafe extern "C" fn layout_arena_fc_run_cache_hit_count(arena: *mut c_void) }) } +#[unsafe(no_mangle)] +pub unsafe extern "C" fn layout_arena_note_inline_layout_damage(arena: *mut c_void, mut box_: NodeSlotId) { + abort_on_panic(|| { + assert!(!arena.is_null(), "layout node arena handle is null"); + // SAFETY: The C++ caller keeps the arena and layout tree alive for this synchronous call. + let arena = unsafe { &*arena.cast::() }; + // OPTIMIZATION: The edit invalidates line data at its direct parent and every formatting + // ancestor. Preserve the structural proof along the same unbounded path as the fragment + // epoch bumps so each affected inline context can reuse its unchanged line prefix. + while !box_.is_invalid() { + let data = arena.data(box_); + arena.fc_run_cache_store().note_inline_layout_damage(box_); + // SAFETY: data() validated that box_ names a live slot, and the layout tree is stable + // for the duration of this synchronous topology update. + box_ = unsafe { (&raw const (*data).parent).read() }; + } + }); +} + #[unsafe(no_mangle)] pub unsafe extern "C" fn layout_arena_set_text_content( arena: *mut c_void, @@ -1121,22 +1140,6 @@ pub unsafe extern "C" fn layout_arena_set_replaced_content_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 { &*arena.cast::() } - .fc_run_cache_store() - .note_viewport_size(viewport_inline_size_raw, viewport_block_size_raw); - }); -} - #[unsafe(no_mangle)] pub unsafe extern "C" fn layout_arena_set_raw_table_column_span( arena: *mut c_void, diff --git a/Libraries/LibWeb/Rust/src/layout/line_builder.rs b/Libraries/LibWeb/Rust/src/layout/line_builder.rs index dea438f0ccebd..b9eca05fab5d9 100644 --- a/Libraries/LibWeb/Rust/src/layout/line_builder.rs +++ b/Libraries/LibWeb/Rust/src/layout/line_builder.rs @@ -66,9 +66,15 @@ pub(crate) struct LineBuilder<'builder, 'context> { impl<'builder, 'context> LineBuilder<'builder, 'context> { pub(crate) fn new(context: &'builder InlineFormattingContext<'context>) -> Self { + let mut builder = Self::initialized(context); + builder.begin_new_line(false, true, ForcedBreak::No); + builder + } + + fn initialized(context: &'builder InlineFormattingContext<'context>) -> Self { let style = context.style(context.containing_block); let containing_inline_size = context.input.containing_block_constraints.inline_basis(); - let mut builder = Self { + Self { context, available_inline_size_for_current_line: AvailableSize::Indefinite, current_block_offset: CssPixels::default(), @@ -83,8 +89,20 @@ impl<'builder, 'context> LineBuilder<'builder, 'context> { should_advance_to_last_line_box_block_end: false, current_line_committed_pending_margin: false, pending_margin_follows_block_level_box: false, - }; + } + } + + pub(crate) fn new_after_reused_lines(context: &'builder InlineFormattingContext<'context>) -> Self { + assert!(!context.line_data().line_boxes.is_empty()); + let current_block_offset = context.line_data().line_boxes.last().unwrap().physical_vertical_end(); + let mut builder = Self::initialized(context); + builder.current_block_offset = current_block_offset; + context + .line_data_mut() + .line_boxes + .push(LineBoxData::new(builder.direction, builder.writing_mode)); builder.begin_new_line(false, true, ForcedBreak::No); + builder.current_line_committed_pending_margin = true; builder } diff --git a/Libraries/LibWeb/Rust/src/layout/sizing_context.rs b/Libraries/LibWeb/Rust/src/layout/sizing_context.rs index 44b953a9d7dc0..c5e17d92bd250 100644 --- a/Libraries/LibWeb/Rust/src/layout/sizing_context.rs +++ b/Libraries/LibWeb/Rust/src/layout/sizing_context.rs @@ -1084,6 +1084,25 @@ impl SizingContext { return; } + let inline_size = self.calculate_atomic_root_content_inline_size(node, available_space, constraints, None); + self.used(node).set_content_inline_size(inline_size); + + let inline_definite_space = AvailableSpace { + inline_size: AvailableSize::definite(inline_size), + block_size: AvailableSize::Indefinite, + }; + self.resolve_used_block_size_if_not_treated_as_auto(node, inline_definite_space, constraints); + self.make_button_content_box_definite(node, layout_mode, available_space, constraints, None); + } + + fn calculate_atomic_root_content_inline_size( + &self, + node: Node, + available_space: AvailableSpace, + constraints: ContainingBlockConstraints, + intrinsic_content_inline_size: Option, + ) -> CssPixels { + let style = self.style(node); let unconstrained_inline_size = if self.should_treat_inline_size_as_auto(node, available_space) { if matches!(available_space.inline_size, AvailableSize::Definite(_)) { let used = self.used(node); @@ -1103,9 +1122,11 @@ impl SizingContext { .min(preferred) } } else if available_space.inline_size == AvailableSize::MinContent { - self.calculate_min_content_inline_size(node, constraints) + intrinsic_content_inline_size + .unwrap_or_else(|| self.calculate_min_content_inline_size(node, constraints)) } else { - self.calculate_max_content_inline_size(node, constraints) + intrinsic_content_inline_size + .unwrap_or_else(|| self.calculate_max_content_inline_size(node, constraints)) } } else if style.width().contains_percentage() && !matches!(available_space.inline_size, AvailableSize::Definite(_)) { CssPixels::default() @@ -1130,14 +1151,39 @@ impl SizingContext { constraints, )); } - self.used(node).set_content_inline_size(inline_size); + inline_size + } - let inline_definite_space = AvailableSpace { - inline_size: AvailableSize::definite(inline_size), - block_size: AvailableSize::Indefinite, + pub(crate) fn paired_min_content_inline_size_for_atomic_root( + &self, + node: Node, + available_space: AvailableSpace, + constraints: ContainingBlockConstraints, + ) -> Option { + if available_space.inline_size != AvailableSize::MaxContent + || self.box_is_sized_as_replaced_element(node, available_space, constraints) + { + return None; + } + let min_content_inline_size = if self.has_children(node) { + self.intrinsic_inline_measurement_cache_get( + node, + IntrinsicSizeCacheKind::MaxContentInline, + cache_key(None, constraints), + )? + .min_content_inline_size_from_max_content_layout? + } else { + CssPixels::default() }; - self.resolve_used_block_size_if_not_treated_as_auto(node, inline_definite_space, constraints); - self.make_button_content_box_definite(node, layout_mode, available_space, constraints, None); + Some(self.calculate_atomic_root_content_inline_size( + node, + AvailableSpace { + inline_size: AvailableSize::MinContent, + ..available_space + }, + constraints, + Some(min_content_inline_size), + )) } fn calculate_stretch_fit_inline_size(&self, node: Node, available: AvailableSize) -> CssPixels { @@ -1242,6 +1288,8 @@ impl SizingContext { key, IntrinsicInlineSizeMeasurement { automatic_content_inline_size: result.automatic_content_inline_size, + min_content_inline_size_from_max_content_layout: result + .min_content_inline_size_from_max_content_layout, available_block_size, content_inline_size: used.content_inline_size.get(), content_block_size: used.content_block_size.get(), @@ -1381,9 +1429,32 @@ impl SizingContext { if !self.has_children(node) { return CssPixels::default(); } + if let Some(cached) = self.intrinsic_inline_measurement_cache_get( + node, + IntrinsicSizeCacheKind::MinContentInline, + cache_key(None, constraints), + ) { + return cached.automatic_content_inline_size; + } + if let Some(min_content_inline_size) = self.paired_min_content_inline_size(node, constraints) { + return min_content_inline_size; + } self.measure_intrinsic_inline_size(node, constraints, IntrinsicSizeCacheKind::MinContentInline) } + fn paired_min_content_inline_size( + &self, + node: Node, + constraints: ContainingBlockConstraints, + ) -> Option { + self.intrinsic_inline_measurement_cache_get( + node, + IntrinsicSizeCacheKind::MaxContentInline, + cache_key(None, constraints), + )? + .min_content_inline_size_from_max_content_layout + } + pub(crate) fn calculate_max_content_inline_size( &self, node: Node, @@ -1585,6 +1656,9 @@ impl SizingContext { ), ); result.automatic_content_inline_size = clamp_to_max_dimension_value(result.automatic_content_inline_size); + result.min_content_inline_size_from_max_content_layout = result + .min_content_inline_size_from_max_content_layout + .map(clamp_to_max_dimension_value); let value = result.automatic_content_inline_size; self.cache_intrinsic_inline_measurement(node, kind, key, &root, result, block_size); value @@ -1838,6 +1912,7 @@ impl SizingContext { should_collect_devtools_layout_data: false, treat_block_axis_percentage_insets_as_auto_beyond_root: false, fragments: None, + previous_line_data: None, }; let mut table = TableFormattingContext::new(&table_run); let table_available = table_used.available_inner_space_or_constraints_from(available_space); diff --git a/Libraries/LibWeb/Rust/src/layout/svg_formatting_context.rs b/Libraries/LibWeb/Rust/src/layout/svg_formatting_context.rs index 727c4e884e53e..3b6c31ddafde7 100644 --- a/Libraries/LibWeb/Rust/src/layout/svg_formatting_context.rs +++ b/Libraries/LibWeb/Rust/src/layout/svg_formatting_context.rs @@ -402,6 +402,7 @@ impl SvgFormattingContext { should_collect_devtools_layout_data: self.should_collect_devtools_layout_data, treat_block_axis_percentage_insets_as_auto_beyond_root: self.treat_block_axis_percentage_insets_as_auto_beyond_root, fragments: self.fragments.clone(), + previous_line_data: None, } } diff --git a/Libraries/LibWeb/Rust/src/layout/table_formatting_context.rs b/Libraries/LibWeb/Rust/src/layout/table_formatting_context.rs index 43aa91c6d4efb..9dbd97f7f604f 100644 --- a/Libraries/LibWeb/Rust/src/layout/table_formatting_context.rs +++ b/Libraries/LibWeb/Rust/src/layout/table_formatting_context.rs @@ -887,6 +887,7 @@ impl TableFormattingContext { should_collect_devtools_layout_data: self.should_collect_devtools_layout_data, treat_block_axis_percentage_insets_as_auto_beyond_root: self.treat_block_axis_percentage_insets_as_auto_beyond_root, fragments: self.fragments.clone(), + previous_line_data: None, } } diff --git a/Tests/LibWeb/Text/expected/css/style-invalidation/size-query-container-scans.txt b/Tests/LibWeb/Text/expected/css/style-invalidation/size-query-container-scans.txt index aeeb7fb29b164..44572893d17c0 100644 --- a/Tests/LibWeb/Text/expected/css/style-invalidation/size-query-container-scans.txt +++ b/Tests/LibWeb/Text/expected/css/style-invalidation/size-query-container-scans.txt @@ -1,4 +1,4 @@ dependent before: rgb(0, 0, 0) -elements walked after resizing a container no query names: 1 +elements walked after resizing a container no query names: 0 elements walked after resizing the query container: 1 dependent after: rgb(1, 2, 3) diff --git a/Tests/LibWeb/Text/expected/layout-run-cache-escaped-abspos.txt b/Tests/LibWeb/Text/expected/layout-run-cache-escaped-abspos.txt new file mode 100644 index 0000000000000..f9b5bec4f3585 --- /dev/null +++ b/Tests/LibWeb/Text/expected/layout-run-cache-escaped-abspos.txt @@ -0,0 +1,3 @@ +hit delta: >=1 +left before: 290 +left after: 190 diff --git a/Tests/LibWeb/Text/expected/layout-run-cache-inline-damage.txt b/Tests/LibWeb/Text/expected/layout-run-cache-inline-damage.txt new file mode 100644 index 0000000000000..6ea05da76755f --- /dev/null +++ b/Tests/LibWeb/Text/expected/layout-run-cache-inline-damage.txt @@ -0,0 +1,5 @@ +after insert: 40x40 +inserted: 20,20 +last: 0,30 +after remove: 40x30 +last: 30,20 diff --git a/Tests/LibWeb/Text/expected/layout-run-cache-optimization-scenarios.txt b/Tests/LibWeb/Text/expected/layout-run-cache-optimization-scenarios.txt new file mode 100644 index 0000000000000..d784269b4b4bb --- /dev/null +++ b/Tests/LibWeb/Text/expected/layout-run-cache-optimization-scenarios.txt @@ -0,0 +1,2 @@ +layout optimization scenarios cases: 444 +layout optimization scenarios failures: 0 diff --git a/Tests/LibWeb/Text/expected/layout-run-cache-viewport-resize-scenarios.txt b/Tests/LibWeb/Text/expected/layout-run-cache-viewport-resize-scenarios.txt new file mode 100644 index 0000000000000..ee4899e53bccc --- /dev/null +++ b/Tests/LibWeb/Text/expected/layout-run-cache-viewport-resize-scenarios.txt @@ -0,0 +1,34 @@ +viewport {"format":"block","depth":1,"dependency":"vw"} cached: yes +viewport {"format":"block","depth":1,"dependency":"vh"} cached: yes +viewport {"format":"block","depth":1,"dependency":"absolute"} cached: yes +viewport {"format":"block","depth":1,"dependency":"fixed"} cached: yes +viewport {"format":"block","depth":2,"dependency":"vw"} cached: yes +viewport {"format":"block","depth":2,"dependency":"vh"} cached: yes +viewport {"format":"block","depth":2,"dependency":"absolute"} cached: yes +viewport {"format":"block","depth":2,"dependency":"fixed"} cached: yes +viewport {"format":"inlineBlock","depth":1,"dependency":"vw"} cached: yes +viewport {"format":"inlineBlock","depth":1,"dependency":"vh"} cached: yes +viewport {"format":"inlineBlock","depth":1,"dependency":"absolute"} cached: yes +viewport {"format":"inlineBlock","depth":1,"dependency":"fixed"} cached: yes +viewport {"format":"inlineBlock","depth":2,"dependency":"vw"} cached: yes +viewport {"format":"inlineBlock","depth":2,"dependency":"vh"} cached: yes +viewport {"format":"inlineBlock","depth":2,"dependency":"absolute"} cached: yes +viewport {"format":"inlineBlock","depth":2,"dependency":"fixed"} cached: yes +viewport {"format":"flex","depth":1,"dependency":"vw"} cached: yes +viewport {"format":"flex","depth":1,"dependency":"vh"} cached: yes +viewport {"format":"flex","depth":1,"dependency":"absolute"} cached: yes +viewport {"format":"flex","depth":1,"dependency":"fixed"} cached: yes +viewport {"format":"flex","depth":2,"dependency":"vw"} cached: yes +viewport {"format":"flex","depth":2,"dependency":"vh"} cached: yes +viewport {"format":"flex","depth":2,"dependency":"absolute"} cached: yes +viewport {"format":"flex","depth":2,"dependency":"fixed"} cached: yes +viewport {"format":"grid","depth":1,"dependency":"vw"} cached: yes +viewport {"format":"grid","depth":1,"dependency":"vh"} cached: yes +viewport {"format":"grid","depth":1,"dependency":"absolute"} cached: yes +viewport {"format":"grid","depth":1,"dependency":"fixed"} cached: yes +viewport {"format":"grid","depth":2,"dependency":"vw"} cached: yes +viewport {"format":"grid","depth":2,"dependency":"vh"} cached: yes +viewport {"format":"grid","depth":2,"dependency":"absolute"} cached: yes +viewport {"format":"grid","depth":2,"dependency":"fixed"} cached: yes +viewport resize scenarios cases: 32 +viewport resize scenarios failures: 0 diff --git a/Tests/LibWeb/Text/expected/layout-run-cache-viewport-resize.txt b/Tests/LibWeb/Text/expected/layout-run-cache-viewport-resize.txt new file mode 100644 index 0000000000000..692a9c67cdf6b --- /dev/null +++ b/Tests/LibWeb/Text/expected/layout-run-cache-viewport-resize.txt @@ -0,0 +1,3 @@ +hit delta: 1 +cached width: 100 +dependent width: 100 diff --git a/Tests/LibWeb/Text/input/layout-optimization-test-matrix.js b/Tests/LibWeb/Text/input/layout-optimization-test-matrix.js new file mode 100644 index 0000000000000..591612d6808a0 --- /dev/null +++ b/Tests/LibWeb/Text/input/layout-optimization-test-matrix.js @@ -0,0 +1,68 @@ +function matrixCases(axes) { + let cases = [{}]; + for (const [name, values] of Object.entries(axes)) { + const expanded = []; + for (const existing of cases) { + for (const value of values) expanded.push({ ...existing, [name]: value }); + } + cases = expanded; + } + return cases; +} + +function rounded(value) { + return Math.round(value * 64) / 64; +} + +function relativeRect(element, ancestor) { + const rect = element.getBoundingClientRect(); + const ancestorRect = ancestor.getBoundingClientRect(); + return [ + rounded(rect.left - ancestorRect.left), + rounded(rect.top - ancestorRect.top), + rounded(rect.width), + rounded(rect.height), + ]; +} + +class LayoutTestMatrix { + constructor(name) { + this.name = name; + this.caseCount = 0; + this.failures = []; + } + + run(label, callback) { + ++this.caseCount; + try { + callback(); + } catch (error) { + this.failures.push(`${label}: ${error}`); + } + } + + async runAsync(label, callback) { + ++this.caseCount; + try { + await callback(); + } catch (error) { + this.failures.push(`${label}: ${error}`); + } + } + + expect(label, actual, expected) { + const actualJSON = JSON.stringify(actual); + const expectedJSON = JSON.stringify(expected); + if (actualJSON !== expectedJSON) throw new Error(`${label}: expected ${expectedJSON}, got ${actualJSON}`); + } + + expectTrue(label, condition) { + if (!condition) throw new Error(`${label}: expected true`); + } + + print() { + for (const failure of this.failures) println(`FAIL: ${failure}`); + println(`${this.name} cases: ${this.caseCount}`); + println(`${this.name} failures: ${this.failures.length}`); + } +} diff --git a/Tests/LibWeb/Text/input/layout-run-cache-escaped-abspos.html b/Tests/LibWeb/Text/input/layout-run-cache-escaped-abspos.html new file mode 100644 index 0000000000000..22df69ccb88cf --- /dev/null +++ b/Tests/LibWeb/Text/input/layout-run-cache-escaped-abspos.html @@ -0,0 +1,33 @@ + + + +
+
+
+
+
+
+
+ diff --git a/Tests/LibWeb/Text/input/layout-run-cache-inline-damage.html b/Tests/LibWeb/Text/input/layout-run-cache-inline-damage.html new file mode 100644 index 0000000000000..35ecc2fba67d0 --- /dev/null +++ b/Tests/LibWeb/Text/input/layout-run-cache-inline-damage.html @@ -0,0 +1,48 @@ + + + +
+ diff --git a/Tests/LibWeb/Text/input/layout-run-cache-optimization-scenarios.html b/Tests/LibWeb/Text/input/layout-run-cache-optimization-scenarios.html new file mode 100644 index 0000000000000..d16ad73bcd7db --- /dev/null +++ b/Tests/LibWeb/Text/input/layout-run-cache-optimization-scenarios.html @@ -0,0 +1,369 @@ + + + + + diff --git a/Tests/LibWeb/Text/input/layout-run-cache-viewport-resize-scenarios.html b/Tests/LibWeb/Text/input/layout-run-cache-viewport-resize-scenarios.html new file mode 100644 index 0000000000000..97681522eb8da --- /dev/null +++ b/Tests/LibWeb/Text/input/layout-run-cache-viewport-resize-scenarios.html @@ -0,0 +1,93 @@ + + + + + diff --git a/Tests/LibWeb/Text/input/layout-run-cache-viewport-resize.html b/Tests/LibWeb/Text/input/layout-run-cache-viewport-resize.html new file mode 100644 index 0000000000000..3559b9cf7a8fd --- /dev/null +++ b/Tests/LibWeb/Text/input/layout-run-cache-viewport-resize.html @@ -0,0 +1,31 @@ + + + +