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 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
67const 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 #[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]
90pub(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 fn AddModule(
138 &self,
139 realm: &mut CurrentRealm,
140 module_url: USVString,
141 options: &WorkletOptions,
142 ) -> Rc<Promise> {
143 let promise = Promise::new_in_realm(realm);
145
146 let module_url_record = match self.window.Document().base_url().join(&module_url.0) {
148 Ok(url) => url,
149 Err(err) => {
150 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 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 debug!("Returning promise.");
182 promise
183 }
184}
185
186#[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#[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#[derive(Clone, JSTraceable)]
268pub(crate) struct WorkletThreadPool {
269 #[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 #[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 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 #[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 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 #[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 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
391enum WorkletData {
393 Task(WorkletId, WorkletTask),
394 StartSwapRoles(Sender<WorkletData>),
395 FinishSwapRoles(Swapper<WorkletThreadRole>),
396 WakeUp,
397 Quit,
398}
399
400enum 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
419struct 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#[derive(Clone)]
446struct WorkletThreadInit {
447 primary_sender: Sender<WorkletData>,
449 hot_backup_sender: Sender<WorkletData>,
450 cold_backup_sender: Sender<WorkletData>,
451
452 global_init: WorkletGlobalScopeInit,
454}
455
456struct WorkletCspProcessor {}
457
458impl CspViolationsProcessor for WorkletCspProcessor {
459 fn process_csp_violations(&self, _violations: Vec<Violation>) {}
460}
461
462#[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)]
464struct WorkletThread {
465 role: WorkletThreadRole,
467
468 control_receiver: Receiver<WorkletControl>,
470
471 primary_sender: Sender<WorkletData>,
473 hot_backup_sender: Sender<WorkletData>,
474 cold_backup_sender: Sender<WorkletData>,
475
476 global_init: WorkletGlobalScopeInit,
478
479 global_scopes: FxHashMap<WorkletId, Dom<WorkletGlobalScope>>,
481
482 control_buffer: Option<WorkletControl>,
484
485 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 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 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 fn run(&mut self, cx: &mut JSContext) {
539 loop {
540 let message = self.role.receiver.recv().unwrap();
542 match message {
543 WorkletData::Task(id, task) => {
545 self.perform_a_worklet_task(cx, id, task);
546 },
547 WorkletData::StartSwapRoles(sender) => {
554 let (our_swapper, their_swapper) = swapper();
555 match sender.send(WorkletData::FinishSwapRoles(their_swapper)) {
556 Ok(_) => {},
557 Err(_) => {
558 return;
561 },
562 };
563 let _ = our_swapper.swap(&mut self.role);
564 },
565 WorkletData::FinishSwapRoles(swapper) => {
568 let _ = swapper.swap(&mut self.role);
569 },
570 WorkletData::WakeUp => {},
572 WorkletData::Quit => {
574 return;
575 },
576 }
577 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 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 #[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 #[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 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 #[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 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 let ok = script.is_some_and(|s| global_scope.evaluate_js(s.into(), cx).is_ok());
721
722 if !ok {
723 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 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 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 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 fn run_in_script_thread<T>(&self, task: T)
799 where
800 T: TaskBox + 'static,
801 {
802 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#[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 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}