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
46type TimerKey = i32;
47type RunStepsDeadline = Instant;
48type CompletionStep = Box<dyn FnOnce(&GlobalScope, CanGc) + 'static>;
49
50/// <https://html.spec.whatwg.org/multipage/#run-steps-after-a-timeout>
51/// OrderingIdentifier per spec ("orderingIdentifier")
52type OrderingIdentifier = DOMString;
53
54#[derive(JSTraceable, MallocSizeOf)]
55struct OrderingEntry {
56    milliseconds: u64,
57    start_seq: u64,
58    handle: OneshotTimerHandle,
59}
60
61// Per-ordering queues map
62type OrderingQueues = FxHashMap<OrderingIdentifier, Vec<OrderingEntry>>;
63
64// Active timers map for Run Steps After A Timeout
65type RunStepsActiveMap = FxHashMap<TimerKey, RunStepsDeadline>;
66
67#[derive(Clone, Copy, Debug, Eq, Hash, JSTraceable, MallocSizeOf, Ord, PartialEq, PartialOrd)]
68pub(crate) struct OneshotTimerHandle(i32);
69
70#[derive(DenyPublicFields, JSTraceable, MallocSizeOf)]
71#[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)]
72pub(crate) struct OneshotTimers {
73    global_scope: Dom<GlobalScope>,
74    js_timers: JsTimers,
75    next_timer_handle: Cell<OneshotTimerHandle>,
76    timers: DomRefCell<VecDeque<OneshotTimer>>,
77    suspended_since: Cell<Option<Instant>>,
78    /// Initially 0, increased whenever the associated document is reactivated
79    /// by the amount of ms the document was inactive. The current time can be
80    /// offset back by this amount for a coherent time across document
81    /// activations.
82    suspension_offset: Cell<Duration>,
83    /// Calls to `fire_timer` with a different argument than this get ignored.
84    /// They were previously scheduled and got invalidated when
85    ///  - timers were suspended,
86    ///  - the timer it was scheduled for got canceled or
87    ///  - a timer was added with an earlier callback time. In this case the
88    ///    original timer is rescheduled when it is the next one to get called.
89    #[no_trace]
90    expected_event_id: Cell<TimerEventId>,
91    /// <https://html.spec.whatwg.org/multipage/#map-of-active-timers>
92    /// TODO this should also be used for the other timers
93    /// as per <html.spec.whatwg.org/multipage/#map-of-settimeout-and-setinterval-ids>Z.
94    map_of_active_timers: DomRefCell<RunStepsActiveMap>,
95
96    /// <https://html.spec.whatwg.org/multipage/#run-steps-after-a-timeout>
97    /// Step 4.2 Wait until any invocations of this algorithm that had the same global and orderingIdentifier,
98    /// that started before this one, and whose milliseconds is less than or equal to this one's, have completed.
99    runsteps_queues: DomRefCell<OrderingQueues>,
100
101    /// <html.spec.whatwg.org/multipage/#timers:unique-internal-value-5>
102    next_runsteps_key: Cell<TimerKey>,
103
104    /// <https://html.spec.whatwg.org/multipage/#run-steps-after-a-timeout>
105    /// Start order sequence to break ties for Step 4.2.
106    runsteps_start_seq: Cell<u64>,
107}
108
109#[derive(DenyPublicFields, JSTraceable, MallocSizeOf)]
110struct OneshotTimer {
111    handle: OneshotTimerHandle,
112    #[no_trace]
113    source: TimerSource,
114    callback: OneshotTimerCallback,
115    scheduled_for: Instant,
116}
117
118// This enum is required to work around the fact that trait objects do not support generic methods.
119// A replacement trait would have a method such as
120//     `invoke<T: DomObject>(self: Box<Self>, this: &T, js_timers: &JsTimers);`.
121#[derive(JSTraceable, MallocSizeOf)]
122pub(crate) enum OneshotTimerCallback {
123    XhrTimeout(XHRTimeoutCallback),
124    EventSourceTimeout(EventSourceTimeoutCallback),
125    JsTimer(JsTimerTask),
126    #[cfg(feature = "testbinding")]
127    TestBindingCallback(TestBindingCallback),
128    RefreshRedirectDue(RefreshRedirectDue),
129    /// <https://html.spec.whatwg.org/multipage/#run-steps-after-a-timeout>
130    RunStepsAfterTimeout {
131        /// Step 1. timerKey
132        timer_key: i32,
133        /// Step 4. orderingIdentifier
134        ordering_id: DOMString,
135        /// Spec: milliseconds (the algorithm input)
136        milliseconds: u64,
137        /// Perform completionSteps.
138        #[no_trace]
139        #[ignore_malloc_size_of = "Closure"]
140        completion: CompletionStep,
141    },
142}
143
144impl OneshotTimerCallback {
145    fn invoke<T: DomObject>(self, this: &T, js_timers: &JsTimers, can_gc: CanGc) {
146        match self {
147            OneshotTimerCallback::XhrTimeout(callback) => callback.invoke(can_gc),
148            OneshotTimerCallback::EventSourceTimeout(callback) => callback.invoke(),
149            OneshotTimerCallback::JsTimer(task) => task.invoke(this, js_timers, can_gc),
150            #[cfg(feature = "testbinding")]
151            OneshotTimerCallback::TestBindingCallback(callback) => callback.invoke(),
152            OneshotTimerCallback::RefreshRedirectDue(callback) => callback.invoke(can_gc),
153            OneshotTimerCallback::RunStepsAfterTimeout { completion, .. } => {
154                // <https://html.spec.whatwg.org/multipage/#run-steps-after-a-timeout>
155                // Step 4.4 Perform completionSteps.
156                completion(&this.global(), can_gc);
157            },
158        }
159    }
160}
161
162impl Ord for OneshotTimer {
163    fn cmp(&self, other: &OneshotTimer) -> Ordering {
164        match self.scheduled_for.cmp(&other.scheduled_for).reverse() {
165            Ordering::Equal => self.handle.cmp(&other.handle).reverse(),
166            res => res,
167        }
168    }
169}
170
171impl PartialOrd for OneshotTimer {
172    fn partial_cmp(&self, other: &OneshotTimer) -> Option<Ordering> {
173        Some(self.cmp(other))
174    }
175}
176
177impl Eq for OneshotTimer {}
178impl PartialEq for OneshotTimer {
179    fn eq(&self, other: &OneshotTimer) -> bool {
180        std::ptr::eq(self, other)
181    }
182}
183
184impl OneshotTimers {
185    pub(crate) fn new(global_scope: &GlobalScope) -> OneshotTimers {
186        OneshotTimers {
187            global_scope: Dom::from_ref(global_scope),
188            js_timers: JsTimers::default(),
189            next_timer_handle: Cell::new(OneshotTimerHandle(1)),
190            timers: DomRefCell::new(VecDeque::new()),
191            suspended_since: Cell::new(None),
192            suspension_offset: Cell::new(Duration::ZERO),
193            expected_event_id: Cell::new(TimerEventId(0)),
194            map_of_active_timers: Default::default(),
195            runsteps_queues: Default::default(),
196            next_runsteps_key: Cell::new(1),
197            runsteps_start_seq: Cell::new(0),
198        }
199    }
200
201    /// <https://html.spec.whatwg.org/multipage/#run-steps-after-a-timeout>
202    #[inline]
203    pub(crate) fn now_for_runsteps(&self) -> Instant {
204        // Step 2. Let startTime be the current high resolution time given global.
205        self.base_time()
206    }
207
208    /// <https://html.spec.whatwg.org/multipage/#run-steps-after-a-timeout>
209    /// Step 1. Let timerKey be a new unique internal value.
210    pub(crate) fn fresh_runsteps_key(&self) -> TimerKey {
211        let k = self.next_runsteps_key.get();
212        self.next_runsteps_key.set(k + 1);
213        k
214    }
215
216    /// <https://html.spec.whatwg.org/multipage/#run-steps-after-a-timeout>
217    /// Step 3. Set global's map of active timers[timerKey] to startTime plus milliseconds.
218    pub(crate) fn runsteps_set_active(&self, timer_key: TimerKey, deadline: RunStepsDeadline) {
219        self.map_of_active_timers
220            .borrow_mut()
221            .insert(timer_key, deadline);
222    }
223
224    /// <https://html.spec.whatwg.org/multipage/#run-steps-after-a-timeout>
225    /// Helper for Step 4.2: maintain per-ordering sorted queue by (milliseconds, startSeq, handle).
226    fn runsteps_enqueue_sorted(
227        &self,
228        ordering_id: &DOMString,
229        handle: OneshotTimerHandle,
230        milliseconds: u64,
231    ) {
232        let mut map = self.runsteps_queues.borrow_mut();
233        let q = map.entry(ordering_id.clone()).or_default();
234
235        let seq = {
236            let cur = self.runsteps_start_seq.get();
237            self.runsteps_start_seq.set(cur + 1);
238            cur
239        };
240
241        let key = OrderingEntry {
242            milliseconds,
243            start_seq: seq,
244            handle,
245        };
246
247        let idx = q
248            .binary_search_by(|ordering_entry| {
249                match ordering_entry.milliseconds.cmp(&milliseconds) {
250                    Ordering::Less => Ordering::Less,
251                    Ordering::Greater => Ordering::Greater,
252                    Ordering::Equal => ordering_entry.start_seq.cmp(&seq),
253                }
254            })
255            .unwrap_or_else(|i| i);
256
257        q.insert(idx, key);
258    }
259
260    pub(crate) fn schedule_callback(
261        &self,
262        callback: OneshotTimerCallback,
263        duration: Duration,
264        source: TimerSource,
265    ) -> OneshotTimerHandle {
266        let new_handle = self.next_timer_handle.get();
267        self.next_timer_handle
268            .set(OneshotTimerHandle(new_handle.0 + 1));
269
270        let timer = OneshotTimer {
271            handle: new_handle,
272            source,
273            callback,
274            scheduled_for: self.base_time() + duration,
275        };
276
277        // https://html.spec.whatwg.org/multipage/#run-steps-after-a-timeout
278        // Step 4.2: maintain per-orderingIdentifier order by milliseconds (and start order for ties).
279        if let OneshotTimerCallback::RunStepsAfterTimeout {
280            ordering_id,
281            milliseconds,
282            ..
283        } = &timer.callback
284        {
285            self.runsteps_enqueue_sorted(ordering_id, new_handle, *milliseconds);
286        }
287
288        {
289            let mut timers = self.timers.borrow_mut();
290            let insertion_index = timers.binary_search(&timer).err().unwrap();
291            timers.insert(insertion_index, timer);
292        }
293
294        if self.is_next_timer(new_handle) {
295            self.schedule_timer_call();
296        }
297
298        new_handle
299    }
300
301    pub(crate) fn unschedule_callback(&self, handle: OneshotTimerHandle) {
302        let was_next = self.is_next_timer(handle);
303
304        self.timers.borrow_mut().retain(|t| t.handle != handle);
305
306        if was_next {
307            self.invalidate_expected_event_id();
308            self.schedule_timer_call();
309        }
310    }
311
312    fn is_next_timer(&self, handle: OneshotTimerHandle) -> bool {
313        match self.timers.borrow().back() {
314            None => false,
315            Some(max_timer) => max_timer.handle == handle,
316        }
317    }
318
319    /// <https://html.spec.whatwg.org/multipage/#timer-initialisation-steps>
320    pub(crate) fn fire_timer(&self, id: TimerEventId, global: &GlobalScope, can_gc: CanGc) {
321        // Step 9.2. If id does not exist in global's map of setTimeout and setInterval IDs, then abort these steps.
322        let expected_id = self.expected_event_id.get();
323        if expected_id != id {
324            debug!(
325                "ignoring timer fire event {:?} (expected {:?})",
326                id, expected_id
327            );
328            return;
329        }
330
331        assert!(self.suspended_since.get().is_none());
332
333        let base_time = self.base_time();
334
335        // Since the event id was the expected one, at least one timer should be due.
336        if base_time < self.timers.borrow().back().unwrap().scheduled_for {
337            warn!("Unexpected timing!");
338            return;
339        }
340
341        // select timers to run to prevent firing timers
342        // that were installed during fire of another timer
343        let mut timers_to_run = Vec::new();
344
345        loop {
346            let mut timers = self.timers.borrow_mut();
347
348            if timers.is_empty() || timers.back().unwrap().scheduled_for > base_time {
349                break;
350            }
351
352            timers_to_run.push(timers.pop_back().unwrap());
353        }
354
355        for timer in timers_to_run {
356            // Since timers can be coalesced together inside a task,
357            // this loop can keep running, including after an interrupt of the JS,
358            // and prevent a clean-shutdown of a JS-running thread.
359            // This check prevents such a situation.
360            if !global.can_continue_running() {
361                return;
362            }
363            match &timer.callback {
364                // TODO: https://github.com/servo/servo/issues/40060
365                OneshotTimerCallback::RunStepsAfterTimeout { ordering_id, .. } => {
366                    // Step 4.2 Wait until any invocations of this algorithm that had the same global and orderingIdentifier,
367                    // that started before this one, and whose milliseconds is less than or equal to this one's, have completed.
368                    let head_handle_opt = {
369                        let queues_ref = self.runsteps_queues.borrow();
370                        queues_ref
371                            .get(ordering_id)
372                            .and_then(|v| v.first().map(|t| t.handle))
373                    };
374                    let is_head = head_handle_opt.is_none_or(|head| head == timer.handle);
375
376                    if !is_head {
377                        // TODO: this re queuing would go away when we revisit timers implementation.
378                        let rein = OneshotTimer {
379                            handle: timer.handle,
380                            source: timer.source,
381                            callback: timer.callback,
382                            scheduled_for: self.base_time(),
383                        };
384                        let mut timers = self.timers.borrow_mut();
385                        let idx = timers.binary_search(&rein).err().unwrap();
386                        timers.insert(idx, rein);
387                        continue;
388                    }
389
390                    let (timer_key, ordering_id_owned, completion) = match timer.callback {
391                        OneshotTimerCallback::RunStepsAfterTimeout {
392                            timer_key,
393                            ordering_id,
394                            milliseconds: _,
395                            completion,
396                        } => (timer_key, ordering_id, completion),
397                        _ => unreachable!(),
398                    };
399
400                    // Step 4.3 Optionally, wait a further implementation-defined length of time.
401                    // (No additional delay applied.)
402
403                    // Step 4.4 Perform completionSteps.
404                    (completion)(global, can_gc);
405
406                    // Step 4.5 Remove global's map of active timers[timerKey].
407                    self.map_of_active_timers.borrow_mut().remove(&timer_key);
408
409                    {
410                        let mut queues_mut = self.runsteps_queues.borrow_mut();
411                        if let Some(q) = queues_mut.get_mut(&ordering_id_owned) {
412                            if !q.is_empty() {
413                                q.remove(0);
414                            }
415                            if q.is_empty() {
416                                queues_mut.remove(&ordering_id_owned);
417                            }
418                        }
419                    }
420                },
421                _ => {
422                    let cb = timer.callback;
423                    cb.invoke(global, &self.js_timers, can_gc);
424                },
425            }
426        }
427
428        self.schedule_timer_call();
429    }
430
431    fn base_time(&self) -> Instant {
432        let offset = self.suspension_offset.get();
433        match self.suspended_since.get() {
434            Some(suspend_time) => suspend_time - offset,
435            None => Instant::now() - offset,
436        }
437    }
438
439    pub(crate) fn slow_down(&self) {
440        let min_duration_ms = pref!(js_timers_minimum_duration) as u64;
441        self.js_timers
442            .set_min_duration(Duration::from_millis(min_duration_ms));
443    }
444
445    pub(crate) fn speed_up(&self) {
446        self.js_timers.remove_min_duration();
447    }
448
449    pub(crate) fn suspend(&self) {
450        // Suspend is idempotent: do nothing if the timers are already suspended.
451        if self.suspended_since.get().is_some() {
452            return warn!("Suspending an already suspended timer.");
453        }
454
455        debug!("Suspending timers.");
456        self.suspended_since.set(Some(Instant::now()));
457        self.invalidate_expected_event_id();
458    }
459
460    pub(crate) fn resume(&self) {
461        // Resume is idempotent: do nothing if the timers are already resumed.
462        let additional_offset = match self.suspended_since.get() {
463            Some(suspended_since) => Instant::now() - suspended_since,
464            None => return warn!("Resuming an already resumed timer."),
465        };
466
467        debug!("Resuming timers.");
468        self.suspension_offset
469            .set(self.suspension_offset.get() + additional_offset);
470        self.suspended_since.set(None);
471
472        self.schedule_timer_call();
473    }
474
475    /// <https://html.spec.whatwg.org/multipage/#timer-initialisation-steps>
476    fn schedule_timer_call(&self) {
477        if self.suspended_since.get().is_some() {
478            // The timer will be scheduled when the pipeline is fully activated.
479            return;
480        }
481
482        let timers = self.timers.borrow();
483        let Some(timer) = timers.back() else {
484            return;
485        };
486
487        let expected_event_id = self.invalidate_expected_event_id();
488        // Step 12. Let completionStep be an algorithm step which queues a global
489        // task on the timer task source given global to run task.
490        let callback = TimerListener {
491            context: Trusted::new(&*self.global_scope),
492            task_source: self
493                .global_scope
494                .task_manager()
495                .timer_task_source()
496                .to_sendable(),
497            source: timer.source,
498            id: expected_event_id,
499        }
500        .into_callback();
501
502        let event_request = TimerEventRequest {
503            callback,
504            duration: timer.scheduled_for - self.base_time(),
505        };
506
507        self.global_scope.schedule_timer(event_request);
508    }
509
510    fn invalidate_expected_event_id(&self) -> TimerEventId {
511        let TimerEventId(currently_expected) = self.expected_event_id.get();
512        let next_id = TimerEventId(currently_expected + 1);
513        debug!(
514            "invalidating expected timer (was {:?}, now {:?}",
515            currently_expected, next_id
516        );
517        self.expected_event_id.set(next_id);
518        next_id
519    }
520
521    #[allow(clippy::too_many_arguments)]
522    pub(crate) fn set_timeout_or_interval(
523        &self,
524        global: &GlobalScope,
525        callback: TimerCallback,
526        arguments: Vec<HandleValue>,
527        timeout: Duration,
528        is_interval: IsInterval,
529        source: TimerSource,
530        can_gc: CanGc,
531    ) -> Fallible<i32> {
532        self.js_timers.set_timeout_or_interval(
533            global,
534            callback,
535            arguments,
536            timeout,
537            is_interval,
538            source,
539            can_gc,
540        )
541    }
542
543    pub(crate) fn clear_timeout_or_interval(&self, global: &GlobalScope, handle: i32) {
544        self.js_timers.clear_timeout_or_interval(global, handle)
545    }
546}
547
548#[derive(Clone, Copy, Eq, Hash, JSTraceable, MallocSizeOf, Ord, PartialEq, PartialOrd)]
549pub(crate) struct JsTimerHandle(i32);
550
551#[derive(DenyPublicFields, JSTraceable, MallocSizeOf)]
552pub(crate) struct JsTimers {
553    next_timer_handle: Cell<JsTimerHandle>,
554    /// <https://html.spec.whatwg.org/multipage/#list-of-active-timers>
555    active_timers: DomRefCell<FxHashMap<JsTimerHandle, JsTimerEntry>>,
556    /// The nesting level of the currently executing timer task or 0.
557    nesting_level: Cell<u32>,
558    /// Used to introduce a minimum delay in event intervals
559    min_duration: Cell<Option<Duration>>,
560}
561
562#[derive(JSTraceable, MallocSizeOf)]
563struct JsTimerEntry {
564    oneshot_handle: OneshotTimerHandle,
565}
566
567// Holder for the various JS values associated with setTimeout
568// (ie. function value to invoke and all arguments to pass
569//      to the function when calling it)
570// TODO: Handle rooting during invocation when movable GC is turned on
571#[derive(JSTraceable, MallocSizeOf)]
572pub(crate) struct JsTimerTask {
573    handle: JsTimerHandle,
574    #[no_trace]
575    source: TimerSource,
576    callback: InternalTimerCallback,
577    is_interval: IsInterval,
578    nesting_level: u32,
579    duration: Duration,
580    is_user_interacting: bool,
581}
582
583// Enum allowing more descriptive values for the is_interval field
584#[derive(Clone, Copy, JSTraceable, MallocSizeOf, PartialEq)]
585pub(crate) enum IsInterval {
586    Interval,
587    NonInterval,
588}
589
590pub(crate) enum TimerCallback {
591    StringTimerCallback(TrustedScriptOrString),
592    FunctionTimerCallback(Rc<Function>),
593}
594
595#[derive(Clone, JSTraceable, MallocSizeOf)]
596#[cfg_attr(crown, allow(crown::unrooted_must_root))]
597enum InternalTimerCallback {
598    StringTimerCallback(DOMString),
599    FunctionTimerCallback(
600        #[conditional_malloc_size_of] Rc<Function>,
601        #[ignore_malloc_size_of = "mozjs"] Rc<Box<[Heap<JSVal>]>>,
602    ),
603}
604
605impl Default for JsTimers {
606    fn default() -> Self {
607        JsTimers {
608            next_timer_handle: Cell::new(JsTimerHandle(1)),
609            active_timers: DomRefCell::new(FxHashMap::default()),
610            nesting_level: Cell::new(0),
611            min_duration: Cell::new(None),
612        }
613    }
614}
615
616impl JsTimers {
617    /// <https://html.spec.whatwg.org/multipage/#timer-initialisation-steps>
618    #[allow(clippy::too_many_arguments)]
619    #[cfg_attr(crown, allow(crown::unrooted_must_root))]
620    pub(crate) fn set_timeout_or_interval(
621        &self,
622        global: &GlobalScope,
623        callback: TimerCallback,
624        arguments: Vec<HandleValue>,
625        timeout: Duration,
626        is_interval: IsInterval,
627        source: TimerSource,
628        can_gc: CanGc,
629    ) -> Fallible<i32> {
630        let callback = match callback {
631            TimerCallback::StringTimerCallback(trusted_script_or_string) => {
632                // Step 9.6.1.1. Let globalName be "Window" if global is a Window object; "WorkerGlobalScope" otherwise.
633                let global_name = if global.is::<Window>() {
634                    "Window"
635                } else {
636                    "WorkerGlobalScope"
637                };
638                // Step 9.6.1.2. Let methodName be "setInterval" if repeat is true; "setTimeout" otherwise.
639                let method_name = if is_interval == IsInterval::Interval {
640                    "setInterval"
641                } else {
642                    "setTimeout"
643                };
644                // Step 9.6.1.3. Let sink be a concatenation of globalName, U+0020 SPACE, and methodName.
645                let sink = format!("{} {}", global_name, method_name);
646                // Step 9.6.1.4. Set handler to the result of invoking the
647                // Get Trusted Type compliant string algorithm with TrustedScript, global, handler, sink, and "script".
648                let code_str = TrustedScript::get_trusted_script_compliant_string(
649                    global,
650                    trusted_script_or_string,
651                    &sink,
652                    can_gc,
653                )?;
654                // Step 9.6.3. Perform EnsureCSPDoesNotBlockStringCompilation(realm, « », handler, handler, timer, « », handler).
655                // If this throws an exception, catch it, report it for global, and abort these steps.
656                if global
657                    .get_csp_list()
658                    .is_js_evaluation_allowed(global, &code_str.str())
659                {
660                    // Step 9.6.2. Assert: handler is a string.
661                    InternalTimerCallback::StringTimerCallback(code_str)
662                } else {
663                    return Ok(0);
664                }
665            },
666            TimerCallback::FunctionTimerCallback(function) => {
667                // This is a bit complicated, but this ensures that the vector's
668                // buffer isn't reallocated (and moved) after setting the Heap values
669                let mut args = Vec::with_capacity(arguments.len());
670                for _ in 0..arguments.len() {
671                    args.push(Heap::default());
672                }
673                for (i, item) in arguments.iter().enumerate() {
674                    args.get_mut(i).unwrap().set(item.get());
675                }
676                // Step 9.5. If handler is a Function, then invoke handler given arguments and "report",
677                // and with callback this value set to thisArg.
678                InternalTimerCallback::FunctionTimerCallback(
679                    function,
680                    Rc::new(args.into_boxed_slice()),
681                )
682            },
683        };
684
685        // Step 2. If previousId was given, let id be previousId; otherwise,
686        // let id be an implementation-defined integer that is greater than zero
687        // and does not already exist in global's map of setTimeout and setInterval IDs.
688        let JsTimerHandle(new_handle) = self.next_timer_handle.get();
689        self.next_timer_handle.set(JsTimerHandle(new_handle + 1));
690
691        // Step 3. If the surrounding agent's event loop's currently running task
692        // is a task that was created by this algorithm, then let nesting level
693        // be the task's timer nesting level. Otherwise, let nesting level be 0.
694        let mut task = JsTimerTask {
695            handle: JsTimerHandle(new_handle),
696            source,
697            callback,
698            is_interval,
699            is_user_interacting: ScriptThread::is_user_interacting(),
700            nesting_level: 0,
701            duration: Duration::ZERO,
702        };
703
704        // Step 4. If timeout is less than 0, then set timeout to 0.
705        task.duration = timeout.max(Duration::ZERO);
706
707        self.initialize_and_schedule(global, task);
708
709        // Step 15. Return id.
710        Ok(new_handle)
711    }
712
713    pub(crate) fn clear_timeout_or_interval(&self, global: &GlobalScope, handle: i32) {
714        let mut active_timers = self.active_timers.borrow_mut();
715
716        if let Some(entry) = active_timers.remove(&JsTimerHandle(handle)) {
717            global.unschedule_callback(entry.oneshot_handle);
718        }
719    }
720
721    pub(crate) fn set_min_duration(&self, duration: Duration) {
722        self.min_duration.set(Some(duration));
723    }
724
725    pub(crate) fn remove_min_duration(&self) {
726        self.min_duration.set(None);
727    }
728
729    // see step 13 of https://html.spec.whatwg.org/multipage/#timer-initialisation-steps
730    fn user_agent_pad(&self, current_duration: Duration) -> Duration {
731        match self.min_duration.get() {
732            Some(min_duration) => min_duration.max(current_duration),
733            None => current_duration,
734        }
735    }
736
737    /// <https://html.spec.whatwg.org/multipage/#timer-initialisation-steps>
738    fn initialize_and_schedule(&self, global: &GlobalScope, mut task: JsTimerTask) {
739        let handle = task.handle;
740        let mut active_timers = self.active_timers.borrow_mut();
741
742        // Step 3. If the surrounding agent's event loop's currently running task
743        // is a task that was created by this algorithm, then let nesting level be
744        // the task's timer nesting level. Otherwise, let nesting level be 0.
745        let nesting_level = self.nesting_level.get();
746
747        let duration = self.user_agent_pad(clamp_duration(nesting_level, task.duration));
748        // Step 10. Increment nesting level by one.
749        // Step 11. Set task's timer nesting level to nesting level.
750        task.nesting_level = nesting_level + 1;
751
752        // Step 13. Set uniqueHandle to the result of running steps after a timeout given global,
753        // "setTimeout/setInterval", timeout, and completionStep.
754        let callback = OneshotTimerCallback::JsTimer(task);
755        let oneshot_handle = global.schedule_callback(callback, duration);
756
757        // Step 14. Set global's map of setTimeout and setInterval IDs[id] to uniqueHandle.
758        let entry = active_timers
759            .entry(handle)
760            .or_insert(JsTimerEntry { oneshot_handle });
761        entry.oneshot_handle = oneshot_handle;
762    }
763}
764
765/// Step 5 of <https://html.spec.whatwg.org/multipage/#timer-initialisation-steps>
766fn clamp_duration(nesting_level: u32, unclamped: Duration) -> Duration {
767    // Step 5. If nesting level is greater than 5, and timeout is less than 4, then set timeout to 4.
768    let lower_bound_ms = if nesting_level > 5 { 4 } else { 0 };
769    let lower_bound = Duration::from_millis(lower_bound_ms);
770    lower_bound.max(unclamped)
771}
772
773impl JsTimerTask {
774    // see https://html.spec.whatwg.org/multipage/#timer-initialisation-steps
775    pub(crate) fn invoke<T: DomObject>(self, this: &T, timers: &JsTimers, can_gc: CanGc) {
776        // step 9.2 can be ignored, because we proactively prevent execution
777        // of this task when its scheduled execution is canceled.
778
779        // prep for step ? in nested set_timeout_or_interval calls
780        timers.nesting_level.set(self.nesting_level);
781
782        let _guard = ScriptThread::user_interacting_guard();
783        match self.callback {
784            InternalTimerCallback::StringTimerCallback(ref code_str) => {
785                // Step 6.4. Let settings object be global's relevant settings object.
786                // Step 6. Let realm be global's relevant realm.
787                let global = this.global();
788                // Step 7. Let initiating script be the active script.
789                let cx = GlobalScope::get_cx();
790                // Step 9.6.7. If initiating script is not null, then:
791                rooted!(in(*cx) let mut rval = UndefinedValue());
792                // Step 9.6.7.1. Set fetch options to a script fetch options whose cryptographic nonce
793                // is initiating script's fetch options's cryptographic nonce,
794                // integrity metadata is the empty string, parser metadata is "not-parser-inserted",
795                // credentials mode is initiating script's fetch options's credentials mode,
796                // referrer policy is initiating script's fetch options's referrer policy,
797                // and fetch priority is "auto".
798                // Step 9.6.8. Let script be the result of creating a classic script given handler,
799                // settings object, base URL, and fetch options.
800                // Step 9.6.9. Run the classic script script.
801                //
802                // FIXME(cybai): Use base url properly by saving private reference for timers (#27260)
803                _ = global.evaluate_js_on_global_with_result(
804                    &code_str.str(),
805                    rval.handle_mut(),
806                    ScriptFetchOptions::default_classic_script(&global),
807                    // Step 9.6. Let base URL be settings object's API base URL.
808                    // Step 9.7.2. Set base URL to initiating script's base URL.
809                    global.api_base_url(),
810                    can_gc,
811                    Some(IntroductionType::DOM_TIMER),
812                );
813            },
814            // Step 9.5. If handler is a Function, then invoke handler given arguments and
815            // "report", and with callback this value set to thisArg.
816            InternalTimerCallback::FunctionTimerCallback(ref function, ref arguments) => {
817                let arguments = self.collect_heap_args(arguments);
818                rooted!(in(*GlobalScope::get_cx()) let mut value: JSVal);
819                let _ = function.Call_(this, arguments, value.handle_mut(), Report, can_gc);
820            },
821        };
822
823        // reset nesting level (see above)
824        timers.nesting_level.set(0);
825
826        // Step 9.9. If repeat is true, then perform the timer initialization steps again,
827        // given global, handler, timeout, arguments, true, and id.
828        //
829        // Since we choose proactively prevent execution (see 4.1 above), we must only
830        // reschedule repeating timers when they were not canceled as part of step 4.2.
831        if self.is_interval == IsInterval::Interval &&
832            timers.active_timers.borrow().contains_key(&self.handle)
833        {
834            timers.initialize_and_schedule(&this.global(), self);
835        }
836    }
837
838    fn collect_heap_args<'b>(&self, args: &'b [Heap<JSVal>]) -> Vec<HandleValue<'b>> {
839        args.iter().map(|arg| arg.as_handle_value()).collect()
840    }
841}
842
843/// Describes the source that requested the [`TimerEvent`].
844#[derive(Clone, Copy, Debug, Deserialize, MallocSizeOf, Serialize)]
845pub enum TimerSource {
846    /// The event was requested from a window (`ScriptThread`).
847    FromWindow(PipelineId),
848    /// The event was requested from a worker (`DedicatedGlobalWorkerScope`).
849    FromWorker,
850}
851
852/// The id to be used for a [`TimerEvent`] is defined by the corresponding [`TimerEventRequest`].
853#[derive(Clone, Copy, Debug, Deserialize, Eq, MallocSizeOf, PartialEq, Serialize)]
854pub struct TimerEventId(pub u32);
855
856/// A notification that a timer has fired. [`TimerSource`] must be `FromWindow` when
857/// dispatched to `ScriptThread` and must be `FromWorker` when dispatched to a
858/// `DedicatedGlobalWorkerScope`
859#[derive(Clone, Copy, Debug, Deserialize, Serialize)]
860pub struct TimerEvent(pub TimerSource, pub TimerEventId);
861
862/// A wrapper between timer events coming in over IPC, and the event-loop.
863#[derive(Clone)]
864struct TimerListener {
865    task_source: SendableTaskSource,
866    context: Trusted<GlobalScope>,
867    source: TimerSource,
868    id: TimerEventId,
869}
870
871impl TimerListener {
872    /// Handle a timer-event coming from the [`timers::TimerScheduler`]
873    /// by queuing the appropriate task on the relevant event-loop.
874    /// <https://html.spec.whatwg.org/multipage/#timer-initialisation-steps>
875    fn handle(&self, event: TimerEvent) {
876        let context = self.context.clone();
877        // Step 9. Let task be a task that runs the following substeps:
878        self.task_source.queue(task!(timer_event: move || {
879                let global = context.root();
880                let TimerEvent(source, id) = event;
881                match source {
882                    TimerSource::FromWorker => {
883                        global.downcast::<WorkerGlobalScope>().expect("Window timer delivered to worker");
884                    },
885                    TimerSource::FromWindow(pipeline) => {
886                        assert_eq!(pipeline, global.pipeline_id());
887                        global.downcast::<Window>().expect("Worker timer delivered to window");
888                    },
889                };
890                global.fire_timer(id, CanGc::note());
891            })
892        );
893    }
894
895    fn into_callback(self) -> BoxedTimerCallback {
896        let timer_event = TimerEvent(self.source, self.id);
897        Box::new(move || self.handle(timer_event))
898    }
899}