diff --git a/moli-layout/src/taffy_tree.rs b/moli-layout/src/taffy_tree.rs index 61b7bf278d..18b89f5233 100644 --- a/moli-layout/src/taffy_tree.rs +++ b/moli-layout/src/taffy_tree.rs @@ -113,7 +113,7 @@ impl PreparedWorldLayout { } self.feedback_invalidation_marks[id.index()] = true; invalidated.push(id); - world.boxes[id.index()].clear_layout_caches(); + world.clear_layout_caches(id); if let Some(parent) = world.boxes[id.index()].layout_parent { self.feedback_invalidation_worklist.push(parent); } @@ -138,8 +138,9 @@ pub(crate) fn prepare_world_layout( where N: Copy + Debug + Eq + Hash, { - for layout_box in &mut world.boxes { - layout_box.clear_layout_caches(); + for index in 0..world.boxes.len() { + world.clear_layout_caches(LayoutBoxId::from_index(index)); + let layout_box = &mut world.boxes[index]; layout_box.unrounded_layout = Layout::with_order(0); layout_box.final_layout = Layout::with_order(0); layout_box.layout_parent = None; @@ -1720,13 +1721,16 @@ where if self.is_viewport_taffy_node(node_id) { self.viewport_layout.cache.get(inputs) } else { - self.boxes[LayoutBoxId::from_taffy(node_id).index()] - .cache - .get(inputs) + self.numeric_cache(LayoutBoxId::from_taffy(node_id)) + .and_then(|cache| cache.get(inputs)) } } fn cache_store(&mut self, node_id: NodeId, inputs: &LayoutInput, output: LayoutOutput) { + if inputs.run_mode == RunMode::PerformHiddenLayout { + // Taffy's store ignores this mode; avoid allocating an empty cache. + return; + } if self.is_viewport_taffy_node(node_id) { self.viewport_layout.cache.store(inputs, output); } else { @@ -1737,7 +1741,7 @@ where // first-class overflow dependencies. self.record_numeric_layout_touch(id); } - self.boxes[id.index()].cache.store(inputs, output); + self.numeric_cache_mut(id).store(inputs, output); } } @@ -1745,7 +1749,7 @@ where if self.is_viewport_taffy_node(node_id) { self.viewport_layout.cache.clear(); } else { - self.boxes[LayoutBoxId::from_taffy(node_id).index()].clear_layout_caches(); + self.clear_layout_caches(LayoutBoxId::from_taffy(node_id)); } } } @@ -3199,6 +3203,9 @@ fn measure_text( } } +#[cfg(test)] +mod cache_storage_tests; + #[cfg(test)] mod tests { use super::{inline_percentage_basis, round_layout_to_css_subpixels}; diff --git a/moli-layout/src/taffy_tree/cache_storage_tests.rs b/moli-layout/src/taffy_tree/cache_storage_tests.rs new file mode 100644 index 0000000000..7cdcb89bd5 --- /dev/null +++ b/moli-layout/src/taffy_tree/cache_storage_tests.rs @@ -0,0 +1,145 @@ +use super::*; +use crate::{LayoutDisplay, PaintColor, ResolvedLayoutStyle}; +use taffy::WritingMode; + +fn block(source: usize) -> crate::LayoutBox { + LayoutWorld::new_box( + Some(source), + None, + None, + format!("cache-block-{source}"), + None, + None, + None, + LayoutBoxKind::PrincipalBlock, + ResolvedLayoutStyle::synthetic( + LayoutDisplay::Block, + Style::default(), + PaintColor::TRANSPARENT, + ), + None, + ) +} + +fn input() -> LayoutInput { + LayoutInput { + run_mode: RunMode::PerformLayout, + sizing_mode: SizingMode::InherentSize, + sizing_purpose: SizingPurpose::Layout, + axis: taffy::RequestedAxis::Both, + known_dimensions: Size { + width: Some(60.0), + height: Some(20.0), + }, + definite_dimensions: Size { + width: Some(60.0), + height: Some(20.0), + }, + parent_size: Size { + width: Some(240.0), + height: None, + }, + parent_writing_mode: WritingMode::HorizontalTb, + available_space: Size { + width: AvailableSpace::Definite(60.0), + height: AvailableSpace::MaxContent, + }, + block_auto_behavior: AutoSizeBehavior::FitContent, + vertical_margins_are_collapsible: taffy::Line::FALSE, + } +} + +fn output(width: f32) -> LayoutOutput { + let size = Size { + width, + height: 20.0, + }; + LayoutOutput::from_sizes(size, size) +} + +#[test] +fn numeric_cache_growth_keeps_box_viewport_and_invalidation_owners_distinct() { + let mut world = LayoutWorld::new(block(0), false); + let input = input(); + let root = world.root().to_taffy(); + assert_eq!(world.cache_get(root, &input), None); + world.cache_store(root, &input, output(60.0)); + assert_eq!(world.cache_get(root, &input), Some(output(60.0))); + assert_eq!( + world.cache_get( + root, + &LayoutInput { + sizing_purpose: SizingPurpose::IntrinsicContribution, + ..input + } + ), + None + ); + + // Numeric box growth reuses the old viewport ID for a real box. Its + // cache starts empty, while the viewport's own cache survives the growth. + let sibling = world.viewport_taffy_node(); + world.cache_store(sibling, &input, output(70.0)); + world.boxes.push(block(1)); + assert_eq!(world.cache_get(sibling, &input), None); + let viewport = world.viewport_taffy_node(); + assert_eq!(world.cache_get(viewport, &input), Some(output(70.0))); + + world.cache_store(sibling, &input, output(80.0)); + world.cache_clear(root); + assert_eq!(world.cache_get(root, &input), None); + assert_eq!(world.cache_get(sibling, &input), Some(output(80.0))); + assert_eq!(world.cache_get(viewport, &input), Some(output(70.0))); + world.cache_clear(viewport); + assert_eq!(world.cache_get(viewport, &input), None); + assert_eq!(world.cache_get(sibling, &input), Some(output(80.0))); +} + +#[test] +fn numeric_cache_allocation_follows_actual_stores_in_a_large_flattened_tree() { + let mut world = LayoutWorld::new(block(0), false); + for source in 1..=4096 { + let mut text = block(source); + text.inline_flattened = true; + text.inline_context_owner = Some(world.root()); + world.boxes.push(text); + } + let sibling = LayoutBoxId::from_index(world.len()); + world.boxes.push(block(4097)); + let input = input(); + let root = world.root().to_taffy(); + world.cache_store(root, &input, output(60.0)); + world.cache_store(sibling.to_taffy(), &input, output(80.0)); + assert_eq!(world.cache_get(root, &input), Some(output(60.0))); + assert_eq!( + world.cache_get(sibling.to_taffy(), &input), + Some(output(80.0)) + ); + let allocated_bytes = world.box_caches.capacity() * std::mem::size_of::() + + world.box_cache_slots.capacity() * std::mem::size_of::(); + assert!( + allocated_bytes < 64 * 1024, + "4098 construction boxes with two stores should not allocate dense caches: {allocated_bytes} bytes" + ); + // Admission hints must not prevent a later valid store on a flattened box. + let previously_unvisited = LayoutBoxId::from_index(4096).to_taffy(); + world.cache_store(previously_unvisited, &input, output(90.0)); + assert_eq!( + world.cache_get(previously_unvisited, &input), + Some(output(90.0)) + ); +} + +#[test] +fn hidden_numeric_store_does_not_materialize_empty_cache_storage() { + let mut world = LayoutWorld::new(block(0), false); + let input = LayoutInput { + run_mode: RunMode::PerformHiddenLayout, + ..input() + }; + world.cache_store(world.root().to_taffy(), &input, output(60.0)); + world.cache_store(world.viewport_taffy_node(), &input, output(70.0)); + assert!(world.box_caches.is_empty()); + assert!(world.box_cache_slots.is_empty()); + assert_eq!(world.cache_get(world.root().to_taffy(), &input), None); +} diff --git a/moli-layout/src/world.rs b/moli-layout/src/world.rs index 4ebd9ce0c1..d9d8aa7f73 100644 --- a/moli-layout/src/world.rs +++ b/moli-layout/src/world.rs @@ -279,7 +279,6 @@ pub struct LayoutBox { /// This is browser-owned canonical geometry rather than a retained Taffy /// object. It is frozen with the rest of the layout tree for CSSOM reads. pub(crate) resolved_grid_tracks: Option, - pub(crate) cache: Cache, pub(crate) table_measure_cache: Option>, pub(crate) unrounded_layout: Layout, pub(crate) final_layout: Layout, @@ -551,13 +550,6 @@ impl Default for ViewportLayoutState { } impl LayoutBox { - pub(crate) fn clear_layout_caches(&mut self) { - self.cache.clear(); - if let Some(cache) = &mut self.table_measure_cache { - cache.clear(); - } - } - pub fn kind(&self) -> LayoutBoxKind { self.kind } @@ -647,6 +639,11 @@ where N: Copy + Debug + Eq + Hash, { pub(crate) boxes: Vec>, + /// Pass-local numeric caches, created only for boxes with actual stores. + /// Flattened text boxes need no empty Taffy measurement arrays. + pub(crate) box_caches: Vec, + /// Box-indexed lookup into compact cache storage; usize::MAX is absent. + pub(crate) box_cache_slots: Vec, pub(crate) source_mapping: HashMap, pub(crate) display_contents_mapping: HashMap>, pub(crate) root: LayoutBoxId, @@ -674,6 +671,8 @@ where pub(crate) fn new(root: LayoutBox, root_is_document_element: bool) -> Self { Self { boxes: vec![root], + box_caches: Vec::new(), + box_cache_slots: Vec::new(), source_mapping: HashMap::new(), display_contents_mapping: HashMap::new(), root: LayoutBoxId::from_index(0), @@ -691,6 +690,47 @@ where } } + pub(crate) fn clear_layout_caches(&mut self, id: LayoutBoxId) { + if let Some(cache) = self + .box_cache_slots + .get(id.index()) + .and_then(|slot| self.box_caches.get_mut(*slot)) + { + cache.clear(); + } + if let Some(cache) = &mut self.boxes[id.index()].table_measure_cache { + cache.clear(); + } + } + + pub(crate) fn numeric_cache(&self, id: LayoutBoxId) -> Option<&Cache> { + self.box_cache_slots + .get(id.index()) + .and_then(|slot| self.box_caches.get(*slot)) + } + + pub(crate) fn numeric_cache_mut(&mut self, id: LayoutBoxId) -> &mut Cache { + self.box_cache_slots.resize(self.boxes.len(), usize::MAX); + let mut slot = self.box_cache_slots[id.index()]; + if slot == usize::MAX { + if self.box_caches.is_empty() { + // Reserve without initializing caches for unvisited boxes. + // This is a hint, not an eligibility restriction: a caller + // may subsequently store any valid construction/numeric box. + let candidates = self + .boxes + .iter() + .filter(|layout_box| !layout_box.inline_flattened) + .count(); + self.box_caches.reserve_exact(candidates); + } + slot = self.box_caches.len(); + self.box_caches.push(Cache::new()); + self.box_cache_slots[id.index()] = slot; + } + &mut self.box_caches[slot] + } + pub fn root(&self) -> LayoutBoxId { self.root } @@ -1206,7 +1246,6 @@ where collapsed_table_border_part: false, inline_formatting_context: false, resolved_grid_tracks: None, - cache: Cache::new(), table_measure_cache: None, unrounded_layout: Layout::with_order(0), final_layout: Layout::with_order(0),