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;
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::{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(realm, Error::Syntax(None));
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.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 pipeline_id: PipelineId,
326 worklet_id: WorkletId,
327 global_type: WorkletGlobalScopeType,
328 origin: ImmutableOrigin,
329 base_url: ServoUrl,
330 script_url: ServoUrl,
331 policy_container: PolicyContainer,
332 credentials: RequestCredentials,
333 pending_tasks_struct: PendingTasksStruct,
334 promise: &Rc<Promise>,
335 inherited_secure_context: Option<bool>,
336 ) {
337 for sender in &[
339 &self.control_sender_0,
340 &self.control_sender_1,
341 &self.control_sender_2,
342 ] {
343 let _ = sender.send(WorkletControl::FetchAndInvokeAWorkletScript {
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 pipeline_id: PipelineId,
402 worklet_id: WorkletId,
403 global_type: WorkletGlobalScopeType,
404 origin: ImmutableOrigin,
405 base_url: ServoUrl,
406 script_url: ServoUrl,
407 policy_container: PolicyContainer,
408 credentials: RequestCredentials,
409 pending_tasks_struct: PendingTasksStruct,
410 promise: TrustedPromise,
411 inherited_secure_context: Option<bool>,
412 },
413}
414
415struct WorkletThreadRole {
422 receiver: Receiver<WorkletData>,
423 sender: Sender<WorkletData>,
424 is_hot_backup: bool,
425 is_cold_backup: bool,
426}
427
428impl WorkletThreadRole {
429 fn new(is_hot_backup: bool, is_cold_backup: bool) -> WorkletThreadRole {
430 let (sender, receiver) = unbounded();
431 WorkletThreadRole {
432 sender,
433 receiver,
434 is_hot_backup,
435 is_cold_backup,
436 }
437 }
438}
439
440#[derive(Clone)]
442struct WorkletThreadInit {
443 primary_sender: Sender<WorkletData>,
445 hot_backup_sender: Sender<WorkletData>,
446 cold_backup_sender: Sender<WorkletData>,
447
448 global_init: WorkletGlobalScopeInit,
450}
451
452struct WorkletCspProcessor {}
453
454impl CspViolationsProcessor for WorkletCspProcessor {
455 fn process_csp_violations(&self, _cx: &mut JSContext, _violations: Vec<Violation>) {}
456}
457
458#[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)]
460struct WorkletThread {
461 role: WorkletThreadRole,
463
464 control_receiver: Receiver<WorkletControl>,
466
467 primary_sender: Sender<WorkletData>,
469 hot_backup_sender: Sender<WorkletData>,
470 cold_backup_sender: Sender<WorkletData>,
471
472 global_init: WorkletGlobalScopeInit,
474
475 global_scopes: FxHashMap<WorkletId, Dom<WorkletGlobalScope>>,
477
478 control_buffer: Option<WorkletControl>,
480
481 runtime: Runtime,
483 should_gc: bool,
484 gc_threshold: u32,
485}
486
487#[expect(unsafe_code)]
488unsafe impl JSTraceable for WorkletThread {
489 unsafe fn trace(&self, trc: *mut JSTracer) {
490 debug!("Tracing worklet thread.");
491 unsafe { self.global_scopes.trace(trc) };
492 }
493}
494
495impl WorkletThread {
496 #[allow(unsafe_code)]
497 fn spawn(
499 role: WorkletThreadRole,
500 init: WorkletThreadInit,
501 thread_index: u8,
502 ) -> Sender<WorkletControl> {
503 let (control_sender, control_receiver) = unbounded();
504 let _ = thread::Builder::new()
505 .name(format!("Worklet#{thread_index}"))
506 .spawn(move || {
507 debug!("Initializing worklet thread.");
511 thread_state::initialize(ThreadState::SCRIPT | ThreadState::IN_WORKER);
512 let runtime = Runtime::new(None);
513 let mut cx = unsafe { runtime.cx() };
514 let mut thread = RootedTraceableBox::new(WorkletThread {
515 role,
516 control_receiver,
517 primary_sender: init.primary_sender,
518 hot_backup_sender: init.hot_backup_sender,
519 cold_backup_sender: init.cold_backup_sender,
520 global_init: init.global_init,
521 global_scopes: FxHashMap::default(),
522 control_buffer: None,
523 runtime,
524 should_gc: false,
525 gc_threshold: MIN_GC_THRESHOLD,
526 });
527 thread.run(&mut cx);
528 })
529 .expect("Couldn't start worklet thread");
530 control_sender
531 }
532
533 fn run(&mut self, cx: &mut JSContext) {
535 loop {
536 let message = self.role.receiver.recv().unwrap();
538 match message {
539 WorkletData::Task(id, task) => {
541 self.perform_a_worklet_task(cx, id, task);
542 },
543 WorkletData::StartSwapRoles(sender) => {
550 let (our_swapper, their_swapper) = swapper();
551 match sender.send(WorkletData::FinishSwapRoles(their_swapper)) {
552 Ok(_) => {},
553 Err(_) => {
554 return;
557 },
558 };
559 let _ = our_swapper.swap(&mut self.role);
560 },
561 WorkletData::FinishSwapRoles(swapper) => {
564 let _ = swapper.swap(&mut self.role);
565 },
566 WorkletData::WakeUp => {},
568 WorkletData::Quit => {
570 return;
571 },
572 }
573 if self.role.is_cold_backup {
577 if let Some(control) = self.control_buffer.take() {
578 self.process_control(control, cx);
579 }
580 while let Ok(control) = self.control_receiver.try_recv() {
581 self.process_control(control, cx);
582 }
583 self.gc(cx);
584 } else if self.control_buffer.is_none() &&
585 let Ok(control) = self.control_receiver.try_recv()
586 {
587 self.control_buffer = Some(control);
588 let msg = WorkletData::StartSwapRoles(self.role.sender.clone());
589 let _ = self.cold_backup_sender.send(msg);
590 }
591 if self.current_memory_usage() > self.gc_threshold {
595 if self.role.is_hot_backup || self.role.is_cold_backup {
596 self.should_gc = false;
597 self.gc(cx);
598 } else if !self.should_gc {
599 self.should_gc = true;
600 let msg = WorkletData::StartSwapRoles(self.role.sender.clone());
601 let _ = self.hot_backup_sender.send(msg);
602 }
603 }
604 }
605 }
606
607 #[expect(unsafe_code)]
609 fn current_memory_usage(&self) -> u32 {
610 unsafe { JS_GetGCParameter(self.runtime.cx_no_gc(), JSGCParamKey::JSGC_BYTES) }
611 }
612
613 #[expect(unsafe_code)]
615 fn gc(&mut self, cx: &mut JSContext) {
616 debug!(
617 "BEGIN GC (usage = {}, threshold = {}).",
618 self.current_memory_usage(),
619 self.gc_threshold
620 );
621 unsafe { JS_GC(cx, GCReason::API) };
622 self.gc_threshold = max(MIN_GC_THRESHOLD, self.current_memory_usage() * 2);
623 debug!(
624 "END GC (usage = {}, threshold = {}).",
625 self.current_memory_usage(),
626 self.gc_threshold
627 );
628 }
629
630 fn get_worklet_global_scope(
633 &mut self,
634 pipeline_id: PipelineId,
635 worklet_id: WorkletId,
636 inherited_secure_context: Option<bool>,
637 global_type: WorkletGlobalScopeType,
638 base_url: ServoUrl,
639 cx: &mut JSContext,
640 ) -> DomRoot<WorkletGlobalScope> {
641 match self.global_scopes.entry(worklet_id) {
642 hash_map::Entry::Occupied(entry) => DomRoot::from_ref(entry.get()),
643 hash_map::Entry::Vacant(entry) => {
644 debug!("Creating new worklet global scope.");
645 let executor = WorkletExecutor::new(worklet_id, self.primary_sender.clone());
646 let result = WorkletGlobalScope::new(
647 global_type,
648 pipeline_id,
649 base_url,
650 inherited_secure_context,
651 executor,
652 &self.global_init,
653 cx,
654 );
655 entry.insert(Dom::from_ref(&*result));
656 result
657 },
658 }
659 }
660
661 #[allow(clippy::too_many_arguments)]
664 fn fetch_and_invoke_a_worklet_script(
665 &self,
666 global_scope: &WorkletGlobalScope,
667 pipeline_id: PipelineId,
668 origin: ImmutableOrigin,
669 script_url: ServoUrl,
670 policy_container: PolicyContainer,
671 credentials: RequestCredentials,
672 pending_tasks_struct: PendingTasksStruct,
673 promise: TrustedPromise,
674 cx: &mut JSContext,
675 ) {
676 debug!("Fetching from {}.", script_url);
677 let global = global_scope.upcast::<GlobalScope>();
685 let resource_fetcher = self.global_init.resource_threads.sender();
686 let request = RequestBuilder::new(
687 None,
688 UrlWithBlobClaim::from_url_without_having_claimed_blob(script_url),
689 global.get_referrer(),
690 )
691 .destination(Destination::Script)
692 .mode(RequestMode::CorsMode)
693 .credentials_mode(credentials.convert())
694 .policy_container(policy_container)
695 .origin(origin);
696
697 let script = load_whole_resource(
698 request,
699 &resource_fetcher,
700 global,
701 &WorkletCspProcessor {},
702 cx,
703 )
704 .ok()
705 .and_then(|(_, bytes, _)| String::from_utf8(bytes).ok());
706
707 let ok = script.is_some_and(|s| global_scope.evaluate_js(s.into(), cx).is_ok());
714
715 if !ok {
716 debug!("Failed to load script.");
718 let old_counter = pending_tasks_struct.set_counter_to(-1);
719 if old_counter > 0 {
720 self.run_in_script_thread(promise.reject_task(Error::Abort(None)));
721 }
722 } else {
723 debug!("Finished adding script.");
725 let old_counter = pending_tasks_struct.decrement_counter_by(1);
726 if old_counter == 1 {
727 debug!("Resolving promise.");
728 let msg = MainThreadScriptMsg::WorkletLoaded(pipeline_id);
729 self.global_init
730 .to_script_thread_sender
731 .send(msg)
732 .expect("Worklet thread outlived script thread.");
733 self.run_in_script_thread(promise.resolve_task(()));
734 }
735 }
736 }
737
738 fn perform_a_worklet_task(&self, cx: &mut JSContext, worklet_id: WorkletId, task: WorkletTask) {
740 match self.global_scopes.get(&worklet_id) {
741 Some(global) => global.perform_a_worklet_task(cx, task),
742 None => warn!("No such worklet as {:?}.", worklet_id),
743 }
744 }
745
746 fn process_control(&mut self, control: WorkletControl, cx: &mut js::context::JSContext) {
748 match control {
749 WorkletControl::ExitWorklet(worklet_id) => {
750 self.global_scopes.remove(&worklet_id);
751 },
752 WorkletControl::FetchAndInvokeAWorkletScript {
753 pipeline_id,
754 worklet_id,
755 global_type,
756 origin,
757 base_url,
758 script_url,
759 policy_container,
760 credentials,
761 pending_tasks_struct,
762 promise,
763 inherited_secure_context,
764 } => {
765 let global = self.get_worklet_global_scope(
766 pipeline_id,
767 worklet_id,
768 inherited_secure_context,
769 global_type,
770 base_url,
771 cx,
772 );
773 self.fetch_and_invoke_a_worklet_script(
774 &global,
775 pipeline_id,
776 origin,
777 script_url,
778 policy_container,
779 credentials,
780 pending_tasks_struct,
781 promise,
782 cx,
783 )
784 },
785 }
786 }
787
788 fn run_in_script_thread<T>(&self, task: T)
790 where
791 T: TaskBox + 'static,
792 {
793 let msg = CommonScriptMsg::Task(
796 ScriptThreadEventCategory::WorkletEvent,
797 Box::new(task),
798 None,
799 TaskSourceName::DOMManipulation,
800 );
801 let msg = MainThreadScriptMsg::Common(msg);
802 self.global_init
803 .to_script_thread_sender
804 .send(msg)
805 .expect("Worklet thread outlived script thread.");
806 }
807}
808
809#[derive(Clone, JSTraceable, MallocSizeOf)]
811pub(crate) struct WorkletExecutor {
812 worklet_id: WorkletId,
813 #[no_trace]
814 primary_sender: Sender<WorkletData>,
815}
816
817impl WorkletExecutor {
818 fn new(worklet_id: WorkletId, primary_sender: Sender<WorkletData>) -> WorkletExecutor {
819 WorkletExecutor {
820 worklet_id,
821 primary_sender,
822 }
823 }
824
825 pub(crate) fn schedule_a_worklet_task(&self, task: WorkletTask) {
827 let _ = self
828 .primary_sender
829 .send(WorkletData::Task(self.worklet_id, task));
830 }
831}