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 nodes
Use a small box index and materialize pass-local Taffy caches only for actual stores. Preserve viewport and selective invalidation semantics, avoid hidden-mode allocations, and expose optional allocation diagnostics.
This commit is contained in:
@@ -326,6 +326,19 @@ where
|
||||
let phase_started = Instant::now();
|
||||
let numeric_metrics = compute_world_layout_with_scrollbars(&mut world, request.viewport);
|
||||
let numeric_layout_elapsed = phase_started.elapsed();
|
||||
if moli_trace::cpu_profile_enabled() {
|
||||
tracing::info!(
|
||||
target: "moli_cpu_profile",
|
||||
stage = "layout_numeric_cache",
|
||||
box_count = world.len(),
|
||||
cache_count = world.box_caches.len(),
|
||||
cache_capacity = world.box_caches.capacity(),
|
||||
box_index_slots = world.box_cache_slots.len(),
|
||||
estimated_cache_bytes = world.box_caches.capacity()
|
||||
* std::mem::size_of::<taffy::Cache>()
|
||||
+ world.box_cache_slots.capacity() * std::mem::size_of::<usize>(),
|
||||
);
|
||||
}
|
||||
let phase_started = Instant::now();
|
||||
let mut embedded_frames = HashMap::new();
|
||||
for (index, layout_box) in world.boxes.iter().enumerate() {
|
||||
|
||||
@@ -4,7 +4,7 @@ use parley::{AlignmentOptions, PositionedLayoutItem, YieldData};
|
||||
use style::Atom;
|
||||
use taffy::{
|
||||
AbsoluteAxis, AlignContent, AlignContentKeyword, AlignmentSafety, AutoSizeBehavior,
|
||||
AvailableSpace, BlockContext, BlockFormattingContext, BoxSizing, Cache, CacheTree, Clear,
|
||||
AvailableSpace, BlockContext, BlockFormattingContext, BoxSizing, CacheTree, Clear,
|
||||
DetailedGridInfo, Dimension, Display, FlexDirection, FloatDirection, Layout,
|
||||
LayoutBlockContainer, LayoutFlexboxContainer, LayoutGridContainer, LayoutInput, LayoutOutput,
|
||||
LayoutPartialTree, LeafLayoutContext, Line, MaybeMath, MaybeResolve, NodeId, Point,
|
||||
@@ -1721,13 +1721,17 @@ where
|
||||
if self.is_viewport_taffy_node(node_id) {
|
||||
self.viewport_layout.cache.get(inputs)
|
||||
} else {
|
||||
self.box_caches
|
||||
.get(LayoutBoxId::from_taffy(node_id).index())
|
||||
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 Cache::store ignores this mode. Do not materialize
|
||||
// measurement storage for an operation that cannot write it.
|
||||
return;
|
||||
}
|
||||
if self.is_viewport_taffy_node(node_id) {
|
||||
self.viewport_layout.cache.store(inputs, output);
|
||||
} else {
|
||||
@@ -1738,10 +1742,7 @@ where
|
||||
// first-class overflow dependencies.
|
||||
self.record_numeric_layout_touch(id);
|
||||
}
|
||||
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);
|
||||
self.numeric_cache_mut(id).store(inputs, output);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -423,3 +423,78 @@ fn numeric_cache_growth_keeps_box_viewport_and_invalidation_owners_distinct() {
|
||||
assert_eq!(world.cache_get(viewport, &input), None);
|
||||
assert_eq!(world.cache_get(sibling, &input), Some(sibling_output));
|
||||
}
|
||||
|
||||
fn many_flattened_boxes() -> (LayoutWorld<u8>, LayoutBoxId, LayoutInput, LayoutOutput) {
|
||||
let mut world = text_world(false);
|
||||
let output = measure(&mut world, inputs(), true);
|
||||
let style = world.boxes[0].style.clone();
|
||||
for _ in 0..4096 {
|
||||
let mut text = LayoutWorld::new_box(
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
"flattened text".to_owned(),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
LayoutBoxKind::Text,
|
||||
style.clone(),
|
||||
Some("text".into()),
|
||||
);
|
||||
text.inline_flattened = true;
|
||||
text.inline_context_owner = Some(world.root());
|
||||
world.boxes.push(text);
|
||||
}
|
||||
let sibling = LayoutBoxId::from_index(world.len());
|
||||
let mut extra = text_world(false).boxes.remove(0);
|
||||
extra.source = Some(1);
|
||||
world.boxes.push(extra);
|
||||
let input = LayoutInput {
|
||||
run_mode: RunMode::PerformLayout,
|
||||
..inputs()
|
||||
};
|
||||
(world, sibling, input, output)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn numeric_cache_allocation_follows_actual_stores_in_a_large_flattened_tree() {
|
||||
let (mut world, sibling, input, output) = many_flattened_boxes();
|
||||
world.cache_store(world.root().to_taffy(), &input, output);
|
||||
world.cache_store(sibling.to_taffy(), &input, output);
|
||||
assert_eq!(
|
||||
world.cache_get(world.root().to_taffy(), &input),
|
||||
Some(output)
|
||||
);
|
||||
assert_eq!(world.cache_get(sibling.to_taffy(), &input), Some(output));
|
||||
let bytes = world.box_caches.capacity() * std::mem::size_of::<taffy::Cache>()
|
||||
+ world.box_cache_slots.capacity() * std::mem::size_of::<usize>();
|
||||
eprintln!(
|
||||
"numeric-cache boxes={} initialized={} allocated_slots={} visible_capacity_bytes={bytes}",
|
||||
world.len(),
|
||||
world
|
||||
.box_caches
|
||||
.iter()
|
||||
.filter(|cache| !cache.is_empty())
|
||||
.count(),
|
||||
world.box_caches.capacity(),
|
||||
);
|
||||
// Two numeric consumers must not initialize thousands of 792-byte caches
|
||||
// for text boxes whose geometry comes from their containing IFC.
|
||||
assert!(bytes <= 64 * 1024, "{bytes} bytes for two numeric stores");
|
||||
world.cache_clear(world.root().to_taffy());
|
||||
assert_eq!(world.cache_get(world.root().to_taffy(), &input), None);
|
||||
assert_eq!(world.cache_get(sibling.to_taffy(), &input), Some(output));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn numeric_hidden_cache_store_does_not_allocate_measurement_storage() {
|
||||
let (mut world, _, input, output) = many_flattened_boxes();
|
||||
let input = LayoutInput {
|
||||
run_mode: RunMode::PerformHiddenLayout,
|
||||
..input
|
||||
};
|
||||
world.cache_store(world.root().to_taffy(), &input, output);
|
||||
assert_eq!(world.cache_get(world.root().to_taffy(), &input), None);
|
||||
assert_eq!(world.box_caches.capacity(), 0);
|
||||
assert_eq!(world.box_cache_slots.capacity(), 0);
|
||||
}
|
||||
|
||||
@@ -630,11 +630,12 @@ 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.
|
||||
/// Numeric caches live for this world only. Materialize a cache only for
|
||||
/// an actual store; flattened text boxes do not need measurement arrays.
|
||||
pub(crate) box_caches: Vec<Cache>,
|
||||
/// Box-indexed lookup into the compact cache storage. usize::MAX means
|
||||
/// that no cache has been stored for this box in the current world.
|
||||
pub(crate) box_cache_slots: Vec<usize>,
|
||||
/// 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>,
|
||||
@@ -688,6 +689,7 @@ where
|
||||
boxes,
|
||||
inline_layouts: Vec::new(),
|
||||
box_caches: Vec::new(),
|
||||
box_cache_slots: Vec::new(),
|
||||
collapsed_table_borders: HashMap::new(),
|
||||
resolved_grid_tracks: HashMap::new(),
|
||||
source_mapping,
|
||||
@@ -714,7 +716,11 @@ where
|
||||
}
|
||||
|
||||
pub(crate) fn clear_layout_caches(&mut self, id: LayoutBoxId) {
|
||||
if let Some(cache) = self.box_caches.get_mut(id.index()) {
|
||||
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 {
|
||||
@@ -722,6 +728,34 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
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() {
|
||||
// A capacity hint, not eligibility: numeric growth or an
|
||||
// unusual caller may still store any valid box afterwards.
|
||||
// Reserve without initializing caches for unvisited boxes.
|
||||
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
|
||||
}
|
||||
|
||||
@@ -117,7 +117,7 @@ fn realtime_table_reuses_actual_box_storage_within_the_default_budget() {
|
||||
const ITEMS: usize = 256;
|
||||
let mut vm = table_fixture(ITEMS);
|
||||
// A smaller real table admits the actual box storage without increasing
|
||||
// the production limit. The 1024-item fixture covers compact fallback.
|
||||
// the production limit. The 1024-item fixture covers full-scale admission.
|
||||
let initial = vm.eval(READ).unwrap();
|
||||
let cold = main_stats(&vm).unwrap();
|
||||
assert!(cold.retained_boxes >= 2 * ITEMS, "{cold:?}");
|
||||
|
||||
Reference in New Issue
Block a user