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