diff --git a/moli-layout/src/paint/cull.rs b/moli-layout/src/paint/cull.rs index d401df6053..f0d4dcf96f 100644 --- a/moli-layout/src/paint/cull.rs +++ b/moli-layout/src/paint/cull.rs @@ -253,7 +253,7 @@ where if !layout_box.style.is_visible() { return PaintCullBounds::Empty; } - let Some(borders) = layout_box.collapsed_table_borders.as_ref() else { + let Some(borders) = projection.world.collapsed_table_borders.get(&id) else { return PaintCullBounds::Empty; }; let mut bounds = PaintCullBounds::Empty; diff --git a/moli-layout/src/paint/table.rs b/moli-layout/src/paint/table.rs index d91dee7234..f29b8d9335 100644 --- a/moli-layout/src/paint/table.rs +++ b/moli-layout/src/paint/table.rs @@ -20,7 +20,7 @@ pub(super) fn project_collapsed_table_borders( if !layout_box.style.is_visible() { return; } - let Some(borders) = layout_box.collapsed_table_borders.as_ref() else { + let Some(borders) = projection.world.collapsed_table_borders.get(&id) else { return; }; let geometry = &projection.boxes[id.index()]; diff --git a/moli-layout/src/projection.rs b/moli-layout/src/projection.rs index 17b47ab050..e54316b531 100644 --- a/moli-layout/src/projection.rs +++ b/moli-layout/src/projection.rs @@ -969,7 +969,10 @@ where }, horizontal: layout_box.style.uses_horizontal_writing_mode(), }); - let resolved_grid_tracks = layout_box.resolved_grid_tracks.clone(); + let resolved_grid_tracks = world + .resolved_grid_tracks + .get(&LayoutBoxId::from_index(geometry.id.index())) + .cloned(); FrozenLayoutBox { geometry, scroll_extent, diff --git a/moli-layout/src/stacking.rs b/moli-layout/src/stacking.rs index 421d298814..6553e09d27 100644 --- a/moli-layout/src/stacking.rs +++ b/moli-layout/src/stacking.rs @@ -113,7 +113,7 @@ fn emit_context( kind: UnitKind::Contents, sequence: next_sequence(sequence), }); - if world.boxes[root.index()].collapsed_table_borders.is_some() { + if world.collapsed_table_borders.contains_key(&root) { collection.table_collapsed_borders.push(PaintUnit { id: root, kind: UnitKind::TableCollapsedBorders, @@ -245,7 +245,7 @@ fn collect_subtree( for child in ordered_children(world, id) { collect_subtree(world, child, descendant_group, sequence, collection); } - if layout_box.collapsed_table_borders.is_some() { + if world.collapsed_table_borders.contains_key(&id) { push_unit( collection, group, diff --git a/moli-layout/src/table/collapsed_borders.rs b/moli-layout/src/table/collapsed_borders.rs index 744176d2e4..4a80fd0609 100644 --- a/moli-layout/src/table/collapsed_borders.rs +++ b/moli-layout/src/table/collapsed_borders.rs @@ -468,7 +468,7 @@ where { world.boxes[part.index()].collapsed_table_border_part = true; } - world.boxes[root.index()].collapsed_table_borders = Some(borders); + world.collapsed_table_borders.insert(root, borders); } pub(super) fn set_collapsed_border_geometry( @@ -479,7 +479,7 @@ pub(super) fn set_collapsed_border_geometry( ) where N: Copy + Debug + Eq + Hash, { - if let Some(borders) = world.boxes[root.index()].collapsed_table_borders.as_mut() { + if let Some(borders) = world.collapsed_table_borders.get_mut(&root) { borders.set_geometry(columns, rows); } } diff --git a/moli-layout/src/taffy_tree.rs b/moli-layout/src/taffy_tree.rs index a0d721b888..954526b239 100644 --- a/moli-layout/src/taffy_tree.rs +++ b/moli-layout/src/taffy_tree.rs @@ -4,7 +4,7 @@ use parley::{AlignmentOptions, PositionedLayoutItem, YieldData}; use style::Atom; use taffy::{ AbsoluteAxis, AlignContent, AlignContentKeyword, AlignmentSafety, AutoSizeBehavior, - AvailableSpace, BlockContext, BlockFormattingContext, BoxSizing, CacheTree, Clear, + AvailableSpace, BlockContext, BlockFormattingContext, BoxSizing, Cache, CacheTree, Clear, DetailedGridInfo, Dimension, Display, FlexDirection, FloatDirection, Layout, LayoutBlockContainer, LayoutFlexboxContainer, LayoutGridContainer, LayoutInput, LayoutOutput, LayoutPartialTree, LeafLayoutContext, Line, MaybeMath, MaybeResolve, NodeId, Point, @@ -114,7 +114,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); } @@ -140,15 +140,16 @@ where N: Copy + Debug + Eq + Hash, { world.prepare_inline_storage(); - for layout_box in &mut world.boxes { - layout_box.clear_layout_caches(); + world.resolved_grid_tracks.clear(); + 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; layout_box.layout_children.clear(); layout_box.positioned_containing_block = None; layout_box.inline_static_position = None; - layout_box.resolved_grid_tracks = None; } world.viewport_layout.children.clear(); @@ -1720,9 +1721,9 @@ 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.box_caches + .get(LayoutBoxId::from_taffy(node_id).index()) + .and_then(|cache| cache.get(inputs)) } } @@ -1737,7 +1738,10 @@ where // first-class overflow dependencies. self.record_numeric_layout_touch(id); } - self.boxes[id.index()].cache.store(inputs, output); + if self.box_caches.len() <= id.index() { + self.box_caches.resize_with(self.boxes.len(), Cache::new); + } + self.box_caches[id.index()].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)); } } } @@ -1876,8 +1880,8 @@ where else { return; }; - self.boxes[LayoutBoxId::from_taffy(node_id).index()].resolved_grid_tracks = - Some(resolved_grid_tracks); + self.resolved_grid_tracks + .insert(LayoutBoxId::from_taffy(node_id), resolved_grid_tracks); } } diff --git a/moli-layout/src/taffy_tree/measurement_tests.rs b/moli-layout/src/taffy_tree/measurement_tests.rs index 65b17ee0bd..5800b95134 100644 --- a/moli-layout/src/taffy_tree/measurement_tests.rs +++ b/moli-layout/src/taffy_tree/measurement_tests.rs @@ -375,3 +375,51 @@ fn inline_measurement_width_dependencies_remain_in_the_key() { ); } } + +#[test] +fn numeric_cache_growth_keeps_box_viewport_and_invalidation_owners_distinct() { + let mut world = text_world(false); + let output = measure(&mut world, inputs(), true); + let input = LayoutInput { + run_mode: RunMode::PerformLayout, + ..inputs() + }; + let root = world.root().to_taffy(); + assert_eq!(world.cache_get(root, &input), None); + world.cache_store(root, &input, output); + assert_eq!(world.cache_get(root, &input), Some(output)); + assert_eq!( + world.cache_get( + root, + &LayoutInput { + sizing_purpose: SizingPurpose::IntrinsicContribution, + ..input + } + ), + None + ); + + // The old viewport ID becomes a real box ID when numeric preparation + // appends a box. Its cache must start empty, without losing viewport work. + let sibling = world.viewport_taffy_node(); + let mut viewport_output = output; + viewport_output.size.width += 10.0; + world.cache_store(sibling, &input, viewport_output); + let mut extra = text_world(false).boxes.remove(0); + extra.source = Some(1); + world.boxes.push(extra); + assert_eq!(world.cache_get(sibling, &input), None); + let viewport = world.viewport_taffy_node(); + assert_eq!(world.cache_get(viewport, &input), Some(viewport_output)); + + let mut sibling_output = output; + sibling_output.size.width += 20.0; + world.cache_store(sibling, &input, sibling_output); + world.cache_clear(root); + assert_eq!(world.cache_get(root, &input), None); + assert_eq!(world.cache_get(sibling, &input), Some(sibling_output)); + assert_eq!(world.cache_get(viewport, &input), Some(viewport_output)); + world.cache_clear(viewport); + assert_eq!(world.cache_get(viewport, &input), None); + assert_eq!(world.cache_get(sibling, &input), Some(sibling_output)); +} diff --git a/moli-layout/src/working_set/retention.rs b/moli-layout/src/working_set/retention.rs index cc7db4f2b5..32315864c6 100644 --- a/moli-layout/src/working_set/retention.rs +++ b/moli-layout/src/working_set/retention.rs @@ -96,10 +96,7 @@ where layout_box.replaced_context = None; layout_box.replaced_image = None; layout_box.css_images = Default::default(); - layout_box.collapsed_table_borders = None; layout_box.collapsed_table_border_part = false; - layout_box.resolved_grid_tracks = None; - layout_box.cache.clear(); layout_box.table_measure_cache = None; layout_box.unrounded_layout = taffy::Layout::with_order(0); layout_box.final_layout = taffy::Layout::with_order(0); diff --git a/moli-layout/src/world.rs b/moli-layout/src/world.rs index 41f3367290..e980d2e116 100644 --- a/moli-layout/src/world.rs +++ b/moli-layout/src/world.rs @@ -261,23 +261,11 @@ pub struct LayoutBox { pub(crate) replaced_context: Option, pub(crate) replaced_image: Option, pub(crate) css_images: crate::source::LayoutCssImageResources, - /// Winning collapsed-table edges owned by the table wrapper for this pass. - /// - /// The record contains only resolved numeric/color/style data. It is - /// produced before Taffy sizing, completed with grid-line geometry after - /// sizing, and consumed once by immutable paint projection. - pub(crate) collapsed_table_borders: Option, /// Suppresses ordinary per-box border paint for parts participating in a /// collapsed table. Their authored borders have already entered the table /// owner's conflict-resolution grid. pub(crate) collapsed_table_border_part: bool, pub(crate) inline_formatting_context: bool, - /// Used Grid tracks produced by the numeric layout pass. - /// - /// 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, @@ -549,13 +537,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 } @@ -649,6 +630,17 @@ where /// large empty slots out of persistent box objects. Retention extracts /// only actual owners into the existing sparse working-set storage. pub(crate) inline_layouts: Vec>, + /// Numeric caches live for this world only. Allocate dense slots on the + /// first actual store, after construction and numeric-only box growth. + /// Keeping empty caches out of LayoutBox avoids retaining their fixed + /// measurement arrays with an otherwise reusable construction graph. + pub(crate) box_caches: Vec, + /// Winning collapsed-table edges, computed by the shared table formatter + /// and consumed by paint projection. Only actual table owners have entries. + pub(crate) collapsed_table_borders: HashMap, + /// Used Grid tracks from this numeric pass. Freeze the actual owners' + /// results for CSSOM; retain neither these records nor empty per-box slots. + pub(crate) resolved_grid_tracks: HashMap, pub(crate) source_mapping: HashMap, pub(crate) display_contents_mapping: HashMap>, pub(crate) root: LayoutBoxId, @@ -695,6 +687,9 @@ where Self { boxes, inline_layouts: Vec::new(), + box_caches: Vec::new(), + collapsed_table_borders: HashMap::new(), + resolved_grid_tracks: HashMap::new(), source_mapping, display_contents_mapping: HashMap::new(), root: LayoutBoxId::from_index(0), @@ -718,6 +713,15 @@ where self.inline_layouts.resize_with(self.boxes.len(), || None); } + pub(crate) fn clear_layout_caches(&mut self, id: LayoutBoxId) { + if let Some(cache) = self.box_caches.get_mut(id.index()) { + cache.clear(); + } + if let Some(cache) = &mut self.boxes[id.index()].table_measure_cache { + cache.clear(); + } + } + pub fn root(&self) -> LayoutBoxId { self.root } @@ -1260,11 +1264,8 @@ where replaced_context: None, replaced_image: None, css_images: crate::source::LayoutCssImageResources::default(), - collapsed_table_borders: None, 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), diff --git a/moli-renderer-v8/src/script_vm/tests/realtime_working_sets/table.rs b/moli-renderer-v8/src/script_vm/tests/realtime_working_sets/table.rs index 99b6e13ef8..42dbfb5dbc 100644 --- a/moli-renderer-v8/src/script_vm/tests/realtime_working_sets/table.rs +++ b/moli-renderer-v8/src/script_vm/tests/realtime_working_sets/table.rs @@ -51,6 +51,7 @@ fn realtime_table_reuses_unchanged_text_results_within_the_default_budget() { let cold = main_stats(&vm).unwrap(); assert!(cold.retained_boxes >= 2 * ITEMS, "{cold:?}"); assert!(cold.estimated_construction_bytes > 0, "{cold:?}"); + assert_eq!(cold.retained_owned_boxes, cold.retained_boxes, "{cold:?}"); assert!(cold.estimated_retained_bytes <= moli_layout::DEFAULT_DOCUMENT_WORKING_SET_BYTES); let builds = vm.layout_pass_observability_for_test().1; vm.eval("sheet.querySelector('.item').style.width='333px'")