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::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
65const 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 #[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]
88pub(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 fn AddModule(
136 &self,
137 module_url: USVString,
138 options: &WorkletOptions,
139 comp: InRealm,
140 can_gc: CanGc,
141 ) -> Rc<Promise> {
142 let promise = Promise::new_in_current_realm(comp, can_gc);
144
145 let module_url_record = match self.window.Document().base_url().join(&module_url.0) {
147 Ok(url) => url,
148 Err(err) => {
149 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 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 debug!("Returning promise.");
181 promise
182 }
183}
184
185#[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#[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#[derive(Clone, JSTraceable)]
267pub(crate) struct WorkletThreadPool {
268 #[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 #[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 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 #[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 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 #[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 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
390enum WorkletData {
392 Task(WorkletId, WorkletTask),
393 StartSwapRoles(Sender<WorkletData>),
394 FinishSwapRoles(Swapper<WorkletThreadRole>),
395 WakeUp,
396 Quit,
397}
398
399enum 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
418struct 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#[derive(Clone)]
445struct WorkletThreadInit {
446 primary_sender: Sender<WorkletData>,
448 hot_backup_sender: Sender<WorkletData>,
449 cold_backup_sender: Sender<WorkletData>,
450
451 global_init: WorkletGlobalScopeInit,
453}
454
455struct WorkletCspProcessor {}
456
457impl CspViolationsProcessor for WorkletCspProcessor {
458 fn process_csp_violations(&self, _violations: Vec<Violation>) {}
459}
460
461#[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)]
463struct WorkletThread {
464 role: WorkletThreadRole,
466
467 control_receiver: Receiver<WorkletControl>,
469
470 primary_sender: Sender<WorkletData>,
472 hot_backup_sender: Sender<WorkletData>,
473 cold_backup_sender: Sender<WorkletData>,
474
475 global_init: WorkletGlobalScopeInit,
477
478 global_scopes: FxHashMap<WorkletId, Dom<WorkletGlobalScope>>,
480
481 control_buffer: Option<WorkletControl>,
483
484 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 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 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 fn run(&mut self, cx: &mut js::context::JSContext) {
538 loop {
539 let message = self.role.receiver.recv().unwrap();
541 match message {
542 WorkletData::Task(id, task) => {
544 self.perform_a_worklet_task(id, task);
545 },
546 WorkletData::StartSwapRoles(sender) => {
553 let (our_swapper, their_swapper) = swapper();
554 match sender.send(WorkletData::FinishSwapRoles(their_swapper)) {
555 Ok(_) => {},
556 Err(_) => {
557 return;
560 },
561 };
562 let _ = our_swapper.swap(&mut self.role);
563 },
564 WorkletData::FinishSwapRoles(swapper) => {
567 let _ = swapper.swap(&mut self.role);
568 },
569 WorkletData::WakeUp => {},
571 WorkletData::Quit => {
573 return;
574 },
575 }
576 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 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 #[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 #[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 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 #[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 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 let ok = script.is_some_and(|s| global_scope.evaluate_js(s.into(), cx).is_ok());
720
721 if !ok {
722 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 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 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 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 fn run_in_script_thread<T>(&self, task: T)
798 where
799 T: TaskBox + 'static,
800 {
801 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#[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 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}