Up until this point, I've been using closures the way it's defined in the wasm-bingen guide:
The wasm-bindgen guide: Web-sys: Closures
But now I have a bad memory leak issue (browser/workstation freezes, OOM within 20 seconds) when I use more than just test data.
Navigation: psuedo-code:
ListView:
- Deletes content (e.g. DetailView).
- loop and create a list of HTML links, each one creating a long-lived closure (and capturing it's environment) for opening DetailView.
let callback_enter = {
let big_obj_copy = big_obj.clone();
let mut sm_obj_copy = sm_obj.clone();
move |event: Event| {
event.prevent_default();
let anchor = event
.current_target()
.expect("Some: `EventTarget`")
.dyn_into::<web_sys::HtmlAnchorElement>()
.expect("Some: `HtmlAnchorElement`");
sm_obj_copy
.entries
.insert(String::from("anchor"), sm_objEnum::Anchor(anchor));
controller::show_detail(1, &big_obj_copy, &mut sm_obj_copy);
}
};
let enter_element = get_element_by_id("item-XXXX-detail");
let enter_htmlanchorelement = enter_element
.dyn_ref::<HtmlAnchorElement>()
.expect("Some: `HtmlAnchorElement`");
let enter_closure =
Closure::wrap(Box::new(callback_enter) as Box<dyn FnMut(Event)>);
let enter_closure_ref = enter_closure.as_ref().unchecked_ref();
enter_htmlanchorelement.set_onclick(Some(enter_closure_ref));
enter_closure.forget();
DetailView:
- Deletes content (e.g. ListView).
- Displays content based on caputured environment from ListView,
- contains a long-lived closure (caputuring environment), for returning to ListView.
let callback_leave = {
let big_obj_copy = big_obj.clone();
let mut sm_obj_copy = sm_obj.clone();
move |event: Event| {
event.prevent_default();
sm_obj_copy.entries.remove("anchor"); // no longer needed
controller::show_list(1, &big_obj_copy, &mut sm_obj_copy);
}
};
let element = get_element_by_id("detail-leave");
let htmlbuttonelement = element
.dyn_ref::<HtmlButtonElement>()
.expect("Some: `HtmlButtonElement`");
let closure = Closure::wrap(Box::new(callback_leave) as Box<dyn FnMut(Event)>);
let closure_ref = closure.as_ref().unchecked_ref();
htmlbuttonelement.set_onclick(Some(closure_ref));
closure.forget();
Filter psuedo-code:
ListView (list view)
- loop and create a list of HTML links, each one creating a long-lived closure (and capturing it's environment) for opening DetailView.
(see ListView closure above)
FilterBox
- On keyup: Deletes ListView. Recreates (filtered) ListView.
let callback_keyup = {
let big_obj_copy = big_obj.clone();
let mut sm_obj_copy = sm_obj.clone();
move |event: Event| {
event.prevent_default();
// ... get element references
// ... update reset button
// remove list
remove_elements(vec!["list-wrapper"]);
// ... update listeners
// ... add list
add_list(&big_obj_copy, &sm_obj_copy);
}
};
If I have less than ten items I dont't notice anything. Clicking back and forth is OK. Filtering the list is OK.
But when I have thousands of items. I realize that on every rountrip to the ListView, or on every KeyUp event, I am creating thousands of new closures that use .forget()
. That's my best understanding of the situation at the moment.
So one thing I tried to to change as many closures to Box+FnOnce
as possible. But that had almost no effect because they still required .forget()
to compile.
The next thing I tried was adding a drop handler based on this link:
[Question] How do I remove event listener?
So I was able to add this:
pub struct Listener {
element: web_sys::EventTarget,
name: &'static str,
callback: Closure<dyn FnMut(Event)>,
}
impl Listener {
pub fn new<F>(element: EventTarget, name: &'static str, callback: F) -> Self
where
F: FnMut(Event) + 'static,
{
let callback = Closure::wrap(Box::new(callback) as Box<dyn FnMut(Event)>);
let cb_ref = callback.as_ref().unchecked_ref();
element
.add_event_listener_with_callback(name, cb_ref)
.expect("Result<(), JsValue>");
Self { element, name, callback }
}
}
impl Drop for Listener {
fn drop(&mut self) {
let name = self.name;
let cb_ref = self.callback.as_ref().unchecked_ref();
self.element
.remove_event_listener_with_callback(name, cb_ref)
.expect("Result<(), JsValue>");
}
}
And use it like this
let leave_el = get_element_by_id("detail-leave");
let leave_tgt = leave_el.clone().into();
let leave_el_ref = leave_el
.dyn_ref::<HtmlButtonElement>()
.expect("Some: `HtmlButtonElement`");
// add listener stuff
let leave_cb = {
let big_obj_copy = big_obj.clone();
let sm_obj_copy = sm_obj.clone();
move |event: Event| {
event.prevent_default();
controller::stg_init(1, &big_obj_copy, &sm_obj_copy);
}
};
let leave_ls = Listener::new(leave_tgt, "click", leave_cb);
// drop listener stuff
let leave_drop = move || { drop(leave_ls); };
let leave_cl = Closure::once(Box::new(leave_drop) as Box<dyn FnOnce()>);
let leave_cl_ref = leave_cl.as_ref().unchecked_ref();
leave_el_ref.set_onclick(Some(leave_cl_ref));
leave_cl.forget();
But the issues remain:
- If I have thousands of links - and only one gets clicked - then only one closure gets dropped. I need all the list closures dropped.
- The keyup, and navigate-back modules are separate from, and do not have access to, the drop handlers created in the ListView, so how could they trigger them?
Any ideas would be appreciated. Thanks.