Skip to main content

incular_runtime/
tasks.rs

1//! Application work and the UI-thread completion bridge.
2//!
3//! The target executor (Tokio on native targets, browser microtasks on
4//! `wasm32`) owns every general future, timer, and blocking job. This module
5//! owns only Incular-specific lifetime metadata and a bounded message drain
6//! into the retained UI runtime; it never polls a user future itself.
7
8use std::{
9    collections::HashMap,
10    panic::{self, AssertUnwindSafe},
11    rc::{Rc, Weak},
12    sync::{
13        Arc, Mutex, Weak as ArcWeak,
14        atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering},
15        mpsc,
16    },
17};
18
19use incular_platform::WindowId;
20use incular_widgets::internal::ElementId;
21
22use crate::{
23    Runtime,
24    executor::{AbortHandle, Completion, Executor},
25};
26
27const DEFAULT_UI_MESSAGE_BUDGET: usize = 128;
28
29/// Native event-loop wake bridge. Implementations are thread-safe and must
30/// never borrow the retained UI runtime directly.
31pub trait RuntimeWake: Send + Sync + 'static {
32    fn wake(&self);
33}
34
35/// Failure of Tokio-backed work as observed by the UI owner.
36#[derive(Clone, Debug, PartialEq, Eq)]
37pub enum TaskFailure {
38    Cancelled,
39    Panicked,
40    RuntimeStopped,
41}
42
43/// A small application-state value for asynchronous results. It is not an
44/// executor; applications normally store it in a [`Signal`](crate::Signal)
45/// from a UI callback. The error type is application-owned so callers can
46/// preserve structured domain failures instead of converting everything to a
47/// string or a framework-specific snapshot.
48#[derive(Clone, Debug, PartialEq, Eq)]
49pub enum AsyncState<T, E> {
50    Idle,
51    Loading,
52    Ready(T),
53    Error(E),
54}
55
56impl<T, E> AsyncState<T, E> {
57    #[must_use]
58    pub const fn is_loading(&self) -> bool {
59        matches!(self, Self::Loading)
60    }
61
62    #[must_use]
63    pub const fn is_terminal(&self) -> bool {
64        matches!(self, Self::Ready(_) | Self::Error(_))
65    }
66
67    #[must_use]
68    pub const fn is_idle(&self) -> bool {
69        matches!(self, Self::Idle)
70    }
71
72    #[must_use]
73    pub const fn is_ready(&self) -> bool {
74        matches!(self, Self::Ready(_))
75    }
76
77    #[must_use]
78    pub const fn is_error(&self) -> bool {
79        matches!(self, Self::Error(_))
80    }
81
82    /// Maps a successful value while preserving loading/idle/error states.
83    #[must_use]
84    pub fn map<U>(self, map: impl FnOnce(T) -> U) -> AsyncState<U, E> {
85        match self {
86            Self::Idle => AsyncState::Idle,
87            Self::Loading => AsyncState::Loading,
88            Self::Ready(value) => AsyncState::Ready(map(value)),
89            Self::Error(error) => AsyncState::Error(error),
90        }
91    }
92}
93
94/// Backwards-compatible task failure state. New application code should use
95/// [`AsyncState<T, E>`] with its own error type; this alias is retained for
96/// task handles whose cancellation errors are owned by the runtime.
97pub type AsyncValue<T> = AsyncState<T, TaskFailure>;
98
99/// A cancellation and diagnostics handle for application work tracked by Incular.
100/// It intentionally is not another `JoinHandle`: use [`TokioHandle`] for
101/// direct Tokio APIs when component ownership is not required.
102#[derive(Clone)]
103pub struct TaskHandle {
104    control: Arc<TaskControl>,
105}
106
107impl TaskHandle {
108    /// Requests cancellation. Tokio async tasks are aborted; a started
109    /// `spawn_blocking` job may finish, but its UI result is discarded.
110    pub fn cancel(&self) {
111        self.control.cancel();
112    }
113
114    #[must_use]
115    pub fn is_cancelled(&self) -> bool {
116        self.control.is_cancelled()
117    }
118}
119
120/// A typed UI-observable result of a application task. This wrapper adds ownership
121/// and cancellation metadata; it does not implement `Future` or duplicate
122/// Tokio's join API.
123pub struct Task<T> {
124    state: Arc<Mutex<AsyncValue<T>>>,
125    handle: TaskHandle,
126}
127
128impl<T: Clone> Task<T> {
129    #[must_use]
130    pub fn value(&self) -> AsyncValue<T> {
131        if self.handle.is_cancelled() {
132            AsyncValue::Error(TaskFailure::Cancelled)
133        } else {
134            self.state.lock().expect("task state mutex").clone()
135        }
136    }
137}
138
139impl<T> Task<T> {
140    pub fn cancel(&self) {
141        self.handle.cancel();
142    }
143
144    #[must_use]
145    pub fn handle(&self) -> TaskHandle {
146        self.handle.clone()
147    }
148}
149
150/// Structured Incular ownership for application tasks. Scopes may be associated with
151/// a generational element identity; completions verify that identity on the UI
152/// thread before running a local callback.
153#[derive(Clone)]
154pub struct TaskScope {
155    state: Arc<ScopeState>,
156}
157
158struct ScopeState {
159    cancelled: AtomicBool,
160    owner: Mutex<Option<ElementId>>,
161    window: Mutex<Option<WindowId>>,
162    children: Mutex<HashMap<u64, ArcWeak<TaskControl>>>,
163    child_scopes: Mutex<Vec<ArcWeak<ScopeState>>>,
164    parent: ArcWeak<ScopeState>,
165}
166
167impl TaskScope {
168    fn new() -> Self {
169        Self {
170            state: Arc::new(ScopeState {
171                cancelled: AtomicBool::new(false),
172                owner: Mutex::new(None),
173                window: Mutex::new(None),
174                children: Mutex::new(HashMap::new()),
175                child_scopes: Mutex::new(Vec::new()),
176                parent: ArcWeak::new(),
177            }),
178        }
179    }
180
181    /// Creates a scope whose cancellation is bounded by this scope. This is
182    /// the ownership relation used for application → window → component work.
183    #[must_use]
184    pub(crate) fn child(&self) -> Self {
185        let child = Self {
186            state: Arc::new(ScopeState {
187                cancelled: AtomicBool::new(self.is_cancelled()),
188                owner: Mutex::new(None),
189                window: Mutex::new(self.window_id()),
190                children: Mutex::new(HashMap::new()),
191                child_scopes: Mutex::new(Vec::new()),
192                parent: Arc::downgrade(&self.state),
193            }),
194        };
195        self.state
196            .child_scopes
197            .lock()
198            .expect("task scope children mutex")
199            .push(Arc::downgrade(&child.state));
200        child
201    }
202
203    pub fn cancel(&self) {
204        if self.state.cancelled.swap(true, Ordering::AcqRel) {
205            return;
206        }
207        let children: Vec<_> = self
208            .state
209            .children
210            .lock()
211            .expect("task scope mutex")
212            .values()
213            .filter_map(ArcWeak::upgrade)
214            .collect();
215        for child in children {
216            child.cancel();
217        }
218        let child_scopes: Vec<_> = self
219            .state
220            .child_scopes
221            .lock()
222            .expect("task scope children mutex")
223            .drain(..)
224            .filter_map(|scope| scope.upgrade())
225            .collect();
226        for scope in child_scopes {
227            TaskScope { state: scope }.cancel();
228        }
229    }
230
231    #[must_use]
232    pub fn is_cancelled(&self) -> bool {
233        self.state.cancelled.load(Ordering::Acquire)
234            || self
235                .state
236                .parent
237                .upgrade()
238                .is_some_and(|parent| parent.cancelled.load(Ordering::Acquire))
239    }
240
241    pub(crate) fn bind_owner(&self, owner: ElementId) {
242        *self.state.owner.lock().expect("task scope owner mutex") = Some(owner);
243    }
244
245    pub(crate) fn bind_window(&self, window: WindowId) {
246        *self.state.window.lock().expect("task scope window mutex") = Some(window);
247    }
248
249    #[must_use]
250    pub(crate) fn window_id(&self) -> Option<WindowId> {
251        (*self.state.window.lock().expect("task scope window mutex")).or_else(|| {
252            self.state
253                .parent
254                .upgrade()
255                .and_then(|parent| *parent.window.lock().expect("task scope window mutex"))
256        })
257    }
258
259    fn attach(&self, control: &Arc<TaskControl>) {
260        self.state
261            .children
262            .lock()
263            .expect("task scope mutex")
264            .insert(control.id, Arc::downgrade(control));
265    }
266
267    fn detach(&self, id: u64) {
268        self.state
269            .children
270            .lock()
271            .expect("task scope mutex")
272            .remove(&id);
273    }
274}
275
276impl Drop for TaskScope {
277    fn drop(&mut self) {
278        // A task only keeps a weak reference to its scope. The final owner
279        // therefore cancels children rather than accidentally extending them.
280        if Arc::strong_count(&self.state) == 1 {
281            self.cancel();
282        }
283    }
284}
285
286pub(crate) struct TaskControl {
287    id: u64,
288    cancelled: AtomicBool,
289    scope: ArcWeak<ScopeState>,
290    abort: Mutex<Option<AbortHandle>>,
291}
292
293impl TaskControl {
294    fn cancel(&self) {
295        self.cancelled.store(true, Ordering::Release);
296        if let Some(abort) = self.abort.lock().expect("task abort mutex").as_ref() {
297            abort.abort();
298        }
299    }
300
301    pub(crate) fn is_cancelled(&self) -> bool {
302        self.cancelled.load(Ordering::Acquire)
303            || self
304                .scope
305                .upgrade()
306                .is_none_or(|scope| scope.cancelled.load(Ordering::Acquire))
307    }
308
309    pub(crate) fn owner_is_stale(&self, runtime: &Runtime) -> bool {
310        let Some(scope) = self.scope.upgrade() else {
311            return true;
312        };
313        scope
314            .owner
315            .lock()
316            .expect("task scope owner mutex")
317            .is_some_and(|owner| !runtime.tree().element_exists(owner))
318    }
319
320    pub(crate) fn window_id(&self) -> Option<WindowId> {
321        self.scope.upgrade().and_then(|scope| {
322            (*scope.window.lock().expect("task scope window mutex")).or_else(|| {
323                scope
324                    .parent
325                    .upgrade()
326                    .and_then(|parent| *parent.window.lock().expect("task scope window mutex"))
327            })
328        })
329    }
330}
331
332/// The explicit Tokio handle exposed for advanced libraries. Direct tasks use
333/// Tokio/application lifetime; they are not automatically component-scoped.
334///
335/// Browser targets have no Tokio runtime; use the spawner APIs there.
336#[cfg(not(target_arch = "wasm32"))]
337pub type TokioHandle = tokio::runtime::Handle;
338
339/// UI-thread-only helper that launches application work with Incular owner tracking.
340#[derive(Clone)]
341pub struct RuntimeSpawner {
342    scheduler: Weak<std::cell::RefCell<TaskScheduler>>,
343    application_scope: TaskScope,
344    window_id: Option<WindowId>,
345}
346
347impl RuntimeSpawner {
348    pub(crate) const fn window_id(&self) -> Option<WindowId> {
349        self.window_id
350    }
351
352    /// Starts application-scoped application work. Its output is observed through the
353    /// returned state handle; use `spawn_into` to update UI-local state.
354    pub fn spawn<F, T>(&self, future: F) -> Task<T>
355    where
356        F: std::future::Future<Output = T> + Send + 'static,
357        T: Send + 'static,
358    {
359        self.spawn_in(&self.application_scope, future)
360    }
361
362    pub fn spawn_in<F, T>(&self, scope: &TaskScope, future: F) -> Task<T>
363    where
364        F: std::future::Future<Output = T> + Send + 'static,
365        T: Send + 'static,
366    {
367        self.scheduler
368            .upgrade()
369            .expect("runtime spawner outlived runtime")
370            .borrow_mut()
371            .spawn_task(scope, future)
372    }
373
374    /// Starts application work and invokes `complete` later on the UI thread. The
375    /// callback may capture non-`Send` `Signal`s because only the `Send` output
376    /// crosses the Tokio/UI boundary.
377    pub fn spawn_into<F, T>(
378        &self,
379        future: F,
380        complete: impl FnOnce(Result<T, TaskFailure>, &mut Runtime) + 'static,
381    ) -> TaskHandle
382    where
383        F: std::future::Future<Output = T> + Send + 'static,
384        T: Send + 'static,
385    {
386        self.spawn_into_in(&self.application_scope, future, complete)
387    }
388
389    pub fn spawn_into_in<F, T>(
390        &self,
391        scope: &TaskScope,
392        future: F,
393        complete: impl FnOnce(Result<T, TaskFailure>, &mut Runtime) + 'static,
394    ) -> TaskHandle
395    where
396        F: std::future::Future<Output = T> + Send + 'static,
397        T: Send + 'static,
398    {
399        self.scheduler
400            .upgrade()
401            .expect("runtime spawner outlived runtime")
402            .borrow_mut()
403            .spawn_into(scope, future, complete)
404    }
405
406    /// Submits work to Tokio's blocking facility. A cancellation after work
407    /// begins is represented as an uninterested/discarded UI completion.
408    pub fn spawn_blocking<T>(
409        &self,
410        work: impl FnOnce() -> T + Send + 'static,
411        complete: impl FnOnce(Result<T, TaskFailure>, &mut Runtime) + 'static,
412    ) -> TaskHandle
413    where
414        T: Send + 'static,
415    {
416        self.spawn_blocking_in(&self.application_scope, work, complete)
417    }
418
419    pub fn spawn_blocking_in<T>(
420        &self,
421        scope: &TaskScope,
422        work: impl FnOnce() -> T + Send + 'static,
423        complete: impl FnOnce(Result<T, TaskFailure>, &mut Runtime) + 'static,
424    ) -> TaskHandle
425    where
426        T: Send + 'static,
427    {
428        self.scheduler
429            .upgrade()
430            .expect("runtime spawner outlived runtime")
431            .borrow_mut()
432            .spawn_blocking_in(scope, work, complete)
433    }
434
435    #[must_use]
436    pub fn scope(&self) -> TaskScope {
437        self.application_scope.child()
438    }
439
440    #[must_use]
441    #[cfg(not(target_arch = "wasm32"))]
442    pub fn tokio_handle(&self) -> TokioHandle {
443        self.scheduler
444            .upgrade()
445            .expect("runtime spawner outlived runtime")
446            .borrow()
447            .tokio_handle()
448    }
449
450    #[must_use]
451    pub fn dispatcher(&self) -> UiDispatcher {
452        self.scheduler
453            .upgrade()
454            .expect("runtime spawner outlived runtime")
455            .borrow()
456            .dispatcher_for(self.window_id)
457    }
458}
459
460/// Cross-thread dispatcher into the unique UI owner. It is a message sender,
461/// not access to `Runtime`, and does not imply a redraw.
462#[derive(Clone)]
463pub struct UiDispatcher {
464    bridge: Arc<RuntimeBridge>,
465    window_id: Option<WindowId>,
466}
467
468impl UiDispatcher {
469    pub fn dispatch(&self, callback: impl FnOnce(&mut Runtime) + Send + 'static) {
470        self.bridge.enqueue(RuntimeMessage::Dispatch {
471            window_id: self.window_id,
472            callback: Box::new(callback),
473        });
474    }
475}
476
477pub(crate) type UiCallback = Box<dyn FnOnce(&mut Runtime) + Send>;
478pub(crate) type TaskFinish = Box<dyn FnOnce(&mut Runtime) -> Option<TaskFailure>>;
479pub(crate) type TaskDiscard = Box<dyn FnOnce()>;
480
481enum RuntimeMessage {
482    TaskCompleted(u64),
483    Dispatch {
484        window_id: Option<WindowId>,
485        callback: UiCallback,
486    },
487}
488
489struct RuntimeBridge {
490    sender: mpsc::Sender<RuntimeMessage>,
491    wake: Mutex<Option<Arc<dyn RuntimeWake>>>,
492    accepting: AtomicBool,
493    wake_pending: AtomicBool,
494    queued: AtomicUsize,
495    queue_peak: AtomicUsize,
496    messages_enqueued: AtomicU64,
497    wakes: AtomicU64,
498    coalesced_wakes: AtomicU64,
499}
500
501impl RuntimeBridge {
502    fn enqueue(&self, message: RuntimeMessage) {
503        if !self.accepting.load(Ordering::Acquire) {
504            return;
505        }
506        // Count before sending: once sent, the UI thread may receive and
507        // decrement before this thread runs again, which would wrap the
508        // counter if the increment came second.
509        let queued = self.queued.fetch_add(1, Ordering::AcqRel) + 1;
510        if self.sender.send(message).is_err() {
511            self.queued.fetch_sub(1, Ordering::AcqRel);
512            return;
513        }
514        self.messages_enqueued.fetch_add(1, Ordering::Relaxed);
515        self.queue_peak.fetch_max(queued, Ordering::Relaxed);
516        self.request_wake();
517    }
518
519    fn request_wake(&self) {
520        if self.wake_pending.swap(true, Ordering::AcqRel) {
521            self.coalesced_wakes.fetch_add(1, Ordering::Relaxed);
522            return;
523        }
524        self.wakes.fetch_add(1, Ordering::Relaxed);
525        if let Some(wake) = self.wake.lock().expect("runtime wake mutex").as_ref() {
526            wake.wake();
527        }
528    }
529
530    fn set_wake(&self, wake: Arc<dyn RuntimeWake>) {
531        *self.wake.lock().expect("runtime wake mutex") = Some(wake.clone());
532        if self.queued.load(Ordering::Acquire) > 0 {
533            self.wakes.fetch_add(1, Ordering::Relaxed);
534            wake.wake();
535        }
536    }
537}
538
539pub(crate) struct PendingTask {
540    pub(crate) control: Arc<TaskControl>,
541    pub(crate) finish: TaskFinish,
542    pub(crate) discard: TaskDiscard,
543    pub(crate) blocking: bool,
544}
545
546/// Diagnostics of Incular/Tokio integration, not Tokio's internal executor.
547#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
548pub struct RuntimeDiagnostics {
549    pub app_scoped_tasks_spawned: u64,
550    pub owner_scoped_tasks_spawned: u64,
551    pub active_tracked_tasks: usize,
552    pub tasks_completed: u64,
553    pub tasks_cancelled: u64,
554    pub task_join_failures: u64,
555    pub task_panics: u64,
556    pub blocking_jobs_spawned: u64,
557    pub blocking_jobs_completed: u64,
558    pub blocking_results_discarded: u64,
559    pub ui_messages_enqueued: u64,
560    pub ui_messages_processed: u64,
561    pub ui_message_queue_peak: usize,
562    pub winit_runtime_wakes: u64,
563    pub coalesced_wakes: u64,
564    pub stale_completion_rejections: u64,
565}
566
567pub(crate) enum UiWork {
568    Dispatch {
569        window_id: Option<WindowId>,
570        callback: UiCallback,
571    },
572    Task(PendingTask),
573}
574
575/// Owns a Tokio runtime and a UI-thread message receiver. It is not an
576/// executor implementation: it starts application tasks and drains their owned UI
577/// completion messages fairly.
578pub(crate) struct TaskScheduler {
579    bridge: Arc<RuntimeBridge>,
580    receiver: mpsc::Receiver<RuntimeMessage>,
581    pending: HashMap<u64, PendingTask>,
582    executor: Executor,
583    application_scope: TaskScope,
584    next_task: u64,
585    diagnostics: RuntimeDiagnostics,
586}
587
588impl TaskScheduler {
589    pub(crate) fn new() -> Rc<std::cell::RefCell<Self>> {
590        let (sender, receiver) = mpsc::channel();
591        let bridge = Arc::new(RuntimeBridge {
592            sender,
593            wake: Mutex::new(None),
594            accepting: AtomicBool::new(true),
595            wake_pending: AtomicBool::new(false),
596            queued: AtomicUsize::new(0),
597            queue_peak: AtomicUsize::new(0),
598            messages_enqueued: AtomicU64::new(0),
599            wakes: AtomicU64::new(0),
600            coalesced_wakes: AtomicU64::new(0),
601        });
602        Rc::new(std::cell::RefCell::new(Self {
603            bridge,
604            receiver,
605            pending: HashMap::new(),
606            executor: Executor::new(),
607            application_scope: TaskScope::new(),
608            next_task: 1,
609            diagnostics: RuntimeDiagnostics::default(),
610        }))
611    }
612
613    pub(crate) fn spawner(scheduler: &Rc<std::cell::RefCell<Self>>) -> RuntimeSpawner {
614        let borrowed = scheduler.borrow();
615        RuntimeSpawner {
616            scheduler: Rc::downgrade(scheduler),
617            application_scope: borrowed.application_scope.clone(),
618            window_id: None,
619        }
620    }
621
622    pub(crate) fn spawner_for(
623        scheduler: &Rc<std::cell::RefCell<Self>>,
624        window_id: WindowId,
625    ) -> RuntimeSpawner {
626        let mut spawner = Self::spawner(scheduler);
627        spawner.window_id = Some(window_id);
628        spawner
629    }
630
631    #[cfg(not(target_arch = "wasm32"))]
632    pub(crate) fn tokio_handle(&self) -> TokioHandle {
633        self.executor.handle()
634    }
635
636    fn next_control(&mut self, scope: &TaskScope) -> Arc<TaskControl> {
637        let id = self.next_task;
638        self.next_task = self
639            .next_task
640            .checked_add(1)
641            .expect("runtime task identity space exhausted");
642        let control = Arc::new(TaskControl {
643            id,
644            cancelled: AtomicBool::new(
645                scope.is_cancelled() || !self.bridge.accepting.load(Ordering::Acquire),
646            ),
647            scope: Arc::downgrade(&scope.state),
648            abort: Mutex::new(None),
649        });
650        scope.attach(&control);
651        // The declarative root is bound to its Element after its initial
652        // builder returns, so scope identity—not only an already-bound ID—is
653        // the stable classification during application construction.
654        if !Arc::ptr_eq(&scope.state, &self.application_scope.state) {
655            self.diagnostics.owner_scoped_tasks_spawned += 1;
656        } else {
657            self.diagnostics.app_scoped_tasks_spawned += 1;
658        }
659        control
660    }
661
662    fn spawn_task<F, T>(&mut self, scope: &TaskScope, future: F) -> Task<T>
663    where
664        F: std::future::Future<Output = T> + Send + 'static,
665        T: Send + 'static,
666    {
667        let state = Arc::new(Mutex::new(AsyncValue::Loading));
668        let state_for_finish = state.clone();
669        let state_for_discard = state.clone();
670        let handle = self.spawn_impl(
671            scope,
672            future,
673            move |outcome, _| {
674                let failure = outcome.as_ref().err().cloned();
675                *state_for_finish.lock().expect("task state mutex") = match outcome {
676                    Ok(value) => AsyncValue::Ready(value),
677                    Err(failure) => AsyncValue::Error(failure),
678                };
679                failure
680            },
681            move || {
682                *state_for_discard.lock().expect("task state mutex") =
683                    AsyncValue::Error(TaskFailure::Cancelled);
684            },
685        );
686        Task { state, handle }
687    }
688
689    fn spawn_into<F, T>(
690        &mut self,
691        scope: &TaskScope,
692        future: F,
693        complete: impl FnOnce(Result<T, TaskFailure>, &mut Runtime) + 'static,
694    ) -> TaskHandle
695    where
696        F: std::future::Future<Output = T> + Send + 'static,
697        T: Send + 'static,
698    {
699        self.spawn_impl(
700            scope,
701            future,
702            move |outcome, runtime| {
703                let failure = outcome.as_ref().err().cloned();
704                let callback = panic::catch_unwind(AssertUnwindSafe(|| complete(outcome, runtime)));
705                if callback.is_err() {
706                    Some(TaskFailure::Panicked)
707                } else {
708                    failure
709                }
710            },
711            || {},
712        )
713    }
714
715    pub(crate) fn spawn_blocking<T>(
716        &mut self,
717        work: impl FnOnce() -> T + Send + 'static,
718        complete: impl FnOnce(Result<T, TaskFailure>, &mut Runtime) + 'static,
719    ) -> TaskHandle
720    where
721        T: Send + 'static,
722    {
723        let scope = self.application_scope.clone();
724        self.spawn_blocking_in(&scope, work, complete)
725    }
726
727    fn spawn_blocking_in<T>(
728        &mut self,
729        scope: &TaskScope,
730        work: impl FnOnce() -> T + Send + 'static,
731        complete: impl FnOnce(Result<T, TaskFailure>, &mut Runtime) + 'static,
732    ) -> TaskHandle
733    where
734        T: Send + 'static,
735    {
736        self.diagnostics.blocking_jobs_spawned += 1;
737        self.register(
738            scope,
739            true,
740            |executor, complete| executor.spawn_blocking(work, complete),
741            move |outcome, runtime| {
742                let failure = outcome.as_ref().err().cloned();
743                let callback = panic::catch_unwind(AssertUnwindSafe(|| complete(outcome, runtime)));
744                if callback.is_err() {
745                    Some(TaskFailure::Panicked)
746                } else {
747                    failure
748                }
749            },
750            || {},
751        )
752    }
753
754    fn spawn_impl<F, T>(
755        &mut self,
756        scope: &TaskScope,
757        future: F,
758        finish: impl FnOnce(Result<T, TaskFailure>, &mut Runtime) -> Option<TaskFailure> + 'static,
759        discard: impl FnOnce() + 'static,
760    ) -> TaskHandle
761    where
762        F: std::future::Future<Output = T> + Send + 'static,
763        T: Send + 'static,
764    {
765        self.register(
766            scope,
767            false,
768            |executor, complete| executor.spawn(future, complete),
769            finish,
770            discard,
771        )
772    }
773
774    /// Starts work only after its control exists, so work whose scope is
775    /// already cancelled (or whose runtime stopped) is never handed to the
776    /// executor at all.
777    fn register<T>(
778        &mut self,
779        scope: &TaskScope,
780        blocking: bool,
781        start: impl FnOnce(&Executor, Completion<T>) -> AbortHandle,
782        finish: impl FnOnce(Result<T, TaskFailure>, &mut Runtime) -> Option<TaskFailure> + 'static,
783        discard: impl FnOnce() + 'static,
784    ) -> TaskHandle
785    where
786        T: Send + 'static,
787    {
788        let control = self.next_control(scope);
789        let task_handle = TaskHandle {
790            control: control.clone(),
791        };
792        if control.is_cancelled() {
793            discard();
794            self.diagnostics.tasks_cancelled += u64::from(!blocking);
795            self.diagnostics.blocking_results_discarded += u64::from(blocking);
796            scope.detach(control.id);
797            return task_handle;
798        }
799
800        let result = Arc::new(Mutex::new(None));
801        let result_for_completion = result.clone();
802        let complete_id = control.id;
803        let bridge = self.bridge.clone();
804        let abort = start(
805            &self.executor,
806            Box::new(move |outcome| {
807                *result_for_completion.lock().expect("task result mutex") = Some(outcome);
808                bridge.enqueue(RuntimeMessage::TaskCompleted(complete_id));
809            }),
810        );
811        *control.abort.lock().expect("task abort mutex") = Some(abort);
812        self.pending.insert(
813            control.id,
814            PendingTask {
815                control,
816                finish: Box::new(move |runtime| {
817                    let outcome = result
818                        .lock()
819                        .expect("task result mutex")
820                        .take()
821                        .unwrap_or(Err(TaskFailure::RuntimeStopped));
822                    finish(outcome, runtime)
823                }),
824                discard: Box::new(discard),
825                blocking,
826            },
827        );
828        task_handle
829    }
830
831    pub(crate) fn take_turn(&mut self) -> Vec<UiWork> {
832        // Clearing before the drain prevents a producer racing with this turn
833        // from being lost: it will enqueue one new native user event.
834        self.bridge.wake_pending.store(false, Ordering::Release);
835        let mut work = Vec::new();
836        let mut processed = 0;
837        while processed < DEFAULT_UI_MESSAGE_BUDGET {
838            let message = match self.receiver.try_recv() {
839                Ok(message) => message,
840                Err(mpsc::TryRecvError::Empty | mpsc::TryRecvError::Disconnected) => break,
841            };
842            processed += 1;
843            self.bridge.queued.fetch_sub(1, Ordering::AcqRel);
844            self.diagnostics.ui_messages_processed += 1;
845            match message {
846                RuntimeMessage::Dispatch {
847                    window_id,
848                    callback,
849                } => work.push(UiWork::Dispatch {
850                    window_id,
851                    callback,
852                }),
853                RuntimeMessage::TaskCompleted(id) => {
854                    if let Some(pending) = self.pending.remove(&id) {
855                        work.push(UiWork::Task(pending));
856                    }
857                }
858            }
859        }
860        if processed == DEFAULT_UI_MESSAGE_BUDGET && self.bridge.queued.load(Ordering::Acquire) > 0
861        {
862            self.bridge.request_wake();
863        }
864        work
865    }
866
867    pub(crate) fn accepting(&self) -> bool {
868        self.bridge.accepting.load(Ordering::Acquire)
869    }
870
871    pub(crate) fn detach_scope(&self, control: &TaskControl) {
872        if let Some(scope) = control.scope.upgrade() {
873            TaskScope { state: scope }.detach(control.id);
874        }
875    }
876
877    pub(crate) fn record_discard(&mut self, blocking: bool, stale_owner: bool) {
878        if blocking {
879            self.diagnostics.blocking_results_discarded += 1;
880        } else {
881            self.diagnostics.tasks_cancelled += 1;
882        }
883        if stale_owner {
884            self.diagnostics.stale_completion_rejections += 1;
885        }
886    }
887
888    pub(crate) fn record_completion(
889        &mut self,
890        blocking: bool,
891        failure: Option<TaskFailure>,
892    ) -> Option<TaskFailure> {
893        if blocking {
894            self.diagnostics.blocking_jobs_completed += 1;
895        } else {
896            self.diagnostics.tasks_completed += 1;
897        }
898        match failure {
899            Some(TaskFailure::Cancelled) => {
900                self.diagnostics.tasks_cancelled += 1;
901                None
902            }
903            Some(TaskFailure::Panicked) => {
904                self.diagnostics.task_join_failures += 1;
905                self.diagnostics.task_panics += 1;
906                Some(TaskFailure::Panicked)
907            }
908            Some(TaskFailure::RuntimeStopped) => {
909                self.diagnostics.task_join_failures += 1;
910                None
911            }
912            None => None,
913        }
914    }
915
916    pub(crate) fn dispatcher_for(&self, window_id: Option<WindowId>) -> UiDispatcher {
917        UiDispatcher {
918            bridge: self.bridge.clone(),
919            window_id,
920        }
921    }
922
923    pub(crate) fn set_wake(&self, wake: Arc<dyn RuntimeWake>) {
924        self.bridge.set_wake(wake);
925    }
926
927    pub(crate) fn shutdown(&mut self) {
928        if !self.bridge.accepting.swap(false, Ordering::AcqRel) {
929            return;
930        }
931        self.application_scope.cancel();
932        let pending = std::mem::take(&mut self.pending);
933        for (_, task) in pending {
934            task.control.cancel();
935            (task.discard)();
936            if task.blocking {
937                self.diagnostics.blocking_results_discarded += 1;
938            } else {
939                self.diagnostics.tasks_cancelled += 1;
940            }
941        }
942        while self.receiver.try_recv().is_ok() {
943            self.bridge.queued.fetch_sub(1, Ordering::AcqRel);
944        }
945        self.executor.shutdown();
946    }
947
948    pub(crate) fn diagnostics(&self) -> RuntimeDiagnostics {
949        let mut diagnostics = self.diagnostics;
950        diagnostics.active_tracked_tasks = self.pending.len();
951        diagnostics.ui_messages_enqueued = self.bridge.messages_enqueued.load(Ordering::Relaxed);
952        diagnostics.ui_message_queue_peak = self.bridge.queue_peak.load(Ordering::Relaxed);
953        diagnostics.winit_runtime_wakes = self.bridge.wakes.load(Ordering::Relaxed);
954        diagnostics.coalesced_wakes = self.bridge.coalesced_wakes.load(Ordering::Relaxed);
955        diagnostics
956    }
957
958    pub(crate) fn active_ownership(&self) -> (usize, usize) {
959        self.pending.values().fold((0, 0), |(app, owner), task| {
960            let application_owned = task
961                .control
962                .scope
963                .upgrade()
964                .is_some_and(|scope| Arc::ptr_eq(&scope, &self.application_scope.state));
965            if application_owned {
966                (app + 1, owner)
967            } else {
968                (app, owner + 1)
969            }
970        })
971    }
972}
973
974impl Drop for TaskScheduler {
975    fn drop(&mut self) {
976        // A headless/runtime owner that is simply dropped gets the same bounded
977        // Tokio shutdown policy as an explicit native-window close.
978        self.shutdown();
979    }
980}