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