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