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
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    state_arena: StateArena,
413    external_state_owners: RefCell<HashMap<StateId, Rc<StateHandleLease>>>,
414    live_recompose_scope_count: Cell<usize>,
415    runtime_id: RuntimeId,
416}
417
418struct TaskEntry {
419    id: u64,
420    future: Pin<Box<dyn Future<Output = ()> + 'static>>,
421    /// Whether this task can currently make progress.
422    ///
423    /// Set when the task is spawned and every time its waker fires, cleared
424    /// immediately before it is polled. A task that returned `Poll::Pending`
425    /// and has not been woken since is *parked*: polling it again would only
426    /// return `Poll::Pending` a second time, and — more importantly — it is
427    /// not a reason to keep asking the display for frames. See
428    /// [`RuntimeInner::has_pending_ui`].
429    ///
430    /// Shared with the task's [`Waker`], which may be woken from any thread.
431    runnable: Arc<AtomicBool>,
432    /// The waker handed to this task's future, so a wake can be attributed to
433    /// the one task that is ready rather than to all of them.
434    waker: Waker,
435}
436
437impl RuntimeInner {
438    fn new(scheduler: Arc<dyn RuntimeScheduler>) -> Self {
439        let (tx, rx) = mpsc::channel();
440        let dispatcher = Arc::new(UiDispatcherInner::new(scheduler.clone(), tx));
441        Self {
442            scheduler,
443            needs_frame: RefCell::new(false),
444            node_updates: RefCell::new(Vec::new()),
445            invalid_scopes: RefCell::new(HashSet::default()),
446            scope_queue: RefCell::new(Vec::new()),
447            frame_callbacks: RefCell::new(VecDeque::new()),
448            next_frame_callback_id: Cell::new(1),
449            last_frame_time_nanos: Cell::new(None),
450            ui_dispatcher: dispatcher,
451            ui_rx: RefCell::new(rx),
452            local_tasks: RefCell::new(VecDeque::new()),
453            ui_conts: RefCell::new(UiContinuationMap::default()),
454            next_cont_id: Cell::new(1),
455            ui_thread_id: std::thread::current().id(),
456            tasks: RefCell::new(Vec::new()),
457            next_task_id: Cell::new(1),
458            state_arena: StateArena::default(),
459            external_state_owners: RefCell::new(HashMap::default()),
460            live_recompose_scope_count: Cell::new(0),
461            runtime_id: RuntimeId::next(),
462        }
463    }
464
465    fn schedule(&self) {
466        *self.needs_frame.borrow_mut() = true;
467        self.scheduler.schedule_frame();
468    }
469
470    fn enqueue_update(&self, command: Command) {
471        self.node_updates.borrow_mut().push(command);
472        self.schedule(); // Ensure frame is scheduled to process the command
473    }
474
475    fn take_updates(&self) -> Vec<Command> {
476        let updates = self.node_updates.borrow_mut().drain(..).collect::<Vec<_>>();
477        updates
478    }
479
480    fn has_updates(&self) -> bool {
481        !self.node_updates.borrow().is_empty() || self.has_invalid_scopes()
482    }
483
484    fn register_invalid_scope(&self, id: ScopeId, scope: Weak<RecomposeScopeInner>) {
485        let mut invalid = self.invalid_scopes.borrow_mut();
486        if invalid.insert(id) {
487            self.scope_queue.borrow_mut().push((id, scope));
488            self.schedule();
489        }
490    }
491
492    fn requeue_invalid_scope(&self, id: ScopeId, scope: Weak<RecomposeScopeInner>) {
493        if self.invalid_scopes.borrow().contains(&id) {
494            self.scope_queue.borrow_mut().push((id, scope));
495            self.schedule();
496        }
497    }
498
499    fn mark_scope_recomposed(&self, id: ScopeId) {
500        self.invalid_scopes.borrow_mut().remove(&id);
501    }
502
503    fn take_invalidated_scopes(&self) -> Vec<(ScopeId, Weak<RecomposeScopeInner>)> {
504        let mut queue = self.scope_queue.borrow_mut();
505        if queue.is_empty() {
506            return Vec::new();
507        }
508        let pending: Vec<_> = queue.drain(..).collect();
509        drop(queue);
510        let invalid = self.invalid_scopes.borrow();
511        pending
512            .into_iter()
513            .filter(|(id, _)| invalid.contains(id))
514            .collect()
515    }
516
517    fn has_invalid_scopes(&self) -> bool {
518        !self.invalid_scopes.borrow().is_empty()
519    }
520
521    fn increment_live_recompose_scope_count(&self) {
522        self.live_recompose_scope_count
523            .set(self.live_recompose_scope_count.get().saturating_add(1));
524    }
525
526    fn decrement_live_recompose_scope_count(&self) {
527        self.live_recompose_scope_count
528            .set(self.live_recompose_scope_count.get().saturating_sub(1));
529    }
530
531    fn live_recompose_scope_count(&self) -> usize {
532        self.live_recompose_scope_count.get()
533    }
534
535    fn has_frame_callbacks(&self) -> bool {
536        !self.frame_callbacks.borrow().is_empty()
537    }
538
539    /// Queues a closure that is already bound to the UI thread's local queue.
540    ///
541    /// The closure may capture `Rc`/`RefCell` values because it never leaves the
542    /// runtime thread. Callers must only invoke this from the runtime thread.
543    fn enqueue_ui_task(&self, task: Box<dyn FnOnce() + 'static>) {
544        self.local_tasks.borrow_mut().push_back(task);
545        self.schedule();
546    }
547
548    fn spawn_ui_task(&self, future: Pin<Box<dyn Future<Output = ()> + 'static>>) -> u64 {
549        let id = self.next_task_id.get();
550        self.next_task_id.set(id + 1);
551        let runnable = Arc::new(AtomicBool::new(true));
552        let waker = RuntimeTaskWaker::new(self, Arc::clone(&runnable)).into_waker();
553        self.tasks.borrow_mut().push(TaskEntry {
554            id,
555            future,
556            runnable,
557            waker,
558        });
559        self.schedule();
560        id
561    }
562
563    fn cancel_task(&self, id: u64) {
564        let mut tasks = self.tasks.borrow_mut();
565        if tasks.iter().any(|entry| entry.id == id) {
566            tasks.retain(|entry| entry.id != id);
567        }
568    }
569
570    fn poll_async_tasks(&self) -> bool {
571        let mut tasks_ref = self.tasks.borrow_mut();
572        let tasks = std::mem::take(&mut *tasks_ref);
573        drop(tasks_ref);
574        let mut pending = Vec::with_capacity(tasks.len());
575        let mut made_progress = false;
576        for mut entry in tasks.into_iter() {
577            // Claim the wake before polling, not after: the future may be woken
578            // from another thread while it is being polled, and that wake has to
579            // survive into the next pass rather than be cleared by this one.
580            if !entry.runnable.swap(false, Ordering::AcqRel) {
581                pending.push(entry);
582                continue;
583            }
584            let mut cx = Context::from_waker(&entry.waker);
585            match entry.future.as_mut().poll(&mut cx) {
586                Poll::Ready(()) => {
587                    made_progress = true;
588                }
589                Poll::Pending => {
590                    pending.push(entry);
591                }
592            }
593        }
594        if !pending.is_empty() {
595            self.tasks.borrow_mut().extend(pending);
596        }
597        made_progress
598    }
599
600    fn drain_ui(&self) {
601        loop {
602            let mut executed = false;
603
604            {
605                let rx = &mut *self.ui_rx.borrow_mut();
606                for message in rx.try_iter() {
607                    executed = true;
608                    let _guard = PendingGuard::new(&self.ui_dispatcher.pending);
609                    match message {
610                        UiMessage::Task(task) => {
611                            task();
612                        }
613                        UiMessage::Invoke { id, value } => {
614                            self.invoke_ui_cont(id, value);
615                        }
616                    }
617                }
618            }
619
620            loop {
621                let task = {
622                    let mut local = self.local_tasks.borrow_mut();
623                    local.pop_front()
624                };
625
626                match task {
627                    Some(task) => {
628                        executed = true;
629                        task();
630                    }
631                    None => break,
632                }
633            }
634
635            if self.poll_async_tasks() {
636                executed = true;
637            }
638
639            if !executed {
640                break;
641            }
642        }
643
644        // Draining is the point at which tasks park, so it is also the first
645        // moment we can tell that the runtime has gone quiet. Checking here
646        // rather than waiting for the next frame-callback drain saves the app
647        // one wasted wake every time it settles.
648        self.clear_needs_frame_if_idle();
649    }
650
651    fn has_pending_ui(&self) -> bool {
652        let local_pending = self
653            .local_tasks
654            .try_borrow()
655            .map(|tasks| !tasks.is_empty())
656            .unwrap_or(true);
657
658        local_pending || self.ui_dispatcher.has_pending() || self.has_runnable_tasks()
659    }
660
661    /// Whether any spawned task could make progress if it were polled now.
662    ///
663    /// Only *runnable* tasks count. A task that is merely alive is not pending
664    /// work: an effect awaiting a back gesture, a network reply, or a frame it
665    /// has not asked for yet will do nothing if polled, so counting it as
666    /// pending pinned `needs_frame` true for the lifetime of the composition.
667    /// Since almost every app spawns at least one long-lived effect, that kept
668    /// the frame clock running forever and asked the display for 60 frames a
669    /// second on a screen where nothing moved. A task awaiting the frame clock
670    /// keeps frames coming through `has_frame_callbacks` instead, which is the
671    /// honest reason to want one.
672    fn has_runnable_tasks(&self) -> bool {
673        self.tasks
674            .try_borrow()
675            .map(|tasks| {
676                tasks
677                    .iter()
678                    .any(|task| task.runnable.load(Ordering::Acquire))
679            })
680            .unwrap_or(true)
681    }
682
683    fn register_ui_cont<T: 'static>(&self, f: impl FnOnce(T) + 'static) -> u64 {
684        debug_assert_eq!(
685            std::thread::current().id(),
686            self.ui_thread_id,
687            "UI continuation registered off the runtime thread",
688        );
689        let id = self.next_cont_id.get();
690        self.next_cont_id.set(id + 1);
691        let callback = RefCell::new(Some(f));
692        self.ui_conts.borrow_mut().insert(
693            id,
694            Box::new(move |value: Box<dyn Any>| {
695                let Ok(value) = value.downcast::<T>() else {
696                    return false;
697                };
698                let Some(slot) = callback.borrow_mut().take() else {
699                    return true;
700                };
701                slot(*value);
702                true
703            }),
704        );
705        id
706    }
707
708    fn invoke_ui_cont(&self, id: u64, value: Box<dyn Any + Send>) {
709        debug_assert_eq!(
710            std::thread::current().id(),
711            self.ui_thread_id,
712            "UI continuation invoked off the runtime thread",
713        );
714        let callback = { self.ui_conts.borrow_mut().remove(&id) };
715        if let Some(callback) = callback {
716            let value: Box<dyn Any> = value;
717            if !callback(value) {
718                self.ui_conts.borrow_mut().insert(id, callback);
719            }
720        }
721    }
722
723    fn cancel_ui_cont(&self, id: u64) {
724        self.ui_conts.borrow_mut().remove(&id);
725    }
726
727    fn register_frame_callback(&self, callback: Box<dyn FnOnce(u64) + 'static>) -> FrameCallbackId {
728        let id = self.next_frame_callback_id.get();
729        self.next_frame_callback_id.set(id + 1);
730        self.frame_callbacks
731            .borrow_mut()
732            .push_back(FrameCallbackEntry {
733                id,
734                callback: Some(callback),
735            });
736        self.schedule();
737        id
738    }
739
740    fn cancel_frame_callback(&self, id: FrameCallbackId) {
741        let mut callbacks = self.frame_callbacks.borrow_mut();
742        if let Some(index) = callbacks.iter().position(|entry| entry.id == id) {
743            callbacks.remove(index);
744        }
745        drop(callbacks);
746        self.clear_needs_frame_if_idle();
747    }
748
749    /// Stops asking the display for frames when nothing is left for one to do.
750    ///
751    /// These four are the complete set of reasons to want another frame: a
752    /// scope to recompose, a queued node update, a registered frame callback,
753    /// or UI work that can run. If none of them holds, the next frame would
754    /// wake the process, walk an unchanged tree, and present nothing.
755    fn clear_needs_frame_if_idle(&self) {
756        if !self.has_invalid_scopes()
757            && !self.has_updates()
758            && !self.has_frame_callbacks()
759            && !self.has_pending_ui()
760        {
761            *self.needs_frame.borrow_mut() = false;
762        }
763    }
764
765    fn drain_frame_callbacks(&self, frame_time_nanos: u64) {
766        self.last_frame_time_nanos.set(Some(frame_time_nanos));
767        let mut callbacks = self.frame_callbacks.borrow_mut();
768        let mut pending: Vec<Box<dyn FnOnce(u64) + 'static>> = Vec::with_capacity(callbacks.len());
769        while let Some(mut entry) = callbacks.pop_front() {
770            if let Some(callback) = entry.callback.take() {
771                pending.push(callback);
772            }
773        }
774        drop(callbacks);
775
776        // Wrap ALL frame callbacks in a single mutable snapshot so state changes
777        // are properly applied to the global snapshot and visible to subsequent reads.
778        // Using a single snapshot for all callbacks avoids stack exhaustion from
779        // repeated snapshot creation in long-running animation loops.
780        if !pending.is_empty() {
781            let _ = crate::run_in_mutable_snapshot(|| {
782                for callback in pending {
783                    callback(frame_time_nanos);
784                }
785            });
786        }
787
788        self.clear_needs_frame_if_idle();
789    }
790
791    fn debug_stats(&self) -> RuntimeDebugStats {
792        let node_updates = self.node_updates.borrow();
793        let invalid_scopes = self.invalid_scopes.borrow();
794        let scope_queue = self.scope_queue.borrow();
795        let frame_callbacks = self.frame_callbacks.borrow();
796        let local_tasks = self.local_tasks.borrow();
797        let ui_conts = self.ui_conts.borrow();
798        let tasks = self.tasks.borrow();
799        let external_state_owners = self.external_state_owners.borrow();
800
801        RuntimeDebugStats {
802            node_updates_len: node_updates.len(),
803            node_updates_cap: node_updates.capacity(),
804            invalid_scopes_len: invalid_scopes.len(),
805            invalid_scopes_cap: invalid_scopes.capacity(),
806            scope_queue_len: scope_queue.len(),
807            scope_queue_cap: scope_queue.capacity(),
808            frame_callbacks_len: frame_callbacks.len(),
809            frame_callbacks_cap: frame_callbacks.capacity(),
810            local_tasks_len: local_tasks.len(),
811            local_tasks_cap: local_tasks.capacity(),
812            ui_conts_len: ui_conts.len(),
813            ui_conts_cap: ui_conts.capacity(),
814            tasks_len: tasks.len(),
815            tasks_cap: tasks.capacity(),
816            external_state_owners_len: external_state_owners.len(),
817            external_state_owners_cap: external_state_owners.capacity(),
818            ui_dispatcher_pending: self.ui_dispatcher.pending.load(Ordering::SeqCst),
819        }
820    }
821}
822
823#[derive(Clone)]
824pub struct Runtime {
825    inner: Rc<RuntimeInner>,
826}
827
828impl Runtime {
829    pub fn new(scheduler: Arc<dyn RuntimeScheduler>) -> Self {
830        let inner = Rc::new(RuntimeInner::new(scheduler));
831        let runtime = Self { inner };
832        let handle = runtime.handle();
833        register_runtime_handle(&handle);
834        LAST_RUNTIME.with(|slot| *slot.borrow_mut() = Some(handle));
835        runtime
836    }
837
838    pub fn handle(&self) -> RuntimeHandle {
839        RuntimeHandle {
840            inner: Rc::downgrade(&self.inner),
841            dispatcher: UiDispatcher::new(self.inner.ui_dispatcher.clone()),
842            ui_thread_id: self.inner.ui_thread_id,
843            id: self.inner.runtime_id,
844        }
845    }
846
847    pub fn has_updates(&self) -> bool {
848        self.inner.has_updates()
849    }
850
851    pub fn needs_frame(&self) -> bool {
852        // The stored flag is only half the answer. A task woken from another
853        // thread cannot touch this runtime's `RefCell`s, so its waker can only
854        // set the shared `runnable` flag and ping the scheduler.
855        //
856        // A merely pending UI continuation does NOT belong here: main's "Avoid
857        // frames for idle UI continuations" moved that to an update-only wake
858        // (`has_pending_ui` in the app shell), so an idle continuation no
859        // longer costs a frame.
860        *self.inner.needs_frame.borrow() || self.inner.has_runnable_tasks()
861    }
862
863    pub fn set_needs_frame(&self, value: bool) {
864        *self.inner.needs_frame.borrow_mut() = value;
865    }
866
867    /// Animation-clock time of the most recent frame-callback drain. This is
868    /// the same clock `Animatable`s advance on (virtual under robot exact
869    /// captures), so per-frame integrators derive dt from it instead of wall
870    /// time.
871    pub fn last_frame_time_nanos(&self) -> Option<u64> {
872        self.inner.last_frame_time_nanos.get()
873    }
874
875    #[cfg(any(feature = "internal", test))]
876    pub fn frame_clock(&self) -> FrameClock {
877        FrameClock::new(self.handle())
878    }
879}
880
881impl Drop for Runtime {
882    fn drop(&mut self) {
883        if Rc::strong_count(&self.inner) != 1 {
884            return;
885        }
886        unregister_runtime_handle(self.inner.runtime_id);
887        LAST_RUNTIME.with(|slot| {
888            let should_clear = slot
889                .borrow()
890                .as_ref()
891                .is_some_and(|handle| handle.id() == self.inner.runtime_id);
892            if should_clear {
893                *slot.borrow_mut() = None;
894            }
895        });
896    }
897}
898
899#[derive(Default)]
900pub struct DefaultScheduler;
901
902impl RuntimeScheduler for DefaultScheduler {
903    fn schedule_frame(&self) {}
904}
905
906#[cfg(test)]
907#[derive(Default)]
908pub struct TestScheduler;
909
910#[cfg(test)]
911impl RuntimeScheduler for TestScheduler {
912    fn schedule_frame(&self) {}
913}
914
915#[cfg(test)]
916pub struct TestRuntime {
917    runtime: Runtime,
918}
919
920#[cfg(test)]
921impl Default for TestRuntime {
922    fn default() -> Self {
923        Self::new()
924    }
925}
926
927#[cfg(test)]
928impl TestRuntime {
929    pub fn new() -> Self {
930        Self {
931            runtime: Runtime::new(Arc::new(TestScheduler)),
932        }
933    }
934
935    pub fn handle(&self) -> RuntimeHandle {
936        self.runtime.handle()
937    }
938}
939
940#[derive(Clone)]
941pub struct RuntimeHandle {
942    inner: Weak<RuntimeInner>,
943    dispatcher: UiDispatcher,
944    ui_thread_id: ThreadId,
945    id: RuntimeId,
946}
947
948pub struct TaskHandle {
949    id: u64,
950    runtime: RuntimeHandle,
951}
952
953struct DeferredStateRelease {
954    runtime: RuntimeHandle,
955    id: StateId,
956}
957
958pub(crate) struct StateHandleLease {
959    id: StateId,
960    runtime: RuntimeHandle,
961}
962
963impl StateHandleLease {
964    pub(crate) fn id(&self) -> StateId {
965        self.id
966    }
967
968    pub(crate) fn runtime(&self) -> RuntimeHandle {
969        self.runtime.clone()
970    }
971}
972
973impl Drop for StateHandleLease {
974    fn drop(&mut self) {
975        defer_state_release(self.runtime.clone(), self.id);
976    }
977}
978
979impl RuntimeHandle {
980    pub fn id(&self) -> RuntimeId {
981        self.id
982    }
983
984    pub(crate) fn alloc_state<T: Clone + 'static>(&self, value: T) -> Rc<StateHandleLease> {
985        let id = self.with_state_arena(|arena| arena.alloc(value, self.clone()));
986        let lease = Rc::new(StateHandleLease {
987            id,
988            runtime: self.clone(),
989        });
990        self.with_state_arena(|arena| arena.register_lease(id, &lease));
991        lease
992    }
993
994    pub(crate) fn alloc_state_with_policy<T: Clone + 'static>(
995        &self,
996        value: T,
997        policy: Arc<dyn MutationPolicy<T>>,
998    ) -> Rc<StateHandleLease> {
999        let id =
1000            self.with_state_arena(|arena| arena.alloc_with_policy(value, self.clone(), policy));
1001        let lease = Rc::new(StateHandleLease {
1002            id,
1003            runtime: self.clone(),
1004        });
1005        self.with_state_arena(|arena| arena.register_lease(id, &lease));
1006        lease
1007    }
1008
1009    pub(crate) fn alloc_persistent_state<T: Clone + 'static>(
1010        &self,
1011        value: T,
1012    ) -> crate::MutableState<T> {
1013        let lease = self.alloc_state(value);
1014        if let Some(inner) = self.inner.upgrade() {
1015            inner
1016                .external_state_owners
1017                .borrow_mut()
1018                .insert(lease.id(), Rc::clone(&lease));
1019        }
1020        crate::MutableState::from_lease(&lease)
1021    }
1022
1023    pub(crate) fn retain_state_lease(&self, id: StateId) -> Option<Rc<StateHandleLease>> {
1024        self.with_state_arena(|arena| arena.retain_lease(id))
1025    }
1026
1027    pub(crate) fn with_state_arena<R>(&self, f: impl FnOnce(&StateArena) -> R) -> R {
1028        self.try_with_state_arena(f)
1029            .unwrap_or_else(|| panic!("runtime dropped"))
1030    }
1031
1032    pub(crate) fn try_with_state_arena<R>(&self, f: impl FnOnce(&StateArena) -> R) -> Option<R> {
1033        self.inner.upgrade().map(|inner| f(&inner.state_arena))
1034    }
1035
1036    fn release_state_immediate(&self, id: StateId) {
1037        if let Some(inner) = self.inner.upgrade() {
1038            inner.state_arena.release(id);
1039        }
1040    }
1041
1042    pub fn state_arena_stats(&self) -> (usize, usize) {
1043        self.try_with_state_arena(StateArena::stats)
1044            .unwrap_or_default()
1045    }
1046
1047    pub fn state_arena_debug_stats(&self) -> StateArenaDebugStats {
1048        self.try_with_state_arena(StateArena::debug_stats)
1049            .unwrap_or_default()
1050    }
1051
1052    pub fn debug_stats(&self) -> RuntimeDebugStats {
1053        self.inner
1054            .upgrade()
1055            .map(|inner| inner.debug_stats())
1056            .unwrap_or_default()
1057    }
1058
1059    pub(crate) fn unregister_state_scope(&self, id: StateId, scope_id: ScopeId) {
1060        if let Some(inner) = self.inner.upgrade() {
1061            inner.state_arena.unregister_scope(id, scope_id);
1062        }
1063    }
1064
1065    pub fn schedule(&self) {
1066        if let Some(inner) = self.inner.upgrade() {
1067            inner.schedule();
1068        }
1069    }
1070
1071    pub(crate) fn enqueue_node_update(&self, command: Command) {
1072        if let Some(inner) = self.inner.upgrade() {
1073            inner.enqueue_update(command);
1074        }
1075    }
1076
1077    /// Schedules work that must run on the runtime thread.
1078    ///
1079    /// The closure executes on the UI thread immediately when the runtime
1080    /// drains its local queue, so it may capture `Rc`/`RefCell` values. Calling
1081    /// this from any other thread is a logic error and will panic in debug
1082    /// builds via the inner assertion.
1083    pub fn enqueue_ui_task(&self, task: Box<dyn FnOnce() + 'static>) {
1084        if let Some(inner) = self.inner.upgrade() {
1085            inner.enqueue_ui_task(task);
1086        } else {
1087            task();
1088        }
1089    }
1090
1091    pub fn spawn_ui<F>(&self, fut: F) -> Option<TaskHandle>
1092    where
1093        F: Future<Output = ()> + 'static,
1094    {
1095        self.inner.upgrade().map(|inner| {
1096            let id = inner.spawn_ui_task(Box::pin(fut));
1097            TaskHandle {
1098                id,
1099                runtime: self.clone(),
1100            }
1101        })
1102    }
1103
1104    pub fn cancel_task(&self, id: u64) {
1105        if let Some(inner) = self.inner.upgrade() {
1106            inner.cancel_task(id);
1107        }
1108    }
1109
1110    /// Enqueues work from any thread to run on the UI thread.
1111    ///
1112    /// The closure must be `Send` because it may cross threads before executing
1113    /// on the runtime thread. Use this when posting from background work.
1114    pub fn post_ui(&self, task: impl FnOnce() + Send + 'static) {
1115        self.dispatcher.post(task);
1116    }
1117
1118    pub fn register_ui_cont<T: 'static>(&self, f: impl FnOnce(T) + 'static) -> Option<u64> {
1119        self.inner.upgrade().map(|inner| inner.register_ui_cont(f))
1120    }
1121
1122    pub fn cancel_ui_cont(&self, id: u64) {
1123        if let Some(inner) = self.inner.upgrade() {
1124            inner.cancel_ui_cont(id);
1125        }
1126    }
1127
1128    pub fn drain_ui(&self) {
1129        if let Some(inner) = self.inner.upgrade() {
1130            inner.drain_ui();
1131        }
1132    }
1133
1134    pub fn has_pending_ui(&self) -> bool {
1135        self.inner
1136            .upgrade()
1137            .map(|inner| inner.has_pending_ui())
1138            .unwrap_or_else(|| self.dispatcher.has_pending())
1139    }
1140
1141    pub fn register_frame_callback(
1142        &self,
1143        callback: impl FnOnce(u64) + 'static,
1144    ) -> Option<FrameCallbackId> {
1145        self.inner
1146            .upgrade()
1147            .map(|inner| inner.register_frame_callback(Box::new(callback)))
1148    }
1149
1150    pub fn cancel_frame_callback(&self, id: FrameCallbackId) {
1151        if let Some(inner) = self.inner.upgrade() {
1152            inner.cancel_frame_callback(id);
1153        }
1154    }
1155
1156    pub fn drain_frame_callbacks(&self, frame_time_nanos: u64) {
1157        if let Some(inner) = self.inner.upgrade() {
1158            inner.drain_frame_callbacks(frame_time_nanos);
1159        }
1160    }
1161
1162    /// Animation-clock time of the most recent frame-callback drain (see
1163    /// [`Runtime::last_frame_time_nanos`]).
1164    pub fn last_frame_time_nanos(&self) -> Option<u64> {
1165        self.inner
1166            .upgrade()
1167            .and_then(|inner| inner.last_frame_time_nanos.get())
1168    }
1169
1170    #[cfg(any(feature = "internal", test))]
1171    pub fn frame_clock(&self) -> FrameClock {
1172        FrameClock::new(self.clone())
1173    }
1174
1175    pub fn set_needs_frame(&self, value: bool) {
1176        if let Some(inner) = self.inner.upgrade() {
1177            *inner.needs_frame.borrow_mut() = value;
1178        }
1179    }
1180
1181    pub(crate) fn take_updates(&self) -> Vec<Command> {
1182        self.inner
1183            .upgrade()
1184            .map(|inner| inner.take_updates())
1185            .unwrap_or_default()
1186    }
1187
1188    pub fn has_updates(&self) -> bool {
1189        self.inner
1190            .upgrade()
1191            .map(|inner| inner.has_updates())
1192            .unwrap_or(false)
1193    }
1194
1195    pub(crate) fn mark_scope_recomposed(&self, id: ScopeId) {
1196        if let Some(inner) = self.inner.upgrade() {
1197            inner.mark_scope_recomposed(id);
1198        }
1199    }
1200
1201    pub(crate) fn register_invalid_scope(&self, id: ScopeId, scope: Weak<RecomposeScopeInner>) {
1202        if let Some(inner) = self.inner.upgrade() {
1203            inner.register_invalid_scope(id, scope);
1204        }
1205    }
1206
1207    pub(crate) fn requeue_invalid_scope(&self, id: ScopeId, scope: Weak<RecomposeScopeInner>) {
1208        if let Some(inner) = self.inner.upgrade() {
1209            inner.requeue_invalid_scope(id, scope);
1210        }
1211    }
1212
1213    pub(crate) fn take_invalidated_scopes(&self) -> Vec<(ScopeId, Weak<RecomposeScopeInner>)> {
1214        self.inner
1215            .upgrade()
1216            .map(|inner| inner.take_invalidated_scopes())
1217            .unwrap_or_default()
1218    }
1219
1220    pub fn has_invalid_scopes(&self) -> bool {
1221        self.inner
1222            .upgrade()
1223            .map(|inner| inner.has_invalid_scopes())
1224            .unwrap_or(false)
1225    }
1226
1227    pub(crate) fn increment_live_recompose_scope_count(&self) {
1228        if let Some(inner) = self.inner.upgrade() {
1229            inner.increment_live_recompose_scope_count();
1230        }
1231    }
1232
1233    pub(crate) fn decrement_live_recompose_scope_count(&self) {
1234        if let Some(inner) = self.inner.upgrade() {
1235            inner.decrement_live_recompose_scope_count();
1236        }
1237    }
1238
1239    fn live_recompose_scope_count(&self) -> usize {
1240        self.inner
1241            .upgrade()
1242            .map(|inner| inner.live_recompose_scope_count())
1243            .unwrap_or_default()
1244    }
1245
1246    #[doc(hidden)]
1247    pub fn debug_invalid_scope_ids(&self) -> Vec<usize> {
1248        self.inner
1249            .upgrade()
1250            .map(|inner| inner.invalid_scopes.borrow().iter().copied().collect())
1251            .unwrap_or_default()
1252    }
1253
1254    pub fn has_frame_callbacks(&self) -> bool {
1255        self.inner
1256            .upgrade()
1257            .map(|inner| inner.has_frame_callbacks())
1258            .unwrap_or(false)
1259    }
1260
1261    pub fn assert_ui_thread(&self) {
1262        debug_assert_eq!(
1263            std::thread::current().id(),
1264            self.ui_thread_id,
1265            "state mutated off the runtime's UI thread"
1266        );
1267    }
1268
1269    pub fn dispatcher(&self) -> UiDispatcher {
1270        self.dispatcher.clone()
1271    }
1272
1273    #[doc(hidden)]
1274    pub fn with_deferred_state_releases<R>(&self, f: impl FnOnce() -> R) -> R {
1275        let _scope = enter_state_teardown_scope();
1276        f()
1277    }
1278}
1279
1280impl TaskHandle {
1281    pub fn cancel(self) {
1282        self.runtime.cancel_task(self.id);
1283    }
1284}
1285
1286pub(crate) struct FrameCallbackEntry {
1287    id: FrameCallbackId,
1288    callback: Option<Box<dyn FnOnce(u64) + 'static>>,
1289}
1290
1291/// The waker for one spawned UI task.
1292///
1293/// Waking marks that task runnable and asks the platform for a frame, which is
1294/// what eventually reaches [`RuntimeInner::poll_async_tasks`]. The `runnable`
1295/// flag is the half that matters for idling: without it a wake is
1296/// indistinguishable from any other, so the runtime cannot tell a task that is
1297/// ready from one that is parked.
1298///
1299/// `runnable` is an `Arc<AtomicBool>` rather than a `Cell` because a task may
1300/// legitimately be woken from another thread — a JNI callback, a worker
1301/// finishing, a platform service replying.
1302#[cfg(not(target_arch = "wasm32"))]
1303struct RuntimeTaskWaker {
1304    scheduler: Arc<dyn RuntimeScheduler>,
1305    runnable: Arc<AtomicBool>,
1306}
1307
1308#[cfg(target_arch = "wasm32")]
1309struct RuntimeTaskWaker {
1310    runtime_id: RuntimeId,
1311    runnable: Arc<AtomicBool>,
1312}
1313
1314impl RuntimeTaskWaker {
1315    #[cfg(not(target_arch = "wasm32"))]
1316    fn new(inner: &RuntimeInner, runnable: Arc<AtomicBool>) -> Self {
1317        let scheduler = inner.scheduler.clone();
1318        Self {
1319            scheduler,
1320            runnable,
1321        }
1322    }
1323
1324    #[cfg(target_arch = "wasm32")]
1325    fn new(inner: &RuntimeInner, runnable: Arc<AtomicBool>) -> Self {
1326        let runtime_id = inner.runtime_id;
1327        Self {
1328            runtime_id,
1329            runnable,
1330        }
1331    }
1332
1333    fn into_waker(self) -> Waker {
1334        futures_task::waker(Arc::new(self))
1335    }
1336}
1337
1338impl futures_task::ArcWake for RuntimeTaskWaker {
1339    #[cfg(not(target_arch = "wasm32"))]
1340    fn wake_by_ref(arc_self: &Arc<Self>) {
1341        arc_self.runnable.store(true, Ordering::Release);
1342        arc_self.scheduler.schedule_frame();
1343    }
1344
1345    #[cfg(target_arch = "wasm32")]
1346    fn wake_by_ref(arc_self: &Arc<Self>) {
1347        arc_self.runnable.store(true, Ordering::Release);
1348        REGISTERED_RUNTIMES.with(|registry| {
1349            if let Some(handle) = registry.borrow().get(&arc_self.runtime_id).cloned() {
1350                handle.schedule();
1351            }
1352        });
1353    }
1354}
1355
1356thread_local! {
1357    static NEXT_RUNTIME_ID: Cell<u32> = const { Cell::new(1) };
1358    static ACTIVE_RUNTIMES: RefCell<Vec<RuntimeHandle>> = const { RefCell::new(Vec::new()) };
1359    static LAST_RUNTIME: RefCell<Option<RuntimeHandle>> = const { RefCell::new(None) };
1360    static REGISTERED_RUNTIMES: RefCell<HashMap<RuntimeId, RuntimeHandle>> = RefCell::new(HashMap::default());
1361    static STATE_TEARDOWN_DEPTH: Cell<usize> = const { Cell::new(0) };
1362    static DEFERRED_STATE_RELEASES: RefCell<Vec<DeferredStateRelease>> = const { RefCell::new(Vec::new()) };
1363}
1364
1365#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
1366pub struct RuntimeThreadLocalDebugStats {
1367    pub active_runtimes_len: usize,
1368    pub active_runtimes_cap: usize,
1369    pub registered_runtimes_len: usize,
1370    pub registered_runtimes_cap: usize,
1371    pub deferred_state_releases_len: usize,
1372    pub deferred_state_releases_cap: usize,
1373}
1374
1375/// Gets the current runtime handle from thread-local storage.
1376///
1377/// Returns the most recently pushed active runtime, or the last known runtime.
1378/// Used by fling animation and other components that need access to the runtime.
1379pub fn current_runtime_handle() -> Option<RuntimeHandle> {
1380    if let Some(handle) = ACTIVE_RUNTIMES.with(|stack| stack.borrow().last().cloned()) {
1381        return Some(handle);
1382    }
1383    LAST_RUNTIME.with(|slot| slot.borrow().clone())
1384}
1385
1386pub(crate) fn runtime_handle_by_id(id: RuntimeId) -> Option<RuntimeHandle> {
1387    REGISTERED_RUNTIMES.with(|registry| registry.borrow().get(&id).cloned())
1388}
1389
1390pub(crate) fn live_recompose_scope_count() -> usize {
1391    REGISTERED_RUNTIMES.with(|registry| {
1392        registry
1393            .borrow()
1394            .values()
1395            .map(RuntimeHandle::live_recompose_scope_count)
1396            .sum()
1397    })
1398}
1399
1400pub fn debug_runtime_thread_local_stats() -> RuntimeThreadLocalDebugStats {
1401    let (active_runtimes_len, active_runtimes_cap) = ACTIVE_RUNTIMES.with(|stack| {
1402        let stack = stack.borrow();
1403        (stack.len(), stack.capacity())
1404    });
1405    let (registered_runtimes_len, registered_runtimes_cap) = REGISTERED_RUNTIMES.with(|registry| {
1406        let registry = registry.borrow();
1407        (registry.len(), registry.capacity())
1408    });
1409    let (deferred_state_releases_len, deferred_state_releases_cap) =
1410        DEFERRED_STATE_RELEASES.with(|releases| {
1411            let releases = releases.borrow();
1412            (releases.len(), releases.capacity())
1413        });
1414
1415    RuntimeThreadLocalDebugStats {
1416        active_runtimes_len,
1417        active_runtimes_cap,
1418        registered_runtimes_len,
1419        registered_runtimes_cap,
1420        deferred_state_releases_len,
1421        deferred_state_releases_cap,
1422    }
1423}
1424
1425fn register_runtime_handle(handle: &RuntimeHandle) {
1426    REGISTERED_RUNTIMES.with(|registry| {
1427        registry.borrow_mut().insert(handle.id(), handle.clone());
1428    });
1429}
1430
1431fn unregister_runtime_handle(id: RuntimeId) {
1432    REGISTERED_RUNTIMES.with(|registry| {
1433        registry.borrow_mut().remove(&id);
1434    });
1435}
1436
1437fn defer_state_release(runtime: RuntimeHandle, id: StateId) {
1438    let teardown_active = STATE_TEARDOWN_DEPTH.with(|depth| depth.get() > 0);
1439    if teardown_active {
1440        DEFERRED_STATE_RELEASES.with(|releases| {
1441            releases
1442                .borrow_mut()
1443                .push(DeferredStateRelease { runtime, id });
1444        });
1445    } else {
1446        runtime.release_state_immediate(id);
1447    }
1448}
1449
1450fn flush_deferred_state_releases() {
1451    DEFERRED_STATE_RELEASES.with(|releases| {
1452        let mut releases = releases.borrow_mut();
1453        while let Some(deferred) = releases.pop() {
1454            deferred.runtime.release_state_immediate(deferred.id);
1455        }
1456    });
1457}
1458
1459pub(crate) struct StateTeardownScope;
1460
1461pub(crate) fn enter_state_teardown_scope() -> StateTeardownScope {
1462    STATE_TEARDOWN_DEPTH.with(|depth| depth.set(depth.get() + 1));
1463    StateTeardownScope
1464}
1465
1466impl Drop for StateTeardownScope {
1467    fn drop(&mut self) {
1468        STATE_TEARDOWN_DEPTH.with(|depth| {
1469            let next = depth.get().saturating_sub(1);
1470            depth.set(next);
1471            if next == 0 {
1472                flush_deferred_state_releases();
1473            }
1474        });
1475    }
1476}
1477
1478pub(crate) fn push_active_runtime(handle: &RuntimeHandle) {
1479    register_runtime_handle(handle);
1480    ACTIVE_RUNTIMES.with(|stack| stack.borrow_mut().push(handle.clone()));
1481    LAST_RUNTIME.with(|slot| *slot.borrow_mut() = Some(handle.clone()));
1482}
1483
1484pub(crate) fn pop_active_runtime() {
1485    ACTIVE_RUNTIMES.with(|stack| {
1486        stack.borrow_mut().pop();
1487    });
1488}
1489
1490/// Schedule a new frame render using the most recently active runtime handle.
1491pub fn schedule_frame() {
1492    if let Some(handle) = current_runtime_handle() {
1493        handle.schedule();
1494        return;
1495    }
1496    log::debug!(
1497        target: "cranpose::runtime",
1498        "ignoring frame request without an active runtime",
1499    );
1500}
1501
1502/// Schedule an in-place node update using the most recently active runtime.
1503pub fn schedule_node_update(
1504    update: impl FnOnce(&mut dyn Applier) -> Result<(), NodeError> + 'static,
1505) {
1506    if let Some(handle) = current_runtime_handle() {
1507        handle.enqueue_node_update(Command::callback(update));
1508    } else {
1509        drop(update);
1510        log::debug!(
1511            target: "cranpose::runtime",
1512            "ignoring node update request without an active runtime",
1513        );
1514    }
1515}
1516
1517#[cfg(test)]
1518mod tests {
1519    use super::*;
1520
1521    #[test]
1522    fn state_arena_alloc_skips_free_slot_outside_cell_storage() {
1523        let runtime = TestRuntime::new();
1524        let arena = StateArena::default();
1525        let first = arena.alloc(1_i32, runtime.handle());
1526        arena.inner.borrow_mut().free.push(u32::MAX);
1527
1528        let second = arena.alloc(2_i32, runtime.handle());
1529
1530        assert_eq!(first.slot(), 0);
1531        assert_eq!(second.slot(), 1);
1532        assert!(arena.get_typed_opt::<i32>(first).is_some());
1533        assert!(arena.get_typed_opt::<i32>(second).is_some());
1534    }
1535
1536    #[test]
1537    fn state_arena_alloc_skips_occupied_free_slot() {
1538        let runtime = TestRuntime::new();
1539        let arena = StateArena::default();
1540        let first = arena.alloc(1_i32, runtime.handle());
1541        arena.inner.borrow_mut().free.push(first.slot());
1542
1543        let second = arena.alloc(2_i32, runtime.handle());
1544
1545        assert_ne!(first.slot(), second.slot());
1546        assert_eq!(second.slot(), 1);
1547        assert!(arena.get_typed_opt::<i32>(first).is_some());
1548        assert!(arena.get_typed_opt::<i32>(second).is_some());
1549    }
1550
1551    #[test]
1552    fn state_arena_register_lease_ignores_stale_id() {
1553        let runtime = TestRuntime::new();
1554        let arena = StateArena::default();
1555        let stale_id = StateId::new(99, 0);
1556        let lease = Rc::new(StateHandleLease {
1557            id: stale_id,
1558            runtime: runtime.handle(),
1559        });
1560
1561        arena.register_lease(stale_id, &lease);
1562
1563        assert!(arena.retain_lease(stale_id).is_none());
1564    }
1565
1566    #[test]
1567    fn ui_continuation_type_mismatch_is_ignored_until_matching_payload() {
1568        let runtime = TestRuntime::new();
1569        let handle = runtime.handle();
1570        let received = Rc::new(Cell::new(None));
1571        let received_for_continuation = Rc::clone(&received);
1572        let cont_id = handle
1573            .register_ui_cont(move |value: u32| {
1574                received_for_continuation.set(Some(value));
1575            })
1576            .expect("test runtime is alive");
1577
1578        handle.dispatcher().post_invoke(cont_id, "wrong payload");
1579        handle.drain_ui();
1580
1581        assert_eq!(received.get(), None);
1582        assert_eq!(handle.debug_stats().ui_conts_len, 1);
1583
1584        handle.dispatcher().post_invoke(cont_id, 42_u32);
1585        handle.drain_ui();
1586
1587        assert_eq!(received.get(), Some(42));
1588        assert_eq!(handle.debug_stats().ui_conts_len, 0);
1589    }
1590
1591    #[test]
1592    fn ui_dispatcher_failed_send_does_not_leave_pending_work() {
1593        let runtime = Runtime::new(Arc::new(TestScheduler));
1594        let dispatcher = runtime.handle().dispatcher();
1595
1596        drop(runtime);
1597
1598        assert!(!dispatcher.has_pending());
1599        dispatcher.post(|| {});
1600        assert!(!dispatcher.has_pending());
1601        dispatcher.post_invoke(404, 12_u32);
1602        assert!(!dispatcher.has_pending());
1603    }
1604
1605    #[test]
1606    fn pending_guard_does_not_wrap_on_underflow() {
1607        let counter = AtomicUsize::new(0);
1608
1609        {
1610            let _guard = PendingGuard::new(&counter);
1611        }
1612
1613        assert_eq!(counter.load(Ordering::SeqCst), 0);
1614    }
1615
1616    #[test]
1617    fn pending_guard_decrements_pending_count() {
1618        let counter = AtomicUsize::new(2);
1619
1620        {
1621            let _guard = PendingGuard::new(&counter);
1622        }
1623
1624        assert_eq!(counter.load(Ordering::SeqCst), 1);
1625    }
1626
1627    #[test]
1628    fn schedule_frame_without_runtime_is_ignored() {
1629        let ok = std::thread::spawn(|| std::panic::catch_unwind(super::schedule_frame).is_ok())
1630            .join()
1631            .expect("test thread should join");
1632
1633        assert!(ok);
1634    }
1635
1636    #[test]
1637    fn schedule_node_update_without_runtime_is_ignored() {
1638        let ok = std::thread::spawn(|| {
1639            std::panic::catch_unwind(|| {
1640                super::schedule_node_update(|_| Ok(()));
1641            })
1642            .is_ok()
1643        })
1644        .join()
1645        .expect("test thread should join");
1646
1647        assert!(ok);
1648    }
1649}