1use std::cell::OnceCell;
14use std::cmp::max;
15use std::collections::hash_map;
16use std::rc::Rc;
17use std::sync::Arc;
18use std::sync::atomic::{AtomicIsize, Ordering};
19use std::thread;
20
21use base::IpcSend;
22use base::id::{PipelineId, WebViewId};
23use crossbeam_channel::{Receiver, Sender, unbounded};
24use dom_struct::dom_struct;
25use js::jsapi::{GCReason, JSGCParamKey, JSTracer};
26use js::rust::wrappers2::{JS_GC, JS_GetGCParameter};
27use malloc_size_of::malloc_size_of_is_0;
28use net_traits::policy_container::PolicyContainer;
29use net_traits::request::{Destination, RequestBuilder, RequestMode};
30use rustc_hash::FxHashMap;
31use servo_url::{ImmutableOrigin, ServoUrl};
32use style::thread_state::{self, ThreadState};
33use swapper::{Swapper, swapper};
34use uuid::Uuid;
35
36use crate::conversions::Convert;
37use crate::dom::bindings::codegen::Bindings::RequestBinding::RequestCredentials;
38use crate::dom::bindings::codegen::Bindings::WindowBinding::Window_Binding::WindowMethods;
39use crate::dom::bindings::codegen::Bindings::WorkletBinding::{WorkletMethods, WorkletOptions};
40use crate::dom::bindings::error::Error;
41use crate::dom::bindings::inheritance::Castable;
42use crate::dom::bindings::refcounted::TrustedPromise;
43use crate::dom::bindings::reflector::{DomGlobal, Reflector, reflect_dom_object};
44use crate::dom::bindings::root::{Dom, DomRoot};
45use crate::dom::bindings::str::USVString;
46use crate::dom::bindings::trace::{CustomTraceable, JSTraceable, RootedTraceableBox};
47use crate::dom::csp::Violation;
48use crate::dom::globalscope::GlobalScope;
49use crate::dom::promise::Promise;
50#[cfg(feature = "testbinding")]
51use crate::dom::testworkletglobalscope::TestWorkletTask;
52use crate::dom::window::Window;
53use crate::dom::workletglobalscope::{
54 WorkletGlobalScope, WorkletGlobalScopeInit, WorkletGlobalScopeType, WorkletTask,
55};
56use crate::fetch::{CspViolationsProcessor, load_whole_resource};
57use crate::messaging::{CommonScriptMsg, MainThreadScriptMsg};
58use crate::realms::InRealm;
59use crate::script_runtime::{CanGc, Runtime, ScriptThreadEventCategory};
60use crate::script_thread::ScriptThread;
61use crate::task::TaskBox;
62use crate::task_source::TaskSourceName;
63
64const 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 #[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]
87pub(crate) struct Worklet {
89 reflector: Reflector,
90 window: Dom<Window>,
91 global_type: WorkletGlobalScopeType,
92 droppable_field: DroppableField,
93}
94
95impl Worklet {
96 fn new_inherited(window: &Window, global_type: WorkletGlobalScopeType) -> Worklet {
97 Worklet {
98 reflector: Reflector::new(),
99 window: Dom::from_ref(window),
100 global_type,
101 droppable_field: DroppableField {
102 worklet_id: WorkletId::new(),
103 thread_pool: OnceCell::new(),
104 },
105 }
106 }
107
108 pub(crate) fn new(
109 window: &Window,
110 global_type: WorkletGlobalScopeType,
111 can_gc: CanGc,
112 ) -> DomRoot<Worklet> {
113 debug!("Creating worklet {:?}.", global_type);
114 reflect_dom_object(
115 Box::new(Worklet::new_inherited(window, global_type)),
116 window,
117 can_gc,
118 )
119 }
120
121 pub(crate) fn worklet_id(&self) -> WorkletId {
122 self.droppable_field.worklet_id
123 }
124
125 #[expect(dead_code)]
126 pub(crate) fn worklet_global_scope_type(&self) -> WorkletGlobalScopeType {
127 self.global_type
128 }
129}
130
131impl WorkletMethods<crate::DomTypeHolder> for Worklet {
132 fn AddModule(
134 &self,
135 module_url: USVString,
136 options: &WorkletOptions,
137 comp: InRealm,
138 can_gc: CanGc,
139 ) -> Rc<Promise> {
140 let promise = Promise::new_in_current_realm(comp, can_gc);
142
143 let module_url_record = match self.window.Document().base_url().join(&module_url.0) {
145 Ok(url) => url,
146 Err(err) => {
147 debug!("URL {:?} parse error {:?}.", module_url.0, err);
149 promise.reject_error(Error::Syntax(None), can_gc);
150 return promise;
151 },
152 };
153 debug!("Adding Worklet module {}.", module_url_record);
154
155 let pending_tasks_struct = PendingTasksStruct::new();
157 let global_scope = self.window.as_global_scope();
158
159 self.droppable_field
160 .thread_pool
161 .get_or_init(|| ScriptThread::worklet_thread_pool(self.global().image_cache()))
162 .fetch_and_invoke_a_worklet_script(
163 self.window.webview_id(),
164 self.window.pipeline_id(),
165 self.droppable_field.worklet_id,
166 self.global_type,
167 self.window.origin().immutable().clone(),
168 global_scope.api_base_url(),
169 module_url_record,
170 global_scope.policy_container(),
171 options.credentials,
172 pending_tasks_struct,
173 &promise,
174 global_scope.inherited_secure_context(),
175 );
176
177 debug!("Returning promise.");
179 promise
180 }
181}
182
183#[derive(Clone, Copy, Debug, Eq, Hash, JSTraceable, PartialEq)]
185pub(crate) struct WorkletId(#[no_trace] Uuid);
186
187malloc_size_of_is_0!(WorkletId);
188
189impl WorkletId {
190 fn new() -> WorkletId {
191 WorkletId(Uuid::new_v4())
192 }
193}
194
195#[derive(Clone, Debug)]
197struct PendingTasksStruct(Arc<AtomicIsize>);
198
199impl PendingTasksStruct {
200 fn new() -> PendingTasksStruct {
201 PendingTasksStruct(Arc::new(AtomicIsize::new(
202 WORKLET_THREAD_POOL_SIZE as isize,
203 )))
204 }
205
206 fn set_counter_to(&self, value: isize) -> isize {
207 self.0.swap(value, Ordering::AcqRel)
208 }
209
210 fn decrement_counter_by(&self, offset: isize) -> isize {
211 self.0.fetch_sub(offset, Ordering::AcqRel)
212 }
213}
214
215#[derive(Clone, JSTraceable)]
265pub(crate) struct WorkletThreadPool {
266 #[no_trace]
268 primary_sender: Sender<WorkletData>,
269 #[no_trace]
270 hot_backup_sender: Sender<WorkletData>,
271 #[no_trace]
272 cold_backup_sender: Sender<WorkletData>,
273 #[no_trace]
275 control_sender_0: Sender<WorkletControl>,
276 #[no_trace]
277 control_sender_1: Sender<WorkletControl>,
278 #[no_trace]
279 control_sender_2: Sender<WorkletControl>,
280}
281
282impl Drop for WorkletThreadPool {
283 fn drop(&mut self) {
284 let _ = self.cold_backup_sender.send(WorkletData::Quit);
285 let _ = self.hot_backup_sender.send(WorkletData::Quit);
286 let _ = self.primary_sender.send(WorkletData::Quit);
287 }
288}
289
290impl WorkletThreadPool {
291 pub(crate) fn spawn(global_init: WorkletGlobalScopeInit) -> WorkletThreadPool {
294 let primary_role = WorkletThreadRole::new(false, false);
295 let hot_backup_role = WorkletThreadRole::new(true, false);
296 let cold_backup_role = WorkletThreadRole::new(false, true);
297 let primary_sender = primary_role.sender.clone();
298 let hot_backup_sender = hot_backup_role.sender.clone();
299 let cold_backup_sender = cold_backup_role.sender.clone();
300 let init = WorkletThreadInit {
301 primary_sender: primary_sender.clone(),
302 hot_backup_sender: hot_backup_sender.clone(),
303 cold_backup_sender: cold_backup_sender.clone(),
304 global_init,
305 };
306 WorkletThreadPool {
307 primary_sender,
308 hot_backup_sender,
309 cold_backup_sender,
310 control_sender_0: WorkletThread::spawn(primary_role, init.clone(), 0),
311 control_sender_1: WorkletThread::spawn(hot_backup_role, init.clone(), 1),
312 control_sender_2: WorkletThread::spawn(cold_backup_role, init, 2),
313 }
314 }
315
316 #[allow(clippy::too_many_arguments)]
321 fn fetch_and_invoke_a_worklet_script(
322 &self,
323 webview_id: WebViewId,
324 pipeline_id: PipelineId,
325 worklet_id: WorkletId,
326 global_type: WorkletGlobalScopeType,
327 origin: ImmutableOrigin,
328 base_url: ServoUrl,
329 script_url: ServoUrl,
330 policy_container: PolicyContainer,
331 credentials: RequestCredentials,
332 pending_tasks_struct: PendingTasksStruct,
333 promise: &Rc<Promise>,
334 inherited_secure_context: Option<bool>,
335 ) {
336 for sender in &[
338 &self.control_sender_0,
339 &self.control_sender_1,
340 &self.control_sender_2,
341 ] {
342 let _ = sender.send(WorkletControl::FetchAndInvokeAWorkletScript {
343 webview_id,
344 pipeline_id,
345 worklet_id,
346 global_type,
347 origin: origin.clone(),
348 base_url: base_url.clone(),
349 script_url: script_url.clone(),
350 policy_container: policy_container.clone(),
351 credentials,
352 pending_tasks_struct: pending_tasks_struct.clone(),
353 promise: TrustedPromise::new(promise.clone()),
354 inherited_secure_context,
355 });
356 }
357 self.wake_threads();
358 }
359
360 pub(crate) fn exit_worklet(&self, worklet_id: WorkletId) {
361 for sender in &[
362 &self.control_sender_0,
363 &self.control_sender_1,
364 &self.control_sender_2,
365 ] {
366 let _ = sender.send(WorkletControl::ExitWorklet(worklet_id));
367 }
368 self.wake_threads();
369 }
370
371 #[cfg(feature = "testbinding")]
373 pub(crate) fn test_worklet_lookup(&self, id: WorkletId, key: String) -> Option<String> {
374 let (sender, receiver) = unbounded();
375 let msg = WorkletData::Task(id, WorkletTask::Test(TestWorkletTask::Lookup(key, sender)));
376 let _ = self.primary_sender.send(msg);
377 receiver.recv().expect("Test worklet has died?")
378 }
379
380 fn wake_threads(&self) {
381 let _ = self.cold_backup_sender.send(WorkletData::WakeUp);
383 let _ = self.hot_backup_sender.send(WorkletData::WakeUp);
384 let _ = self.primary_sender.send(WorkletData::WakeUp);
385 }
386}
387
388enum WorkletData {
390 Task(WorkletId, WorkletTask),
391 StartSwapRoles(Sender<WorkletData>),
392 FinishSwapRoles(Swapper<WorkletThreadRole>),
393 WakeUp,
394 Quit,
395}
396
397enum WorkletControl {
399 ExitWorklet(WorkletId),
400 FetchAndInvokeAWorkletScript {
401 webview_id: WebViewId,
402 pipeline_id: PipelineId,
403 worklet_id: WorkletId,
404 global_type: WorkletGlobalScopeType,
405 origin: ImmutableOrigin,
406 base_url: ServoUrl,
407 script_url: ServoUrl,
408 policy_container: PolicyContainer,
409 credentials: RequestCredentials,
410 pending_tasks_struct: PendingTasksStruct,
411 promise: TrustedPromise,
412 inherited_secure_context: Option<bool>,
413 },
414}
415
416struct WorkletThreadRole {
423 receiver: Receiver<WorkletData>,
424 sender: Sender<WorkletData>,
425 is_hot_backup: bool,
426 is_cold_backup: bool,
427}
428
429impl WorkletThreadRole {
430 fn new(is_hot_backup: bool, is_cold_backup: bool) -> WorkletThreadRole {
431 let (sender, receiver) = unbounded();
432 WorkletThreadRole {
433 sender,
434 receiver,
435 is_hot_backup,
436 is_cold_backup,
437 }
438 }
439}
440
441#[derive(Clone)]
443struct WorkletThreadInit {
444 primary_sender: Sender<WorkletData>,
446 hot_backup_sender: Sender<WorkletData>,
447 cold_backup_sender: Sender<WorkletData>,
448
449 global_init: WorkletGlobalScopeInit,
451}
452
453struct WorkletCspProcessor {}
454
455impl CspViolationsProcessor for WorkletCspProcessor {
456 fn process_csp_violations(&self, _violations: Vec<Violation>) {}
457}
458
459#[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)]
461struct WorkletThread {
462 role: WorkletThreadRole,
464
465 control_receiver: Receiver<WorkletControl>,
467
468 primary_sender: Sender<WorkletData>,
470 hot_backup_sender: Sender<WorkletData>,
471 cold_backup_sender: Sender<WorkletData>,
472
473 global_init: WorkletGlobalScopeInit,
475
476 global_scopes: FxHashMap<WorkletId, Dom<WorkletGlobalScope>>,
478
479 control_buffer: Option<WorkletControl>,
481
482 runtime: Runtime,
484 should_gc: bool,
485 gc_threshold: u32,
486}
487
488#[expect(unsafe_code)]
489unsafe impl JSTraceable for WorkletThread {
490 unsafe fn trace(&self, trc: *mut JSTracer) {
491 debug!("Tracing worklet thread.");
492 unsafe { self.global_scopes.trace(trc) };
493 }
494}
495
496impl WorkletThread {
497 #[cfg_attr(crown, allow(crown::unrooted_must_root))]
499 fn spawn(
500 role: WorkletThreadRole,
501 init: WorkletThreadInit,
502 thread_index: u8,
503 ) -> Sender<WorkletControl> {
504 let (control_sender, control_receiver) = unbounded();
505 let _ = thread::Builder::new()
506 .name(format!("Worklet#{thread_index}"))
507 .spawn(move || {
508 debug!("Initializing worklet thread.");
512 thread_state::initialize(ThreadState::SCRIPT | ThreadState::IN_WORKER);
513 let mut thread = RootedTraceableBox::new(WorkletThread {
514 role,
515 control_receiver,
516 primary_sender: init.primary_sender,
517 hot_backup_sender: init.hot_backup_sender,
518 cold_backup_sender: init.cold_backup_sender,
519 global_init: init.global_init,
520 global_scopes: FxHashMap::default(),
521 control_buffer: None,
522 runtime: Runtime::new(None),
523 should_gc: false,
524 gc_threshold: MIN_GC_THRESHOLD,
525 });
526 thread.run(CanGc::note());
527 })
528 .expect("Couldn't start worklet thread");
529 control_sender
530 }
531
532 fn run(&mut self, can_gc: CanGc) {
534 loop {
535 let message = self.role.receiver.recv().unwrap();
537 match message {
538 WorkletData::Task(id, task) => {
540 self.perform_a_worklet_task(id, task);
541 },
542 WorkletData::StartSwapRoles(sender) => {
549 let (our_swapper, their_swapper) = swapper();
550 match sender.send(WorkletData::FinishSwapRoles(their_swapper)) {
551 Ok(_) => {},
552 Err(_) => {
553 return;
556 },
557 };
558 let _ = our_swapper.swap(&mut self.role);
559 },
560 WorkletData::FinishSwapRoles(swapper) => {
563 let _ = swapper.swap(&mut self.role);
564 },
565 WorkletData::WakeUp => {},
567 WorkletData::Quit => {
569 return;
570 },
571 }
572 if self.role.is_cold_backup {
576 if let Some(control) = self.control_buffer.take() {
577 self.process_control(control, can_gc);
578 }
579 while let Ok(control) = self.control_receiver.try_recv() {
580 self.process_control(control, can_gc);
581 }
582 self.gc();
583 } else if self.control_buffer.is_none() {
584 if let Ok(control) = self.control_receiver.try_recv() {
585 self.control_buffer = Some(control);
586 let msg = WorkletData::StartSwapRoles(self.role.sender.clone());
587 let _ = self.cold_backup_sender.send(msg);
588 }
589 }
590 if self.current_memory_usage() > self.gc_threshold {
594 if self.role.is_hot_backup || self.role.is_cold_backup {
595 self.should_gc = false;
596 self.gc();
597 } else if !self.should_gc {
598 self.should_gc = true;
599 let msg = WorkletData::StartSwapRoles(self.role.sender.clone());
600 let _ = self.hot_backup_sender.send(msg);
601 }
602 }
603 }
604 }
605
606 #[expect(unsafe_code)]
608 fn current_memory_usage(&self) -> u32 {
609 unsafe { JS_GetGCParameter(self.runtime.cx_no_gc(), JSGCParamKey::JSGC_BYTES) }
610 }
611
612 #[expect(unsafe_code)]
614 fn gc(&mut self) {
615 debug!(
616 "BEGIN GC (usage = {}, threshold = {}).",
617 self.current_memory_usage(),
618 self.gc_threshold
619 );
620 unsafe { JS_GC(self.runtime.cx(), GCReason::API) };
621 self.gc_threshold = max(MIN_GC_THRESHOLD, self.current_memory_usage() * 2);
622 debug!(
623 "END GC (usage = {}, threshold = {}).",
624 self.current_memory_usage(),
625 self.gc_threshold
626 );
627 }
628
629 fn get_worklet_global_scope(
632 &mut self,
633 webview_id: WebViewId,
634 pipeline_id: PipelineId,
635 worklet_id: WorkletId,
636 inherited_secure_context: Option<bool>,
637 global_type: WorkletGlobalScopeType,
638 base_url: ServoUrl,
639 ) -> DomRoot<WorkletGlobalScope> {
640 match self.global_scopes.entry(worklet_id) {
641 hash_map::Entry::Occupied(entry) => DomRoot::from_ref(entry.get()),
642 hash_map::Entry::Vacant(entry) => {
643 debug!("Creating new worklet global scope.");
644 let executor = WorkletExecutor::new(worklet_id, self.primary_sender.clone());
645 let result = WorkletGlobalScope::new(
646 global_type,
647 webview_id,
648 pipeline_id,
649 base_url,
650 inherited_secure_context,
651 executor,
652 &self.global_init,
653 );
654 entry.insert(Dom::from_ref(&*result));
655 result
656 },
657 }
658 }
659
660 #[allow(clippy::too_many_arguments)]
663 fn fetch_and_invoke_a_worklet_script(
664 &self,
665 global_scope: &WorkletGlobalScope,
666 pipeline_id: PipelineId,
667 origin: ImmutableOrigin,
668 script_url: ServoUrl,
669 policy_container: PolicyContainer,
670 credentials: RequestCredentials,
671 pending_tasks_struct: PendingTasksStruct,
672 promise: TrustedPromise,
673 can_gc: CanGc,
674 ) {
675 debug!("Fetching from {}.", script_url);
676 let global = global_scope.upcast::<GlobalScope>();
684 let resource_fetcher = self.global_init.resource_threads.sender();
685 let request = RequestBuilder::new(None, script_url, global.get_referrer())
686 .destination(Destination::Script)
687 .mode(RequestMode::CorsMode)
688 .credentials_mode(credentials.convert())
689 .policy_container(policy_container)
690 .origin(origin);
691
692 let script = load_whole_resource(
693 request,
694 &resource_fetcher,
695 global,
696 &WorkletCspProcessor {},
697 can_gc,
698 )
699 .ok()
700 .and_then(|(_, bytes)| String::from_utf8(bytes).ok());
701
702 let ok = script.is_some_and(|s| global_scope.evaluate_js(s.into(), can_gc).is_ok());
709
710 if !ok {
711 debug!("Failed to load script.");
713 let old_counter = pending_tasks_struct.set_counter_to(-1);
714 if old_counter > 0 {
715 self.run_in_script_thread(promise.reject_task(Error::Abort));
716 }
717 } else {
718 debug!("Finished adding script.");
720 let old_counter = pending_tasks_struct.decrement_counter_by(1);
721 if old_counter == 1 {
722 debug!("Resolving promise.");
723 let msg = MainThreadScriptMsg::WorkletLoaded(pipeline_id);
724 self.global_init
725 .to_script_thread_sender
726 .send(msg)
727 .expect("Worklet thread outlived script thread.");
728 self.run_in_script_thread(promise.resolve_task(()));
729 }
730 }
731 }
732
733 fn perform_a_worklet_task(&self, worklet_id: WorkletId, task: WorkletTask) {
735 match self.global_scopes.get(&worklet_id) {
736 Some(global) => global.perform_a_worklet_task(task),
737 None => warn!("No such worklet as {:?}.", worklet_id),
738 }
739 }
740
741 fn process_control(&mut self, control: WorkletControl, can_gc: CanGc) {
743 match control {
744 WorkletControl::ExitWorklet(worklet_id) => {
745 self.global_scopes.remove(&worklet_id);
746 },
747 WorkletControl::FetchAndInvokeAWorkletScript {
748 webview_id,
749 pipeline_id,
750 worklet_id,
751 global_type,
752 origin,
753 base_url,
754 script_url,
755 policy_container,
756 credentials,
757 pending_tasks_struct,
758 promise,
759 inherited_secure_context,
760 } => {
761 let global = self.get_worklet_global_scope(
762 webview_id,
763 pipeline_id,
764 worklet_id,
765 inherited_secure_context,
766 global_type,
767 base_url,
768 );
769 self.fetch_and_invoke_a_worklet_script(
770 &global,
771 pipeline_id,
772 origin,
773 script_url,
774 policy_container,
775 credentials,
776 pending_tasks_struct,
777 promise,
778 can_gc,
779 )
780 },
781 }
782 }
783
784 fn run_in_script_thread<T>(&self, task: T)
786 where
787 T: TaskBox + 'static,
788 {
789 let msg = CommonScriptMsg::Task(
792 ScriptThreadEventCategory::WorkletEvent,
793 Box::new(task),
794 None,
795 TaskSourceName::DOMManipulation,
796 );
797 let msg = MainThreadScriptMsg::Common(msg);
798 self.global_init
799 .to_script_thread_sender
800 .send(msg)
801 .expect("Worklet thread outlived script thread.");
802 }
803}
804
805#[derive(Clone, JSTraceable, MallocSizeOf)]
807pub(crate) struct WorkletExecutor {
808 worklet_id: WorkletId,
809 #[no_trace]
810 primary_sender: Sender<WorkletData>,
811}
812
813impl WorkletExecutor {
814 fn new(worklet_id: WorkletId, primary_sender: Sender<WorkletData>) -> WorkletExecutor {
815 WorkletExecutor {
816 worklet_id,
817 primary_sender,
818 }
819 }
820
821 pub(crate) fn schedule_a_worklet_task(&self, task: WorkletTask) {
823 let _ = self
824 .primary_sender
825 .send(WorkletData::Task(self.worklet_id, task));
826 }
827}