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