mirror of
https://github.com/servo/servo.git
synced 2025-06-17 12:54:28 +00:00
This change has two parts which depend on each other: 1. An early exit in the layout process, which allows for skipping display list construction entirely when nothing would change. 2. A simplification and unification of the way that "fake" animation frames are triggered. Now this happens on an entire ScriptThread at once and is based on whether or not any Pipeline triggered a display list update. Animations are never canceled in the compositor when the Pipeline isn't updating, instead the fake animation frame is triggered far enough in the future that an unexpected compositor tick will cancel it. This could happen, for instance, if some other Pipeline in some other ScriptThread produced a new display list for a tick. This makes everything simpler about these ticks. The goal is that in a future change the ScriptThread-based animation ticks will be made more generic so that they can throttle the number of "update the rendering" calls triggered by script. This should make Servo do a lot less work when moving the cursor over a page. Before it would constantly produce new display lists. Fixes: #17029. Testing: This should not cause any web observable changes. The fact that all WPT tests keep passing is the test for this change. Signed-off-by: Martin Robinson <mrobinson@igalia.com> Co-authored-by: Oriol Brufau <obrufau@igalia.com>
649 lines
23 KiB
Rust
649 lines
23 KiB
Rust
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
|
|
|
use std::cell::Cell;
|
|
use std::cmp::{Ord, Ordering};
|
|
use std::collections::{HashMap, VecDeque};
|
|
use std::default::Default;
|
|
use std::rc::Rc;
|
|
use std::time::{Duration, Instant};
|
|
|
|
use base::id::PipelineId;
|
|
use deny_public_fields::DenyPublicFields;
|
|
use js::jsapi::Heap;
|
|
use js::jsval::{JSVal, UndefinedValue};
|
|
use js::rust::HandleValue;
|
|
use serde::{Deserialize, Serialize};
|
|
use servo_config::pref;
|
|
use timers::{BoxedTimerCallback, TimerEventRequest};
|
|
|
|
use crate::dom::bindings::callback::ExceptionHandling::Report;
|
|
use crate::dom::bindings::cell::DomRefCell;
|
|
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::{DomGlobal, DomObject};
|
|
use crate::dom::bindings::root::Dom;
|
|
use crate::dom::bindings::str::DOMString;
|
|
use crate::dom::document::{ImageAnimationUpdateCallback, RefreshRedirectDue};
|
|
use crate::dom::eventsource::EventSourceTimeoutCallback;
|
|
use crate::dom::globalscope::GlobalScope;
|
|
#[cfg(feature = "testbinding")]
|
|
use crate::dom::testbinding::TestBindingCallback;
|
|
use crate::dom::types::{Window, WorkerGlobalScope};
|
|
use crate::dom::xmlhttprequest::XHRTimeoutCallback;
|
|
use crate::script_module::ScriptFetchOptions;
|
|
use crate::script_runtime::CanGc;
|
|
use crate::script_thread::ScriptThread;
|
|
use crate::task_source::SendableTaskSource;
|
|
|
|
#[derive(Clone, Copy, Debug, Eq, Hash, JSTraceable, MallocSizeOf, Ord, PartialEq, PartialOrd)]
|
|
pub(crate) struct OneshotTimerHandle(i32);
|
|
|
|
#[derive(DenyPublicFields, JSTraceable, MallocSizeOf)]
|
|
#[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)]
|
|
pub(crate) struct OneshotTimers {
|
|
global_scope: Dom<GlobalScope>,
|
|
js_timers: JsTimers,
|
|
next_timer_handle: Cell<OneshotTimerHandle>,
|
|
timers: DomRefCell<VecDeque<OneshotTimer>>,
|
|
suspended_since: Cell<Option<Instant>>,
|
|
/// Initially 0, increased whenever the associated document is reactivated
|
|
/// by the amount of ms the document was inactive. The current time can be
|
|
/// offset back by this amount for a coherent time across document
|
|
/// activations.
|
|
suspension_offset: Cell<Duration>,
|
|
/// Calls to `fire_timer` with a different argument than this get ignored.
|
|
/// They were previously scheduled and got invalidated when
|
|
/// - timers were suspended,
|
|
/// - the timer it was scheduled for got canceled or
|
|
/// - a timer was added with an earlier callback time. In this case the
|
|
/// original timer is rescheduled when it is the next one to get called.
|
|
#[no_trace]
|
|
expected_event_id: Cell<TimerEventId>,
|
|
}
|
|
|
|
#[derive(DenyPublicFields, JSTraceable, MallocSizeOf)]
|
|
struct OneshotTimer {
|
|
handle: OneshotTimerHandle,
|
|
#[no_trace]
|
|
source: TimerSource,
|
|
callback: OneshotTimerCallback,
|
|
scheduled_for: Instant,
|
|
}
|
|
|
|
// This enum is required to work around the fact that trait objects do not support generic methods.
|
|
// A replacement trait would have a method such as
|
|
// `invoke<T: DomObject>(self: Box<Self>, this: &T, js_timers: &JsTimers);`.
|
|
#[derive(JSTraceable, MallocSizeOf)]
|
|
pub(crate) enum OneshotTimerCallback {
|
|
XhrTimeout(XHRTimeoutCallback),
|
|
EventSourceTimeout(EventSourceTimeoutCallback),
|
|
JsTimer(JsTimerTask),
|
|
#[cfg(feature = "testbinding")]
|
|
TestBindingCallback(TestBindingCallback),
|
|
RefreshRedirectDue(RefreshRedirectDue),
|
|
ImageAnimationUpdate(ImageAnimationUpdateCallback),
|
|
}
|
|
|
|
impl OneshotTimerCallback {
|
|
fn invoke<T: DomObject>(self, this: &T, js_timers: &JsTimers, can_gc: CanGc) {
|
|
match self {
|
|
OneshotTimerCallback::XhrTimeout(callback) => callback.invoke(can_gc),
|
|
OneshotTimerCallback::EventSourceTimeout(callback) => callback.invoke(),
|
|
OneshotTimerCallback::JsTimer(task) => task.invoke(this, js_timers, can_gc),
|
|
#[cfg(feature = "testbinding")]
|
|
OneshotTimerCallback::TestBindingCallback(callback) => callback.invoke(),
|
|
OneshotTimerCallback::RefreshRedirectDue(callback) => callback.invoke(can_gc),
|
|
OneshotTimerCallback::ImageAnimationUpdate(callback) => callback.invoke(can_gc),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Ord for OneshotTimer {
|
|
fn cmp(&self, other: &OneshotTimer) -> Ordering {
|
|
match self.scheduled_for.cmp(&other.scheduled_for).reverse() {
|
|
Ordering::Equal => self.handle.cmp(&other.handle).reverse(),
|
|
res => res,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl PartialOrd for OneshotTimer {
|
|
fn partial_cmp(&self, other: &OneshotTimer) -> Option<Ordering> {
|
|
Some(self.cmp(other))
|
|
}
|
|
}
|
|
|
|
impl Eq for OneshotTimer {}
|
|
impl PartialEq for OneshotTimer {
|
|
fn eq(&self, other: &OneshotTimer) -> bool {
|
|
std::ptr::eq(self, other)
|
|
}
|
|
}
|
|
|
|
impl OneshotTimers {
|
|
pub(crate) fn new(global_scope: &GlobalScope) -> OneshotTimers {
|
|
OneshotTimers {
|
|
global_scope: Dom::from_ref(global_scope),
|
|
js_timers: JsTimers::default(),
|
|
next_timer_handle: Cell::new(OneshotTimerHandle(1)),
|
|
timers: DomRefCell::new(VecDeque::new()),
|
|
suspended_since: Cell::new(None),
|
|
suspension_offset: Cell::new(Duration::ZERO),
|
|
expected_event_id: Cell::new(TimerEventId(0)),
|
|
}
|
|
}
|
|
|
|
pub(crate) fn schedule_callback(
|
|
&self,
|
|
callback: OneshotTimerCallback,
|
|
duration: Duration,
|
|
source: TimerSource,
|
|
) -> OneshotTimerHandle {
|
|
let new_handle = self.next_timer_handle.get();
|
|
self.next_timer_handle
|
|
.set(OneshotTimerHandle(new_handle.0 + 1));
|
|
|
|
let timer = OneshotTimer {
|
|
handle: new_handle,
|
|
source,
|
|
callback,
|
|
scheduled_for: self.base_time() + duration,
|
|
};
|
|
|
|
{
|
|
let mut timers = self.timers.borrow_mut();
|
|
let insertion_index = timers.binary_search(&timer).err().unwrap();
|
|
timers.insert(insertion_index, timer);
|
|
}
|
|
|
|
if self.is_next_timer(new_handle) {
|
|
self.schedule_timer_call();
|
|
}
|
|
|
|
new_handle
|
|
}
|
|
|
|
pub(crate) fn unschedule_callback(&self, handle: OneshotTimerHandle) {
|
|
let was_next = self.is_next_timer(handle);
|
|
|
|
self.timers.borrow_mut().retain(|t| t.handle != handle);
|
|
|
|
if was_next {
|
|
self.invalidate_expected_event_id();
|
|
self.schedule_timer_call();
|
|
}
|
|
}
|
|
|
|
fn is_next_timer(&self, handle: OneshotTimerHandle) -> bool {
|
|
match self.timers.borrow().back() {
|
|
None => false,
|
|
Some(max_timer) => max_timer.handle == handle,
|
|
}
|
|
}
|
|
|
|
pub(crate) fn fire_timer(&self, id: TimerEventId, global: &GlobalScope, can_gc: CanGc) {
|
|
let expected_id = self.expected_event_id.get();
|
|
if expected_id != id {
|
|
debug!(
|
|
"ignoring timer fire event {:?} (expected {:?})",
|
|
id, expected_id
|
|
);
|
|
return;
|
|
}
|
|
|
|
assert!(self.suspended_since.get().is_none());
|
|
|
|
let base_time = self.base_time();
|
|
|
|
// Since the event id was the expected one, at least one timer should be due.
|
|
if base_time < self.timers.borrow().back().unwrap().scheduled_for {
|
|
warn!("Unexpected timing!");
|
|
return;
|
|
}
|
|
|
|
// select timers to run to prevent firing timers
|
|
// that were installed during fire of another timer
|
|
let mut timers_to_run = Vec::new();
|
|
|
|
loop {
|
|
let mut timers = self.timers.borrow_mut();
|
|
|
|
if timers.is_empty() || timers.back().unwrap().scheduled_for > base_time {
|
|
break;
|
|
}
|
|
|
|
timers_to_run.push(timers.pop_back().unwrap());
|
|
}
|
|
|
|
for timer in timers_to_run {
|
|
// Since timers can be coalesced together inside a task,
|
|
// this loop can keep running, including after an interrupt of the JS,
|
|
// and prevent a clean-shutdown of a JS-running thread.
|
|
// This check prevents such a situation.
|
|
if !global.can_continue_running() {
|
|
return;
|
|
}
|
|
let callback = timer.callback;
|
|
callback.invoke(global, &self.js_timers, can_gc);
|
|
}
|
|
|
|
self.schedule_timer_call();
|
|
}
|
|
|
|
fn base_time(&self) -> Instant {
|
|
let offset = self.suspension_offset.get();
|
|
match self.suspended_since.get() {
|
|
Some(suspend_time) => suspend_time - offset,
|
|
None => Instant::now() - offset,
|
|
}
|
|
}
|
|
|
|
pub(crate) fn slow_down(&self) {
|
|
let min_duration_ms = pref!(js_timers_minimum_duration) as u64;
|
|
self.js_timers
|
|
.set_min_duration(Duration::from_millis(min_duration_ms));
|
|
}
|
|
|
|
pub(crate) fn speed_up(&self) {
|
|
self.js_timers.remove_min_duration();
|
|
}
|
|
|
|
pub(crate) fn suspend(&self) {
|
|
// Suspend is idempotent: do nothing if the timers are already suspended.
|
|
if self.suspended_since.get().is_some() {
|
|
return warn!("Suspending an already suspended timer.");
|
|
}
|
|
|
|
debug!("Suspending timers.");
|
|
self.suspended_since.set(Some(Instant::now()));
|
|
self.invalidate_expected_event_id();
|
|
}
|
|
|
|
pub(crate) fn resume(&self) {
|
|
// Resume is idempotent: do nothing if the timers are already resumed.
|
|
let additional_offset = match self.suspended_since.get() {
|
|
Some(suspended_since) => Instant::now() - suspended_since,
|
|
None => return warn!("Resuming an already resumed timer."),
|
|
};
|
|
|
|
debug!("Resuming timers.");
|
|
self.suspension_offset
|
|
.set(self.suspension_offset.get() + additional_offset);
|
|
self.suspended_since.set(None);
|
|
|
|
self.schedule_timer_call();
|
|
}
|
|
|
|
fn schedule_timer_call(&self) {
|
|
if self.suspended_since.get().is_some() {
|
|
// The timer will be scheduled when the pipeline is fully activated.
|
|
return;
|
|
}
|
|
|
|
let timers = self.timers.borrow();
|
|
let Some(timer) = timers.back() else {
|
|
return;
|
|
};
|
|
|
|
let expected_event_id = self.invalidate_expected_event_id();
|
|
let callback = TimerListener {
|
|
context: Trusted::new(&*self.global_scope),
|
|
task_source: self
|
|
.global_scope
|
|
.task_manager()
|
|
.timer_task_source()
|
|
.to_sendable(),
|
|
source: timer.source,
|
|
id: expected_event_id,
|
|
}
|
|
.into_callback();
|
|
|
|
let event_request = TimerEventRequest {
|
|
callback,
|
|
duration: timer.scheduled_for - Instant::now(),
|
|
};
|
|
|
|
self.global_scope.schedule_timer(event_request);
|
|
}
|
|
|
|
fn invalidate_expected_event_id(&self) -> TimerEventId {
|
|
let TimerEventId(currently_expected) = self.expected_event_id.get();
|
|
let next_id = TimerEventId(currently_expected + 1);
|
|
debug!(
|
|
"invalidating expected timer (was {:?}, now {:?}",
|
|
currently_expected, next_id
|
|
);
|
|
self.expected_event_id.set(next_id);
|
|
next_id
|
|
}
|
|
|
|
pub(crate) fn set_timeout_or_interval(
|
|
&self,
|
|
global: &GlobalScope,
|
|
callback: TimerCallback,
|
|
arguments: Vec<HandleValue>,
|
|
timeout: Duration,
|
|
is_interval: IsInterval,
|
|
source: TimerSource,
|
|
) -> i32 {
|
|
self.js_timers.set_timeout_or_interval(
|
|
global,
|
|
callback,
|
|
arguments,
|
|
timeout,
|
|
is_interval,
|
|
source,
|
|
)
|
|
}
|
|
|
|
pub(crate) fn clear_timeout_or_interval(&self, global: &GlobalScope, handle: i32) {
|
|
self.js_timers.clear_timeout_or_interval(global, handle)
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Copy, Eq, Hash, JSTraceable, MallocSizeOf, Ord, PartialEq, PartialOrd)]
|
|
pub(crate) struct JsTimerHandle(i32);
|
|
|
|
#[derive(DenyPublicFields, JSTraceable, MallocSizeOf)]
|
|
pub(crate) struct JsTimers {
|
|
next_timer_handle: Cell<JsTimerHandle>,
|
|
/// <https://html.spec.whatwg.org/multipage/#list-of-active-timers>
|
|
active_timers: DomRefCell<HashMap<JsTimerHandle, JsTimerEntry>>,
|
|
/// The nesting level of the currently executing timer task or 0.
|
|
nesting_level: Cell<u32>,
|
|
/// Used to introduce a minimum delay in event intervals
|
|
min_duration: Cell<Option<Duration>>,
|
|
}
|
|
|
|
#[derive(JSTraceable, MallocSizeOf)]
|
|
struct JsTimerEntry {
|
|
oneshot_handle: OneshotTimerHandle,
|
|
}
|
|
|
|
// Holder for the various JS values associated with setTimeout
|
|
// (ie. function value to invoke and all arguments to pass
|
|
// to the function when calling it)
|
|
// TODO: Handle rooting during invocation when movable GC is turned on
|
|
#[derive(JSTraceable, MallocSizeOf)]
|
|
pub(crate) struct JsTimerTask {
|
|
#[ignore_malloc_size_of = "Because it is non-owning"]
|
|
handle: JsTimerHandle,
|
|
#[no_trace]
|
|
source: TimerSource,
|
|
callback: InternalTimerCallback,
|
|
is_interval: IsInterval,
|
|
nesting_level: u32,
|
|
duration: Duration,
|
|
is_user_interacting: bool,
|
|
}
|
|
|
|
// Enum allowing more descriptive values for the is_interval field
|
|
#[derive(Clone, Copy, JSTraceable, MallocSizeOf, PartialEq)]
|
|
pub(crate) enum IsInterval {
|
|
Interval,
|
|
NonInterval,
|
|
}
|
|
|
|
#[derive(Clone)]
|
|
pub(crate) enum TimerCallback {
|
|
StringTimerCallback(DOMString),
|
|
FunctionTimerCallback(Rc<Function>),
|
|
}
|
|
|
|
#[derive(Clone, JSTraceable, MallocSizeOf)]
|
|
enum InternalTimerCallback {
|
|
StringTimerCallback(DOMString),
|
|
FunctionTimerCallback(
|
|
#[ignore_malloc_size_of = "Rc"] Rc<Function>,
|
|
#[ignore_malloc_size_of = "Rc"] Rc<Box<[Heap<JSVal>]>>,
|
|
),
|
|
}
|
|
|
|
impl Default for JsTimers {
|
|
fn default() -> Self {
|
|
JsTimers {
|
|
next_timer_handle: Cell::new(JsTimerHandle(1)),
|
|
active_timers: DomRefCell::new(HashMap::new()),
|
|
nesting_level: Cell::new(0),
|
|
min_duration: Cell::new(None),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl JsTimers {
|
|
// see https://html.spec.whatwg.org/multipage/#timer-initialisation-steps
|
|
pub(crate) fn set_timeout_or_interval(
|
|
&self,
|
|
global: &GlobalScope,
|
|
callback: TimerCallback,
|
|
arguments: Vec<HandleValue>,
|
|
timeout: Duration,
|
|
is_interval: IsInterval,
|
|
source: TimerSource,
|
|
) -> i32 {
|
|
let callback = match callback {
|
|
TimerCallback::StringTimerCallback(code_str) => {
|
|
if global.is_js_evaluation_allowed(code_str.as_ref()) {
|
|
InternalTimerCallback::StringTimerCallback(code_str)
|
|
} else {
|
|
return 0;
|
|
}
|
|
},
|
|
TimerCallback::FunctionTimerCallback(function) => {
|
|
// This is a bit complicated, but this ensures that the vector's
|
|
// buffer isn't reallocated (and moved) after setting the Heap values
|
|
let mut args = Vec::with_capacity(arguments.len());
|
|
for _ in 0..arguments.len() {
|
|
args.push(Heap::default());
|
|
}
|
|
for (i, item) in arguments.iter().enumerate() {
|
|
args.get_mut(i).unwrap().set(item.get());
|
|
}
|
|
InternalTimerCallback::FunctionTimerCallback(
|
|
function,
|
|
Rc::new(args.into_boxed_slice()),
|
|
)
|
|
},
|
|
};
|
|
|
|
// step 2
|
|
let JsTimerHandle(new_handle) = self.next_timer_handle.get();
|
|
self.next_timer_handle.set(JsTimerHandle(new_handle + 1));
|
|
|
|
// step 3 as part of initialize_and_schedule below
|
|
|
|
// step 4
|
|
let mut task = JsTimerTask {
|
|
handle: JsTimerHandle(new_handle),
|
|
source,
|
|
callback,
|
|
is_interval,
|
|
is_user_interacting: ScriptThread::is_user_interacting(),
|
|
nesting_level: 0,
|
|
duration: Duration::ZERO,
|
|
};
|
|
|
|
// step 5
|
|
task.duration = timeout.max(Duration::ZERO);
|
|
|
|
// step 3, 6-9, 11-14
|
|
self.initialize_and_schedule(global, task);
|
|
|
|
// step 10
|
|
new_handle
|
|
}
|
|
|
|
pub(crate) fn clear_timeout_or_interval(&self, global: &GlobalScope, handle: i32) {
|
|
let mut active_timers = self.active_timers.borrow_mut();
|
|
|
|
if let Some(entry) = active_timers.remove(&JsTimerHandle(handle)) {
|
|
global.unschedule_callback(entry.oneshot_handle);
|
|
}
|
|
}
|
|
|
|
pub(crate) fn set_min_duration(&self, duration: Duration) {
|
|
self.min_duration.set(Some(duration));
|
|
}
|
|
|
|
pub(crate) fn remove_min_duration(&self) {
|
|
self.min_duration.set(None);
|
|
}
|
|
|
|
// see step 13 of https://html.spec.whatwg.org/multipage/#timer-initialisation-steps
|
|
fn user_agent_pad(&self, current_duration: Duration) -> Duration {
|
|
match self.min_duration.get() {
|
|
Some(min_duration) => min_duration.max(current_duration),
|
|
None => current_duration,
|
|
}
|
|
}
|
|
|
|
// see https://html.spec.whatwg.org/multipage/#timer-initialisation-steps
|
|
fn initialize_and_schedule(&self, global: &GlobalScope, mut task: JsTimerTask) {
|
|
let handle = task.handle;
|
|
let mut active_timers = self.active_timers.borrow_mut();
|
|
|
|
// step 6
|
|
let nesting_level = self.nesting_level.get();
|
|
|
|
// step 7, 13
|
|
let duration = self.user_agent_pad(clamp_duration(nesting_level, task.duration));
|
|
// step 8, 9
|
|
task.nesting_level = nesting_level + 1;
|
|
|
|
// essentially step 11, 12, and 14
|
|
let callback = OneshotTimerCallback::JsTimer(task);
|
|
let oneshot_handle = global.schedule_callback(callback, duration);
|
|
|
|
// step 3
|
|
let entry = active_timers
|
|
.entry(handle)
|
|
.or_insert(JsTimerEntry { oneshot_handle });
|
|
entry.oneshot_handle = oneshot_handle;
|
|
}
|
|
}
|
|
|
|
// see step 7 of https://html.spec.whatwg.org/multipage/#timer-initialisation-steps
|
|
fn clamp_duration(nesting_level: u32, unclamped: Duration) -> Duration {
|
|
let lower_bound_ms = if nesting_level > 5 { 4 } else { 0 };
|
|
let lower_bound = Duration::from_millis(lower_bound_ms);
|
|
lower_bound.max(unclamped)
|
|
}
|
|
|
|
impl JsTimerTask {
|
|
// see https://html.spec.whatwg.org/multipage/#timer-initialisation-steps
|
|
pub(crate) fn invoke<T: DomObject>(self, this: &T, timers: &JsTimers, can_gc: CanGc) {
|
|
// step 4.1 can be ignored, because we proactively prevent execution
|
|
// of this task when its scheduled execution is canceled.
|
|
|
|
// prep for step 6 in nested set_timeout_or_interval calls
|
|
timers.nesting_level.set(self.nesting_level);
|
|
|
|
// step 4.2
|
|
let was_user_interacting = ScriptThread::is_user_interacting();
|
|
ScriptThread::set_user_interacting(self.is_user_interacting);
|
|
match self.callback {
|
|
InternalTimerCallback::StringTimerCallback(ref code_str) => {
|
|
let global = this.global();
|
|
let cx = GlobalScope::get_cx();
|
|
rooted!(in(*cx) let mut rval = UndefinedValue());
|
|
// FIXME(cybai): Use base url properly by saving private reference for timers (#27260)
|
|
global.evaluate_js_on_global_with_result(
|
|
code_str,
|
|
rval.handle_mut(),
|
|
ScriptFetchOptions::default_classic_script(&global),
|
|
global.api_base_url(),
|
|
can_gc,
|
|
);
|
|
},
|
|
InternalTimerCallback::FunctionTimerCallback(ref function, ref arguments) => {
|
|
let arguments = self.collect_heap_args(arguments);
|
|
rooted!(in(*GlobalScope::get_cx()) let mut value: JSVal);
|
|
let _ = function.Call_(this, arguments, value.handle_mut(), Report, can_gc);
|
|
},
|
|
};
|
|
ScriptThread::set_user_interacting(was_user_interacting);
|
|
|
|
// reset nesting level (see above)
|
|
timers.nesting_level.set(0);
|
|
|
|
// step 4.3
|
|
// Since we choose proactively prevent execution (see 4.1 above), we must only
|
|
// reschedule repeating timers when they were not canceled as part of step 4.2.
|
|
if self.is_interval == IsInterval::Interval &&
|
|
timers.active_timers.borrow().contains_key(&self.handle)
|
|
{
|
|
timers.initialize_and_schedule(&this.global(), self);
|
|
}
|
|
}
|
|
|
|
// Returning Handles directly from Heap values is inherently unsafe, but here it's
|
|
// always done via rooted JsTimers, which is safe.
|
|
#[allow(unsafe_code)]
|
|
fn collect_heap_args<'b>(&self, args: &'b [Heap<JSVal>]) -> Vec<HandleValue<'b>> {
|
|
args.iter()
|
|
.map(|arg| unsafe { HandleValue::from_raw(arg.handle()) })
|
|
.collect()
|
|
}
|
|
}
|
|
|
|
/// Describes the source that requested the [`TimerEvent`].
|
|
#[derive(Clone, Copy, Debug, Deserialize, MallocSizeOf, Serialize)]
|
|
pub enum TimerSource {
|
|
/// The event was requested from a window (`ScriptThread`).
|
|
FromWindow(PipelineId),
|
|
/// The event was requested from a worker (`DedicatedGlobalWorkerScope`).
|
|
FromWorker,
|
|
}
|
|
|
|
/// The id to be used for a [`TimerEvent`] is defined by the corresponding [`TimerEventRequest`].
|
|
#[derive(Clone, Copy, Debug, Deserialize, Eq, MallocSizeOf, PartialEq, Serialize)]
|
|
pub struct TimerEventId(pub u32);
|
|
|
|
/// A notification that a timer has fired. [`TimerSource`] must be `FromWindow` when
|
|
/// dispatched to `ScriptThread` and must be `FromWorker` when dispatched to a
|
|
/// `DedicatedGlobalWorkerScope`
|
|
#[derive(Clone, Copy, Debug, Deserialize, Serialize)]
|
|
pub struct TimerEvent(pub TimerSource, pub TimerEventId);
|
|
|
|
/// A wrapper between timer events coming in over IPC, and the event-loop.
|
|
#[derive(Clone)]
|
|
struct TimerListener {
|
|
task_source: SendableTaskSource,
|
|
context: Trusted<GlobalScope>,
|
|
source: TimerSource,
|
|
id: TimerEventId,
|
|
}
|
|
|
|
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
|
|
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 {
|
|
let timer_event = TimerEvent(self.source, self.id);
|
|
Box::new(move || self.handle(timer_event))
|
|
}
|
|
}
|