Skip to main content

cranpose_core/
snapshot_state_observer.rs

1// Complex types are inherent to the observer pattern with nested callbacks and state tracking
2#![allow(clippy::type_complexity)]
3
4use std::{
5    any::{Any, TypeId},
6    cell::{Cell, RefCell},
7    hash::{Hash, Hasher},
8    rc::{Rc, Weak},
9    sync::Arc,
10};
11
12use smallvec::SmallVec;
13
14use crate::{
15    RecomposeScope, RecomposeScopeInner, ScopeId,
16    collections::map::{HashMap, HashSet},
17    hash::default as default_hash,
18    snapshot_v2::{ReadObserver, StateObjectId, register_apply_observer},
19    state::StateObject,
20};
21
22/// Executes a callback once changes are delivered.
23type Executor = dyn Fn(Box<dyn FnOnce() + 'static>) + 'static;
24
25/// Observer that records state object reads performed inside a given scope and
26/// notifies the caller when any of the observed objects change.
27///
28/// This is a pragmatic Rust translation of Jetpack Compose's
29/// `SnapshotStateObserver`. The implementation focuses on the core behaviour
30/// needed by the Cranpose runtime:
31/// - Tracking state object reads per logical scope.
32/// - Reacting to snapshot apply notifications.
33/// - Scheduling invalidation callbacks via the supplied executor.
34///
35/// Advanced features from the Kotlin version (derived state tracking, change
36/// coalescing, queue minimisation) are deferred
37#[derive(Clone)]
38pub struct SnapshotStateObserver {
39    inner: Rc<SnapshotStateObserverInner>,
40}
41
42#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
43pub struct SnapshotStateObserverDebugStats {
44    pub scopes_len: usize,
45    pub scopes_cap: usize,
46    pub fast_scopes_len: usize,
47    pub fast_scopes_cap: usize,
48    pub stateless_scope_count: usize,
49    pub observed_state_count: usize,
50    pub observed_state_capacity: usize,
51}
52
53impl SnapshotStateObserver {
54    /// Create a new observer that schedules callbacks using `on_changed_executor`.
55    pub fn new(on_changed_executor: impl Fn(Box<dyn FnOnce() + 'static>) + 'static) -> Self {
56        let inner = Rc::new(SnapshotStateObserverInner::new(on_changed_executor));
57        inner.set_self(Rc::downgrade(&inner));
58        Self { inner }
59    }
60
61    /// Observe state object reads performed while executing `block`.
62    ///
63    /// Subsequent calls to `observe_reads` replace any previously recorded
64    /// observations for the provided `scope`. When one of the observed objects
65    /// mutates, `on_value_changed_for_scope` will be invoked on the executor.
66    pub fn observe_reads<T, R>(
67        &self,
68        scope: T,
69        on_value_changed_for_scope: impl Fn(&T) + 'static,
70        block: impl FnOnce() -> R,
71    ) -> R
72    where
73        T: Any + Clone + Eq + Hash + 'static,
74    {
75        self.inner
76            .observe_reads(scope, on_value_changed_for_scope, block)
77    }
78
79    /// Notify the observer that a new composition frame is starting.
80    pub fn begin_frame(&self) {
81        self.inner.begin_frame();
82    }
83
84    /// Drop bookkeeping for scopes that were released during the current frame.
85    pub fn prune_dead_scopes(&self) {
86        self.inner.prune_dead_scopes();
87    }
88
89    /// Temporarily pause read observation while executing `block`.
90    pub fn with_no_observations<R>(&self, block: impl FnOnce() -> R) -> R {
91        self.inner.with_no_observations(block)
92    }
93
94    /// Remove any recorded reads for `scope`.
95    pub fn clear<T>(&self, scope: &T)
96    where
97        T: Any + Eq + Hash + 'static,
98    {
99        self.inner.clear(scope);
100    }
101
102    /// Remove recorded reads for scopes that satisfy `predicate`.
103    pub fn clear_if(&self, predicate: impl Fn(&dyn Any) -> bool) {
104        self.inner.clear_if(predicate);
105    }
106
107    /// Remove all recorded observations.
108    pub fn clear_all(&self) {
109        self.inner.clear_all();
110    }
111
112    /// Begin listening for snapshot apply notifications.
113    pub fn start(&self) {
114        let weak = Rc::downgrade(&self.inner);
115        self.inner.start(weak);
116    }
117
118    /// Stop listening for snapshot apply notifications.
119    pub fn stop(&self) {
120        self.inner.stop();
121    }
122
123    pub fn debug_stats(&self) -> SnapshotStateObserverDebugStats {
124        self.inner.debug_stats()
125    }
126
127    /// Test-only helper to simulate snapshot changes.
128    #[cfg(test)]
129    pub fn notify_changes(&self, modified: &[Arc<dyn StateObject>]) {
130        self.inner.handle_apply(modified);
131    }
132}
133
134struct SnapshotStateObserverInner {
135    // Borrowing invariants:
136    // - `observe_reads` may push/pop `active_read_targets`, but it must not hold that
137    //   RefCell borrow across the user-provided `block`; the scoped guard only keeps
138    //   ownership of the target handle, not an active borrow.
139    // - `read_dispatcher` borrows `active_read_targets` just long enough to clone the
140    //   current target, then mutates only the selected `ObservedIds`.
141    // - `handle_apply` snapshots `observed_to_scopes` and `indexed_scopes` into a
142    //   separate notification list before borrowing any `ScopeEntry`, so map borrows
143    //   are never held while callbacks may inspect or mutate entry state.
144    // - `replace_observed_ids`, `clear`, `clear_if`, and `prune_dead_scopes` may borrow
145    //   scope maps mutably, but they must release any `ScopeEntry` borrow before removing
146    //   the entry from `indexed_scopes`, `fast_scopes`, `owned_scopes`, or
147    //   `observed_to_scopes`.
148    executor: Rc<Executor>,
149    owned_scopes: RefCell<HashMap<OwnedScopeIndexKey, OwnedScopeBucket>>,
150    fast_scopes: RefCell<HashMap<ScopeId, Rc<RefCell<ScopeEntry>>>>,
151    indexed_scopes: RefCell<HashMap<usize, Rc<RefCell<ScopeEntry>>>>,
152    observed_to_scopes: RefCell<HashMap<StateObjectId, HashSet<usize>>>,
153    pause_count: Rc<Cell<usize>>,
154    active_read_targets: Rc<RefCell<Vec<Rc<RefCell<ObservedIds>>>>>,
155    read_dispatcher: ReadObserver,
156    apply_handle: RefCell<Option<crate::snapshot_v2::ObserverHandle>>,
157    weak_self: RefCell<Weak<SnapshotStateObserverInner>>,
158    frame_version: Cell<u64>,
159    next_entry_id: Cell<usize>,
160}
161
162#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
163struct OwnedScopeIndexKey {
164    type_id: TypeId,
165    value_hash: u64,
166}
167
168type OwnedScopeBucket = SmallVec<[Rc<RefCell<ScopeEntry>>; 1]>;
169
170fn owned_scope_index_key<T>(scope: &T) -> OwnedScopeIndexKey
171where
172    T: Any + Hash + 'static,
173{
174    let mut hasher = default_hash::new();
175    scope.hash(&mut hasher);
176    OwnedScopeIndexKey {
177        type_id: TypeId::of::<T>(),
178        value_hash: hasher.finish(),
179    }
180}
181
182impl SnapshotStateObserverInner {
183    const MIN_RETAINED_SCOPE_CAPACITY: usize = 256;
184
185    fn new(on_changed_executor: impl Fn(Box<dyn FnOnce() + 'static>) + 'static) -> Self {
186        let pause_count = Rc::new(Cell::new(0));
187        let active_read_targets = Rc::new(RefCell::new(Vec::<Rc<RefCell<ObservedIds>>>::new()));
188        let dispatcher_pause_count = Rc::clone(&pause_count);
189        let dispatcher_targets = Rc::clone(&active_read_targets);
190        let read_dispatcher: ReadObserver = Arc::new(move |state| {
191            if dispatcher_pause_count.get() > 0 {
192                return;
193            }
194            let observed = dispatcher_targets.borrow().last().cloned();
195            if let Some(observed) = observed {
196                observed.borrow_mut().insert(state);
197            }
198        });
199
200        Self {
201            executor: Rc::new(on_changed_executor),
202            owned_scopes: RefCell::new(HashMap::default()),
203            fast_scopes: RefCell::new(HashMap::default()),
204            indexed_scopes: RefCell::new(HashMap::default()),
205            observed_to_scopes: RefCell::new(HashMap::default()),
206            pause_count,
207            active_read_targets,
208            read_dispatcher,
209            apply_handle: RefCell::new(None),
210            weak_self: RefCell::new(Weak::new()),
211            frame_version: Cell::new(0),
212            next_entry_id: Cell::new(0),
213        }
214    }
215
216    fn set_self(&self, weak: Weak<SnapshotStateObserverInner>) {
217        self.weak_self.replace(weak);
218    }
219
220    fn begin_frame(&self) {
221        let next = self.frame_version.get().wrapping_add(1);
222        self.frame_version.set(next);
223        self.prune_dead_scopes();
224    }
225
226    fn observe_reads<T, R>(
227        &self,
228        scope: T,
229        on_value_changed_for_scope: impl Fn(&T) + 'static,
230        block: impl FnOnce() -> R,
231    ) -> R
232    where
233        T: Any + Clone + Eq + Hash + 'static,
234    {
235        let frame_version = self.frame_version.get();
236        let has_frame_version = frame_version != 0;
237
238        let on_changed: Rc<dyn Fn(&dyn Any)> = {
239            let callback = Rc::new(on_value_changed_for_scope);
240            Rc::new(move |scope_any: &dyn Any| {
241                if let Some(typed) = scope_any.downcast_ref::<T>() {
242                    callback(typed);
243                }
244            })
245        };
246
247        let existing_entry = self.find_scope_entry(&scope);
248        if let Some(entry) = existing_entry.as_ref() {
249            let already_observed = {
250                let mut entry_mut = entry.borrow_mut();
251                entry_mut.update(scope.clone(), on_changed.clone());
252                has_frame_version && entry_mut.last_seen_version == frame_version
253            };
254            if already_observed {
255                return block();
256            }
257        }
258
259        let observed = Rc::new(RefCell::new(ObservedIds::new()));
260        self.active_read_targets
261            .borrow_mut()
262            .push(Rc::clone(&observed));
263        struct ActiveObservationGuard {
264            stack: Rc<RefCell<Vec<Rc<RefCell<ObservedIds>>>>>,
265        }
266        impl Drop for ActiveObservationGuard {
267            fn drop(&mut self) {
268                self.stack.borrow_mut().pop();
269            }
270        }
271        let _guard = ActiveObservationGuard {
272            stack: Rc::clone(&self.active_read_targets),
273        };
274
275        let result = self.run_with_read_observer(block);
276
277        if observed.borrow().is_empty() {
278            if existing_entry.is_some() {
279                self.clear(&scope);
280            }
281            return result;
282        }
283
284        let observed = {
285            let mut observed = observed.borrow_mut();
286            std::mem::replace(&mut *observed, ObservedIds::new())
287        };
288        let entry = existing_entry
289            .unwrap_or_else(|| self.insert_scope_entry(scope.clone(), on_changed.clone()));
290        {
291            let mut entry_mut = entry.borrow_mut();
292            entry_mut.update(scope, on_changed);
293            entry_mut.last_seen_version = if has_frame_version {
294                frame_version
295            } else {
296                u64::MAX
297            };
298        }
299        self.replace_observed_ids(&entry, observed);
300
301        result
302    }
303
304    fn with_no_observations<R>(&self, block: impl FnOnce() -> R) -> R {
305        self.pause_count.set(self.pause_count.get() + 1);
306        let result = block();
307        self.pause_count
308            .set(self.pause_count.get().saturating_sub(1));
309        result
310    }
311
312    fn clear<T>(&self, scope: &T)
313    where
314        T: Any + Eq + Hash + 'static,
315    {
316        if let Some(rc_scope) = (scope as &dyn Any).downcast_ref::<RecomposeScope>() {
317            if let Some(entry) = self.fast_scopes.borrow_mut().remove(&rc_scope.id()) {
318                self.unregister_entry(&entry);
319            }
320            return;
321        }
322
323        let removed = self.remove_owned_scope_entry(scope);
324        if let Some(entry) = removed {
325            self.unregister_entry(&entry);
326        }
327    }
328
329    fn clear_if(&self, predicate: impl Fn(&dyn Any) -> bool) {
330        let removed_fast = {
331            let mut fast_scopes = self.fast_scopes.borrow_mut();
332            let removed_ids: Vec<_> = fast_scopes
333                .iter()
334                .filter(|(_, entry)| entry.borrow().matches_predicate(&predicate))
335                .map(|(scope_id, _)| *scope_id)
336                .collect();
337            removed_ids
338                .into_iter()
339                .filter_map(|scope_id| fast_scopes.remove(&scope_id))
340                .collect::<Vec<_>>()
341        };
342        let removed_owned =
343            { self.partition_owned_scopes(|entry| entry.matches_predicate(&predicate)) };
344
345        for entry in removed_fast.into_iter().chain(removed_owned) {
346            self.unregister_entry(&entry);
347        }
348    }
349
350    fn clear_all(&self) {
351        self.fast_scopes.borrow_mut().clear();
352        self.owned_scopes.borrow_mut().clear();
353        self.indexed_scopes.borrow_mut().clear();
354        self.observed_to_scopes.borrow_mut().clear();
355    }
356
357    fn start(&self, weak_self: Weak<SnapshotStateObserverInner>) {
358        if self.apply_handle.borrow().is_some() {
359            return;
360        }
361
362        let handle = register_apply_observer(Rc::new(move |modified, _snapshot_id| {
363            if let Some(inner) = weak_self.upgrade() {
364                inner.handle_apply(modified);
365            }
366        }));
367        self.apply_handle.replace(Some(handle));
368    }
369
370    fn stop(&self) {
371        if let Some(handle) = self.apply_handle.borrow_mut().take() {
372            drop(handle);
373        }
374    }
375
376    fn find_scope_entry<T>(&self, scope: &T) -> Option<Rc<RefCell<ScopeEntry>>>
377    where
378        T: Any + Eq + Hash + 'static,
379    {
380        if let Some(scope) = (scope as &dyn Any).downcast_ref::<RecomposeScope>() {
381            return self.fast_scopes.borrow().get(&scope.id()).cloned();
382        }
383
384        self.find_owned_scope_entry(scope)
385    }
386
387    fn insert_scope_entry(
388        &self,
389        scope: impl Any + Clone + Eq + Hash + 'static,
390        on_changed: Rc<dyn Fn(&dyn Any)>,
391    ) -> Rc<RefCell<ScopeEntry>> {
392        let entry_id = self.next_entry_id.get();
393        self.next_entry_id.set(entry_id.wrapping_add(1));
394        let recompose_scope_id = (&scope as &dyn Any)
395            .downcast_ref::<RecomposeScope>()
396            .map(RecomposeScope::id);
397        let owned_scope_key = recompose_scope_id
398            .is_none()
399            .then(|| owned_scope_index_key(&scope));
400        let entry = Rc::new(RefCell::new(ScopeEntry::new(entry_id, scope, on_changed)));
401        self.indexed_scopes
402            .borrow_mut()
403            .insert(entry_id, Rc::clone(&entry));
404        if let Some(scope_id) = recompose_scope_id {
405            self.fast_scopes
406                .borrow_mut()
407                .insert(scope_id, Rc::clone(&entry));
408        } else if let Some(scope_key) = owned_scope_key {
409            self.owned_scopes
410                .borrow_mut()
411                .entry(scope_key)
412                .or_default()
413                .push(Rc::clone(&entry));
414        }
415        entry
416    }
417
418    fn prune_dead_scopes(&self) {
419        let removed_fast = {
420            let mut fast_scopes = self.fast_scopes.borrow_mut();
421            let removed_ids: Vec<_> = fast_scopes
422                .iter()
423                .filter(|(_, entry)| !entry.borrow().should_retain())
424                .map(|(scope_id, _)| *scope_id)
425                .collect();
426            let removed = removed_ids
427                .into_iter()
428                .filter_map(|scope_id| fast_scopes.remove(&scope_id))
429                .collect::<Vec<_>>();
430            shrink_map_if_sparse(&mut fast_scopes, Self::MIN_RETAINED_SCOPE_CAPACITY);
431            removed
432        };
433
434        let removed_owned = { self.partition_owned_scopes(|entry| !entry.should_retain()) };
435
436        for entry in removed_fast.into_iter().chain(removed_owned) {
437            self.unregister_entry(&entry);
438        }
439    }
440
441    fn find_owned_scope_entry<T>(&self, scope: &T) -> Option<Rc<RefCell<ScopeEntry>>>
442    where
443        T: Any + Eq + Hash + 'static,
444    {
445        let key = owned_scope_index_key(scope);
446        self.owned_scopes.borrow().get(&key).and_then(|bucket| {
447            bucket
448                .iter()
449                .find(|entry| entry.borrow().matches_scope(scope))
450                .cloned()
451        })
452    }
453
454    fn remove_owned_scope_entry<T>(&self, scope: &T) -> Option<Rc<RefCell<ScopeEntry>>>
455    where
456        T: Any + Eq + Hash + 'static,
457    {
458        let key = owned_scope_index_key(scope);
459        let mut owned_scopes = self.owned_scopes.borrow_mut();
460        let mut removed = None;
461        let mut remove_bucket = false;
462        if let Some(bucket) = owned_scopes.get_mut(&key)
463            && let Some(index) = bucket
464                .iter()
465                .position(|entry| entry.borrow().matches_scope(scope))
466        {
467            removed = Some(bucket.remove(index));
468            remove_bucket = bucket.is_empty();
469        }
470        if remove_bucket {
471            owned_scopes.remove(&key);
472        }
473        shrink_map_if_sparse(&mut owned_scopes, Self::MIN_RETAINED_SCOPE_CAPACITY);
474        removed
475    }
476
477    fn partition_owned_scopes(
478        &self,
479        should_remove: impl Fn(&ScopeEntry) -> bool,
480    ) -> Vec<Rc<RefCell<ScopeEntry>>> {
481        let mut owned_scopes = self.owned_scopes.borrow_mut();
482        let mut retained = HashMap::default();
483        let mut removed = Vec::new();
484        for (key, mut bucket) in owned_scopes.drain() {
485            let mut retained_bucket = OwnedScopeBucket::new();
486            for entry in bucket.drain(..) {
487                if should_remove(&entry.borrow()) {
488                    removed.push(entry);
489                } else {
490                    retained_bucket.push(entry);
491                }
492            }
493            if !retained_bucket.is_empty() {
494                retained.insert(key, retained_bucket);
495            }
496        }
497        *owned_scopes = retained;
498        shrink_map_if_sparse(&mut owned_scopes, Self::MIN_RETAINED_SCOPE_CAPACITY);
499        removed
500    }
501
502    fn debug_stats(&self) -> SnapshotStateObserverDebugStats {
503        let owned_scopes = self.owned_scopes.borrow();
504        let fast_scopes = self.fast_scopes.borrow();
505        let owned_scope_len = owned_scopes.values().map(SmallVec::len).sum::<usize>();
506        let owned_scope_cap =
507            owned_scopes.capacity() + owned_scopes.values().map(SmallVec::capacity).sum::<usize>();
508        let scopes_len = owned_scope_len + fast_scopes.len();
509        let scopes_cap = owned_scope_cap + fast_scopes.capacity();
510        let mut observed_state_count = 0;
511        let mut observed_state_capacity = 0;
512        let mut stateless_scope_count = 0;
513
514        for entry in owned_scopes
515            .values()
516            .flat_map(|bucket| bucket.iter())
517            .chain(fast_scopes.values())
518        {
519            let entry = entry.borrow();
520            observed_state_count += entry.observed.len();
521            observed_state_capacity += entry.observed.capacity();
522            stateless_scope_count += usize::from(entry.observed.is_empty());
523        }
524
525        SnapshotStateObserverDebugStats {
526            scopes_len,
527            scopes_cap,
528            fast_scopes_len: fast_scopes.len(),
529            fast_scopes_cap: fast_scopes.capacity(),
530            stateless_scope_count,
531            observed_state_count,
532            observed_state_capacity,
533        }
534    }
535
536    fn run_with_read_observer<R>(&self, block: impl FnOnce() -> R) -> R {
537        // Kotlin uses Snapshot.observeInternal which creates a TransparentObserverMutableSnapshot,
538        // not a readonly snapshot. This allows writes to happen during observation (composition).
539        use crate::snapshot_v2::take_transparent_observer_mutable_snapshot;
540
541        // Create a transparent mutable snapshot (not readonly!) for observation
542        // This matches Kotlin's Snapshot.observeInternal behavior
543        let snapshot =
544            take_transparent_observer_mutable_snapshot(Some(self.read_dispatcher.clone()), None);
545        let result = snapshot.enter(block);
546        snapshot.dispose();
547        result
548    }
549
550    fn handle_apply(&self, modified: &[Arc<dyn StateObject>]) {
551        if modified.is_empty() {
552            return;
553        }
554
555        let mut seen_scope_ids: HashSet<usize> = HashSet::default();
556        let mut to_notify: Vec<Rc<RefCell<ScopeEntry>>> = Vec::new();
557        {
558            let observed_to_scopes = self.observed_to_scopes.borrow();
559            let indexed_scopes = self.indexed_scopes.borrow();
560            for state in modified {
561                if let Some(scope_ids) = observed_to_scopes.get(&state.object_id().as_usize()) {
562                    let mut ordered_scope_ids: SmallVec<[usize; 8]> =
563                        scope_ids.iter().copied().collect();
564                    ordered_scope_ids.sort_unstable();
565                    for scope_id in ordered_scope_ids {
566                        if seen_scope_ids.insert(scope_id)
567                            && let Some(entry) = indexed_scopes.get(&scope_id)
568                        {
569                            to_notify.push(entry.clone());
570                        }
571                    }
572                }
573            }
574        }
575
576        if to_notify.is_empty() {
577            return;
578        }
579
580        for entry in to_notify {
581            let executor = self.executor.clone();
582            executor(Box::new(move || {
583                if let Ok(entry) = entry.try_borrow() {
584                    entry.notify();
585                }
586            }));
587        }
588    }
589
590    fn replace_observed_ids(&self, entry: &Rc<RefCell<ScopeEntry>>, observed: ObservedIds) {
591        let (entry_id, previous) = {
592            let mut entry_mut = entry.borrow_mut();
593            let entry_id = entry_mut.id;
594            let previous = std::mem::replace(&mut entry_mut.observed, observed);
595            (entry_id, previous)
596        };
597        self.unregister_observed_ids(entry_id, &previous);
598        let entry_ref = entry.borrow();
599        self.register_observed_ids(entry_id, &entry_ref.observed);
600    }
601
602    fn register_observed_ids(&self, entry_id: usize, observed: &ObservedIds) {
603        let mut observed_to_scopes = self.observed_to_scopes.borrow_mut();
604        for state_id in observed.iter() {
605            let scope_ids = observed_to_scopes.entry(state_id).or_default();
606            scope_ids.insert(entry_id);
607        }
608    }
609
610    fn unregister_observed_ids(&self, entry_id: usize, observed: &ObservedIds) {
611        let mut observed_to_scopes = self.observed_to_scopes.borrow_mut();
612        let mut emptied = SmallVec::<[StateObjectId; MAX_OBSERVED_STATES]>::new();
613        for state_id in observed.iter() {
614            if let Some(scope_ids) = observed_to_scopes.get_mut(&state_id) {
615                scope_ids.remove(&entry_id);
616                if scope_ids.is_empty() {
617                    emptied.push(state_id);
618                }
619            }
620        }
621        for state_id in emptied {
622            observed_to_scopes.remove(&state_id);
623        }
624        shrink_map_if_sparse(&mut observed_to_scopes, Self::MIN_RETAINED_SCOPE_CAPACITY);
625    }
626
627    fn unregister_entry(&self, entry: &Rc<RefCell<ScopeEntry>>) {
628        let (entry_id, observed) = {
629            let mut entry_mut = entry.borrow_mut();
630            let observed = std::mem::replace(&mut entry_mut.observed, ObservedIds::new());
631            (entry_mut.id, observed)
632        };
633        self.unregister_observed_ids(entry_id, &observed);
634        self.indexed_scopes.borrow_mut().remove(&entry_id);
635    }
636}
637
638fn shrink_map_if_sparse<K, V>(map: &mut HashMap<K, V>, min_retained_capacity: usize)
639where
640    K: Eq + std::hash::Hash,
641{
642    if map.capacity() <= map.len().max(min_retained_capacity).saturating_mul(4) {
643        return;
644    }
645
646    let retained = map.len().max(min_retained_capacity);
647    let mut rebuilt = HashMap::default();
648    rebuilt.reserve(retained);
649    rebuilt.extend(map.drain());
650    *map = rebuilt;
651}
652
653enum ObservedIds {
654    Small(SmallVec<[ObservedState; MAX_OBSERVED_STATES]>),
655    Large(HashMap<StateObjectId, Option<Box<dyn Any>>>),
656}
657
658struct ObservedState {
659    id: StateObjectId,
660    _lease: Option<Box<dyn Any>>,
661}
662
663impl ObservedIds {
664    fn new() -> Self {
665        ObservedIds::Small(SmallVec::new())
666    }
667
668    fn insert(&mut self, state: &dyn StateObject) {
669        let id = state.object_id().as_usize();
670        match self {
671            ObservedIds::Small(small) => {
672                if small.iter().any(|observed| observed.id == id) {
673                    return;
674                }
675                if small.len() < MAX_OBSERVED_STATES {
676                    small.push(ObservedState {
677                        id,
678                        _lease: state.observation_lease(),
679                    });
680                } else {
681                    let mut large =
682                        HashMap::with_capacity_and_hasher(small.len() + 1, Default::default());
683                    for observed in small.drain(..) {
684                        large.insert(observed.id, observed._lease);
685                    }
686                    large.insert(id, state.observation_lease());
687                    *self = ObservedIds::Large(large);
688                }
689            }
690            ObservedIds::Large(large) => {
691                // One lookup rather than two: this runs per observed state per
692                // read, which is the hottest path the observer has.
693                large.entry(id).or_insert_with(|| state.observation_lease());
694            }
695        }
696    }
697
698    fn is_empty(&self) -> bool {
699        match self {
700            ObservedIds::Small(small) => small.is_empty(),
701            ObservedIds::Large(large) => large.is_empty(),
702        }
703    }
704
705    fn len(&self) -> usize {
706        match self {
707            ObservedIds::Small(small) => small.len(),
708            ObservedIds::Large(large) => large.len(),
709        }
710    }
711
712    fn capacity(&self) -> usize {
713        match self {
714            ObservedIds::Small(small) => small.capacity(),
715            ObservedIds::Large(large) => large.capacity(),
716        }
717    }
718
719    fn iter(&self) -> Box<dyn Iterator<Item = StateObjectId> + '_> {
720        match self {
721            ObservedIds::Small(small) => Box::new(small.iter().map(|observed| observed.id)),
722            ObservedIds::Large(large) => Box::new(large.keys().copied()),
723        }
724    }
725}
726
727const MAX_OBSERVED_STATES: usize = 8;
728
729enum ScopeStorage {
730    Owned(Box<dyn Any>),
731    RecomposeScope {
732        id: ScopeId,
733        weak: Weak<RecomposeScopeInner>,
734    },
735}
736
737struct ScopeEntry {
738    id: usize,
739    scope: ScopeStorage,
740    on_changed: Rc<dyn Fn(&dyn Any)>,
741    observed: ObservedIds,
742    last_seen_version: u64,
743}
744
745impl ScopeEntry {
746    fn new<T>(id: usize, scope: T, on_changed: Rc<dyn Fn(&dyn Any)>) -> Self
747    where
748        T: Any + 'static,
749    {
750        Self {
751            id,
752            scope: ScopeStorage::from_value(scope),
753            on_changed,
754            observed: ObservedIds::new(),
755            last_seen_version: u64::MAX,
756        }
757    }
758
759    fn update<T>(&mut self, new_scope: T, on_changed: Rc<dyn Fn(&dyn Any)>)
760    where
761        T: Any + 'static,
762    {
763        self.scope = ScopeStorage::from_value(new_scope);
764        self.on_changed = on_changed;
765    }
766
767    fn matches_scope<T>(&self, scope: &T) -> bool
768    where
769        T: Any + Eq + 'static,
770    {
771        if let Some(scope) = (scope as &dyn Any).downcast_ref::<RecomposeScope>() {
772            return matches!(
773                &self.scope,
774                ScopeStorage::RecomposeScope { id, .. } if *id == scope.id()
775            );
776        }
777
778        match &self.scope {
779            ScopeStorage::Owned(stored) => stored
780                .downcast_ref::<T>()
781                .map(|stored| stored == scope)
782                .unwrap_or(false),
783            ScopeStorage::RecomposeScope { .. } => false,
784        }
785    }
786
787    fn matches_predicate(&self, predicate: &impl Fn(&dyn Any) -> bool) -> bool {
788        match &self.scope {
789            ScopeStorage::Owned(scope) => predicate(scope.as_ref()),
790            ScopeStorage::RecomposeScope { weak, .. } => weak
791                .upgrade()
792                .map(|inner| predicate(&RecomposeScope { inner }))
793                .unwrap_or(true),
794        }
795    }
796
797    fn should_retain(&self) -> bool {
798        match &self.scope {
799            ScopeStorage::Owned(_) => true,
800            ScopeStorage::RecomposeScope { weak, .. } => weak.upgrade().is_some(),
801        }
802    }
803
804    fn notify(&self) {
805        match &self.scope {
806            ScopeStorage::Owned(scope) => (self.on_changed)(scope.as_ref()),
807            ScopeStorage::RecomposeScope { weak, .. } => {
808                if let Some(inner) = weak.upgrade() {
809                    (self.on_changed)(&RecomposeScope { inner });
810                }
811            }
812        }
813    }
814}
815
816impl ScopeStorage {
817    fn from_value<T>(value: T) -> Self
818    where
819        T: Any + 'static,
820    {
821        let any = &value as &dyn Any;
822        if let Some(scope) = any.downcast_ref::<RecomposeScope>() {
823            Self::RecomposeScope {
824                id: scope.id(),
825                weak: scope.downgrade(),
826            }
827        } else {
828            Self::Owned(Box::new(value))
829        }
830    }
831}
832
833#[cfg(test)]
834mod tests {
835    use std::cell::Cell;
836
837    use super::*;
838    use crate::{
839        snapshot_v2::{TestRuntimeGuard, reset_runtime_for_tests, take_mutable_snapshot},
840        state::{NeverEqual, SnapshotMutableState},
841    };
842
843    fn reset_runtime() -> TestRuntimeGuard {
844        reset_runtime_for_tests()
845    }
846
847    #[derive(Clone, Eq, Hash, PartialEq)]
848    struct TestScope(&'static str);
849
850    #[test]
851    fn notifies_scope_when_state_changes() {
852        let _guard = reset_runtime();
853
854        let state = SnapshotMutableState::new_in_arc(0, Arc::new(NeverEqual));
855        let triggered = Rc::new(Cell::new(0));
856        let observer_trigger = triggered.clone();
857
858        let observer = SnapshotStateObserver::new(|callback| callback());
859        observer.start();
860
861        let scope = TestScope("scope");
862        observer.observe_reads(
863            scope.clone(),
864            move |_| {
865                observer_trigger.set(observer_trigger.get() + 1);
866            },
867            || {
868                let _ = state.get();
869            },
870        );
871
872        let snapshot = take_mutable_snapshot(None, None);
873        snapshot.enter(|| {
874            state.set(1);
875        });
876        snapshot.apply().check();
877
878        assert_eq!(triggered.get(), 1);
879        observer.stop();
880    }
881
882    #[test]
883    fn clear_removes_scope_observation() {
884        let _guard = reset_runtime();
885
886        let state = SnapshotMutableState::new_in_arc(0, Arc::new(NeverEqual));
887        let triggered = Rc::new(Cell::new(0));
888        let observer_trigger = triggered.clone();
889
890        let observer = SnapshotStateObserver::new(|callback| callback());
891        observer.start();
892
893        let scope = TestScope("scope");
894        observer.observe_reads(
895            scope.clone(),
896            move |_| {
897                observer_trigger.set(observer_trigger.get() + 1);
898            },
899            || {
900                let _ = state.get();
901            },
902        );
903
904        observer.clear(&scope);
905
906        let snapshot = take_mutable_snapshot(None, None);
907        snapshot.enter(|| {
908            state.set(1);
909        });
910        snapshot.apply().check();
911
912        assert_eq!(triggered.get(), 0);
913        observer.stop();
914    }
915
916    #[test]
917    fn repeated_owned_scope_observations_reuse_the_same_entry() {
918        let _guard = reset_runtime();
919
920        let state = SnapshotMutableState::new_in_arc(0, Arc::new(NeverEqual));
921        let observer = SnapshotStateObserver::new(|callback| callback());
922        let scope = TestScope("scope");
923
924        observer.observe_reads(
925            scope.clone(),
926            |_| {},
927            || {
928                let _ = state.get();
929            },
930        );
931        observer.observe_reads(
932            scope,
933            |_| {},
934            || {
935                let _ = state.get();
936            },
937        );
938
939        let stats = observer.debug_stats();
940        assert_eq!(stats.scopes_len, 1);
941        assert_eq!(stats.fast_scopes_len, 0);
942    }
943
944    #[test]
945    fn with_no_observations_skips_reads() {
946        let _guard = reset_runtime();
947
948        let state = SnapshotMutableState::new_in_arc(0, Arc::new(NeverEqual));
949        let triggered = Rc::new(Cell::new(0));
950        let observer_trigger = triggered.clone();
951
952        let observer = SnapshotStateObserver::new(|callback| callback());
953        observer.start();
954
955        let scope = TestScope("scope");
956        observer.observe_reads(
957            scope.clone(),
958            move |_| {
959                observer_trigger.set(observer_trigger.get() + 1);
960            },
961            || {
962                observer.with_no_observations(|| {
963                    let _ = state.get();
964                });
965            },
966        );
967
968        let snapshot = take_mutable_snapshot(None, None);
969        snapshot.enter(|| {
970            state.set(1);
971        });
972        snapshot.apply().check();
973
974        assert_eq!(triggered.get(), 0);
975        observer.stop();
976    }
977
978    #[test]
979    fn nested_observe_reads_attributes_state_to_innermost_scope_only() {
980        let _guard = reset_runtime();
981
982        let state = SnapshotMutableState::new_in_arc(0, Arc::new(NeverEqual));
983        let outer_triggered = Rc::new(Cell::new(0));
984        let inner_triggered = Rc::new(Cell::new(0));
985
986        let observer = SnapshotStateObserver::new(|callback| callback());
987        observer.start();
988
989        let outer_scope = TestScope("outer");
990        let inner_scope = TestScope("inner");
991        observer.observe_reads(
992            outer_scope.clone(),
993            {
994                let outer_triggered = Rc::clone(&outer_triggered);
995                move |_| outer_triggered.set(outer_triggered.get() + 1)
996            },
997            || {
998                observer.observe_reads(
999                    inner_scope.clone(),
1000                    {
1001                        let inner_triggered = Rc::clone(&inner_triggered);
1002                        move |_| inner_triggered.set(inner_triggered.get() + 1)
1003                    },
1004                    || {
1005                        let _ = state.get();
1006                    },
1007                );
1008            },
1009        );
1010
1011        let snapshot = take_mutable_snapshot(None, None);
1012        snapshot.enter(|| {
1013            state.set(1);
1014        });
1015        snapshot.apply().check();
1016
1017        assert_eq!(outer_triggered.get(), 0);
1018        assert_eq!(inner_triggered.get(), 1);
1019        observer.stop();
1020    }
1021
1022    #[test]
1023    fn clearing_one_scope_keeps_shared_state_registered_for_other_scope() {
1024        let _guard = reset_runtime();
1025
1026        let state = SnapshotMutableState::new_in_arc(0, Arc::new(NeverEqual));
1027        let first_triggered = Rc::new(Cell::new(0));
1028        let second_triggered = Rc::new(Cell::new(0));
1029
1030        let observer = SnapshotStateObserver::new(|callback| callback());
1031        observer.start();
1032
1033        let first_scope = TestScope("first");
1034        let second_scope = TestScope("second");
1035        observer.observe_reads(
1036            first_scope.clone(),
1037            {
1038                let first_triggered = Rc::clone(&first_triggered);
1039                move |_| first_triggered.set(first_triggered.get() + 1)
1040            },
1041            || {
1042                let _ = state.get();
1043            },
1044        );
1045        observer.observe_reads(
1046            second_scope.clone(),
1047            {
1048                let second_triggered = Rc::clone(&second_triggered);
1049                move |_| second_triggered.set(second_triggered.get() + 1)
1050            },
1051            || {
1052                let _ = state.get();
1053            },
1054        );
1055
1056        observer.clear(&first_scope);
1057
1058        let snapshot = take_mutable_snapshot(None, None);
1059        snapshot.enter(|| {
1060            state.set(1);
1061        });
1062        snapshot.apply().check();
1063
1064        assert_eq!(first_triggered.get(), 0);
1065        assert_eq!(second_triggered.get(), 1);
1066        observer.stop();
1067    }
1068
1069    #[test]
1070    fn shared_state_notifies_scopes_in_registration_order() {
1071        let _guard = reset_runtime();
1072
1073        let state = SnapshotMutableState::new_in_arc(0, Arc::new(NeverEqual));
1074        let notifications = Rc::new(RefCell::new(Vec::new()));
1075
1076        let observer = SnapshotStateObserver::new(|callback| callback());
1077        observer.start();
1078
1079        observer.observe_reads(
1080            TestScope("first"),
1081            {
1082                let notifications = Rc::clone(&notifications);
1083                move |_| notifications.borrow_mut().push("first")
1084            },
1085            || {
1086                let _ = state.get();
1087            },
1088        );
1089        observer.observe_reads(
1090            TestScope("second"),
1091            {
1092                let notifications = Rc::clone(&notifications);
1093                move |_| notifications.borrow_mut().push("second")
1094            },
1095            || {
1096                let _ = state.get();
1097            },
1098        );
1099
1100        let snapshot = take_mutable_snapshot(None, None);
1101        snapshot.enter(|| {
1102            state.set(1);
1103        });
1104        snapshot.apply().check();
1105
1106        assert_eq!(notifications.borrow().as_slice(), &["first", "second"]);
1107        observer.stop();
1108    }
1109
1110    #[test]
1111    fn stateless_recompose_scope_does_not_retain_observer_entry() {
1112        let _guard = reset_runtime();
1113
1114        let observer = SnapshotStateObserver::new(|callback| callback());
1115        let runtime = crate::TestRuntime::new();
1116        let scope = RecomposeScope::new_for_test(runtime.handle());
1117
1118        observer.observe_reads(scope, |_| {}, || {});
1119
1120        let stats = observer.debug_stats();
1121        assert_eq!(stats.scopes_len, 0);
1122        assert_eq!(stats.fast_scopes_len, 0);
1123        assert_eq!(stats.stateless_scope_count, 0);
1124    }
1125
1126    #[test]
1127    fn scope_that_stops_reading_state_is_removed_immediately() {
1128        let _guard = reset_runtime();
1129
1130        let state = SnapshotMutableState::new_in_arc(0, Arc::new(NeverEqual));
1131        let observer = SnapshotStateObserver::new(|callback| callback());
1132        let runtime = crate::TestRuntime::new();
1133        let scope = RecomposeScope::new_for_test(runtime.handle());
1134        let triggered = Rc::new(Cell::new(0));
1135        let observer_trigger = Rc::clone(&triggered);
1136
1137        observer.observe_reads(
1138            scope.clone(),
1139            move |_| observer_trigger.set(observer_trigger.get() + 1),
1140            || {
1141                let _ = state.get();
1142            },
1143        );
1144
1145        let after_stateful = observer.debug_stats();
1146        assert_eq!(after_stateful.scopes_len, 1);
1147        assert_eq!(after_stateful.fast_scopes_len, 1);
1148
1149        observer.observe_reads(scope, |_| {}, || {});
1150
1151        let after_stateless = observer.debug_stats();
1152        assert_eq!(after_stateless.scopes_len, 0);
1153        assert_eq!(after_stateless.fast_scopes_len, 0);
1154
1155        let snapshot = take_mutable_snapshot(None, None);
1156        snapshot.enter(|| {
1157            state.set(1);
1158        });
1159        snapshot.apply().check();
1160
1161        assert_eq!(triggered.get(), 0);
1162    }
1163
1164    #[test]
1165    fn begin_frame_prunes_dropped_recompose_scope_entries() {
1166        let _guard = reset_runtime();
1167
1168        let state = SnapshotMutableState::new_in_arc(0, Arc::new(NeverEqual));
1169        let observer = SnapshotStateObserver::new(|callback| callback());
1170        let runtime = crate::TestRuntime::new();
1171        let scope = RecomposeScope::new_for_test(runtime.handle());
1172
1173        observer.observe_reads(
1174            scope.clone(),
1175            |_| {},
1176            || {
1177                let _ = state.get();
1178            },
1179        );
1180
1181        let before_prune = observer.debug_stats();
1182        assert_eq!(before_prune.scopes_len, 1);
1183        assert_eq!(before_prune.fast_scopes_len, 1);
1184
1185        drop(scope);
1186        observer.begin_frame();
1187
1188        let after_prune = observer.debug_stats();
1189        assert_eq!(after_prune.scopes_len, 0);
1190        assert_eq!(after_prune.fast_scopes_len, 0);
1191    }
1192}