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.
This commit is contained in:
ldm0
2026-10-15 16:33:05 +08:00
parent cce1265498
commit 96cc04d1e8
10 changed files with 98 additions and 44 deletions
+1 -1
View File
@@ -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;
+1 -1
View File
@@ -20,7 +20,7 @@ pub(super) fn project_collapsed_table_borders<N>(
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()];
+4 -1
View File
@@ -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,
+2 -2
View File
@@ -113,7 +113,7 @@ fn emit_context<N>(
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<N>(
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,
+2 -2
View File
@@ -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<N>(
@@ -479,7 +479,7 @@ pub(super) fn set_collapsed_border_geometry<N>(
) 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);
}
}
+16 -12
View File
@@ -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);
}
}
@@ -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));
}
-3
View File
@@ -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);
+23 -22
View File
@@ -261,23 +261,11 @@ pub struct LayoutBox<N> {
pub(crate) replaced_context: Option<ReplacedContext>,
pub(crate) replaced_image: Option<crate::LayoutImageResource>,
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<crate::table::CollapsedTableBorders>,
/// 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<LayoutResolvedGridTracks>,
pub(crate) cache: Cache,
pub(crate) table_measure_cache: Option<Box<crate::table::TableMeasureCache>>,
pub(crate) unrounded_layout: Layout,
pub(crate) final_layout: Layout,
@@ -549,13 +537,6 @@ impl Default for ViewportLayoutState {
}
impl<N> LayoutBox<N> {
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<Option<InlineFormattingContext>>,
/// 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<Cache>,
/// 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<LayoutBoxId, crate::table::CollapsedTableBorders>,
/// 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<LayoutBoxId, LayoutResolvedGridTracks>,
pub(crate) source_mapping: HashMap<N, LayoutBoxId>,
pub(crate) display_contents_mapping: HashMap<N, Vec<LayoutBoxId>>,
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),
@@ -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'")