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