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