Skip to content

Commit 45a550b

Browse files
jprochazkemilk
authored andcommitted
Fix: Don't .forget() RAF closure (emilk#4551)
The closure is now stored in `WebRunner` and dropped in the next `request_animation_frame` instead of being "leaked" via `forget` --------- Co-authored-by: Emil Ernerfeldt <emil.ernerfeldt@gmail.com>
1 parent e23f800 commit 45a550b

File tree

1 file changed

+35
-11
lines changed

1 file changed

+35
-11
lines changed

crates/eframe/src/web/web_runner.rs

+35-11
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,4 @@
1-
use std::{
2-
cell::{Cell, RefCell},
3-
rc::Rc,
4-
};
1+
use std::{cell::RefCell, rc::Rc};
52

63
use wasm_bindgen::prelude::*;
74

@@ -29,7 +26,7 @@ pub struct WebRunner {
2926
events_to_unsubscribe: Rc<RefCell<Vec<EventToUnsubscribe>>>,
3027

3128
/// Current animation frame in flight.
32-
request_animation_frame_id: Cell<Option<i32>>,
29+
frame: Rc<RefCell<Option<AnimationFrameRequest>>>,
3330

3431
resize_observer: Rc<RefCell<Option<ResizeObserverContext>>>,
3532
}
@@ -49,7 +46,7 @@ impl WebRunner {
4946
panic_handler,
5047
runner: Rc::new(RefCell::new(None)),
5148
events_to_unsubscribe: Rc::new(RefCell::new(Default::default())),
52-
request_animation_frame_id: Cell::new(None),
49+
frame: Default::default(),
5350
resize_observer: Default::default(),
5451
}
5552
}
@@ -125,9 +122,9 @@ impl WebRunner {
125122
pub fn destroy(&self) {
126123
self.unsubscribe_from_all_events();
127124

128-
if let Some(id) = self.request_animation_frame_id.get() {
125+
if let Some(frame) = self.frame.take() {
129126
let window = web_sys::window().unwrap();
130-
window.cancel_animation_frame(id).ok();
127+
window.cancel_animation_frame(frame.id).ok();
131128
}
132129

133130
if let Some(runner) = self.runner.replace(None) {
@@ -202,16 +199,33 @@ impl WebRunner {
202199
Ok(())
203200
}
204201

202+
/// Request an animation frame from the browser in which we can perform a paint.
203+
///
204+
/// It is safe to call `request_animation_frame` multiple times in quick succession,
205+
/// this function guarantees that only one animation frame is scheduled at a time.
205206
pub(crate) fn request_animation_frame(&self) -> Result<(), wasm_bindgen::JsValue> {
207+
if self.frame.borrow().is_some() {
208+
// there is already an animation frame in flight
209+
return Ok(());
210+
}
211+
206212
let window = web_sys::window().unwrap();
207213
let closure = Closure::once({
208214
let runner_ref = self.clone();
209-
move || events::paint_and_schedule(&runner_ref)
215+
move || {
216+
// We can paint now, so clear the animation frame.
217+
// This drops the `closure` and allows another
218+
// animation frame to be scheduled
219+
let _ = runner_ref.frame.take();
220+
events::paint_and_schedule(&runner_ref)
221+
}
210222
});
211223

212224
let id = window.request_animation_frame(closure.as_ref().unchecked_ref())?;
213-
self.request_animation_frame_id.set(Some(id));
214-
closure.forget(); // We must forget it, or else the callback is canceled on drop
225+
self.frame.borrow_mut().replace(AnimationFrameRequest {
226+
id,
227+
_closure: closure,
228+
});
215229

216230
Ok(())
217231
}
@@ -232,6 +246,16 @@ impl WebRunner {
232246

233247
// ----------------------------------------------------------------------------
234248

249+
// https://rustwasm.github.io/wasm-bindgen/api/wasm_bindgen/closure/struct.Closure.html#using-fnonce-and-closureonce-with-requestanimationframe
250+
struct AnimationFrameRequest {
251+
/// Represents the ID of a frame in flight.
252+
id: i32,
253+
254+
/// The callback given to `request_animation_frame`, stored here both to prevent it
255+
/// from being canceled, and from having to `.forget()` it.
256+
_closure: Closure<dyn FnMut() -> Result<(), JsValue>>,
257+
}
258+
235259
struct ResizeObserverContext {
236260
resize_observer: web_sys::ResizeObserver,
237261
closure: Closure<dyn FnMut(js_sys::Array)>,

0 commit comments

Comments
 (0)