Merge pull request #208 from DioxusLabs/jk/use-future-leak

feat: auto cancel tasks when scopes are dropped
This commit is contained in:
Jonathan Kelley authored and GitHub committed 2022-02-27 23:39:06 -05:00
commit 42979d922b
1 file changed
+60 -19
+60 -19
View File
@@ -7,7 +7,7 @@ use std::{
any::{Any, TypeId}, any::{Any, TypeId},
borrow::Borrow, borrow::Borrow,
cell::{Cell, RefCell}, cell::{Cell, RefCell},
collections::HashMap, collections::{HashMap, HashSet},
future::Future, future::Future,
pin::Pin, pin::Pin,
rc::Rc, rc::Rc,
@@ -68,7 +68,12 @@ impl ScopeArena {
heuristics: RefCell::new(FxHashMap::default()), heuristics: RefCell::new(FxHashMap::default()),
free_scopes: RefCell::new(Vec::new()), free_scopes: RefCell::new(Vec::new()),
nodes: RefCell::new(nodes), nodes: RefCell::new(nodes),
tasks: TaskQueue::new(sender), tasks: Rc::new(TaskQueue {
tasks: RefCell::new(FxHashMap::default()),
task_map: RefCell::new(FxHashMap::default()),
gen: Cell::new(0),
sender,
}),
} }
} }
@@ -179,6 +184,15 @@ impl ScopeArena {
log::trace!("removing scope {:?}", id); log::trace!("removing scope {:?}", id);
self.ensure_drop_safety(id); self.ensure_drop_safety(id);
// Dispose of any ongoing tasks
let mut tasks = self.tasks.tasks.borrow_mut();
let mut task_map = self.tasks.task_map.borrow_mut();
if let Some(cur_tasks) = task_map.remove(&id) {
for task in cur_tasks {
tasks.remove(&task);
}
}
// Safety: // Safety:
// - ensure_drop_safety ensures that no references to this scope are in use // - ensure_drop_safety ensures that no references to this scope are in use
// - this raw pointer is removed from the map // - this raw pointer is removed from the map
@@ -435,7 +449,13 @@ pub struct ScopeId(pub usize);
/// once a Task has been completed. /// once a Task has been completed.
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)] #[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)]
pub struct TaskId(pub usize); pub struct TaskId {
/// The global ID of the task
pub id: usize,
/// The original scope that this task was scheduled in
pub scope: ScopeId,
}
/// Every component in Dioxus is represented by a `ScopeState`. /// Every component in Dioxus is represented by a `ScopeState`.
/// ///
@@ -745,7 +765,7 @@ impl ScopeState {
.unbounded_send(SchedulerMsg::NewTask(self.our_arena_idx)) .unbounded_send(SchedulerMsg::NewTask(self.our_arena_idx))
.unwrap(); .unwrap();
self.tasks.push_fut(fut) self.tasks.spawn(self.our_arena_idx, fut)
} }
/// Spawns the future but does not return the TaskId /// Spawns the future but does not return the TaskId
@@ -753,10 +773,24 @@ impl ScopeState {
self.push_future(fut); self.push_future(fut);
} }
/// Spawn a future that Dioxus will never clean up
///
/// This is good for tasks that need to be run after the component has been dropped.
pub fn spawn_forever(&self, fut: impl Future<Output = ()> + 'static) -> TaskId {
// wake up the scheduler if it is sleeping
self.tasks
.sender
.unbounded_send(SchedulerMsg::NewTask(self.our_arena_idx))
.unwrap();
// The root scope will never be unmounted so we can just add the task at the top of the app
self.tasks.spawn(ScopeId(0), fut)
}
/// Informs the scheduler that this task is no longer needed and should be removed /// Informs the scheduler that this task is no longer needed and should be removed
/// on next poll. /// on next poll.
pub fn remove_future(&self, id: TaskId) { pub fn remove_future(&self, id: TaskId) {
self.tasks.remove_fut(id); self.tasks.remove(id);
} }
/// Take a lazy VNode structure and actually build it with the context of the VDom's efficient VNode allocator. /// Take a lazy VNode structure and actually build it with the context of the VDom's efficient VNode allocator.
@@ -940,36 +974,43 @@ impl BumpFrame {
pub(crate) struct TaskQueue { pub(crate) struct TaskQueue {
pub(crate) tasks: RefCell<FxHashMap<TaskId, InnerTask>>, pub(crate) tasks: RefCell<FxHashMap<TaskId, InnerTask>>,
pub(crate) task_map: RefCell<FxHashMap<ScopeId, HashSet<TaskId>>>,
gen: Cell<usize>, gen: Cell<usize>,
sender: UnboundedSender<SchedulerMsg>, sender: UnboundedSender<SchedulerMsg>,
} }
pub(crate) type InnerTask = Pin<Box<dyn Future<Output = ()>>>; pub(crate) type InnerTask = Pin<Box<dyn Future<Output = ()>>>;
impl TaskQueue { impl TaskQueue {
fn new(sender: UnboundedSender<SchedulerMsg>) -> Rc<Self> { fn spawn(&self, scope: ScopeId, task: impl Future<Output = ()> + 'static) -> TaskId {
Rc::new(Self {
tasks: RefCell::new(FxHashMap::default()),
gen: Cell::new(0),
sender,
})
}
fn push_fut(&self, task: impl Future<Output = ()> + 'static) -> TaskId {
let pinned = Box::pin(task); let pinned = Box::pin(task);
let id = self.gen.get(); let id = self.gen.get();
self.gen.set(id + 1); self.gen.set(id + 1);
let tid = TaskId(id); let tid = TaskId { id, scope };
self.tasks.borrow_mut().insert(tid, pinned); self.tasks.borrow_mut().insert(tid, pinned);
// also add to the task map
// when the component is unmounted we know to remove it from the map
self.task_map
.borrow_mut()
.entry(scope)
.or_default()
.insert(tid);
tid tid
} }
fn remove_fut(&self, id: TaskId) {
fn remove(&self, id: TaskId) {
if let Ok(mut tasks) = self.tasks.try_borrow_mut() { if let Ok(mut tasks) = self.tasks.try_borrow_mut() {
let _ = tasks.remove(&id); let _ = tasks.remove(&id);
} else { }
// todo: it should be okay to remote a fut while the queue is being polled
// However, it's not currently possible to do that. // the task map is still around, but it'll be removed when the scope is unmounted
log::trace!("Unable to remove task from task queue. This is probably a bug."); if let Some(task_map) = self.task_map.borrow_mut().get_mut(&id.scope) {
task_map.remove(&id);
} }
} }
pub(crate) fn has_tasks(&self) -> bool { pub(crate) fn has_tasks(&self) -> bool {
!self.tasks.borrow().is_empty() !self.tasks.borrow().is_empty()
} }