/* 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::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 rustc_hash::FxHashMap; 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::codegen::UnionTypes::TrustedScriptOrString; use crate::dom::bindings::error::Fallible; use crate::dom::bindings::inheritance::Castable; use crate::dom::bindings::refcounted::Trusted; use crate::dom::bindings::reflector::{DomGlobal, DomObject}; use crate::dom::bindings::root::{AsHandleValue, Dom}; use crate::dom::bindings::str::DOMString; use crate::dom::csp::CspReporting; use crate::dom::document::RefreshRedirectDue; use crate::dom::eventsource::EventSourceTimeoutCallback; use crate::dom::globalscope::GlobalScope; #[cfg(feature = "testbinding")] use crate::dom::testbinding::TestBindingCallback; use crate::dom::trustedscript::TrustedScript; use crate::dom::types::{Window, WorkerGlobalScope}; use crate::dom::xmlhttprequest::XHRTimeoutCallback; use crate::script_module::ScriptFetchOptions; use crate::script_runtime::{CanGc, IntroductionType}; 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, js_timers: JsTimers, next_timer_handle: Cell, timers: DomRefCell>, suspended_since: Cell>, /// 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, /// 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, } #[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(self: Box, 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), } impl OneshotTimerCallback { fn invoke(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), } } } 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 { 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) { // Step 9.2. If id does not exist in global's map of setTimeout and setInterval IDs, then abort these steps. 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(); // Step 12. Let completionStep be an algorithm step which queues a global // task on the timer task source given global to run task. 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 } #[allow(clippy::too_many_arguments)] pub(crate) fn set_timeout_or_interval( &self, global: &GlobalScope, callback: TimerCallback, arguments: Vec, timeout: Duration, is_interval: IsInterval, source: TimerSource, can_gc: CanGc, ) -> Fallible { self.js_timers.set_timeout_or_interval( global, callback, arguments, timeout, is_interval, source, can_gc, ) } 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, /// active_timers: DomRefCell>, /// The nesting level of the currently executing timer task or 0. nesting_level: Cell, /// Used to introduce a minimum delay in event intervals min_duration: Cell>, } #[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 { 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, } pub(crate) enum TimerCallback { StringTimerCallback(TrustedScriptOrString), FunctionTimerCallback(Rc), } #[derive(Clone, JSTraceable, MallocSizeOf)] #[cfg_attr(crown, allow(crown::unrooted_must_root))] enum InternalTimerCallback { StringTimerCallback(DOMString), FunctionTimerCallback( #[conditional_malloc_size_of] Rc, #[ignore_malloc_size_of = "Rc"] Rc]>>, ), } impl Default for JsTimers { fn default() -> Self { JsTimers { next_timer_handle: Cell::new(JsTimerHandle(1)), active_timers: DomRefCell::new(FxHashMap::default()), nesting_level: Cell::new(0), min_duration: Cell::new(None), } } } impl JsTimers { /// #[allow(clippy::too_many_arguments)] #[cfg_attr(crown, allow(crown::unrooted_must_root))] pub(crate) fn set_timeout_or_interval( &self, global: &GlobalScope, callback: TimerCallback, arguments: Vec, timeout: Duration, is_interval: IsInterval, source: TimerSource, can_gc: CanGc, ) -> Fallible { let callback = match callback { TimerCallback::StringTimerCallback(trusted_script_or_string) => { // Step 9.6.1.1. Let globalName be "Window" if global is a Window object; "WorkerGlobalScope" otherwise. let global_name = if global.is::() { "Window" } else { "WorkerGlobalScope" }; // Step 9.6.1.2. Let methodName be "setInterval" if repeat is true; "setTimeout" otherwise. let method_name = if is_interval == IsInterval::Interval { "setInterval" } else { "setTimeout" }; // Step 9.6.1.3. Let sink be a concatenation of globalName, U+0020 SPACE, and methodName. let sink = format!("{} {}", global_name, method_name); // Step 9.6.1.4. Set handler to the result of invoking the // Get Trusted Type compliant string algorithm with TrustedScript, global, handler, sink, and "script". let code_str = TrustedScript::get_trusted_script_compliant_string( global, trusted_script_or_string, &sink, can_gc, )?; // Step 9.6.3. Perform EnsureCSPDoesNotBlockStringCompilation(realm, « », handler, handler, timer, « », handler). // If this throws an exception, catch it, report it for global, and abort these steps. if global .get_csp_list() .is_js_evaluation_allowed(global, code_str.str()) { // Step 9.6.2. Assert: handler is a string. InternalTimerCallback::StringTimerCallback(code_str) } else { return Ok(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()); } // Step 9.5. If handler is a Function, then invoke handler given arguments and "report", // and with callback this value set to thisArg. InternalTimerCallback::FunctionTimerCallback( function, Rc::new(args.into_boxed_slice()), ) }, }; // Step 2. If previousId was given, let id be previousId; otherwise, // let id be an implementation-defined integer that is greater than zero // and does not already exist in global's map of setTimeout and setInterval IDs. let JsTimerHandle(new_handle) = self.next_timer_handle.get(); self.next_timer_handle.set(JsTimerHandle(new_handle + 1)); // Step 3. If the surrounding agent's event loop's currently running task // is a task that was created by this algorithm, then let nesting level // be the task's timer nesting level. Otherwise, let nesting level be 0. 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 4. If timeout is less than 0, then set timeout to 0. task.duration = timeout.max(Duration::ZERO); self.initialize_and_schedule(global, task); // Step 15. Return id. Ok(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, } } /// fn initialize_and_schedule(&self, global: &GlobalScope, mut task: JsTimerTask) { let handle = task.handle; let mut active_timers = self.active_timers.borrow_mut(); // Step 3. If the surrounding agent's event loop's currently running task // is a task that was created by this algorithm, then let nesting level be // the task's timer nesting level. Otherwise, let nesting level be 0. let nesting_level = self.nesting_level.get(); let duration = self.user_agent_pad(clamp_duration(nesting_level, task.duration)); // Step 10. Increment nesting level by one. // Step 11. Set task's timer nesting level to nesting level. task.nesting_level = nesting_level + 1; // Step 13. Set uniqueHandle to the result of running steps after a timeout given global, // "setTimeout/setInterval", timeout, and completionStep. let callback = OneshotTimerCallback::JsTimer(task); let oneshot_handle = global.schedule_callback(callback, duration); // Step 14. Set global's map of setTimeout and setInterval IDs[id] to uniqueHandle. let entry = active_timers .entry(handle) .or_insert(JsTimerEntry { oneshot_handle }); entry.oneshot_handle = oneshot_handle; } } /// Step 5 of fn clamp_duration(nesting_level: u32, unclamped: Duration) -> Duration { // Step 5. If nesting level is greater than 5, and timeout is less than 4, then set timeout to 4. 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(self, this: &T, timers: &JsTimers, can_gc: CanGc) { // step 9.2 can be ignored, because we proactively prevent execution // of this task when its scheduled execution is canceled. // prep for step ? in nested set_timeout_or_interval calls timers.nesting_level.set(self.nesting_level); let _guard = ScriptThread::user_interacting_guard(); match self.callback { InternalTimerCallback::StringTimerCallback(ref code_str) => { // Step 6.4. Let settings object be global's relevant settings object. // Step 6. Let realm be global's relevant realm. let global = this.global(); // Step 7. Let initiating script be the active script. let cx = GlobalScope::get_cx(); // Step 9.6.7. If initiating script is not null, then: rooted!(in(*cx) let mut rval = UndefinedValue()); // Step 9.6.7.1. Set fetch options to a script fetch options whose cryptographic nonce // is initiating script's fetch options's cryptographic nonce, // integrity metadata is the empty string, parser metadata is "not-parser-inserted", // credentials mode is initiating script's fetch options's credentials mode, // referrer policy is initiating script's fetch options's referrer policy, // and fetch priority is "auto". // Step 9.6.8. Let script be the result of creating a classic script given handler, // settings object, base URL, and fetch options. // Step 9.6.9. Run the classic script script. // // FIXME(cybai): Use base url properly by saving private reference for timers (#27260) _ = global.evaluate_js_on_global_with_result( code_str.str(), rval.handle_mut(), ScriptFetchOptions::default_classic_script(&global), // Step 9.6. Let base URL be settings object's API base URL. // Step 9.7.2. Set base URL to initiating script's base URL. global.api_base_url(), can_gc, Some(IntroductionType::DOM_TIMER), ); }, // Step 9.5. If handler is a Function, then invoke handler given arguments and // "report", and with callback this value set to thisArg. 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); }, }; // reset nesting level (see above) timers.nesting_level.set(0); // Step 9.9. If repeat is true, then perform the timer initialization steps again, // given global, handler, timeout, arguments, true, and id. // // 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); } } fn collect_heap_args<'b>(&self, args: &'b [Heap]) -> Vec> { args.iter().map(|arg| arg.as_handle_value()).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, 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 9. Let task be a task that runs the following substeps: self.task_source.queue(task!(timer_event: move || { let global = context.root(); let TimerEvent(source, id) = event; match source { TimerSource::FromWorker => { global.downcast::().expect("Window timer delivered to worker"); }, TimerSource::FromWindow(pipeline) => { assert_eq!(pipeline, global.pipeline_id()); global.downcast::().expect("Worker timer delivered to window"); }, }; 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)) } }