Skip to main content

script/dom/worklet/
worklet.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//! An implementation of Houdini worklets.
6//!
7//! The goal of this implementation is to maximize responsiveness of worklets,
8//! and in particular to ensure that the thread performing worklet tasks
9//! is never busy GCing or loading worklet code. We do this by providing a custom
10//! thread pool implementation, which only performs GC or code loading on
11//! a backup thread, not on the primary worklet thread.
12
13#![cfg_attr(crown, allow(crown::jscontext_first_arg))]
14
15use std::cell::{self, Cell, RefCell, RefMut};
16use std::cmp::max;
17use std::collections::hash_map;
18use std::rc::Rc;
19use std::sync::Arc;
20use std::sync::atomic::{AtomicBool, AtomicIsize, Ordering};
21use std::thread;
22
23use crossbeam_channel::{Receiver, SendError, Sender, unbounded};
24use dom_struct::dom_struct;
25use js::context::JSContext;
26use js::jsapi::{GCReason, JSGCParamKey, JSTracer};
27use js::realm::CurrentRealm;
28use js::rust::wrappers2::{JS_GC, JS_GetGCParameter};
29use malloc_size_of::malloc_size_of_is_0;
30use net_traits::policy_container::PolicyContainer;
31use net_traits::request::{Destination, Origin, PreloadedResources, RequestClient};
32use rustc_hash::FxHashMap;
33use script_bindings::reflector::{Reflector, reflect_dom_object};
34use servo_base::id::PipelineId;
35use servo_url::{ImmutableOrigin, ServoUrl};
36use style::thread_state::{self, ThreadState};
37use swapper::{Swapper, swapper};
38use uuid::Uuid;
39
40use crate::conversions::Convert;
41use crate::dom::RootedPromise;
42use crate::dom::bindings::codegen::Bindings::RequestBinding::RequestCredentials;
43use crate::dom::bindings::codegen::Bindings::WindowBinding::Window_Binding::WindowMethods;
44use crate::dom::bindings::codegen::Bindings::WorkletBinding::{WorkletMethods, WorkletOptions};
45use crate::dom::bindings::error::Error;
46use crate::dom::bindings::inheritance::Castable;
47use crate::dom::bindings::refcounted::TrustedPromise;
48use crate::dom::bindings::root::{Dom, DomRoot};
49use crate::dom::bindings::str::USVString;
50use crate::dom::bindings::trace::{JSTraceable, RootedTraceableBox};
51use crate::dom::globalscope::GlobalScope;
52use crate::dom::promise::Promise;
53use crate::dom::window::Window;
54use crate::dom::workletglobalscope::{
55    WorkletGlobalScope, WorkletGlobalScopeInit, WorkletGlobalScopeType,
56};
57use crate::messaging::{CommonScriptMsg, MainThreadScriptMsg, ScriptEventLoopSender};
58use crate::modules::script_module::fetch_a_module_script_graph;
59use crate::realms::enter_auto_realm;
60use crate::runtime::script_runtime::{IntroductionType, Runtime, ScriptThreadEventCategory};
61use crate::tasks::task_source::TaskSourceName;
62use crate::url::ensure_blob_referenced_by_url_is_kept_alive;
63
64// Magic numbers
65const WORKLET_THREAD_POOL_SIZE: u32 = 3;
66const MIN_GC_THRESHOLD: u32 = 1_000_000;
67
68type LazyCellWithBoxedInitializer<T> = cell::LazyCell<T, Box<dyn FnOnce() -> T>>;
69
70#[derive(JSTraceable, MallocSizeOf)]
71struct DroppableField {
72    worklet_id: WorkletId,
73    /// The cached version of the script thread's WorkletThreadPool. We keep this cached
74    /// because we may need to access it after the script thread has terminated.
75    /// NOTE: Do not access the `thread_pool` field directly, instead use the
76    /// `Worklet::worklet_thread_pool` method to access the Thread Pool.
77    #[ignore_malloc_size_of = "Difficult to measure memory usage of Rc<...> types"]
78    thread_pool: LazyCellWithBoxedInitializer<Rc<dyn WorkletThreadPool>>,
79
80    /// NOTE: The `is_thread_pool_initialized` field is a temporary workaround because
81    /// using the `LazyCell::get()` method requires Rust version >1.94.0 and is not
82    /// supported by the current MSRV (Minimum Supported Rust Version).
83    is_thread_pool_initialized: Cell<bool>,
84}
85
86impl Drop for DroppableField {
87    fn drop(&mut self) {
88        let worklet_id = self.worklet_id;
89        if self.is_thread_pool_initialized.get() {
90            self.thread_pool.exit_worklet(worklet_id);
91        }
92    }
93}
94
95#[dom_struct]
96/// <https://drafts.css-houdini.org/worklets/#worklet>
97pub(crate) struct Worklet {
98    reflector: Reflector,
99    window: Dom<Window>,
100    global_type: WorkletGlobalScopeType,
101    droppable_field: DroppableField,
102}
103
104impl Worklet {
105    fn new_inherited(
106        window: &Window,
107        global_type: WorkletGlobalScopeType,
108        thread_pool_constructor: Box<dyn FnOnce() -> Rc<dyn WorkletThreadPool>>,
109    ) -> Worklet {
110        Worklet {
111            reflector: Reflector::new(),
112            window: Dom::from_ref(window),
113            global_type,
114            droppable_field: DroppableField {
115                worklet_id: WorkletId::new(),
116                thread_pool: LazyCellWithBoxedInitializer::new(thread_pool_constructor),
117                is_thread_pool_initialized: Cell::new(false),
118            },
119        }
120    }
121
122    pub(crate) fn new(
123        cx: &mut JSContext,
124        window: &Window,
125        global_type: WorkletGlobalScopeType,
126        thread_pool_constructor: Box<dyn FnOnce() -> Rc<dyn WorkletThreadPool>>,
127    ) -> DomRoot<Worklet> {
128        debug!("Creating worklet {:?}.", global_type);
129        reflect_dom_object(
130            cx,
131            Box::new(Worklet::new_inherited(
132                window,
133                global_type,
134                thread_pool_constructor,
135            )),
136            window,
137        )
138    }
139
140    pub(crate) fn worklet_thread_pool(&self) -> Rc<dyn WorkletThreadPool> {
141        self.droppable_field.is_thread_pool_initialized.set(true);
142        self.droppable_field.thread_pool.clone()
143    }
144
145    #[cfg(feature = "testbinding")]
146    pub(crate) fn worklet_id(&self) -> WorkletId {
147        self.droppable_field.worklet_id
148    }
149
150    #[expect(dead_code)]
151    pub(crate) fn worklet_global_scope_type(&self) -> WorkletGlobalScopeType {
152        self.global_type
153    }
154}
155
156impl WorkletMethods<crate::DomTypeHolder> for Worklet {
157    /// <https://html.spec.whatwg.org/multipage/#dom-worklet-addmodule>
158    fn AddModule(
159        &self,
160        realm: &mut CurrentRealm,
161        module_url: USVString,
162        options: &WorkletOptions,
163    ) -> RootedPromise {
164        let promise = Promise::new_in_realm_rooted(realm);
165
166        // Step 1. Let outsideSettings be the relevant settings object of this.
167        // Step 2. Let moduleURLRecord be the result of encoding-parsing a URL given moduleURL, relative to outsideSettings.
168        let module_url_record = match self.window.Document().base_url().join(&module_url.0) {
169            Ok(url) => url,
170            Err(err) => {
171                // Step 3. If moduleURLRecord is failure, then return a promise rejected with a "SyntaxError" DOMException.
172                debug!("URL {:?} parse error {:?}.", module_url.0, err);
173                promise.reject_error(realm, Error::Syntax(None));
174
175                return promise;
176            },
177        };
178        debug!("Adding Worklet module {}.", module_url_record);
179
180        let global_scope = self.window.as_global_scope();
181
182        let pending_tasks_struct = PendingTasksStruct::new();
183
184        // NOTE: The following steps are split between `WorkletThread::get_worklet_global_scope` and `WorkledThread::fetch_and_invoke_a_worklet_script` methods:
185        // Step 5. Let workletInstance be this.
186        // Step 6. Run the following steps in parallel:
187        // Step 6.1. If workletInstance's global scopes is empty:
188        // Step 6.1.1. Create a worklet global scope given workletInstance.
189        // Step 6.1.2. Optionally, create additional global scope instances given workletInstance, depending on the specific worklet in question and its specification.
190        // Step 6.1.3. Wait for all steps of the creation process(es) — including those taking place within the worklet agents — to complete, before moving on.
191        // Step 6.2. Let pendingTasks be workletInstance's global scopes's size.
192
193        // Step 6.3. Let addedSuccessfully be false.
194        // NOTE: We skip step 6.3 because we do not implement the `added modules list` yet
195        // <https://html.spec.whatwg.org/multipage/#concept-worklet-added-modules-list>
196
197        self.worklet_thread_pool()
198            .fetch_and_invoke_a_worklet_script(
199                self.window.pipeline_id(),
200                self.droppable_field.worklet_id,
201                self.global_type,
202                self.window.origin().immutable().clone(),
203                global_scope.api_base_url(),
204                module_url_record,
205                global_scope.policy_container(),
206                options.credentials,
207                pending_tasks_struct,
208                &promise,
209                global_scope.inherited_secure_context(),
210            );
211
212        // Step 7. Return promise
213        debug!("Returning promise.");
214        promise
215    }
216}
217
218/// A guid for worklets.
219#[derive(Clone, Copy, Debug, Eq, Hash, JSTraceable, PartialEq)]
220pub(crate) struct WorkletId(#[no_trace] Uuid);
221
222malloc_size_of_is_0!(WorkletId);
223
224impl WorkletId {
225    fn new() -> WorkletId {
226        WorkletId(Uuid::new_v4())
227    }
228}
229
230/// <https://drafts.css-houdini.org/worklets/#pending-tasks-struct>
231#[derive(Clone, Debug)]
232pub(crate) struct PendingTasksStruct(Arc<AtomicIsize>);
233
234impl PendingTasksStruct {
235    fn new() -> PendingTasksStruct {
236        PendingTasksStruct(Arc::new(AtomicIsize::new(
237            WORKLET_THREAD_POOL_SIZE as isize,
238        )))
239    }
240
241    fn set_counter_to(&self, value: isize) -> isize {
242        self.0.swap(value, Ordering::AcqRel)
243    }
244
245    fn decrement_counter_by(&self, offset: isize) -> isize {
246        self.0.fetch_sub(offset, Ordering::AcqRel)
247    }
248}
249
250pub trait WorkletThreadPool: JSTraceable {
251    /// Loads a worklet module into every thread in this thread pool.
252    /// If all of the threads load successfully, the promise is resolved.
253    /// If any of the threads fails to load, the promise is rejected.
254    /// NOTE: The method implements the Step 6 of AddModule
255    /// <https://html.spec.whatwg.org/multipage/#dom-worklet-addmodule>
256    #[allow(clippy::too_many_arguments)]
257    fn fetch_and_invoke_a_worklet_script(
258        &self,
259        pipeline_id: PipelineId,
260        worklet_id: WorkletId,
261        global_type: WorkletGlobalScopeType,
262        origin: ImmutableOrigin,
263        base_url: ServoUrl,
264        script_url: ServoUrl,
265        policy_container: PolicyContainer,
266        credentials: RequestCredentials,
267        pending_tasks_struct: PendingTasksStruct,
268        promise: &RootedPromise,
269        inherited_secure_context: Option<bool>,
270    );
271    /// Request that the [`WorkletGlobalScope`] associated with the [`WorkletId`]
272    /// be removed from all the threads in the thread pool.
273    fn exit_worklet(&self, worklet_id: WorkletId);
274    /// Signal all the threads in the pool that there may be control messages to
275    /// process.
276    fn wake_threads(&self);
277    /// Queue a [`WorkletTask`] for execution on this [`WorkletThreadPool`].
278    /// The task will be executed in the context of the [`WorkletGlobalScope`]
279    /// represented by the [`WorketId`].
280    fn perform_a_worklet_task(&self, worklet_id: WorkletId, worklet_task: WorkletTask);
281}
282
283/// The `StatelessWorkletThreadPool` executes the associated
284/// [`WorkletTask`]s in a dedicated thread pool with the assumption that
285/// the tasks are idempotent. This is useful for the paint worklet, for
286/// example.
287///
288/// The goal is to ensure that there is a primary worklet thread,
289/// which is able to responsively execute worklet code. In particular,
290/// worklet execution should not be delayed by GC, or by script
291/// loading.
292///
293/// To achieve this, we implement a three-thread pool, with the
294/// threads cycling between three thread roles:
295///
296///  * The primary worklet thread is the one available to execute
297///    worklet code.
298///
299///  * The hot backup thread may peform GC, but otherwise is expected
300///    to take over the primary role.
301///
302///  * The cold backup thread may peform script loading and other
303///    long-running tasks.
304///
305/// In the implementation, we use two kinds of messages:
306///
307///  * Data messages are expected to be processed quickly, and include
308///    the worklet tasks to be performed by the primary thread, as
309///    well as requests to change role or quit execution.
310///
311///  * Control messages are expected to be processed more slowly, and
312///    include script loading.
313///
314/// Data messages are targeted at a role, for example, task execution
315/// is expected to be performed by whichever thread is currently
316/// primary. Control messages are targeted at a thread, for example
317/// adding a module is performed in every thread, even if they change roles
318/// in the middle of module loading.
319///
320/// The thread pool lives in the script thread, and is initialized
321/// when a worklet adds a module. It is dropped when the script thread
322/// is dropped, and asks each of the worklet threads to quit.
323///
324/// Layout can end up blocking on the primary worklet thread
325/// (e.g. when invoking a paint callback), so it is important to avoid
326/// deadlock by making sure the primary worklet thread doesn't end up
327/// blocking waiting on layout. In particular, since the constellation
328/// can block waiting on layout, this means the primary worklet thread
329/// can't block waiting on the constellation. In general, the primary
330/// worklet thread shouldn't perform any blocking operations. If a worklet
331/// thread needs to do anything blocking, it should send a control
332/// message, to make sure that the blocking operation is performed
333/// by a backup thread, not by the primary thread.
334
335#[derive(Clone, JSTraceable)]
336pub(crate) struct StatelessWorkletThreadPool {
337    // Channels to send data messages to the three roles.
338    #[no_trace]
339    primary_sender: Sender<WorkletData>,
340    #[no_trace]
341    hot_backup_sender: Sender<WorkletData>,
342    #[no_trace]
343    cold_backup_sender: Sender<WorkletData>,
344    // Channels to send control messages to the three threads.
345    #[no_trace]
346    control_sender_0: Sender<WorkletControl>,
347    #[no_trace]
348    control_sender_1: Sender<WorkletControl>,
349    #[no_trace]
350    control_sender_2: Sender<WorkletControl>,
351}
352
353impl Drop for StatelessWorkletThreadPool {
354    fn drop(&mut self) {
355        let _ = self.cold_backup_sender.send(WorkletData::Quit);
356        let _ = self.hot_backup_sender.send(WorkletData::Quit);
357        let _ = self.primary_sender.send(WorkletData::Quit);
358    }
359}
360
361impl StatelessWorkletThreadPool {
362    /// Create a new thread pool and spawn the threads.
363    /// When the thread pool is dropped, the threads will be asked to quit.
364    pub(crate) fn spawn(global_init: WorkletGlobalScopeInit) -> StatelessWorkletThreadPool {
365        let primary_role = WorkletThreadRole::new(false, false);
366        let hot_backup_role = WorkletThreadRole::new(true, false);
367        let cold_backup_role = WorkletThreadRole::new(false, true);
368        let primary_sender = primary_role.sender.clone();
369        let hot_backup_sender = hot_backup_role.sender.clone();
370        let cold_backup_sender = cold_backup_role.sender.clone();
371        let init = WorkletThreadInit {
372            primary_sender: primary_sender.clone(),
373            hot_backup_sender: hot_backup_sender.clone(),
374            cold_backup_sender: cold_backup_sender.clone(),
375            global_init,
376        };
377        StatelessWorkletThreadPool {
378            primary_sender,
379            hot_backup_sender,
380            cold_backup_sender,
381            control_sender_0: WorkletThread::spawn(primary_role, init.clone(), 0),
382            control_sender_1: WorkletThread::spawn(hot_backup_role, init.clone(), 1),
383            control_sender_2: WorkletThread::spawn(cold_backup_role, init, 2),
384        }
385    }
386}
387
388impl WorkletThreadPool for StatelessWorkletThreadPool {
389    #[allow(clippy::too_many_arguments)]
390    fn fetch_and_invoke_a_worklet_script(
391        &self,
392        pipeline_id: PipelineId,
393        worklet_id: WorkletId,
394        global_type: WorkletGlobalScopeType,
395        origin: ImmutableOrigin,
396        base_url: ServoUrl,
397        script_url: ServoUrl,
398        policy_container: PolicyContainer,
399        credentials: RequestCredentials,
400        pending_tasks_struct: PendingTasksStruct,
401        promise: &RootedPromise,
402        inherited_secure_context: Option<bool>,
403    ) {
404        // Send each thread a control message asking it to load the script.
405        for sender in &[
406            &self.control_sender_0,
407            &self.control_sender_1,
408            &self.control_sender_2,
409        ] {
410            let _ = sender.send(WorkletControl::FetchAndInvokeAWorkletScript {
411                pipeline_id,
412                worklet_id,
413                global_type,
414                origin: origin.clone(),
415                base_url: base_url.clone(),
416                script_url: script_url.clone(),
417                policy_container: policy_container.clone(),
418                credentials,
419                pending_tasks_struct: pending_tasks_struct.clone(),
420                promise: TrustedPromise::from(promise),
421                inherited_secure_context,
422            });
423        }
424        self.wake_threads();
425    }
426
427    fn exit_worklet(&self, worklet_id: WorkletId) {
428        for sender in &[
429            &self.control_sender_0,
430            &self.control_sender_1,
431            &self.control_sender_2,
432        ] {
433            let _ = sender.send(WorkletControl::ExitWorklet(worklet_id));
434        }
435        self.wake_threads();
436    }
437
438    fn wake_threads(&self) {
439        // If any of the threads are blocked waiting on data, wake them up.
440        let _ = self.cold_backup_sender.send(WorkletData::WakeUp);
441        let _ = self.hot_backup_sender.send(WorkletData::WakeUp);
442        let _ = self.primary_sender.send(WorkletData::WakeUp);
443    }
444
445    /// Queue the [`WorkletTask`] for execution on the primary thread.
446    fn perform_a_worklet_task(&self, worklet_id: WorkletId, worklet_task: WorkletTask) {
447        let msg = WorkletData::Task(worklet_id, worklet_task);
448        let _ = self.primary_sender.send(msg);
449    }
450}
451
452/// A task which can be performed in the context of a [`WorkletGlobalScope`].
453type WorkletTask = Box<dyn FnOnce(&mut JSContext, &WorkletGlobalScope) + Send>;
454
455/// The data messages sent to worklet threads
456enum WorkletData {
457    Task(WorkletId, WorkletTask),
458    StartSwapRoles(Sender<WorkletData>),
459    FinishSwapRoles(Swapper<WorkletThreadRole>),
460    WakeUp,
461    Quit,
462}
463
464/// The control message sent to worklet threads
465pub(crate) enum WorkletControl {
466    ExitWorklet(WorkletId),
467    FetchAndInvokeAWorkletScript {
468        pipeline_id: PipelineId,
469        worklet_id: WorkletId,
470        global_type: WorkletGlobalScopeType,
471        origin: ImmutableOrigin,
472        base_url: ServoUrl,
473        script_url: ServoUrl,
474        policy_container: PolicyContainer,
475        credentials: RequestCredentials,
476        pending_tasks_struct: PendingTasksStruct,
477        promise: TrustedPromise,
478        inherited_secure_context: Option<bool>,
479    },
480    Common(CommonScriptMsg),
481}
482
483/// A role that a worklet thread can be playing.
484///
485/// These roles are used as tokens or capabilities, we track unique
486/// ownership using Rust's types, and use atomic swapping to exchange
487/// them between worklet threads. This ensures that each thread pool has
488/// exactly one primary, one hot backup and one cold backup.
489struct WorkletThreadRole {
490    receiver: Receiver<WorkletData>,
491    sender: Sender<WorkletData>,
492    is_hot_backup: bool,
493    is_cold_backup: bool,
494}
495
496impl WorkletThreadRole {
497    fn new(is_hot_backup: bool, is_cold_backup: bool) -> WorkletThreadRole {
498        let (sender, receiver) = unbounded();
499        WorkletThreadRole {
500            sender,
501            receiver,
502            is_hot_backup,
503            is_cold_backup,
504        }
505    }
506}
507
508/// Data to initialize a worklet thread.
509#[derive(Clone)]
510struct WorkletThreadInit {
511    /// Senders
512    primary_sender: Sender<WorkletData>,
513    hot_backup_sender: Sender<WorkletData>,
514    cold_backup_sender: Sender<WorkletData>,
515
516    /// Data for initializing new worklet global scopes
517    global_init: WorkletGlobalScopeInit,
518}
519
520/// A thread for executing worklets.
521#[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)]
522struct WorkletThread {
523    /// Which role the thread is currently playing
524    role: WorkletThreadRole,
525
526    /// The thread's receiver for control messages
527    control_receiver: Receiver<WorkletControl>,
528    /// The sender for sending control messages to this thread's event loop
529    control_sender: Sender<WorkletControl>,
530
531    /// Senders
532    primary_sender: Sender<WorkletData>,
533    hot_backup_sender: Sender<WorkletData>,
534    cold_backup_sender: Sender<WorkletData>,
535
536    /// Data for initializing new worklet global scopes
537    global_init: WorkletGlobalScopeInit,
538
539    /// The global scopes created by this thread
540    global_scopes: FxHashMap<WorkletId, Dom<WorkletGlobalScope>>,
541
542    /// A one-place buffer for control messages
543    control_buffer: Option<WorkletControl>,
544
545    /// A flag that is set when a `WorkletThread` begins shutting down.
546    closing: Arc<AtomicBool>,
547
548    /// The JS runtime
549    runtime: Runtime,
550    should_gc: bool,
551    gc_threshold: u32,
552}
553
554#[expect(unsafe_code)]
555unsafe impl JSTraceable for WorkletThread {
556    unsafe fn trace(&self, trc: *mut JSTracer) {
557        debug!("Tracing worklet thread.");
558        unsafe { self.global_scopes.trace(trc) };
559    }
560}
561
562impl WorkletThread {
563    #[allow(unsafe_code)]
564    /// Spawn a new worklet thread, returning the channel to send it control messages.
565    fn spawn(
566        role: WorkletThreadRole,
567        init: WorkletThreadInit,
568        thread_index: u8,
569    ) -> Sender<WorkletControl> {
570        let (control_sender, control_receiver) = unbounded();
571        let control_sender_clone = control_sender.clone();
572        let _ = thread::Builder::new()
573            .name(format!("Worklet#{thread_index}"))
574            .spawn(move || {
575                // TODO: add a new IN_WORKLET thread state?
576                // TODO: set interrupt handler?
577                // TODO: configure the JS runtime (e.g. discourage GC, encourage agressive JIT)
578                debug!("Initializing worklet thread.");
579                thread_state::initialize(ThreadState::SCRIPT | ThreadState::IN_WORKER);
580                let runtime = Runtime::new(None);
581                let mut cx = unsafe { runtime.cx() };
582                let mut thread = RootedTraceableBox::new(WorkletThread {
583                    role,
584                    control_receiver,
585                    control_sender: control_sender_clone,
586                    primary_sender: init.primary_sender,
587                    hot_backup_sender: init.hot_backup_sender,
588                    cold_backup_sender: init.cold_backup_sender,
589                    global_init: init.global_init,
590                    global_scopes: FxHashMap::default(),
591                    control_buffer: None,
592                    runtime,
593                    should_gc: false,
594                    closing: Arc::new(AtomicBool::new(false)),
595                    gc_threshold: MIN_GC_THRESHOLD,
596                });
597                thread.run(&mut cx);
598            })
599            .expect("Couldn't start worklet thread");
600        control_sender
601    }
602
603    /// The main event loop for a worklet thread
604    fn run(&mut self, cx: &mut JSContext) {
605        loop {
606            // The handler for data messages
607            let message = self.role.receiver.recv().unwrap();
608            match message {
609                // The whole point of this thread pool is to perform tasks!
610                WorkletData::Task(id, task) => {
611                    self.perform_a_worklet_task(cx, id, task);
612                },
613                // To start swapping roles, get ready to perform an atomic swap,
614                // and block waiting for the other end to finish it.
615                // NOTE: the cold backup can block on the primary or the hot backup;
616                //       the hot backup can block on the primary;
617                //       the primary can block on nothing;
618                //       this total ordering on thread roles is what guarantees deadlock-freedom.
619                WorkletData::StartSwapRoles(sender) => {
620                    let (our_swapper, their_swapper) = swapper();
621                    match sender.send(WorkletData::FinishSwapRoles(their_swapper)) {
622                        Ok(_) => {},
623                        Err(_) => {
624                            // This might happen if the script thread shuts down while
625                            // waiting for the worklet to finish.
626                            return;
627                        },
628                    };
629                    let _ = our_swapper.swap(&mut self.role);
630                },
631                // To finish swapping roles, perform the atomic swap.
632                // The other end should have already started the swap, so this shouldn't block.
633                WorkletData::FinishSwapRoles(swapper) => {
634                    let _ = swapper.swap(&mut self.role);
635                },
636                // Wake up! There may be control messages to process.
637                WorkletData::WakeUp => {},
638                // Quit!
639                WorkletData::Quit => {
640                    return;
641                },
642            }
643
644            // Only process control messages if we're the cold backup,
645            // otherwise if there are outstanding control messages,
646            // try to become the cold backup.
647            if self.role.is_cold_backup {
648                if let Some(control) = self.control_buffer.take() {
649                    self.process_control(control, cx);
650                }
651                while let Ok(control) = self.control_receiver.try_recv() {
652                    self.process_control(control, cx);
653                }
654
655                for worklet_global_scope in self.global_scopes.values() {
656                    worklet_global_scope.perform_a_microtask_checkpoint(cx);
657                }
658
659                self.gc(cx);
660            } else if self.control_buffer.is_none() &&
661                let Ok(control) = self.control_receiver.try_recv()
662            {
663                self.control_buffer = Some(control);
664                let msg = WorkletData::StartSwapRoles(self.role.sender.clone());
665                let _ = self.cold_backup_sender.send(msg);
666            }
667            // If we are tight on memory, and we're a backup then perform a gc.
668            // If we are tight on memory, and we're the primary then try to become the hot backup.
669            // Hopefully this happens soon!
670            if self.current_memory_usage() > self.gc_threshold {
671                if self.role.is_hot_backup || self.role.is_cold_backup {
672                    self.should_gc = false;
673                    self.gc(cx);
674                } else if !self.should_gc {
675                    self.should_gc = true;
676                    let msg = WorkletData::StartSwapRoles(self.role.sender.clone());
677                    let _ = self.hot_backup_sender.send(msg);
678                }
679            }
680        }
681    }
682
683    /// The current memory usage of the thread
684    #[expect(unsafe_code)]
685    fn current_memory_usage(&self) -> u32 {
686        unsafe { JS_GetGCParameter(self.runtime.cx_no_gc(), JSGCParamKey::JSGC_BYTES) }
687    }
688
689    /// Perform a GC.
690    #[expect(unsafe_code)]
691    fn gc(&mut self, cx: &mut JSContext) {
692        debug!(
693            "BEGIN GC (usage = {}, threshold = {}).",
694            self.current_memory_usage(),
695            self.gc_threshold
696        );
697        unsafe { JS_GC(cx, GCReason::API) };
698        self.gc_threshold = max(MIN_GC_THRESHOLD, self.current_memory_usage() * 2);
699        debug!(
700            "END GC (usage = {}, threshold = {}).",
701            self.current_memory_usage(),
702            self.gc_threshold
703        );
704    }
705
706    /// Get the worklet global scope for a given worklet.
707    /// Creates the worklet global scope if it doesn't exist.
708    fn get_worklet_global_scope(
709        &mut self,
710        cx: &mut JSContext,
711        pipeline_id: PipelineId,
712        worklet_id: WorkletId,
713        inherited_secure_context: Option<bool>,
714        global_type: WorkletGlobalScopeType,
715        base_url: ServoUrl,
716    ) -> DomRoot<WorkletGlobalScope> {
717        match self.global_scopes.entry(worklet_id) {
718            hash_map::Entry::Occupied(entry) => DomRoot::from_ref(entry.get()),
719
720            // Step 6.1. If workletInstance's global scopes is empty:
721            hash_map::Entry::Vacant(entry) => {
722                debug!("Creating new worklet global scope.");
723
724                // Step 6.1.1. Create a worklet global scope given workletInstance.
725                let executor = WorkletExecutor {
726                    worklet_id,
727                    primary_sender: self.primary_sender.clone(),
728                    hot_backup_sender: self.hot_backup_sender.clone(),
729                    cold_backup_sender: self.cold_backup_sender.clone(),
730                    control_sender: self.control_sender.clone(),
731                };
732
733                let result = WorkletGlobalScope::new(
734                    global_type,
735                    pipeline_id,
736                    base_url,
737                    inherited_secure_context,
738                    executor,
739                    &self.global_init,
740                    cx,
741                    self.closing.clone(),
742                );
743                entry.insert(Dom::from_ref(&*result));
744                result
745            },
746        }
747    }
748
749    /// Fetch and invoke a worklet script.
750    /// <https://html.spec.whatwg.org/multipage/#fetch-a-worklet-script-graph>
751    #[allow(clippy::too_many_arguments)]
752    fn fetch_and_invoke_a_worklet_script(
753        &self,
754        global_scope: &WorkletGlobalScope,
755        pipeline_id: PipelineId,
756        origin: ImmutableOrigin,
757        script_url: ServoUrl,
758        policy_container: PolicyContainer,
759        credentials: RequestCredentials,
760        pending_tasks_struct: PendingTasksStruct,
761        promise: TrustedPromise,
762        cx: &mut JSContext,
763    ) {
764        debug!("Fetching from {}.", script_url);
765        // TODO: Settings object?
766
767        // TODO: Fetch the script asynchronously?
768        // TODO: Caching.
769        let global = global_scope.upcast::<GlobalScope>();
770
771        // Step 1. Let requestURL be request's URL.
772        let request_client = RequestClient {
773            preloaded_resources: PreloadedResources::default(),
774            policy_container,
775            origin: Origin::Origin(origin),
776            is_nested_browsing_context: global.is_nested_browsing_context(),
777            insecure_requests_policy: global.insecure_requests_policy(),
778            has_trustworthy_ancestor_origin: global.has_trustworthy_ancestor_origin(),
779        };
780
781        // Step 2. If moduleResponsesMap[requestURL] is "fetching", wait in parallel until that entry's value changes, then queue a task on the networking task source to proceed with running the following steps.
782        // NOTE: We do not perform the Step 2 because Worklet currently does not implement a `module responses map`
783        // <https://html.spec.whatwg.org/multipage/#concept-worklet-module-responses-map>
784
785        // `fetch_a_module_script_graph` requires the `on_complete` closure to be cloneable
786        // therefore, we wrap the TrustedPromise in an Rc to make it cloneable and RefCell allows calling `reject_task` and `resolve_task`
787        let promise_task = Rc::new(RefCell::new(Some(promise)));
788        let script_thread_sender = self.global_init.to_script_thread_sender.clone();
789        let rooted_global = DomRoot::from_ref(global);
790        let script_url = ensure_blob_referenced_by_url_is_kept_alive(global, script_url);
791
792        // NOTE: We implement the rest of the steps in AddModule here
793        // <https://html.spec.whatwg.org/multipage/#dom-worklet-addmodule>
794        // Step 6.4. For each workletGlobalScope of workletInstance's global scopes,
795        // queue a global task on the networking task source given workletGlobalScope to fetch a worklet script graph given moduleURLRecord,
796        // outsideSettings, workletInstance's worklet destination type, options["credentials"], workletGlobalScope's relevant settings object,
797        // workletInstance's module responses map, and the following steps given script:
798        fetch_a_module_script_graph(
799            cx,
800            global,
801            script_url,
802            request_client,
803            Destination::PaintWorklet,
804            global.get_referrer(),
805            credentials.convert(),
806            Some(IntroductionType::WORKLET),
807            move |cx, module_tree| {
808                match module_tree {
809                    // Step 6.4.1. If script is null:
810                    None => {
811                        debug!("Failed to load script.");
812
813                        reject_promise(
814                            &pending_tasks_struct,
815                            promise_task.borrow_mut(),
816                            script_thread_sender.clone(),
817                        );
818                    },
819                    Some(script) => {
820                        let mut realm = enter_auto_realm(cx, &*rooted_global);
821                        let cx = &mut realm.current_realm();
822
823                        // Step 6.4.2. If script's error to rethrow is not null:
824                        // NOTE: The `AddModule` specification in the Step 6.4.2.1.1.2. requires the promise to be rejected with the script's "rethrow error".
825                        // However, the `JSVal` from `get_rethrow_error` cannot be used with the `promise_task` here because they are from different runtimes.
826                        // So we throw an AbortError instead.
827                        if script.get_rethrow_error().take().is_some() {
828                            // Step 6.4.2.1. and its substeps are handled by `reject_promise` function
829                            reject_promise(
830                                &pending_tasks_struct,
831                                promise_task.borrow_mut(),
832                                script_thread_sender.clone(),
833                            );
834
835                            // Step 6.4.2.2. Abort these steps.
836                            return;
837                        }
838
839                        // Step 6.4.4. Run a module script given script.
840                        rooted_global.run_a_module_script(cx, script, false);
841
842                        // NOTE: we are treating all negative values as -1
843                        // Step 6.4.5.1. If pendingTasks is not −1:
844                        // Step 6.4.5.1.1. Set pendingTasks to pendingTasks − 1.
845                        let old_counter = pending_tasks_struct.decrement_counter_by(1);
846                        // Step Step 6.4.5.1.2. If pendingTasks is 0 then, resolve promise.
847                        if old_counter == 1 {
848                            debug!("Resolving promise.");
849
850                            let msg = MainThreadScriptMsg::WorkletLoaded(pipeline_id);
851                            script_thread_sender
852                                .send(msg)
853                                .expect("Worklet thread outlived script thread.");
854
855                            let task = promise_task
856                                .borrow_mut()
857                                .take()
858                                .expect("promise_task must be consumed exactly once")
859                                .resolve_task(());
860
861                            let msg = CommonScriptMsg::Task(
862                                ScriptThreadEventCategory::WorkletEvent,
863                                Box::new(task),
864                                None,
865                                TaskSourceName::Networking,
866                            );
867
868                            // Step 6.4.5. Queue a global task on the networking task source given workletInstance's relevant global object to perform the following steps:
869                            let msg = MainThreadScriptMsg::Common(msg);
870                            script_thread_sender
871                                .send(msg)
872                                .expect("Worklet thread outlived script thread.");
873                        }
874                    },
875                }
876            },
877        );
878    }
879
880    /// Run the steps for the `WorkletTask` for a given Worklet.
881    fn perform_a_worklet_task(
882        &self,
883        cx: &mut JSContext,
884        worklet_id: WorkletId,
885        worklet_task: WorkletTask,
886    ) {
887        match self.global_scopes.get(&worklet_id) {
888            Some(global) => worklet_task(cx, global),
889            None => warn!("No such worklet as {:?}.", worklet_id),
890        }
891    }
892
893    /// Process a control message.
894    fn process_control(&mut self, control: WorkletControl, cx: &mut js::context::JSContext) {
895        match control {
896            WorkletControl::ExitWorklet(worklet_id) => {
897                self.global_scopes.remove(&worklet_id);
898            },
899            WorkletControl::FetchAndInvokeAWorkletScript {
900                pipeline_id,
901                worklet_id,
902                global_type,
903                origin,
904                base_url,
905                script_url,
906                policy_container,
907                credentials,
908                pending_tasks_struct,
909                promise,
910                inherited_secure_context,
911            } => {
912                // A worklet global scope is created here as part of the AddModule specs.
913                // <https://html.spec.whatwg.org/multipage/#dom-worklet-addmodule>
914                // 6.1.3. Wait for all steps of the creation process(es) — including those taking place within the worklet agents — to complete, before moving on.
915                let global = self.get_worklet_global_scope(
916                    cx,
917                    pipeline_id,
918                    worklet_id,
919                    inherited_secure_context,
920                    global_type,
921                    base_url,
922                );
923                self.fetch_and_invoke_a_worklet_script(
924                    &global,
925                    pipeline_id,
926                    origin,
927                    script_url,
928                    policy_container,
929                    credentials,
930                    pending_tasks_struct,
931                    promise,
932                    cx,
933                )
934            },
935            WorkletControl::Common(script_msg) => {
936                if let CommonScriptMsg::Task(_, task, _, _) = script_msg {
937                    task.run_box(cx);
938                }
939            },
940        }
941    }
942}
943
944/// This function is an abstraction of steps 6.4.1.1 and 6.4.2.1 of the `AddModule` spec
945/// <https://html.spec.whatwg.org/multipage/#dom-worklet-addmodule>
946pub(crate) fn reject_promise(
947    pending_tasks_struct: &PendingTasksStruct,
948    mut promise_task: RefMut<'_, Option<TrustedPromise>>,
949    script_thread_sender: Sender<MainThreadScriptMsg>,
950) {
951    // Step 6.4.1.1.1.1. Set pendingTasks to −1
952    let old_counter = pending_tasks_struct.set_counter_to(-1);
953
954    // 6.4.1.1.1. If pendingTasks is not −1:
955    if old_counter > 0 {
956        // 6.4.1.1.1.2. Reject promise with an "AbortError" DOMException
957        let task = promise_task
958            .take()
959            .expect("promise_task must be consumed exactly once")
960            .reject_task(Error::Abort(None));
961
962        let msg = CommonScriptMsg::Task(
963            ScriptThreadEventCategory::WorkletEvent,
964            Box::new(task),
965            None,
966            TaskSourceName::Networking,
967        );
968
969        // Step 6.4.1.1. Queue a global task on the networking task source given workletInstance's relevant global object to perform the following steps:
970        let msg = MainThreadScriptMsg::Common(msg);
971        script_thread_sender
972            .send(msg)
973            .expect("Worklet thread outlived script thread.");
974    }
975}
976
977/// An executor of worklet tasks
978#[derive(Clone, JSTraceable, MallocSizeOf)]
979pub(crate) struct WorkletExecutor {
980    worklet_id: WorkletId,
981    #[no_trace]
982    primary_sender: Sender<WorkletData>,
983    #[no_trace]
984    hot_backup_sender: Sender<WorkletData>,
985    #[no_trace]
986    cold_backup_sender: Sender<WorkletData>,
987    #[no_trace]
988    control_sender: Sender<WorkletControl>,
989}
990
991impl WorkletExecutor {
992    /// If any of the threads are blocked waiting on data, wake them up.
993    pub(crate) fn wake_threads(&self) -> Result<(), SendError<()>> {
994        self.cold_backup_sender
995            .send(WorkletData::WakeUp)
996            .map_err(|_| SendError(()))?;
997        self.hot_backup_sender
998            .send(WorkletData::WakeUp)
999            .map_err(|_| SendError(()))?;
1000        self.primary_sender
1001            .send(WorkletData::WakeUp)
1002            .map_err(|_| SendError(()))
1003    }
1004
1005    /// Schedule a worklet task to be peformed by the worklet thread pool.
1006    pub(crate) fn schedule_a_worklet_task(&self, task: WorkletTask) {
1007        let _ = self
1008            .primary_sender
1009            .send(WorkletData::Task(self.worklet_id, task));
1010    }
1011
1012    pub(crate) fn send_control_message(
1013        &self,
1014        control_message: WorkletControl,
1015    ) -> Result<(), SendError<()>> {
1016        self.control_sender
1017            .send(control_message)
1018            .map_err(|_| SendError(()))?;
1019        self.wake_threads()
1020    }
1021
1022    pub(crate) fn event_loop_sender(&self) -> ScriptEventLoopSender {
1023        ScriptEventLoopSender::Worklet(self.clone())
1024    }
1025}