Skip to main content

cranpose_core/
memory_applier.rs

1use std::{any::TypeId, cmp::Reverse, collections::BinaryHeap};
2
3use smallvec::SmallVec;
4
5use crate::{
6    Applier, Node, NodeError, NodeId, NodeMarks, RecycleOrigin, RecyclePool, RecycledNode,
7    RuntimeHandle, bubble_passes,
8    collections::map::{HashMap, HashSet},
9    layout_dirt_roots,
10    node_marks::MarkPlace,
11    stable_index,
12};
13
14mod applier_impl;
15
16/// The stock [`Applier`]: it keeps nodes in memory under ids that stay stable for the life of the
17/// node.
18#[derive(Default)]
19pub struct MemoryApplier {
20    /// Behind one allocation, so lending the applier to a layout pass moves a
21    /// pointer.
22    pub(crate) state: Box<MemoryApplierState>,
23}
24
25#[derive(Default)]
26pub(crate) struct MemoryApplierState {
27    pub(crate) nodes: Vec<Option<Box<dyn Node>>>,
28    pub(crate) physical_stable_ids: Vec<u32>,
29    pub(crate) physical_warm_recycled_origins: Vec<RecycleOrigin>,
30    pub(crate) stable_index: stable_index::StableIndex,
31    pub(crate) free_ids: BinaryHeap<Reverse<usize>>,
32    pub(crate) high_ids: HashMap<NodeId, HighIdSlot>,
33    pub(crate) next_stable_id: NodeId,
34    pub(crate) layout_runtime: Option<RuntimeHandle>,
35    pub(crate) recycle_pools: HashMap<TypeId, RecyclePool>,
36    pub(crate) structural_change_parents: Vec<NodeId>,
37    pub(crate) bubble_passes: bubble_passes::BubblePasses,
38    pub(crate) layout_dirt_roots: layout_dirt_roots::LayoutDirtRoots,
39}
40
41// A node stored under an id too high for the dense table: the node while it
42// lives, whether it came from a warm shell, and the generation its id has
43// reached, which stays after the node goes.
44#[derive(Default)]
45pub(crate) struct HighIdSlot {
46    node: Option<Box<dyn Node>>,
47    origin: RecycleOrigin,
48    generation: u32,
49}
50
51/// Reusable ancestry storage for [`MemoryApplier::scene_nodes_attached_to_into`].
52///
53/// Attachment results are kept only for one batch, so callers can reuse this
54/// value without keeping stale attachment results after graph mutations.
55#[derive(Default)]
56pub struct SceneNodeAttachmentScratch {
57    attached: NodeMarks,
58    path: Vec<MarkPlace>,
59}
60
61const ATTACHED: u8 = 1;
62const DETACHED: u8 = 2;
63
64struct RemovalFrame {
65    node_id: NodeId,
66    children: SmallVec<[NodeId; 8]>,
67    next_child: usize,
68}
69
70/// Sizes of a [`MemoryApplier`]'s tables, for leak and memory checks. A `_len` field counts entries
71/// and a `_cap` field the capacity reserved.
72#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
73pub struct MemoryApplierDebugStats {
74    /// The id that the next created node gets.
75    pub next_stable_id: NodeId,
76    /// Live nodes.
77    pub nodes_len: usize,
78    /// Slots in the dense node table, both live and free.
79    pub nodes_cap: usize,
80    /// Entries in the table that gives each slot its stable id.
81    pub physical_stable_ids_len: usize,
82    /// Capacity reserved for the stable ids of table slots.
83    pub physical_stable_ids_cap: usize,
84    /// Live entries in the index from stable ids to table slots.
85    pub stable_to_physical_len: usize,
86    /// Capacity reserved for the index from stable ids to table slots.
87    pub stable_to_physical_cap: usize,
88    /// Stable ids that carry a generation counter.
89    pub stable_generations_len: usize,
90    /// Capacity reserved for the generation counters of stable ids.
91    pub stable_generations_cap: usize,
92    /// Dense table slots free for reuse.
93    pub free_ids_len: usize,
94    /// Capacity reserved for the free slot ids.
95    pub free_ids_cap: usize,
96    /// Nodes stored under ids too high for the dense table.
97    pub high_id_nodes_len: usize,
98    /// Capacity reserved for the high-id nodes.
99    pub high_id_nodes_cap: usize,
100    /// Generation counters kept for high-id nodes.
101    pub high_id_generations_len: usize,
102    /// Capacity reserved for the generations of high-id nodes.
103    pub high_id_generations_cap: usize,
104    /// Node types that have recycled nodes pooled.
105    pub recycled_type_count: usize,
106    /// Capacity reserved for the pools of recycled nodes.
107    pub recycled_type_cap: usize,
108    /// Pooled recycled nodes of all types.
109    pub recycled_node_count: usize,
110    /// Capacity reserved for the pooled recycled nodes.
111    pub recycled_node_capacity: usize,
112    /// Nodes recycled through the warm path, both live and pooled.
113    pub warm_recycled_node_id_count: usize,
114    /// Capacity reserved for the warm recycled nodes.
115    pub warm_recycled_node_id_capacity: usize,
116}
117
118impl MemoryApplier {
119    const EAGER_COMPACT_NODE_LEN: usize = 1_024;
120    pub(crate) const HIGH_ID_THRESHOLD: NodeId = NodeId(1_000_000_000);
121    const INVALID_STABLE_ID: u32 = u32::MAX;
122    const INITIAL_DENSE_NODE_CAP: usize = 32;
123    const LARGE_DENSE_NODE_GROWTH_THRESHOLD: usize = 32 * 1024;
124    const LARGE_DENSE_NODE_GROWTH_DIVISOR: usize = 4;
125
126    fn pack_stable_id(stable_id: NodeId) -> u32 {
127        u32::try_from(stable_id.0).expect("stable id overflow")
128    }
129
130    fn unpack_stable_id(stable_id: u32) -> NodeId {
131        NodeId(stable_id as usize)
132    }
133
134    fn next_dense_node_target_len(old_len: usize) -> usize {
135        if old_len < Self::INITIAL_DENSE_NODE_CAP {
136            return Self::INITIAL_DENSE_NODE_CAP;
137        }
138        if old_len < Self::LARGE_DENSE_NODE_GROWTH_THRESHOLD {
139            return old_len.saturating_mul(2);
140        }
141
142        let incremental_growth =
143            (old_len / Self::LARGE_DENSE_NODE_GROWTH_DIVISOR).max(Self::INITIAL_DENSE_NODE_CAP);
144        old_len.saturating_add(incremental_growth)
145    }
146
147    fn ensure_dense_node_storage_capacity(&mut self) {
148        let len = self
149            .state
150            .nodes
151            .len()
152            .max(self.state.physical_stable_ids.len())
153            .max(self.state.physical_warm_recycled_origins.len());
154        if len < self.state.nodes.capacity()
155            && len < self.state.physical_stable_ids.capacity()
156            && len < self.state.physical_warm_recycled_origins.capacity()
157        {
158            return;
159        }
160
161        let target = Self::next_dense_node_target_len(len);
162        if self.state.nodes.capacity() < target {
163            self.state
164                .nodes
165                .reserve_exact(target.saturating_sub(self.state.nodes.len()));
166        }
167        if self.state.physical_stable_ids.capacity() < target {
168            self.state
169                .physical_stable_ids
170                .reserve_exact(target.saturating_sub(self.state.physical_stable_ids.len()));
171        }
172        if self.state.physical_warm_recycled_origins.capacity() < target {
173            self.state.physical_warm_recycled_origins.reserve_exact(
174                target.saturating_sub(self.state.physical_warm_recycled_origins.len()),
175            );
176        }
177    }
178
179    /// Creates an applier with no nodes.
180    pub fn new() -> Self {
181        Self::default()
182    }
183
184    /// Resolves a scene-scope candidate the way structural records are
185    /// resolved: to its nearest non-virtual ancestor, and only while still
186    /// attached to `root`. A node detached after recording must not reach the
187    /// scoped scene update — an id the graph cannot resolve forces it to give
188    /// up and rebuild the whole scene.
189    pub fn scene_node_attached_to(&mut self, node_id: NodeId, root: NodeId) -> Option<NodeId> {
190        let (resolved, _) = self.first_non_virtual_ancestor(node_id)?;
191        self.is_attached_to(resolved, root).then_some(resolved)
192    }
193
194    /// [`Self::scene_node_attached_to`] for each of `nodes`, in order, with
195    /// every ancestor looked up at most once: a batch costs the distinct
196    /// ancestors of its nodes, not each node's depth.
197    pub fn scene_nodes_attached_to(
198        &mut self,
199        nodes: impl IntoIterator<Item = NodeId>,
200        root: NodeId,
201    ) -> Vec<Option<NodeId>> {
202        let mut result = Vec::new();
203        self.scene_nodes_attached_to_into(
204            nodes,
205            root,
206            &mut result,
207            &mut SceneNodeAttachmentScratch::default(),
208        );
209        result
210    }
211
212    /// Fills `output` with [`Self::scene_nodes_attached_to`] results, reusing
213    /// its storage and the ancestry cache in `scratch` across batches.
214    ///
215    /// Each distinct ancestor is inspected at most once per call. The cache
216    /// is cleared for each call, so graph mutations are observed by the next
217    /// batch.
218    ///
219    /// ```
220    /// use cranpose_core::{MemoryApplier, NodeId, SceneNodeAttachmentScratch};
221    ///
222    /// let mut applier = MemoryApplier::new();
223    /// let mut scratch = SceneNodeAttachmentScratch::default();
224    /// let mut attached: Vec<Option<NodeId>> = Vec::new();
225    /// let root = NodeId::default();
226    /// applier.scene_nodes_attached_to_into([], root, &mut attached, &mut scratch);
227    /// assert!(attached.is_empty());
228    /// ```
229    pub fn scene_nodes_attached_to_into(
230        &mut self,
231        nodes: impl IntoIterator<Item = NodeId>,
232        root: NodeId,
233        output: &mut Vec<Option<NodeId>>,
234        scratch: &mut SceneNodeAttachmentScratch,
235    ) {
236        let nodes = nodes.into_iter();
237        output.clear();
238        let (lower_bound, upper_bound) = nodes.size_hint();
239        if lower_bound == 0 && upper_bound == Some(0) {
240            scratch.path.clear();
241            return;
242        }
243        scratch.attached.reset(self);
244        scratch.attached.set(self, root, ATTACHED);
245        output.reserve(lower_bound);
246        scratch.path.clear();
247        for node_id in nodes {
248            let Some((resolved, mut current)) = self.first_non_virtual_ancestor(node_id) else {
249                output.push(None);
250                continue;
251            };
252            scratch.path.clear();
253            let answer = loop {
254                match scratch.attached.mark_at(current) {
255                    ATTACHED => break true,
256                    DETACHED => break false,
257                    _ => {}
258                }
259                scratch.path.push(current);
260                match self.parent_at(current) {
261                    Some(parent) if scratch.path.len() < 100_000 => {
262                        current = MarkPlace::of(self, parent);
263                    }
264                    _ => break false,
265                }
266            };
267            let mark = if answer { ATTACHED } else { DETACHED };
268            for visited in scratch.path.drain(..) {
269                *scratch.attached.entry_at(visited) = mark;
270            }
271            output.push(answer.then_some(resolved));
272        }
273    }
274
275    /// Drains the parents recorded via [`Applier::record_structural_change`],
276    /// keeping only nodes still attached to `root` (a parent that was itself
277    /// removed is covered by its own surviving ancestor's record). A virtual
278    /// parent — a subcompose slot wrapper the render graph never contains —
279    /// is reported as its nearest non-virtual ancestor: that is the node
280    /// whose graph child set the change altered, and an id the graph cannot
281    /// resolve would force the scoped scene update to give up and rebuild.
282    pub fn take_structural_change_parents_attached_to(&mut self, root: NodeId) -> Vec<NodeId> {
283        let mut candidates = Vec::new();
284        self.take_structural_change_parents_into(&mut candidates);
285        let mut seen = HashSet::<NodeId>::default();
286        candidates.retain_mut(|parent_id| {
287            let Some((resolved, _)) = self.first_non_virtual_ancestor(*parent_id) else {
288                return false;
289            };
290            *parent_id = resolved;
291            self.is_attached_to(resolved, root) && seen.insert(resolved)
292        });
293        candidates
294    }
295
296    /// Takes the recorded structural-change candidates into reusable storage.
297    /// Candidates are not resolved or attachment-filtered; callers that route
298    /// them into scenes should use [`Self::scene_nodes_attached_to_into`].
299    ///
300    /// ```
301    /// use cranpose_core::MemoryApplier;
302    ///
303    /// let mut applier = MemoryApplier::new();
304    /// let mut nodes = Vec::new();
305    /// applier.take_structural_change_parents_into(&mut nodes);
306    /// assert!(nodes.is_empty());
307    /// ```
308    pub fn take_structural_change_parents_into(&mut self, output: &mut Vec<NodeId>) {
309        output.clear();
310        std::mem::swap(&mut self.state.structural_change_parents, output);
311    }
312
313    // The first of `node_id` and its ancestors that is not virtual, and
314    // where it is stored; an id with no node is its own answer.
315    fn first_non_virtual_ancestor(&self, node_id: NodeId) -> Option<(NodeId, MarkPlace)> {
316        let mut current = node_id;
317        for _ in 0..100_000 {
318            let (place, is_virtual) = match self.state.stable_index.slot_and_virtual(current) {
319                Some((slot, is_virtual)) => (MarkPlace::Dense(slot), is_virtual),
320                None => (
321                    MarkPlace::Elsewhere(current),
322                    self.high_id_node(current).is_ok_and(Node::is_virtual),
323                ),
324            };
325            if !is_virtual {
326                return Some((current, place));
327            }
328            current = self.parent_at(place)?;
329        }
330        None
331    }
332
333    fn is_attached_to(&mut self, node_id: NodeId, root: NodeId) -> bool {
334        let mut current = node_id;
335        for _ in 0..100_000 {
336            if current == root {
337                return true;
338            }
339            match self.get_mut(current) {
340                Ok(node) => match node.parent() {
341                    Some(parent) => current = parent,
342                    None => return false,
343                },
344                Err(_) => return false,
345            }
346        }
347        false
348    }
349
350    /// Runs `f` on the node `id` as an `N`. Returns an error if the node does not exist or is not
351    /// an `N`.
352    pub fn with_node<N: Node + 'static, R>(
353        &mut self,
354        id: NodeId,
355        f: impl FnOnce(&mut N) -> R,
356    ) -> Result<R, NodeError> {
357        let typed = self
358            .get_mut(id)?
359            .as_any_mut()
360            .downcast_mut::<N>()
361            .ok_or_else(|| NodeError::TypeMismatch {
362                id,
363                expected: std::any::type_name::<N>(),
364            })?;
365        Ok(f(typed))
366    }
367
368    /// Returns the number of live nodes.
369    pub fn len(&self) -> usize {
370        self.state.nodes.iter().filter(|n| n.is_some()).count()
371    }
372
373    /// Returns the number of slots in the dense node table, both live and free.
374    pub fn capacity(&self) -> usize {
375        self.state.nodes.len()
376    }
377
378    /// Returns the number of free slots in the dense node table.
379    pub fn tombstone_count(&self) -> usize {
380        self.state.nodes.iter().filter(|n| n.is_none()).count()
381    }
382
383    /// Returns the number of freed table slots that new nodes can reuse.
384    pub fn freelist_len(&self) -> usize {
385        self.state.free_ids.len()
386    }
387
388    /// Returns the number of pooled recycled nodes of all types.
389    pub fn debug_recycled_node_count(&self) -> usize {
390        self.total_recycled_node_count()
391    }
392
393    /// Returns the number of pooled recycled nodes of type `N`.
394    pub fn debug_recycled_node_count_for<N: Node + 'static>(&self) -> usize {
395        let key = TypeId::of::<N>();
396        self.state
397            .recycle_pools
398            .get(&key)
399            .map_or(0, RecyclePool::len)
400    }
401
402    /// Returns the sizes of the applier's tables, for leak and memory checks.
403    pub fn debug_stats(&self) -> MemoryApplierDebugStats {
404        MemoryApplierDebugStats {
405            next_stable_id: self.state.next_stable_id,
406            nodes_len: self.len(),
407            nodes_cap: self.state.nodes.len(),
408            physical_stable_ids_len: self.state.physical_stable_ids.len(),
409            physical_stable_ids_cap: self.state.physical_stable_ids.capacity(),
410            stable_to_physical_len: self.state.stable_index.live(),
411            stable_to_physical_cap: self.state.stable_index.capacity(),
412            stable_generations_len: self.state.stable_index.occupied(),
413            stable_generations_cap: self.state.stable_index.capacity(),
414            free_ids_len: self.state.free_ids.len(),
415            free_ids_cap: self.state.free_ids.capacity(),
416            high_id_nodes_len: self.high_id_nodes().count(),
417            high_id_nodes_cap: self.state.high_ids.capacity(),
418            high_id_generations_len: self.state.high_ids.len(),
419            high_id_generations_cap: self.state.high_ids.capacity(),
420            recycled_type_count: self
421                .state
422                .recycle_pools
423                .values()
424                .filter(|pool| pool.len() > 0)
425                .count(),
426            recycled_type_cap: self.state.recycle_pools.capacity(),
427            recycled_node_count: self.total_recycled_node_count(),
428            recycled_node_capacity: self.total_recycled_node_capacity(),
429            warm_recycled_node_id_count: self.total_warm_recycled_node_id_count(),
430            warm_recycled_node_id_capacity: self.total_warm_recycled_node_id_capacity(),
431        }
432    }
433
434    /// Returns `true` if the applier holds no live nodes.
435    pub fn is_empty(&self) -> bool {
436        self.len() == 0
437    }
438
439    /// Calls `visit` with every live node, dense and high-id alike.
440    pub fn for_each_node_mut(&mut self, mut visit: impl FnMut(&mut dyn Node)) {
441        for node in self.state.nodes.iter_mut().flatten() {
442            visit(node.as_mut());
443        }
444        for node in self
445            .state
446            .high_ids
447            .values_mut()
448            .filter_map(|slot| slot.node.as_mut())
449        {
450            visit(node.as_mut());
451        }
452    }
453
454    /// Returns the heap bytes that the live nodes take, including what they own.
455    pub fn debug_live_node_heap_bytes(&self) -> usize {
456        let dense_nodes = self
457            .state
458            .nodes
459            .iter()
460            .flatten()
461            .map(|node| size_of_val(&**node) + node.debug_heap_bytes())
462            .sum::<usize>();
463        let high_id_nodes = self
464            .high_id_nodes()
465            .map(|node| size_of_val(node) + node.debug_heap_bytes())
466            .sum::<usize>();
467        dense_nodes + high_id_nodes
468    }
469
470    /// Returns the heap bytes that the pooled recycled nodes take, including what they own.
471    pub fn debug_recycled_node_heap_bytes(&self) -> usize {
472        self.state
473            .recycle_pools
474            .values()
475            .flat_map(RecyclePool::nodes)
476            .map(|node| size_of_val(&*node.node) + node.node.debug_heap_bytes())
477            .sum::<usize>()
478    }
479
480    /// Gives the applier the runtime of its composition, so code that runs during layout with only
481    /// the applier in hand can reach the runtime.
482    pub fn set_runtime_handle(&mut self, handle: RuntimeHandle) {
483        self.state.layout_runtime = Some(handle);
484    }
485
486    /// Removes the runtime handle.
487    pub fn clear_runtime_handle(&mut self) {
488        self.state.layout_runtime = None;
489    }
490
491    /// Returns the runtime handle set with `set_runtime_handle`, if any.
492    pub fn runtime_handle(&self) -> Option<RuntimeHandle> {
493        self.state.layout_runtime.clone()
494    }
495
496    fn total_recycled_node_count(&self) -> usize {
497        self.state
498            .recycle_pools
499            .values()
500            .map(RecyclePool::len)
501            .sum()
502    }
503
504    fn total_recycled_node_capacity(&self) -> usize {
505        self.state
506            .recycle_pools
507            .values()
508            .map(RecyclePool::capacity)
509            .sum()
510    }
511
512    fn total_warm_recycled_node_id_count(&self) -> usize {
513        self.live_warm_recycled_origin_count()
514            + self
515                .state
516                .recycle_pools
517                .values()
518                .map(|pool| pool.warm.len() + pool.returning.len())
519                .sum::<usize>()
520    }
521
522    fn total_warm_recycled_node_id_capacity(&self) -> usize {
523        self.live_warm_recycled_origin_capacity()
524            + self
525                .state
526                .recycle_pools
527                .values()
528                .map(|pool| pool.warm.capacity() + pool.returning.capacity())
529                .sum::<usize>()
530    }
531
532    fn remember_recycle_pool_limit(&mut self, key: TypeId, recycle_pool_limit: Option<usize>) {
533        if recycle_pool_limit.is_some() || self.state.recycle_pools.contains_key(&key) {
534            self.state.recycle_pools.entry(key).or_default().limit = recycle_pool_limit;
535        }
536    }
537
538    fn warm_recycled_pool_len(&self, key: TypeId) -> usize {
539        self.state
540            .recycle_pools
541            .get(&key)
542            .map_or(0, |pool| pool.warm.len())
543    }
544
545    fn live_warm_recycled_origin_count(&self) -> usize {
546        self.state
547            .physical_warm_recycled_origins
548            .iter()
549            .zip(self.state.nodes.iter())
550            .filter(|(origin, node)| **origin == RecycleOrigin::Warm && node.is_some())
551            .count()
552            + self
553                .state
554                .high_ids
555                .values()
556                .filter(|slot| slot.origin == RecycleOrigin::Warm && slot.node.is_some())
557                .count()
558    }
559
560    fn high_id_nodes(&self) -> impl Iterator<Item = &dyn Node> {
561        self.state
562            .high_ids
563            .values()
564            .filter_map(|slot| slot.node.as_deref())
565    }
566
567    fn live_warm_recycled_origin_capacity(&self) -> usize {
568        self.state.physical_warm_recycled_origins.capacity() + self.state.high_ids.capacity()
569    }
570
571    fn push_recycled_node(
572        &mut self,
573        key: TypeId,
574        recycle_pool_limit: Option<usize>,
575        recycled: RecycledNode,
576    ) {
577        self.remember_recycle_pool_limit(key, recycle_pool_limit);
578        let pool = self.state.recycle_pools.entry(key).or_default();
579        pool.remember_prototype(recycled.node.as_ref());
580        pool.push(recycled);
581    }
582
583    fn seed_recycled_node_shell_impl(
584        &mut self,
585        key: TypeId,
586        recycle_pool_limit: Option<usize>,
587        shell: Box<dyn Node>,
588    ) {
589        let limit = recycle_pool_limit.unwrap_or(usize::MAX);
590        if self.warm_recycled_pool_len(key) >= limit {
591            return;
592        }
593
594        self.remember_recycle_pool_limit(key, recycle_pool_limit);
595        let stable_id = self.state.next_stable_id;
596        self.state.next_stable_id = self.state.next_stable_id.next();
597        let pool = self.state.recycle_pools.entry(key).or_default();
598        pool.remember_prototype(shell.as_ref());
599        pool.push_warm(RecycledNode::from_shell(
600            stable_id,
601            shell,
602            RecycleOrigin::Warm,
603        ));
604    }
605
606    fn prune_stable_generations(&mut self) {
607        let retained_len = self.state.stable_index.live() + self.total_recycled_node_count();
608        if retained_len == self.state.stable_index.occupied() {
609            return;
610        }
611
612        let recycled: HashSet<NodeId> = self
613            .state
614            .recycle_pools
615            .values()
616            .flat_map(RecyclePool::nodes)
617            .map(RecycledNode::stable_id)
618            .collect();
619        self.state
620            .stable_index
621            .retain_retired(|stable_id| recycled.contains(&stable_id));
622    }
623
624    /// Returns an indented text outline of the tree below `root` for debugging, or a `(no root)`
625    /// line when `root` is `None`.
626    pub fn dump_tree(&self, root: Option<NodeId>) -> String {
627        let mut output = String::new();
628        if let Some(root_id) = root {
629            self.dump_node(&mut output, root_id, 0);
630        } else {
631            output.push_str("(no root)\n");
632        }
633        output
634    }
635
636    fn dump_node(&self, output: &mut String, id: NodeId, depth: usize) {
637        let indent = "  ".repeat(depth);
638        let Ok(node) = self.get_ref(id) else {
639            if let Some(physical_id) = self.resolve_node_index(id) {
640                output.push_str(&format!(
641                    "{indent}[{id}] (missing physical node {physical_id})\n"
642                ));
643            } else {
644                output.push_str(&format!("{indent}[{id}] (missing)\n"));
645            }
646            return;
647        };
648        let type_name = std::any::type_name_of_val(node);
649        output.push_str(&format!("{indent}[{id}] {type_name}\n"));
650
651        let mut children = SmallVec::<[NodeId; 8]>::new();
652        node.collect_children_into(&mut children);
653        for child_id in children {
654            self.dump_node(output, child_id, depth + 1);
655        }
656    }
657
658    /// Moves into `out` the nodes the layout dirty bubbles started from
659    /// since the last call, for a layout pass that starts from them, and
660    /// forgets them. `false` when there were more than the applier keeps:
661    /// `out` is then empty and the pass walks down from the root.
662    pub fn take_layout_dirt_roots(&mut self, out: &mut Vec<NodeId>) -> bool {
663        self.state.layout_dirt_roots.take_into(out)
664    }
665
666    /// Whether a layout dirty bubble started since the last
667    /// [`Self::take_layout_dirt_roots`].
668    pub fn has_layout_dirt_roots(&self) -> bool {
669        self.state.layout_dirt_roots.any()
670    }
671
672    #[inline]
673    pub(crate) fn resolve_node_index(&self, id: NodeId) -> Option<usize> {
674        self.state.stable_index.slot(id)
675    }
676
677    fn contains_node_id(&self, id: NodeId) -> bool {
678        self.resolve_node_index(id).is_some() || self.high_id_node(id).is_ok()
679    }
680
681    fn insert_high_id_node(
682        &mut self,
683        stable_id: NodeId,
684        node: Box<dyn Node>,
685        origin: RecycleOrigin,
686    ) {
687        let slot = self.state.high_ids.entry(stable_id).or_default();
688        slot.node = Some(node);
689        slot.origin = origin;
690    }
691
692    fn insert_available_with_id(&mut self, stable_id: NodeId, node: Box<dyn Node>) {
693        if stable_id >= Self::HIGH_ID_THRESHOLD {
694            self.insert_high_id_node(stable_id, node, RecycleOrigin::Cold);
695            return;
696        }
697
698        let is_virtual = node.is_virtual();
699        let physical_id = if let Some(Reverse(free_physical_id)) = self.state.free_ids.pop() {
700            self.state.nodes[free_physical_id] = Some(node);
701            self.state.physical_stable_ids[free_physical_id] = Self::pack_stable_id(stable_id);
702            self.state.physical_warm_recycled_origins[free_physical_id] = RecycleOrigin::Cold;
703            free_physical_id
704        } else {
705            self.ensure_dense_node_storage_capacity();
706            let physical_id = self.state.nodes.len();
707            self.state.nodes.push(Some(node));
708            self.state
709                .physical_stable_ids
710                .push(Self::pack_stable_id(stable_id));
711            self.state
712                .physical_warm_recycled_origins
713                .push(RecycleOrigin::Cold);
714            physical_id
715        };
716
717        self.state.next_stable_id = self.state.next_stable_id.max(stable_id.next());
718        self.state.physical_stable_ids[physical_id] = Self::pack_stable_id(stable_id);
719        self.state.stable_index.set_slot(stable_id, physical_id);
720        if is_virtual {
721            self.state.stable_index.mark_virtual(stable_id);
722        }
723    }
724
725    /// Borrows a live node without requiring exclusive access to the tree.
726    /// Returns [`NodeError::Missing`] when `id` does not name a live node.
727    #[inline]
728    pub fn get_ref(&self, id: NodeId) -> Result<&dyn Node, NodeError> {
729        match self.resolve_node_index(id) {
730            Some(physical_id) => self
731                .state
732                .nodes
733                .get(physical_id)
734                .and_then(Option::as_deref)
735                .ok_or(NodeError::Missing { id }),
736            None => self.high_id_node(id),
737        }
738    }
739
740    // The parent of the node at `place`, `None` for a root or a node that
741    // is gone.
742    #[inline]
743    pub(crate) fn parent_at(&self, place: MarkPlace) -> Option<NodeId> {
744        match place {
745            MarkPlace::Dense(slot) => self.state.nodes.get(slot)?.as_deref()?.parent(),
746            MarkPlace::Elsewhere(id) => self.high_id_node(id).ok()?.parent(),
747        }
748    }
749
750    pub(crate) fn high_id_node(&self, id: NodeId) -> Result<&dyn Node, NodeError> {
751        self.state
752            .high_ids
753            .get(&id)
754            .and_then(|slot| slot.node.as_deref())
755            .ok_or(NodeError::Missing { id })
756    }
757
758    fn high_id_node_mut(&mut self, id: NodeId) -> Result<&mut dyn Node, NodeError> {
759        self.state
760            .high_ids
761            .get_mut(&id)
762            .and_then(|slot| slot.node.as_deref_mut())
763            .ok_or(NodeError::Missing { id })
764    }
765
766    fn node_parent(&self, id: NodeId) -> Result<Option<NodeId>, NodeError> {
767        Ok(self.get_ref(id)?.parent())
768    }
769
770    fn collect_owned_children(
771        &self,
772        node_id: NodeId,
773        out: &mut SmallVec<[NodeId; 8]>,
774    ) -> Result<(), NodeError> {
775        self.get_ref(node_id)?.collect_owned_children_into(out);
776        out.retain(|child_id| {
777            self.node_parent(*child_id)
778                .is_ok_and(|parent| parent == Some(node_id))
779        });
780        Ok(())
781    }
782
783    fn remove_node_storage(&mut self, node_id: NodeId) -> Result<(), NodeError> {
784        if let Some(slot) = self.state.high_ids.get_mut(&node_id)
785            && let Some(node) = slot.node.take()
786        {
787            let origin = std::mem::take(&mut slot.origin);
788            slot.generation = slot.generation.wrapping_add(1);
789            if let Some(key) = node.recycle_key() {
790                let recycle_pool_limit = node.recycle_pool_limit();
791                self.push_recycled_node(
792                    key,
793                    recycle_pool_limit,
794                    RecycledNode::new(node_id, node, origin),
795                );
796            }
797            return Ok(());
798        }
799
800        let physical_id = self
801            .resolve_node_index(node_id)
802            .ok_or(NodeError::Missing { id: node_id })?;
803        if let Some(node) = self.state.nodes[physical_id].take()
804            && let Some(key) = node.recycle_key()
805        {
806            let recycle_pool_limit = node.recycle_pool_limit();
807            let origin = self
808                .state
809                .physical_warm_recycled_origins
810                .get_mut(physical_id)
811                .map(std::mem::take)
812                .unwrap_or_default();
813            self.push_recycled_node(
814                key,
815                recycle_pool_limit,
816                RecycledNode::new(node_id, node, origin),
817            );
818        }
819        self.state.physical_stable_ids[physical_id] = Self::INVALID_STABLE_ID;
820        self.state.stable_index.release(node_id);
821        self.state.free_ids.push(Reverse(physical_id));
822        Ok(())
823    }
824
825    fn remove_subtree_postorder(&mut self, id: NodeId) -> Result<usize, NodeError> {
826        self.get_ref(id)?;
827
828        let mut root_children = SmallVec::<[NodeId; 8]>::new();
829        self.collect_owned_children(id, &mut root_children)?;
830
831        let mut stack = Vec::new();
832        stack.push(RemovalFrame {
833            node_id: id,
834            children: root_children,
835            next_child: 0,
836        });
837        let mut max_depth = stack.len();
838
839        while let Some(frame) = stack.last_mut() {
840            if frame.next_child < frame.children.len() {
841                let child_id = frame.children[frame.next_child];
842                frame.next_child += 1;
843
844                if let Ok(child) = self.get_mut(child_id) {
845                    child.on_removed_from_parent();
846                    child.unmount();
847                }
848
849                let mut child_children = SmallVec::<[NodeId; 8]>::new();
850                self.collect_owned_children(child_id, &mut child_children)?;
851                stack.push(RemovalFrame {
852                    node_id: child_id,
853                    children: child_children,
854                    next_child: 0,
855                });
856                max_depth = max_depth.max(stack.len());
857                continue;
858            }
859
860            let node_id = frame.node_id;
861            stack.pop();
862            self.remove_node_storage(node_id)?;
863        }
864
865        Ok(max_depth)
866    }
867
868    #[cfg(test)]
869    pub(crate) fn debug_remove_max_traversal_depth(
870        &mut self,
871        id: NodeId,
872    ) -> Result<usize, NodeError> {
873        self.remove_subtree_postorder(id)
874    }
875}