Fix event handler memory leak (#2298)

* fix event handler memory leak and double drops

* Prevent double dropping generational boxes

* recycle instead of dropping
This commit is contained in:
Evan Almloff authored and GitHub committed 2024-04-26 00:47:22 -04:00
1 parent a27d4e71ed
commit 47c87568e1
7 files changed
+182 -54

No files matched your search

+56 -11
View File
@@ -1,6 +1,9 @@
use crate::{global_context::current_scope_id, Runtime, ScopeId};
use generational_box::GenerationalBox;
use std::{cell::Cell, rc::Rc};
use std::{
cell::{Cell, RefCell},
rc::Rc,
};
/// A wrapper around some generic data that handles the event's state
///
@@ -163,9 +166,34 @@ impl<T: std::fmt::Debug> std::fmt::Debug for Event<T> {
/// ```
pub struct EventHandler<T = ()> {
pub(crate) origin: ScopeId,
// During diffing components with EventHandler, we move the EventHandler over in place instead of rerunning the child component.
// ```rust
// #[component]
// fn Child(onclick: EventHandler<MouseEvent>) -> Element {
// rsx!{
// button {
// // Diffing Child will not rerun this component, it will just update the EventHandler in place so that if this callback is called, it will run the latest version of the callback
// onclick: move |evt| cx.onclick.call(evt),
// }
// }
/// }
/// ```
// This is both more efficient and allows us to avoid out of date EventHandlers.
//
// We double box here because we want the data to be copy (GenerationalBox) and still update in place (ExternalListenerCallback)
// This isn't an ideal solution for performance, but it is non-breaking and fixes the issues described in https://github.com/DioxusLabs/dioxus/pull/2298
pub(super) callback: GenerationalBox<Option<ExternalListenerCallback<T>>>,
}
impl<T> std::fmt::Debug for EventHandler<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("EventHandler")
.field("origin", &self.origin)
.field("callback", &self.callback)
.finish()
}
}
impl<T: 'static> Default for EventHandler<T> {
fn default() -> Self {
EventHandler::new(|_| {})
@@ -192,15 +220,29 @@ impl<T: 'static> PartialEq for EventHandler<T> {
}
}
type ExternalListenerCallback<T> = Box<dyn FnMut(T)>;
type ExternalListenerCallback<T> = Rc<RefCell<dyn FnMut(T)>>;
impl<T: 'static> EventHandler<T> {
/// Create a new [`EventHandler`] from an [`FnMut`]
/// Create a new [`EventHandler`] from an [`FnMut`]. The callback is owned by the current scope and will be dropped when the scope is dropped.
/// This should not be called directly in the body of a component because it will not be dropped until the component is dropped.
#[track_caller]
pub fn new(mut f: impl FnMut(T) + 'static) -> EventHandler<T> {
let callback = GenerationalBox::leak(Some(Box::new(move |event: T| {
let owner = crate::innerlude::current_owner::<generational_box::UnsyncStorage>();
let callback = owner.insert(Some(Rc::new(RefCell::new(move |event: T| {
f(event);
}) as Box<dyn FnMut(T)>));
})) as Rc<RefCell<dyn FnMut(T)>>));
EventHandler {
callback,
origin: current_scope_id().expect("to be in a dioxus runtime"),
}
}
/// Leak a new [`EventHandler`] that will not be dropped unless it is manually dropped.
#[track_caller]
pub fn leak(mut f: impl FnMut(T) + 'static) -> EventHandler<T> {
let callback = GenerationalBox::leak(Some(Rc::new(RefCell::new(move |event: T| {
f(event);
})) as Rc<RefCell<dyn FnMut(T)>>));
EventHandler {
callback,
origin: current_scope_id().expect("to be in a dioxus runtime"),
@@ -211,9 +253,12 @@ impl<T: 'static> EventHandler<T> {
///
/// This borrows the event using a RefCell. Recursively calling a listener will cause a panic.
pub fn call(&self, event: T) {
if let Some(callback) = self.callback.write().as_mut() {
if let Some(callback) = self.callback.read().as_ref() {
Runtime::with(|rt| rt.scope_stack.borrow_mut().push(self.origin));
callback(event);
{
let mut callback = callback.borrow_mut();
callback(event);
}
Runtime::with(|rt| rt.scope_stack.borrow_mut().pop());
}
}
@@ -227,16 +272,16 @@ impl<T: 'static> EventHandler<T> {
#[doc(hidden)]
/// This should only be used by the `rsx!` macro.
pub fn __set(&mut self, value: impl FnMut(T) + 'static) {
self.callback.set(Some(Box::new(value)));
pub fn __set(&mut self, value: ExternalListenerCallback<T>) {
self.callback.set(Some(value));
}
#[doc(hidden)]
/// This should only be used by the `rsx!` macro.
pub fn __take(&self) -> ExternalListenerCallback<T> {
self.callback
.manually_drop()
.expect("Signal has already been dropped")
.read()
.clone()
.expect("EventHandler was manually dropped")
}
}
+2 -2
View File
@@ -737,7 +737,7 @@ impl AttributeValue {
pub fn listener<T: 'static>(mut callback: impl FnMut(Event<T>) + 'static) -> AttributeValue {
// TODO: maybe don't use the copy-variant of EventHandler here?
// Maybe, create an Owned variant so we are less likely to run into leaks
AttributeValue::Listener(EventHandler::new(move |event: Event<dyn Any>| {
AttributeValue::Listener(EventHandler::leak(move |event: Event<dyn Any>| {
let data = event.data.downcast::<T>().unwrap();
callback(Event {
propagates: event.propagates,
@@ -761,7 +761,7 @@ impl std::fmt::Debug for AttributeValue {
Self::Float(arg0) => f.debug_tuple("Float").field(arg0).finish(),
Self::Int(arg0) => f.debug_tuple("Int").field(arg0).finish(),
Self::Bool(arg0) => f.debug_tuple("Bool").field(arg0).finish(),
Self::Listener(_) => f.debug_tuple("Listener").finish(),
Self::Listener(listener) => f.debug_tuple("Listener").field(listener).finish(),
Self::Any(_) => f.debug_tuple("Any").finish(),
Self::None => write!(f, "None"),
}