Skip to main content

incular_runtime/
tasks.rs

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