script/dom/
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::OnceCell;
14use std::cmp::max;
15use std::collections::hash_map;
16use std::rc::Rc;
17use std::sync::Arc;
18use std::sync::atomic::{AtomicIsize, Ordering};
19use std::thread;
20
21use base::IpcSend;
22use base::id::{PipelineId, WebViewId};
23use crossbeam_channel::{Receiver, Sender, unbounded};
24use dom_struct::dom_struct;
25use js::jsapi::{GCReason, JSGCParamKey, JSTracer};
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, RequestBuilder, RequestMode};
30use rustc_hash::FxHashMap;
31use servo_url::{ImmutableOrigin, ServoUrl};
32use style::thread_state::{self, ThreadState};
33use swapper::{Swapper, swapper};
34use uuid::Uuid;
35
36use crate::conversions::Convert;
37use crate::dom::bindings::codegen::Bindings::RequestBinding::RequestCredentials;
38use crate::dom::bindings::codegen::Bindings::WindowBinding::Window_Binding::WindowMethods;
39use crate::dom::bindings::codegen::Bindings::WorkletBinding::{WorkletMethods, WorkletOptions};
40use crate::dom::bindings::error::Error;
41use crate::dom::bindings::inheritance::Castable;
42use crate::dom::bindings::refcounted::TrustedPromise;
43use crate::dom::bindings::reflector::{DomGlobal, Reflector, reflect_dom_object};
44use crate::dom::bindings::root::{Dom, DomRoot};
45use crate::dom::bindings::str::USVString;
46use crate::dom::bindings::trace::{CustomTraceable, JSTraceable, RootedTraceableBox};
47use crate::dom::csp::Violation;
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::fetch::{CspViolationsProcessor, load_whole_resource};
57use crate::messaging::{CommonScriptMsg, MainThreadScriptMsg};
58use crate::realms::InRealm;
59use crate::script_runtime::{CanGc, Runtime, ScriptThreadEventCategory};
60use crate::script_thread::ScriptThread;
61use crate::task::TaskBox;
62use crate::task_source::TaskSourceName;
63
64// Magic numbers
65const WORKLET_THREAD_POOL_SIZE: u32 = 3;
66const MIN_GC_THRESHOLD: u32 = 1_000_000;
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    #[ignore_malloc_size_of = "Difficult to measure memory usage of Rc<...> types"]
74    thread_pool: OnceCell<Rc<WorkletThreadPool>>,
75}
76
77impl Drop for DroppableField {
78    fn drop(&mut self) {
79        let worklet_id = self.worklet_id;
80        if let Some(thread_pool) = self.thread_pool.get_mut() {
81            thread_pool.exit_worklet(worklet_id);
82        }
83    }
84}
85
86#[dom_struct]
87/// <https://drafts.css-houdini.org/worklets/#worklet>
88pub(crate) struct Worklet {
89    reflector: Reflector,
90    window: Dom<Window>,
91    global_type: WorkletGlobalScopeType,
92    droppable_field: DroppableField,
93}
94
95impl Worklet {
96    fn new_inherited(window: &Window, global_type: WorkletGlobalScopeType) -> Worklet {
97        Worklet {
98            reflector: Reflector::new(),
99            window: Dom::from_ref(window),
100            global_type,
101            droppable_field: DroppableField {
102                worklet_id: WorkletId::new(),
103                thread_pool: OnceCell::new(),
104            },
105        }
106    }
107
108    pub(crate) fn new(
109        window: &Window,
110        global_type: WorkletGlobalScopeType,
111        can_gc: CanGc,
112    ) -> DomRoot<Worklet> {
113        debug!("Creating worklet {:?}.", global_type);
114        reflect_dom_object(
115            Box::new(Worklet::new_inherited(window, global_type)),
116            window,
117            can_gc,
118        )
119    }
120
121    pub(crate) fn worklet_id(&self) -> WorkletId {
122        self.droppable_field.worklet_id
123    }
124
125    #[expect(dead_code)]
126    pub(crate) fn worklet_global_scope_type(&self) -> WorkletGlobalScopeType {
127        self.global_type
128    }
129}
130
131impl WorkletMethods<crate::DomTypeHolder> for Worklet {
132    /// <https://drafts.css-houdini.org/worklets/#dom-worklet-addmodule>
133    fn AddModule(
134        &self,
135        module_url: USVString,
136        options: &WorkletOptions,
137        comp: InRealm,
138        can_gc: CanGc,
139    ) -> Rc<Promise> {
140        // Step 1.
141        let promise = Promise::new_in_current_realm(comp, can_gc);
142
143        // Step 3.
144        let module_url_record = match self.window.Document().base_url().join(&module_url.0) {
145            Ok(url) => url,
146            Err(err) => {
147                // Step 4.
148                debug!("URL {:?} parse error {:?}.", module_url.0, err);
149                promise.reject_error(Error::Syntax(None), can_gc);
150                return promise;
151            },
152        };
153        debug!("Adding Worklet module {}.", module_url_record);
154
155        // Steps 6-12 in parallel.
156        let pending_tasks_struct = PendingTasksStruct::new();
157        let global_scope = self.window.as_global_scope();
158
159        self.droppable_field
160            .thread_pool
161            .get_or_init(|| ScriptThread::worklet_thread_pool(self.global().image_cache()))
162            .fetch_and_invoke_a_worklet_script(
163                self.window.webview_id(),
164                self.window.pipeline_id(),
165                self.droppable_field.worklet_id,
166                self.global_type,
167                self.window.origin().immutable().clone(),
168                global_scope.api_base_url(),
169                module_url_record,
170                global_scope.policy_container(),
171                options.credentials,
172                pending_tasks_struct,
173                &promise,
174                global_scope.inherited_secure_context(),
175            );
176
177        // Step 5.
178        debug!("Returning promise.");
179        promise
180    }
181}
182
183/// A guid for worklets.
184#[derive(Clone, Copy, Debug, Eq, Hash, JSTraceable, PartialEq)]
185pub(crate) struct WorkletId(#[no_trace] Uuid);
186
187malloc_size_of_is_0!(WorkletId);
188
189impl WorkletId {
190    fn new() -> WorkletId {
191        WorkletId(Uuid::new_v4())
192    }
193}
194
195/// <https://drafts.css-houdini.org/worklets/#pending-tasks-struct>
196#[derive(Clone, Debug)]
197struct PendingTasksStruct(Arc<AtomicIsize>);
198
199impl PendingTasksStruct {
200    fn new() -> PendingTasksStruct {
201        PendingTasksStruct(Arc::new(AtomicIsize::new(
202            WORKLET_THREAD_POOL_SIZE as isize,
203        )))
204    }
205
206    fn set_counter_to(&self, value: isize) -> isize {
207        self.0.swap(value, Ordering::AcqRel)
208    }
209
210    fn decrement_counter_by(&self, offset: isize) -> isize {
211        self.0.fetch_sub(offset, Ordering::AcqRel)
212    }
213}
214
215/// Worklets execute in a dedicated thread pool.
216///
217/// The goal is to ensure that there is a primary worklet thread,
218/// which is able to responsively execute worklet code. In particular,
219/// worklet execution should not be delayed by GC, or by script
220/// loading.
221///
222/// To achieve this, we implement a three-thread pool, with the
223/// threads cycling between three thread roles:
224///
225///  * The primary worklet thread is the one available to execute
226///    worklet code.
227///
228///  * The hot backup thread may peform GC, but otherwise is expected
229///    to take over the primary role.
230///
231///  * The cold backup thread may peform script loading and other
232///    long-running tasks.
233///
234/// In the implementation, we use two kinds of messages:
235///
236///  * Data messages are expected to be processed quickly, and include
237///    the worklet tasks to be performed by the primary thread, as
238///    well as requests to change role or quit execution.
239///
240///  * Control messages are expected to be processed more slowly, and
241///    include script loading.
242///
243/// Data messages are targeted at a role, for example, task execution
244/// is expected to be performed by whichever thread is currently
245/// primary. Control messages are targeted at a thread, for example
246/// adding a module is performed in every thread, even if they change roles
247/// in the middle of module loading.
248///
249/// The thread pool lives in the script thread, and is initialized
250/// when a worklet adds a module. It is dropped when the script thread
251/// is dropped, and asks each of the worklet threads to quit.
252///
253/// Layout can end up blocking on the primary worklet thread
254/// (e.g. when invoking a paint callback), so it is important to avoid
255/// deadlock by making sure the primary worklet thread doesn't end up
256/// blocking waiting on layout. In particular, since the constellation
257/// can block waiting on layout, this means the primary worklet thread
258/// can't block waiting on the constellation. In general, the primary
259/// worklet thread shouldn't perform any blocking operations. If a worklet
260/// thread needs to do anything blocking, it should send a control
261/// message, to make sure that the blocking operation is performed
262/// by a backup thread, not by the primary thread.
263
264#[derive(Clone, JSTraceable)]
265pub(crate) struct WorkletThreadPool {
266    // Channels to send data messages to the three roles.
267    #[no_trace]
268    primary_sender: Sender<WorkletData>,
269    #[no_trace]
270    hot_backup_sender: Sender<WorkletData>,
271    #[no_trace]
272    cold_backup_sender: Sender<WorkletData>,
273    // Channels to send control messages to the three threads.
274    #[no_trace]
275    control_sender_0: Sender<WorkletControl>,
276    #[no_trace]
277    control_sender_1: Sender<WorkletControl>,
278    #[no_trace]
279    control_sender_2: Sender<WorkletControl>,
280}
281
282impl Drop for WorkletThreadPool {
283    fn drop(&mut self) {
284        let _ = self.cold_backup_sender.send(WorkletData::Quit);
285        let _ = self.hot_backup_sender.send(WorkletData::Quit);
286        let _ = self.primary_sender.send(WorkletData::Quit);
287    }
288}
289
290impl WorkletThreadPool {
291    /// Create a new thread pool and spawn the threads.
292    /// When the thread pool is dropped, the threads will be asked to quit.
293    pub(crate) fn spawn(global_init: WorkletGlobalScopeInit) -> WorkletThreadPool {
294        let primary_role = WorkletThreadRole::new(false, false);
295        let hot_backup_role = WorkletThreadRole::new(true, false);
296        let cold_backup_role = WorkletThreadRole::new(false, true);
297        let primary_sender = primary_role.sender.clone();
298        let hot_backup_sender = hot_backup_role.sender.clone();
299        let cold_backup_sender = cold_backup_role.sender.clone();
300        let init = WorkletThreadInit {
301            primary_sender: primary_sender.clone(),
302            hot_backup_sender: hot_backup_sender.clone(),
303            cold_backup_sender: cold_backup_sender.clone(),
304            global_init,
305        };
306        WorkletThreadPool {
307            primary_sender,
308            hot_backup_sender,
309            cold_backup_sender,
310            control_sender_0: WorkletThread::spawn(primary_role, init.clone(), 0),
311            control_sender_1: WorkletThread::spawn(hot_backup_role, init.clone(), 1),
312            control_sender_2: WorkletThread::spawn(cold_backup_role, init, 2),
313        }
314    }
315
316    /// Loads a worklet module into every worklet thread.
317    /// If all of the threads load successfully, the promise is resolved.
318    /// If any of the threads fails to load, the promise is rejected.
319    /// <https://drafts.css-houdini.org/worklets/#fetch-and-invoke-a-worklet-script>
320    #[allow(clippy::too_many_arguments)]
321    fn fetch_and_invoke_a_worklet_script(
322        &self,
323        webview_id: WebViewId,
324        pipeline_id: PipelineId,
325        worklet_id: WorkletId,
326        global_type: WorkletGlobalScopeType,
327        origin: ImmutableOrigin,
328        base_url: ServoUrl,
329        script_url: ServoUrl,
330        policy_container: PolicyContainer,
331        credentials: RequestCredentials,
332        pending_tasks_struct: PendingTasksStruct,
333        promise: &Rc<Promise>,
334        inherited_secure_context: Option<bool>,
335    ) {
336        // Send each thread a control message asking it to load the script.
337        for sender in &[
338            &self.control_sender_0,
339            &self.control_sender_1,
340            &self.control_sender_2,
341        ] {
342            let _ = sender.send(WorkletControl::FetchAndInvokeAWorkletScript {
343                webview_id,
344                pipeline_id,
345                worklet_id,
346                global_type,
347                origin: origin.clone(),
348                base_url: base_url.clone(),
349                script_url: script_url.clone(),
350                policy_container: policy_container.clone(),
351                credentials,
352                pending_tasks_struct: pending_tasks_struct.clone(),
353                promise: TrustedPromise::new(promise.clone()),
354                inherited_secure_context,
355            });
356        }
357        self.wake_threads();
358    }
359
360    pub(crate) fn exit_worklet(&self, worklet_id: WorkletId) {
361        for sender in &[
362            &self.control_sender_0,
363            &self.control_sender_1,
364            &self.control_sender_2,
365        ] {
366            let _ = sender.send(WorkletControl::ExitWorklet(worklet_id));
367        }
368        self.wake_threads();
369    }
370
371    /// For testing.
372    #[cfg(feature = "testbinding")]
373    pub(crate) fn test_worklet_lookup(&self, id: WorkletId, key: String) -> Option<String> {
374        let (sender, receiver) = unbounded();
375        let msg = WorkletData::Task(id, WorkletTask::Test(TestWorkletTask::Lookup(key, sender)));
376        let _ = self.primary_sender.send(msg);
377        receiver.recv().expect("Test worklet has died?")
378    }
379
380    fn wake_threads(&self) {
381        // If any of the threads are blocked waiting on data, wake them up.
382        let _ = self.cold_backup_sender.send(WorkletData::WakeUp);
383        let _ = self.hot_backup_sender.send(WorkletData::WakeUp);
384        let _ = self.primary_sender.send(WorkletData::WakeUp);
385    }
386}
387
388/// The data messages sent to worklet threads
389enum WorkletData {
390    Task(WorkletId, WorkletTask),
391    StartSwapRoles(Sender<WorkletData>),
392    FinishSwapRoles(Swapper<WorkletThreadRole>),
393    WakeUp,
394    Quit,
395}
396
397/// The control message sent to worklet threads
398enum WorkletControl {
399    ExitWorklet(WorkletId),
400    FetchAndInvokeAWorkletScript {
401        webview_id: WebViewId,
402        pipeline_id: PipelineId,
403        worklet_id: WorkletId,
404        global_type: WorkletGlobalScopeType,
405        origin: ImmutableOrigin,
406        base_url: ServoUrl,
407        script_url: ServoUrl,
408        policy_container: PolicyContainer,
409        credentials: RequestCredentials,
410        pending_tasks_struct: PendingTasksStruct,
411        promise: TrustedPromise,
412        inherited_secure_context: Option<bool>,
413    },
414}
415
416/// A role that a worklet thread can be playing.
417///
418/// These roles are used as tokens or capabilities, we track unique
419/// ownership using Rust's types, and use atomic swapping to exchange
420/// them between worklet threads. This ensures that each thread pool has
421/// exactly one primary, one hot backup and one cold backup.
422struct WorkletThreadRole {
423    receiver: Receiver<WorkletData>,
424    sender: Sender<WorkletData>,
425    is_hot_backup: bool,
426    is_cold_backup: bool,
427}
428
429impl WorkletThreadRole {
430    fn new(is_hot_backup: bool, is_cold_backup: bool) -> WorkletThreadRole {
431        let (sender, receiver) = unbounded();
432        WorkletThreadRole {
433            sender,
434            receiver,
435            is_hot_backup,
436            is_cold_backup,
437        }
438    }
439}
440
441/// Data to initialize a worklet thread.
442#[derive(Clone)]
443struct WorkletThreadInit {
444    /// Senders
445    primary_sender: Sender<WorkletData>,
446    hot_backup_sender: Sender<WorkletData>,
447    cold_backup_sender: Sender<WorkletData>,
448
449    /// Data for initializing new worklet global scopes
450    global_init: WorkletGlobalScopeInit,
451}
452
453struct WorkletCspProcessor {}
454
455impl CspViolationsProcessor for WorkletCspProcessor {
456    fn process_csp_violations(&self, _violations: Vec<Violation>) {}
457}
458
459/// A thread for executing worklets.
460#[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)]
461struct WorkletThread {
462    /// Which role the thread is currently playing
463    role: WorkletThreadRole,
464
465    /// The thread's receiver for control messages
466    control_receiver: Receiver<WorkletControl>,
467
468    /// Senders
469    primary_sender: Sender<WorkletData>,
470    hot_backup_sender: Sender<WorkletData>,
471    cold_backup_sender: Sender<WorkletData>,
472
473    /// Data for initializing new worklet global scopes
474    global_init: WorkletGlobalScopeInit,
475
476    /// The global scopes created by this thread
477    global_scopes: FxHashMap<WorkletId, Dom<WorkletGlobalScope>>,
478
479    /// A one-place buffer for control messages
480    control_buffer: Option<WorkletControl>,
481
482    /// The JS runtime
483    runtime: Runtime,
484    should_gc: bool,
485    gc_threshold: u32,
486}
487
488#[expect(unsafe_code)]
489unsafe impl JSTraceable for WorkletThread {
490    unsafe fn trace(&self, trc: *mut JSTracer) {
491        debug!("Tracing worklet thread.");
492        unsafe { self.global_scopes.trace(trc) };
493    }
494}
495
496impl WorkletThread {
497    /// Spawn a new worklet thread, returning the channel to send it control messages.
498    #[cfg_attr(crown, allow(crown::unrooted_must_root))]
499    fn spawn(
500        role: WorkletThreadRole,
501        init: WorkletThreadInit,
502        thread_index: u8,
503    ) -> Sender<WorkletControl> {
504        let (control_sender, control_receiver) = unbounded();
505        let _ = thread::Builder::new()
506            .name(format!("Worklet#{thread_index}"))
507            .spawn(move || {
508                // TODO: add a new IN_WORKLET thread state?
509                // TODO: set interrupt handler?
510                // TODO: configure the JS runtime (e.g. discourage GC, encourage agressive JIT)
511                debug!("Initializing worklet thread.");
512                thread_state::initialize(ThreadState::SCRIPT | ThreadState::IN_WORKER);
513                let mut thread = RootedTraceableBox::new(WorkletThread {
514                    role,
515                    control_receiver,
516                    primary_sender: init.primary_sender,
517                    hot_backup_sender: init.hot_backup_sender,
518                    cold_backup_sender: init.cold_backup_sender,
519                    global_init: init.global_init,
520                    global_scopes: FxHashMap::default(),
521                    control_buffer: None,
522                    runtime: Runtime::new(None),
523                    should_gc: false,
524                    gc_threshold: MIN_GC_THRESHOLD,
525                });
526                thread.run(CanGc::note());
527            })
528            .expect("Couldn't start worklet thread");
529        control_sender
530    }
531
532    /// The main event loop for a worklet thread
533    fn run(&mut self, can_gc: CanGc) {
534        loop {
535            // The handler for data messages
536            let message = self.role.receiver.recv().unwrap();
537            match message {
538                // The whole point of this thread pool is to perform tasks!
539                WorkletData::Task(id, task) => {
540                    self.perform_a_worklet_task(id, task);
541                },
542                // To start swapping roles, get ready to perform an atomic swap,
543                // and block waiting for the other end to finish it.
544                // NOTE: the cold backup can block on the primary or the hot backup;
545                //       the hot backup can block on the primary;
546                //       the primary can block on nothing;
547                //       this total ordering on thread roles is what guarantees deadlock-freedom.
548                WorkletData::StartSwapRoles(sender) => {
549                    let (our_swapper, their_swapper) = swapper();
550                    match sender.send(WorkletData::FinishSwapRoles(their_swapper)) {
551                        Ok(_) => {},
552                        Err(_) => {
553                            // This might happen if the script thread shuts down while
554                            // waiting for the worklet to finish.
555                            return;
556                        },
557                    };
558                    let _ = our_swapper.swap(&mut self.role);
559                },
560                // To finish swapping roles, perform the atomic swap.
561                // The other end should have already started the swap, so this shouldn't block.
562                WorkletData::FinishSwapRoles(swapper) => {
563                    let _ = swapper.swap(&mut self.role);
564                },
565                // Wake up! There may be control messages to process.
566                WorkletData::WakeUp => {},
567                // Quit!
568                WorkletData::Quit => {
569                    return;
570                },
571            }
572            // Only process control messages if we're the cold backup,
573            // otherwise if there are outstanding control messages,
574            // try to become the cold backup.
575            if self.role.is_cold_backup {
576                if let Some(control) = self.control_buffer.take() {
577                    self.process_control(control, can_gc);
578                }
579                while let Ok(control) = self.control_receiver.try_recv() {
580                    self.process_control(control, can_gc);
581                }
582                self.gc();
583            } else if self.control_buffer.is_none() {
584                if let Ok(control) = self.control_receiver.try_recv() {
585                    self.control_buffer = Some(control);
586                    let msg = WorkletData::StartSwapRoles(self.role.sender.clone());
587                    let _ = self.cold_backup_sender.send(msg);
588                }
589            }
590            // If we are tight on memory, and we're a backup then perform a gc.
591            // If we are tight on memory, and we're the primary then try to become the hot backup.
592            // Hopefully this happens soon!
593            if self.current_memory_usage() > self.gc_threshold {
594                if self.role.is_hot_backup || self.role.is_cold_backup {
595                    self.should_gc = false;
596                    self.gc();
597                } else if !self.should_gc {
598                    self.should_gc = true;
599                    let msg = WorkletData::StartSwapRoles(self.role.sender.clone());
600                    let _ = self.hot_backup_sender.send(msg);
601                }
602            }
603        }
604    }
605
606    /// The current memory usage of the thread
607    #[expect(unsafe_code)]
608    fn current_memory_usage(&self) -> u32 {
609        unsafe { JS_GetGCParameter(self.runtime.cx_no_gc(), JSGCParamKey::JSGC_BYTES) }
610    }
611
612    /// Perform a GC.
613    #[expect(unsafe_code)]
614    fn gc(&mut self) {
615        debug!(
616            "BEGIN GC (usage = {}, threshold = {}).",
617            self.current_memory_usage(),
618            self.gc_threshold
619        );
620        unsafe { JS_GC(self.runtime.cx(), GCReason::API) };
621        self.gc_threshold = max(MIN_GC_THRESHOLD, self.current_memory_usage() * 2);
622        debug!(
623            "END GC (usage = {}, threshold = {}).",
624            self.current_memory_usage(),
625            self.gc_threshold
626        );
627    }
628
629    /// Get the worklet global scope for a given worklet.
630    /// Creates the worklet global scope if it doesn't exist.
631    fn get_worklet_global_scope(
632        &mut self,
633        webview_id: WebViewId,
634        pipeline_id: PipelineId,
635        worklet_id: WorkletId,
636        inherited_secure_context: Option<bool>,
637        global_type: WorkletGlobalScopeType,
638        base_url: ServoUrl,
639    ) -> DomRoot<WorkletGlobalScope> {
640        match self.global_scopes.entry(worklet_id) {
641            hash_map::Entry::Occupied(entry) => DomRoot::from_ref(entry.get()),
642            hash_map::Entry::Vacant(entry) => {
643                debug!("Creating new worklet global scope.");
644                let executor = WorkletExecutor::new(worklet_id, self.primary_sender.clone());
645                let result = WorkletGlobalScope::new(
646                    global_type,
647                    webview_id,
648                    pipeline_id,
649                    base_url,
650                    inherited_secure_context,
651                    executor,
652                    &self.global_init,
653                );
654                entry.insert(Dom::from_ref(&*result));
655                result
656            },
657        }
658    }
659
660    /// Fetch and invoke a worklet script.
661    /// <https://html.spec.whatwg.org/multipage/#fetch-a-worklet-script-graph>
662    #[allow(clippy::too_many_arguments)]
663    fn fetch_and_invoke_a_worklet_script(
664        &self,
665        global_scope: &WorkletGlobalScope,
666        pipeline_id: PipelineId,
667        origin: ImmutableOrigin,
668        script_url: ServoUrl,
669        policy_container: PolicyContainer,
670        credentials: RequestCredentials,
671        pending_tasks_struct: PendingTasksStruct,
672        promise: TrustedPromise,
673        can_gc: CanGc,
674    ) {
675        debug!("Fetching from {}.", script_url);
676        // Step 1.
677        // TODO: Settings object?
678
679        // Step 2.
680        // TODO: Fetch a module graph, not just a single script.
681        // TODO: Fetch the script asynchronously?
682        // TODO: Caching.
683        let global = global_scope.upcast::<GlobalScope>();
684        let resource_fetcher = self.global_init.resource_threads.sender();
685        let request = RequestBuilder::new(None, script_url, global.get_referrer())
686            .destination(Destination::Script)
687            .mode(RequestMode::CorsMode)
688            .credentials_mode(credentials.convert())
689            .policy_container(policy_container)
690            .origin(origin);
691
692        let script = load_whole_resource(
693            request,
694            &resource_fetcher,
695            global,
696            &WorkletCspProcessor {},
697            can_gc,
698        )
699        .ok()
700        .and_then(|(_, bytes)| String::from_utf8(bytes).ok());
701
702        // Step 4.
703        // NOTE: the spec parses and executes the script in separate steps,
704        // but our JS API doesn't separate these, so we do the steps out of order.
705        // Also, the spec currently doesn't allow exceptions to be propagated
706        // to the main script thread.
707        // https://github.com/w3c/css-houdini-drafts/issues/407
708        let ok = script.is_some_and(|s| global_scope.evaluate_js(s.into(), can_gc).is_ok());
709
710        if !ok {
711            // Step 3.
712            debug!("Failed to load script.");
713            let old_counter = pending_tasks_struct.set_counter_to(-1);
714            if old_counter > 0 {
715                self.run_in_script_thread(promise.reject_task(Error::Abort));
716            }
717        } else {
718            // Step 5.
719            debug!("Finished adding script.");
720            let old_counter = pending_tasks_struct.decrement_counter_by(1);
721            if old_counter == 1 {
722                debug!("Resolving promise.");
723                let msg = MainThreadScriptMsg::WorkletLoaded(pipeline_id);
724                self.global_init
725                    .to_script_thread_sender
726                    .send(msg)
727                    .expect("Worklet thread outlived script thread.");
728                self.run_in_script_thread(promise.resolve_task(()));
729            }
730        }
731    }
732
733    /// Perform a task.
734    fn perform_a_worklet_task(&self, worklet_id: WorkletId, task: WorkletTask) {
735        match self.global_scopes.get(&worklet_id) {
736            Some(global) => global.perform_a_worklet_task(task),
737            None => warn!("No such worklet as {:?}.", worklet_id),
738        }
739    }
740
741    /// Process a control message.
742    fn process_control(&mut self, control: WorkletControl, can_gc: CanGc) {
743        match control {
744            WorkletControl::ExitWorklet(worklet_id) => {
745                self.global_scopes.remove(&worklet_id);
746            },
747            WorkletControl::FetchAndInvokeAWorkletScript {
748                webview_id,
749                pipeline_id,
750                worklet_id,
751                global_type,
752                origin,
753                base_url,
754                script_url,
755                policy_container,
756                credentials,
757                pending_tasks_struct,
758                promise,
759                inherited_secure_context,
760            } => {
761                let global = self.get_worklet_global_scope(
762                    webview_id,
763                    pipeline_id,
764                    worklet_id,
765                    inherited_secure_context,
766                    global_type,
767                    base_url,
768                );
769                self.fetch_and_invoke_a_worklet_script(
770                    &global,
771                    pipeline_id,
772                    origin,
773                    script_url,
774                    policy_container,
775                    credentials,
776                    pending_tasks_struct,
777                    promise,
778                    can_gc,
779                )
780            },
781        }
782    }
783
784    /// Run a task in the main script thread.
785    fn run_in_script_thread<T>(&self, task: T)
786    where
787        T: TaskBox + 'static,
788    {
789        // NOTE: It's unclear which task source should be used here:
790        // https://drafts.css-houdini.org/worklets/#dom-worklet-addmodule
791        let msg = CommonScriptMsg::Task(
792            ScriptThreadEventCategory::WorkletEvent,
793            Box::new(task),
794            None,
795            TaskSourceName::DOMManipulation,
796        );
797        let msg = MainThreadScriptMsg::Common(msg);
798        self.global_init
799            .to_script_thread_sender
800            .send(msg)
801            .expect("Worklet thread outlived script thread.");
802    }
803}
804
805/// An executor of worklet tasks
806#[derive(Clone, JSTraceable, MallocSizeOf)]
807pub(crate) struct WorkletExecutor {
808    worklet_id: WorkletId,
809    #[no_trace]
810    primary_sender: Sender<WorkletData>,
811}
812
813impl WorkletExecutor {
814    fn new(worklet_id: WorkletId, primary_sender: Sender<WorkletData>) -> WorkletExecutor {
815        WorkletExecutor {
816            worklet_id,
817            primary_sender,
818        }
819    }
820
821    /// Schedule a worklet task to be peformed by the worklet thread pool.
822    pub(crate) fn schedule_a_worklet_task(&self, task: WorkletTask) {
823        let _ = self
824            .primary_sender
825            .send(WorkletData::Task(self.worklet_id, task));
826    }
827}