Skip to main content

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