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::generic_channel::GenericSend;
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 #[cfg(feature = "testbinding")]
122 pub(crate) fn worklet_id(&self) -> WorkletId {
123 self.droppable_field.worklet_id
124 }
125
126 #[expect(dead_code)]
127 pub(crate) fn worklet_global_scope_type(&self) -> WorkletGlobalScopeType {
128 self.global_type
129 }
130}
131
132impl WorkletMethods<crate::DomTypeHolder> for Worklet {
133 fn AddModule(
135 &self,
136 module_url: USVString,
137 options: &WorkletOptions,
138 comp: InRealm,
139 can_gc: CanGc,
140 ) -> Rc<Promise> {
141 let promise = Promise::new_in_current_realm(comp, can_gc);
143
144 let module_url_record = match self.window.Document().base_url().join(&module_url.0) {
146 Ok(url) => url,
147 Err(err) => {
148 debug!("URL {:?} parse error {:?}.", module_url.0, err);
150 promise.reject_error(Error::Syntax(None), can_gc);
151 return promise;
152 },
153 };
154 debug!("Adding Worklet module {}.", module_url_record);
155
156 let pending_tasks_struct = PendingTasksStruct::new();
158 let global_scope = self.window.as_global_scope();
159
160 self.droppable_field
161 .thread_pool
162 .get_or_init(|| ScriptThread::worklet_thread_pool(self.global().image_cache()))
163 .fetch_and_invoke_a_worklet_script(
164 self.window.webview_id(),
165 self.window.pipeline_id(),
166 self.droppable_field.worklet_id,
167 self.global_type,
168 self.window.origin().immutable().clone(),
169 global_scope.api_base_url(),
170 module_url_record,
171 global_scope.policy_container(),
172 options.credentials,
173 pending_tasks_struct,
174 &promise,
175 global_scope.inherited_secure_context(),
176 );
177
178 debug!("Returning promise.");
180 promise
181 }
182}
183
184#[derive(Clone, Copy, Debug, Eq, Hash, JSTraceable, PartialEq)]
186pub(crate) struct WorkletId(#[no_trace] Uuid);
187
188malloc_size_of_is_0!(WorkletId);
189
190impl WorkletId {
191 fn new() -> WorkletId {
192 WorkletId(Uuid::new_v4())
193 }
194}
195
196#[derive(Clone, Debug)]
198struct PendingTasksStruct(Arc<AtomicIsize>);
199
200impl PendingTasksStruct {
201 fn new() -> PendingTasksStruct {
202 PendingTasksStruct(Arc::new(AtomicIsize::new(
203 WORKLET_THREAD_POOL_SIZE as isize,
204 )))
205 }
206
207 fn set_counter_to(&self, value: isize) -> isize {
208 self.0.swap(value, Ordering::AcqRel)
209 }
210
211 fn decrement_counter_by(&self, offset: isize) -> isize {
212 self.0.fetch_sub(offset, Ordering::AcqRel)
213 }
214}
215
216#[derive(Clone, JSTraceable)]
266pub(crate) struct WorkletThreadPool {
267 #[no_trace]
269 primary_sender: Sender<WorkletData>,
270 #[no_trace]
271 hot_backup_sender: Sender<WorkletData>,
272 #[no_trace]
273 cold_backup_sender: Sender<WorkletData>,
274 #[no_trace]
276 control_sender_0: Sender<WorkletControl>,
277 #[no_trace]
278 control_sender_1: Sender<WorkletControl>,
279 #[no_trace]
280 control_sender_2: Sender<WorkletControl>,
281}
282
283impl Drop for WorkletThreadPool {
284 fn drop(&mut self) {
285 let _ = self.cold_backup_sender.send(WorkletData::Quit);
286 let _ = self.hot_backup_sender.send(WorkletData::Quit);
287 let _ = self.primary_sender.send(WorkletData::Quit);
288 }
289}
290
291impl WorkletThreadPool {
292 pub(crate) fn spawn(global_init: WorkletGlobalScopeInit) -> WorkletThreadPool {
295 let primary_role = WorkletThreadRole::new(false, false);
296 let hot_backup_role = WorkletThreadRole::new(true, false);
297 let cold_backup_role = WorkletThreadRole::new(false, true);
298 let primary_sender = primary_role.sender.clone();
299 let hot_backup_sender = hot_backup_role.sender.clone();
300 let cold_backup_sender = cold_backup_role.sender.clone();
301 let init = WorkletThreadInit {
302 primary_sender: primary_sender.clone(),
303 hot_backup_sender: hot_backup_sender.clone(),
304 cold_backup_sender: cold_backup_sender.clone(),
305 global_init,
306 };
307 WorkletThreadPool {
308 primary_sender,
309 hot_backup_sender,
310 cold_backup_sender,
311 control_sender_0: WorkletThread::spawn(primary_role, init.clone(), 0),
312 control_sender_1: WorkletThread::spawn(hot_backup_role, init.clone(), 1),
313 control_sender_2: WorkletThread::spawn(cold_backup_role, init, 2),
314 }
315 }
316
317 #[allow(clippy::too_many_arguments)]
322 fn fetch_and_invoke_a_worklet_script(
323 &self,
324 webview_id: WebViewId,
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 webview_id,
345 pipeline_id,
346 worklet_id,
347 global_type,
348 origin: origin.clone(),
349 base_url: base_url.clone(),
350 script_url: script_url.clone(),
351 policy_container: policy_container.clone(),
352 credentials,
353 pending_tasks_struct: pending_tasks_struct.clone(),
354 promise: TrustedPromise::new(promise.clone()),
355 inherited_secure_context,
356 });
357 }
358 self.wake_threads();
359 }
360
361 pub(crate) fn exit_worklet(&self, worklet_id: WorkletId) {
362 for sender in &[
363 &self.control_sender_0,
364 &self.control_sender_1,
365 &self.control_sender_2,
366 ] {
367 let _ = sender.send(WorkletControl::ExitWorklet(worklet_id));
368 }
369 self.wake_threads();
370 }
371
372 #[cfg(feature = "testbinding")]
374 pub(crate) fn test_worklet_lookup(&self, id: WorkletId, key: String) -> Option<String> {
375 let (sender, receiver) = unbounded();
376 let msg = WorkletData::Task(id, WorkletTask::Test(TestWorkletTask::Lookup(key, sender)));
377 let _ = self.primary_sender.send(msg);
378 receiver.recv().expect("Test worklet has died?")
379 }
380
381 fn wake_threads(&self) {
382 let _ = self.cold_backup_sender.send(WorkletData::WakeUp);
384 let _ = self.hot_backup_sender.send(WorkletData::WakeUp);
385 let _ = self.primary_sender.send(WorkletData::WakeUp);
386 }
387}
388
389enum WorkletData {
391 Task(WorkletId, WorkletTask),
392 StartSwapRoles(Sender<WorkletData>),
393 FinishSwapRoles(Swapper<WorkletThreadRole>),
394 WakeUp,
395 Quit,
396}
397
398enum WorkletControl {
400 ExitWorklet(WorkletId),
401 FetchAndInvokeAWorkletScript {
402 webview_id: WebViewId,
403 pipeline_id: PipelineId,
404 worklet_id: WorkletId,
405 global_type: WorkletGlobalScopeType,
406 origin: ImmutableOrigin,
407 base_url: ServoUrl,
408 script_url: ServoUrl,
409 policy_container: PolicyContainer,
410 credentials: RequestCredentials,
411 pending_tasks_struct: PendingTasksStruct,
412 promise: TrustedPromise,
413 inherited_secure_context: Option<bool>,
414 },
415}
416
417struct WorkletThreadRole {
424 receiver: Receiver<WorkletData>,
425 sender: Sender<WorkletData>,
426 is_hot_backup: bool,
427 is_cold_backup: bool,
428}
429
430impl WorkletThreadRole {
431 fn new(is_hot_backup: bool, is_cold_backup: bool) -> WorkletThreadRole {
432 let (sender, receiver) = unbounded();
433 WorkletThreadRole {
434 sender,
435 receiver,
436 is_hot_backup,
437 is_cold_backup,
438 }
439 }
440}
441
442#[derive(Clone)]
444struct WorkletThreadInit {
445 primary_sender: Sender<WorkletData>,
447 hot_backup_sender: Sender<WorkletData>,
448 cold_backup_sender: Sender<WorkletData>,
449
450 global_init: WorkletGlobalScopeInit,
452}
453
454struct WorkletCspProcessor {}
455
456impl CspViolationsProcessor for WorkletCspProcessor {
457 fn process_csp_violations(&self, _violations: Vec<Violation>) {}
458}
459
460#[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)]
462struct WorkletThread {
463 role: WorkletThreadRole,
465
466 control_receiver: Receiver<WorkletControl>,
468
469 primary_sender: Sender<WorkletData>,
471 hot_backup_sender: Sender<WorkletData>,
472 cold_backup_sender: Sender<WorkletData>,
473
474 global_init: WorkletGlobalScopeInit,
476
477 global_scopes: FxHashMap<WorkletId, Dom<WorkletGlobalScope>>,
479
480 control_buffer: Option<WorkletControl>,
482
483 runtime: Runtime,
485 should_gc: bool,
486 gc_threshold: u32,
487}
488
489#[expect(unsafe_code)]
490unsafe impl JSTraceable for WorkletThread {
491 unsafe fn trace(&self, trc: *mut JSTracer) {
492 debug!("Tracing worklet thread.");
493 unsafe { self.global_scopes.trace(trc) };
494 }
495}
496
497impl WorkletThread {
498 #[allow(unsafe_code)]
499 fn spawn(
501 role: WorkletThreadRole,
502 init: WorkletThreadInit,
503 thread_index: u8,
504 ) -> Sender<WorkletControl> {
505 let (control_sender, control_receiver) = unbounded();
506 let _ = thread::Builder::new()
507 .name(format!("Worklet#{thread_index}"))
508 .spawn(move || {
509 debug!("Initializing worklet thread.");
513 thread_state::initialize(ThreadState::SCRIPT | ThreadState::IN_WORKER);
514 let runtime = Runtime::new(None);
515 let mut cx = unsafe { runtime.cx() };
516 let mut thread = RootedTraceableBox::new(WorkletThread {
517 role,
518 control_receiver,
519 primary_sender: init.primary_sender,
520 hot_backup_sender: init.hot_backup_sender,
521 cold_backup_sender: init.cold_backup_sender,
522 global_init: init.global_init,
523 global_scopes: FxHashMap::default(),
524 control_buffer: None,
525 runtime,
526 should_gc: false,
527 gc_threshold: MIN_GC_THRESHOLD,
528 });
529 thread.run(&mut cx);
530 })
531 .expect("Couldn't start worklet thread");
532 control_sender
533 }
534
535 fn run(&mut self, cx: &mut js::context::JSContext) {
537 loop {
538 let message = self.role.receiver.recv().unwrap();
540 match message {
541 WorkletData::Task(id, task) => {
543 self.perform_a_worklet_task(id, task);
544 },
545 WorkletData::StartSwapRoles(sender) => {
552 let (our_swapper, their_swapper) = swapper();
553 match sender.send(WorkletData::FinishSwapRoles(their_swapper)) {
554 Ok(_) => {},
555 Err(_) => {
556 return;
559 },
560 };
561 let _ = our_swapper.swap(&mut self.role);
562 },
563 WorkletData::FinishSwapRoles(swapper) => {
566 let _ = swapper.swap(&mut self.role);
567 },
568 WorkletData::WakeUp => {},
570 WorkletData::Quit => {
572 return;
573 },
574 }
575 if self.role.is_cold_backup {
579 if let Some(control) = self.control_buffer.take() {
580 self.process_control(control, cx);
581 }
582 while let Ok(control) = self.control_receiver.try_recv() {
583 self.process_control(control, cx);
584 }
585 self.gc(cx);
586 } else if self.control_buffer.is_none() {
587 if let Ok(control) = self.control_receiver.try_recv() {
588 self.control_buffer = Some(control);
589 let msg = WorkletData::StartSwapRoles(self.role.sender.clone());
590 let _ = self.cold_backup_sender.send(msg);
591 }
592 }
593 if self.current_memory_usage() > self.gc_threshold {
597 if self.role.is_hot_backup || self.role.is_cold_backup {
598 self.should_gc = false;
599 self.gc(cx);
600 } else if !self.should_gc {
601 self.should_gc = true;
602 let msg = WorkletData::StartSwapRoles(self.role.sender.clone());
603 let _ = self.hot_backup_sender.send(msg);
604 }
605 }
606 }
607 }
608
609 #[expect(unsafe_code)]
611 fn current_memory_usage(&self) -> u32 {
612 unsafe { JS_GetGCParameter(self.runtime.cx_no_gc(), JSGCParamKey::JSGC_BYTES) }
613 }
614
615 #[expect(unsafe_code)]
617 fn gc(&mut self, cx: &mut js::context::JSContext) {
618 debug!(
619 "BEGIN GC (usage = {}, threshold = {}).",
620 self.current_memory_usage(),
621 self.gc_threshold
622 );
623 unsafe { JS_GC(cx, GCReason::API) };
624 self.gc_threshold = max(MIN_GC_THRESHOLD, self.current_memory_usage() * 2);
625 debug!(
626 "END GC (usage = {}, threshold = {}).",
627 self.current_memory_usage(),
628 self.gc_threshold
629 );
630 }
631
632 #[expect(clippy::too_many_arguments)]
633 fn get_worklet_global_scope(
636 &mut self,
637 webview_id: WebViewId,
638 pipeline_id: PipelineId,
639 worklet_id: WorkletId,
640 inherited_secure_context: Option<bool>,
641 global_type: WorkletGlobalScopeType,
642 base_url: ServoUrl,
643 cx: &mut js::context::JSContext,
644 ) -> DomRoot<WorkletGlobalScope> {
645 match self.global_scopes.entry(worklet_id) {
646 hash_map::Entry::Occupied(entry) => DomRoot::from_ref(entry.get()),
647 hash_map::Entry::Vacant(entry) => {
648 debug!("Creating new worklet global scope.");
649 let executor = WorkletExecutor::new(worklet_id, self.primary_sender.clone());
650 let result = WorkletGlobalScope::new(
651 global_type,
652 webview_id,
653 pipeline_id,
654 base_url,
655 inherited_secure_context,
656 executor,
657 &self.global_init,
658 cx,
659 );
660 entry.insert(Dom::from_ref(&*result));
661 result
662 },
663 }
664 }
665
666 #[allow(clippy::too_many_arguments)]
669 fn fetch_and_invoke_a_worklet_script(
670 &self,
671 global_scope: &WorkletGlobalScope,
672 pipeline_id: PipelineId,
673 origin: ImmutableOrigin,
674 script_url: ServoUrl,
675 policy_container: PolicyContainer,
676 credentials: RequestCredentials,
677 pending_tasks_struct: PendingTasksStruct,
678 promise: TrustedPromise,
679 cx: &mut js::context::JSContext,
680 ) {
681 debug!("Fetching from {}.", script_url);
682 let global = global_scope.upcast::<GlobalScope>();
690 let resource_fetcher = self.global_init.resource_threads.sender();
691 let request = RequestBuilder::new(None, script_url, global.get_referrer())
692 .destination(Destination::Script)
693 .mode(RequestMode::CorsMode)
694 .credentials_mode(credentials.convert())
695 .policy_container(policy_container)
696 .origin(origin);
697
698 let script = load_whole_resource(
699 request,
700 &resource_fetcher,
701 global,
702 &WorkletCspProcessor {},
703 CanGc::from_cx(cx),
704 )
705 .ok()
706 .and_then(|(_, bytes, _)| String::from_utf8(bytes).ok());
707
708 let ok = script.is_some_and(|s| {
715 global_scope
716 .evaluate_js(s.into(), CanGc::from_cx(cx))
717 .is_ok()
718 });
719
720 if !ok {
721 debug!("Failed to load script.");
723 let old_counter = pending_tasks_struct.set_counter_to(-1);
724 if old_counter > 0 {
725 self.run_in_script_thread(promise.reject_task(Error::Abort(None)));
726 }
727 } else {
728 debug!("Finished adding script.");
730 let old_counter = pending_tasks_struct.decrement_counter_by(1);
731 if old_counter == 1 {
732 debug!("Resolving promise.");
733 let msg = MainThreadScriptMsg::WorkletLoaded(pipeline_id);
734 self.global_init
735 .to_script_thread_sender
736 .send(msg)
737 .expect("Worklet thread outlived script thread.");
738 self.run_in_script_thread(promise.resolve_task(()));
739 }
740 }
741 }
742
743 fn perform_a_worklet_task(&self, worklet_id: WorkletId, task: WorkletTask) {
745 match self.global_scopes.get(&worklet_id) {
746 Some(global) => global.perform_a_worklet_task(task),
747 None => warn!("No such worklet as {:?}.", worklet_id),
748 }
749 }
750
751 fn process_control(&mut self, control: WorkletControl, cx: &mut js::context::JSContext) {
753 match control {
754 WorkletControl::ExitWorklet(worklet_id) => {
755 self.global_scopes.remove(&worklet_id);
756 },
757 WorkletControl::FetchAndInvokeAWorkletScript {
758 webview_id,
759 pipeline_id,
760 worklet_id,
761 global_type,
762 origin,
763 base_url,
764 script_url,
765 policy_container,
766 credentials,
767 pending_tasks_struct,
768 promise,
769 inherited_secure_context,
770 } => {
771 let global = self.get_worklet_global_scope(
772 webview_id,
773 pipeline_id,
774 worklet_id,
775 inherited_secure_context,
776 global_type,
777 base_url,
778 cx,
779 );
780 self.fetch_and_invoke_a_worklet_script(
781 &global,
782 pipeline_id,
783 origin,
784 script_url,
785 policy_container,
786 credentials,
787 pending_tasks_struct,
788 promise,
789 cx,
790 )
791 },
792 }
793 }
794
795 fn run_in_script_thread<T>(&self, task: T)
797 where
798 T: TaskBox + 'static,
799 {
800 let msg = CommonScriptMsg::Task(
803 ScriptThreadEventCategory::WorkletEvent,
804 Box::new(task),
805 None,
806 TaskSourceName::DOMManipulation,
807 );
808 let msg = MainThreadScriptMsg::Common(msg);
809 self.global_init
810 .to_script_thread_sender
811 .send(msg)
812 .expect("Worklet thread outlived script thread.");
813 }
814}
815
816#[derive(Clone, JSTraceable, MallocSizeOf)]
818pub(crate) struct WorkletExecutor {
819 worklet_id: WorkletId,
820 #[no_trace]
821 primary_sender: Sender<WorkletData>,
822}
823
824impl WorkletExecutor {
825 fn new(worklet_id: WorkletId, primary_sender: Sender<WorkletData>) -> WorkletExecutor {
826 WorkletExecutor {
827 worklet_id,
828 primary_sender,
829 }
830 }
831
832 pub(crate) fn schedule_a_worklet_task(&self, task: WorkletTask) {
834 let _ = self
835 .primary_sender
836 .send(WorkletData::Task(self.worklet_id, task));
837 }
838}