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