script/
timers.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
5use std::cell::Cell;
6use std::cmp::{Ord, Ordering};
7use std::collections::VecDeque;
8use std::default::Default;
9use std::rc::Rc;
10use std::time::{Duration, Instant};
11
12use base::id::PipelineId;
13use deny_public_fields::DenyPublicFields;
14use js::jsapi::Heap;
15use js::jsval::{JSVal, UndefinedValue};
16use js::rust::HandleValue;
17use rustc_hash::FxHashMap;
18use serde::{Deserialize, Serialize};
19use servo_config::pref;
20use timers::{BoxedTimerCallback, TimerEventRequest};
21
22use crate::dom::bindings::callback::ExceptionHandling::Report;
23use crate::dom::bindings::cell::DomRefCell;
24use crate::dom::bindings::codegen::Bindings::FunctionBinding::Function;
25use crate::dom::bindings::codegen::UnionTypes::TrustedScriptOrString;
26use crate::dom::bindings::error::Fallible;
27use crate::dom::bindings::inheritance::Castable;
28use crate::dom::bindings::refcounted::Trusted;
29use crate::dom::bindings::reflector::{DomGlobal, DomObject};
30use crate::dom::bindings::root::{AsHandleValue, Dom};
31use crate::dom::bindings::str::DOMString;
32use crate::dom::csp::CspReporting;
33use crate::dom::document::RefreshRedirectDue;
34use crate::dom::eventsource::EventSourceTimeoutCallback;
35use crate::dom::globalscope::GlobalScope;
36#[cfg(feature = "testbinding")]
37use crate::dom::testbinding::TestBindingCallback;
38use crate::dom::trustedscript::TrustedScript;
39use crate::dom::types::{Window, WorkerGlobalScope};
40use crate::dom::xmlhttprequest::XHRTimeoutCallback;
41use crate::script_module::ScriptFetchOptions;
42use crate::script_runtime::{CanGc, IntroductionType};
43use crate::script_thread::ScriptThread;
44use crate::task_source::SendableTaskSource;
45
46#[derive(Clone, Copy, Debug, Eq, Hash, JSTraceable, MallocSizeOf, Ord, PartialEq, PartialOrd)]
47pub(crate) struct OneshotTimerHandle(i32);
48
49#[derive(DenyPublicFields, JSTraceable, MallocSizeOf)]
50#[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)]
51pub(crate) struct OneshotTimers {
52    global_scope: Dom<GlobalScope>,
53    js_timers: JsTimers,
54    next_timer_handle: Cell<OneshotTimerHandle>,
55    timers: DomRefCell<VecDeque<OneshotTimer>>,
56    suspended_since: Cell<Option<Instant>>,
57    /// Initially 0, increased whenever the associated document is reactivated
58    /// by the amount of ms the document was inactive. The current time can be
59    /// offset back by this amount for a coherent time across document
60    /// activations.
61    suspension_offset: Cell<Duration>,
62    /// Calls to `fire_timer` with a different argument than this get ignored.
63    /// They were previously scheduled and got invalidated when
64    ///  - timers were suspended,
65    ///  - the timer it was scheduled for got canceled or
66    ///  - a timer was added with an earlier callback time. In this case the
67    ///    original timer is rescheduled when it is the next one to get called.
68    #[no_trace]
69    expected_event_id: Cell<TimerEventId>,
70}
71
72#[derive(DenyPublicFields, JSTraceable, MallocSizeOf)]
73struct OneshotTimer {
74    handle: OneshotTimerHandle,
75    #[no_trace]
76    source: TimerSource,
77    callback: OneshotTimerCallback,
78    scheduled_for: Instant,
79}
80
81// This enum is required to work around the fact that trait objects do not support generic methods.
82// A replacement trait would have a method such as
83//     `invoke<T: DomObject>(self: Box<Self>, this: &T, js_timers: &JsTimers);`.
84#[derive(JSTraceable, MallocSizeOf)]
85pub(crate) enum OneshotTimerCallback {
86    XhrTimeout(XHRTimeoutCallback),
87    EventSourceTimeout(EventSourceTimeoutCallback),
88    JsTimer(JsTimerTask),
89    #[cfg(feature = "testbinding")]
90    TestBindingCallback(TestBindingCallback),
91    RefreshRedirectDue(RefreshRedirectDue),
92}
93
94impl OneshotTimerCallback {
95    fn invoke<T: DomObject>(self, this: &T, js_timers: &JsTimers, can_gc: CanGc) {
96        match self {
97            OneshotTimerCallback::XhrTimeout(callback) => callback.invoke(can_gc),
98            OneshotTimerCallback::EventSourceTimeout(callback) => callback.invoke(),
99            OneshotTimerCallback::JsTimer(task) => task.invoke(this, js_timers, can_gc),
100            #[cfg(feature = "testbinding")]
101            OneshotTimerCallback::TestBindingCallback(callback) => callback.invoke(),
102            OneshotTimerCallback::RefreshRedirectDue(callback) => callback.invoke(can_gc),
103        }
104    }
105}
106
107impl Ord for OneshotTimer {
108    fn cmp(&self, other: &OneshotTimer) -> Ordering {
109        match self.scheduled_for.cmp(&other.scheduled_for).reverse() {
110            Ordering::Equal => self.handle.cmp(&other.handle).reverse(),
111            res => res,
112        }
113    }
114}
115
116impl PartialOrd for OneshotTimer {
117    fn partial_cmp(&self, other: &OneshotTimer) -> Option<Ordering> {
118        Some(self.cmp(other))
119    }
120}
121
122impl Eq for OneshotTimer {}
123impl PartialEq for OneshotTimer {
124    fn eq(&self, other: &OneshotTimer) -> bool {
125        std::ptr::eq(self, other)
126    }
127}
128
129impl OneshotTimers {
130    pub(crate) fn new(global_scope: &GlobalScope) -> OneshotTimers {
131        OneshotTimers {
132            global_scope: Dom::from_ref(global_scope),
133            js_timers: JsTimers::default(),
134            next_timer_handle: Cell::new(OneshotTimerHandle(1)),
135            timers: DomRefCell::new(VecDeque::new()),
136            suspended_since: Cell::new(None),
137            suspension_offset: Cell::new(Duration::ZERO),
138            expected_event_id: Cell::new(TimerEventId(0)),
139        }
140    }
141
142    pub(crate) fn schedule_callback(
143        &self,
144        callback: OneshotTimerCallback,
145        duration: Duration,
146        source: TimerSource,
147    ) -> OneshotTimerHandle {
148        let new_handle = self.next_timer_handle.get();
149        self.next_timer_handle
150            .set(OneshotTimerHandle(new_handle.0 + 1));
151
152        let timer = OneshotTimer {
153            handle: new_handle,
154            source,
155            callback,
156            scheduled_for: self.base_time() + duration,
157        };
158
159        {
160            let mut timers = self.timers.borrow_mut();
161            let insertion_index = timers.binary_search(&timer).err().unwrap();
162            timers.insert(insertion_index, timer);
163        }
164
165        if self.is_next_timer(new_handle) {
166            self.schedule_timer_call();
167        }
168
169        new_handle
170    }
171
172    pub(crate) fn unschedule_callback(&self, handle: OneshotTimerHandle) {
173        let was_next = self.is_next_timer(handle);
174
175        self.timers.borrow_mut().retain(|t| t.handle != handle);
176
177        if was_next {
178            self.invalidate_expected_event_id();
179            self.schedule_timer_call();
180        }
181    }
182
183    fn is_next_timer(&self, handle: OneshotTimerHandle) -> bool {
184        match self.timers.borrow().back() {
185            None => false,
186            Some(max_timer) => max_timer.handle == handle,
187        }
188    }
189
190    /// <https://html.spec.whatwg.org/multipage/#timer-initialisation-steps>
191    pub(crate) fn fire_timer(&self, id: TimerEventId, global: &GlobalScope, can_gc: CanGc) {
192        // Step 9.2. If id does not exist in global's map of setTimeout and setInterval IDs, then abort these steps.
193        let expected_id = self.expected_event_id.get();
194        if expected_id != id {
195            debug!(
196                "ignoring timer fire event {:?} (expected {:?})",
197                id, expected_id
198            );
199            return;
200        }
201
202        assert!(self.suspended_since.get().is_none());
203
204        let base_time = self.base_time();
205
206        // Since the event id was the expected one, at least one timer should be due.
207        if base_time < self.timers.borrow().back().unwrap().scheduled_for {
208            warn!("Unexpected timing!");
209            return;
210        }
211
212        // select timers to run to prevent firing timers
213        // that were installed during fire of another timer
214        let mut timers_to_run = Vec::new();
215
216        loop {
217            let mut timers = self.timers.borrow_mut();
218
219            if timers.is_empty() || timers.back().unwrap().scheduled_for > base_time {
220                break;
221            }
222
223            timers_to_run.push(timers.pop_back().unwrap());
224        }
225
226        for timer in timers_to_run {
227            // Since timers can be coalesced together inside a task,
228            // this loop can keep running, including after an interrupt of the JS,
229            // and prevent a clean-shutdown of a JS-running thread.
230            // This check prevents such a situation.
231            if !global.can_continue_running() {
232                return;
233            }
234            let callback = timer.callback;
235            callback.invoke(global, &self.js_timers, can_gc);
236        }
237
238        self.schedule_timer_call();
239    }
240
241    fn base_time(&self) -> Instant {
242        let offset = self.suspension_offset.get();
243        match self.suspended_since.get() {
244            Some(suspend_time) => suspend_time - offset,
245            None => Instant::now() - offset,
246        }
247    }
248
249    pub(crate) fn slow_down(&self) {
250        let min_duration_ms = pref!(js_timers_minimum_duration) as u64;
251        self.js_timers
252            .set_min_duration(Duration::from_millis(min_duration_ms));
253    }
254
255    pub(crate) fn speed_up(&self) {
256        self.js_timers.remove_min_duration();
257    }
258
259    pub(crate) fn suspend(&self) {
260        // Suspend is idempotent: do nothing if the timers are already suspended.
261        if self.suspended_since.get().is_some() {
262            return warn!("Suspending an already suspended timer.");
263        }
264
265        debug!("Suspending timers.");
266        self.suspended_since.set(Some(Instant::now()));
267        self.invalidate_expected_event_id();
268    }
269
270    pub(crate) fn resume(&self) {
271        // Resume is idempotent: do nothing if the timers are already resumed.
272        let additional_offset = match self.suspended_since.get() {
273            Some(suspended_since) => Instant::now() - suspended_since,
274            None => return warn!("Resuming an already resumed timer."),
275        };
276
277        debug!("Resuming timers.");
278        self.suspension_offset
279            .set(self.suspension_offset.get() + additional_offset);
280        self.suspended_since.set(None);
281
282        self.schedule_timer_call();
283    }
284
285    /// <https://html.spec.whatwg.org/multipage/#timer-initialisation-steps>
286    fn schedule_timer_call(&self) {
287        if self.suspended_since.get().is_some() {
288            // The timer will be scheduled when the pipeline is fully activated.
289            return;
290        }
291
292        let timers = self.timers.borrow();
293        let Some(timer) = timers.back() else {
294            return;
295        };
296
297        let expected_event_id = self.invalidate_expected_event_id();
298        // Step 12. Let completionStep be an algorithm step which queues a global
299        // task on the timer task source given global to run task.
300        let callback = TimerListener {
301            context: Trusted::new(&*self.global_scope),
302            task_source: self
303                .global_scope
304                .task_manager()
305                .timer_task_source()
306                .to_sendable(),
307            source: timer.source,
308            id: expected_event_id,
309        }
310        .into_callback();
311
312        let event_request = TimerEventRequest {
313            callback,
314            duration: timer.scheduled_for - Instant::now(),
315        };
316
317        self.global_scope.schedule_timer(event_request);
318    }
319
320    fn invalidate_expected_event_id(&self) -> TimerEventId {
321        let TimerEventId(currently_expected) = self.expected_event_id.get();
322        let next_id = TimerEventId(currently_expected + 1);
323        debug!(
324            "invalidating expected timer (was {:?}, now {:?}",
325            currently_expected, next_id
326        );
327        self.expected_event_id.set(next_id);
328        next_id
329    }
330
331    #[allow(clippy::too_many_arguments)]
332    pub(crate) fn set_timeout_or_interval(
333        &self,
334        global: &GlobalScope,
335        callback: TimerCallback,
336        arguments: Vec<HandleValue>,
337        timeout: Duration,
338        is_interval: IsInterval,
339        source: TimerSource,
340        can_gc: CanGc,
341    ) -> Fallible<i32> {
342        self.js_timers.set_timeout_or_interval(
343            global,
344            callback,
345            arguments,
346            timeout,
347            is_interval,
348            source,
349            can_gc,
350        )
351    }
352
353    pub(crate) fn clear_timeout_or_interval(&self, global: &GlobalScope, handle: i32) {
354        self.js_timers.clear_timeout_or_interval(global, handle)
355    }
356}
357
358#[derive(Clone, Copy, Eq, Hash, JSTraceable, MallocSizeOf, Ord, PartialEq, PartialOrd)]
359pub(crate) struct JsTimerHandle(i32);
360
361#[derive(DenyPublicFields, JSTraceable, MallocSizeOf)]
362pub(crate) struct JsTimers {
363    next_timer_handle: Cell<JsTimerHandle>,
364    /// <https://html.spec.whatwg.org/multipage/#list-of-active-timers>
365    active_timers: DomRefCell<FxHashMap<JsTimerHandle, JsTimerEntry>>,
366    /// The nesting level of the currently executing timer task or 0.
367    nesting_level: Cell<u32>,
368    /// Used to introduce a minimum delay in event intervals
369    min_duration: Cell<Option<Duration>>,
370}
371
372#[derive(JSTraceable, MallocSizeOf)]
373struct JsTimerEntry {
374    oneshot_handle: OneshotTimerHandle,
375}
376
377// Holder for the various JS values associated with setTimeout
378// (ie. function value to invoke and all arguments to pass
379//      to the function when calling it)
380// TODO: Handle rooting during invocation when movable GC is turned on
381#[derive(JSTraceable, MallocSizeOf)]
382pub(crate) struct JsTimerTask {
383    handle: JsTimerHandle,
384    #[no_trace]
385    source: TimerSource,
386    callback: InternalTimerCallback,
387    is_interval: IsInterval,
388    nesting_level: u32,
389    duration: Duration,
390    is_user_interacting: bool,
391}
392
393// Enum allowing more descriptive values for the is_interval field
394#[derive(Clone, Copy, JSTraceable, MallocSizeOf, PartialEq)]
395pub(crate) enum IsInterval {
396    Interval,
397    NonInterval,
398}
399
400pub(crate) enum TimerCallback {
401    StringTimerCallback(TrustedScriptOrString),
402    FunctionTimerCallback(Rc<Function>),
403}
404
405#[derive(Clone, JSTraceable, MallocSizeOf)]
406#[cfg_attr(crown, allow(crown::unrooted_must_root))]
407enum InternalTimerCallback {
408    StringTimerCallback(DOMString),
409    FunctionTimerCallback(
410        #[conditional_malloc_size_of] Rc<Function>,
411        #[ignore_malloc_size_of = "Rc"] Rc<Box<[Heap<JSVal>]>>,
412    ),
413}
414
415impl Default for JsTimers {
416    fn default() -> Self {
417        JsTimers {
418            next_timer_handle: Cell::new(JsTimerHandle(1)),
419            active_timers: DomRefCell::new(FxHashMap::default()),
420            nesting_level: Cell::new(0),
421            min_duration: Cell::new(None),
422        }
423    }
424}
425
426impl JsTimers {
427    /// <https://html.spec.whatwg.org/multipage/#timer-initialisation-steps>
428    #[allow(clippy::too_many_arguments)]
429    #[cfg_attr(crown, allow(crown::unrooted_must_root))]
430    pub(crate) fn set_timeout_or_interval(
431        &self,
432        global: &GlobalScope,
433        callback: TimerCallback,
434        arguments: Vec<HandleValue>,
435        timeout: Duration,
436        is_interval: IsInterval,
437        source: TimerSource,
438        can_gc: CanGc,
439    ) -> Fallible<i32> {
440        let callback = match callback {
441            TimerCallback::StringTimerCallback(trusted_script_or_string) => {
442                // Step 9.6.1.1. Let globalName be "Window" if global is a Window object; "WorkerGlobalScope" otherwise.
443                let global_name = if global.is::<Window>() {
444                    "Window"
445                } else {
446                    "WorkerGlobalScope"
447                };
448                // Step 9.6.1.2. Let methodName be "setInterval" if repeat is true; "setTimeout" otherwise.
449                let method_name = if is_interval == IsInterval::Interval {
450                    "setInterval"
451                } else {
452                    "setTimeout"
453                };
454                // Step 9.6.1.3. Let sink be a concatenation of globalName, U+0020 SPACE, and methodName.
455                let sink = format!("{} {}", global_name, method_name);
456                // Step 9.6.1.4. Set handler to the result of invoking the
457                // Get Trusted Type compliant string algorithm with TrustedScript, global, handler, sink, and "script".
458                let code_str = TrustedScript::get_trusted_script_compliant_string(
459                    global,
460                    trusted_script_or_string,
461                    &sink,
462                    can_gc,
463                )?;
464                // Step 9.6.3. Perform EnsureCSPDoesNotBlockStringCompilation(realm, « », handler, handler, timer, « », handler).
465                // If this throws an exception, catch it, report it for global, and abort these steps.
466                if global
467                    .get_csp_list()
468                    .is_js_evaluation_allowed(global, code_str.as_ref())
469                {
470                    // Step 9.6.2. Assert: handler is a string.
471                    InternalTimerCallback::StringTimerCallback(code_str)
472                } else {
473                    return Ok(0);
474                }
475            },
476            TimerCallback::FunctionTimerCallback(function) => {
477                // This is a bit complicated, but this ensures that the vector's
478                // buffer isn't reallocated (and moved) after setting the Heap values
479                let mut args = Vec::with_capacity(arguments.len());
480                for _ in 0..arguments.len() {
481                    args.push(Heap::default());
482                }
483                for (i, item) in arguments.iter().enumerate() {
484                    args.get_mut(i).unwrap().set(item.get());
485                }
486                // Step 9.5. If handler is a Function, then invoke handler given arguments and "report",
487                // and with callback this value set to thisArg.
488                InternalTimerCallback::FunctionTimerCallback(
489                    function,
490                    Rc::new(args.into_boxed_slice()),
491                )
492            },
493        };
494
495        // Step 2. If previousId was given, let id be previousId; otherwise,
496        // let id be an implementation-defined integer that is greater than zero
497        // and does not already exist in global's map of setTimeout and setInterval IDs.
498        let JsTimerHandle(new_handle) = self.next_timer_handle.get();
499        self.next_timer_handle.set(JsTimerHandle(new_handle + 1));
500
501        // Step 3. If the surrounding agent's event loop's currently running task
502        // is a task that was created by this algorithm, then let nesting level
503        // be the task's timer nesting level. Otherwise, let nesting level be 0.
504        let mut task = JsTimerTask {
505            handle: JsTimerHandle(new_handle),
506            source,
507            callback,
508            is_interval,
509            is_user_interacting: ScriptThread::is_user_interacting(),
510            nesting_level: 0,
511            duration: Duration::ZERO,
512        };
513
514        // Step 4. If timeout is less than 0, then set timeout to 0.
515        task.duration = timeout.max(Duration::ZERO);
516
517        self.initialize_and_schedule(global, task);
518
519        // Step 15. Return id.
520        Ok(new_handle)
521    }
522
523    pub(crate) fn clear_timeout_or_interval(&self, global: &GlobalScope, handle: i32) {
524        let mut active_timers = self.active_timers.borrow_mut();
525
526        if let Some(entry) = active_timers.remove(&JsTimerHandle(handle)) {
527            global.unschedule_callback(entry.oneshot_handle);
528        }
529    }
530
531    pub(crate) fn set_min_duration(&self, duration: Duration) {
532        self.min_duration.set(Some(duration));
533    }
534
535    pub(crate) fn remove_min_duration(&self) {
536        self.min_duration.set(None);
537    }
538
539    // see step 13 of https://html.spec.whatwg.org/multipage/#timer-initialisation-steps
540    fn user_agent_pad(&self, current_duration: Duration) -> Duration {
541        match self.min_duration.get() {
542            Some(min_duration) => min_duration.max(current_duration),
543            None => current_duration,
544        }
545    }
546
547    /// <https://html.spec.whatwg.org/multipage/#timer-initialisation-steps>
548    fn initialize_and_schedule(&self, global: &GlobalScope, mut task: JsTimerTask) {
549        let handle = task.handle;
550        let mut active_timers = self.active_timers.borrow_mut();
551
552        // Step 3. If the surrounding agent's event loop's currently running task
553        // is a task that was created by this algorithm, then let nesting level be
554        // the task's timer nesting level. Otherwise, let nesting level be 0.
555        let nesting_level = self.nesting_level.get();
556
557        let duration = self.user_agent_pad(clamp_duration(nesting_level, task.duration));
558        // Step 10. Increment nesting level by one.
559        // Step 11. Set task's timer nesting level to nesting level.
560        task.nesting_level = nesting_level + 1;
561
562        // Step 13. Set uniqueHandle to the result of running steps after a timeout given global,
563        // "setTimeout/setInterval", timeout, and completionStep.
564        let callback = OneshotTimerCallback::JsTimer(task);
565        let oneshot_handle = global.schedule_callback(callback, duration);
566
567        // Step 14. Set global's map of setTimeout and setInterval IDs[id] to uniqueHandle.
568        let entry = active_timers
569            .entry(handle)
570            .or_insert(JsTimerEntry { oneshot_handle });
571        entry.oneshot_handle = oneshot_handle;
572    }
573}
574
575/// Step 5 of <https://html.spec.whatwg.org/multipage/#timer-initialisation-steps>
576fn clamp_duration(nesting_level: u32, unclamped: Duration) -> Duration {
577    // Step 5. If nesting level is greater than 5, and timeout is less than 4, then set timeout to 4.
578    let lower_bound_ms = if nesting_level > 5 { 4 } else { 0 };
579    let lower_bound = Duration::from_millis(lower_bound_ms);
580    lower_bound.max(unclamped)
581}
582
583impl JsTimerTask {
584    // see https://html.spec.whatwg.org/multipage/#timer-initialisation-steps
585    pub(crate) fn invoke<T: DomObject>(self, this: &T, timers: &JsTimers, can_gc: CanGc) {
586        // step 9.2 can be ignored, because we proactively prevent execution
587        // of this task when its scheduled execution is canceled.
588
589        // prep for step ? in nested set_timeout_or_interval calls
590        timers.nesting_level.set(self.nesting_level);
591
592        let _guard = ScriptThread::user_interacting_guard();
593        match self.callback {
594            InternalTimerCallback::StringTimerCallback(ref code_str) => {
595                // Step 6.4. Let settings object be global's relevant settings object.
596                // Step 6. Let realm be global's relevant realm.
597                let global = this.global();
598                // Step 7. Let initiating script be the active script.
599                let cx = GlobalScope::get_cx();
600                // Step 9.6.7. If initiating script is not null, then:
601                rooted!(in(*cx) let mut rval = UndefinedValue());
602                // Step 9.6.7.1. Set fetch options to a script fetch options whose cryptographic nonce
603                // is initiating script's fetch options's cryptographic nonce,
604                // integrity metadata is the empty string, parser metadata is "not-parser-inserted",
605                // credentials mode is initiating script's fetch options's credentials mode,
606                // referrer policy is initiating script's fetch options's referrer policy,
607                // and fetch priority is "auto".
608                // Step 9.6.8. Let script be the result of creating a classic script given handler,
609                // settings object, base URL, and fetch options.
610                // Step 9.6.9. Run the classic script script.
611                //
612                // FIXME(cybai): Use base url properly by saving private reference for timers (#27260)
613                _ = global.evaluate_js_on_global_with_result(
614                    code_str,
615                    rval.handle_mut(),
616                    ScriptFetchOptions::default_classic_script(&global),
617                    // Step 9.6. Let base URL be settings object's API base URL.
618                    // Step 9.7.2. Set base URL to initiating script's base URL.
619                    global.api_base_url(),
620                    can_gc,
621                    Some(IntroductionType::DOM_TIMER),
622                );
623            },
624            // Step 9.5. If handler is a Function, then invoke handler given arguments and
625            // "report", and with callback this value set to thisArg.
626            InternalTimerCallback::FunctionTimerCallback(ref function, ref arguments) => {
627                let arguments = self.collect_heap_args(arguments);
628                rooted!(in(*GlobalScope::get_cx()) let mut value: JSVal);
629                let _ = function.Call_(this, arguments, value.handle_mut(), Report, can_gc);
630            },
631        };
632
633        // reset nesting level (see above)
634        timers.nesting_level.set(0);
635
636        // Step 9.9. If repeat is true, then perform the timer initialization steps again,
637        // given global, handler, timeout, arguments, true, and id.
638        //
639        // Since we choose proactively prevent execution (see 4.1 above), we must only
640        // reschedule repeating timers when they were not canceled as part of step 4.2.
641        if self.is_interval == IsInterval::Interval &&
642            timers.active_timers.borrow().contains_key(&self.handle)
643        {
644            timers.initialize_and_schedule(&this.global(), self);
645        }
646    }
647
648    fn collect_heap_args<'b>(&self, args: &'b [Heap<JSVal>]) -> Vec<HandleValue<'b>> {
649        args.iter().map(|arg| arg.as_handle_value()).collect()
650    }
651}
652
653/// Describes the source that requested the [`TimerEvent`].
654#[derive(Clone, Copy, Debug, Deserialize, MallocSizeOf, Serialize)]
655pub enum TimerSource {
656    /// The event was requested from a window (`ScriptThread`).
657    FromWindow(PipelineId),
658    /// The event was requested from a worker (`DedicatedGlobalWorkerScope`).
659    FromWorker,
660}
661
662/// The id to be used for a [`TimerEvent`] is defined by the corresponding [`TimerEventRequest`].
663#[derive(Clone, Copy, Debug, Deserialize, Eq, MallocSizeOf, PartialEq, Serialize)]
664pub struct TimerEventId(pub u32);
665
666/// A notification that a timer has fired. [`TimerSource`] must be `FromWindow` when
667/// dispatched to `ScriptThread` and must be `FromWorker` when dispatched to a
668/// `DedicatedGlobalWorkerScope`
669#[derive(Clone, Copy, Debug, Deserialize, Serialize)]
670pub struct TimerEvent(pub TimerSource, pub TimerEventId);
671
672/// A wrapper between timer events coming in over IPC, and the event-loop.
673#[derive(Clone)]
674struct TimerListener {
675    task_source: SendableTaskSource,
676    context: Trusted<GlobalScope>,
677    source: TimerSource,
678    id: TimerEventId,
679}
680
681impl TimerListener {
682    /// Handle a timer-event coming from the [`timers::TimerScheduler`]
683    /// by queuing the appropriate task on the relevant event-loop.
684    /// <https://html.spec.whatwg.org/multipage/#timer-initialisation-steps>
685    fn handle(&self, event: TimerEvent) {
686        let context = self.context.clone();
687        // Step 9. Let task be a task that runs the following substeps:
688        self.task_source.queue(task!(timer_event: move || {
689                let global = context.root();
690                let TimerEvent(source, id) = event;
691                match source {
692                    TimerSource::FromWorker => {
693                        global.downcast::<WorkerGlobalScope>().expect("Window timer delivered to worker");
694                    },
695                    TimerSource::FromWindow(pipeline) => {
696                        assert_eq!(pipeline, global.pipeline_id());
697                        global.downcast::<Window>().expect("Worker timer delivered to window");
698                    },
699                };
700                global.fire_timer(id, CanGc::note());
701            })
702        );
703    }
704
705    fn into_callback(self) -> BoxedTimerCallback {
706        let timer_event = TimerEvent(self.source, self.id);
707        Box::new(move || self.handle(timer_event))
708    }
709}