1#![deny(missing_docs)]
2#![doc = include_str!("../README.md")]
3#![deny(unsafe_code)]
4
5pub extern crate self as cranpose_core;
6
7mod applier_host;
8mod blocking;
9mod bubble_passes;
10mod callbacks;
11mod composer;
12pub mod composer_context;
15mod composition;
16mod composition_locals;
17pub mod concurrency;
18mod debug_trace;
19mod effect_key;
20mod emit;
21pub mod env_flags;
22#[cfg(any(feature = "internal", test))]
23mod frame_clock;
24mod hooks;
25mod launched_effect;
26mod node_marks;
27pub mod owned;
29pub mod platform;
30mod recompose;
31mod retention;
32pub mod runtime;
35mod slot;
36pub mod snapshot_double_index_heap;
38pub mod snapshot_id_set;
40pub mod snapshot_pinning;
42pub mod snapshot_state_observer;
44pub mod snapshot_v2;
45mod snapshot_weak_set;
46pub mod source_trace;
47mod stable_index;
48mod state;
49#[doc(hidden)]
50pub use source_trace::__source_scope;
51#[doc(hidden)]
52pub use thread_state::with_or_fresh;
53pub mod subcompose;
54mod thread_state;
55
56#[cfg(feature = "internal")]
57#[doc(hidden)]
58pub mod internal {
59 pub use crate::frame_clock::{FrameCallbackRegistration, FrameClock};
60}
61pub use applier_host::{ApplierGuard, ApplierHost, ConcreteApplierHost, NodeDisposal};
62pub use blocking::{
63 BlockingError, BlockingExecutor, BlockingExecutorConfig, BlockingTask, launchBlocking,
64 withBlocking,
65};
66pub use callbacks::{
67 CallbackHolder, CallbackHolder1, ParamReturnState, ParamSlot, ParamState, ReturnSlot,
68 SharedParam, refresh_param, refresh_shared_param,
69};
70pub use composer::{
71 BranchGroupGuard, CapturedCompositionContext, ComposableGroup, Composer,
72 ScopedBranchGroupGuard, ValueSlotHandle,
73};
74pub(crate) use composer::{ComposerCore, EmittedNode, ParentAttachMode, ParentFrame};
75pub use composition::{Composition, ROOT_RENDER_REPLAY_LIMIT};
76pub use composition_locals::{
77 CompositionLocal, CompositionLocalProvider, ProvidedValue, StaticCompositionLocal,
78 compositionLocalOf, compositionLocalOfWithPolicy, staticCompositionLocalOf,
79 staticCompositionLocalOfWithPolicy,
80};
81pub(crate) use composition_locals::{LocalStateEntry, StaticLocalEntry};
82pub use concurrency::{
83 CollectEvents, CoroutineScope, Delay, EventChannel, EventSender, EventStream, EventStreamNext,
84 ProduceScope, collectAsState, delay, interval, produceState, rememberCoroutineScope,
85 rememberEventStream, spawn_ui_task,
86};
87#[doc(hidden)]
88pub use debug_trace::{
89 debug_label_current_scope, debug_live_recompose_scope_count,
90 debug_recompose_scope_registry_stats, debug_scope_invalidation_sources, debug_scope_label,
91};
92pub use hooks::{
93 derivedStateOf, mutableStateList, mutableStateListOf, mutableStateMap, mutableStateMapOf,
94 mutableStateOf, mutableStateOfNeverEqual, ownedMutableStateOf, ownedMutableStateOfNeverEqual,
95 remember, rememberKeyed, rememberMutableStateOf, rememberMutableStateOfNeverEqual,
96 rememberUpdatedState, try_mutableStateOf,
97};
98#[cfg(feature = "internal")]
99#[doc(hidden)]
100pub use hooks::{withFrameMillis, withFrameNanos};
101pub use launched_effect::{
102 __launched_effect_async_impl, __launched_effect_impl, CancelToken, LaunchedEffect,
103 LaunchedEffectAsync, LaunchedEffectScope, TaskSite,
104};
105pub use node_marks::NodeMarks;
106pub use owned::Owned;
107pub use platform::{Clock, RuntimeScheduler, SchedulerRef, scheduler_ref};
108pub use retention::{RetentionBudget, RetentionEvictionPolicy, RetentionMode, RetentionPolicy};
109#[doc(hidden)]
110pub use runtime::{
111 DefaultScheduler, Runtime, RuntimeHandle, StateId, TaskHandle, UiDispatcher,
112 current_runtime_handle, label_next_ui_task, schedule_frame, schedule_node_update,
113};
114pub use slot::{
115 SlotDebugAnchor, SlotDebugEntry, SlotDebugEntryKind, SlotDebugGroup, SlotDebugScope,
116 SlotDebugSnapshot, SlotRetentionDebugStats, SlotTable, SlotTableDebugStats,
117 SlotTableLocalDebugStats, SlotTableMutationDebugStats,
118};
119#[doc(hidden)]
120pub use snapshot_state_observer::SnapshotStateObserver;
121
122pub fn run_in_mutable_snapshot<T>(block: impl FnOnce() -> T) -> Result<T, &'static str> {
147 let snapshot = snapshot_v2::take_mutable_snapshot(None, None);
148
149 let _applied_guard = AppliedSnapshotFlagGuard::enter();
150 let value = snapshot.enter(block);
151
152 match snapshot.apply() {
153 snapshot_v2::SnapshotApplyResult::Success => Ok(value),
154 snapshot_v2::SnapshotApplyResult::Failure => Err("Snapshot apply failed"),
155 }
156}
157
158struct AppliedSnapshotFlagGuard {
159 previous: bool,
160}
161
162impl AppliedSnapshotFlagGuard {
163 fn enter() -> Self {
164 let previous = thread_state::cells(|cells| cells.in_applied_snapshot.replace(true));
165 Self { previous }
166 }
167}
168
169impl Drop for AppliedSnapshotFlagGuard {
170 fn drop(&mut self) {
171 thread_state::cells(|cells| cells.in_applied_snapshot.set(self.previous));
172 }
173}
174
175pub fn dispatch_ui_event<T>(block: impl FnOnce() -> T) -> Option<T> {
190 run_in_mutable_snapshot(block).ok()
191}
192
193#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
196pub struct CompositionPassDebugStats {
197 pub commands_len: usize,
199 pub commands_cap: usize,
201 pub command_payload_len_bytes: usize,
203 pub command_payload_cap_bytes: usize,
205 pub sync_children_len: usize,
207 pub sync_children_cap: usize,
209 pub sync_child_ids_len: usize,
211 pub sync_child_ids_cap: usize,
213 pub side_effects_len: usize,
215 pub side_effects_cap: usize,
217}
218
219#[must_use]
221pub struct EventHandlerScopeGuard {
222 previous: bool,
223}
224
225impl Drop for EventHandlerScopeGuard {
226 fn drop(&mut self) {
227 thread_state::cells(|cells| cells.in_event_handler.set(self.previous));
228 }
229}
230
231pub fn enter_event_handler_scope() -> EventHandlerScopeGuard {
234 let previous = thread_state::cells(|cells| cells.in_event_handler.replace(true));
235 EventHandlerScopeGuard { previous }
236}
237
238pub fn in_event_handler() -> bool {
240 thread_state::cells(|cells| cells.in_event_handler.get())
241}
242
243pub fn in_applied_snapshot() -> bool {
245 thread_state::cells(|cells| cells.in_applied_snapshot.get())
246}
247
248use std::{
249 any::{Any, TypeId},
250 cell::{Cell, Ref, RefCell, RefMut},
251 cmp::Reverse,
252 collections::BinaryHeap,
253 hash::{Hash, Hasher},
254 rc::{Rc, Weak},
255 sync::OnceLock,
256};
257
258#[cfg(test)]
259pub(crate) use runtime::{TestRuntime, TestScheduler};
260use smallvec::SmallVec;
261
262use crate::collections::map::{HashMap, HashSet};
263
264pub type Key = u64;
267pub type NodeId = usize;
269
270#[cfg(any(test, debug_assertions))]
271#[derive(Clone, Debug, PartialEq, Eq)]
272pub(crate) struct LocationKeyDebugInfo {
273 file: String,
274 line: u32,
275 column: u32,
276}
277
278#[cfg(any(test, debug_assertions))]
279fn register_location_key_debug_info(key: Key, file: &str, line: u32, column: u32) {
280 let info = || LocationKeyDebugInfo {
281 file: file.to_owned(),
282 line,
283 column,
284 };
285 let collision = thread_state::runtimes(|state| {
286 let mut registry = state.location_keys.borrow_mut();
287 match registry.entry(key) {
288 std::collections::hash_map::Entry::Vacant(entry) => {
289 thread_state::watch_thread_exit();
290 entry.insert(info());
291 None
292 }
293 std::collections::hash_map::Entry::Occupied(entry) => {
294 let existing = entry.get();
295 (existing.file != file || existing.line != line || existing.column != column)
296 .then(|| (existing.clone(), info()))
297 }
298 }
299 });
300 if let Some((existing, incoming)) = collision {
301 thread_state::cells(|cells| {
302 let count = &cells.location_key_collisions;
303 count.set(count.get().saturating_add(1));
304 });
305 log::error!("location key collision: key={key} first={existing:?} second={incoming:?}");
306 }
307}
308
309#[cfg(all(debug_assertions, not(test)))]
310fn location_key_diagnostics_enabled() -> bool {
311 crate::env_flag!("CRANPOSE_LOCATION_KEY_DIAGNOSTICS")
312}
313
314#[cfg(test)]
315pub(crate) fn register_location_key_debug_info_for_test(
316 key: Key,
317 file: &str,
318 line: u32,
319 column: u32,
320) {
321 register_location_key_debug_info(key, file, line, column);
322}
323
324#[cfg(test)]
325pub(crate) fn location_key_debug_collision_count_for_test() -> usize {
326 thread_state::cells(|cells| cells.location_key_collisions.get())
327}
328
329#[cfg(test)]
330pub(crate) fn location_key_debug_info_for_test(key: Key) -> Option<LocationKeyDebugInfo> {
331 thread_state::runtimes(|state| state.location_keys.borrow().get(&key).cloned())
332}
333
334#[cfg(test)]
335pub(crate) fn slot_validation_diagnostics_enabled() -> bool {
336 true
337}
338
339#[cfg(all(debug_assertions, not(test)))]
340pub(crate) fn slot_validation_diagnostics_enabled() -> bool {
341 crate::env_flag!("CRANPOSE_VALIDATE_SLOTS")
342}
343
344const fn source_location_hash(file: &str, line: u32, column: u32) -> u64 {
345 position_location_hash(file_location_hash(file), line, column)
346}
347
348const fn file_location_hash(file: &str) -> u64 {
349 fnv1a_location_key_bytes(0xcbf2_9ce4_8422_2325u64, file.as_bytes())
350}
351
352fn static_file_location_hash(cells: &thread_state::ThreadCells, file: &'static str) -> u64 {
357 let address = file.as_ptr() as usize;
358 let cached = &cells.file_hashes[(address >> 4) % thread_state::FILE_HASH_SLOTS];
359 let (cached_address, cached_len, hash) = cached.get();
360 if cached_address == address && cached_len == file.len() {
361 return hash;
362 }
363 let hash = file_location_hash(file);
364 cached.set((address, file.len(), hash));
365 hash
366}
367
368const fn position_location_hash(mut hash: u64, line: u32, column: u32) -> u64 {
369 hash = fnv1a_location_key_bytes(hash, &[0xff]);
370 hash = fnv1a_location_key_bytes(hash, &line.to_le_bytes());
371 hash = fnv1a_location_key_bytes(hash, &[0xfe]);
372 hash = fnv1a_location_key_bytes(hash, &column.to_le_bytes());
373 hash
374}
375
376const fn fnv1a_location_key_bytes(mut hash: u64, bytes: &[u8]) -> u64 {
377 let mut index = 0;
378 while index < bytes.len() {
379 hash = (hash ^ bytes[index] as u64).wrapping_mul(0x0000_0100_0000_01b3);
380 index += 1;
381 }
382 hash
383}
384
385const fn avalanche_location_key(mut value: u64) -> u64 {
386 value ^= value >> 33;
387 value = value.wrapping_mul(0xff51_afd7_ed55_8ccd);
388 value ^= value >> 33;
389 value = value.wrapping_mul(0xc4ce_b9fe_1a85_ec53);
390 value ^ (value >> 33)
391}
392
393#[doc(hidden)]
394#[track_caller]
395pub fn caller_location_key() -> Key {
396 let caller = std::panic::Location::caller();
397 let file = caller.file();
398 if cfg!(feature = "hot-reload")
399 && let Some(key) = hot_call_site_key(file, caller.line(), caller.column())
400 {
401 return key;
402 }
403 call_site_key(caller)
404}
405
406fn call_site_key(caller: &'static std::panic::Location<'static>) -> Key {
412 let address = std::ptr::from_ref(caller).addr();
413 thread_state::cells(|cells| {
414 let cached = &cells.call_site_keys[(address >> 3) % thread_state::CALL_SITE_KEY_SLOTS];
415 let (cached_address, key) = cached.get();
416 if cached_address == address {
417 return key;
418 }
419 let file = caller.file();
420 let key = registered_location_key(
421 static_file_location_hash(cells, file),
422 file,
423 caller.line(),
424 caller.column(),
425 );
426 cached.set((address, key));
427 key
428 })
429}
430
431#[derive(Clone, Copy)]
432pub(crate) struct HotOrigin {
433 file: &'static str,
434 line: u32,
435 end: u32,
436 identity: Key,
437}
438
439impl HotOrigin {
440 pub(crate) const NONE: Self = Self {
441 file: "",
442 line: 0,
443 end: 0,
444 identity: 0,
445 };
446}
447
448#[doc(hidden)]
450pub struct HotOriginGuard {
451 previous: HotOrigin,
452}
453
454impl Drop for HotOriginGuard {
455 fn drop(&mut self) {
456 let previous = self.previous;
457 thread_state::cells(|cells| cells.hot_origin.set(previous));
458 }
459}
460
461#[doc(hidden)]
468pub fn hot_origin(file: &'static str, line: u32, end: u32, identity: Key) -> HotOriginGuard {
469 let next = HotOrigin {
470 file,
471 line,
472 end,
473 identity,
474 };
475 HotOriginGuard {
476 previous: thread_state::cells(|cells| cells.hot_origin.replace(next)),
477 }
478}
479
480fn hot_call_site_key(file: &str, line: u32, column: u32) -> Option<Key> {
481 let origin = thread_state::cells(|cells| cells.hot_origin.get());
482 if origin.file.is_empty() || !(origin.line..=origin.end).contains(&line) || origin.file != file
483 {
484 return None;
485 }
486 let relative = line.wrapping_sub(origin.line);
487 let hash = fnv1a_location_key_bytes(origin.identity, &relative.to_le_bytes());
488 Some(avalanche_location_key(fnv1a_location_key_bytes(
489 hash ^ 0xfc,
490 &column.to_le_bytes(),
491 )))
492}
493
494#[doc(hidden)]
497pub const fn hot_branch_key(file: &str, path_hash: u64) -> Key {
498 let hash = file_location_hash(file);
499 avalanche_location_key(fnv1a_location_key_bytes(
500 hash ^ 0xfd,
501 &path_hash.to_le_bytes(),
502 ))
503}
504
505#[doc(hidden)]
508pub const fn hot_definition_key(file: &str, module: &str, name: &str) -> Key {
509 let mut hash = file_location_hash(file);
510 hash = fnv1a_location_key_bytes(hash ^ 0xfe, module.as_bytes());
511 avalanche_location_key(fnv1a_location_key_bytes(hash ^ 0xff, name.as_bytes()))
512}
513
514#[doc(hidden)]
515#[track_caller]
516pub fn composable_identity_key(definition: Key) -> Key {
517 (definition.wrapping_mul(0x0000_0100_0000_01b3) ^ caller_location_key())
518 .wrapping_mul(0x0000_0100_0000_01b3)
519}
520
521#[doc(hidden)]
522pub fn composable_definition_key(
523 file: &str,
524 line: u32,
525 column: u32,
526 marker: std::any::TypeId,
527) -> Key {
528 let mut hasher = std::collections::hash_map::DefaultHasher::new();
529 std::hash::Hash::hash(&marker, &mut hasher);
530 location_key(file, line, column) ^ avalanche_location_key(std::hash::Hasher::finish(&hasher))
531}
532
533#[doc(hidden)]
536pub const fn const_location_key(file: &str, line: u32, column: u32) -> Key {
537 avalanche_location_key(source_location_hash(file, line, column))
538}
539
540#[macro_export]
543macro_rules! location_key {
544 () => {{
545 const KEY: $crate::Key = $crate::const_location_key(file!(), line!(), column!());
546 $crate::noted_location_key(KEY, file!(), line!(), column!())
547 }};
548}
549
550#[doc(hidden)]
553#[inline(always)]
554pub fn noted_location_key(key: Key, file: &str, line: u32, column: u32) -> Key {
555 note_location_key(key, file, line, column);
556 key
557}
558
559#[doc(hidden)]
560pub fn cached_composable_definition_key(
561 cell: &OnceLock<Key>,
562 file: &str,
563 line: u32,
564 column: u32,
565 marker: TypeId,
566) -> Key {
567 *cell.get_or_init(|| composable_definition_key(file, line, column, marker))
568}
569
570pub fn location_key(file: &str, line: u32, column: u32) -> Key {
573 registered_location_key(file_location_hash(file), file, line, column)
574}
575
576fn registered_location_key(file_hash: u64, file: &str, line: u32, column: u32) -> Key {
577 let key = avalanche_location_key(position_location_hash(file_hash, line, column));
578 note_location_key(key, file, line, column);
579 key
580}
581
582#[cfg(any(test, debug_assertions))]
584fn note_location_key(key: Key, file: &str, line: u32, column: u32) {
585 #[cfg(test)]
586 register_location_key_debug_info(key, file, line, column);
587 #[cfg(all(debug_assertions, not(test)))]
588 if location_key_diagnostics_enabled() {
589 register_location_key_debug_info(key, file, line, column);
590 }
591}
592
593#[cfg(not(any(test, debug_assertions)))]
594fn note_location_key(_key: Key, _file: &str, _line: u32, _column: u32) {}
595
596#[doc(hidden)]
597#[inline]
598pub fn __branch_group_scope_deferred(key: Key) -> Option<ScopedBranchGroupGuard> {
599 composer_context::with_current_core(|core| {
600 ScopedBranchGroupGuard::open(&core.branch_folds, key)
601 })
602}
603
604#[doc(hidden)]
607pub const fn branch_location_key(file: &str, line: u32, column: u32, branch: u32) -> Key {
608 let mut hash = source_location_hash(file, line, column);
609 hash = fnv1a_location_key_bytes(hash, &[0xfd]);
610 hash = fnv1a_location_key_bytes(hash, &branch.to_le_bytes());
611 avalanche_location_key(hash)
612}
613
614#[derive(Copy, Clone, Debug, Hash, Eq, PartialEq, Default)]
620pub struct AnchorId {
621 id: u32,
622 generation: u32,
623}
624
625impl AnchorId {
626 pub(crate) const INVALID: AnchorId = AnchorId {
627 id: 0,
628 generation: 0,
629 };
630
631 pub(crate) fn new(id: usize) -> Self {
632 Self {
633 id: crate::slot::checked_usize_to_u32(id, "anchor id"),
634 generation: 1,
635 }
636 }
637
638 pub fn is_valid(&self) -> bool {
640 self.id != 0
641 }
642}
643
644pub(crate) type ScopeId = usize;
645pub(crate) type FrameCallbackId = u64;
646type LocalStackSnapshot = Option<Rc<composer::LocalFrame>>;
651
652#[derive(Clone)]
653pub(crate) struct LocalKey(Rc<()>);
654
655impl LocalKey {
656 fn new() -> Self {
657 Self(Rc::new(()))
658 }
659
660 pub(crate) fn entry_source(&self) -> Key {
661 avalanche_location_key(Rc::as_ptr(&self.0) as usize as u64)
662 }
663}
664
665impl std::fmt::Debug for LocalKey {
666 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
667 f.debug_tuple("LocalKey")
668 .field(&(Rc::as_ptr(&self.0) as usize))
669 .finish()
670 }
671}
672
673impl PartialEq for LocalKey {
674 fn eq(&self, other: &Self) -> bool {
675 Rc::ptr_eq(&self.0, &other.0)
676 }
677}
678
679impl Eq for LocalKey {}
680
681impl Hash for LocalKey {
682 fn hash<H: Hasher>(&self, state: &mut H) {
683 Rc::as_ptr(&self.0).hash(state);
684 }
685}
686
687struct RecomposeCallback {
691 body: Box<dyn FnMut(&Composer) + 'static>,
692 stateless_body: Option<fn() -> TypeId>,
693}
694
695fn scope_activity_epoch() -> u64 {
696 thread_state::cells(|cells| cells.scope_activity_epoch.get())
697}
698
699fn note_scope_activity_change() {
700 thread_state::cells(|cells| {
701 let epoch = &cells.scope_activity_epoch;
702 epoch.set(epoch.get() + 1);
703 });
704}
705
706fn read_cell<T: Default, R>(cell: &Cell<T>, read: impl FnOnce(&T) -> R) -> R {
711 let value = cell.take();
712 let result = read(&value);
713 cell.set(value);
714 result
715}
716
717fn update_cell<T: Default, R>(cell: &Cell<T>, update: impl FnOnce(&mut T) -> R) -> R {
719 let mut value = cell.take();
720 let result = update(&mut value);
721 cell.set(value);
722 result
723}
724
725type ScopeLink = Cell<Option<Weak<RecomposeScopeInner>>>;
726
727fn link_scope(link: &ScopeLink, target: Option<&RecomposeScope>) {
728 update_cell(link, |link| {
729 if link.as_ref().map(Weak::as_ptr) != target.map(|scope| Rc::as_ptr(&scope.inner)) {
730 *link = target.map(RecomposeScope::downgrade);
731 note_scope_activity_change();
732 }
733 });
734}
735
736const NO_PARENT_HINT: NodeId = NodeId::MAX;
739
740pub(crate) struct RecomposeScopeInner {
741 runtime: runtime::ScopeRuntime,
742 invalid: Cell<bool>,
743 enqueued: Cell<bool>,
744 active: Cell<bool>,
745 active_epoch: Cell<u64>,
746 deactivations: Cell<u64>,
747 composed_once: Cell<bool>,
748 pending_recompose: Cell<bool>,
749 force_reuse: Cell<bool>,
750 force_recompose: Cell<bool>,
751 derivation: Cell<bool>,
752 retention_mode: Cell<RetentionMode>,
753 parent_hint: Cell<NodeId>,
755 group_anchor: Cell<AnchorId>,
756 recompose: Cell<Option<RecomposeCallback>>,
757 parent_scope: ScopeLink,
758 lifetime_owner_scope: ScopeLink,
759 local_stack: Cell<LocalStackSnapshot>,
760 #[cfg(feature = "inspection")]
761 source_trace: RefCell<Rc<[source_trace::SourceLocation]>>,
762 #[cfg(all(feature = "inspection", debug_assertions))]
763 recompositions: std::cell::OnceCell<source_trace::RecompositionCounter>,
764 slots_storage_key: Cell<usize>,
765 slots_runtime_state: Cell<Option<std::rc::Weak<crate::composer::ComposerRuntimeState>>>,
766 state_subscriptions: Cell<StateIds>,
767 invalidation_sources: Cell<StateIds>,
768 unknown_invalidation_source: Cell<bool>,
769}
770
771type StateIds = SmallVec<[StateId; 2]>;
772
773enum ScopeOwner {
774 Root,
775 Live(Rc<RecomposeScopeInner>),
776 Dropped,
777}
778
779impl RecomposeScopeInner {
780 fn owner(&self) -> ScopeOwner {
781 let upgrade = |owner: &Option<Weak<RecomposeScopeInner>>| {
782 owner.as_ref().map(|owner| {
783 owner
784 .upgrade()
785 .map_or(ScopeOwner::Dropped, ScopeOwner::Live)
786 })
787 };
788 read_cell(&self.parent_scope, upgrade)
789 .or_else(|| read_cell(&self.lifetime_owner_scope, upgrade))
790 .unwrap_or(ScopeOwner::Root)
791 }
792
793 fn is_effectively_active(&self, epoch: u64) -> bool {
794 if self.active_epoch.get() == epoch {
795 return true;
796 }
797 if !self.active.get() {
798 return false;
799 }
800 let active = match self.owner() {
801 ScopeOwner::Root => true,
802 ScopeOwner::Dropped => false,
803 ScopeOwner::Live(owner) => owner.is_effectively_active(epoch),
804 };
805 if active {
806 self.active_epoch.set(epoch);
807 }
808 active
809 }
810
811 fn new(runtime: runtime::ScopeRuntime) -> Self {
812 runtime.increment_live_recompose_scope_count();
813 Self {
814 runtime,
815 invalid: Cell::new(false),
816 enqueued: Cell::new(false),
817 active: Cell::new(true),
818 active_epoch: Cell::new(0),
819 deactivations: Cell::new(0),
820 composed_once: Cell::new(false),
821 pending_recompose: Cell::new(false),
822 force_reuse: Cell::new(false),
823 force_recompose: Cell::new(false),
824 derivation: Cell::new(false),
825 retention_mode: Cell::new(RetentionMode::DisposeWhenInactive),
826 parent_hint: Cell::new(NO_PARENT_HINT),
827 group_anchor: Cell::new(AnchorId::INVALID),
828 recompose: Cell::new(None),
829 parent_scope: Cell::new(None),
830 lifetime_owner_scope: Cell::new(None),
831 local_stack: Cell::new(None),
832 #[cfg(feature = "inspection")]
833 source_trace: RefCell::new(Rc::from([])),
834 #[cfg(all(feature = "inspection", debug_assertions))]
835 recompositions: std::cell::OnceCell::new(),
836 slots_storage_key: Cell::new(0),
837 slots_runtime_state: Cell::new(None),
838 state_subscriptions: Cell::new(StateIds::new()),
839 invalidation_sources: Cell::new(StateIds::new()),
840 unknown_invalidation_source: Cell::new(false),
841 }
842 }
843
844 fn id(&self) -> ScopeId {
845 std::ptr::from_ref(self).addr()
846 }
847}
848
849fn push_unique_state_id(ids: &mut StateIds, state_id: StateId) {
850 if !ids.contains(&state_id) {
851 ids.push(state_id);
852 }
853}
854
855impl Drop for RecomposeScopeInner {
856 fn drop(&mut self) {
857 note_scope_activity_change();
858 let id = self.id();
859 self.runtime.decrement_live_recompose_scope_count();
860 let subscriptions = std::mem::take(self.state_subscriptions.get_mut());
861 for state_id in subscriptions {
862 self.runtime.unregister_state_scope(state_id, id);
863 }
864 #[cfg(debug_assertions)]
865 {
866 thread_state::try_runtimes(|state| {
867 state.scope_labels.borrow_mut().remove(&id);
868 state.scope_invalidation_sources.borrow_mut().remove(&id);
869 });
870 }
871 if self.enqueued.replace(false) {
872 self.runtime.mark_scope_recomposed();
873 }
874 }
875}
876
877#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
879pub struct RecomposeScopeRegistryDebugStats {
880 pub len: usize,
882 pub capacity: usize,
884}
885
886#[derive(Clone)]
889pub struct RecomposeScope {
890 inner: Rc<RecomposeScopeInner>,
891}
892
893impl PartialEq for RecomposeScope {
894 fn eq(&self, other: &Self) -> bool {
895 Rc::ptr_eq(&self.inner, &other.inner)
896 }
897}
898
899impl Eq for RecomposeScope {}
900
901impl Hash for RecomposeScope {
902 fn hash<H: Hasher>(&self, state: &mut H) {
903 self.id().hash(state);
904 }
905}
906
907impl RecomposeScope {
908 pub(crate) fn mark_derivation(&self) {
912 self.inner.derivation.set(true);
913 }
914
915 pub(crate) fn is_derivation(&self) -> bool {
916 self.inner.derivation.get()
917 }
918
919 fn new(runtime: RuntimeHandle) -> Self {
920 Self {
921 inner: Rc::new(RecomposeScopeInner::new(runtime.scope_runtime())),
922 }
923 }
924
925 pub(crate) fn downgrade(&self) -> Weak<RecomposeScopeInner> {
926 Rc::downgrade(&self.inner)
927 }
928
929 pub(crate) fn upgrade(weak: &Weak<RecomposeScopeInner>) -> Option<Self> {
930 weak.upgrade().map(|inner| Self { inner })
931 }
932
933 pub fn id(&self) -> ScopeId {
935 self.inner.id()
936 }
937
938 pub fn is_invalid(&self) -> bool {
940 self.inner.invalid.get()
941 }
942
943 pub(crate) fn is_enqueued(&self) -> bool {
944 self.inner.enqueued.get()
945 }
946
947 pub fn is_active(&self) -> bool {
949 self.inner.active.get()
950 }
951
952 pub fn owner_chain_deactivation_epoch(&self) -> u64 {
959 let mut total = self.inner.deactivations.get();
960 let mut owner = self.inner.owner();
961 while let ScopeOwner::Live(scope) = owner {
962 total = total.wrapping_add(scope.deactivations.get());
963 owner = scope.owner();
964 }
965 total
966 }
967
968 pub(crate) fn is_effectively_active(&self) -> bool {
969 self.inner.is_effectively_active(scope_activity_epoch())
970 }
971
972 fn record_state_subscription(&self, state_id: StateId) {
973 update_cell(&self.inner.state_subscriptions, |ids| {
974 push_unique_state_id(ids, state_id);
975 });
976 }
977
978 fn record_unknown_invalidation_source(&self) {
979 self.inner.unknown_invalidation_source.set(true);
980 update_cell(&self.inner.invalidation_sources, StateIds::clear);
981 }
982
983 fn record_state_invalidation_source(&self, state_id: StateId) {
984 if !self.inner.unknown_invalidation_source.get() {
985 update_cell(&self.inner.invalidation_sources, |ids| {
986 push_unique_state_id(ids, state_id);
987 });
988 }
989 }
990
991 fn enqueue_invalidation(&self) {
992 self.inner.invalid.set(true);
993 if !self.is_effectively_active() {
994 return;
995 }
996 if !self.inner.enqueued.replace(true) {
997 self.inner.runtime.register_invalid_scope(self.downgrade());
998 }
999 }
1000
1001 fn invalidate(&self) {
1002 self.record_unknown_invalidation_source();
1003 self.enqueue_invalidation();
1004 }
1005
1006 pub(crate) fn invalidate_from_state(&self, state_id: StateId) {
1007 self.record_state_invalidation_source(state_id);
1008 self.enqueue_invalidation();
1009 }
1010
1011 fn mark_recomposed(&self) {
1012 self.inner.invalid.set(false);
1013 self.inner.force_reuse.set(false);
1014 self.inner.force_recompose.set(false);
1015 self.inner.unknown_invalidation_source.set(false);
1016 update_cell(&self.inner.invalidation_sources, StateIds::clear);
1017 if self.inner.enqueued.replace(false) {
1018 self.inner.runtime.mark_scope_recomposed();
1019 }
1020 let pending = self.inner.pending_recompose.replace(false);
1021 if pending {
1022 if self.inner.active.get() {
1023 self.invalidate();
1024 } else {
1025 self.inner.invalid.set(true);
1026 }
1027 }
1028 }
1029
1030 fn set_boxed_recompose(
1031 &self,
1032 body: Box<dyn FnMut(&Composer) + 'static>,
1033 stateless_body: Option<fn() -> TypeId>,
1034 ) {
1035 #[cfg(feature = "inspection")]
1036 self.inner
1037 .source_trace
1038 .replace(source_trace::current_source_trace());
1039 self.inner.recompose.set(Some(RecomposeCallback {
1040 body,
1041 stateless_body,
1042 }));
1043 }
1044
1045 fn reruns_stateless(&self, body: TypeId) -> bool {
1046 read_cell(&self.inner.recompose, |callback| {
1047 callback
1048 .as_ref()
1049 .and_then(|callback| callback.stateless_body)
1050 .is_some_and(|current_body| current_body() == body)
1051 })
1052 }
1053
1054 fn run_recompose(&self, composer: &Composer) -> bool {
1055 #[cfg(feature = "inspection")]
1056 let _source_context = source_trace::restore_source_trace(&self.inner.source_trace.borrow());
1057 let Some(mut callback) = self.inner.recompose.take() else {
1058 return false;
1059 };
1060 (callback.body)(composer);
1061 update_cell(&self.inner.recompose, |slot| {
1063 if slot.is_none() {
1064 *slot = Some(callback);
1065 }
1066 });
1067 true
1068 }
1069
1070 fn has_recompose_callback(&self) -> bool {
1071 read_cell(&self.inner.recompose, Option::is_some)
1072 }
1073
1074 fn snapshot_locals(&self, stack: &LocalStackSnapshot) {
1075 update_cell(&self.inner.local_stack, |locals| {
1076 let unchanged = match (&*locals, stack) {
1077 (Some(current), Some(stack)) => Rc::ptr_eq(current, stack),
1078 (current, stack) => current.is_none() && stack.is_none(),
1079 };
1080 if !unchanged {
1081 locals.clone_from(stack);
1082 }
1083 });
1084 }
1085
1086 fn local_stack(&self) -> LocalStackSnapshot {
1087 read_cell(&self.inner.local_stack, Clone::clone)
1088 }
1089
1090 fn set_parent_hint(&self, parent: Option<NodeId>) {
1091 self.inner.parent_hint.set(parent.unwrap_or(NO_PARENT_HINT));
1092 }
1093
1094 fn set_parent_scope(&self, parent: Option<&RecomposeScope>) {
1095 link_scope(&self.inner.parent_scope, parent);
1096 }
1097
1098 fn parent_scope(&self) -> Option<RecomposeScope> {
1099 read_cell(&self.inner.parent_scope, |parent| {
1100 parent.as_ref().and_then(RecomposeScope::upgrade)
1101 })
1102 }
1103
1104 fn set_lifetime_owner_scope(&self, owner: Option<&RecomposeScope>) {
1105 link_scope(&self.inner.lifetime_owner_scope, owner);
1106 }
1107
1108 #[cfg(test)]
1109 fn lifetime_owner_scope(&self) -> Option<RecomposeScope> {
1110 read_cell(&self.inner.lifetime_owner_scope, |owner| {
1111 owner.as_ref().and_then(RecomposeScope::upgrade)
1112 })
1113 }
1114
1115 fn callback_promotion_target(&self) -> Option<RecomposeScope> {
1116 let mut current = self.parent_scope();
1117 while let Some(scope) = current {
1118 if scope.has_recompose_callback() {
1119 return Some(scope);
1120 }
1121 current = scope.parent_scope();
1122 }
1123 None
1124 }
1125
1126 fn parent_hint(&self) -> Option<NodeId> {
1127 Some(self.inner.parent_hint.get()).filter(|&hint| hint != NO_PARENT_HINT)
1128 }
1129
1130 pub(crate) fn group_anchor(&self) -> AnchorId {
1131 self.inner.group_anchor.get()
1132 }
1133
1134 pub(crate) fn set_group_anchor(&self, anchor: AnchorId) {
1135 self.inner.group_anchor.set(anchor);
1136 }
1137
1138 fn set_slots_host(&self, host: &SlotsHost) {
1139 let storage_key = host.storage_key();
1140 update_cell(&self.inner.slots_runtime_state, |runtime_state| {
1141 if self.inner.slots_storage_key.get() == storage_key
1142 && runtime_state.as_ref().map(std::rc::Weak::as_ptr) == host.runtime_state_ptr()
1143 {
1144 return;
1145 }
1146 self.inner.slots_storage_key.set(storage_key);
1147 *runtime_state = host.runtime_state().map(|state| Rc::downgrade(&state));
1148 });
1149 }
1150
1151 pub(crate) fn slots_storage_key(&self) -> Option<usize> {
1152 let key = self.inner.slots_storage_key.get();
1153 (key != 0).then_some(key)
1154 }
1155
1156 pub(crate) fn slots_host_identity(
1159 &self,
1160 ) -> (usize, *const crate::composer::ComposerRuntimeState) {
1161 let state = read_cell(&self.inner.slots_runtime_state, |state| {
1162 state
1163 .as_ref()
1164 .map_or(std::ptr::null(), std::rc::Weak::as_ptr)
1165 });
1166 (self.inner.slots_storage_key.get(), state)
1167 }
1168
1169 pub(crate) fn slots_runtime_state(&self) -> Option<Rc<crate::composer::ComposerRuntimeState>> {
1170 read_cell(&self.inner.slots_runtime_state, |state| {
1171 state.as_ref().and_then(std::rc::Weak::upgrade)
1172 })
1173 }
1174
1175 pub fn deactivate(&self) {
1178 if !self.inner.active.replace(false) {
1179 return;
1180 }
1181 note_scope_activity_change();
1182 self.inner
1183 .deactivations
1184 .set(self.inner.deactivations.get() + 1);
1185 if self.inner.enqueued.replace(false) {
1186 self.inner.runtime.mark_scope_recomposed();
1187 }
1188 }
1189
1190 pub(crate) fn defer_until_reactivated(&self) {
1191 if self.inner.enqueued.replace(false) {
1192 self.inner.runtime.mark_scope_recomposed();
1193 }
1194 }
1195
1196 pub fn reactivate(&self) {
1199 self.inner.active.set(true);
1200 if self.inner.invalid.get()
1201 && self.is_effectively_active()
1202 && !self.inner.enqueued.replace(true)
1203 {
1204 self.inner.runtime.register_invalid_scope(self.downgrade());
1205 }
1206 }
1207
1208 pub fn force_reuse(&self) {
1211 self.inner.force_reuse.set(true);
1212 self.inner.force_recompose.set(false);
1213 self.inner.pending_recompose.set(true);
1214 }
1215
1216 pub(crate) fn request_pending_recompose(&self) {
1217 self.inner.pending_recompose.set(true);
1218 }
1219
1220 pub fn force_recompose(&self) {
1223 self.inner.force_recompose.set(true);
1224 self.inner.force_reuse.set(false);
1225 self.inner.pending_recompose.set(false);
1226 }
1227
1228 pub(crate) fn set_retention_mode(&self, mode: RetentionMode) {
1229 self.inner.retention_mode.set(mode);
1230 }
1231
1232 pub(crate) fn retention_mode(&self) -> RetentionMode {
1233 self.inner.retention_mode.get()
1234 }
1235
1236 pub fn should_recompose(&self) -> bool {
1240 if self.inner.force_recompose.replace(false) {
1241 self.inner.force_reuse.set(false);
1242 return true;
1243 }
1244 if self.inner.force_reuse.replace(false) {
1245 return false;
1246 }
1247 self.is_invalid()
1248 }
1249
1250 pub fn has_composed_once(&self) -> bool {
1252 self.inner.composed_once.get()
1253 }
1254
1255 fn mark_composed_once(&self) {
1256 self.inner.composed_once.set(true);
1257 }
1258
1259 fn invalidated_only_by(&self, allowed_sources: &HashSet<StateId>) -> Option<bool> {
1260 if self.inner.unknown_invalidation_source.get() {
1261 return None;
1262 }
1263 read_cell(&self.inner.invalidation_sources, |sources| {
1264 (!sources.is_empty()).then(|| {
1265 sources
1266 .iter()
1267 .all(|source| allowed_sources.contains(source))
1268 })
1269 })
1270 }
1271
1272 fn has_unknown_invalidation_source(&self) -> bool {
1273 self.inner.unknown_invalidation_source.get()
1274 }
1275}
1276
1277#[cfg(test)]
1278impl RecomposeScope {
1279 pub(crate) fn new_for_test(runtime: RuntimeHandle) -> Self {
1280 Self::new(runtime)
1281 }
1282}
1283
1284#[derive(Debug, Clone, Copy, Default)]
1286pub struct RecomposeOptions {
1287 pub force_reuse: bool,
1289 pub force_recompose: bool,
1291 pub retention: RetentionMode,
1293}
1294
1295#[derive(Debug, Clone, PartialEq, Eq)]
1297pub enum NodeError {
1298 Missing {
1300 id: NodeId,
1302 },
1303 TypeMismatch {
1305 id: NodeId,
1307 expected: &'static str,
1309 },
1310 MissingContext {
1312 id: NodeId,
1314 reason: &'static str,
1316 },
1317 AlreadyExists {
1319 id: NodeId,
1321 },
1322 MalformedCommandPayload {
1324 tag: &'static str,
1326 },
1327 SlotHostUnavailable {
1329 operation: &'static str,
1331 reason: &'static str,
1333 },
1334 RecompositionLimitExceeded {
1336 operation: &'static str,
1338 limit: usize,
1340 },
1341}
1342
1343impl std::fmt::Display for NodeError {
1344 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1345 match self {
1346 NodeError::Missing { id } => write!(f, "node {id} missing"),
1347 NodeError::TypeMismatch { id, expected } => {
1348 write!(f, "node {id} type mismatch; expected {expected}")
1349 }
1350 NodeError::MissingContext { id, reason } => {
1351 write!(f, "missing context for node {id}: {reason}")
1352 }
1353 NodeError::AlreadyExists { id } => {
1354 write!(f, "node {id} already exists")
1355 }
1356 NodeError::MalformedCommandPayload { tag } => {
1357 write!(f, "command queue missing or invalid {tag} payload")
1358 }
1359 NodeError::SlotHostUnavailable { operation, reason } => {
1360 write!(f, "{operation} cannot access slot host: {reason}")
1361 }
1362 NodeError::RecompositionLimitExceeded { operation, limit } => {
1363 write!(
1364 f,
1365 "{operation} exceeded {limit} iterations while reconciling composition"
1366 )
1367 }
1368 }
1369 }
1370}
1371
1372impl std::error::Error for NodeError {}
1373
1374pub use subcompose::{
1375 ContentTypeReusePolicy, DefaultSlotReusePolicy, SlotId, SlotReusePolicy, SubcomposeState,
1376};
1377
1378#[derive(Copy, Clone, Debug, PartialEq, Eq)]
1380pub enum Phase {
1381 Compose,
1383 Measure,
1385 Layout,
1387}
1388
1389pub use composer_context::{
1390 __current_composer, note_nested_slots_host, with_composer as with_current_composer,
1391};
1392
1393#[expect(non_snake_case)]
1396pub fn withCurrentComposer<R>(f: impl FnOnce(&Composer) -> R) -> R {
1397 composer_context::with_composer(f)
1398}
1399
1400fn with_current_composer_opt<R>(f: impl FnOnce(&Composer) -> R) -> Option<R> {
1401 composer_context::try_with_composer(f)
1402}
1403
1404#[doc(hidden)]
1405pub fn current_recompose_scope_invalidated_only_by(
1406 allowed_sources: impl IntoIterator<Item = StateId>,
1407) -> Option<bool> {
1408 with_current_composer_opt(|composer| {
1409 let allowed_sources = allowed_sources.into_iter().collect();
1410 let mut scope = composer.current_recompose_scope();
1411 let mut saw_unknown_source = false;
1412 while let Some(current) = scope {
1413 if current.has_unknown_invalidation_source() {
1414 saw_unknown_source = true;
1415 scope = current.parent_scope();
1416 continue;
1417 }
1418 if let Some(matches) = current.invalidated_only_by(&allowed_sources) {
1419 return Some(matches);
1420 }
1421 scope = current.parent_scope();
1422 }
1423 saw_unknown_source.then_some(false)
1424 })
1425 .flatten()
1426}
1427
1428fn key_scoped<K: Hash, R>(
1429 key: &K,
1430 caller: &'static std::panic::Location<'static>,
1431 content: impl FnOnce() -> R,
1432) -> R {
1433 let seed = explicit_group_key_seed(key, caller);
1434 with_current_composer(|composer| composer.with_group_seed(seed, |_| content()))
1435}
1436
1437#[track_caller]
1440pub fn with_key<K: Hash>(key: &K, content: impl FnOnce()) {
1441 key_scoped(key, std::panic::Location::caller(), content);
1442}
1443
1444#[track_caller]
1455pub fn key<K: Hash, R>(keys: K, content: impl FnOnce() -> R) -> R {
1456 key_scoped(&keys, std::panic::Location::caller(), content)
1457}
1458
1459#[track_caller]
1478pub fn movable<K: Hash>(key: K, content: impl FnOnce()) {
1479 let id = hash_key(&key);
1480 with_current_composer(|composer| composer.with_movable_group(id, |_| content()));
1481}
1482
1483#[expect(non_snake_case)]
1517#[track_caller]
1518pub fn rememberMovableContentOf(content: impl Fn() + 'static) -> MovableContent {
1519 let runtime = with_current_composer(composer::Composer::runtime_handle);
1520 let id = remember(|| runtime.next_movable_content_id()).with(|id| *id);
1521 MovableContent {
1522 id,
1523 content: Rc::new(content),
1524 }
1525}
1526
1527#[expect(non_snake_case)]
1535pub fn movableContentOf<K: Hash>(key: K, content: impl Fn() + 'static) -> MovableContent {
1536 MovableContent {
1537 id: hash_key(&key),
1538 content: Rc::new(content),
1539 }
1540}
1541
1542#[derive(Clone)]
1544pub struct MovableContent {
1545 id: Key,
1546 content: Rc<dyn Fn()>,
1547}
1548
1549impl MovableContent {
1550 pub fn show(&self) {
1554 let content = Rc::clone(&self.content);
1555 let id = self.id;
1556 with_current_composer(|composer| composer.with_movable_group(id, |_| content()));
1557 }
1558
1559 pub fn forget(&self) {
1562 forget_movable_id(self.id);
1563 }
1564}
1565
1566impl std::fmt::Debug for MovableContent {
1567 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1568 f.debug_struct("MovableContent")
1569 .field("id", &self.id)
1570 .finish()
1571 }
1572}
1573
1574impl PartialEq for MovableContent {
1575 fn eq(&self, other: &Self) -> bool {
1576 self.id == other.id && Rc::ptr_eq(&self.content, &other.content)
1577 }
1578}
1579
1580pub fn forget_movable<K: Hash>(key: K) {
1586 let id = hash_key(&key);
1587 forget_movable_id(id);
1588}
1589
1590fn forget_movable_id(id: Key) {
1591 let runtime = composer_context::try_with_composer(composer::Composer::runtime_handle)
1592 .or_else(runtime::current_runtime_handle);
1593 match runtime {
1594 Some(runtime) => runtime.forget_movable(id),
1595 None => log::error!("forget_movable called without an active runtime"),
1596 }
1597}
1598
1599#[derive(Default)]
1600struct DisposableEffectState {
1601 key: Option<effect_key::EffectKey>,
1602 cleanup: Option<Box<dyn FnOnce()>>,
1603}
1604
1605impl DisposableEffectState {
1606 fn should_run<K: PartialEq + 'static>(&self, keys: &K) -> bool {
1607 self.key.as_ref().is_none_or(|current| !current.holds(keys))
1608 }
1609
1610 fn set_key(&mut self, key: effect_key::EffectKey) {
1611 self.key = Some(key);
1612 }
1613
1614 fn set_cleanup(&mut self, cleanup: Option<Box<dyn FnOnce()>>) {
1615 self.cleanup = cleanup;
1616 }
1617
1618 fn run_cleanup(&mut self) {
1619 if let Some(cleanup) = self.cleanup.take() {
1620 cleanup();
1621 }
1622 }
1623}
1624
1625impl Drop for DisposableEffectState {
1626 fn drop(&mut self) {
1627 self.run_cleanup();
1628 }
1629}
1630
1631#[derive(Clone, Copy, Debug, Default)]
1634pub struct DisposableEffectScope;
1635
1636#[derive(Default)]
1639pub struct DisposableEffectResult {
1640 cleanup: Option<Box<dyn FnOnce()>>,
1641}
1642
1643impl DisposableEffectScope {
1644 pub fn on_dispose(&self, cleanup: impl FnOnce() + 'static) -> DisposableEffectResult {
1647 DisposableEffectResult::new(cleanup)
1648 }
1649}
1650
1651impl DisposableEffectResult {
1652 pub fn new(cleanup: impl FnOnce() + 'static) -> Self {
1654 Self {
1655 cleanup: Some(Box::new(cleanup)),
1656 }
1657 }
1658
1659 fn into_cleanup(self) -> Option<Box<dyn FnOnce()>> {
1660 self.cleanup
1661 }
1662}
1663
1664#[expect(non_snake_case)]
1667pub fn SideEffect(effect: impl FnOnce() + 'static) {
1668 with_current_composer(|composer| composer.register_side_effect(effect));
1669}
1670
1671#[doc(hidden)]
1674pub fn __disposable_effect_impl<K, F>(group_key: Key, keys: K, effect: F)
1675where
1676 K: PartialEq + 'static,
1677 F: FnOnce(DisposableEffectScope) -> DisposableEffectResult + 'static,
1678{
1679 with_current_composer(|composer| {
1680 composer.with_group(group_key, |composer| {
1681 let state = composer.remember_effect::<DisposableEffectState>();
1682 if state.with(|state| state.should_run(&keys)) {
1683 let key = effect_key::EffectKey::new(keys);
1684 state.update(|state| {
1685 state.run_cleanup();
1686 state.set_key(key);
1687 });
1688 let mut effect_opt = Some(effect);
1689 composer.register_side_effect(move || {
1690 if let Some(effect) = effect_opt.take() {
1691 let result = effect(DisposableEffectScope);
1692 state.update(|state| state.set_cleanup(result.into_cleanup()));
1693 }
1694 });
1695 }
1696 });
1697 });
1698}
1699
1700#[expect(non_snake_case)]
1708#[track_caller]
1709pub fn DisposableEffect<K, F>(keys: K, effect: F)
1710where
1711 K: PartialEq + 'static,
1712 F: FnOnce(DisposableEffectScope) -> DisposableEffectResult + 'static,
1713{
1714 __disposable_effect_impl(crate::caller_location_key(), keys, effect);
1715}
1716
1717#[macro_export]
1722macro_rules! clone_captures {
1723 ($($alias:ident $(= $value:expr)?),+ $(,)?; $body:expr) => {{
1724 $(let $alias = $crate::clone_captures!(@clone $alias $(= $value)?);)+
1725 $body
1726 }};
1727 (@clone $alias:ident = $value:expr) => {
1728 ($value).clone()
1729 };
1730 (@clone $alias:ident) => {
1731 $alias.clone()
1732 };
1733}
1734
1735pub fn with_node_mut<N: Node + 'static, R>(
1738 id: NodeId,
1739 f: impl FnOnce(&mut N) -> R,
1740) -> Result<R, NodeError> {
1741 with_current_composer(|composer| composer.with_node_mut(id, f))
1742}
1743
1744pub fn push_parent(id: NodeId) {
1747 with_current_composer(|composer| composer.push_parent(id));
1748}
1749
1750pub fn pop_parent() {
1753 with_current_composer(composer::Composer::pop_parent);
1754}
1755
1756pub trait Node: Any {
1759 fn mount(&mut self) {}
1761 fn unmount(&mut self) {}
1763 fn insert_child(&mut self, _child: NodeId) -> bool {
1769 false
1770 }
1771 fn remove_child(&mut self, _child: NodeId) -> bool {
1776 false
1777 }
1778 fn move_child(&mut self, _from: usize, _to: usize) {}
1780 fn update_children(&mut self, _children: &[NodeId]) {}
1782 fn collect_children_into(&self, out: &mut SmallVec<[NodeId; 8]>) {
1786 out.clear();
1787 }
1788 fn collect_owned_children_into(&self, out: &mut SmallVec<[NodeId; 8]>) {
1792 self.collect_children_into(out);
1793 }
1794 fn owned_child_index(&self, child: NodeId) -> Option<usize> {
1806 let mut children = SmallVec::<[NodeId; 8]>::new();
1807 self.collect_owned_children_into(&mut children);
1808 children.iter().position(|&id| id == child)
1809 }
1810 fn set_node_id(&mut self, _id: NodeId) {}
1813 fn on_attached_to_parent(&mut self, _parent: NodeId) {}
1816 fn on_removed_from_parent(&mut self) {}
1819 fn parent(&self) -> Option<NodeId> {
1822 None
1823 }
1824 fn mark_needs_layout(&self) {}
1827 fn needs_layout(&self) -> bool {
1829 false
1830 }
1831 fn mark_needs_measure(&self) {}
1834 fn needs_measure(&self) -> bool {
1836 false
1837 }
1838 fn mark_descendant_needs_layout(&self, measure: bool) {
1843 if measure {
1844 self.mark_needs_measure();
1845 } else {
1846 self.mark_needs_layout();
1847 }
1848 }
1849 fn descendant_needs_layout(&self) -> bool {
1851 false
1852 }
1853 fn descendant_needs_measure(&self) -> bool {
1855 false
1856 }
1857 fn layout_dirty(&self) -> bool {
1860 self.needs_measure() || self.needs_layout() || self.descendant_needs_layout()
1861 }
1862 fn is_virtual(&self) -> bool {
1865 false
1866 }
1867 fn mark_needs_semantics(&self) {}
1869 fn mark_descendant_needs_semantics(&self) {
1873 self.mark_needs_semantics();
1874 }
1875 fn needs_semantics(&self) -> bool {
1877 false
1878 }
1879 fn set_parent_for_bubbling(&mut self, parent: NodeId) {
1888 if self.parent().is_none() {
1889 self.on_attached_to_parent(parent);
1890 }
1891 }
1892
1893 fn recycle_key(&self) -> Option<TypeId> {
1895 None
1896 }
1897
1898 fn recycle_pool_limit(&self) -> Option<usize> {
1900 None
1901 }
1902
1903 fn prepare_for_recycle(&mut self) {}
1905
1906 fn rehouse_for_recycle(&self) -> Option<Box<dyn Node>> {
1911 None
1912 }
1913
1914 fn rehouse_for_live_compaction(&mut self) -> Option<Box<dyn Node>> {
1920 None
1921 }
1922
1923 fn debug_heap_bytes(&self) -> usize {
1925 0
1926 }
1927}
1928
1929pub fn bubble_layout_dirty(applier: &mut dyn Applier, node_id: NodeId) {
1947 DirtyBubble::LAYOUT.apply(applier, node_id);
1948}
1949
1950pub fn bubble_measure_dirty(applier: &mut dyn Applier, node_id: NodeId) {
1960 DirtyBubble::MEASURE.apply(applier, node_id);
1961}
1962
1963pub fn bubble_semantics_dirty(applier: &mut dyn Applier, node_id: NodeId) {
1969 DirtyBubble::SEMANTICS.apply(applier, node_id);
1970}
1971
1972pub fn queue_semantics_invalidation(node_id: NodeId) {
1977 let _ = composer_context::try_with_composer(|composer| {
1978 composer.enqueue_semantics_invalidation(node_id);
1979 });
1980}
1981
1982pub fn bubble_layout_dirty_in_composer<N: Node + 'static>(node_id: NodeId) {
2004 bubble_layout_dirty_composer::<N>(node_id);
2005}
2006
2007pub fn bubble_measure_dirty_in_composer(node_id: NodeId) {
2013 with_current_composer(|composer| {
2014 composer.commands_mut().push(Command::BubbleDirty {
2015 node_id,
2016 bubble: DirtyBubble {
2017 layout: false,
2018 measure: true,
2019 semantics: false,
2020 },
2021 });
2022 });
2023}
2024
2025pub fn bubble_semantics_dirty_in_composer<N: Node + 'static>(node_id: NodeId) {
2032 bubble_semantics_dirty_composer::<N>(node_id);
2033}
2034
2035fn bubble_layout_dirty_composer<N: Node + 'static>(mut node_id: NodeId) {
2036 let _ = with_node_mut(node_id, |node: &mut N| {
2037 node.mark_needs_layout();
2038 });
2039
2040 while let Ok(Some(pid)) = with_node_mut(node_id, |node: &mut N| node.parent()) {
2041 let parent_id = pid;
2042
2043 let advanced = with_node_mut(parent_id, |node: &mut N| {
2044 node.mark_descendant_needs_layout(false);
2045 true
2046 })
2047 .unwrap_or(false);
2048
2049 if advanced {
2050 node_id = parent_id;
2051 } else {
2052 break;
2053 }
2054 }
2055}
2056
2057fn bubble_semantics_dirty_composer<N: Node + 'static>(mut node_id: NodeId) {
2058 let _ = with_node_mut(node_id, |node: &mut N| {
2059 node.mark_needs_semantics();
2060 });
2061
2062 while let Ok(Some(pid)) = with_node_mut(node_id, |node: &mut N| node.parent()) {
2063 let parent_id = pid;
2064
2065 let advanced = with_node_mut(parent_id, |node: &mut N| {
2066 if !node.needs_semantics() {
2067 node.mark_descendant_needs_semantics();
2068 }
2069 true
2070 })
2071 .unwrap_or(false);
2072
2073 if advanced {
2074 node_id = parent_id;
2075 } else {
2076 break;
2077 }
2078 }
2079}
2080
2081impl dyn Node {
2082 pub fn as_any_mut(&mut self) -> &mut dyn Any {
2084 self
2085 }
2086}
2087
2088pub struct RecycledNode {
2091 stable_id: NodeId,
2092 node: Box<dyn Node>,
2093 warm_origin: bool,
2094}
2095
2096impl RecycledNode {
2097 fn new(stable_id: NodeId, mut node: Box<dyn Node>, warm_origin: bool) -> Self {
2101 let node = node.rehouse_for_recycle().unwrap_or_else(|| {
2102 node.prepare_for_recycle();
2103 node
2104 });
2105 Self {
2106 stable_id,
2107 node,
2108 warm_origin,
2109 }
2110 }
2111
2112 fn from_shell(stable_id: NodeId, node: Box<dyn Node>, warm_origin: bool) -> Self {
2113 Self {
2114 stable_id,
2115 node,
2116 warm_origin,
2117 }
2118 }
2119
2120 pub fn stable_id(&self) -> NodeId {
2122 self.stable_id
2123 }
2124
2125 fn warm_origin(&self) -> bool {
2126 self.warm_origin
2127 }
2128
2129 fn set_warm_origin(&mut self, warm_origin: bool) {
2130 self.warm_origin = warm_origin;
2131 }
2132
2133 pub fn node_mut(&mut self) -> &mut dyn Node {
2135 self.node.as_mut()
2136 }
2137
2138 pub fn into_parts(self) -> (NodeId, Box<dyn Node>, bool) {
2141 (self.stable_id, self.node, self.warm_origin)
2142 }
2143}
2144
2145#[derive(Debug, Clone, PartialEq, Eq)]
2147pub struct RecycledNodeInsertion {
2148 pub id: NodeId,
2150 pub stable_id_reused: bool,
2152 pub fallback_error: Option<NodeError>,
2154}
2155
2156impl RecycledNodeInsertion {
2157 fn reused(stable_id: NodeId) -> Self {
2158 Self {
2159 id: stable_id,
2160 stable_id_reused: true,
2161 fallback_error: None,
2162 }
2163 }
2164
2165 fn fresh(id: NodeId, fallback_error: Option<NodeError>) -> Self {
2166 Self {
2167 id,
2168 stable_id_reused: false,
2169 fallback_error,
2170 }
2171 }
2172}
2173
2174pub trait Applier: Any {
2177 fn create(&mut self, node: Box<dyn Node>) -> NodeId;
2179 fn get_mut(&mut self, id: NodeId) -> Result<&mut dyn Node, NodeError>;
2181 fn remove(&mut self, id: NodeId) -> Result<(), NodeError>;
2183
2184 fn begin_bubble_batch(&mut self) {}
2188
2189 fn end_bubble_batch(&mut self) {}
2191
2192 fn forget_bubble_passes(&mut self) {}
2195
2196 fn note_bubble_pass(&mut self, _id: NodeId, _flags: u8) -> bool {
2201 false
2202 }
2203
2204 fn record_structural_change(&mut self, _parent_id: NodeId) {}
2210
2211 fn node_generation(&self, id: NodeId) -> u32;
2215
2216 fn insert_with_id(&mut self, id: NodeId, node: Box<dyn Node>) -> Result<(), NodeError>;
2224
2225 fn insert_recycled_node_or_create(
2228 &mut self,
2229 stable_id: NodeId,
2230 node: Box<dyn Node>,
2231 ) -> RecycledNodeInsertion {
2232 let id = self.create(node);
2233 RecycledNodeInsertion::fresh(id, Some(NodeError::AlreadyExists { id: stable_id }))
2234 }
2235
2236 fn as_any(&self) -> &dyn Any
2238 where
2239 Self: Sized,
2240 {
2241 self
2242 }
2243
2244 fn as_any_mut(&mut self) -> &mut dyn Any
2246 where
2247 Self: Sized,
2248 {
2249 self
2250 }
2251
2252 fn compact(&mut self) {}
2254
2255 fn take_recycled_node(&mut self, _key: TypeId) -> Option<RecycledNode> {
2257 None
2258 }
2259
2260 fn set_recycled_node_origin(&mut self, _id: NodeId, _warm_origin: bool) {}
2262
2263 fn seed_recycled_node_shell(
2265 &mut self,
2266 _key: TypeId,
2267 _recycle_pool_limit: Option<usize>,
2268 _shell: Box<dyn Node>,
2269 ) {
2270 }
2271
2272 fn record_fresh_recyclable_creation(&mut self, _key: TypeId) {}
2274
2275 fn clear_recycled_nodes(&mut self) {}
2277}
2278
2279type CommandCallback = Box<dyn FnOnce(&mut dyn Applier) -> Result<(), NodeError> + 'static>;
2280
2281#[derive(Copy, Clone, Debug, PartialEq, Eq)]
2282pub(crate) struct DirtyBubble {
2283 layout: bool,
2284 measure: bool,
2285 semantics: bool,
2286}
2287
2288impl DirtyBubble {
2289 const LAYOUT: Self = Self {
2290 layout: true,
2291 measure: false,
2292 semantics: false,
2293 };
2294
2295 const MEASURE: Self = Self {
2296 layout: false,
2297 measure: true,
2298 semantics: false,
2299 };
2300
2301 pub(crate) const LAYOUT_AND_MEASURE: Self = Self {
2302 layout: true,
2303 measure: true,
2304 semantics: false,
2305 };
2306
2307 pub(crate) const SEMANTICS: Self = Self {
2308 layout: false,
2309 measure: false,
2310 semantics: true,
2311 };
2312
2313 fn apply(self, applier: &mut dyn Applier, node_id: NodeId) {
2316 self.walk(applier, Some(node_id), true);
2317 }
2318
2319 fn apply_to_ancestors(self, applier: &mut dyn Applier, node_id: NodeId) {
2322 let parent = applier.get_mut(node_id).ok().and_then(|node| node.parent());
2323 self.walk(applier, parent, false);
2324 }
2325
2326 fn walk(self, applier: &mut dyn Applier, first: Option<NodeId>, mut start: bool) {
2333 let flags =
2334 u8::from(self.layout) | u8::from(self.measure) << 1 | u8::from(self.semantics) << 2;
2335 let mut next = first;
2336 while let Some(id) = next {
2337 if !start && applier.note_bubble_pass(id, flags) {
2338 break;
2339 }
2340 let Ok(node) = applier.get_mut(id) else {
2341 break;
2342 };
2343 self.mark(node, start);
2344 next = node.parent();
2345 start = start && node.is_virtual();
2346 }
2347 }
2348
2349 fn mark(self, node: &mut dyn Node, start: bool) {
2352 if start {
2353 if self.layout {
2354 node.mark_needs_layout();
2355 }
2356 if self.measure {
2357 node.mark_needs_measure();
2358 }
2359 if self.semantics {
2360 node.mark_needs_semantics();
2361 }
2362 return;
2363 }
2364 let marked = if self.measure {
2365 node.descendant_needs_measure()
2366 } else {
2367 node.descendant_needs_layout()
2368 };
2369 if (self.layout || self.measure) && !marked {
2370 node.mark_descendant_needs_layout(self.measure);
2371 }
2372 if self.semantics && !node.needs_semantics() {
2373 node.mark_descendant_needs_semantics();
2374 }
2375 }
2376}
2377
2378pub(crate) enum Command {
2379 BubbleDirty {
2380 node_id: NodeId,
2381 bubble: DirtyBubble,
2382 },
2383 RemoveNode {
2384 id: NodeId,
2385 },
2386 MountNode {
2387 id: NodeId,
2388 },
2389 AttachChild {
2390 parent_id: NodeId,
2391 child_id: NodeId,
2392 insert_index: Option<usize>,
2393 bubble: DirtyBubble,
2394 },
2395 InsertChild {
2396 parent_id: NodeId,
2397 child_id: NodeId,
2398 appended_index: usize,
2399 insert_index: usize,
2400 bubble: DirtyBubble,
2401 },
2402 MoveChild {
2403 parent_id: NodeId,
2404 from_index: usize,
2405 to_index: usize,
2406 bubble: DirtyBubble,
2407 },
2408 RemoveChild {
2409 parent_id: NodeId,
2410 child_id: NodeId,
2411 },
2412 DetachChild {
2413 parent_id: NodeId,
2414 child_id: NodeId,
2415 },
2416 SyncChildren {
2417 parent_id: NodeId,
2418 expected_children: ChildList,
2419 },
2420 Callback(CommandCallback),
2421}
2422
2423#[derive(Copy, Clone, Debug, PartialEq, Eq)]
2424struct DeferredChildCleanup {
2425 child_id: NodeId,
2426 generation: u32,
2427 removed_from_parent: bool,
2428}
2429
2430#[derive(Default)]
2431struct DeferredChildCleanupQueue {
2432 pending: Vec<DeferredChildCleanup>,
2433 preserved: Vec<(NodeId, u32)>,
2434}
2435
2436impl DeferredChildCleanupQueue {
2437 fn push(&mut self, child_id: NodeId, generation: u32, removed_from_parent: bool) {
2438 if self
2439 .preserved
2440 .iter()
2441 .any(|&(preserved_id, preserved_generation)| {
2442 preserved_id == child_id && preserved_generation == generation
2443 })
2444 {
2445 return;
2446 }
2447 self.pending.push(DeferredChildCleanup {
2448 child_id,
2449 generation,
2450 removed_from_parent,
2451 });
2452 }
2453
2454 fn preserve(&mut self, child_id: NodeId, generation: u32) {
2455 if !self
2456 .preserved
2457 .iter()
2458 .any(|&(preserved_id, preserved_generation)| {
2459 preserved_id == child_id && preserved_generation == generation
2460 })
2461 {
2462 self.preserved.push((child_id, generation));
2463 }
2464 self.pending
2465 .retain(|cleanup| cleanup.child_id != child_id || cleanup.generation != generation);
2466 }
2467
2468 fn flush(&mut self, applier: &mut dyn Applier) -> Result<(), NodeError> {
2469 for cleanup in self.pending.drain(..) {
2470 cleanup_detached_child(applier, cleanup)?;
2471 }
2472 Ok(())
2473 }
2474
2475 fn clear(&mut self) {
2476 self.pending.clear();
2477 self.preserved.clear();
2478 }
2479}
2480
2481impl Command {
2482 pub(crate) fn callback(
2483 callback: impl FnOnce(&mut dyn Applier) -> Result<(), NodeError> + 'static,
2484 ) -> Self {
2485 Self::Callback(Box::new(callback))
2486 }
2487
2488 pub(crate) fn apply(self, applier: &mut dyn Applier) -> Result<(), NodeError> {
2489 let mut deferred_cleanup = DeferredChildCleanupQueue::default();
2490 self.apply_with_cleanup(applier, &mut deferred_cleanup)?;
2491 deferred_cleanup.flush(applier)
2492 }
2493
2494 fn apply_with_cleanup(
2495 self,
2496 applier: &mut dyn Applier,
2497 deferred_cleanup: &mut DeferredChildCleanupQueue,
2498 ) -> Result<(), NodeError> {
2499 match self {
2500 Self::BubbleDirty { node_id, bubble } => {
2501 bubble.apply(applier, node_id);
2502 Ok(())
2503 }
2504 Self::RemoveNode { id } => {
2505 if let Ok(node) = applier.get_mut(id) {
2506 node.unmount();
2507 }
2508 match applier.remove(id) {
2509 Ok(()) | Err(NodeError::Missing { .. }) => Ok(()),
2510 Err(err) => Err(err),
2511 }
2512 }
2513 Self::MountNode { id } => {
2514 let node = match applier.get_mut(id) {
2515 Ok(node) => node,
2516 Err(NodeError::Missing { .. }) => return Ok(()),
2517 Err(err) => return Err(err),
2518 };
2519 node.set_node_id(id);
2520 node.mount();
2521 Ok(())
2522 }
2523 Self::AttachChild {
2524 parent_id,
2525 child_id,
2526 insert_index,
2527 bubble,
2528 } => {
2529 attach_child_at(applier, parent_id, child_id, insert_index, bubble);
2530 Ok(())
2531 }
2532 Self::InsertChild {
2533 parent_id,
2534 child_id,
2535 appended_index,
2536 insert_index,
2537 bubble,
2538 } => {
2539 insert_child_with_reparenting(applier, parent_id, child_id);
2540 bubble.apply(applier, parent_id);
2541 if insert_index != appended_index
2542 && let Ok(parent_node) = applier.get_mut(parent_id)
2543 {
2544 parent_node.move_child(appended_index, insert_index);
2545 }
2546 Ok(())
2547 }
2548 Self::MoveChild {
2549 parent_id,
2550 from_index,
2551 to_index,
2552 bubble,
2553 } => {
2554 if let Ok(parent_node) = applier.get_mut(parent_id) {
2555 parent_node.move_child(from_index, to_index);
2556 }
2557 bubble.apply(applier, parent_id);
2558 note_structural_move(parent_id, from_index, to_index);
2559 applier.record_structural_change(parent_id);
2560 Ok(())
2561 }
2562 Self::RemoveChild {
2563 parent_id,
2564 child_id,
2565 } => apply_remove_child(applier, parent_id, child_id, deferred_cleanup),
2566 Self::DetachChild {
2567 parent_id,
2568 child_id,
2569 } => {
2570 let generation = applier.node_generation(child_id);
2571 detach_child_from_parent(applier, parent_id, child_id)?;
2572 deferred_cleanup.preserve(child_id, generation);
2573 Ok(())
2574 }
2575 Self::SyncChildren {
2576 parent_id,
2577 expected_children,
2578 } => sync_children(applier, parent_id, &expected_children, deferred_cleanup),
2579 Self::Callback(callback) => callback(applier),
2580 }
2581 }
2582}
2583
2584const COMMAND_FLUSH_THRESHOLD: usize = 4096;
2585type ChildList = SmallVec<[NodeId; 4]>;
2586const SMALL_CHILD_SYNC_LINEAR_THRESHOLD: usize = 8;
2587
2588#[derive(Copy, Clone)]
2589enum CommandTag {
2590 BubbleDirty,
2591 RemoveNode,
2592 MountNode,
2593 AttachChild,
2594 InsertChild,
2595 MoveChild,
2596 RemoveChild,
2597 DetachChild,
2598 SyncChildren,
2599 Callback,
2600}
2601
2602impl CommandTag {
2603 fn label(self) -> &'static str {
2604 match self {
2605 Self::BubbleDirty => "BubbleDirty",
2606 Self::RemoveNode => "RemoveNode",
2607 Self::MountNode => "MountNode",
2608 Self::AttachChild => "AttachChild",
2609 Self::InsertChild => "InsertChild",
2610 Self::MoveChild => "MoveChild",
2611 Self::RemoveChild => "RemoveChild",
2612 Self::DetachChild => "DetachChild",
2613 Self::SyncChildren => "SyncChildren",
2614 Self::Callback => "Callback",
2615 }
2616 }
2617}
2618
2619#[derive(Copy, Clone)]
2620struct BubbleDirtyCommand {
2621 node_id: NodeId,
2622 bubble: DirtyBubble,
2623}
2624
2625#[derive(Copy, Clone)]
2626struct AttachChildCommand {
2627 parent_id: NodeId,
2628 child_id: NodeId,
2629 insert_index: Option<usize>,
2630 bubble: DirtyBubble,
2631}
2632
2633#[derive(Copy, Clone)]
2634struct InsertChildCommand {
2635 parent_id: NodeId,
2636 child_id: NodeId,
2637 appended_index: usize,
2638 insert_index: usize,
2639 bubble: DirtyBubble,
2640}
2641
2642#[derive(Copy, Clone)]
2643struct MoveChildCommand {
2644 parent_id: NodeId,
2645 from_index: usize,
2646 to_index: usize,
2647 bubble: DirtyBubble,
2648}
2649
2650#[derive(Copy, Clone)]
2651struct RemoveChildCommand {
2652 parent_id: NodeId,
2653 child_id: NodeId,
2654}
2655
2656#[derive(Copy, Clone)]
2657struct DetachChildCommand {
2658 parent_id: NodeId,
2659 child_id: NodeId,
2660}
2661
2662struct SyncChildrenCommand {
2663 parent_id: NodeId,
2664 child_start: usize,
2665 child_len: usize,
2666}
2667
2668#[derive(Default)]
2672pub(crate) struct CommandQueue {
2673 tags: Vec<CommandTag>,
2674 bubble_dirty: Vec<BubbleDirtyCommand>,
2675 remove_nodes: Vec<NodeId>,
2676 mount_nodes: Vec<NodeId>,
2677 attach_children: Vec<AttachChildCommand>,
2678 insert_children: Vec<InsertChildCommand>,
2679 move_children: Vec<MoveChildCommand>,
2680 remove_children: Vec<RemoveChildCommand>,
2681 detach_children: Vec<DetachChildCommand>,
2682 sync_children: Vec<SyncChildrenCommand>,
2683 sync_child_ids: Vec<NodeId>,
2684 callbacks: Vec<CommandCallback>,
2685 cleanup: DeferredChildCleanupQueue,
2686}
2687
2688impl CommandQueue {
2689 fn push_tag(&mut self, tag: CommandTag) {
2690 self.tags.push(tag);
2691 }
2692
2693 fn push(&mut self, command: Command) {
2694 match command {
2695 Command::BubbleDirty { node_id, bubble } => {
2696 self.bubble_dirty
2697 .push(BubbleDirtyCommand { node_id, bubble });
2698 self.push_tag(CommandTag::BubbleDirty);
2699 }
2700 Command::RemoveNode { id } => {
2701 self.remove_nodes.push(id);
2702 self.push_tag(CommandTag::RemoveNode);
2703 }
2704 Command::MountNode { id } => {
2705 self.mount_nodes.push(id);
2706 self.push_tag(CommandTag::MountNode);
2707 }
2708 Command::AttachChild {
2709 parent_id,
2710 child_id,
2711 insert_index,
2712 bubble,
2713 } => {
2714 self.attach_children.push(AttachChildCommand {
2715 parent_id,
2716 child_id,
2717 insert_index,
2718 bubble,
2719 });
2720 self.push_tag(CommandTag::AttachChild);
2721 }
2722 Command::InsertChild {
2723 parent_id,
2724 child_id,
2725 appended_index,
2726 insert_index,
2727 bubble,
2728 } => {
2729 self.insert_children.push(InsertChildCommand {
2730 parent_id,
2731 child_id,
2732 appended_index,
2733 insert_index,
2734 bubble,
2735 });
2736 self.push_tag(CommandTag::InsertChild);
2737 }
2738 Command::MoveChild {
2739 parent_id,
2740 from_index,
2741 to_index,
2742 bubble,
2743 } => {
2744 self.move_children.push(MoveChildCommand {
2745 parent_id,
2746 from_index,
2747 to_index,
2748 bubble,
2749 });
2750 self.push_tag(CommandTag::MoveChild);
2751 }
2752 Command::RemoveChild {
2753 parent_id,
2754 child_id,
2755 } => {
2756 self.remove_children.push(RemoveChildCommand {
2757 parent_id,
2758 child_id,
2759 });
2760 self.push_tag(CommandTag::RemoveChild);
2761 }
2762 Command::DetachChild {
2763 parent_id,
2764 child_id,
2765 } => {
2766 self.detach_children.push(DetachChildCommand {
2767 parent_id,
2768 child_id,
2769 });
2770 self.push_tag(CommandTag::DetachChild);
2771 }
2772 Command::SyncChildren {
2773 parent_id,
2774 expected_children,
2775 } => {
2776 let child_start = self.sync_child_ids.len();
2777 let child_len = expected_children.len();
2778 self.sync_child_ids.extend(expected_children);
2779 self.sync_children.push(SyncChildrenCommand {
2780 parent_id,
2781 child_start,
2782 child_len,
2783 });
2784 self.push_tag(CommandTag::SyncChildren);
2785 }
2786 Command::Callback(callback) => {
2787 self.callbacks.push(callback);
2788 self.push_tag(CommandTag::Callback);
2789 }
2790 }
2791 }
2792
2793 fn len(&self) -> usize {
2794 self.tags.len()
2795 }
2796
2797 fn capacity(&self) -> usize {
2798 self.tags.capacity()
2799 }
2800
2801 fn payload_len_bytes(&self) -> usize {
2802 self.bubble_dirty
2803 .len()
2804 .saturating_mul(std::mem::size_of::<BubbleDirtyCommand>())
2805 .saturating_add(
2806 self.remove_nodes
2807 .len()
2808 .saturating_mul(std::mem::size_of::<NodeId>()),
2809 )
2810 .saturating_add(
2811 self.mount_nodes
2812 .len()
2813 .saturating_mul(std::mem::size_of::<NodeId>()),
2814 )
2815 .saturating_add(
2816 self.attach_children
2817 .len()
2818 .saturating_mul(std::mem::size_of::<AttachChildCommand>()),
2819 )
2820 .saturating_add(
2821 self.insert_children
2822 .len()
2823 .saturating_mul(std::mem::size_of::<InsertChildCommand>()),
2824 )
2825 .saturating_add(
2826 self.move_children
2827 .len()
2828 .saturating_mul(std::mem::size_of::<MoveChildCommand>()),
2829 )
2830 .saturating_add(
2831 self.remove_children
2832 .len()
2833 .saturating_mul(std::mem::size_of::<RemoveChildCommand>()),
2834 )
2835 .saturating_add(
2836 self.detach_children
2837 .len()
2838 .saturating_mul(std::mem::size_of::<DetachChildCommand>()),
2839 )
2840 .saturating_add(
2841 self.sync_children
2842 .len()
2843 .saturating_mul(std::mem::size_of::<SyncChildrenCommand>()),
2844 )
2845 .saturating_add(
2846 self.sync_child_ids
2847 .len()
2848 .saturating_mul(std::mem::size_of::<NodeId>()),
2849 )
2850 .saturating_add(
2851 self.callbacks
2852 .len()
2853 .saturating_mul(std::mem::size_of::<CommandCallback>()),
2854 )
2855 }
2856
2857 fn payload_capacity_bytes(&self) -> usize {
2858 self.bubble_dirty
2859 .capacity()
2860 .saturating_mul(std::mem::size_of::<BubbleDirtyCommand>())
2861 .saturating_add(
2862 self.remove_nodes
2863 .capacity()
2864 .saturating_mul(std::mem::size_of::<NodeId>()),
2865 )
2866 .saturating_add(
2867 self.mount_nodes
2868 .capacity()
2869 .saturating_mul(std::mem::size_of::<NodeId>()),
2870 )
2871 .saturating_add(
2872 self.attach_children
2873 .capacity()
2874 .saturating_mul(std::mem::size_of::<AttachChildCommand>()),
2875 )
2876 .saturating_add(
2877 self.insert_children
2878 .capacity()
2879 .saturating_mul(std::mem::size_of::<InsertChildCommand>()),
2880 )
2881 .saturating_add(
2882 self.move_children
2883 .capacity()
2884 .saturating_mul(std::mem::size_of::<MoveChildCommand>()),
2885 )
2886 .saturating_add(
2887 self.remove_children
2888 .capacity()
2889 .saturating_mul(std::mem::size_of::<RemoveChildCommand>()),
2890 )
2891 .saturating_add(
2892 self.detach_children
2893 .capacity()
2894 .saturating_mul(std::mem::size_of::<DetachChildCommand>()),
2895 )
2896 .saturating_add(
2897 self.sync_children
2898 .capacity()
2899 .saturating_mul(std::mem::size_of::<SyncChildrenCommand>()),
2900 )
2901 .saturating_add(
2902 self.sync_child_ids
2903 .capacity()
2904 .saturating_mul(std::mem::size_of::<NodeId>()),
2905 )
2906 .saturating_add(
2907 self.callbacks
2908 .capacity()
2909 .saturating_mul(std::mem::size_of::<CommandCallback>()),
2910 )
2911 }
2912
2913 pub(crate) fn apply(&mut self, applier: &mut dyn Applier) -> Result<(), NodeError> {
2916 applier.begin_bubble_batch();
2917 let result = self.apply_in_order(applier);
2918 applier.end_bubble_batch();
2919 self.clear();
2920 result
2921 }
2922
2923 fn clear(&mut self) {
2924 let Self {
2925 tags,
2926 bubble_dirty,
2927 remove_nodes,
2928 mount_nodes,
2929 attach_children,
2930 insert_children,
2931 move_children,
2932 remove_children,
2933 detach_children,
2934 sync_children,
2935 sync_child_ids,
2936 callbacks,
2937 cleanup,
2938 } = self;
2939 tags.clear();
2940 bubble_dirty.clear();
2941 remove_nodes.clear();
2942 mount_nodes.clear();
2943 attach_children.clear();
2944 insert_children.clear();
2945 move_children.clear();
2946 remove_children.clear();
2947 detach_children.clear();
2948 sync_children.clear();
2949 sync_child_ids.clear();
2950 callbacks.clear();
2951 cleanup.clear();
2952 }
2953
2954 fn apply_in_order(&mut self, applier: &mut dyn Applier) -> Result<(), NodeError> {
2955 let mut payloads = CommandPayloads {
2956 bubble_dirty: self.bubble_dirty.drain(..),
2957 remove_nodes: self.remove_nodes.drain(..),
2958 mount_nodes: self.mount_nodes.drain(..),
2959 attach_children: self.attach_children.drain(..),
2960 insert_children: self.insert_children.drain(..),
2961 move_children: self.move_children.drain(..),
2962 remove_children: self.remove_children.drain(..),
2963 detach_children: self.detach_children.drain(..),
2964 sync_children: self.sync_children.drain(..),
2965 sync_child_ids: &self.sync_child_ids,
2966 callbacks: self.callbacks.drain(..),
2967 };
2968 for &tag in &self.tags {
2969 payloads.apply(tag, applier, &mut self.cleanup)?;
2970 }
2971 payloads.assert_all_taken();
2972 self.cleanup.flush(applier)
2973 }
2974}
2975
2976struct CommandPayloads<'a> {
2979 bubble_dirty: std::vec::Drain<'a, BubbleDirtyCommand>,
2980 remove_nodes: std::vec::Drain<'a, NodeId>,
2981 mount_nodes: std::vec::Drain<'a, NodeId>,
2982 attach_children: std::vec::Drain<'a, AttachChildCommand>,
2983 insert_children: std::vec::Drain<'a, InsertChildCommand>,
2984 move_children: std::vec::Drain<'a, MoveChildCommand>,
2985 remove_children: std::vec::Drain<'a, RemoveChildCommand>,
2986 detach_children: std::vec::Drain<'a, DetachChildCommand>,
2987 sync_children: std::vec::Drain<'a, SyncChildrenCommand>,
2988 sync_child_ids: &'a [NodeId],
2989 callbacks: std::vec::Drain<'a, CommandCallback>,
2990}
2991
2992impl CommandPayloads<'_> {
2993 fn apply(
2994 &mut self,
2995 tag: CommandTag,
2996 applier: &mut dyn Applier,
2997 cleanup: &mut DeferredChildCleanupQueue,
2998 ) -> Result<(), NodeError> {
2999 match tag {
3000 CommandTag::BubbleDirty
3001 | CommandTag::RemoveNode
3002 | CommandTag::MountNode
3003 | CommandTag::Callback => self.apply_node_command(tag, applier, cleanup),
3004 CommandTag::AttachChild
3005 | CommandTag::InsertChild
3006 | CommandTag::MoveChild
3007 | CommandTag::RemoveChild
3008 | CommandTag::DetachChild
3009 | CommandTag::SyncChildren => self.apply_child_command(tag, applier, cleanup),
3010 }
3011 }
3012
3013 fn apply_node_command(
3014 &mut self,
3015 tag: CommandTag,
3016 applier: &mut dyn Applier,
3017 cleanup: &mut DeferredChildCleanupQueue,
3018 ) -> Result<(), NodeError> {
3019 match tag {
3020 CommandTag::BubbleDirty => {
3021 let BubbleDirtyCommand { node_id, bubble } =
3022 next_command_payload(&mut self.bubble_dirty, tag)?;
3023 Command::BubbleDirty { node_id, bubble }.apply_with_cleanup(applier, cleanup)?;
3024 }
3025 CommandTag::RemoveNode => {
3026 let id = next_command_payload(&mut self.remove_nodes, tag)?;
3027 Command::RemoveNode { id }.apply_with_cleanup(applier, cleanup)?;
3028 }
3029 CommandTag::MountNode => {
3030 let id = next_command_payload(&mut self.mount_nodes, tag)?;
3031 Command::MountNode { id }.apply_with_cleanup(applier, cleanup)?;
3032 }
3033 CommandTag::Callback => {
3034 let callback = next_command_payload(&mut self.callbacks, tag)?;
3035 Command::Callback(callback).apply_with_cleanup(applier, cleanup)?;
3036 applier.forget_bubble_passes();
3037 }
3038 CommandTag::AttachChild
3039 | CommandTag::InsertChild
3040 | CommandTag::MoveChild
3041 | CommandTag::RemoveChild
3042 | CommandTag::DetachChild
3043 | CommandTag::SyncChildren => return Err(command_payload_error(tag)),
3044 }
3045 Ok(())
3046 }
3047
3048 fn apply_child_command(
3049 &mut self,
3050 tag: CommandTag,
3051 applier: &mut dyn Applier,
3052 cleanup: &mut DeferredChildCleanupQueue,
3053 ) -> Result<(), NodeError> {
3054 match tag {
3055 CommandTag::AttachChild => {
3056 let AttachChildCommand {
3057 parent_id,
3058 child_id,
3059 insert_index,
3060 bubble,
3061 } = next_command_payload(&mut self.attach_children, tag)?;
3062 Command::AttachChild {
3063 parent_id,
3064 child_id,
3065 insert_index,
3066 bubble,
3067 }
3068 .apply_with_cleanup(applier, cleanup)?;
3069 }
3070 CommandTag::InsertChild => {
3071 let InsertChildCommand {
3072 parent_id,
3073 child_id,
3074 appended_index,
3075 insert_index,
3076 bubble,
3077 } = next_command_payload(&mut self.insert_children, tag)?;
3078 Command::InsertChild {
3079 parent_id,
3080 child_id,
3081 appended_index,
3082 insert_index,
3083 bubble,
3084 }
3085 .apply_with_cleanup(applier, cleanup)?;
3086 }
3087 CommandTag::MoveChild => {
3088 let MoveChildCommand {
3089 parent_id,
3090 from_index,
3091 to_index,
3092 bubble,
3093 } = next_command_payload(&mut self.move_children, tag)?;
3094 Command::MoveChild {
3095 parent_id,
3096 from_index,
3097 to_index,
3098 bubble,
3099 }
3100 .apply_with_cleanup(applier, cleanup)?;
3101 }
3102 CommandTag::RemoveChild => {
3103 let RemoveChildCommand {
3104 parent_id,
3105 child_id,
3106 } = next_command_payload(&mut self.remove_children, tag)?;
3107 Command::RemoveChild {
3108 parent_id,
3109 child_id,
3110 }
3111 .apply_with_cleanup(applier, cleanup)?;
3112 }
3113 CommandTag::DetachChild => {
3114 let DetachChildCommand {
3115 parent_id,
3116 child_id,
3117 } = next_command_payload(&mut self.detach_children, tag)?;
3118 Command::DetachChild {
3119 parent_id,
3120 child_id,
3121 }
3122 .apply_with_cleanup(applier, cleanup)?;
3123 }
3124 CommandTag::SyncChildren => self.sync_children(applier, cleanup)?,
3125 CommandTag::BubbleDirty
3126 | CommandTag::RemoveNode
3127 | CommandTag::MountNode
3128 | CommandTag::Callback => return Err(command_payload_error(tag)),
3129 }
3130 Ok(())
3131 }
3132
3133 fn sync_children(
3134 &mut self,
3135 applier: &mut dyn Applier,
3136 cleanup: &mut DeferredChildCleanupQueue,
3137 ) -> Result<(), NodeError> {
3138 let tag = CommandTag::SyncChildren;
3139 let SyncChildrenCommand {
3140 parent_id,
3141 child_start,
3142 child_len,
3143 } = next_command_payload(&mut self.sync_children, tag)?;
3144 let expected_children = child_start
3145 .checked_add(child_len)
3146 .and_then(|child_end| self.sync_child_ids.get(child_start..child_end))
3147 .ok_or_else(|| command_payload_error(tag))?;
3148 sync_children(applier, parent_id, expected_children, cleanup)
3149 }
3150
3151 fn assert_all_taken(&mut self) {
3152 debug_assert!(self.bubble_dirty.next().is_none());
3153 debug_assert!(self.remove_nodes.next().is_none());
3154 debug_assert!(self.mount_nodes.next().is_none());
3155 debug_assert!(self.attach_children.next().is_none());
3156 debug_assert!(self.insert_children.next().is_none());
3157 debug_assert!(self.move_children.next().is_none());
3158 debug_assert!(self.remove_children.next().is_none());
3159 debug_assert!(self.detach_children.next().is_none());
3160 debug_assert!(self.sync_children.next().is_none());
3161 debug_assert!(self.callbacks.next().is_none());
3162 }
3163}
3164
3165fn command_payload_error(tag: CommandTag) -> NodeError {
3166 NodeError::MalformedCommandPayload { tag: tag.label() }
3167}
3168
3169fn next_command_payload<T>(
3170 payloads: &mut impl Iterator<Item = T>,
3171 tag: CommandTag,
3172) -> Result<T, NodeError> {
3173 payloads.next().ok_or_else(|| command_payload_error(tag))
3174}
3175
3176fn attach_child_at(
3177 applier: &mut dyn Applier,
3178 parent_id: NodeId,
3179 child_id: NodeId,
3180 insert_index: Option<usize>,
3181 bubble: DirtyBubble,
3182) {
3183 if insert_child_with_reparenting(applier, parent_id, child_id) {
3184 if let Some(target) = insert_index {
3185 move_attached_child_left_to(applier, parent_id, child_id, target);
3186 }
3187 bubble.apply(applier, parent_id);
3188 } else if let Ok(child) = applier.get_mut(child_id) {
3189 let dirty_bubble = DirtyBubble {
3190 layout: child.layout_dirty(),
3191 measure: child.needs_measure() || child.descendant_needs_measure(),
3192 semantics: false,
3193 };
3194 dirty_bubble.apply_to_ancestors(applier, child_id);
3195 }
3196}
3197
3198fn move_attached_child_left_to(
3199 applier: &mut dyn Applier,
3200 parent_id: NodeId,
3201 child_id: NodeId,
3202 target: usize,
3203) {
3204 let Ok(parent_node) = applier.get_mut(parent_id) else {
3205 return;
3206 };
3207 let Some(current_index) = parent_node.owned_child_index(child_id) else {
3208 return;
3209 };
3210 if target < current_index {
3211 parent_node.move_child(current_index, target);
3212 note_structural_move(parent_id, current_index, target);
3213 }
3214}
3215
3216fn insert_child_with_reparenting(
3217 applier: &mut dyn Applier,
3218 parent_id: NodeId,
3219 child_id: NodeId,
3220) -> bool {
3221 if parent_id == child_id {
3222 debug_assert_ne!(
3223 parent_id, child_id,
3224 "a node cannot be attached as its own child"
3225 );
3226 return false;
3227 }
3228
3229 let old_parent = applier
3230 .get_mut(child_id)
3231 .ok()
3232 .and_then(|node| node.parent());
3233 if old_parent != Some(parent_id) {
3234 applier.forget_bubble_passes();
3235 }
3236 if let Some(old_parent_id) = old_parent
3237 && old_parent_id != parent_id
3238 {
3239 let removed = applier
3240 .get_mut(old_parent_id)
3241 .is_ok_and(|old_parent_node| old_parent_node.remove_child(child_id));
3242 if let Ok(child_node) = applier.get_mut(child_id) {
3243 child_node.on_removed_from_parent();
3244 }
3245 if removed {
3246 DirtyBubble::LAYOUT_AND_MEASURE.apply(applier, old_parent_id);
3247 note_structural("reparent-detach", old_parent_id, child_id);
3248 applier.record_structural_change(old_parent_id);
3249 }
3250 }
3251
3252 let inserted = applier
3253 .get_mut(parent_id)
3254 .is_ok_and(|parent_node| parent_node.insert_child(child_id));
3255 if inserted {
3256 note_structural("attach", parent_id, child_id);
3257 applier.record_structural_change(parent_id);
3258 }
3259 if let Ok(child_node) = applier.get_mut(child_id) {
3260 child_node.on_attached_to_parent(parent_id);
3261 }
3262 inserted
3263}
3264
3265fn apply_remove_child(
3266 applier: &mut dyn Applier,
3267 parent_id: NodeId,
3268 child_id: NodeId,
3269 deferred_cleanup: &mut DeferredChildCleanupQueue,
3270) -> Result<(), NodeError> {
3271 detach_child_from_parent(applier, parent_id, child_id)?;
3272
3273 let generation = applier.node_generation(child_id);
3274 let removed_from_parent = if let Ok(node) = applier.get_mut(child_id) {
3275 node.parent().is_none()
3276 } else {
3277 return Ok(());
3278 };
3279 deferred_cleanup.push(child_id, generation, removed_from_parent);
3280 Ok(())
3281}
3282
3283fn detach_child_from_parent(
3284 applier: &mut dyn Applier,
3285 parent_id: NodeId,
3286 child_id: NodeId,
3287) -> Result<(), NodeError> {
3288 let removed = applier
3289 .get_mut(parent_id)
3290 .is_ok_and(|parent_node| parent_node.remove_child(child_id));
3291 if removed {
3292 DirtyBubble::LAYOUT_AND_MEASURE.apply(applier, parent_id);
3293 note_structural("detach", parent_id, child_id);
3294 applier.record_structural_change(parent_id);
3295 }
3296
3297 if let Ok(node) = applier.get_mut(child_id) {
3298 match node.parent() {
3299 Some(existing_parent_id) if existing_parent_id == parent_id => {
3300 node.on_removed_from_parent();
3301 applier.forget_bubble_passes();
3302 }
3303 None => {}
3304 Some(_) => return Ok(()),
3305 }
3306 } else {
3307 return Ok(());
3308 }
3309
3310 Ok(())
3311}
3312
3313fn cleanup_detached_child(
3314 applier: &mut dyn Applier,
3315 cleanup: DeferredChildCleanup,
3316) -> Result<(), NodeError> {
3317 if applier.node_generation(cleanup.child_id) != cleanup.generation {
3318 return Ok(());
3319 }
3320
3321 let parent_id = match applier.get_mut(cleanup.child_id) {
3322 Ok(node) => node.parent(),
3323 Err(NodeError::Missing { .. }) => return Ok(()),
3324 Err(err) => return Err(err),
3325 };
3326 if parent_id.is_some() {
3327 return Ok(());
3328 }
3329
3330 if let Ok(node) = applier.get_mut(cleanup.child_id) {
3331 if !cleanup.removed_from_parent {
3332 node.on_removed_from_parent();
3333 }
3334 node.unmount();
3335 }
3336 match applier.remove(cleanup.child_id) {
3337 Ok(()) | Err(NodeError::Missing { .. }) => Ok(()),
3338 Err(err) => Err(err),
3339 }
3340}
3341
3342fn remove_child_and_cleanup_now(
3343 applier: &mut dyn Applier,
3344 parent_id: NodeId,
3345 child_id: NodeId,
3346) -> Result<(), NodeError> {
3347 let mut deferred_cleanup = DeferredChildCleanupQueue::default();
3348 apply_remove_child(applier, parent_id, child_id, &mut deferred_cleanup)?;
3349 deferred_cleanup.flush(applier)
3350}
3351
3352fn collect_current_children(applier: &mut dyn Applier, parent_id: NodeId) -> ChildList {
3353 let mut scratch = SmallVec::<[NodeId; 8]>::new();
3354 if let Ok(node) = applier.get_mut(parent_id) {
3355 node.collect_children_into(&mut scratch);
3356 }
3357 let mut current = ChildList::new();
3358 current.extend(scratch);
3359 current
3360}
3361
3362fn sync_children(
3363 applier: &mut dyn Applier,
3364 parent_id: NodeId,
3365 expected_children: &[NodeId],
3366 deferred_cleanup: &mut DeferredChildCleanupQueue,
3367) -> Result<(), NodeError> {
3368 let mut current = collect_current_children(applier, parent_id);
3369 let children_changed = current.as_slice() != expected_children;
3370
3371 if children_changed {
3372 if current.len().max(expected_children.len()) <= SMALL_CHILD_SYNC_LINEAR_THRESHOLD {
3373 sync_children_small(
3374 applier,
3375 parent_id,
3376 &mut current,
3377 expected_children,
3378 deferred_cleanup,
3379 )?;
3380 } else {
3381 let mut target_positions: HashMap<NodeId, usize> = HashMap::default();
3382 target_positions.reserve(expected_children.len());
3383 for (index, &child) in expected_children.iter().enumerate() {
3384 target_positions.insert(child, index);
3385 }
3386
3387 for index in (0..current.len()).rev() {
3388 let child = current[index];
3389 if !target_positions.contains_key(&child) {
3390 current.remove(index);
3391 apply_remove_child(applier, parent_id, child, deferred_cleanup)?;
3392 }
3393 }
3394
3395 let mut current_positions = build_child_positions(¤t);
3396 for (target_index, &child) in expected_children.iter().enumerate() {
3397 if let Some(current_index) = current_positions.get(&child).copied() {
3398 if current_index != target_index {
3399 let from_index = current_index;
3400 let to_index = move_child_in_diff_state(
3401 &mut current,
3402 &mut current_positions,
3403 from_index,
3404 target_index,
3405 );
3406 Command::MoveChild {
3407 parent_id,
3408 from_index,
3409 to_index,
3410 bubble: DirtyBubble::LAYOUT_AND_MEASURE,
3411 }
3412 .apply(applier)?;
3413 }
3414 } else {
3415 let insert_index = target_index.min(current.len());
3416 let appended_index = current.len();
3417 insert_child_into_diff_state(
3418 &mut current,
3419 &mut current_positions,
3420 insert_index,
3421 child,
3422 );
3423 Command::InsertChild {
3424 parent_id,
3425 child_id: child,
3426 appended_index,
3427 insert_index,
3428 bubble: DirtyBubble::LAYOUT_AND_MEASURE,
3429 }
3430 .apply(applier)?;
3431 }
3432 }
3433 }
3434 }
3435
3436 reconcile_children(applier, parent_id, expected_children, !children_changed)
3437}
3438
3439fn sync_children_small(
3440 applier: &mut dyn Applier,
3441 parent_id: NodeId,
3442 current: &mut ChildList,
3443 expected_children: &[NodeId],
3444 deferred_cleanup: &mut DeferredChildCleanupQueue,
3445) -> Result<(), NodeError> {
3446 for index in (0..current.len()).rev() {
3447 let child = current[index];
3448 if !expected_children.contains(&child) {
3449 current.remove(index);
3450 apply_remove_child(applier, parent_id, child, deferred_cleanup)?;
3451 }
3452 }
3453
3454 for (target_index, &child) in expected_children.iter().enumerate() {
3455 if let Some(current_index) = current
3456 .iter()
3457 .position(|¤t_child| current_child == child)
3458 {
3459 if current_index != target_index {
3460 let child = current.remove(current_index);
3461 let to_index = target_index.min(current.len());
3462 current.insert(to_index, child);
3463 Command::MoveChild {
3464 parent_id,
3465 from_index: current_index,
3466 to_index,
3467 bubble: DirtyBubble::LAYOUT_AND_MEASURE,
3468 }
3469 .apply(applier)?;
3470 }
3471 } else {
3472 let insert_index = target_index.min(current.len());
3473 let appended_index = current.len();
3474 current.insert(insert_index, child);
3475 Command::InsertChild {
3476 parent_id,
3477 child_id: child,
3478 appended_index,
3479 insert_index,
3480 bubble: DirtyBubble::LAYOUT_AND_MEASURE,
3481 }
3482 .apply(applier)?;
3483 }
3484 }
3485
3486 Ok(())
3487}
3488
3489fn reconcile_children(
3494 applier: &mut dyn Applier,
3495 parent_id: NodeId,
3496 expected_children: &[NodeId],
3497 children_unchanged: bool,
3498) -> Result<(), NodeError> {
3499 let mut repaired = false;
3500 if children_unchanged {
3501 debug_assert!(
3502 expected_children.iter().all(|&child_id| applier
3503 .get_mut(child_id)
3504 .map_or(true, |node| node.parent().is_none_or(|p| p == parent_id))),
3505 "the children a parent holds must name no other parent"
3506 );
3507 } else {
3508 for &child_id in expected_children {
3509 let needs_attach = applier
3510 .get_mut(child_id)
3511 .is_ok_and(|node| node.parent() != Some(parent_id));
3512 if needs_attach {
3513 insert_child_with_reparenting(applier, parent_id, child_id);
3514 repaired = true;
3515 }
3516 }
3517 }
3518
3519 let is_dirty = children_unchanged
3520 && applier
3521 .get_mut(parent_id)
3522 .is_ok_and(|node| node.layout_dirty());
3523
3524 if repaired {
3525 DirtyBubble::LAYOUT_AND_MEASURE.apply(applier, parent_id);
3526 } else if is_dirty {
3527 DirtyBubble::LAYOUT.apply_to_ancestors(applier, parent_id);
3528 }
3529
3530 Ok(())
3531}
3532
3533#[derive(Default)]
3536pub struct MemoryApplier {
3537 state: Box<MemoryApplierState>,
3540}
3541
3542#[derive(Default)]
3543struct MemoryApplierState {
3544 nodes: Vec<Option<Box<dyn Node>>>,
3545 physical_stable_ids: Vec<u32>,
3546 physical_warm_recycled_origins: Vec<bool>,
3547 stable_index: stable_index::StableIndex,
3548 free_ids: BinaryHeap<Reverse<usize>>,
3549 high_id_nodes: HashMap<NodeId, Box<dyn Node>>,
3550 high_id_warm_recycled_origins: HashMap<NodeId, bool>,
3551 high_id_generations: HashMap<NodeId, u32>,
3552 next_stable_id: NodeId,
3553 layout_runtime: Option<RuntimeHandle>,
3554 slots: Rc<RefCell<SlotTable>>,
3555 recycled_nodes: HashMap<TypeId, Vec<RecycledNode>>,
3556 returning_recycled_nodes: HashMap<TypeId, Vec<RecycledNode>>,
3557 cold_recycled_nodes: HashMap<TypeId, Vec<RecycledNode>>,
3558 recycled_node_limits: HashMap<TypeId, usize>,
3559 warm_recycled_node_targets: HashMap<TypeId, usize>,
3560 fresh_recyclable_creations: HashMap<TypeId, usize>,
3561 recycled_node_prototypes: HashMap<TypeId, Box<dyn Node>>,
3562 structural_change_parents: Vec<NodeId>,
3563 virtual_node_ids: HashSet<NodeId>,
3564 bubble_passes: bubble_passes::BubblePasses,
3565}
3566
3567#[derive(Default)]
3572pub struct SceneNodeAttachmentScratch {
3573 attached: NodeMarks,
3574 path: Vec<NodeId>,
3575}
3576
3577const ATTACHED: u8 = 1;
3578const DETACHED: u8 = 2;
3579
3580struct RemovalFrame {
3581 node_id: NodeId,
3582 children: SmallVec<[NodeId; 8]>,
3583 next_child: usize,
3584}
3585
3586#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
3589pub struct MemoryApplierDebugStats {
3590 pub next_stable_id: NodeId,
3592 pub nodes_len: usize,
3594 pub nodes_cap: usize,
3596 pub physical_stable_ids_len: usize,
3598 pub physical_stable_ids_cap: usize,
3600 pub stable_to_physical_len: usize,
3602 pub stable_to_physical_cap: usize,
3604 pub stable_generations_len: usize,
3606 pub stable_generations_cap: usize,
3608 pub free_ids_len: usize,
3610 pub free_ids_cap: usize,
3612 pub high_id_nodes_len: usize,
3614 pub high_id_nodes_cap: usize,
3616 pub high_id_generations_len: usize,
3618 pub high_id_generations_cap: usize,
3620 pub recycled_type_count: usize,
3622 pub recycled_type_cap: usize,
3624 pub recycled_node_count: usize,
3626 pub recycled_node_capacity: usize,
3628 pub warm_recycled_node_id_count: usize,
3630 pub warm_recycled_node_id_capacity: usize,
3632}
3633
3634impl MemoryApplier {
3635 const EAGER_COMPACT_NODE_LEN: usize = 1_024;
3636 const HIGH_ID_THRESHOLD: NodeId = 1_000_000_000;
3637 const INVALID_STABLE_ID: u32 = u32::MAX;
3638 const INITIAL_DENSE_NODE_CAP: usize = 32;
3639 const LARGE_DENSE_NODE_GROWTH_THRESHOLD: usize = 32 * 1024;
3640 const LARGE_DENSE_NODE_GROWTH_DIVISOR: usize = 4;
3641
3642 fn pack_stable_id(stable_id: NodeId) -> u32 {
3643 u32::try_from(stable_id).expect("stable id overflow")
3644 }
3645
3646 fn unpack_stable_id(stable_id: u32) -> NodeId {
3647 stable_id as NodeId
3648 }
3649
3650 fn next_dense_node_target_len(old_len: usize) -> usize {
3651 if old_len < Self::INITIAL_DENSE_NODE_CAP {
3652 return Self::INITIAL_DENSE_NODE_CAP;
3653 }
3654 if old_len < Self::LARGE_DENSE_NODE_GROWTH_THRESHOLD {
3655 return old_len.saturating_mul(2);
3656 }
3657
3658 let incremental_growth =
3659 (old_len / Self::LARGE_DENSE_NODE_GROWTH_DIVISOR).max(Self::INITIAL_DENSE_NODE_CAP);
3660 old_len.saturating_add(incremental_growth)
3661 }
3662
3663 fn ensure_dense_node_storage_capacity(&mut self) {
3664 let len = self
3665 .state
3666 .nodes
3667 .len()
3668 .max(self.state.physical_stable_ids.len())
3669 .max(self.state.physical_warm_recycled_origins.len());
3670 if len < self.state.nodes.capacity()
3671 && len < self.state.physical_stable_ids.capacity()
3672 && len < self.state.physical_warm_recycled_origins.capacity()
3673 {
3674 return;
3675 }
3676
3677 let target = Self::next_dense_node_target_len(len);
3678 if self.state.nodes.capacity() < target {
3679 self.state
3680 .nodes
3681 .reserve_exact(target.saturating_sub(self.state.nodes.len()));
3682 }
3683 if self.state.physical_stable_ids.capacity() < target {
3684 self.state
3685 .physical_stable_ids
3686 .reserve_exact(target.saturating_sub(self.state.physical_stable_ids.len()));
3687 }
3688 if self.state.physical_warm_recycled_origins.capacity() < target {
3689 self.state.physical_warm_recycled_origins.reserve_exact(
3690 target.saturating_sub(self.state.physical_warm_recycled_origins.len()),
3691 );
3692 }
3693 }
3694
3695 pub fn new() -> Self {
3697 Self::default()
3698 }
3699
3700 pub fn slots_handle(&self) -> Rc<RefCell<SlotTable>> {
3703 Rc::clone(&self.state.slots)
3704 }
3705
3706 pub fn scene_node_attached_to(&mut self, node_id: NodeId, root: NodeId) -> Option<NodeId> {
3712 let resolved = self.first_non_virtual_ancestor(node_id)?;
3713 self.is_attached_to(resolved, root).then_some(resolved)
3714 }
3715
3716 pub fn scene_nodes_attached_to(
3720 &mut self,
3721 nodes: impl IntoIterator<Item = NodeId>,
3722 root: NodeId,
3723 ) -> Vec<Option<NodeId>> {
3724 let mut result = Vec::new();
3725 self.scene_nodes_attached_to_into(
3726 nodes,
3727 root,
3728 &mut result,
3729 &mut SceneNodeAttachmentScratch::default(),
3730 );
3731 result
3732 }
3733
3734 pub fn scene_nodes_attached_to_into(
3752 &mut self,
3753 nodes: impl IntoIterator<Item = NodeId>,
3754 root: NodeId,
3755 output: &mut Vec<Option<NodeId>>,
3756 scratch: &mut SceneNodeAttachmentScratch,
3757 ) {
3758 let nodes = nodes.into_iter();
3759 output.clear();
3760 let (lower_bound, upper_bound) = nodes.size_hint();
3761 if lower_bound == 0 && upper_bound == Some(0) {
3762 scratch.path.clear();
3763 return;
3764 }
3765 scratch.attached.reset(self);
3766 scratch.attached.set(self, root, ATTACHED);
3767 output.reserve(lower_bound);
3768 scratch.path.clear();
3769 for node_id in nodes {
3770 let Some(resolved) = self.first_non_virtual_ancestor(node_id) else {
3771 output.push(None);
3772 continue;
3773 };
3774 let mut current = resolved;
3775 scratch.path.clear();
3776 let answer = loop {
3777 match scratch.attached.get(self, current) {
3778 ATTACHED => break true,
3779 DETACHED => break false,
3780 _ => {}
3781 }
3782 scratch.path.push(current);
3783 match self.get_mut(current).ok().and_then(|node| node.parent()) {
3784 Some(parent) if scratch.path.len() < 100_000 => current = parent,
3785 _ => break false,
3786 }
3787 };
3788 let mark = if answer { ATTACHED } else { DETACHED };
3789 for visited in scratch.path.drain(..) {
3790 scratch.attached.set(self, visited, mark);
3791 }
3792 output.push(answer.then_some(resolved));
3793 }
3794 }
3795
3796 pub fn take_structural_change_parents_attached_to(&mut self, root: NodeId) -> Vec<NodeId> {
3804 let mut candidates = Vec::new();
3805 self.take_structural_change_parents_into(&mut candidates);
3806 let mut seen = HashSet::<NodeId>::default();
3807 candidates.retain_mut(|parent_id| {
3808 let Some(resolved) = self.first_non_virtual_ancestor(*parent_id) else {
3809 return false;
3810 };
3811 *parent_id = resolved;
3812 self.is_attached_to(resolved, root) && seen.insert(resolved)
3813 });
3814 candidates
3815 }
3816
3817 pub fn take_structural_change_parents_into(&mut self, output: &mut Vec<NodeId>) {
3830 output.clear();
3831 std::mem::swap(&mut self.state.structural_change_parents, output);
3832 }
3833
3834 fn first_non_virtual_ancestor(&mut self, node_id: NodeId) -> Option<NodeId> {
3835 if self.state.virtual_node_ids.is_empty() {
3836 return Some(node_id);
3837 }
3838 let mut current = node_id;
3839 for _ in 0..100_000 {
3840 if !self.state.virtual_node_ids.contains(¤t) {
3841 return Some(current);
3842 }
3843 match self.get_mut(current) {
3844 Ok(node) => current = node.parent()?,
3845 Err(_) => return None,
3846 }
3847 }
3848 None
3849 }
3850
3851 fn is_attached_to(&mut self, node_id: NodeId, root: NodeId) -> bool {
3852 let mut current = node_id;
3853 for _ in 0..100_000 {
3854 if current == root {
3855 return true;
3856 }
3857 match self.get_mut(current) {
3858 Ok(node) => match node.parent() {
3859 Some(parent) => current = parent,
3860 None => return false,
3861 },
3862 Err(_) => return false,
3863 }
3864 }
3865 false
3866 }
3867
3868 pub fn with_node<N: Node + 'static, R>(
3871 &mut self,
3872 id: NodeId,
3873 f: impl FnOnce(&mut N) -> R,
3874 ) -> Result<R, NodeError> {
3875 let typed = self
3876 .get_mut(id)?
3877 .as_any_mut()
3878 .downcast_mut::<N>()
3879 .ok_or_else(|| NodeError::TypeMismatch {
3880 id,
3881 expected: std::any::type_name::<N>(),
3882 })?;
3883 Ok(f(typed))
3884 }
3885
3886 pub fn len(&self) -> usize {
3888 self.state.nodes.iter().filter(|n| n.is_some()).count()
3889 }
3890
3891 pub fn capacity(&self) -> usize {
3893 self.state.nodes.len()
3894 }
3895
3896 pub fn tombstone_count(&self) -> usize {
3898 self.state.nodes.iter().filter(|n| n.is_none()).count()
3899 }
3900
3901 pub fn freelist_len(&self) -> usize {
3903 self.state.free_ids.len()
3904 }
3905
3906 pub fn debug_recycled_node_count(&self) -> usize {
3908 self.total_recycled_node_count()
3909 }
3910
3911 pub fn debug_recycled_node_count_for<N: Node + 'static>(&self) -> usize {
3913 let key = TypeId::of::<N>();
3914 self.state.recycled_nodes.get(&key).map_or(0, Vec::len)
3915 + self
3916 .state
3917 .returning_recycled_nodes
3918 .get(&key)
3919 .map_or(0, Vec::len)
3920 + self.state.cold_recycled_nodes.get(&key).map_or(0, Vec::len)
3921 }
3922
3923 pub fn debug_stats(&self) -> MemoryApplierDebugStats {
3925 let mut recycled_keys: HashSet<TypeId> = HashSet::default();
3926 recycled_keys.extend(self.state.recycled_nodes.keys().copied());
3927 recycled_keys.extend(self.state.returning_recycled_nodes.keys().copied());
3928 recycled_keys.extend(self.state.cold_recycled_nodes.keys().copied());
3929
3930 MemoryApplierDebugStats {
3931 next_stable_id: self.state.next_stable_id,
3932 nodes_len: self.len(),
3933 nodes_cap: self.state.nodes.len(),
3934 physical_stable_ids_len: self.state.physical_stable_ids.len(),
3935 physical_stable_ids_cap: self.state.physical_stable_ids.capacity(),
3936 stable_to_physical_len: self.state.stable_index.live(),
3937 stable_to_physical_cap: self.state.stable_index.capacity(),
3938 stable_generations_len: self.state.stable_index.occupied(),
3939 stable_generations_cap: self.state.stable_index.capacity(),
3940 free_ids_len: self.state.free_ids.len(),
3941 free_ids_cap: self.state.free_ids.capacity(),
3942 high_id_nodes_len: self.state.high_id_nodes.len(),
3943 high_id_nodes_cap: self.state.high_id_nodes.capacity(),
3944 high_id_generations_len: self.state.high_id_generations.len(),
3945 high_id_generations_cap: self.state.high_id_generations.capacity(),
3946 recycled_type_count: recycled_keys.len(),
3947 recycled_type_cap: self.state.recycled_nodes.capacity()
3948 + self.state.returning_recycled_nodes.capacity()
3949 + self.state.cold_recycled_nodes.capacity(),
3950 recycled_node_count: self.total_recycled_node_count(),
3951 recycled_node_capacity: self.total_recycled_node_capacity(),
3952 warm_recycled_node_id_count: self.total_warm_recycled_node_id_count(),
3953 warm_recycled_node_id_capacity: self.total_warm_recycled_node_id_capacity(),
3954 }
3955 }
3956
3957 pub fn is_empty(&self) -> bool {
3959 self.len() == 0
3960 }
3961
3962 pub fn for_each_node_mut(&mut self, mut visit: impl FnMut(&mut dyn Node)) {
3964 for node in self.state.nodes.iter_mut().flatten() {
3965 visit(node.as_mut());
3966 }
3967 for node in self.state.high_id_nodes.values_mut() {
3968 visit(node.as_mut());
3969 }
3970 }
3971
3972 pub fn debug_live_node_heap_bytes(&self) -> usize {
3974 let dense_nodes = self
3975 .state
3976 .nodes
3977 .iter()
3978 .flatten()
3979 .map(|node| std::mem::size_of_val(&**node) + node.debug_heap_bytes())
3980 .sum::<usize>();
3981 let high_id_nodes = self
3982 .state
3983 .high_id_nodes
3984 .values()
3985 .map(|node| std::mem::size_of_val(&**node) + node.debug_heap_bytes())
3986 .sum::<usize>();
3987 dense_nodes + high_id_nodes
3988 }
3989
3990 pub fn debug_recycled_node_heap_bytes(&self) -> usize {
3992 let pool_bytes = |pools: &HashMap<TypeId, Vec<RecycledNode>>| {
3993 pools
3994 .values()
3995 .flat_map(|nodes| nodes.iter())
3996 .map(|node| std::mem::size_of_val(&*node.node) + node.node.debug_heap_bytes())
3997 .sum::<usize>()
3998 };
3999
4000 pool_bytes(&self.state.recycled_nodes)
4001 + pool_bytes(&self.state.returning_recycled_nodes)
4002 + pool_bytes(&self.state.cold_recycled_nodes)
4003 }
4004
4005 pub fn set_runtime_handle(&mut self, handle: RuntimeHandle) {
4008 self.state.layout_runtime = Some(handle);
4009 }
4010
4011 pub fn clear_runtime_handle(&mut self) {
4013 self.state.layout_runtime = None;
4014 }
4015
4016 pub fn runtime_handle(&self) -> Option<RuntimeHandle> {
4018 self.state.layout_runtime.clone()
4019 }
4020
4021 fn pool_node_count(pools: &HashMap<TypeId, Vec<RecycledNode>>) -> usize {
4022 pools.values().map(Vec::len).sum()
4023 }
4024
4025 fn pool_node_capacity(pools: &HashMap<TypeId, Vec<RecycledNode>>) -> usize {
4026 pools.values().map(Vec::capacity).sum()
4027 }
4028
4029 fn total_recycled_node_count(&self) -> usize {
4030 Self::pool_node_count(&self.state.recycled_nodes)
4031 + Self::pool_node_count(&self.state.returning_recycled_nodes)
4032 + Self::pool_node_count(&self.state.cold_recycled_nodes)
4033 }
4034
4035 fn total_recycled_node_capacity(&self) -> usize {
4036 Self::pool_node_capacity(&self.state.recycled_nodes)
4037 + Self::pool_node_capacity(&self.state.returning_recycled_nodes)
4038 + Self::pool_node_capacity(&self.state.cold_recycled_nodes)
4039 }
4040
4041 fn total_warm_recycled_node_id_count(&self) -> usize {
4042 self.live_warm_recycled_origin_count()
4043 + Self::pool_node_count(&self.state.recycled_nodes)
4044 + Self::pool_node_count(&self.state.returning_recycled_nodes)
4045 }
4046
4047 fn total_warm_recycled_node_id_capacity(&self) -> usize {
4048 self.live_warm_recycled_origin_capacity()
4049 + Self::pool_node_capacity(&self.state.recycled_nodes)
4050 + Self::pool_node_capacity(&self.state.returning_recycled_nodes)
4051 }
4052
4053 fn remember_recycle_pool_limit(&mut self, key: TypeId, recycle_pool_limit: Option<usize>) {
4054 if let Some(limit) = recycle_pool_limit {
4055 self.state.recycled_node_limits.insert(key, limit);
4056 } else {
4057 self.state.recycled_node_limits.remove(&key);
4058 }
4059 }
4060
4061 fn recycle_pool_limit_for(&self, key: TypeId) -> Option<usize> {
4062 self.state.recycled_node_limits.get(&key).copied()
4063 }
4064
4065 fn warm_recycled_pool_len(&self, key: TypeId) -> usize {
4066 self.state.recycled_nodes.get(&key).map_or(0, Vec::len)
4067 }
4068
4069 fn warm_recycled_node_target(&self, key: TypeId) -> usize {
4070 self.state
4071 .warm_recycled_node_targets
4072 .get(&key)
4073 .copied()
4074 .unwrap_or(0)
4075 }
4076
4077 fn warm_recycled_node_target_limit(&self, key: TypeId) -> usize {
4078 let Some(limit) = self.recycle_pool_limit_for(key) else {
4079 return usize::MAX;
4080 };
4081 if limit <= 8 { limit } else { limit / 4 }
4082 }
4083
4084 fn update_warm_recycled_node_target(&mut self, key: TypeId, observed_demand: usize) -> usize {
4085 let target_limit = self.warm_recycled_node_target_limit(key);
4086 let existing = self.warm_recycled_node_target(key).min(target_limit);
4087 if observed_demand == 0 {
4088 return existing;
4089 }
4090
4091 let target = match self.recycle_pool_limit_for(key) {
4092 Some(limit) if limit > 8 => target_limit,
4093 Some(_) => observed_demand.min(target_limit),
4094 None => observed_demand,
4095 };
4096 self.state.warm_recycled_node_targets.insert(key, target);
4097 target
4098 }
4099
4100 fn remember_recycled_node_prototype(&mut self, key: TypeId, shell: &dyn Node) {
4101 if self.state.recycled_node_prototypes.contains_key(&key) {
4102 return;
4103 }
4104 if let Some(prototype) = shell.rehouse_for_recycle() {
4105 self.state.recycled_node_prototypes.insert(key, prototype);
4106 }
4107 }
4108
4109 fn live_warm_recycled_origin_count(&self) -> usize {
4110 self.state
4111 .physical_warm_recycled_origins
4112 .iter()
4113 .zip(self.state.nodes.iter())
4114 .filter(|(warm_origin, node)| **warm_origin && node.is_some())
4115 .count()
4116 + self
4117 .state
4118 .high_id_warm_recycled_origins
4119 .values()
4120 .filter(|warm_origin| **warm_origin)
4121 .count()
4122 }
4123
4124 fn live_warm_recycled_origin_capacity(&self) -> usize {
4125 self.state.physical_warm_recycled_origins.capacity()
4126 + self.state.high_id_warm_recycled_origins.capacity()
4127 }
4128
4129 fn push_recycled_node(
4130 &mut self,
4131 key: TypeId,
4132 recycle_pool_limit: Option<usize>,
4133 recycled: RecycledNode,
4134 ) {
4135 self.remember_recycle_pool_limit(key, recycle_pool_limit);
4136 self.remember_recycled_node_prototype(key, recycled.node.as_ref());
4137
4138 let warm_origin = recycled.warm_origin();
4139 let pool = if warm_origin {
4140 self.state.returning_recycled_nodes.entry(key).or_default()
4141 } else {
4142 self.state.cold_recycled_nodes.entry(key).or_default()
4143 };
4144 pool.push(recycled);
4145 if let Some(limit) = recycle_pool_limit
4146 && pool.len() > limit
4147 {
4148 let excess = pool.len() - limit;
4149 let dropped: Vec<_> = pool.drain(0..excess).collect();
4150 drop(dropped);
4151 }
4152 }
4153
4154 fn push_warm_recycled_node(
4155 &mut self,
4156 key: TypeId,
4157 recycle_pool_limit: Option<usize>,
4158 mut recycled: RecycledNode,
4159 ) {
4160 self.remember_recycle_pool_limit(key, recycle_pool_limit);
4161
4162 recycled.set_warm_origin(true);
4163 let mut dropped = Vec::new();
4164 let mut remove_pool_entry = false;
4165 {
4166 let pool = self.state.recycled_nodes.entry(key).or_default();
4167 pool.push(recycled);
4168 if let Some(limit) = recycle_pool_limit
4169 && pool.len() > limit
4170 {
4171 let excess = pool.len() - limit;
4172 dropped = pool.drain(0..excess).collect();
4173 remove_pool_entry = pool.is_empty();
4174 }
4175 }
4176 if remove_pool_entry {
4177 self.state.recycled_nodes.remove(&key);
4178 }
4179 drop(dropped);
4180 }
4181
4182 fn seed_recycled_node_shell_impl(
4183 &mut self,
4184 key: TypeId,
4185 recycle_pool_limit: Option<usize>,
4186 shell: Box<dyn Node>,
4187 ) {
4188 let limit = recycle_pool_limit.unwrap_or(usize::MAX);
4189 if self.warm_recycled_pool_len(key) >= limit {
4190 return;
4191 }
4192
4193 self.remember_recycled_node_prototype(key, shell.as_ref());
4194 let stable_id = self.state.next_stable_id;
4195 self.state.next_stable_id = self.state.next_stable_id.saturating_add(1);
4196 self.push_warm_recycled_node(
4197 key,
4198 recycle_pool_limit,
4199 RecycledNode::from_shell(stable_id, shell, true),
4200 );
4201 }
4202
4203 fn take_recycled_node_from_pool(
4204 pools: &mut HashMap<TypeId, Vec<RecycledNode>>,
4205 key: TypeId,
4206 ) -> Option<RecycledNode> {
4207 let pool = pools.get_mut(&key)?;
4208 let node = pool.pop();
4209 if pool.is_empty() {
4210 pools.remove(&key);
4211 }
4212 node
4213 }
4214
4215 fn compact_idle_warm_pool(&mut self, key: TypeId) {
4216 let Some(pool) = self.state.recycled_nodes.get_mut(&key) else {
4217 return;
4218 };
4219 if pool.capacity() <= pool.len().saturating_mul(4).max(64) {
4220 return;
4221 }
4222
4223 let retained = pool.len();
4224 let mut compacted = Vec::with_capacity(retained);
4225 compacted.append(pool);
4226 let remove_pool_entry = compacted.is_empty();
4227 *pool = compacted;
4228 let _ = pool;
4229
4230 if remove_pool_entry {
4231 self.state.recycled_nodes.remove(&key);
4232 }
4233 }
4234
4235 fn trim_idle_warm_pool_to_target(&mut self, key: TypeId, target: usize) {
4236 let pool_len = self.warm_recycled_pool_len(key);
4237 if pool_len <= target {
4238 return;
4239 }
4240
4241 let Some(pool) = self.state.recycled_nodes.get_mut(&key) else {
4242 return;
4243 };
4244 let removable = (pool_len - target).min(pool.len());
4245 let dropped: Vec<_> = pool.drain(0..removable).collect();
4246 let remove_pool_entry = pool.is_empty();
4247 let _ = pool;
4248
4249 if remove_pool_entry {
4250 self.state.recycled_nodes.remove(&key);
4251 }
4252 drop(dropped);
4253 }
4254
4255 fn replenish_warm_pool_to_target(&mut self, key: TypeId, target: usize) {
4256 let missing = target.saturating_sub(self.warm_recycled_pool_len(key));
4257 if missing == 0 {
4258 return;
4259 }
4260
4261 let recycle_pool_limit = self.recycle_pool_limit_for(key);
4262 let mut shells = Vec::with_capacity(missing);
4263 if let Some(prototype) = self.state.recycled_node_prototypes.get(&key) {
4264 for _ in 0..missing {
4265 let Some(shell) = prototype.rehouse_for_recycle() else {
4266 break;
4267 };
4268 shells.push(shell);
4269 }
4270 }
4271
4272 for shell in shells {
4273 self.seed_recycled_node_shell_impl(key, recycle_pool_limit, shell);
4274 }
4275 }
4276
4277 fn prune_stable_generations(&mut self) {
4278 let retained_len = self.state.stable_index.live() + self.total_recycled_node_count();
4279 if retained_len == self.state.stable_index.occupied() {
4280 return;
4281 }
4282
4283 let recycled: HashSet<NodeId> = [
4284 &self.state.recycled_nodes,
4285 &self.state.returning_recycled_nodes,
4286 &self.state.cold_recycled_nodes,
4287 ]
4288 .into_iter()
4289 .flat_map(|pools| pools.values().flatten().map(RecycledNode::stable_id))
4290 .collect();
4291 self.state
4292 .stable_index
4293 .retain_retired(|stable_id| recycled.contains(&stable_id));
4294 }
4295
4296 pub fn dump_tree(&self, root: Option<NodeId>) -> String {
4299 let mut output = String::new();
4300 if let Some(root_id) = root {
4301 self.dump_node(&mut output, root_id, 0);
4302 } else {
4303 output.push_str("(no root)\n");
4304 }
4305 output
4306 }
4307
4308 fn dump_node(&self, output: &mut String, id: NodeId, depth: usize) {
4309 let indent = " ".repeat(depth);
4310 let Ok(node) = self.get_ref(id) else {
4311 if let Some(physical_id) = self.resolve_node_index(id) {
4312 output.push_str(&format!(
4313 "{indent}[{id}] (missing physical node {physical_id})\n"
4314 ));
4315 } else {
4316 output.push_str(&format!("{indent}[{id}] (missing)\n"));
4317 }
4318 return;
4319 };
4320 let type_name = std::any::type_name_of_val(node);
4321 output.push_str(&format!("{indent}[{id}] {type_name}\n"));
4322
4323 let mut children = SmallVec::<[NodeId; 8]>::new();
4324 node.collect_children_into(&mut children);
4325 for child_id in children {
4326 self.dump_node(output, child_id, depth + 1);
4327 }
4328 }
4329
4330 fn resolve_node_index(&self, id: NodeId) -> Option<usize> {
4331 self.state.stable_index.slot(id)
4332 }
4333
4334 fn contains_node_id(&self, id: NodeId) -> bool {
4335 self.resolve_node_index(id).is_some() || self.state.high_id_nodes.contains_key(&id)
4336 }
4337
4338 fn insert_high_id_node(&mut self, stable_id: NodeId, node: Box<dyn Node>, warm_origin: bool) {
4339 self.state.high_id_nodes.insert(stable_id, node);
4340 self.state
4341 .high_id_warm_recycled_origins
4342 .insert(stable_id, warm_origin);
4343 self.state.high_id_generations.entry(stable_id).or_insert(0);
4344 }
4345
4346 fn insert_available_with_id(&mut self, stable_id: NodeId, node: Box<dyn Node>) {
4347 if stable_id >= Self::HIGH_ID_THRESHOLD {
4348 self.insert_high_id_node(stable_id, node, false);
4349 return;
4350 }
4351
4352 let physical_id = if let Some(Reverse(free_physical_id)) = self.state.free_ids.pop() {
4353 self.state.nodes[free_physical_id] = Some(node);
4354 self.state.physical_stable_ids[free_physical_id] = Self::pack_stable_id(stable_id);
4355 self.state.physical_warm_recycled_origins[free_physical_id] = false;
4356 free_physical_id
4357 } else {
4358 self.ensure_dense_node_storage_capacity();
4359 let physical_id = self.state.nodes.len();
4360 self.state.nodes.push(Some(node));
4361 self.state
4362 .physical_stable_ids
4363 .push(Self::pack_stable_id(stable_id));
4364 self.state.physical_warm_recycled_origins.push(false);
4365 physical_id
4366 };
4367
4368 self.state.next_stable_id = self.state.next_stable_id.max(stable_id.saturating_add(1));
4369 self.state.physical_stable_ids[physical_id] = Self::pack_stable_id(stable_id);
4370 self.state.stable_index.set_slot(stable_id, physical_id);
4371 }
4372
4373 pub fn get_ref(&self, id: NodeId) -> Result<&dyn Node, NodeError> {
4376 if let Some(physical_id) = self.resolve_node_index(id) {
4377 let slot = self
4378 .state
4379 .nodes
4380 .get(physical_id)
4381 .ok_or(NodeError::Missing { id })?
4382 .as_deref()
4383 .ok_or(NodeError::Missing { id })?;
4384 return Ok(slot);
4385 }
4386
4387 self.state
4388 .high_id_nodes
4389 .get(&id)
4390 .map(AsRef::as_ref)
4391 .ok_or(NodeError::Missing { id })
4392 }
4393
4394 fn node_parent(&self, id: NodeId) -> Result<Option<NodeId>, NodeError> {
4395 Ok(self.get_ref(id)?.parent())
4396 }
4397
4398 fn collect_owned_children(
4399 &self,
4400 node_id: NodeId,
4401 out: &mut SmallVec<[NodeId; 8]>,
4402 ) -> Result<(), NodeError> {
4403 self.get_ref(node_id)?.collect_owned_children_into(out);
4404 out.retain(|child_id| {
4405 self.node_parent(*child_id)
4406 .is_ok_and(|parent| parent == Some(node_id))
4407 });
4408 Ok(())
4409 }
4410
4411 fn remove_node_storage(&mut self, node_id: NodeId) -> Result<(), NodeError> {
4412 self.state.virtual_node_ids.remove(&node_id);
4413 if self.state.high_id_nodes.contains_key(&node_id) {
4414 if let Some(node) = self.state.high_id_nodes.remove(&node_id)
4415 && let Some(key) = node.recycle_key()
4416 {
4417 let recycle_pool_limit = node.recycle_pool_limit();
4418 let warm_origin = self
4419 .state
4420 .high_id_warm_recycled_origins
4421 .remove(&node_id)
4422 .unwrap_or(false);
4423 self.push_recycled_node(
4424 key,
4425 recycle_pool_limit,
4426 RecycledNode::new(node_id, node, warm_origin),
4427 );
4428 }
4429 let generation = self.state.high_id_generations.entry(node_id).or_insert(0);
4430 *generation = generation.wrapping_add(1);
4431 return Ok(());
4432 }
4433
4434 let physical_id = self
4435 .resolve_node_index(node_id)
4436 .ok_or(NodeError::Missing { id: node_id })?;
4437 if let Some(node) = self.state.nodes[physical_id].take()
4438 && let Some(key) = node.recycle_key()
4439 {
4440 let recycle_pool_limit = node.recycle_pool_limit();
4441 let warm_origin = self
4442 .state
4443 .physical_warm_recycled_origins
4444 .get_mut(physical_id)
4445 .is_some_and(std::mem::take);
4446 self.push_recycled_node(
4447 key,
4448 recycle_pool_limit,
4449 RecycledNode::new(node_id, node, warm_origin),
4450 );
4451 }
4452 self.state.physical_stable_ids[physical_id] = Self::INVALID_STABLE_ID;
4453 self.state.stable_index.release(node_id);
4454 self.state.free_ids.push(Reverse(physical_id));
4455 Ok(())
4456 }
4457
4458 fn remove_subtree_postorder(&mut self, id: NodeId) -> Result<usize, NodeError> {
4459 self.get_ref(id)?;
4460
4461 let mut root_children = SmallVec::<[NodeId; 8]>::new();
4462 self.collect_owned_children(id, &mut root_children)?;
4463
4464 let mut stack = Vec::new();
4465 stack.push(RemovalFrame {
4466 node_id: id,
4467 children: root_children,
4468 next_child: 0,
4469 });
4470 let mut max_depth = stack.len();
4471
4472 while let Some(frame) = stack.last_mut() {
4473 if frame.next_child < frame.children.len() {
4474 let child_id = frame.children[frame.next_child];
4475 frame.next_child += 1;
4476
4477 if let Ok(child) = self.get_mut(child_id) {
4478 child.on_removed_from_parent();
4479 child.unmount();
4480 }
4481
4482 let mut child_children = SmallVec::<[NodeId; 8]>::new();
4483 self.collect_owned_children(child_id, &mut child_children)?;
4484 stack.push(RemovalFrame {
4485 node_id: child_id,
4486 children: child_children,
4487 next_child: 0,
4488 });
4489 max_depth = max_depth.max(stack.len());
4490 continue;
4491 }
4492
4493 let node_id = frame.node_id;
4494 stack.pop();
4495 self.remove_node_storage(node_id)?;
4496 }
4497
4498 Ok(max_depth)
4499 }
4500
4501 #[cfg(test)]
4502 fn debug_remove_max_traversal_depth(&mut self, id: NodeId) -> Result<usize, NodeError> {
4503 self.remove_subtree_postorder(id)
4504 }
4505}
4506
4507impl Applier for MemoryApplier {
4508 fn record_structural_change(&mut self, parent_id: NodeId) {
4509 if self.state.structural_change_parents.last() != Some(&parent_id) {
4510 self.state.structural_change_parents.push(parent_id);
4511 }
4512 }
4513
4514 fn create(&mut self, node: Box<dyn Node>) -> NodeId {
4515 let stable_id = self.state.next_stable_id;
4516 self.state.next_stable_id = self.state.next_stable_id.saturating_add(1);
4517 if stable_id >= Self::HIGH_ID_THRESHOLD {
4518 self.insert_high_id_node(stable_id, node, false);
4519 return stable_id;
4520 }
4521
4522 let physical_id = if let Some(Reverse(id)) = self.state.free_ids.pop() {
4523 debug_assert!(
4524 self.state.nodes[id].is_none(),
4525 "freelist entry {id} is not None"
4526 );
4527 self.state.nodes[id] = Some(node);
4528 self.state.physical_stable_ids[id] = Self::pack_stable_id(stable_id);
4529 self.state.physical_warm_recycled_origins[id] = false;
4530 id
4531 } else {
4532 self.ensure_dense_node_storage_capacity();
4533 let id = self.state.nodes.len();
4534 self.state.nodes.push(Some(node));
4535 self.state
4536 .physical_stable_ids
4537 .push(Self::pack_stable_id(stable_id));
4538 self.state.physical_warm_recycled_origins.push(false);
4539 id
4540 };
4541 self.state.stable_index.insert_fresh(stable_id, physical_id);
4542 stable_id
4543 }
4544
4545 fn node_generation(&self, id: NodeId) -> u32 {
4546 if id >= Self::HIGH_ID_THRESHOLD {
4547 return self
4548 .state
4549 .high_id_generations
4550 .get(&id)
4551 .copied()
4552 .unwrap_or(0);
4553 }
4554 self.state.stable_index.generation(id)
4555 }
4556
4557 fn get_mut(&mut self, id: NodeId) -> Result<&mut dyn Node, NodeError> {
4558 if let Some(physical_id) = self.resolve_node_index(id) {
4559 let slot = self
4560 .state
4561 .nodes
4562 .get_mut(physical_id)
4563 .ok_or(NodeError::Missing { id })?
4564 .as_deref_mut()
4565 .ok_or(NodeError::Missing { id })?;
4566 return Ok(slot);
4567 }
4568 self.state
4569 .high_id_nodes
4570 .get_mut(&id)
4571 .map(std::convert::AsMut::as_mut)
4572 .ok_or(NodeError::Missing { id })
4573 }
4574
4575 fn remove(&mut self, id: NodeId) -> Result<(), NodeError> {
4576 self.state.bubble_passes.forget();
4577 self.remove_subtree_postorder(id).map(|_| ())
4578 }
4579
4580 fn begin_bubble_batch(&mut self) {
4581 self.state.bubble_passes.begin_batch();
4582 }
4583
4584 fn end_bubble_batch(&mut self) {
4585 self.state.bubble_passes.end_batch();
4586 }
4587
4588 fn forget_bubble_passes(&mut self) {
4589 self.state.bubble_passes.forget();
4590 }
4591
4592 fn note_bubble_pass(&mut self, id: NodeId, flags: u8) -> bool {
4593 self.resolve_node_index(id)
4594 .is_some_and(|index| self.state.bubble_passes.note(index, flags))
4595 }
4596
4597 fn insert_with_id(&mut self, id: NodeId, node: Box<dyn Node>) -> Result<(), NodeError> {
4598 if self.contains_node_id(id) {
4599 return Err(NodeError::AlreadyExists { id });
4600 }
4601 self.insert_available_with_id(id, node);
4602 self.state.virtual_node_ids.insert(id);
4603 Ok(())
4604 }
4605
4606 fn insert_recycled_node_or_create(
4607 &mut self,
4608 stable_id: NodeId,
4609 node: Box<dyn Node>,
4610 ) -> RecycledNodeInsertion {
4611 if self.contains_node_id(stable_id) {
4612 let id = self.create(node);
4613 return RecycledNodeInsertion::fresh(
4614 id,
4615 Some(NodeError::AlreadyExists { id: stable_id }),
4616 );
4617 }
4618
4619 self.insert_available_with_id(stable_id, node);
4620 RecycledNodeInsertion::reused(stable_id)
4621 }
4622
4623 fn compact(&mut self) {
4624 let live_count = self
4625 .state
4626 .nodes
4627 .iter()
4628 .filter(|slot| slot.is_some())
4629 .count();
4630 let tombstone_count = self.state.nodes.len().saturating_sub(live_count);
4631 if tombstone_count == 0 {
4632 return;
4633 }
4634 if self.state.nodes.len() > Self::EAGER_COMPACT_NODE_LEN && tombstone_count < live_count {
4635 return;
4636 }
4637 let rehouse_live_nodes = tombstone_count >= live_count;
4638 let mut packed_nodes = Vec::with_capacity(live_count);
4639 let mut packed_physical_stable_ids = Vec::with_capacity(live_count);
4640 let mut packed_warm_recycled_origins = Vec::with_capacity(live_count);
4641
4642 for physical_id in 0..self.state.nodes.len() {
4643 let Some(mut node) = self.state.nodes[physical_id].take() else {
4644 continue;
4645 };
4646 if rehouse_live_nodes && let Some(rehoused) = node.rehouse_for_live_compaction() {
4647 node = rehoused;
4648 }
4649 let stable_id = std::mem::replace(
4650 &mut self.state.physical_stable_ids[physical_id],
4651 Self::INVALID_STABLE_ID,
4652 );
4653 debug_assert_ne!(
4654 stable_id,
4655 Self::INVALID_STABLE_ID,
4656 "live physical slot must have a stable id",
4657 );
4658 let stable_id = Self::unpack_stable_id(stable_id);
4659 packed_nodes.push(Some(node));
4660 packed_physical_stable_ids.push(Self::pack_stable_id(stable_id));
4661 packed_warm_recycled_origins
4662 .push(self.state.physical_warm_recycled_origins[physical_id]);
4663 self.state
4664 .stable_index
4665 .set_slot(stable_id, packed_nodes.len() - 1);
4666 }
4667
4668 self.state.nodes = packed_nodes;
4669 self.state.physical_stable_ids = packed_physical_stable_ids;
4670 self.state.physical_warm_recycled_origins = packed_warm_recycled_origins;
4671 self.state.free_ids = BinaryHeap::new();
4672 debug_assert_eq!(
4673 self.state.stable_index.live(),
4674 live_count,
4675 "every live node keeps one indexed slot through compaction",
4676 );
4677 self.prune_stable_generations();
4678 }
4679
4680 fn take_recycled_node(&mut self, key: TypeId) -> Option<RecycledNode> {
4681 Self::take_recycled_node_from_pool(&mut self.state.returning_recycled_nodes, key)
4682 .or_else(|| Self::take_recycled_node_from_pool(&mut self.state.recycled_nodes, key))
4683 }
4684
4685 fn set_recycled_node_origin(&mut self, id: NodeId, warm_origin: bool) {
4686 if let Some(physical_id) = self.resolve_node_index(id) {
4687 self.state.physical_warm_recycled_origins[physical_id] = warm_origin;
4688 } else if self.state.high_id_nodes.contains_key(&id) {
4689 self.state
4690 .high_id_warm_recycled_origins
4691 .insert(id, warm_origin);
4692 }
4693 }
4694
4695 fn seed_recycled_node_shell(
4696 &mut self,
4697 key: TypeId,
4698 recycle_pool_limit: Option<usize>,
4699 shell: Box<dyn Node>,
4700 ) {
4701 self.seed_recycled_node_shell_impl(key, recycle_pool_limit, shell);
4702 }
4703
4704 fn record_fresh_recyclable_creation(&mut self, key: TypeId) {
4705 *self
4706 .state
4707 .fresh_recyclable_creations
4708 .entry(key)
4709 .or_insert(0) += 1;
4710 }
4711
4712 fn clear_recycled_nodes(&mut self) {
4713 let returning = std::mem::take(&mut self.state.returning_recycled_nodes);
4714 for (key, mut nodes) in returning {
4715 let pool = self.state.recycled_nodes.entry(key).or_default();
4716 pool.append(&mut nodes);
4717 }
4718
4719 let fresh_recyclable_creations = std::mem::take(&mut self.state.fresh_recyclable_creations);
4720 let cold = std::mem::take(&mut self.state.cold_recycled_nodes);
4721 for (key, mut nodes) in cold {
4722 let needed = fresh_recyclable_creations.get(&key).copied().unwrap_or(0);
4723 if needed > 0 {
4724 let remaining_limit = self
4725 .recycle_pool_limit_for(key)
4726 .unwrap_or(usize::MAX)
4727 .saturating_sub(self.warm_recycled_pool_len(key));
4728 let promote = nodes.len().min(needed).min(remaining_limit);
4729 let split_at = nodes.len().saturating_sub(promote);
4730 let promoted = nodes.split_off(split_at);
4731 for mut recycled in promoted {
4732 recycled.set_warm_origin(true);
4733 self.state
4734 .recycled_nodes
4735 .entry(key)
4736 .or_default()
4737 .push(recycled);
4738 }
4739 }
4740 }
4741
4742 let mut keys: HashSet<TypeId> = HashSet::default();
4743 keys.extend(self.state.recycled_nodes.keys().copied());
4744 keys.extend(self.state.recycled_node_limits.keys().copied());
4745 keys.extend(self.state.warm_recycled_node_targets.keys().copied());
4746 keys.extend(self.state.recycled_node_prototypes.keys().copied());
4747 for key in keys {
4748 let observed_demand = fresh_recyclable_creations.get(&key).copied().unwrap_or(0);
4749 let target = self.update_warm_recycled_node_target(key, observed_demand);
4750 self.replenish_warm_pool_to_target(key, target);
4751 self.trim_idle_warm_pool_to_target(key, target);
4752 self.compact_idle_warm_pool(key);
4753 }
4754 self.prune_stable_generations();
4755 self.compact();
4756 }
4757}
4758
4759pub struct SlotsHost {
4762 storage_key: Cell<usize>,
4763 inner: RefCell<SlotsHostInner>,
4764}
4765
4766#[derive(Debug, Default)]
4767pub(crate) struct SlotPassOutcome {
4768 pub(crate) compacted: bool,
4769 pub(crate) compact_anchor_registry_storage: bool,
4770 pub(crate) compact_payload_storage: bool,
4771 pub(crate) trim_growth_slack: bool,
4772}
4773
4774#[derive(Default)]
4775pub(crate) struct FinishedSlotPass {
4776 pub(crate) outcome: SlotPassOutcome,
4777 pub(crate) detached_root_children: Vec<slot::DetachedSubtree>,
4778}
4779
4780struct ActivePassState {
4781 state: slot::SlotWriteSessionState,
4782 storage_capacity: usize,
4783}
4784
4785struct SlotsHostInner {
4786 table: SlotTable,
4787 nested_hosts: Vec<std::rc::Weak<SlotsHost>>,
4788 lifecycle: slot::SlotLifecycleCoordinator,
4789 runtime_state: Option<Rc<crate::composer::ComposerRuntimeState>>,
4790 active_pass: Option<ActivePassState>,
4791}
4792
4793impl Drop for SlotsHost {
4794 fn drop(&mut self) {
4795 let storage_key = self.storage_key.get();
4796 let inner = self.inner.get_mut();
4797 if let Some(state) = inner.runtime_state.clone() {
4798 if let Err(err) = state.dispose_retained_subtrees_for_host(
4799 storage_key,
4800 &mut inner.table,
4801 &mut inner.lifecycle,
4802 ) {
4803 log::error!(
4804 "retained subtree disposal failed while dropping SlotsHost {storage_key}: {err}"
4805 );
4806 state.abandon_retained_subtrees_for_host(
4807 storage_key,
4808 &mut inner.table,
4809 &mut inner.lifecycle,
4810 );
4811 } else {
4812 state.clear_host_storage_key(storage_key, &mut inner.table);
4813 }
4814 }
4815 inner.lifecycle.dispose_slot_table(&mut inner.table);
4816 }
4817}
4818
4819impl SlotsHost {
4820 pub fn storage_key(&self) -> usize {
4823 self.storage_key.get()
4824 }
4825
4826 pub fn new(storage: SlotTable) -> Self {
4828 let storage_key = storage.storage_id();
4829 Self {
4830 storage_key: Cell::new(storage_key),
4831 inner: RefCell::new(SlotsHostInner {
4832 table: storage,
4833 nested_hosts: Vec::new(),
4834 lifecycle: slot::SlotLifecycleCoordinator::default(),
4835 runtime_state: None,
4836 active_pass: None,
4837 }),
4838 }
4839 }
4840
4841 pub fn note_nested_host(&self, nested: &Rc<SlotsHost>) {
4844 let Ok(mut inner) = self.inner.try_borrow_mut() else {
4845 return;
4846 };
4847 inner.nested_hosts.retain(|held| held.upgrade().is_some());
4848 if inner
4849 .nested_hosts
4850 .iter()
4851 .any(|held| held.upgrade().is_some_and(|host| Rc::ptr_eq(&host, nested)))
4852 {
4853 return;
4854 }
4855 inner.nested_hosts.push(Rc::downgrade(nested));
4856 }
4857
4858 pub(crate) fn forget_effects(&self) -> bool {
4859 let (forgotten, nested, runtime_state) = {
4860 let Ok(mut inner) = self.inner.try_borrow_mut() else {
4861 return false;
4862 };
4863 if inner.active_pass.is_some() {
4864 return false;
4865 }
4866 let drops = inner.table.take_effect_drops();
4867 inner.nested_hosts.retain(|held| held.upgrade().is_some());
4868 let nested: Vec<Rc<SlotsHost>> = inner
4869 .nested_hosts
4870 .iter()
4871 .filter_map(std::rc::Weak::upgrade)
4872 .collect();
4873 (drops, nested, inner.runtime_state.clone())
4874 };
4875 let mut any = !forgotten.is_empty();
4876 drop(forgotten);
4877 for host in nested {
4878 any |= host.forget_effects();
4879 }
4880 if any {
4881 self.borrow()
4882 .scopes()
4883 .for_each(RecomposeScope::force_recompose);
4884 if let Some(runtime_state) = runtime_state {
4885 runtime_state.force_recompose_retained_scopes(self.storage_key());
4886 }
4887 }
4888 any
4889 }
4890
4891 pub(crate) fn bind_runtime_state(&self, state: &Rc<crate::composer::ComposerRuntimeState>) {
4892 let mut inner = self.inner.borrow_mut();
4893 inner.runtime_state = Some(Rc::clone(state));
4894 }
4895
4896 pub(crate) fn rebind_orphaned_runtime_state(
4897 &self,
4898 state: &Rc<crate::composer::ComposerRuntimeState>,
4899 ) -> bool {
4900 let inner = self.inner.borrow();
4901 if inner.active_pass.is_some() {
4902 log::error!("cannot rebind SlotsHost during an active pass");
4903 return false;
4904 }
4905 let Some(bound_state) = inner.runtime_state.as_ref() else {
4906 drop(inner);
4907 self.bind_runtime_state(state);
4908 return true;
4909 };
4910 if Rc::ptr_eq(bound_state, state) {
4911 return true;
4912 }
4913 if bound_state.has_live_applier_host() {
4914 return false;
4915 }
4916 drop(inner);
4917
4918 let mut inner = self.inner.borrow_mut();
4919 let Some(bound_state) = inner.runtime_state.as_ref() else {
4920 inner.runtime_state = Some(Rc::clone(state));
4921 return true;
4922 };
4923 if Rc::ptr_eq(bound_state, state) {
4924 return true;
4925 }
4926 if bound_state.has_live_applier_host() {
4927 return false;
4928 }
4929
4930 let previous_state = Rc::clone(bound_state);
4931 let mut lifecycle = std::mem::take(&mut inner.lifecycle);
4932 lifecycle.flush_pending_drops();
4933 let host_key = self.storage_key();
4934 if previous_state
4935 .dispose_retained_subtrees_for_host(host_key, &mut inner.table, &mut lifecycle)
4936 .is_err()
4937 {
4938 inner.lifecycle = lifecycle;
4939 return false;
4940 }
4941 previous_state.clear_host(self, &mut inner.table);
4942 lifecycle.flush_pending_drops();
4943 inner.runtime_state = Some(Rc::clone(state));
4944 inner.lifecycle = lifecycle;
4945 true
4946 }
4947
4948 pub(crate) fn runtime_state(&self) -> Option<Rc<crate::composer::ComposerRuntimeState>> {
4949 self.inner.borrow().runtime_state.clone()
4950 }
4951
4952 fn runtime_state_ptr(&self) -> Option<*const crate::composer::ComposerRuntimeState> {
4953 self.inner.borrow().runtime_state.as_ref().map(Rc::as_ptr)
4954 }
4955
4956 pub(crate) fn borrow(&self) -> Ref<'_, SlotTable> {
4957 Ref::map(self.inner.borrow(), |inner| &inner.table)
4958 }
4959
4960 pub(crate) fn borrow_mut(&self) -> RefMut<'_, SlotTable> {
4961 RefMut::map(self.inner.borrow_mut(), |inner| &mut inner.table)
4962 }
4963
4964 pub fn into_table(self: Rc<Self>) -> Result<SlotTable, NodeError> {
4967 if Rc::strong_count(&self) != 1 {
4968 return Err(NodeError::SlotHostUnavailable {
4969 operation: "SlotsHost::into_table",
4970 reason: "other host references are alive",
4971 });
4972 }
4973 self.take_table_for_transfer()
4974 }
4975
4976 fn take_table_for_transfer(&self) -> Result<SlotTable, NodeError> {
4977 let inner = self.inner.borrow();
4978 if inner.active_pass.is_some() {
4979 return Err(NodeError::SlotHostUnavailable {
4980 operation: "SlotsHost::into_table",
4981 reason: "slot pass is active",
4982 });
4983 }
4984 drop(inner);
4985 let mut inner = self.inner.borrow_mut();
4986 let mut lifecycle = std::mem::take(&mut inner.lifecycle);
4987 lifecycle.flush_pending_drops();
4988 if let Some(state) = inner.runtime_state.clone() {
4989 let host_key = self.storage_key();
4990 if let Err(error) =
4991 state.dispose_retained_subtrees_for_host(host_key, &mut inner.table, &mut lifecycle)
4992 {
4993 inner.lifecycle = lifecycle;
4994 return Err(error);
4995 }
4996 state.clear_host(self, &mut inner.table);
4997 lifecycle.flush_pending_drops();
4998 }
4999 let taken = std::mem::take(&mut inner.table);
5000 self.storage_key.set(inner.table.storage_id());
5001 inner.runtime_state = None;
5002 inner.lifecycle = lifecycle;
5003 Ok(taken)
5004 }
5005
5006 pub fn reset(&self) -> Result<(), NodeError> {
5009 let inner = self.inner.borrow();
5010 if inner.active_pass.is_some() {
5011 return Err(NodeError::SlotHostUnavailable {
5012 operation: "SlotsHost::reset",
5013 reason: "slot pass is active",
5014 });
5015 }
5016 let runtime_state = inner.runtime_state.clone();
5017 drop(inner);
5018 let mut inner = self.inner.borrow_mut();
5019 let mut lifecycle = std::mem::take(&mut inner.lifecycle);
5020 if let Some(state) = runtime_state {
5021 let host_key = self.storage_key();
5022 if let Err(error) =
5023 state.dispose_retained_subtrees_for_host(host_key, &mut inner.table, &mut lifecycle)
5024 {
5025 inner.lifecycle = lifecycle;
5026 return Err(error);
5027 }
5028 state.clear_host(self, &mut inner.table);
5029 }
5030 lifecycle.dispose_slot_table(&mut inner.table);
5031 inner.table = SlotTable::default();
5032 self.storage_key.set(inner.table.storage_id());
5033 inner.runtime_state = None;
5034 inner.lifecycle = slot::SlotLifecycleCoordinator::default();
5035 Ok(())
5036 }
5037
5038 pub(crate) fn abandon_after_apply_failure(&self) {
5039 let inner = self.inner.borrow();
5040 if inner.active_pass.is_some() {
5041 log::error!("cannot abandon SlotsHost during an active pass");
5042 return;
5043 }
5044 let runtime_state = inner.runtime_state.clone();
5045 drop(inner);
5046 let mut inner = self.inner.borrow_mut();
5047 let mut lifecycle = std::mem::take(&mut inner.lifecycle);
5048 if let Some(state) = runtime_state {
5049 let host_key = self.storage_key();
5050 state.abandon_retained_subtrees_for_host(host_key, &mut inner.table, &mut lifecycle);
5051 }
5052 lifecycle.dispose_slot_table(&mut inner.table);
5053 inner.table = SlotTable::default();
5054 self.storage_key.set(inner.table.storage_id());
5055 inner.runtime_state = None;
5056 inner.lifecycle = slot::SlotLifecycleCoordinator::default();
5057 }
5058
5059 pub(crate) fn debug_stats(&self) -> SlotTableDebugStats {
5060 let inner = self.inner.borrow();
5061 let local = inner.table.debug_stats();
5062 let lifecycle = inner.lifecycle.debug_stats();
5063 let retention = inner
5064 .runtime_state
5065 .clone()
5066 .map(|state| state.slot_retention_debug_stats(self))
5067 .unwrap_or_default();
5068 SlotTableDebugStats::from_parts(local, lifecycle, retention)
5069 }
5070
5071 #[cold]
5072 #[inline(never)]
5073 pub(crate) fn debug_snapshot(&self) -> slot::SlotDebugSnapshot {
5074 let inner = self.inner.borrow();
5075 let mut snapshot = inner.table.debug_snapshot();
5076 if let Some(state) = inner.runtime_state.clone() {
5077 state.fill_slot_debug_snapshot(self, &mut snapshot);
5078 }
5079 snapshot
5080 }
5081
5082 pub(crate) fn begin_pass(&self, mode: slot::SlotPassMode, folds: &Rc<slot::BranchFolds>) {
5083 let mut inner = self.inner.borrow_mut();
5084 if inner.active_pass.is_some() {
5085 log::error!("slot pass already active for host");
5086 return;
5087 }
5088 let mut state = slot::SlotWriteSessionState::new(Rc::clone(folds));
5089 state.reset_for_pass(mode);
5090 let storage_capacity = inner.table.storage_capacity();
5091 inner.active_pass = Some(ActivePassState {
5092 state,
5093 storage_capacity,
5094 });
5095 }
5096
5097 pub(crate) fn has_active_pass(&self) -> bool {
5098 self.inner.borrow().active_pass.is_some()
5099 }
5100
5101 pub(crate) fn abandon_active_pass(&self) {
5102 self.inner.borrow_mut().active_pass = None;
5103 }
5104
5105 pub(crate) fn with_write_session<R>(
5106 &self,
5107 f: impl FnOnce(&mut slot::SlotWriteSession<'_>) -> R,
5108 ) -> R {
5109 let mut inner = self.inner.borrow_mut();
5110 let SlotsHostInner {
5111 table,
5112 lifecycle,
5113 active_pass,
5114 ..
5115 } = &mut *inner;
5116 let active_pass = active_pass
5117 .as_mut()
5118 .expect("slot write session requires an active pass");
5119 let mut session = table.write_session(lifecycle, &mut active_pass.state);
5120 f(&mut session)
5121 }
5122
5123 pub(crate) fn with_table_and_lifecycle_mut<R>(
5124 &self,
5125 f: impl FnOnce(&mut SlotTable, &mut slot::SlotLifecycleCoordinator) -> R,
5126 ) -> R {
5127 let mut inner = self.inner.borrow_mut();
5128 let SlotsHostInner {
5129 table, lifecycle, ..
5130 } = &mut *inner;
5131 f(table, lifecycle)
5132 }
5133
5134 pub(crate) fn finish_pass(
5135 &self,
5136 applier: &mut dyn Applier,
5137 ) -> Result<FinishedSlotPass, NodeError> {
5138 let mut inner = self.inner.borrow_mut();
5139 let SlotsHostInner {
5140 table,
5141 lifecycle,
5142 active_pass: active_pass_slot,
5143 ..
5144 } = &mut *inner;
5145 let Some(mut active_pass) = active_pass_slot.take() else {
5146 return Ok(FinishedSlotPass::default());
5147 };
5148
5149 active_pass.state.flush_payload_location_refreshes(table);
5150
5151 #[cfg(any(test, debug_assertions))]
5152 if let Err(err) = active_pass.state.validate(table) {
5153 log::error!("slot writer invariant violation before finalize_pass: {err:?}");
5154 return Err(NodeError::SlotHostUnavailable {
5155 operation: "SlotsHost::finish_pass",
5156 reason: "slot writer invariant violation",
5157 });
5158 }
5159
5160 let detached_root_children = {
5161 let mut session = table.write_session(lifecycle, &mut active_pass.state);
5162 session.finalize_pass(applier)?
5163 };
5164
5165 Ok(FinishedSlotPass {
5166 outcome: SlotPassOutcome {
5167 compacted: active_pass.state.request_compaction,
5168 compact_anchor_registry_storage: active_pass
5169 .state
5170 .request_anchor_storage_compaction,
5171 compact_payload_storage: active_pass.state.request_payload_storage_compaction,
5172 trim_growth_slack: active_pass.state.removed_nothing()
5173 && table.storage_capacity() > active_pass.storage_capacity,
5174 },
5175 detached_root_children,
5176 })
5177 }
5178
5179 pub(crate) fn flush_pending_drops(&self) {
5180 self.inner.borrow_mut().lifecycle.flush_pending_drops();
5181 }
5182
5183 pub(crate) fn complete_pass_cleanup(&self, outcome: &SlotPassOutcome) {
5184 let mut inner = self.inner.borrow_mut();
5185 let SlotsHostInner {
5186 table,
5187 lifecycle,
5188 runtime_state,
5189 ..
5190 } = &mut *inner;
5191 lifecycle.flush_pending_drops();
5192 if outcome.compacted {
5193 table.compact_storage();
5194 lifecycle.compact_storage();
5195 } else if outcome.trim_growth_slack {
5196 table.trim_growth_slack();
5197 }
5198 if let Some(state) = runtime_state.clone() {
5199 state.compact_table_identity_storage_for_host(
5200 self,
5201 table,
5202 outcome.compact_anchor_registry_storage,
5203 outcome.compact_payload_storage,
5204 );
5205 } else {
5206 if outcome.compact_anchor_registry_storage {
5207 table.compact_anchor_registry_storage(None);
5208 }
5209 if outcome.compact_payload_storage {
5210 table.compact_payload_anchor_registry_storage(None);
5211 }
5212 }
5213 table.assert_fast_integrity("slot pass cleanup");
5214 #[cfg(any(test, debug_assertions))]
5215 {
5216 table.debug_verify();
5217 if let Some(state) = runtime_state.clone() {
5218 state.debug_verify_host(self, table);
5219 }
5220 }
5221 }
5222}
5223
5224fn build_child_positions(children: &[NodeId]) -> HashMap<NodeId, usize> {
5225 let mut positions = HashMap::default();
5226 positions.reserve(children.len());
5227 for (index, &child) in children.iter().enumerate() {
5228 positions.insert(child, index);
5229 }
5230 positions
5231}
5232
5233fn refresh_child_positions(
5234 current: &[NodeId],
5235 positions: &mut HashMap<NodeId, usize>,
5236 start: usize,
5237 end: usize,
5238) {
5239 if current.is_empty() || start >= current.len() {
5240 return;
5241 }
5242 let end = end.min(current.len() - 1);
5243 for (offset, &child) in current[start..=end].iter().enumerate() {
5244 positions.insert(child, start + offset);
5245 }
5246}
5247
5248fn insert_child_into_diff_state(
5249 current: &mut ChildList,
5250 positions: &mut HashMap<NodeId, usize>,
5251 index: usize,
5252 child: NodeId,
5253) {
5254 let index = index.min(current.len());
5255 current.insert(index, child);
5256 refresh_child_positions(current, positions, index, current.len() - 1);
5257}
5258
5259fn move_child_in_diff_state(
5260 current: &mut ChildList,
5261 positions: &mut HashMap<NodeId, usize>,
5262 from_index: usize,
5263 target_index: usize,
5264) -> usize {
5265 let child = current.remove(from_index);
5266 let to_index = target_index.min(current.len());
5267 current.insert(to_index, child);
5268 refresh_child_positions(
5269 current,
5270 positions,
5271 from_index.min(to_index),
5272 from_index.max(to_index),
5273 );
5274 to_index
5275}
5276
5277pub(crate) use state::MutableStateInner;
5278pub use state::{
5279 MutableState, OwnedMutableState, SnapshotStateList, SnapshotStateMap, State,
5280 StateSubscriptionHold,
5281};
5282
5283fn hash_key<K: Hash>(key: &K) -> Key {
5284 let mut hasher = hash::default::new();
5285 key.hash(&mut hasher);
5286 hasher.finish()
5287}
5288
5289pub(crate) fn explicit_group_key_seed<K: Hash>(
5290 key: &K,
5291 caller: &'static std::panic::Location<'static>,
5292) -> slot::GroupKeySeed {
5293 let source_key = location_key(caller.file(), caller.line(), caller.column());
5294 let explicit_key = hash_key(key);
5295 slot::GroupKeySeed::keyed(source_key, explicit_key)
5296}
5297
5298#[cfg(test)]
5299#[path = "tests/mod.rs"]
5300mod tests;
5301
5302#[cfg(test)]
5303#[path = "tests/recursive_decrease_increase_test.rs"]
5304mod recursive_decrease_increase_test;
5305
5306pub mod collections;
5307pub mod hash;
5309
5310#[cfg(any(test, feature = "test-helpers"))]
5313pub mod test_scratch;
5314#[cfg(any(test, feature = "test-helpers"))]
5315pub use test_scratch::test_scratch_dir;
5316
5317pub(crate) fn note_structural(reason: &str, parent_id: NodeId, child_id: NodeId) {
5318 if env_flag!("CRANPOSE_STRUCTURAL_DIAG") {
5319 eprintln!("[structural] {reason} parent={parent_id} child={child_id}");
5320 }
5321}
5322
5323pub(crate) fn note_structural_move(parent_id: NodeId, from_index: usize, to_index: usize) {
5324 if env_flag!("CRANPOSE_STRUCTURAL_DIAG") {
5325 eprintln!("[structural] move parent={parent_id} from={from_index} to={to_index}");
5326 }
5327}