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