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::jsapi::{GCReason, JSGCParamKey, JSTracer};
24use js::rust::wrappers2::{JS_GC, JS_GetGCParameter};
25use malloc_size_of::malloc_size_of_is_0;
26use net_traits::blob_url_store::UrlWithBlobClaim;
27use net_traits::policy_container::PolicyContainer;
28use net_traits::request::{Destination, RequestBuilder, RequestMode};
29use rustc_hash::FxHashMap;
30use servo_base::generic_channel::GenericSend;
31use servo_base::id::{PipelineId, WebViewId};
32use servo_url::{ImmutableOrigin, ServoUrl};
33use style::thread_state::{self, ThreadState};
34use swapper::{Swapper, swapper};
35use uuid::Uuid;
36
37use crate::conversions::Convert;
38use crate::dom::bindings::codegen::Bindings::RequestBinding::RequestCredentials;
39use crate::dom::bindings::codegen::Bindings::WindowBinding::Window_Binding::WindowMethods;
40use crate::dom::bindings::codegen::Bindings::WorkletBinding::{WorkletMethods, WorkletOptions};
41use crate::dom::bindings::error::Error;
42use crate::dom::bindings::inheritance::Castable;
43use crate::dom::bindings::refcounted::TrustedPromise;
44use crate::dom::bindings::reflector::{DomGlobal, Reflector, reflect_dom_object};
45use crate::dom::bindings::root::{Dom, DomRoot};
46use crate::dom::bindings::str::USVString;
47use crate::dom::bindings::trace::{CustomTraceable, JSTraceable, RootedTraceableBox};
48use crate::dom::csp::Violation;
49use crate::dom::globalscope::GlobalScope;
50use crate::dom::promise::Promise;
51#[cfg(feature = "testbinding")]
52use crate::dom::testworkletglobalscope::TestWorkletTask;
53use crate::dom::window::Window;
54use crate::dom::workletglobalscope::{
55    WorkletGlobalScope, WorkletGlobalScopeInit, WorkletGlobalScopeType, WorkletTask,
56};
57use crate::fetch::{CspViolationsProcessor, load_whole_resource};
58use crate::messaging::{CommonScriptMsg, MainThreadScriptMsg};
59use crate::realms::InRealm;
60use crate::script_runtime::{CanGc, Runtime, ScriptThreadEventCategory};
61use crate::script_thread::ScriptThread;
62use crate::task::TaskBox;
63use crate::task_source::TaskSourceName;
64
65// Magic numbers
66const WORKLET_THREAD_POOL_SIZE: u32 = 3;
67const MIN_GC_THRESHOLD: u32 = 1_000_000;
68
69#[derive(JSTraceable, MallocSizeOf)]
70struct DroppableField {
71    worklet_id: WorkletId,
72    /// The cached version of the script thread's WorkletThreadPool. We keep this cached
73    /// because we may need to access it after the script thread has terminated.
74    #[ignore_malloc_size_of = "Difficult to measure memory usage of Rc<...> types"]
75    thread_pool: OnceCell<Rc<WorkletThreadPool>>,
76}
77
78impl Drop for DroppableField {
79    fn drop(&mut self) {
80        let worklet_id = self.worklet_id;
81        if let Some(thread_pool) = self.thread_pool.get_mut() {
82            thread_pool.exit_worklet(worklet_id);
83        }
84    }
85}
86
87#[dom_struct]
88/// <https://drafts.css-houdini.org/worklets/#worklet>
89pub(crate) struct Worklet {
90    reflector: Reflector,
91    window: Dom<Window>,
92    global_type: WorkletGlobalScopeType,
93    droppable_field: DroppableField,
94}
95
96impl Worklet {
97    fn new_inherited(window: &Window, global_type: WorkletGlobalScopeType) -> Worklet {
98        Worklet {
99            reflector: Reflector::new(),
100            window: Dom::from_ref(window),
101            global_type,
102            droppable_field: DroppableField {
103                worklet_id: WorkletId::new(),
104                thread_pool: OnceCell::new(),
105            },
106        }
107    }
108
109    pub(crate) fn new(
110        window: &Window,
111        global_type: WorkletGlobalScopeType,
112        can_gc: CanGc,
113    ) -> DomRoot<Worklet> {
114        debug!("Creating worklet {:?}.", global_type);
115        reflect_dom_object(
116            Box::new(Worklet::new_inherited(window, global_type)),
117            window,
118            can_gc,
119        )
120    }
121
122    #[cfg(feature = "testbinding")]
123    pub(crate) fn worklet_id(&self) -> WorkletId {
124        self.droppable_field.worklet_id
125    }
126
127    #[expect(dead_code)]
128    pub(crate) fn worklet_global_scope_type(&self) -> WorkletGlobalScopeType {
129        self.global_type
130    }
131}
132
133impl WorkletMethods<crate::DomTypeHolder> for Worklet {
134    /// <https://drafts.css-houdini.org/worklets/#dom-worklet-addmodule>
135    fn AddModule(
136        &self,
137        module_url: USVString,
138        options: &WorkletOptions,
139        comp: InRealm,
140        can_gc: CanGc,
141    ) -> Rc<Promise> {
142        // Step 1.
143        let promise = Promise::new_in_current_realm(comp, can_gc);
144
145        // Step 3.
146        let module_url_record = match self.window.Document().base_url().join(&module_url.0) {
147            Ok(url) => url,
148            Err(err) => {
149                // Step 4.
150                debug!("URL {:?} parse error {:?}.", module_url.0, err);
151                promise.reject_error(Error::Syntax(None), can_gc);
152                return promise;
153            },
154        };
155        debug!("Adding Worklet module {}.", module_url_record);
156
157        // Steps 6-12 in parallel.
158        let pending_tasks_struct = PendingTasksStruct::new();
159        let global_scope = self.window.as_global_scope();
160
161        self.droppable_field
162            .thread_pool
163            .get_or_init(|| ScriptThread::worklet_thread_pool(self.global().image_cache()))
164            .fetch_and_invoke_a_worklet_script(
165                self.window.webview_id(),
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        webview_id: WebViewId,
326        pipeline_id: PipelineId,
327        worklet_id: WorkletId,
328        global_type: WorkletGlobalScopeType,
329        origin: ImmutableOrigin,
330        base_url: ServoUrl,
331        script_url: ServoUrl,
332        policy_container: PolicyContainer,
333        credentials: RequestCredentials,
334        pending_tasks_struct: PendingTasksStruct,
335        promise: &Rc<Promise>,
336        inherited_secure_context: Option<bool>,
337    ) {
338        // Send each thread a control message asking it to load the script.
339        for sender in &[
340            &self.control_sender_0,
341            &self.control_sender_1,
342            &self.control_sender_2,
343        ] {
344            let _ = sender.send(WorkletControl::FetchAndInvokeAWorkletScript {
345                webview_id,
346                pipeline_id,
347                worklet_id,
348                global_type,
349                origin: origin.clone(),
350                base_url: base_url.clone(),
351                script_url: script_url.clone(),
352                policy_container: policy_container.clone(),
353                credentials,
354                pending_tasks_struct: pending_tasks_struct.clone(),
355                promise: TrustedPromise::new(promise.clone()),
356                inherited_secure_context,
357            });
358        }
359        self.wake_threads();
360    }
361
362    pub(crate) fn exit_worklet(&self, worklet_id: WorkletId) {
363        for sender in &[
364            &self.control_sender_0,
365            &self.control_sender_1,
366            &self.control_sender_2,
367        ] {
368            let _ = sender.send(WorkletControl::ExitWorklet(worklet_id));
369        }
370        self.wake_threads();
371    }
372
373    /// For testing.
374    #[cfg(feature = "testbinding")]
375    pub(crate) fn test_worklet_lookup(&self, id: WorkletId, key: String) -> Option<String> {
376        let (sender, receiver) = unbounded();
377        let msg = WorkletData::Task(id, WorkletTask::Test(TestWorkletTask::Lookup(key, sender)));
378        let _ = self.primary_sender.send(msg);
379        receiver.recv().expect("Test worklet has died?")
380    }
381
382    fn wake_threads(&self) {
383        // If any of the threads are blocked waiting on data, wake them up.
384        let _ = self.cold_backup_sender.send(WorkletData::WakeUp);
385        let _ = self.hot_backup_sender.send(WorkletData::WakeUp);
386        let _ = self.primary_sender.send(WorkletData::WakeUp);
387    }
388}
389
390/// The data messages sent to worklet threads
391enum WorkletData {
392    Task(WorkletId, WorkletTask),
393    StartSwapRoles(Sender<WorkletData>),
394    FinishSwapRoles(Swapper<WorkletThreadRole>),
395    WakeUp,
396    Quit,
397}
398
399/// The control message sent to worklet threads
400enum WorkletControl {
401    ExitWorklet(WorkletId),
402    FetchAndInvokeAWorkletScript {
403        webview_id: WebViewId,
404        pipeline_id: PipelineId,
405        worklet_id: WorkletId,
406        global_type: WorkletGlobalScopeType,
407        origin: ImmutableOrigin,
408        base_url: ServoUrl,
409        script_url: ServoUrl,
410        policy_container: PolicyContainer,
411        credentials: RequestCredentials,
412        pending_tasks_struct: PendingTasksStruct,
413        promise: TrustedPromise,
414        inherited_secure_context: Option<bool>,
415    },
416}
417
418/// A role that a worklet thread can be playing.
419///
420/// These roles are used as tokens or capabilities, we track unique
421/// ownership using Rust's types, and use atomic swapping to exchange
422/// them between worklet threads. This ensures that each thread pool has
423/// exactly one primary, one hot backup and one cold backup.
424struct WorkletThreadRole {
425    receiver: Receiver<WorkletData>,
426    sender: Sender<WorkletData>,
427    is_hot_backup: bool,
428    is_cold_backup: bool,
429}
430
431impl WorkletThreadRole {
432    fn new(is_hot_backup: bool, is_cold_backup: bool) -> WorkletThreadRole {
433        let (sender, receiver) = unbounded();
434        WorkletThreadRole {
435            sender,
436            receiver,
437            is_hot_backup,
438            is_cold_backup,
439        }
440    }
441}
442
443/// Data to initialize a worklet thread.
444#[derive(Clone)]
445struct WorkletThreadInit {
446    /// Senders
447    primary_sender: Sender<WorkletData>,
448    hot_backup_sender: Sender<WorkletData>,
449    cold_backup_sender: Sender<WorkletData>,
450
451    /// Data for initializing new worklet global scopes
452    global_init: WorkletGlobalScopeInit,
453}
454
455struct WorkletCspProcessor {}
456
457impl CspViolationsProcessor for WorkletCspProcessor {
458    fn process_csp_violations(&self, _violations: Vec<Violation>) {}
459}
460
461/// A thread for executing worklets.
462#[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)]
463struct WorkletThread {
464    /// Which role the thread is currently playing
465    role: WorkletThreadRole,
466
467    /// The thread's receiver for control messages
468    control_receiver: Receiver<WorkletControl>,
469
470    /// Senders
471    primary_sender: Sender<WorkletData>,
472    hot_backup_sender: Sender<WorkletData>,
473    cold_backup_sender: Sender<WorkletData>,
474
475    /// Data for initializing new worklet global scopes
476    global_init: WorkletGlobalScopeInit,
477
478    /// The global scopes created by this thread
479    global_scopes: FxHashMap<WorkletId, Dom<WorkletGlobalScope>>,
480
481    /// A one-place buffer for control messages
482    control_buffer: Option<WorkletControl>,
483
484    /// The JS runtime
485    runtime: Runtime,
486    should_gc: bool,
487    gc_threshold: u32,
488}
489
490#[expect(unsafe_code)]
491unsafe impl JSTraceable for WorkletThread {
492    unsafe fn trace(&self, trc: *mut JSTracer) {
493        debug!("Tracing worklet thread.");
494        unsafe { self.global_scopes.trace(trc) };
495    }
496}
497
498impl WorkletThread {
499    #[allow(unsafe_code)]
500    /// Spawn a new worklet thread, returning the channel to send it control messages.
501    fn spawn(
502        role: WorkletThreadRole,
503        init: WorkletThreadInit,
504        thread_index: u8,
505    ) -> Sender<WorkletControl> {
506        let (control_sender, control_receiver) = unbounded();
507        let _ = thread::Builder::new()
508            .name(format!("Worklet#{thread_index}"))
509            .spawn(move || {
510                // TODO: add a new IN_WORKLET thread state?
511                // TODO: set interrupt handler?
512                // TODO: configure the JS runtime (e.g. discourage GC, encourage agressive JIT)
513                debug!("Initializing worklet thread.");
514                thread_state::initialize(ThreadState::SCRIPT | ThreadState::IN_WORKER);
515                let runtime = Runtime::new(None);
516                let mut cx = unsafe { runtime.cx() };
517                let mut thread = RootedTraceableBox::new(WorkletThread {
518                    role,
519                    control_receiver,
520                    primary_sender: init.primary_sender,
521                    hot_backup_sender: init.hot_backup_sender,
522                    cold_backup_sender: init.cold_backup_sender,
523                    global_init: init.global_init,
524                    global_scopes: FxHashMap::default(),
525                    control_buffer: None,
526                    runtime,
527                    should_gc: false,
528                    gc_threshold: MIN_GC_THRESHOLD,
529                });
530                thread.run(&mut cx);
531            })
532            .expect("Couldn't start worklet thread");
533        control_sender
534    }
535
536    /// The main event loop for a worklet thread
537    fn run(&mut self, cx: &mut js::context::JSContext) {
538        loop {
539            // The handler for data messages
540            let message = self.role.receiver.recv().unwrap();
541            match message {
542                // The whole point of this thread pool is to perform tasks!
543                WorkletData::Task(id, task) => {
544                    self.perform_a_worklet_task(id, task);
545                },
546                // To start swapping roles, get ready to perform an atomic swap,
547                // and block waiting for the other end to finish it.
548                // NOTE: the cold backup can block on the primary or the hot backup;
549                //       the hot backup can block on the primary;
550                //       the primary can block on nothing;
551                //       this total ordering on thread roles is what guarantees deadlock-freedom.
552                WorkletData::StartSwapRoles(sender) => {
553                    let (our_swapper, their_swapper) = swapper();
554                    match sender.send(WorkletData::FinishSwapRoles(their_swapper)) {
555                        Ok(_) => {},
556                        Err(_) => {
557                            // This might happen if the script thread shuts down while
558                            // waiting for the worklet to finish.
559                            return;
560                        },
561                    };
562                    let _ = our_swapper.swap(&mut self.role);
563                },
564                // To finish swapping roles, perform the atomic swap.
565                // The other end should have already started the swap, so this shouldn't block.
566                WorkletData::FinishSwapRoles(swapper) => {
567                    let _ = swapper.swap(&mut self.role);
568                },
569                // Wake up! There may be control messages to process.
570                WorkletData::WakeUp => {},
571                // Quit!
572                WorkletData::Quit => {
573                    return;
574                },
575            }
576            // Only process control messages if we're the cold backup,
577            // otherwise if there are outstanding control messages,
578            // try to become the cold backup.
579            if self.role.is_cold_backup {
580                if let Some(control) = self.control_buffer.take() {
581                    self.process_control(control, cx);
582                }
583                while let Ok(control) = self.control_receiver.try_recv() {
584                    self.process_control(control, cx);
585                }
586                self.gc(cx);
587            } else if self.control_buffer.is_none() {
588                if let Ok(control) = self.control_receiver.try_recv() {
589                    self.control_buffer = Some(control);
590                    let msg = WorkletData::StartSwapRoles(self.role.sender.clone());
591                    let _ = self.cold_backup_sender.send(msg);
592                }
593            }
594            // If we are tight on memory, and we're a backup then perform a gc.
595            // If we are tight on memory, and we're the primary then try to become the hot backup.
596            // Hopefully this happens soon!
597            if self.current_memory_usage() > self.gc_threshold {
598                if self.role.is_hot_backup || self.role.is_cold_backup {
599                    self.should_gc = false;
600                    self.gc(cx);
601                } else if !self.should_gc {
602                    self.should_gc = true;
603                    let msg = WorkletData::StartSwapRoles(self.role.sender.clone());
604                    let _ = self.hot_backup_sender.send(msg);
605                }
606            }
607        }
608    }
609
610    /// The current memory usage of the thread
611    #[expect(unsafe_code)]
612    fn current_memory_usage(&self) -> u32 {
613        unsafe { JS_GetGCParameter(self.runtime.cx_no_gc(), JSGCParamKey::JSGC_BYTES) }
614    }
615
616    /// Perform a GC.
617    #[expect(unsafe_code)]
618    fn gc(&mut self, cx: &mut js::context::JSContext) {
619        debug!(
620            "BEGIN GC (usage = {}, threshold = {}).",
621            self.current_memory_usage(),
622            self.gc_threshold
623        );
624        unsafe { JS_GC(cx, GCReason::API) };
625        self.gc_threshold = max(MIN_GC_THRESHOLD, self.current_memory_usage() * 2);
626        debug!(
627            "END GC (usage = {}, threshold = {}).",
628            self.current_memory_usage(),
629            self.gc_threshold
630        );
631    }
632
633    #[expect(clippy::too_many_arguments)]
634    /// Get the worklet global scope for a given worklet.
635    /// Creates the worklet global scope if it doesn't exist.
636    fn get_worklet_global_scope(
637        &mut self,
638        webview_id: WebViewId,
639        pipeline_id: PipelineId,
640        worklet_id: WorkletId,
641        inherited_secure_context: Option<bool>,
642        global_type: WorkletGlobalScopeType,
643        base_url: ServoUrl,
644        cx: &mut js::context::JSContext,
645    ) -> DomRoot<WorkletGlobalScope> {
646        match self.global_scopes.entry(worklet_id) {
647            hash_map::Entry::Occupied(entry) => DomRoot::from_ref(entry.get()),
648            hash_map::Entry::Vacant(entry) => {
649                debug!("Creating new worklet global scope.");
650                let executor = WorkletExecutor::new(worklet_id, self.primary_sender.clone());
651                let result = WorkletGlobalScope::new(
652                    global_type,
653                    webview_id,
654                    pipeline_id,
655                    base_url,
656                    inherited_secure_context,
657                    executor,
658                    &self.global_init,
659                    cx,
660                );
661                entry.insert(Dom::from_ref(&*result));
662                result
663            },
664        }
665    }
666
667    /// Fetch and invoke a worklet script.
668    /// <https://html.spec.whatwg.org/multipage/#fetch-a-worklet-script-graph>
669    #[allow(clippy::too_many_arguments)]
670    fn fetch_and_invoke_a_worklet_script(
671        &self,
672        global_scope: &WorkletGlobalScope,
673        pipeline_id: PipelineId,
674        origin: ImmutableOrigin,
675        script_url: ServoUrl,
676        policy_container: PolicyContainer,
677        credentials: RequestCredentials,
678        pending_tasks_struct: PendingTasksStruct,
679        promise: TrustedPromise,
680        cx: &mut js::context::JSContext,
681    ) {
682        debug!("Fetching from {}.", script_url);
683        // Step 1.
684        // TODO: Settings object?
685
686        // Step 2.
687        // TODO: Fetch a module graph, not just a single script.
688        // TODO: Fetch the script asynchronously?
689        // TODO: Caching.
690        let global = global_scope.upcast::<GlobalScope>();
691        let resource_fetcher = self.global_init.resource_threads.sender();
692        let request = RequestBuilder::new(
693            None,
694            UrlWithBlobClaim::from_url_without_having_claimed_blob(script_url),
695            global.get_referrer(),
696        )
697        .destination(Destination::Script)
698        .mode(RequestMode::CorsMode)
699        .credentials_mode(credentials.convert())
700        .policy_container(policy_container)
701        .origin(origin);
702
703        let script = load_whole_resource(
704            request,
705            &resource_fetcher,
706            global,
707            &WorkletCspProcessor {},
708            cx,
709        )
710        .ok()
711        .and_then(|(_, bytes, _)| String::from_utf8(bytes).ok());
712
713        // Step 4.
714        // NOTE: the spec parses and executes the script in separate steps,
715        // but our JS API doesn't separate these, so we do the steps out of order.
716        // Also, the spec currently doesn't allow exceptions to be propagated
717        // to the main script thread.
718        // https://github.com/w3c/css-houdini-drafts/issues/407
719        let ok = script.is_some_and(|s| global_scope.evaluate_js(s.into(), cx).is_ok());
720
721        if !ok {
722            // Step 3.
723            debug!("Failed to load script.");
724            let old_counter = pending_tasks_struct.set_counter_to(-1);
725            if old_counter > 0 {
726                self.run_in_script_thread(promise.reject_task(Error::Abort(None)));
727            }
728        } else {
729            // Step 5.
730            debug!("Finished adding script.");
731            let old_counter = pending_tasks_struct.decrement_counter_by(1);
732            if old_counter == 1 {
733                debug!("Resolving promise.");
734                let msg = MainThreadScriptMsg::WorkletLoaded(pipeline_id);
735                self.global_init
736                    .to_script_thread_sender
737                    .send(msg)
738                    .expect("Worklet thread outlived script thread.");
739                self.run_in_script_thread(promise.resolve_task(()));
740            }
741        }
742    }
743
744    /// Perform a task.
745    fn perform_a_worklet_task(&self, worklet_id: WorkletId, task: WorkletTask) {
746        match self.global_scopes.get(&worklet_id) {
747            Some(global) => global.perform_a_worklet_task(task),
748            None => warn!("No such worklet as {:?}.", worklet_id),
749        }
750    }
751
752    /// Process a control message.
753    fn process_control(&mut self, control: WorkletControl, cx: &mut js::context::JSContext) {
754        match control {
755            WorkletControl::ExitWorklet(worklet_id) => {
756                self.global_scopes.remove(&worklet_id);
757            },
758            WorkletControl::FetchAndInvokeAWorkletScript {
759                webview_id,
760                pipeline_id,
761                worklet_id,
762                global_type,
763                origin,
764                base_url,
765                script_url,
766                policy_container,
767                credentials,
768                pending_tasks_struct,
769                promise,
770                inherited_secure_context,
771            } => {
772                let global = self.get_worklet_global_scope(
773                    webview_id,
774                    pipeline_id,
775                    worklet_id,
776                    inherited_secure_context,
777                    global_type,
778                    base_url,
779                    cx,
780                );
781                self.fetch_and_invoke_a_worklet_script(
782                    &global,
783                    pipeline_id,
784                    origin,
785                    script_url,
786                    policy_container,
787                    credentials,
788                    pending_tasks_struct,
789                    promise,
790                    cx,
791                )
792            },
793        }
794    }
795
796    /// Run a task in the main script thread.
797    fn run_in_script_thread<T>(&self, task: T)
798    where
799        T: TaskBox + 'static,
800    {
801        // NOTE: It's unclear which task source should be used here:
802        // https://drafts.css-houdini.org/worklets/#dom-worklet-addmodule
803        let msg = CommonScriptMsg::Task(
804            ScriptThreadEventCategory::WorkletEvent,
805            Box::new(task),
806            None,
807            TaskSourceName::DOMManipulation,
808        );
809        let msg = MainThreadScriptMsg::Common(msg);
810        self.global_init
811            .to_script_thread_sender
812            .send(msg)
813            .expect("Worklet thread outlived script thread.");
814    }
815}
816
817/// An executor of worklet tasks
818#[derive(Clone, JSTraceable, MallocSizeOf)]
819pub(crate) struct WorkletExecutor {
820    worklet_id: WorkletId,
821    #[no_trace]
822    primary_sender: Sender<WorkletData>,
823}
824
825impl WorkletExecutor {
826    fn new(worklet_id: WorkletId, primary_sender: Sender<WorkletData>) -> WorkletExecutor {
827        WorkletExecutor {
828            worklet_id,
829            primary_sender,
830        }
831    }
832
833    /// Schedule a worklet task to be peformed by the worklet thread pool.
834    pub(crate) fn schedule_a_worklet_task(&self, task: WorkletTask) {
835        let _ = self
836            .primary_sender
837            .send(WorkletData::Task(self.worklet_id, task));
838    }
839}