Skip to main content

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