use std::{any::TypeId, cmp::Reverse, collections::BinaryHeap};
use smallvec::SmallVec;
use crate::{
Applier, Node, NodeError, NodeId, NodeMarks, RecycleOrigin, RecyclePool, RecycledNode,
RuntimeHandle, bubble_passes,
collections::map::{HashMap, HashSet},
layout_dirt_roots,
node_marks::MarkPlace,
stable_index,
};
mod applier_impl;
#[derive(Default)]
pub struct MemoryApplier {
pub(crate) state: Box<MemoryApplierState>,
}
#[derive(Default)]
pub(crate) struct MemoryApplierState {
pub(crate) nodes: Vec<Option<Box<dyn Node>>>,
pub(crate) physical_stable_ids: Vec<u32>,
pub(crate) physical_warm_recycled_origins: Vec<RecycleOrigin>,
pub(crate) stable_index: stable_index::StableIndex,
pub(crate) free_ids: BinaryHeap<Reverse<usize>>,
pub(crate) high_ids: HashMap<NodeId, HighIdSlot>,
pub(crate) next_stable_id: NodeId,
pub(crate) layout_runtime: Option<RuntimeHandle>,
pub(crate) recycle_pools: HashMap<TypeId, RecyclePool>,
pub(crate) structural_change_parents: Vec<NodeId>,
pub(crate) virtual_node_ids: HashSet<NodeId>,
pub(crate) bubble_passes: bubble_passes::BubblePasses,
pub(crate) layout_dirt_roots: layout_dirt_roots::LayoutDirtRoots,
}
#[derive(Default)]
pub(crate) struct HighIdSlot {
node: Option<Box<dyn Node>>,
origin: RecycleOrigin,
generation: u32,
}
#[derive(Default)]
pub struct SceneNodeAttachmentScratch {
attached: NodeMarks,
path: Vec<MarkPlace>,
}
const ATTACHED: u8 = 1;
const DETACHED: u8 = 2;
struct RemovalFrame {
node_id: NodeId,
children: SmallVec<[NodeId; 8]>,
next_child: usize,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct MemoryApplierDebugStats {
pub next_stable_id: NodeId,
pub nodes_len: usize,
pub nodes_cap: usize,
pub physical_stable_ids_len: usize,
pub physical_stable_ids_cap: usize,
pub stable_to_physical_len: usize,
pub stable_to_physical_cap: usize,
pub stable_generations_len: usize,
pub stable_generations_cap: usize,
pub free_ids_len: usize,
pub free_ids_cap: usize,
pub high_id_nodes_len: usize,
pub high_id_nodes_cap: usize,
pub high_id_generations_len: usize,
pub high_id_generations_cap: usize,
pub recycled_type_count: usize,
pub recycled_type_cap: usize,
pub recycled_node_count: usize,
pub recycled_node_capacity: usize,
pub warm_recycled_node_id_count: usize,
pub warm_recycled_node_id_capacity: usize,
}
impl MemoryApplier {
const EAGER_COMPACT_NODE_LEN: usize = 1_024;
pub(crate) const HIGH_ID_THRESHOLD: NodeId = NodeId(1_000_000_000);
const INVALID_STABLE_ID: u32 = u32::MAX;
const INITIAL_DENSE_NODE_CAP: usize = 32;
const LARGE_DENSE_NODE_GROWTH_THRESHOLD: usize = 32 * 1024;
const LARGE_DENSE_NODE_GROWTH_DIVISOR: usize = 4;
fn pack_stable_id(stable_id: NodeId) -> u32 {
u32::try_from(stable_id.0).expect("stable id overflow")
}
fn unpack_stable_id(stable_id: u32) -> NodeId {
NodeId(stable_id as usize)
}
fn next_dense_node_target_len(old_len: usize) -> usize {
if old_len < Self::INITIAL_DENSE_NODE_CAP {
return Self::INITIAL_DENSE_NODE_CAP;
}
if old_len < Self::LARGE_DENSE_NODE_GROWTH_THRESHOLD {
return old_len.saturating_mul(2);
}
let incremental_growth =
(old_len / Self::LARGE_DENSE_NODE_GROWTH_DIVISOR).max(Self::INITIAL_DENSE_NODE_CAP);
old_len.saturating_add(incremental_growth)
}
fn ensure_dense_node_storage_capacity(&mut self) {
let len = self
.state
.nodes
.len()
.max(self.state.physical_stable_ids.len())
.max(self.state.physical_warm_recycled_origins.len());
if len < self.state.nodes.capacity()
&& len < self.state.physical_stable_ids.capacity()
&& len < self.state.physical_warm_recycled_origins.capacity()
{
return;
}
let target = Self::next_dense_node_target_len(len);
if self.state.nodes.capacity() < target {
self.state
.nodes
.reserve_exact(target.saturating_sub(self.state.nodes.len()));
}
if self.state.physical_stable_ids.capacity() < target {
self.state
.physical_stable_ids
.reserve_exact(target.saturating_sub(self.state.physical_stable_ids.len()));
}
if self.state.physical_warm_recycled_origins.capacity() < target {
self.state.physical_warm_recycled_origins.reserve_exact(
target.saturating_sub(self.state.physical_warm_recycled_origins.len()),
);
}
}
pub fn new() -> Self {
Self::default()
}
pub fn scene_node_attached_to(&mut self, node_id: NodeId, root: NodeId) -> Option<NodeId> {
let resolved = self.first_non_virtual_ancestor(node_id)?;
self.is_attached_to(resolved, root).then_some(resolved)
}
pub fn scene_nodes_attached_to(
&mut self,
nodes: impl IntoIterator<Item = NodeId>,
root: NodeId,
) -> Vec<Option<NodeId>> {
let mut result = Vec::new();
self.scene_nodes_attached_to_into(
nodes,
root,
&mut result,
&mut SceneNodeAttachmentScratch::default(),
);
result
}
pub fn scene_nodes_attached_to_into(
&mut self,
nodes: impl IntoIterator<Item = NodeId>,
root: NodeId,
output: &mut Vec<Option<NodeId>>,
scratch: &mut SceneNodeAttachmentScratch,
) {
let nodes = nodes.into_iter();
output.clear();
let (lower_bound, upper_bound) = nodes.size_hint();
if lower_bound == 0 && upper_bound == Some(0) {
scratch.path.clear();
return;
}
scratch.attached.reset(self);
scratch.attached.set(self, root, ATTACHED);
output.reserve(lower_bound);
scratch.path.clear();
for node_id in nodes {
let Some(resolved) = self.first_non_virtual_ancestor(node_id) else {
output.push(None);
continue;
};
let mut current = MarkPlace::of(self, resolved);
scratch.path.clear();
let answer = loop {
match scratch.attached.mark_at(current) {
ATTACHED => break true,
DETACHED => break false,
_ => {}
}
scratch.path.push(current);
match self.parent_at(current) {
Some(parent) if scratch.path.len() < 100_000 => {
current = MarkPlace::of(self, parent);
}
_ => break false,
}
};
let mark = if answer { ATTACHED } else { DETACHED };
for visited in scratch.path.drain(..) {
*scratch.attached.entry_at(visited) = mark;
}
output.push(answer.then_some(resolved));
}
}
pub fn take_structural_change_parents_attached_to(&mut self, root: NodeId) -> Vec<NodeId> {
let mut candidates = Vec::new();
self.take_structural_change_parents_into(&mut candidates);
let mut seen = HashSet::<NodeId>::default();
candidates.retain_mut(|parent_id| {
let Some(resolved) = self.first_non_virtual_ancestor(*parent_id) else {
return false;
};
*parent_id = resolved;
self.is_attached_to(resolved, root) && seen.insert(resolved)
});
candidates
}
pub fn take_structural_change_parents_into(&mut self, output: &mut Vec<NodeId>) {
output.clear();
std::mem::swap(&mut self.state.structural_change_parents, output);
}
fn first_non_virtual_ancestor(&mut self, node_id: NodeId) -> Option<NodeId> {
if self.state.virtual_node_ids.is_empty() {
return Some(node_id);
}
let mut current = node_id;
for _ in 0..100_000 {
if !self.state.virtual_node_ids.contains(¤t) {
return Some(current);
}
match self.get_mut(current) {
Ok(node) => current = node.parent()?,
Err(_) => return None,
}
}
None
}
fn is_attached_to(&mut self, node_id: NodeId, root: NodeId) -> bool {
let mut current = node_id;
for _ in 0..100_000 {
if current == root {
return true;
}
match self.get_mut(current) {
Ok(node) => match node.parent() {
Some(parent) => current = parent,
None => return false,
},
Err(_) => return false,
}
}
false
}
pub fn with_node<N: Node + 'static, R>(
&mut self,
id: NodeId,
f: impl FnOnce(&mut N) -> R,
) -> Result<R, NodeError> {
let typed = self
.get_mut(id)?
.as_any_mut()
.downcast_mut::<N>()
.ok_or_else(|| NodeError::TypeMismatch {
id,
expected: std::any::type_name::<N>(),
})?;
Ok(f(typed))
}
pub fn len(&self) -> usize {
self.state.nodes.iter().filter(|n| n.is_some()).count()
}
pub fn capacity(&self) -> usize {
self.state.nodes.len()
}
pub fn tombstone_count(&self) -> usize {
self.state.nodes.iter().filter(|n| n.is_none()).count()
}
pub fn freelist_len(&self) -> usize {
self.state.free_ids.len()
}
pub fn debug_recycled_node_count(&self) -> usize {
self.total_recycled_node_count()
}
pub fn debug_recycled_node_count_for<N: Node + 'static>(&self) -> usize {
let key = TypeId::of::<N>();
self.state
.recycle_pools
.get(&key)
.map_or(0, RecyclePool::len)
}
pub fn debug_stats(&self) -> MemoryApplierDebugStats {
MemoryApplierDebugStats {
next_stable_id: self.state.next_stable_id,
nodes_len: self.len(),
nodes_cap: self.state.nodes.len(),
physical_stable_ids_len: self.state.physical_stable_ids.len(),
physical_stable_ids_cap: self.state.physical_stable_ids.capacity(),
stable_to_physical_len: self.state.stable_index.live(),
stable_to_physical_cap: self.state.stable_index.capacity(),
stable_generations_len: self.state.stable_index.occupied(),
stable_generations_cap: self.state.stable_index.capacity(),
free_ids_len: self.state.free_ids.len(),
free_ids_cap: self.state.free_ids.capacity(),
high_id_nodes_len: self.high_id_nodes().count(),
high_id_nodes_cap: self.state.high_ids.capacity(),
high_id_generations_len: self.state.high_ids.len(),
high_id_generations_cap: self.state.high_ids.capacity(),
recycled_type_count: self
.state
.recycle_pools
.values()
.filter(|pool| pool.len() > 0)
.count(),
recycled_type_cap: self.state.recycle_pools.capacity(),
recycled_node_count: self.total_recycled_node_count(),
recycled_node_capacity: self.total_recycled_node_capacity(),
warm_recycled_node_id_count: self.total_warm_recycled_node_id_count(),
warm_recycled_node_id_capacity: self.total_warm_recycled_node_id_capacity(),
}
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
pub fn for_each_node_mut(&mut self, mut visit: impl FnMut(&mut dyn Node)) {
for node in self.state.nodes.iter_mut().flatten() {
visit(node.as_mut());
}
for node in self
.state
.high_ids
.values_mut()
.filter_map(|slot| slot.node.as_mut())
{
visit(node.as_mut());
}
}
pub fn debug_live_node_heap_bytes(&self) -> usize {
let dense_nodes = self
.state
.nodes
.iter()
.flatten()
.map(|node| size_of_val(&**node) + node.debug_heap_bytes())
.sum::<usize>();
let high_id_nodes = self
.high_id_nodes()
.map(|node| size_of_val(node) + node.debug_heap_bytes())
.sum::<usize>();
dense_nodes + high_id_nodes
}
pub fn debug_recycled_node_heap_bytes(&self) -> usize {
self.state
.recycle_pools
.values()
.flat_map(RecyclePool::nodes)
.map(|node| size_of_val(&*node.node) + node.node.debug_heap_bytes())
.sum::<usize>()
}
pub fn set_runtime_handle(&mut self, handle: RuntimeHandle) {
self.state.layout_runtime = Some(handle);
}
pub fn clear_runtime_handle(&mut self) {
self.state.layout_runtime = None;
}
pub fn runtime_handle(&self) -> Option<RuntimeHandle> {
self.state.layout_runtime.clone()
}
fn total_recycled_node_count(&self) -> usize {
self.state
.recycle_pools
.values()
.map(RecyclePool::len)
.sum()
}
fn total_recycled_node_capacity(&self) -> usize {
self.state
.recycle_pools
.values()
.map(RecyclePool::capacity)
.sum()
}
fn total_warm_recycled_node_id_count(&self) -> usize {
self.live_warm_recycled_origin_count()
+ self
.state
.recycle_pools
.values()
.map(|pool| pool.warm.len() + pool.returning.len())
.sum::<usize>()
}
fn total_warm_recycled_node_id_capacity(&self) -> usize {
self.live_warm_recycled_origin_capacity()
+ self
.state
.recycle_pools
.values()
.map(|pool| pool.warm.capacity() + pool.returning.capacity())
.sum::<usize>()
}
fn remember_recycle_pool_limit(&mut self, key: TypeId, recycle_pool_limit: Option<usize>) {
if recycle_pool_limit.is_some() || self.state.recycle_pools.contains_key(&key) {
self.state.recycle_pools.entry(key).or_default().limit = recycle_pool_limit;
}
}
fn warm_recycled_pool_len(&self, key: TypeId) -> usize {
self.state
.recycle_pools
.get(&key)
.map_or(0, |pool| pool.warm.len())
}
fn live_warm_recycled_origin_count(&self) -> usize {
self.state
.physical_warm_recycled_origins
.iter()
.zip(self.state.nodes.iter())
.filter(|(origin, node)| **origin == RecycleOrigin::Warm && node.is_some())
.count()
+ self
.state
.high_ids
.values()
.filter(|slot| slot.origin == RecycleOrigin::Warm && slot.node.is_some())
.count()
}
fn high_id_nodes(&self) -> impl Iterator<Item = &dyn Node> {
self.state
.high_ids
.values()
.filter_map(|slot| slot.node.as_deref())
}
fn live_warm_recycled_origin_capacity(&self) -> usize {
self.state.physical_warm_recycled_origins.capacity() + self.state.high_ids.capacity()
}
fn push_recycled_node(
&mut self,
key: TypeId,
recycle_pool_limit: Option<usize>,
recycled: RecycledNode,
) {
self.remember_recycle_pool_limit(key, recycle_pool_limit);
let pool = self.state.recycle_pools.entry(key).or_default();
pool.remember_prototype(recycled.node.as_ref());
pool.push(recycled);
}
fn seed_recycled_node_shell_impl(
&mut self,
key: TypeId,
recycle_pool_limit: Option<usize>,
shell: Box<dyn Node>,
) {
let limit = recycle_pool_limit.unwrap_or(usize::MAX);
if self.warm_recycled_pool_len(key) >= limit {
return;
}
self.remember_recycle_pool_limit(key, recycle_pool_limit);
let stable_id = self.state.next_stable_id;
self.state.next_stable_id = self.state.next_stable_id.next();
let pool = self.state.recycle_pools.entry(key).or_default();
pool.remember_prototype(shell.as_ref());
pool.push_warm(RecycledNode::from_shell(
stable_id,
shell,
RecycleOrigin::Warm,
));
}
fn prune_stable_generations(&mut self) {
let retained_len = self.state.stable_index.live() + self.total_recycled_node_count();
if retained_len == self.state.stable_index.occupied() {
return;
}
let recycled: HashSet<NodeId> = self
.state
.recycle_pools
.values()
.flat_map(RecyclePool::nodes)
.map(RecycledNode::stable_id)
.collect();
self.state
.stable_index
.retain_retired(|stable_id| recycled.contains(&stable_id));
}
pub fn dump_tree(&self, root: Option<NodeId>) -> String {
let mut output = String::new();
if let Some(root_id) = root {
self.dump_node(&mut output, root_id, 0);
} else {
output.push_str("(no root)\n");
}
output
}
fn dump_node(&self, output: &mut String, id: NodeId, depth: usize) {
let indent = " ".repeat(depth);
let Ok(node) = self.get_ref(id) else {
if let Some(physical_id) = self.resolve_node_index(id) {
output.push_str(&format!(
"{indent}[{id}] (missing physical node {physical_id})\n"
));
} else {
output.push_str(&format!("{indent}[{id}] (missing)\n"));
}
return;
};
let type_name = std::any::type_name_of_val(node);
output.push_str(&format!("{indent}[{id}] {type_name}\n"));
let mut children = SmallVec::<[NodeId; 8]>::new();
node.collect_children_into(&mut children);
for child_id in children {
self.dump_node(output, child_id, depth + 1);
}
}
pub fn take_layout_dirt_roots(&mut self, out: &mut Vec<NodeId>) -> bool {
self.state.layout_dirt_roots.take_into(out)
}
pub fn has_layout_dirt_roots(&self) -> bool {
self.state.layout_dirt_roots.any()
}
#[inline]
pub(crate) fn resolve_node_index(&self, id: NodeId) -> Option<usize> {
self.state.stable_index.slot(id)
}
fn contains_node_id(&self, id: NodeId) -> bool {
self.resolve_node_index(id).is_some() || self.high_id_node(id).is_ok()
}
fn insert_high_id_node(
&mut self,
stable_id: NodeId,
node: Box<dyn Node>,
origin: RecycleOrigin,
) {
let slot = self.state.high_ids.entry(stable_id).or_default();
slot.node = Some(node);
slot.origin = origin;
}
fn insert_available_with_id(&mut self, stable_id: NodeId, node: Box<dyn Node>) {
if stable_id >= Self::HIGH_ID_THRESHOLD {
self.insert_high_id_node(stable_id, node, RecycleOrigin::Cold);
return;
}
let physical_id = if let Some(Reverse(free_physical_id)) = self.state.free_ids.pop() {
self.state.nodes[free_physical_id] = Some(node);
self.state.physical_stable_ids[free_physical_id] = Self::pack_stable_id(stable_id);
self.state.physical_warm_recycled_origins[free_physical_id] = RecycleOrigin::Cold;
free_physical_id
} else {
self.ensure_dense_node_storage_capacity();
let physical_id = self.state.nodes.len();
self.state.nodes.push(Some(node));
self.state
.physical_stable_ids
.push(Self::pack_stable_id(stable_id));
self.state
.physical_warm_recycled_origins
.push(RecycleOrigin::Cold);
physical_id
};
self.state.next_stable_id = self.state.next_stable_id.max(stable_id.next());
self.state.physical_stable_ids[physical_id] = Self::pack_stable_id(stable_id);
self.state.stable_index.set_slot(stable_id, physical_id);
}
#[inline]
pub fn get_ref(&self, id: NodeId) -> Result<&dyn Node, NodeError> {
match self.resolve_node_index(id) {
Some(physical_id) => self
.state
.nodes
.get(physical_id)
.and_then(Option::as_deref)
.ok_or(NodeError::Missing { id }),
None => self.high_id_node(id),
}
}
#[inline]
pub(crate) fn parent_at(&self, place: MarkPlace) -> Option<NodeId> {
match place {
MarkPlace::Dense(slot) => self.state.nodes.get(slot)?.as_deref()?.parent(),
MarkPlace::Elsewhere(id) => self.high_id_node(id).ok()?.parent(),
}
}
pub(crate) fn high_id_node(&self, id: NodeId) -> Result<&dyn Node, NodeError> {
self.state
.high_ids
.get(&id)
.and_then(|slot| slot.node.as_deref())
.ok_or(NodeError::Missing { id })
}
fn high_id_node_mut(&mut self, id: NodeId) -> Result<&mut dyn Node, NodeError> {
self.state
.high_ids
.get_mut(&id)
.and_then(|slot| slot.node.as_deref_mut())
.ok_or(NodeError::Missing { id })
}
fn node_parent(&self, id: NodeId) -> Result<Option<NodeId>, NodeError> {
Ok(self.get_ref(id)?.parent())
}
fn collect_owned_children(
&self,
node_id: NodeId,
out: &mut SmallVec<[NodeId; 8]>,
) -> Result<(), NodeError> {
self.get_ref(node_id)?.collect_owned_children_into(out);
out.retain(|child_id| {
self.node_parent(*child_id)
.is_ok_and(|parent| parent == Some(node_id))
});
Ok(())
}
fn remove_node_storage(&mut self, node_id: NodeId) -> Result<(), NodeError> {
self.state.virtual_node_ids.remove(&node_id);
if let Some(slot) = self.state.high_ids.get_mut(&node_id)
&& let Some(node) = slot.node.take()
{
let origin = std::mem::take(&mut slot.origin);
slot.generation = slot.generation.wrapping_add(1);
if let Some(key) = node.recycle_key() {
let recycle_pool_limit = node.recycle_pool_limit();
self.push_recycled_node(
key,
recycle_pool_limit,
RecycledNode::new(node_id, node, origin),
);
}
return Ok(());
}
let physical_id = self
.resolve_node_index(node_id)
.ok_or(NodeError::Missing { id: node_id })?;
if let Some(node) = self.state.nodes[physical_id].take()
&& let Some(key) = node.recycle_key()
{
let recycle_pool_limit = node.recycle_pool_limit();
let origin = self
.state
.physical_warm_recycled_origins
.get_mut(physical_id)
.map(std::mem::take)
.unwrap_or_default();
self.push_recycled_node(
key,
recycle_pool_limit,
RecycledNode::new(node_id, node, origin),
);
}
self.state.physical_stable_ids[physical_id] = Self::INVALID_STABLE_ID;
self.state.stable_index.release(node_id);
self.state.free_ids.push(Reverse(physical_id));
Ok(())
}
fn remove_subtree_postorder(&mut self, id: NodeId) -> Result<usize, NodeError> {
self.get_ref(id)?;
let mut root_children = SmallVec::<[NodeId; 8]>::new();
self.collect_owned_children(id, &mut root_children)?;
let mut stack = Vec::new();
stack.push(RemovalFrame {
node_id: id,
children: root_children,
next_child: 0,
});
let mut max_depth = stack.len();
while let Some(frame) = stack.last_mut() {
if frame.next_child < frame.children.len() {
let child_id = frame.children[frame.next_child];
frame.next_child += 1;
if let Ok(child) = self.get_mut(child_id) {
child.on_removed_from_parent();
child.unmount();
}
let mut child_children = SmallVec::<[NodeId; 8]>::new();
self.collect_owned_children(child_id, &mut child_children)?;
stack.push(RemovalFrame {
node_id: child_id,
children: child_children,
next_child: 0,
});
max_depth = max_depth.max(stack.len());
continue;
}
let node_id = frame.node_id;
stack.pop();
self.remove_node_storage(node_id)?;
}
Ok(max_depth)
}
#[cfg(test)]
pub(crate) fn debug_remove_max_traversal_depth(
&mut self,
id: NodeId,
) -> Result<usize, NodeError> {
self.remove_subtree_postorder(id)
}
}