mirror of
https://github.com/lexmount/moli.git
synced 2026-11-03 16:07:36 +00:00
perf(layout): allocate numeric caches only for stored boxes
Keep Taffy caches in pass-owned compact storage instead of every LayoutBox. Allocate entries on actual stores, preserve viewport and per-box invalidation, and skip stores that Taffy ignores for hidden layout. Extracted from96cc04d1e8and6ca12dfc2awithout retained working sets, new layout policies, or renderer lifecycle changes.
This commit is contained in:
@@ -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<N>(
|
||||
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};
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
use super::*;
|
||||
use crate::{LayoutDisplay, PaintColor, ResolvedLayoutStyle};
|
||||
use taffy::WritingMode;
|
||||
|
||||
fn block(source: usize) -> crate::LayoutBox<usize> {
|
||||
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::<taffy::Cache>()
|
||||
+ world.box_cache_slots.capacity() * std::mem::size_of::<usize>();
|
||||
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);
|
||||
}
|
||||
@@ -279,7 +279,6 @@ pub struct LayoutBox<N> {
|
||||
/// 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,
|
||||
@@ -551,13 +550,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
|
||||
}
|
||||
@@ -647,6 +639,11 @@ where
|
||||
N: Copy + Debug + Eq + Hash,
|
||||
{
|
||||
pub(crate) boxes: Vec<LayoutBox<N>>,
|
||||
/// 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<Cache>,
|
||||
/// Box-indexed lookup into compact cache storage; usize::MAX is absent.
|
||||
pub(crate) box_cache_slots: Vec<usize>,
|
||||
pub(crate) source_mapping: HashMap<N, LayoutBoxId>,
|
||||
pub(crate) display_contents_mapping: HashMap<N, Vec<LayoutBoxId>>,
|
||||
pub(crate) root: LayoutBoxId,
|
||||
@@ -674,6 +671,8 @@ where
|
||||
pub(crate) fn new(root: LayoutBox<N>, 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),
|
||||
|
||||
Reference in New Issue
Block a user