script: Move TimerListener creation to OneShotTimers (#34825)

Before each `OneShotTimers` would have a `TimerListener` for its
lifetime, holding the timer `TaskSource`. The issue with this is that
the `TaskSource` for dedicated workers keeps the main thread object
alive, so as long as the `OneShotTimers` alive (until the worker thread
exists), the main thread object would never be garbage collected.

This change makes the creation of the listener on-demand, avoiding the
long-lived handle to the main thread object and slightly simplifying
`OneShotTimers` at the expense of some more operations when scheduling a
timer.

Signed-off-by: Martin Robinson <mrobinson@igalia.com>
This commit is contained in:
Martin Robinson 2025-01-04 12:23:43 +01:00 committed by GitHub
parent b2eda71952
commit 5b6c75e358
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 53 additions and 56 deletions

View file

@ -60,7 +60,7 @@ use script_traits::{
PortMessageTask, ScriptMsg, ScriptToConstellationChan, PortMessageTask, ScriptMsg, ScriptToConstellationChan,
}; };
use servo_url::{ImmutableOrigin, MutableOrigin, ServoUrl}; use servo_url::{ImmutableOrigin, MutableOrigin, ServoUrl};
use timers::{BoxedTimerCallback, TimerEvent, TimerEventId, TimerEventRequest, TimerSource}; use timers::{TimerEventId, TimerEventRequest, TimerSource};
use uuid::Uuid; use uuid::Uuid;
#[cfg(feature = "webgpu")] #[cfg(feature = "webgpu")]
use webgpu::{DeviceLostReason, WebGPUDevice}; use webgpu::{DeviceLostReason, WebGPUDevice};
@ -386,13 +386,6 @@ struct BroadcastListener {
context: Trusted<GlobalScope>, context: Trusted<GlobalScope>,
} }
/// A wrapper between timer events coming in over IPC, and the event-loop.
#[derive(Clone)]
pub(crate) struct TimerListener {
task_source: TaskSource,
context: Trusted<GlobalScope>,
}
type FileListenerCallback = Box<dyn Fn(Rc<Promise>, Fallible<Vec<u8>>) + Send>; type FileListenerCallback = Box<dyn Fn(Rc<Promise>, Fallible<Vec<u8>>) + Send>;
/// A wrapper for the handling of file data received by the ipc router /// A wrapper for the handling of file data received by the ipc router
@ -525,37 +518,6 @@ impl BroadcastListener {
} }
} }
impl TimerListener {
/// Handle a timer-event coming from the [`timers::TimerScheduler`]
/// by queuing the appropriate task on the relevant event-loop.
fn handle(&self, event: TimerEvent) {
let context = self.context.clone();
// Step 18, queue a task,
// https://html.spec.whatwg.org/multipage/#timer-initialisation-steps
let _ = self.task_source.queue(
task!(timer_event: move || {
let global = context.root();
let TimerEvent(source, id) = event;
match source {
TimerSource::FromWorker => {
global.downcast::<WorkerGlobalScope>().expect("Window timer delivered to worker");
},
TimerSource::FromWindow(pipeline) => {
assert_eq!(pipeline, global.pipeline_id());
global.downcast::<Window>().expect("Worker timer delivered to window");
},
};
// Step 7, substeps run in a task.
global.fire_timer(id, CanGc::note());
})
);
}
pub fn into_callback(self) -> BoxedTimerCallback {
Box::new(move |timer_event| self.handle(timer_event))
}
}
impl MessageListener { impl MessageListener {
/// A new message came in, handle it via a task enqueued on the event-loop. /// A new message came in, handle it via a task enqueued on the event-loop.
/// A task is required, since we are using a trusted globalscope, /// A task is required, since we are using a trusted globalscope,
@ -825,15 +787,7 @@ impl GlobalScope {
} }
fn timers(&self) -> &OneshotTimers { fn timers(&self) -> &OneshotTimers {
self.timers.get_or_init(|| { self.timers.get_or_init(|| OneshotTimers::new(self))
OneshotTimers::new(
self,
TimerListener {
context: Trusted::new(self),
task_source: self.task_manager().timer_task_source(),
},
)
})
} }
/// <https://w3c.github.io/ServiceWorker/#get-the-service-worker-registration-object> /// <https://w3c.github.io/ServiceWorker/#get-the-service-worker-registration-object>

View file

@ -14,23 +14,27 @@ use js::jsapi::Heap;
use js::jsval::{JSVal, UndefinedValue}; use js::jsval::{JSVal, UndefinedValue};
use js::rust::HandleValue; use js::rust::HandleValue;
use servo_config::pref; use servo_config::pref;
use timers::{TimerEventId, TimerEventRequest, TimerSource}; use timers::{BoxedTimerCallback, TimerEvent, TimerEventId, TimerEventRequest, TimerSource};
use crate::dom::bindings::callback::ExceptionHandling::Report; use crate::dom::bindings::callback::ExceptionHandling::Report;
use crate::dom::bindings::cell::DomRefCell; use crate::dom::bindings::cell::DomRefCell;
use crate::dom::bindings::codegen::Bindings::FunctionBinding::Function; use crate::dom::bindings::codegen::Bindings::FunctionBinding::Function;
use crate::dom::bindings::inheritance::Castable;
use crate::dom::bindings::refcounted::Trusted;
use crate::dom::bindings::reflector::DomObject; use crate::dom::bindings::reflector::DomObject;
use crate::dom::bindings::root::DomRoot; use crate::dom::bindings::root::DomRoot;
use crate::dom::bindings::str::DOMString; use crate::dom::bindings::str::DOMString;
use crate::dom::document::FakeRequestAnimationFrameCallback; use crate::dom::document::FakeRequestAnimationFrameCallback;
use crate::dom::eventsource::EventSourceTimeoutCallback; use crate::dom::eventsource::EventSourceTimeoutCallback;
use crate::dom::globalscope::{GlobalScope, TimerListener}; use crate::dom::globalscope::GlobalScope;
use crate::dom::htmlmetaelement::RefreshRedirectDue; use crate::dom::htmlmetaelement::RefreshRedirectDue;
use crate::dom::testbinding::TestBindingCallback; use crate::dom::testbinding::TestBindingCallback;
use crate::dom::types::{Window, WorkerGlobalScope};
use crate::dom::xmlhttprequest::XHRTimeoutCallback; use crate::dom::xmlhttprequest::XHRTimeoutCallback;
use crate::script_module::ScriptFetchOptions; use crate::script_module::ScriptFetchOptions;
use crate::script_runtime::CanGc; use crate::script_runtime::CanGc;
use crate::script_thread::ScriptThread; use crate::script_thread::ScriptThread;
use crate::task_source::TaskSource;
#[derive(Clone, Copy, Debug, Eq, Hash, JSTraceable, MallocSizeOf, Ord, PartialEq, PartialOrd)] #[derive(Clone, Copy, Debug, Eq, Hash, JSTraceable, MallocSizeOf, Ord, PartialEq, PartialOrd)]
pub struct OneshotTimerHandle(i32); pub struct OneshotTimerHandle(i32);
@ -38,9 +42,6 @@ pub struct OneshotTimerHandle(i32);
#[derive(DenyPublicFields, JSTraceable, MallocSizeOf)] #[derive(DenyPublicFields, JSTraceable, MallocSizeOf)]
pub struct OneshotTimers { pub struct OneshotTimers {
global_scope: DomRoot<GlobalScope>, global_scope: DomRoot<GlobalScope>,
#[ignore_malloc_size_of = "Missing malloc_size_of for task types"]
#[no_trace]
timer_listener: TimerListener,
js_timers: JsTimers, js_timers: JsTimers,
next_timer_handle: Cell<OneshotTimerHandle>, next_timer_handle: Cell<OneshotTimerHandle>,
timers: DomRefCell<Vec<OneshotTimer>>, timers: DomRefCell<Vec<OneshotTimer>>,
@ -118,10 +119,9 @@ impl PartialEq for OneshotTimer {
} }
impl OneshotTimers { impl OneshotTimers {
pub fn new(global_scope: &GlobalScope, timer_listener: TimerListener) -> OneshotTimers { pub fn new(global_scope: &GlobalScope) -> OneshotTimers {
OneshotTimers { OneshotTimers {
global_scope: DomRoot::from_ref(global_scope), global_scope: DomRoot::from_ref(global_scope),
timer_listener,
js_timers: JsTimers::default(), js_timers: JsTimers::default(),
next_timer_handle: Cell::new(OneshotTimerHandle(1)), next_timer_handle: Cell::new(OneshotTimerHandle(1)),
timers: DomRefCell::new(Vec::new()), timers: DomRefCell::new(Vec::new()),
@ -283,9 +283,15 @@ impl OneshotTimers {
return; return;
}; };
let callback = TimerListener {
context: Trusted::new(&*self.global_scope),
task_source: self.global_scope.task_manager().timer_task_source(),
}
.into_callback();
let expected_event_id = self.invalidate_expected_event_id(); let expected_event_id = self.invalidate_expected_event_id();
let event_request = TimerEventRequest { let event_request = TimerEventRequest {
callback: self.timer_listener.clone().into_callback(), callback,
source: timer.source, source: timer.source,
id: expected_event_id, id: expected_event_id,
duration: timer.scheduled_for - Instant::now(), duration: timer.scheduled_for - Instant::now(),
@ -574,3 +580,40 @@ impl JsTimerTask {
.collect() .collect()
} }
} }
/// A wrapper between timer events coming in over IPC, and the event-loop.
#[derive(Clone)]
struct TimerListener {
task_source: TaskSource,
context: Trusted<GlobalScope>,
}
impl TimerListener {
/// Handle a timer-event coming from the [`timers::TimerScheduler`]
/// by queuing the appropriate task on the relevant event-loop.
fn handle(&self, event: TimerEvent) {
let context = self.context.clone();
// Step 18, queue a task,
// https://html.spec.whatwg.org/multipage/#timer-initialisation-steps
let _ = self.task_source.queue(task!(timer_event: move || {
let global = context.root();
let TimerEvent(source, id) = event;
match source {
TimerSource::FromWorker => {
global.downcast::<WorkerGlobalScope>().expect("Window timer delivered to worker");
},
TimerSource::FromWindow(pipeline) => {
assert_eq!(pipeline, global.pipeline_id());
global.downcast::<Window>().expect("Worker timer delivered to window");
},
};
// Step 7, substeps run in a task.
global.fire_timer(id, CanGc::note());
})
);
}
fn into_callback(self) -> BoxedTimerCallback {
Box::new(move |timer_event| self.handle(timer_event))
}
}