From 96cc04d1e86a4f00445709ca64229fe7b4a1baef Mon Sep 17 00:00:00 2001 From: ldm0 Date: Thu, 15 Oct 2026 16:33:05 +0800 Subject: [PATCH] perf(layout): keep numeric scratch out of retained boxes Keep pass-local Taffy caches on the working world and store resolved Grid tracks and collapsed borders only for their actual owners. Retain construction objects within the unchanged document budget without retaining empty numeric slots. --- moli-layout/src/paint/cull.rs | 2 +- moli-layout/src/paint/table.rs | 2 +- moli-layout/src/projection.rs | 5 +- moli-layout/src/stacking.rs | 4 +- moli-layout/src/table/collapsed_borders.rs | 4 +- moli-layout/src/taffy_tree.rs | 28 ++++++----- .../src/taffy_tree/measurement_tests.rs | 48 +++++++++++++++++++ moli-layout/src/working_set/retention.rs | 3 -- moli-layout/src/world.rs | 45 ++++++++--------- .../tests/realtime_working_sets/table.rs | 1 + 10 files changed, 98 insertions(+), 44 deletions(-) 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'")