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