Skip to main content

cranpose_core/
runtime.rs

1use crate::collections::map::HashMap;
2use crate::collections::map::HashSet;
3use crate::state::{MutationPolicy, NeverEqual};
4use crate::MutableStateInner;
5use std::any::Any;
6use std::cell::{Cell, RefCell};
7use std::collections::VecDeque;
8use std::future::Future;
9use std::pin::Pin;
10use std::rc::{Rc, Weak};
11use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
12use std::sync::{mpsc, Arc};
13use std::task::{Context, Poll, Waker};
14use std::thread::ThreadId;
15use std::thread_local;
16
17#[cfg(any(feature = "internal", test))]
18use crate::frame_clock::FrameClock;
19use crate::platform::RuntimeScheduler;
20use crate::{Applier, Command, FrameCallbackId, NodeError, RecomposeScopeInner, ScopeId};
21
22#[derive(Clone, Copy, PartialEq, Eq)]
23pub(crate) enum FrameCallbackKind {
24    Transient,
25    Perpetual,
26}
27
28enum UiMessage {
29    Task(Box<dyn FnOnce() + Send + 'static>),
30    Invoke { id: u64, value: Box<dyn Any + Send> },
31}
32
33type UiContinuation = Box<dyn Fn(Box<dyn Any>) -> bool + 'static>;
34type UiContinuationMap = HashMap<u64, UiContinuation>;
35
36struct TypedStateCell<T: Clone + 'static> {
37    inner: MutableStateInner<T>,
38}
39
40trait ScopeWatchCell {
41    fn unregister_scope(&self, scope_id: ScopeId);
42}
43
44impl<T: Clone + 'static> ScopeWatchCell for TypedStateCell<T> {
45    fn unregister_scope(&self, scope_id: ScopeId) {
46        self.inner.unregister_scope(scope_id);
47    }
48}
49
50struct StateArenaSlot {
51    generation: u32,
52    cell: Option<Rc<dyn Any>>,
53    watcher_cell: Option<Rc<dyn ScopeWatchCell>>,
54    lease: Option<Weak<StateHandleLease>>,
55}
56
57#[derive(Default)]
58struct StateArenaInner {
59    cells: Vec<StateArenaSlot>,
60    free: Vec<u32>,
61}
62
63#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
64pub struct StateArenaDebugStats {
65    pub cells_len: usize,
66    pub cells_cap: usize,
67    pub free_len: usize,
68    pub free_cap: usize,
69}
70
71#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
72pub struct RuntimeDebugStats {
73    pub node_updates_len: usize,
74    pub node_updates_cap: usize,
75    pub invalid_scopes_len: usize,
76    pub invalid_scopes_cap: usize,
77    pub scope_queue_len: usize,
78    pub scope_queue_cap: usize,
79    pub frame_callbacks_len: usize,
80    pub frame_callbacks_cap: usize,
81    pub local_tasks_len: usize,
82    pub local_tasks_cap: usize,
83    pub ui_conts_len: usize,
84    pub ui_conts_cap: usize,
85    pub tasks_len: usize,
86    pub tasks_cap: usize,
87    pub external_state_owners_len: usize,
88    pub external_state_owners_cap: usize,
89    pub ui_dispatcher_pending: usize,
90}
91
92#[derive(Default)]
93pub(crate) struct StateArena {
94    inner: RefCell<StateArenaInner>,
95}
96
97impl StateArena {
98    pub(crate) fn alloc<T: Clone + 'static>(&self, value: T, runtime: RuntimeHandle) -> StateId {
99        self.alloc_with_policy(value, runtime, Arc::new(NeverEqual))
100    }
101
102    pub(crate) fn alloc_with_policy<T: Clone + 'static>(
103        &self,
104        value: T,
105        runtime: RuntimeHandle,
106        policy: Arc<dyn MutationPolicy<T>>,
107    ) -> StateId {
108        let (slot, generation) = {
109            let mut inner = self.inner.borrow_mut();
110            loop {
111                let Some(slot) = inner.free.pop() else {
112                    let slot = inner.cells.len() as u32;
113                    inner.cells.push(StateArenaSlot {
114                        generation: 0,
115                        cell: None,
116                        watcher_cell: None,
117                        lease: None,
118                    });
119                    break (slot, 0);
120                };
121
122                let Some(entry) = inner.cells.get_mut(slot as usize) else {
123                    continue;
124                };
125                if entry.cell.is_some() {
126                    continue;
127                }
128
129                entry.watcher_cell = None;
130                entry.lease = None;
131                entry.generation = entry.generation.wrapping_add(1);
132                break (slot, entry.generation);
133            }
134        };
135        let id = StateId::new(slot, generation);
136        let inner = MutableStateInner::new_with_policy(value, runtime.clone(), policy);
137        inner.install_snapshot_observer(id);
138        let typed_cell = Rc::new(TypedStateCell { inner });
139        let cell: Rc<dyn Any> = typed_cell.clone();
140        let watcher_cell: Rc<dyn ScopeWatchCell> = typed_cell;
141        let mut arena = self.inner.borrow_mut();
142        let slot_entry = &mut arena.cells[slot as usize];
143        slot_entry.cell = Some(cell);
144        slot_entry.watcher_cell = Some(watcher_cell);
145        id
146    }
147
148    fn get_cell_opt(&self, id: StateId) -> Option<Rc<dyn Any>> {
149        self.inner
150            .borrow()
151            .cells
152            .get(id.slot_index())
153            .filter(|cell| cell.generation == id.generation())
154            .and_then(|cell| cell.cell.as_ref())
155            .cloned()
156    }
157
158    fn get_typed<T: Clone + 'static>(&self, id: StateId) -> Rc<TypedStateCell<T>> {
159        match self.get_cell_opt(id) {
160            None => panic!(
161                "state cell missing: slot={}, gen={}, expected={}",
162                id.slot(),
163                id.generation(),
164                std::any::type_name::<T>(),
165            ),
166            Some(cell) => Rc::downcast::<TypedStateCell<T>>(cell).unwrap_or_else(|_| {
167                panic!(
168                    "state cell type mismatch: slot={}, gen={}, expected={}",
169                    id.slot(),
170                    id.generation(),
171                    std::any::type_name::<T>(),
172                )
173            }),
174        }
175    }
176
177    fn get_typed_opt<T: Clone + 'static>(&self, id: StateId) -> Option<Rc<TypedStateCell<T>>> {
178        Rc::downcast::<TypedStateCell<T>>(self.get_cell_opt(id)?).ok()
179    }
180
181    pub(crate) fn with_typed<T: Clone + 'static, R>(
182        &self,
183        id: StateId,
184        f: impl FnOnce(&MutableStateInner<T>) -> R,
185    ) -> R {
186        let cell = self.get_typed::<T>(id);
187        f(&cell.inner)
188    }
189
190    pub(crate) fn with_typed_opt<T: Clone + 'static, R>(
191        &self,
192        id: StateId,
193        f: impl FnOnce(&MutableStateInner<T>) -> R,
194    ) -> Option<R> {
195        let cell = self.get_typed_opt::<T>(id)?;
196        Some(f(&cell.inner))
197    }
198
199    pub(crate) fn release(&self, id: StateId) {
200        let cell = {
201            let mut inner = self.inner.borrow_mut();
202            let Some(slot) = inner.cells.get_mut(id.slot_index()) else {
203                return;
204            };
205            if slot.generation != id.generation() {
206                return;
207            }
208            slot.lease = None;
209            slot.watcher_cell = None;
210            let cell = slot.cell.take();
211            if cell.is_some() {
212                inner.free.push(id.slot());
213            }
214            cell
215        };
216        drop(cell);
217    }
218
219    pub(crate) fn stats(&self) -> (usize, usize) {
220        let inner = self.inner.borrow();
221        (inner.cells.len(), inner.free.len())
222    }
223
224    pub(crate) fn debug_stats(&self) -> StateArenaDebugStats {
225        let inner = self.inner.borrow();
226        StateArenaDebugStats {
227            cells_len: inner.cells.len(),
228            cells_cap: inner.cells.capacity(),
229            free_len: inner.free.len(),
230            free_cap: inner.free.capacity(),
231        }
232    }
233
234    pub(crate) fn unregister_scope(&self, id: StateId, scope_id: ScopeId) {
235        let watcher_cell = {
236            let inner = self.inner.borrow();
237            inner
238                .cells
239                .get(id.slot_index())
240                .filter(|slot| slot.generation == id.generation())
241                .and_then(|slot| slot.watcher_cell.as_ref())
242                .cloned()
243        };
244        if let Some(watcher_cell) = watcher_cell {
245            watcher_cell.unregister_scope(scope_id);
246        }
247    }
248
249    pub(crate) fn register_lease(&self, id: StateId, lease: &Rc<StateHandleLease>) {
250        let mut inner = self.inner.borrow_mut();
251        let Some(slot) = inner.cells.get_mut(id.slot_index()) else {
252            return;
253        };
254        if slot.generation != id.generation() {
255            return;
256        }
257        slot.lease = Some(Rc::downgrade(lease));
258    }
259
260    pub(crate) fn retain_lease(&self, id: StateId) -> Option<Rc<StateHandleLease>> {
261        let inner = self.inner.borrow();
262        let slot = inner.cells.get(id.slot_index())?;
263        if slot.generation != id.generation() {
264            return None;
265        }
266        slot.lease.as_ref()?.upgrade()
267    }
268}
269
270#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
271pub struct StateId {
272    slot: u32,
273    generation: u32,
274}
275
276impl StateId {
277    const fn new(slot: u32, generation: u32) -> Self {
278        Self { slot, generation }
279    }
280
281    pub(crate) const fn slot(self) -> u32 {
282        self.slot
283    }
284
285    pub(crate) const fn slot_index(self) -> usize {
286        self.slot as usize
287    }
288
289    pub(crate) const fn generation(self) -> u32 {
290        self.generation
291    }
292}
293
294#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
295pub struct RuntimeId(u32);
296
297impl RuntimeId {
298    fn next() -> Self {
299        NEXT_RUNTIME_ID.with(|next| {
300            let id = next.get();
301            next.set(id.wrapping_add(1));
302            Self(id)
303        })
304    }
305}
306
307struct UiDispatcherInner {
308    scheduler: Arc<dyn RuntimeScheduler>,
309    tx: mpsc::Sender<UiMessage>,
310    pending: AtomicUsize,
311}
312
313impl UiDispatcherInner {
314    fn new(scheduler: Arc<dyn RuntimeScheduler>, tx: mpsc::Sender<UiMessage>) -> Self {
315        Self {
316            scheduler,
317            tx,
318            pending: AtomicUsize::new(0),
319        }
320    }
321
322    fn post(&self, task: impl FnOnce() + Send + 'static) {
323        self.pending.fetch_add(1, Ordering::SeqCst);
324        if self.tx.send(UiMessage::Task(Box::new(task))).is_ok() {
325            self.scheduler.schedule_frame();
326        } else {
327            self.pending.fetch_sub(1, Ordering::SeqCst);
328        }
329    }
330
331    fn post_invoke(&self, id: u64, value: Box<dyn Any + Send>) {
332        self.pending.fetch_add(1, Ordering::SeqCst);
333        if self.tx.send(UiMessage::Invoke { id, value }).is_ok() {
334            self.scheduler.schedule_frame();
335        } else {
336            self.pending.fetch_sub(1, Ordering::SeqCst);
337        }
338    }
339
340    fn has_pending(&self) -> bool {
341        self.pending.load(Ordering::SeqCst) > 0
342    }
343}
344
345struct PendingGuard<'a> {
346    counter: &'a AtomicUsize,
347}
348
349impl<'a> PendingGuard<'a> {
350    fn new(counter: &'a AtomicUsize) -> Self {
351        Self { counter }
352    }
353}
354
355impl<'a> Drop for PendingGuard<'a> {
356    fn drop(&mut self) {
357        let mut current = self.counter.load(Ordering::SeqCst);
358        loop {
359            if current == 0 {
360                return;
361            }
362            match self.counter.compare_exchange(
363                current,
364                current - 1,
365                Ordering::SeqCst,
366                Ordering::SeqCst,
367            ) {
368                Ok(_) => return,
369                Err(next) => current = next,
370            }
371        }
372    }
373}
374
375#[derive(Clone)]
376pub struct UiDispatcher {
377    inner: Arc<UiDispatcherInner>,
378}
379
380impl UiDispatcher {
381    fn new(inner: Arc<UiDispatcherInner>) -> Self {
382        Self { inner }
383    }
384
385    pub fn post(&self, task: impl FnOnce() + Send + 'static) {
386        self.inner.post(task);
387    }
388
389    pub fn post_invoke<T>(&self, id: u64, value: T)
390    where
391        T: Send + 'static,
392    {
393        self.inner.post_invoke(id, Box::new(value));
394    }
395
396    pub fn has_pending(&self) -> bool {
397        self.inner.has_pending()
398    }
399}
400
401struct RuntimeInner {
402    scheduler: Arc<dyn RuntimeScheduler>,
403    needs_frame: RefCell<bool>,
404    node_updates: RefCell<Vec<Command>>,
405    invalid_scopes: RefCell<HashSet<ScopeId>>,
406    scope_queue: RefCell<Vec<(ScopeId, Weak<RecomposeScopeInner>)>>,
407    frame_callbacks: RefCell<VecDeque<FrameCallbackEntry>>,
408    next_frame_callback_id: Cell<u64>,
409    last_frame_time_nanos: Cell<Option<u64>>,
410    ui_dispatcher: Arc<UiDispatcherInner>,
411    ui_rx: RefCell<mpsc::Receiver<UiMessage>>,
412    local_tasks: RefCell<VecDeque<Box<dyn FnOnce() + 'static>>>,
413    ui_conts: RefCell<UiContinuationMap>,
414    next_cont_id: Cell<u64>,
415    ui_thread_id: ThreadId,
416    tasks: RefCell<Vec<TaskEntry>>,
417    next_task_id: Cell<u64>,
418    state_arena: StateArena,
419    external_state_owners: RefCell<HashMap<StateId, Rc<StateHandleLease>>>,
420    live_recompose_scope_count: Cell<usize>,
421    runtime_id: RuntimeId,
422}
423
424struct TaskEntry {
425    id: u64,
426    label: String,
427    future: Pin<Box<dyn Future<Output = ()> + 'static>>,
428    /// Whether this task can currently make progress.
429    ///
430    /// Set when the task is spawned and every time its waker fires, cleared
431    /// immediately before it is polled. A task that returned `Poll::Pending`
432    /// and has not been woken since is *parked*: polling it again would only
433    /// return `Poll::Pending` a second time, and — more importantly — it is
434    /// not a reason to keep asking the display for frames. See
435    /// [`RuntimeInner::has_pending_ui`].
436    ///
437    /// Shared with the task's [`Waker`], which may be woken from any thread.
438    runnable: Arc<AtomicBool>,
439    /// The waker handed to this task's future, so a wake can be attributed to
440    /// the one task that is ready rather than to all of them.
441    waker: Waker,
442}
443
444thread_local! {
445    static NEXT_TASK_LABEL: RefCell<Option<String>> = const { RefCell::new(None) };
446}
447
448pub fn label_next_ui_task(label: impl Into<String>) {
449    NEXT_TASK_LABEL.with(|held| *held.borrow_mut() = Some(label.into()));
450}
451
452impl RuntimeInner {
453    fn new(scheduler: Arc<dyn RuntimeScheduler>) -> Self {
454        let (tx, rx) = mpsc::channel();
455        let dispatcher = Arc::new(UiDispatcherInner::new(scheduler.clone(), tx));
456        Self {
457            scheduler,
458            needs_frame: RefCell::new(false),
459            node_updates: RefCell::new(Vec::new()),
460            invalid_scopes: RefCell::new(HashSet::default()),
461            scope_queue: RefCell::new(Vec::new()),
462            frame_callbacks: RefCell::new(VecDeque::new()),
463            next_frame_callback_id: Cell::new(1),
464            last_frame_time_nanos: Cell::new(None),
465            ui_dispatcher: dispatcher,
466            ui_rx: RefCell::new(rx),
467            local_tasks: RefCell::new(VecDeque::new()),
468            ui_conts: RefCell::new(UiContinuationMap::default()),
469            next_cont_id: Cell::new(1),
470            ui_thread_id: std::thread::current().id(),
471            tasks: RefCell::new(Vec::new()),
472            next_task_id: Cell::new(1),
473            state_arena: StateArena::default(),
474            external_state_owners: RefCell::new(HashMap::default()),
475            live_recompose_scope_count: Cell::new(0),
476            runtime_id: RuntimeId::next(),
477        }
478    }
479
480    fn schedule(&self) {
481        *self.needs_frame.borrow_mut() = true;
482        self.scheduler.schedule_frame();
483    }
484
485    fn enqueue_update(&self, command: Command) {
486        self.node_updates.borrow_mut().push(command);
487        self.schedule(); // Ensure frame is scheduled to process the command
488    }
489
490    fn take_updates(&self) -> Vec<Command> {
491        let updates = self.node_updates.borrow_mut().drain(..).collect::<Vec<_>>();
492        updates
493    }
494
495    fn has_updates(&self) -> bool {
496        !self.node_updates.borrow().is_empty() || self.has_invalid_scopes()
497    }
498
499    fn register_invalid_scope(&self, id: ScopeId, scope: Weak<RecomposeScopeInner>) {
500        let mut invalid = self.invalid_scopes.borrow_mut();
501        if invalid.insert(id) {
502            self.scope_queue.borrow_mut().push((id, scope));
503            self.schedule();
504        }
505    }
506
507    fn requeue_invalid_scope(&self, id: ScopeId, scope: Weak<RecomposeScopeInner>) {
508        if self.invalid_scopes.borrow().contains(&id) {
509            self.scope_queue.borrow_mut().push((id, scope));
510            self.schedule();
511        }
512    }
513
514    fn mark_scope_recomposed(&self, id: ScopeId) {
515        self.invalid_scopes.borrow_mut().remove(&id);
516    }
517
518    fn take_invalidated_scopes(&self) -> Vec<(ScopeId, Weak<RecomposeScopeInner>)> {
519        let mut queue = self.scope_queue.borrow_mut();
520        if queue.is_empty() {
521            return Vec::new();
522        }
523        let pending: Vec<_> = queue.drain(..).collect();
524        drop(queue);
525        let invalid = self.invalid_scopes.borrow();
526        pending
527            .into_iter()
528            .filter(|(id, _)| invalid.contains(id))
529            .collect()
530    }
531
532    fn has_invalid_scopes(&self) -> bool {
533        !self.invalid_scopes.borrow().is_empty()
534    }
535
536    fn increment_live_recompose_scope_count(&self) {
537        self.live_recompose_scope_count
538            .set(self.live_recompose_scope_count.get().saturating_add(1));
539    }
540
541    fn decrement_live_recompose_scope_count(&self) {
542        self.live_recompose_scope_count
543            .set(self.live_recompose_scope_count.get().saturating_sub(1));
544    }
545
546    fn live_recompose_scope_count(&self) -> usize {
547        self.live_recompose_scope_count.get()
548    }
549
550    fn has_frame_callbacks(&self) -> bool {
551        !self.frame_callbacks.borrow().is_empty()
552    }
553
554    fn has_transient_frame_callbacks(&self) -> bool {
555        self.frame_callbacks
556            .borrow()
557            .iter()
558            .any(|entry| entry.kind == FrameCallbackKind::Transient)
559    }
560
561    /// Queues a closure that is already bound to the UI thread's local queue.
562    ///
563    /// The closure may capture `Rc`/`RefCell` values because it never leaves the
564    /// runtime thread. Callers must only invoke this from the runtime thread.
565    fn enqueue_ui_task(&self, task: Box<dyn FnOnce() + 'static>) {
566        self.local_tasks.borrow_mut().push_back(task);
567        self.schedule();
568    }
569
570    fn spawn_ui_task(&self, future: Pin<Box<dyn Future<Output = ()> + 'static>>) -> u64 {
571        let id = self.next_task_id.get();
572        self.next_task_id.set(id + 1);
573        let label = NEXT_TASK_LABEL
574            .with(|held| held.borrow_mut().take())
575            .unwrap_or_else(|| "unnamed".to_string());
576        let runnable = Arc::new(AtomicBool::new(true));
577        let waker = RuntimeTaskWaker::new(self, Arc::clone(&runnable)).into_waker();
578        self.tasks.borrow_mut().push(TaskEntry {
579            id,
580            label,
581            future,
582            runnable,
583            waker,
584        });
585        self.schedule();
586        id
587    }
588
589    fn cancel_task(&self, id: u64) {
590        let mut tasks = self.tasks.borrow_mut();
591        if tasks.iter().any(|entry| entry.id == id) {
592            tasks.retain(|entry| entry.id != id);
593        }
594    }
595
596    /// Whether a spawned task is still registered. A task that completed or was
597    /// cancelled is gone from the list.
598    fn has_task(&self, id: u64) -> bool {
599        // `poll_async_tasks` takes the list while polling, so a task being
600        // polled right now is momentarily absent. Only the UI thread polls, and
601        // only the UI thread asks, so that window is never observed.
602        self.tasks
603            .try_borrow()
604            .map(|tasks| tasks.iter().any(|entry| entry.id == id))
605            .unwrap_or(true)
606    }
607
608    fn poll_async_tasks(&self) -> bool {
609        let mut tasks_ref = self.tasks.borrow_mut();
610        let tasks = std::mem::take(&mut *tasks_ref);
611        drop(tasks_ref);
612        let mut pending = Vec::with_capacity(tasks.len());
613        let mut made_progress = false;
614        for mut entry in tasks.into_iter() {
615            // Claim the wake before polling, not after: the future may be woken
616            // from another thread while it is being polled, and that wake has to
617            // survive into the next pass rather than be cleared by this one.
618            if !entry.runnable.swap(false, Ordering::AcqRel) {
619                pending.push(entry);
620                continue;
621            }
622            let mut cx = Context::from_waker(&entry.waker);
623            match entry.future.as_mut().poll(&mut cx) {
624                Poll::Ready(()) => {
625                    made_progress = true;
626                }
627                Poll::Pending => {
628                    pending.push(entry);
629                }
630            }
631        }
632        if !pending.is_empty() {
633            self.tasks.borrow_mut().extend(pending);
634        }
635        made_progress
636    }
637
638    fn drain_ui(&self) {
639        loop {
640            let mut executed = false;
641
642            {
643                let rx = &mut *self.ui_rx.borrow_mut();
644                for message in rx.try_iter() {
645                    executed = true;
646                    let _guard = PendingGuard::new(&self.ui_dispatcher.pending);
647                    match message {
648                        UiMessage::Task(task) => {
649                            task();
650                        }
651                        UiMessage::Invoke { id, value } => {
652                            self.invoke_ui_cont(id, value);
653                        }
654                    }
655                }
656            }
657
658            loop {
659                let task = {
660                    let mut local = self.local_tasks.borrow_mut();
661                    local.pop_front()
662                };
663
664                match task {
665                    Some(task) => {
666                        executed = true;
667                        task();
668                    }
669                    None => break,
670                }
671            }
672
673            if self.poll_async_tasks() {
674                executed = true;
675            }
676
677            if !executed {
678                break;
679            }
680        }
681
682        // Draining is the point at which tasks park, so it is also the first
683        // moment we can tell that the runtime has gone quiet. Checking here
684        // rather than waiting for the next frame-callback drain saves the app
685        // one wasted wake every time it settles.
686        self.clear_needs_frame_if_idle();
687    }
688
689    fn has_pending_ui(&self) -> bool {
690        let local_pending = self
691            .local_tasks
692            .try_borrow()
693            .map(|tasks| !tasks.is_empty())
694            .unwrap_or(true);
695
696        local_pending || self.ui_dispatcher.has_pending() || self.has_runnable_tasks()
697    }
698
699    /// Whether any spawned task could make progress if it were polled now.
700    ///
701    /// Only *runnable* tasks count. A task that is merely alive is not pending
702    /// work: an effect awaiting a back gesture, a network reply, or a frame it
703    /// has not asked for yet will do nothing if polled, so counting it as
704    /// pending pinned `needs_frame` true for the lifetime of the composition.
705    /// Since almost every app spawns at least one long-lived effect, that kept
706    /// the frame clock running forever and asked the display for 60 frames a
707    /// second on a screen where nothing moved. A task awaiting the frame clock
708    /// keeps frames coming through `has_frame_callbacks` instead, which is the
709    /// honest reason to want one.
710    fn has_runnable_tasks(&self) -> bool {
711        self.tasks
712            .try_borrow()
713            .map(|tasks| {
714                tasks
715                    .iter()
716                    .any(|task| task.runnable.load(Ordering::Acquire))
717            })
718            .unwrap_or(true)
719    }
720
721    fn register_ui_cont<T: 'static>(&self, f: impl FnOnce(T) + 'static) -> u64 {
722        debug_assert_eq!(
723            std::thread::current().id(),
724            self.ui_thread_id,
725            "UI continuation registered off the runtime thread",
726        );
727        let id = self.next_cont_id.get();
728        self.next_cont_id.set(id + 1);
729        let callback = RefCell::new(Some(f));
730        self.ui_conts.borrow_mut().insert(
731            id,
732            Box::new(move |value: Box<dyn Any>| {
733                let Ok(value) = value.downcast::<T>() else {
734                    return false;
735                };
736                let Some(slot) = callback.borrow_mut().take() else {
737                    return true;
738                };
739                slot(*value);
740                true
741            }),
742        );
743        id
744    }
745
746    fn invoke_ui_cont(&self, id: u64, value: Box<dyn Any + Send>) {
747        debug_assert_eq!(
748            std::thread::current().id(),
749            self.ui_thread_id,
750            "UI continuation invoked off the runtime thread",
751        );
752        let callback = { self.ui_conts.borrow_mut().remove(&id) };
753        if let Some(callback) = callback {
754            let value: Box<dyn Any> = value;
755            if !callback(value) {
756                self.ui_conts.borrow_mut().insert(id, callback);
757            }
758        }
759    }
760
761    fn cancel_ui_cont(&self, id: u64) {
762        self.ui_conts.borrow_mut().remove(&id);
763    }
764
765    fn register_frame_callback(
766        &self,
767        kind: FrameCallbackKind,
768        callback: Box<dyn FnOnce(u64) + 'static>,
769    ) -> FrameCallbackId {
770        let id = self.next_frame_callback_id.get();
771        self.next_frame_callback_id.set(id + 1);
772        self.frame_callbacks
773            .borrow_mut()
774            .push_back(FrameCallbackEntry {
775                id,
776                kind,
777                callback: Some(callback),
778            });
779        self.schedule();
780        id
781    }
782
783    fn cancel_frame_callback(&self, id: FrameCallbackId) {
784        let mut callbacks = self.frame_callbacks.borrow_mut();
785        if let Some(index) = callbacks.iter().position(|entry| entry.id == id) {
786            callbacks.remove(index);
787        }
788        drop(callbacks);
789        self.clear_needs_frame_if_idle();
790    }
791
792    /// Stops asking the display for frames when nothing is left for one to do.
793    ///
794    /// These four are the complete set of reasons to want another frame: a
795    /// scope to recompose, a queued node update, a registered frame callback,
796    /// or UI work that can run. If none of them holds, the next frame would
797    /// wake the process, walk an unchanged tree, and present nothing.
798    fn clear_needs_frame_if_idle(&self) {
799        if !self.has_invalid_scopes()
800            && !self.has_updates()
801            && !self.has_frame_callbacks()
802            && !self.has_pending_ui()
803        {
804            *self.needs_frame.borrow_mut() = false;
805        }
806    }
807
808    fn drain_frame_callbacks(&self, frame_time_nanos: u64) {
809        self.last_frame_time_nanos.set(Some(frame_time_nanos));
810        let mut callbacks = self.frame_callbacks.borrow_mut();
811        let mut pending: Vec<Box<dyn FnOnce(u64) + 'static>> = Vec::with_capacity(callbacks.len());
812        while let Some(mut entry) = callbacks.pop_front() {
813            if let Some(callback) = entry.callback.take() {
814                pending.push(callback);
815            }
816        }
817        drop(callbacks);
818
819        // Wrap ALL frame callbacks in a single mutable snapshot so state changes
820        // are properly applied to the global snapshot and visible to subsequent reads.
821        // Using a single snapshot for all callbacks avoids stack exhaustion from
822        // repeated snapshot creation in long-running animation loops.
823        if !pending.is_empty() {
824            let _ = crate::run_in_mutable_snapshot(|| {
825                for callback in pending {
826                    callback(frame_time_nanos);
827                }
828            });
829        }
830
831        self.clear_needs_frame_if_idle();
832    }
833
834    fn debug_stats(&self) -> RuntimeDebugStats {
835        let node_updates = self.node_updates.borrow();
836        let invalid_scopes = self.invalid_scopes.borrow();
837        let scope_queue = self.scope_queue.borrow();
838        let frame_callbacks = self.frame_callbacks.borrow();
839        let local_tasks = self.local_tasks.borrow();
840        let ui_conts = self.ui_conts.borrow();
841        let tasks = self.tasks.borrow();
842        let external_state_owners = self.external_state_owners.borrow();
843
844        RuntimeDebugStats {
845            node_updates_len: node_updates.len(),
846            node_updates_cap: node_updates.capacity(),
847            invalid_scopes_len: invalid_scopes.len(),
848            invalid_scopes_cap: invalid_scopes.capacity(),
849            scope_queue_len: scope_queue.len(),
850            scope_queue_cap: scope_queue.capacity(),
851            frame_callbacks_len: frame_callbacks.len(),
852            frame_callbacks_cap: frame_callbacks.capacity(),
853            local_tasks_len: local_tasks.len(),
854            local_tasks_cap: local_tasks.capacity(),
855            ui_conts_len: ui_conts.len(),
856            ui_conts_cap: ui_conts.capacity(),
857            tasks_len: tasks.len(),
858            tasks_cap: tasks.capacity(),
859            external_state_owners_len: external_state_owners.len(),
860            external_state_owners_cap: external_state_owners.capacity(),
861            ui_dispatcher_pending: self.ui_dispatcher.pending.load(Ordering::SeqCst),
862        }
863    }
864}
865
866#[derive(Clone)]
867pub struct Runtime {
868    inner: Rc<RuntimeInner>,
869}
870
871impl Runtime {
872    pub fn new(scheduler: Arc<dyn RuntimeScheduler>) -> Self {
873        let inner = Rc::new(RuntimeInner::new(scheduler));
874        let runtime = Self { inner };
875        let handle = runtime.handle();
876        register_runtime_handle(&handle);
877        LAST_RUNTIME.with(|slot| *slot.borrow_mut() = Some(handle));
878        runtime
879    }
880
881    pub fn handle(&self) -> RuntimeHandle {
882        RuntimeHandle {
883            inner: Rc::downgrade(&self.inner),
884            dispatcher: UiDispatcher::new(self.inner.ui_dispatcher.clone()),
885            ui_thread_id: self.inner.ui_thread_id,
886            id: self.inner.runtime_id,
887        }
888    }
889
890    pub fn has_updates(&self) -> bool {
891        self.inner.has_updates()
892    }
893
894    pub fn needs_frame(&self) -> bool {
895        // The stored flag is only half the answer. A task woken from another
896        // thread cannot touch this runtime's `RefCell`s, so its waker can only
897        // set the shared `runnable` flag and ping the scheduler.
898        //
899        // A merely pending UI continuation does NOT belong here: main's "Avoid
900        // frames for idle UI continuations" moved that to an update-only wake
901        // (`has_pending_ui` in the app shell), so an idle continuation no
902        // longer costs a frame.
903        *self.inner.needs_frame.borrow() || self.inner.has_runnable_tasks()
904    }
905
906    pub fn set_needs_frame(&self, value: bool) {
907        *self.inner.needs_frame.borrow_mut() = value;
908    }
909
910    /// Animation-clock time of the most recent frame-callback drain. This is
911    /// the same clock `Animatable`s advance on (virtual under robot exact
912    /// captures), so per-frame integrators derive dt from it instead of wall
913    /// time.
914    pub fn last_frame_time_nanos(&self) -> Option<u64> {
915        self.inner.last_frame_time_nanos.get()
916    }
917
918    #[cfg(any(feature = "internal", test))]
919    pub fn frame_clock(&self) -> FrameClock {
920        FrameClock::new(self.handle())
921    }
922}
923
924impl Drop for Runtime {
925    fn drop(&mut self) {
926        if Rc::strong_count(&self.inner) != 1 {
927            return;
928        }
929        unregister_runtime_handle(self.inner.runtime_id);
930        LAST_RUNTIME.with(|slot| {
931            let should_clear = slot
932                .borrow()
933                .as_ref()
934                .is_some_and(|handle| handle.id() == self.inner.runtime_id);
935            if should_clear {
936                *slot.borrow_mut() = None;
937            }
938        });
939    }
940}
941
942#[derive(Default)]
943pub struct DefaultScheduler;
944
945impl RuntimeScheduler for DefaultScheduler {
946    fn schedule_frame(&self) {}
947}
948
949#[cfg(test)]
950#[derive(Default)]
951pub struct TestScheduler;
952
953#[cfg(test)]
954impl RuntimeScheduler for TestScheduler {
955    fn schedule_frame(&self) {}
956}
957
958#[cfg(test)]
959pub struct TestRuntime {
960    runtime: Runtime,
961}
962
963#[cfg(test)]
964impl Default for TestRuntime {
965    fn default() -> Self {
966        Self::new()
967    }
968}
969
970#[cfg(test)]
971impl TestRuntime {
972    pub fn new() -> Self {
973        Self {
974            runtime: Runtime::new(Arc::new(TestScheduler)),
975        }
976    }
977
978    pub fn handle(&self) -> RuntimeHandle {
979        self.runtime.handle()
980    }
981}
982
983#[derive(Clone)]
984pub struct RuntimeHandle {
985    inner: Weak<RuntimeInner>,
986    dispatcher: UiDispatcher,
987    ui_thread_id: ThreadId,
988    id: RuntimeId,
989}
990
991pub struct TaskHandle {
992    id: u64,
993    runtime: RuntimeHandle,
994}
995
996struct DeferredStateRelease {
997    runtime: RuntimeHandle,
998    id: StateId,
999}
1000
1001pub(crate) struct StateHandleLease {
1002    id: StateId,
1003    runtime: RuntimeHandle,
1004}
1005
1006impl StateHandleLease {
1007    pub(crate) fn id(&self) -> StateId {
1008        self.id
1009    }
1010
1011    pub(crate) fn runtime(&self) -> RuntimeHandle {
1012        self.runtime.clone()
1013    }
1014}
1015
1016impl Drop for StateHandleLease {
1017    fn drop(&mut self) {
1018        defer_state_release(self.runtime.clone(), self.id);
1019    }
1020}
1021
1022impl RuntimeHandle {
1023    pub fn id(&self) -> RuntimeId {
1024        self.id
1025    }
1026
1027    pub(crate) fn alloc_state<T: Clone + 'static>(&self, value: T) -> Rc<StateHandleLease> {
1028        let id = self.with_state_arena(|arena| arena.alloc(value, self.clone()));
1029        let lease = Rc::new(StateHandleLease {
1030            id,
1031            runtime: self.clone(),
1032        });
1033        self.with_state_arena(|arena| arena.register_lease(id, &lease));
1034        lease
1035    }
1036
1037    pub(crate) fn alloc_state_with_policy<T: Clone + 'static>(
1038        &self,
1039        value: T,
1040        policy: Arc<dyn MutationPolicy<T>>,
1041    ) -> Rc<StateHandleLease> {
1042        let id =
1043            self.with_state_arena(|arena| arena.alloc_with_policy(value, self.clone(), policy));
1044        let lease = Rc::new(StateHandleLease {
1045            id,
1046            runtime: self.clone(),
1047        });
1048        self.with_state_arena(|arena| arena.register_lease(id, &lease));
1049        lease
1050    }
1051
1052    pub(crate) fn alloc_persistent_state<T: Clone + 'static>(
1053        &self,
1054        value: T,
1055    ) -> crate::MutableState<T> {
1056        let lease = self.alloc_state(value);
1057        if let Some(inner) = self.inner.upgrade() {
1058            inner
1059                .external_state_owners
1060                .borrow_mut()
1061                .insert(lease.id(), Rc::clone(&lease));
1062        }
1063        crate::MutableState::from_lease(&lease)
1064    }
1065
1066    pub(crate) fn retain_state_lease(&self, id: StateId) -> Option<Rc<StateHandleLease>> {
1067        self.with_state_arena(|arena| arena.retain_lease(id))
1068    }
1069
1070    pub(crate) fn with_state_arena<R>(&self, f: impl FnOnce(&StateArena) -> R) -> R {
1071        self.try_with_state_arena(f)
1072            .unwrap_or_else(|| panic!("runtime dropped"))
1073    }
1074
1075    pub(crate) fn try_with_state_arena<R>(&self, f: impl FnOnce(&StateArena) -> R) -> Option<R> {
1076        self.inner.upgrade().map(|inner| f(&inner.state_arena))
1077    }
1078
1079    fn release_state_immediate(&self, id: StateId) {
1080        if let Some(inner) = self.inner.upgrade() {
1081            inner.state_arena.release(id);
1082        }
1083    }
1084
1085    pub fn state_arena_stats(&self) -> (usize, usize) {
1086        self.try_with_state_arena(StateArena::stats)
1087            .unwrap_or_default()
1088    }
1089
1090    pub fn state_arena_debug_stats(&self) -> StateArenaDebugStats {
1091        self.try_with_state_arena(StateArena::debug_stats)
1092            .unwrap_or_default()
1093    }
1094
1095    pub fn debug_stats(&self) -> RuntimeDebugStats {
1096        self.inner
1097            .upgrade()
1098            .map(|inner| inner.debug_stats())
1099            .unwrap_or_default()
1100    }
1101
1102    pub fn live_ui_task_labels(&self) -> Vec<(u64, String)> {
1103        self.inner
1104            .upgrade()
1105            .map(|inner| {
1106                inner
1107                    .tasks
1108                    .borrow()
1109                    .iter()
1110                    .map(|entry| (entry.id, entry.label.clone()))
1111                    .collect()
1112            })
1113            .unwrap_or_default()
1114    }
1115
1116    pub(crate) fn unregister_state_scope(&self, id: StateId, scope_id: ScopeId) {
1117        if let Some(inner) = self.inner.upgrade() {
1118            inner.state_arena.unregister_scope(id, scope_id);
1119        }
1120    }
1121
1122    pub fn schedule(&self) {
1123        if let Some(inner) = self.inner.upgrade() {
1124            inner.schedule();
1125        }
1126    }
1127
1128    pub(crate) fn enqueue_node_update(&self, command: Command) {
1129        if let Some(inner) = self.inner.upgrade() {
1130            inner.enqueue_update(command);
1131        }
1132    }
1133
1134    /// Schedules work that must run on the runtime thread.
1135    ///
1136    /// The closure executes on the UI thread immediately when the runtime
1137    /// drains its local queue, so it may capture `Rc`/`RefCell` values. Calling
1138    /// this from any other thread is a logic error and will panic in debug
1139    /// builds via the inner assertion.
1140    pub fn enqueue_ui_task(&self, task: Box<dyn FnOnce() + 'static>) {
1141        if let Some(inner) = self.inner.upgrade() {
1142            inner.enqueue_ui_task(task);
1143        } else {
1144            task();
1145        }
1146    }
1147
1148    pub fn spawn_ui<F>(&self, fut: F) -> Option<TaskHandle>
1149    where
1150        F: Future<Output = ()> + 'static,
1151    {
1152        self.inner.upgrade().map(|inner| {
1153            let id = inner.spawn_ui_task(Box::pin(fut));
1154            TaskHandle {
1155                id,
1156                runtime: self.clone(),
1157            }
1158        })
1159    }
1160
1161    pub fn cancel_task(&self, id: u64) {
1162        if let Some(inner) = self.inner.upgrade() {
1163            inner.cancel_task(id);
1164        }
1165    }
1166
1167    /// Whether the runtime still holds the spawned task `id`.
1168    pub fn has_task(&self, id: u64) -> bool {
1169        self.inner
1170            .upgrade()
1171            .map(|inner| inner.has_task(id))
1172            .unwrap_or(false)
1173    }
1174
1175    /// Enqueues work from any thread to run on the UI thread.
1176    ///
1177    /// The closure must be `Send` because it may cross threads before executing
1178    /// on the runtime thread. Use this when posting from background work.
1179    pub fn post_ui(&self, task: impl FnOnce() + Send + 'static) {
1180        self.dispatcher.post(task);
1181    }
1182
1183    pub fn register_ui_cont<T: 'static>(&self, f: impl FnOnce(T) + 'static) -> Option<u64> {
1184        self.inner.upgrade().map(|inner| inner.register_ui_cont(f))
1185    }
1186
1187    pub fn cancel_ui_cont(&self, id: u64) {
1188        if let Some(inner) = self.inner.upgrade() {
1189            inner.cancel_ui_cont(id);
1190        }
1191    }
1192
1193    pub fn drain_ui(&self) {
1194        if let Some(inner) = self.inner.upgrade() {
1195            inner.drain_ui();
1196        }
1197    }
1198
1199    pub fn has_pending_ui(&self) -> bool {
1200        self.inner
1201            .upgrade()
1202            .map(|inner| inner.has_pending_ui())
1203            .unwrap_or_else(|| self.dispatcher.has_pending())
1204    }
1205
1206    pub fn register_frame_callback(
1207        &self,
1208        callback: impl FnOnce(u64) + 'static,
1209    ) -> Option<FrameCallbackId> {
1210        self.inner.upgrade().map(|inner| {
1211            inner.register_frame_callback(FrameCallbackKind::Transient, Box::new(callback))
1212        })
1213    }
1214
1215    pub fn register_perpetual_frame_callback(
1216        &self,
1217        callback: impl FnOnce(u64) + 'static,
1218    ) -> Option<FrameCallbackId> {
1219        self.inner.upgrade().map(|inner| {
1220            inner.register_frame_callback(FrameCallbackKind::Perpetual, Box::new(callback))
1221        })
1222    }
1223
1224    pub fn cancel_frame_callback(&self, id: FrameCallbackId) {
1225        if let Some(inner) = self.inner.upgrade() {
1226            inner.cancel_frame_callback(id);
1227        }
1228    }
1229
1230    pub fn drain_frame_callbacks(&self, frame_time_nanos: u64) {
1231        if let Some(inner) = self.inner.upgrade() {
1232            inner.drain_frame_callbacks(frame_time_nanos);
1233        }
1234    }
1235
1236    /// Animation-clock time of the most recent frame-callback drain (see
1237    /// [`Runtime::last_frame_time_nanos`]).
1238    pub fn last_frame_time_nanos(&self) -> Option<u64> {
1239        self.inner
1240            .upgrade()
1241            .and_then(|inner| inner.last_frame_time_nanos.get())
1242    }
1243
1244    #[cfg(any(feature = "internal", test))]
1245    pub fn frame_clock(&self) -> FrameClock {
1246        FrameClock::new(self.clone())
1247    }
1248
1249    pub fn set_needs_frame(&self, value: bool) {
1250        if let Some(inner) = self.inner.upgrade() {
1251            *inner.needs_frame.borrow_mut() = value;
1252        }
1253    }
1254
1255    pub(crate) fn take_updates(&self) -> Vec<Command> {
1256        self.inner
1257            .upgrade()
1258            .map(|inner| inner.take_updates())
1259            .unwrap_or_default()
1260    }
1261
1262    pub fn has_updates(&self) -> bool {
1263        self.inner
1264            .upgrade()
1265            .map(|inner| inner.has_updates())
1266            .unwrap_or(false)
1267    }
1268
1269    pub(crate) fn mark_scope_recomposed(&self, id: ScopeId) {
1270        if let Some(inner) = self.inner.upgrade() {
1271            inner.mark_scope_recomposed(id);
1272        }
1273    }
1274
1275    pub(crate) fn register_invalid_scope(&self, id: ScopeId, scope: Weak<RecomposeScopeInner>) {
1276        if let Some(inner) = self.inner.upgrade() {
1277            inner.register_invalid_scope(id, scope);
1278        }
1279    }
1280
1281    pub(crate) fn requeue_invalid_scope(&self, id: ScopeId, scope: Weak<RecomposeScopeInner>) {
1282        if let Some(inner) = self.inner.upgrade() {
1283            inner.requeue_invalid_scope(id, scope);
1284        }
1285    }
1286
1287    pub(crate) fn take_invalidated_scopes(&self) -> Vec<(ScopeId, Weak<RecomposeScopeInner>)> {
1288        self.inner
1289            .upgrade()
1290            .map(|inner| inner.take_invalidated_scopes())
1291            .unwrap_or_default()
1292    }
1293
1294    pub fn has_invalid_scopes(&self) -> bool {
1295        self.inner
1296            .upgrade()
1297            .map(|inner| inner.has_invalid_scopes())
1298            .unwrap_or(false)
1299    }
1300
1301    pub(crate) fn increment_live_recompose_scope_count(&self) {
1302        if let Some(inner) = self.inner.upgrade() {
1303            inner.increment_live_recompose_scope_count();
1304        }
1305    }
1306
1307    pub(crate) fn decrement_live_recompose_scope_count(&self) {
1308        if let Some(inner) = self.inner.upgrade() {
1309            inner.decrement_live_recompose_scope_count();
1310        }
1311    }
1312
1313    fn live_recompose_scope_count(&self) -> usize {
1314        self.inner
1315            .upgrade()
1316            .map(|inner| inner.live_recompose_scope_count())
1317            .unwrap_or_default()
1318    }
1319
1320    #[doc(hidden)]
1321    pub fn debug_invalid_scope_ids(&self) -> Vec<usize> {
1322        self.inner
1323            .upgrade()
1324            .map(|inner| inner.invalid_scopes.borrow().iter().copied().collect())
1325            .unwrap_or_default()
1326    }
1327
1328    pub fn has_frame_callbacks(&self) -> bool {
1329        self.inner
1330            .upgrade()
1331            .map(|inner| inner.has_frame_callbacks())
1332            .unwrap_or(false)
1333    }
1334
1335    pub fn has_transient_frame_callbacks(&self) -> bool {
1336        self.inner
1337            .upgrade()
1338            .map(|inner| inner.has_transient_frame_callbacks())
1339            .unwrap_or(false)
1340    }
1341
1342    pub fn assert_ui_thread(&self) {
1343        debug_assert_eq!(
1344            std::thread::current().id(),
1345            self.ui_thread_id,
1346            "state mutated off the runtime's UI thread"
1347        );
1348    }
1349
1350    pub fn dispatcher(&self) -> UiDispatcher {
1351        self.dispatcher.clone()
1352    }
1353
1354    #[doc(hidden)]
1355    pub fn with_deferred_state_releases<R>(&self, f: impl FnOnce() -> R) -> R {
1356        let _scope = enter_state_teardown_scope();
1357        f()
1358    }
1359}
1360
1361impl TaskHandle {
1362    pub fn cancel(&self) {
1363        self.runtime.cancel_task(self.id);
1364    }
1365
1366    /// Whether the spawned future has finished or been cancelled.
1367    pub fn is_finished(&self) -> bool {
1368        !self.runtime.has_task(self.id)
1369    }
1370}
1371
1372pub(crate) struct FrameCallbackEntry {
1373    id: FrameCallbackId,
1374    kind: FrameCallbackKind,
1375    callback: Option<Box<dyn FnOnce(u64) + 'static>>,
1376}
1377
1378/// The waker for one spawned UI task.
1379///
1380/// Waking marks that task runnable and asks the platform for a frame, which is
1381/// what eventually reaches [`RuntimeInner::poll_async_tasks`]. The `runnable`
1382/// flag is the half that matters for idling: without it a wake is
1383/// indistinguishable from any other, so the runtime cannot tell a task that is
1384/// ready from one that is parked.
1385///
1386/// `runnable` is an `Arc<AtomicBool>` rather than a `Cell` because a task may
1387/// legitimately be woken from another thread — a JNI callback, a worker
1388/// finishing, a platform service replying.
1389#[cfg(not(target_arch = "wasm32"))]
1390struct RuntimeTaskWaker {
1391    scheduler: Arc<dyn RuntimeScheduler>,
1392    runnable: Arc<AtomicBool>,
1393}
1394
1395#[cfg(target_arch = "wasm32")]
1396struct RuntimeTaskWaker {
1397    runtime_id: RuntimeId,
1398    runnable: Arc<AtomicBool>,
1399}
1400
1401impl RuntimeTaskWaker {
1402    #[cfg(not(target_arch = "wasm32"))]
1403    fn new(inner: &RuntimeInner, runnable: Arc<AtomicBool>) -> Self {
1404        let scheduler = inner.scheduler.clone();
1405        Self {
1406            scheduler,
1407            runnable,
1408        }
1409    }
1410
1411    #[cfg(target_arch = "wasm32")]
1412    fn new(inner: &RuntimeInner, runnable: Arc<AtomicBool>) -> Self {
1413        let runtime_id = inner.runtime_id;
1414        Self {
1415            runtime_id,
1416            runnable,
1417        }
1418    }
1419
1420    fn into_waker(self) -> Waker {
1421        futures_task::waker(Arc::new(self))
1422    }
1423}
1424
1425impl futures_task::ArcWake for RuntimeTaskWaker {
1426    #[cfg(not(target_arch = "wasm32"))]
1427    fn wake_by_ref(arc_self: &Arc<Self>) {
1428        arc_self.runnable.store(true, Ordering::Release);
1429        arc_self.scheduler.schedule_frame();
1430    }
1431
1432    #[cfg(target_arch = "wasm32")]
1433    fn wake_by_ref(arc_self: &Arc<Self>) {
1434        arc_self.runnable.store(true, Ordering::Release);
1435        REGISTERED_RUNTIMES.with(|registry| {
1436            if let Some(handle) = registry.borrow().get(&arc_self.runtime_id).cloned() {
1437                handle.schedule();
1438            }
1439        });
1440    }
1441}
1442
1443thread_local! {
1444    static NEXT_RUNTIME_ID: Cell<u32> = const { Cell::new(1) };
1445    static ACTIVE_RUNTIMES: RefCell<Vec<RuntimeHandle>> = const { RefCell::new(Vec::new()) };
1446    static LAST_RUNTIME: RefCell<Option<RuntimeHandle>> = const { RefCell::new(None) };
1447    static REGISTERED_RUNTIMES: RefCell<HashMap<RuntimeId, RuntimeHandle>> = RefCell::new(HashMap::default());
1448    static STATE_TEARDOWN_DEPTH: Cell<usize> = const { Cell::new(0) };
1449    static DEFERRED_STATE_RELEASES: RefCell<Vec<DeferredStateRelease>> = const { RefCell::new(Vec::new()) };
1450}
1451
1452#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
1453pub struct RuntimeThreadLocalDebugStats {
1454    pub active_runtimes_len: usize,
1455    pub active_runtimes_cap: usize,
1456    pub registered_runtimes_len: usize,
1457    pub registered_runtimes_cap: usize,
1458    pub deferred_state_releases_len: usize,
1459    pub deferred_state_releases_cap: usize,
1460}
1461
1462/// Gets the current runtime handle from thread-local storage.
1463///
1464/// Returns the most recently pushed active runtime, or the last known runtime.
1465/// Used by fling animation and other components that need access to the runtime.
1466pub fn current_runtime_handle() -> Option<RuntimeHandle> {
1467    if let Some(handle) = ACTIVE_RUNTIMES.with(|stack| stack.borrow().last().cloned()) {
1468        return Some(handle);
1469    }
1470    LAST_RUNTIME.with(|slot| slot.borrow().clone())
1471}
1472
1473pub(crate) fn runtime_handle_by_id(id: RuntimeId) -> Option<RuntimeHandle> {
1474    REGISTERED_RUNTIMES.with(|registry| registry.borrow().get(&id).cloned())
1475}
1476
1477pub(crate) fn live_recompose_scope_count() -> usize {
1478    REGISTERED_RUNTIMES.with(|registry| {
1479        registry
1480            .borrow()
1481            .values()
1482            .map(RuntimeHandle::live_recompose_scope_count)
1483            .sum()
1484    })
1485}
1486
1487pub fn debug_runtime_thread_local_stats() -> RuntimeThreadLocalDebugStats {
1488    let (active_runtimes_len, active_runtimes_cap) = ACTIVE_RUNTIMES.with(|stack| {
1489        let stack = stack.borrow();
1490        (stack.len(), stack.capacity())
1491    });
1492    let (registered_runtimes_len, registered_runtimes_cap) = REGISTERED_RUNTIMES.with(|registry| {
1493        let registry = registry.borrow();
1494        (registry.len(), registry.capacity())
1495    });
1496    let (deferred_state_releases_len, deferred_state_releases_cap) =
1497        DEFERRED_STATE_RELEASES.with(|releases| {
1498            let releases = releases.borrow();
1499            (releases.len(), releases.capacity())
1500        });
1501
1502    RuntimeThreadLocalDebugStats {
1503        active_runtimes_len,
1504        active_runtimes_cap,
1505        registered_runtimes_len,
1506        registered_runtimes_cap,
1507        deferred_state_releases_len,
1508        deferred_state_releases_cap,
1509    }
1510}
1511
1512fn register_runtime_handle(handle: &RuntimeHandle) {
1513    REGISTERED_RUNTIMES.with(|registry| {
1514        registry.borrow_mut().insert(handle.id(), handle.clone());
1515    });
1516}
1517
1518fn unregister_runtime_handle(id: RuntimeId) {
1519    REGISTERED_RUNTIMES.with(|registry| {
1520        registry.borrow_mut().remove(&id);
1521    });
1522}
1523
1524fn defer_state_release(runtime: RuntimeHandle, id: StateId) {
1525    let teardown_active = STATE_TEARDOWN_DEPTH.with(|depth| depth.get() > 0);
1526    if teardown_active {
1527        DEFERRED_STATE_RELEASES.with(|releases| {
1528            releases
1529                .borrow_mut()
1530                .push(DeferredStateRelease { runtime, id });
1531        });
1532    } else {
1533        runtime.release_state_immediate(id);
1534    }
1535}
1536
1537fn flush_deferred_state_releases() {
1538    DEFERRED_STATE_RELEASES.with(|releases| {
1539        let mut releases = releases.borrow_mut();
1540        while let Some(deferred) = releases.pop() {
1541            deferred.runtime.release_state_immediate(deferred.id);
1542        }
1543    });
1544}
1545
1546pub(crate) struct StateTeardownScope;
1547
1548pub(crate) fn enter_state_teardown_scope() -> StateTeardownScope {
1549    STATE_TEARDOWN_DEPTH.with(|depth| depth.set(depth.get() + 1));
1550    StateTeardownScope
1551}
1552
1553impl Drop for StateTeardownScope {
1554    fn drop(&mut self) {
1555        STATE_TEARDOWN_DEPTH.with(|depth| {
1556            let next = depth.get().saturating_sub(1);
1557            depth.set(next);
1558            if next == 0 {
1559                flush_deferred_state_releases();
1560            }
1561        });
1562    }
1563}
1564
1565pub(crate) fn push_active_runtime(handle: &RuntimeHandle) {
1566    register_runtime_handle(handle);
1567    ACTIVE_RUNTIMES.with(|stack| stack.borrow_mut().push(handle.clone()));
1568    LAST_RUNTIME.with(|slot| *slot.borrow_mut() = Some(handle.clone()));
1569}
1570
1571pub(crate) fn pop_active_runtime() {
1572    ACTIVE_RUNTIMES.with(|stack| {
1573        stack.borrow_mut().pop();
1574    });
1575}
1576
1577/// Schedule a new frame render using the most recently active runtime handle.
1578pub fn schedule_frame() {
1579    if let Some(handle) = current_runtime_handle() {
1580        handle.schedule();
1581        return;
1582    }
1583    log::debug!(
1584        target: "cranpose::runtime",
1585        "ignoring frame request without an active runtime",
1586    );
1587}
1588
1589/// Schedule an in-place node update using the most recently active runtime.
1590pub fn schedule_node_update(
1591    update: impl FnOnce(&mut dyn Applier) -> Result<(), NodeError> + 'static,
1592) {
1593    if let Some(handle) = current_runtime_handle() {
1594        handle.enqueue_node_update(Command::callback(update));
1595    } else {
1596        drop(update);
1597        log::debug!(
1598            target: "cranpose::runtime",
1599            "ignoring node update request without an active runtime",
1600        );
1601    }
1602}
1603
1604#[cfg(test)]
1605mod tests {
1606    use super::*;
1607
1608    #[test]
1609    fn state_arena_alloc_skips_free_slot_outside_cell_storage() {
1610        let runtime = TestRuntime::new();
1611        let arena = StateArena::default();
1612        let first = arena.alloc(1_i32, runtime.handle());
1613        arena.inner.borrow_mut().free.push(u32::MAX);
1614
1615        let second = arena.alloc(2_i32, runtime.handle());
1616
1617        assert_eq!(first.slot(), 0);
1618        assert_eq!(second.slot(), 1);
1619        assert!(arena.get_typed_opt::<i32>(first).is_some());
1620        assert!(arena.get_typed_opt::<i32>(second).is_some());
1621    }
1622
1623    #[test]
1624    fn state_arena_alloc_skips_occupied_free_slot() {
1625        let runtime = TestRuntime::new();
1626        let arena = StateArena::default();
1627        let first = arena.alloc(1_i32, runtime.handle());
1628        arena.inner.borrow_mut().free.push(first.slot());
1629
1630        let second = arena.alloc(2_i32, runtime.handle());
1631
1632        assert_ne!(first.slot(), second.slot());
1633        assert_eq!(second.slot(), 1);
1634        assert!(arena.get_typed_opt::<i32>(first).is_some());
1635        assert!(arena.get_typed_opt::<i32>(second).is_some());
1636    }
1637
1638    #[test]
1639    fn state_arena_register_lease_ignores_stale_id() {
1640        let runtime = TestRuntime::new();
1641        let arena = StateArena::default();
1642        let stale_id = StateId::new(99, 0);
1643        let lease = Rc::new(StateHandleLease {
1644            id: stale_id,
1645            runtime: runtime.handle(),
1646        });
1647
1648        arena.register_lease(stale_id, &lease);
1649
1650        assert!(arena.retain_lease(stale_id).is_none());
1651    }
1652
1653    #[test]
1654    fn ui_continuation_type_mismatch_is_ignored_until_matching_payload() {
1655        let runtime = TestRuntime::new();
1656        let handle = runtime.handle();
1657        let received = Rc::new(Cell::new(None));
1658        let received_for_continuation = Rc::clone(&received);
1659        let cont_id = handle
1660            .register_ui_cont(move |value: u32| {
1661                received_for_continuation.set(Some(value));
1662            })
1663            .expect("test runtime is alive");
1664
1665        handle.dispatcher().post_invoke(cont_id, "wrong payload");
1666        handle.drain_ui();
1667
1668        assert_eq!(received.get(), None);
1669        assert_eq!(handle.debug_stats().ui_conts_len, 1);
1670
1671        handle.dispatcher().post_invoke(cont_id, 42_u32);
1672        handle.drain_ui();
1673
1674        assert_eq!(received.get(), Some(42));
1675        assert_eq!(handle.debug_stats().ui_conts_len, 0);
1676    }
1677
1678    #[test]
1679    fn ui_dispatcher_failed_send_does_not_leave_pending_work() {
1680        let runtime = Runtime::new(Arc::new(TestScheduler));
1681        let dispatcher = runtime.handle().dispatcher();
1682
1683        drop(runtime);
1684
1685        assert!(!dispatcher.has_pending());
1686        dispatcher.post(|| {});
1687        assert!(!dispatcher.has_pending());
1688        dispatcher.post_invoke(404, 12_u32);
1689        assert!(!dispatcher.has_pending());
1690    }
1691
1692    #[test]
1693    fn pending_guard_does_not_wrap_on_underflow() {
1694        let counter = AtomicUsize::new(0);
1695
1696        {
1697            let _guard = PendingGuard::new(&counter);
1698        }
1699
1700        assert_eq!(counter.load(Ordering::SeqCst), 0);
1701    }
1702
1703    #[test]
1704    fn pending_guard_decrements_pending_count() {
1705        let counter = AtomicUsize::new(2);
1706
1707        {
1708            let _guard = PendingGuard::new(&counter);
1709        }
1710
1711        assert_eq!(counter.load(Ordering::SeqCst), 1);
1712    }
1713
1714    #[test]
1715    fn schedule_frame_without_runtime_is_ignored() {
1716        let ok = std::thread::spawn(|| std::panic::catch_unwind(super::schedule_frame).is_ok())
1717            .join()
1718            .expect("test thread should join");
1719
1720        assert!(ok);
1721    }
1722
1723    #[test]
1724    fn schedule_node_update_without_runtime_is_ignored() {
1725        let ok = std::thread::spawn(|| {
1726            std::panic::catch_unwind(|| {
1727                super::schedule_node_update(|_| Ok(()));
1728            })
1729            .is_ok()
1730        })
1731        .join()
1732        .expect("test thread should join");
1733
1734        assert!(ok);
1735    }
1736}