aboutsummaryrefslogtreecommitdiffstats
path: root/components/script/dom/worklet.rs
diff options
context:
space:
mode:
Diffstat (limited to 'components/script/dom/worklet.rs')
-rw-r--r--components/script/dom/worklet.rs760
1 files changed, 760 insertions, 0 deletions
diff --git a/components/script/dom/worklet.rs b/components/script/dom/worklet.rs
new file mode 100644
index 00000000000..46930bbf651
--- /dev/null
+++ b/components/script/dom/worklet.rs
@@ -0,0 +1,760 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+//! An implementation of Houdini worklets.
+//!
+//! The goal of this implementation is to maximize responsiveness of worklets,
+//! and in particular to ensure that the thread performing worklet tasks
+//! is never busy GCing or loading worklet code. We do this by providing a custom
+//! thread pool implementation, which only performs GC or code loading on
+//! a backup thread, not on the primary worklet thread.
+
+use crate::dom::bindings::codegen::Bindings::RequestBinding::RequestCredentials;
+use crate::dom::bindings::codegen::Bindings::WindowBinding::WindowBinding::WindowMethods;
+use crate::dom::bindings::codegen::Bindings::WorkletBinding::WorkletMethods;
+use crate::dom::bindings::codegen::Bindings::WorkletBinding::WorkletOptions;
+use crate::dom::bindings::error::Error;
+use crate::dom::bindings::inheritance::Castable;
+use crate::dom::bindings::refcounted::TrustedPromise;
+use crate::dom::bindings::reflector::reflect_dom_object;
+use crate::dom::bindings::reflector::Reflector;
+use crate::dom::bindings::root::{Dom, DomRoot, RootCollection, ThreadLocalStackRoots};
+use crate::dom::bindings::str::USVString;
+use crate::dom::bindings::trace::JSTraceable;
+use crate::dom::bindings::trace::RootedTraceableBox;
+use crate::dom::globalscope::GlobalScope;
+use crate::dom::promise::Promise;
+use crate::dom::testworkletglobalscope::TestWorkletTask;
+use crate::dom::window::Window;
+use crate::dom::workletglobalscope::WorkletGlobalScope;
+use crate::dom::workletglobalscope::WorkletGlobalScopeInit;
+use crate::dom::workletglobalscope::WorkletGlobalScopeType;
+use crate::dom::workletglobalscope::WorkletTask;
+use crate::fetch::load_whole_resource;
+use crate::realms::InRealm;
+use crate::script_runtime::new_rt_and_cx;
+use crate::script_runtime::CommonScriptMsg;
+use crate::script_runtime::Runtime;
+use crate::script_runtime::ScriptThreadEventCategory;
+use crate::script_thread::{MainThreadScriptMsg, ScriptThread};
+use crate::task::TaskBox;
+use crate::task_source::TaskSourceName;
+use crossbeam_channel::{unbounded, Receiver, Sender};
+use dom_struct::dom_struct;
+use js::jsapi::JSGCParamKey;
+use js::jsapi::JSTracer;
+use js::jsapi::JS_GetGCParameter;
+use js::jsapi::{GCReason, JS_GC};
+use msg::constellation_msg::PipelineId;
+use net_traits::request::Destination;
+use net_traits::request::RequestBuilder;
+use net_traits::request::RequestMode;
+use net_traits::IpcSend;
+use servo_url::ImmutableOrigin;
+use servo_url::ServoUrl;
+use std::cmp::max;
+use std::collections::hash_map;
+use std::collections::HashMap;
+use std::rc::Rc;
+use std::sync::atomic::AtomicIsize;
+use std::sync::atomic::Ordering;
+use std::sync::Arc;
+use std::thread;
+use style::thread_state::{self, ThreadState};
+use swapper::swapper;
+use swapper::Swapper;
+use uuid::Uuid;
+
+// Magic numbers
+const WORKLET_THREAD_POOL_SIZE: u32 = 3;
+const MIN_GC_THRESHOLD: u32 = 1_000_000;
+
+#[dom_struct]
+/// <https://drafts.css-houdini.org/worklets/#worklet>
+pub struct Worklet {
+ reflector: Reflector,
+ window: Dom<Window>,
+ worklet_id: WorkletId,
+ global_type: WorkletGlobalScopeType,
+}
+
+impl Worklet {
+ fn new_inherited(window: &Window, global_type: WorkletGlobalScopeType) -> Worklet {
+ Worklet {
+ reflector: Reflector::new(),
+ window: Dom::from_ref(window),
+ worklet_id: WorkletId::new(),
+ global_type: global_type,
+ }
+ }
+
+ pub fn new(window: &Window, global_type: WorkletGlobalScopeType) -> DomRoot<Worklet> {
+ debug!("Creating worklet {:?}.", global_type);
+ reflect_dom_object(
+ Box::new(Worklet::new_inherited(window, global_type)),
+ window,
+ )
+ }
+
+ pub fn worklet_id(&self) -> WorkletId {
+ self.worklet_id
+ }
+
+ #[allow(dead_code)]
+ pub fn worklet_global_scope_type(&self) -> WorkletGlobalScopeType {
+ self.global_type
+ }
+}
+
+impl WorkletMethods for Worklet {
+ /// <https://drafts.css-houdini.org/worklets/#dom-worklet-addmodule>
+ fn AddModule(
+ &self,
+ module_url: USVString,
+ options: &WorkletOptions,
+ comp: InRealm,
+ ) -> Rc<Promise> {
+ // Step 1.
+ let global = self.window.upcast();
+ let promise = Promise::new_in_current_realm(&global, comp);
+
+ // Step 3.
+ let module_url_record = match self.window.Document().base_url().join(&module_url.0) {
+ Ok(url) => url,
+ Err(err) => {
+ // Step 4.
+ debug!("URL {:?} parse error {:?}.", module_url.0, err);
+ promise.reject_error(Error::Syntax);
+ return promise;
+ },
+ };
+ debug!("Adding Worklet module {}.", module_url_record);
+
+ // Steps 6-12 in parallel.
+ let pending_tasks_struct = PendingTasksStruct::new();
+ let global = self.window.upcast::<GlobalScope>();
+ let pool = ScriptThread::worklet_thread_pool();
+
+ pool.fetch_and_invoke_a_worklet_script(
+ global.pipeline_id(),
+ self.worklet_id,
+ self.global_type,
+ self.window.origin().immutable().clone(),
+ global.api_base_url(),
+ module_url_record,
+ options.credentials.clone(),
+ pending_tasks_struct,
+ &promise,
+ );
+
+ // Step 5.
+ debug!("Returning promise.");
+ promise
+ }
+}
+
+impl Drop for Worklet {
+ fn drop(&mut self) {
+ let script_thread = ScriptThread::worklet_thread_pool();
+ script_thread.exit_worklet(self.worklet_id);
+ }
+}
+
+/// A guid for worklets.
+#[derive(Clone, Copy, Debug, Eq, Hash, JSTraceable, PartialEq)]
+pub struct WorkletId(Uuid);
+
+malloc_size_of_is_0!(WorkletId);
+
+impl WorkletId {
+ fn new() -> WorkletId {
+ WorkletId(servo_rand::random_uuid())
+ }
+}
+
+/// <https://drafts.css-houdini.org/worklets/#pending-tasks-struct>
+#[derive(Clone, Debug)]
+struct PendingTasksStruct(Arc<AtomicIsize>);
+
+impl PendingTasksStruct {
+ fn new() -> PendingTasksStruct {
+ PendingTasksStruct(Arc::new(AtomicIsize::new(
+ WORKLET_THREAD_POOL_SIZE as isize,
+ )))
+ }
+
+ fn set_counter_to(&self, value: isize) -> isize {
+ self.0.swap(value, Ordering::AcqRel)
+ }
+
+ fn decrement_counter_by(&self, offset: isize) -> isize {
+ self.0.fetch_sub(offset, Ordering::AcqRel)
+ }
+}
+
+/// Worklets execute in a dedicated thread pool.
+///
+/// The goal is to ensure that there is a primary worklet thread,
+/// which is able to responsively execute worklet code. In particular,
+/// worklet execution should not be delayed by GC, or by script
+/// loading.
+///
+/// To achieve this, we implement a three-thread pool, with the
+/// threads cycling between three thread roles:
+///
+/// * The primary worklet thread is the one available to execute
+/// worklet code.
+///
+/// * The hot backup thread may peform GC, but otherwise is expected
+/// to take over the primary role.
+///
+/// * The cold backup thread may peform script loading and other
+/// long-running tasks.
+///
+/// In the implementation, we use two kinds of messages:
+///
+/// * Data messages are expected to be processed quickly, and include
+/// the worklet tasks to be performed by the primary thread, as
+/// well as requests to change role or quit execution.
+///
+/// * Control messages are expected to be processed more slowly, and
+/// include script loading.
+///
+/// Data messages are targeted at a role, for example, task execution
+/// is expected to be performed by whichever thread is currently
+/// primary. Control messages are targeted at a thread, for example
+/// adding a module is performed in every thread, even if they change roles
+/// in the middle of module loading.
+///
+/// The thread pool lives in the script thread, and is initialized
+/// when a worklet adds a module. It is dropped when the script thread
+/// is dropped, and asks each of the worklet threads to quit.
+///
+/// The layout thread can end up blocking on the primary worklet thread
+/// (e.g. when invoking a paint callback), so it is important to avoid
+/// deadlock by making sure the primary worklet thread doesn't end up
+/// blocking waiting on layout. In particular, since the constellation
+/// can block waiting on layout, this means the primary worklet thread
+/// can't block waiting on the constellation. In general, the primary
+/// worklet thread shouldn't perform any blocking operations. If a worklet
+/// thread needs to do anything blocking, it should send a control
+/// message, to make sure that the blocking operation is performed
+/// by a backup thread, not by the primary thread.
+
+#[derive(Clone, JSTraceable)]
+pub struct WorkletThreadPool {
+ // Channels to send data messages to the three roles.
+ primary_sender: Sender<WorkletData>,
+ hot_backup_sender: Sender<WorkletData>,
+ cold_backup_sender: Sender<WorkletData>,
+ // Channels to send control messages to the three threads.
+ control_sender_0: Sender<WorkletControl>,
+ control_sender_1: Sender<WorkletControl>,
+ control_sender_2: Sender<WorkletControl>,
+}
+
+impl Drop for WorkletThreadPool {
+ fn drop(&mut self) {
+ let _ = self.cold_backup_sender.send(WorkletData::Quit);
+ let _ = self.hot_backup_sender.send(WorkletData::Quit);
+ let _ = self.primary_sender.send(WorkletData::Quit);
+ }
+}
+
+impl WorkletThreadPool {
+ /// Create a new thread pool and spawn the threads.
+ /// When the thread pool is dropped, the threads will be asked to quit.
+ pub fn spawn(global_init: WorkletGlobalScopeInit) -> WorkletThreadPool {
+ let primary_role = WorkletThreadRole::new(false, false);
+ let hot_backup_role = WorkletThreadRole::new(true, false);
+ let cold_backup_role = WorkletThreadRole::new(false, true);
+ let primary_sender = primary_role.sender.clone();
+ let hot_backup_sender = hot_backup_role.sender.clone();
+ let cold_backup_sender = cold_backup_role.sender.clone();
+ let init = WorkletThreadInit {
+ primary_sender: primary_sender.clone(),
+ hot_backup_sender: hot_backup_sender.clone(),
+ cold_backup_sender: cold_backup_sender.clone(),
+ global_init: global_init,
+ };
+ WorkletThreadPool {
+ primary_sender: primary_sender,
+ hot_backup_sender: hot_backup_sender,
+ cold_backup_sender: cold_backup_sender,
+ control_sender_0: WorkletThread::spawn(primary_role, init.clone()),
+ control_sender_1: WorkletThread::spawn(hot_backup_role, init.clone()),
+ control_sender_2: WorkletThread::spawn(cold_backup_role, init),
+ }
+ }
+
+ /// Loads a worklet module into every worklet thread.
+ /// If all of the threads load successfully, the promise is resolved.
+ /// If any of the threads fails to load, the promise is rejected.
+ /// <https://drafts.css-houdini.org/worklets/#fetch-and-invoke-a-worklet-script>
+ fn fetch_and_invoke_a_worklet_script(
+ &self,
+ pipeline_id: PipelineId,
+ worklet_id: WorkletId,
+ global_type: WorkletGlobalScopeType,
+ origin: ImmutableOrigin,
+ base_url: ServoUrl,
+ script_url: ServoUrl,
+ credentials: RequestCredentials,
+ pending_tasks_struct: PendingTasksStruct,
+ promise: &Rc<Promise>,
+ ) {
+ // Send each thread a control message asking it to load the script.
+ for sender in &[
+ &self.control_sender_0,
+ &self.control_sender_1,
+ &self.control_sender_2,
+ ] {
+ let _ = sender.send(WorkletControl::FetchAndInvokeAWorkletScript {
+ pipeline_id: pipeline_id,
+ worklet_id: worklet_id,
+ global_type: global_type,
+ origin: origin.clone(),
+ base_url: base_url.clone(),
+ script_url: script_url.clone(),
+ credentials: credentials,
+ pending_tasks_struct: pending_tasks_struct.clone(),
+ promise: TrustedPromise::new(promise.clone()),
+ });
+ }
+ self.wake_threads();
+ }
+
+ pub(crate) fn exit_worklet(&self, worklet_id: WorkletId) {
+ for sender in &[
+ &self.control_sender_0,
+ &self.control_sender_1,
+ &self.control_sender_2,
+ ] {
+ let _ = sender.send(WorkletControl::ExitWorklet(worklet_id));
+ }
+ self.wake_threads();
+ }
+
+ /// For testing.
+ pub fn test_worklet_lookup(&self, id: WorkletId, key: String) -> Option<String> {
+ let (sender, receiver) = unbounded();
+ let msg = WorkletData::Task(id, WorkletTask::Test(TestWorkletTask::Lookup(key, sender)));
+ let _ = self.primary_sender.send(msg);
+ receiver.recv().expect("Test worklet has died?")
+ }
+
+ fn wake_threads(&self) {
+ // If any of the threads are blocked waiting on data, wake them up.
+ let _ = self.cold_backup_sender.send(WorkletData::WakeUp);
+ let _ = self.hot_backup_sender.send(WorkletData::WakeUp);
+ let _ = self.primary_sender.send(WorkletData::WakeUp);
+ }
+}
+
+/// The data messages sent to worklet threads
+enum WorkletData {
+ Task(WorkletId, WorkletTask),
+ StartSwapRoles(Sender<WorkletData>),
+ FinishSwapRoles(Swapper<WorkletThreadRole>),
+ WakeUp,
+ Quit,
+}
+
+/// The control message sent to worklet threads
+enum WorkletControl {
+ ExitWorklet(WorkletId),
+ FetchAndInvokeAWorkletScript {
+ pipeline_id: PipelineId,
+ worklet_id: WorkletId,
+ global_type: WorkletGlobalScopeType,
+ origin: ImmutableOrigin,
+ base_url: ServoUrl,
+ script_url: ServoUrl,
+ credentials: RequestCredentials,
+ pending_tasks_struct: PendingTasksStruct,
+ promise: TrustedPromise,
+ },
+}
+
+/// A role that a worklet thread can be playing.
+///
+/// These roles are used as tokens or capabilities, we track unique
+/// ownership using Rust's types, and use atomic swapping to exchange
+/// them between worklet threads. This ensures that each thread pool has
+/// exactly one primary, one hot backup and one cold backup.
+struct WorkletThreadRole {
+ receiver: Receiver<WorkletData>,
+ sender: Sender<WorkletData>,
+ is_hot_backup: bool,
+ is_cold_backup: bool,
+}
+
+impl WorkletThreadRole {
+ fn new(is_hot_backup: bool, is_cold_backup: bool) -> WorkletThreadRole {
+ let (sender, receiver) = unbounded();
+ WorkletThreadRole {
+ sender: sender,
+ receiver: receiver,
+ is_hot_backup: is_hot_backup,
+ is_cold_backup: is_cold_backup,
+ }
+ }
+}
+
+/// Data to initialize a worklet thread.
+#[derive(Clone)]
+struct WorkletThreadInit {
+ /// Senders
+ primary_sender: Sender<WorkletData>,
+ hot_backup_sender: Sender<WorkletData>,
+ cold_backup_sender: Sender<WorkletData>,
+
+ /// Data for initializing new worklet global scopes
+ global_init: WorkletGlobalScopeInit,
+}
+
+/// A thread for executing worklets.
+#[unrooted_must_root_lint::must_root]
+struct WorkletThread {
+ /// Which role the thread is currently playing
+ role: WorkletThreadRole,
+
+ /// The thread's receiver for control messages
+ control_receiver: Receiver<WorkletControl>,
+
+ /// Senders
+ primary_sender: Sender<WorkletData>,
+ hot_backup_sender: Sender<WorkletData>,
+ cold_backup_sender: Sender<WorkletData>,
+
+ /// Data for initializing new worklet global scopes
+ global_init: WorkletGlobalScopeInit,
+
+ /// The global scopes created by this thread
+ global_scopes: HashMap<WorkletId, Dom<WorkletGlobalScope>>,
+
+ /// A one-place buffer for control messages
+ control_buffer: Option<WorkletControl>,
+
+ /// The JS runtime
+ runtime: Runtime,
+ should_gc: bool,
+ gc_threshold: u32,
+}
+
+#[allow(unsafe_code)]
+unsafe impl JSTraceable for WorkletThread {
+ unsafe fn trace(&self, trc: *mut JSTracer) {
+ debug!("Tracing worklet thread.");
+ self.global_scopes.trace(trc);
+ }
+}
+
+impl WorkletThread {
+ /// Spawn a new worklet thread, returning the channel to send it control messages.
+ #[allow(unsafe_code)]
+ #[allow(unrooted_must_root)]
+ fn spawn(role: WorkletThreadRole, init: WorkletThreadInit) -> Sender<WorkletControl> {
+ let (control_sender, control_receiver) = unbounded();
+ // TODO: name this thread
+ thread::spawn(move || {
+ // TODO: add a new IN_WORKLET thread state?
+ // TODO: set interrupt handler?
+ // TODO: configure the JS runtime (e.g. discourage GC, encourage agressive JIT)
+ debug!("Initializing worklet thread.");
+ thread_state::initialize(ThreadState::SCRIPT | ThreadState::IN_WORKER);
+ let roots = RootCollection::new();
+ let _stack_roots = ThreadLocalStackRoots::new(&roots);
+ let mut thread = RootedTraceableBox::new(WorkletThread {
+ role: role,
+ control_receiver: control_receiver,
+ primary_sender: init.primary_sender,
+ hot_backup_sender: init.hot_backup_sender,
+ cold_backup_sender: init.cold_backup_sender,
+ global_init: init.global_init,
+ global_scopes: HashMap::new(),
+ control_buffer: None,
+ runtime: new_rt_and_cx(None),
+ should_gc: false,
+ gc_threshold: MIN_GC_THRESHOLD,
+ });
+ thread.run();
+ });
+ control_sender
+ }
+
+ /// The main event loop for a worklet thread
+ fn run(&mut self) {
+ loop {
+ // The handler for data messages
+ let message = self.role.receiver.recv().unwrap();
+ match message {
+ // The whole point of this thread pool is to perform tasks!
+ WorkletData::Task(id, task) => {
+ self.perform_a_worklet_task(id, task);
+ },
+ // To start swapping roles, get ready to perform an atomic swap,
+ // and block waiting for the other end to finish it.
+ // NOTE: the cold backup can block on the primary or the hot backup;
+ // the hot backup can block on the primary;
+ // the primary can block on nothing;
+ // this total ordering on thread roles is what guarantees deadlock-freedom.
+ WorkletData::StartSwapRoles(sender) => {
+ let (our_swapper, their_swapper) = swapper();
+ sender
+ .send(WorkletData::FinishSwapRoles(their_swapper))
+ .unwrap();
+ let _ = our_swapper.swap(&mut self.role);
+ },
+ // To finish swapping roles, perform the atomic swap.
+ // The other end should have already started the swap, so this shouldn't block.
+ WorkletData::FinishSwapRoles(swapper) => {
+ let _ = swapper.swap(&mut self.role);
+ },
+ // Wake up! There may be control messages to process.
+ WorkletData::WakeUp => {},
+ // Quit!
+ WorkletData::Quit => {
+ return;
+ },
+ }
+ // Only process control messages if we're the cold backup,
+ // otherwise if there are outstanding control messages,
+ // try to become the cold backup.
+ if self.role.is_cold_backup {
+ if let Some(control) = self.control_buffer.take() {
+ self.process_control(control);
+ }
+ while let Ok(control) = self.control_receiver.try_recv() {
+ self.process_control(control);
+ }
+ self.gc();
+ } else if self.control_buffer.is_none() {
+ if let Ok(control) = self.control_receiver.try_recv() {
+ self.control_buffer = Some(control);
+ let msg = WorkletData::StartSwapRoles(self.role.sender.clone());
+ let _ = self.cold_backup_sender.send(msg);
+ }
+ }
+ // If we are tight on memory, and we're a backup then perform a gc.
+ // If we are tight on memory, and we're the primary then try to become the hot backup.
+ // Hopefully this happens soon!
+ if self.current_memory_usage() > self.gc_threshold {
+ if self.role.is_hot_backup || self.role.is_cold_backup {
+ self.should_gc = false;
+ self.gc();
+ } else if !self.should_gc {
+ self.should_gc = true;
+ let msg = WorkletData::StartSwapRoles(self.role.sender.clone());
+ let _ = self.hot_backup_sender.send(msg);
+ }
+ }
+ }
+ }
+
+ /// The current memory usage of the thread
+ #[allow(unsafe_code)]
+ fn current_memory_usage(&self) -> u32 {
+ unsafe { JS_GetGCParameter(self.runtime.cx(), JSGCParamKey::JSGC_BYTES) }
+ }
+
+ /// Perform a GC.
+ #[allow(unsafe_code)]
+ fn gc(&mut self) {
+ debug!(
+ "BEGIN GC (usage = {}, threshold = {}).",
+ self.current_memory_usage(),
+ self.gc_threshold
+ );
+ unsafe { JS_GC(self.runtime.cx(), GCReason::API) };
+ self.gc_threshold = max(MIN_GC_THRESHOLD, self.current_memory_usage() * 2);
+ debug!(
+ "END GC (usage = {}, threshold = {}).",
+ self.current_memory_usage(),
+ self.gc_threshold
+ );
+ }
+
+ /// Get the worklet global scope for a given worklet.
+ /// Creates the worklet global scope if it doesn't exist.
+ fn get_worklet_global_scope(
+ &mut self,
+ pipeline_id: PipelineId,
+ worklet_id: WorkletId,
+ global_type: WorkletGlobalScopeType,
+ base_url: ServoUrl,
+ ) -> DomRoot<WorkletGlobalScope> {
+ match self.global_scopes.entry(worklet_id) {
+ hash_map::Entry::Occupied(entry) => DomRoot::from_ref(entry.get()),
+ hash_map::Entry::Vacant(entry) => {
+ debug!("Creating new worklet global scope.");
+ let executor = WorkletExecutor::new(worklet_id, self.primary_sender.clone());
+ let result = global_type.new(
+ &self.runtime,
+ pipeline_id,
+ base_url,
+ executor,
+ &self.global_init,
+ );
+ entry.insert(Dom::from_ref(&*result));
+ result
+ },
+ }
+ }
+
+ /// Fetch and invoke a worklet script.
+ /// <https://drafts.css-houdini.org/worklets/#fetch-and-invoke-a-worklet-script>
+ fn fetch_and_invoke_a_worklet_script(
+ &self,
+ global_scope: &WorkletGlobalScope,
+ pipeline_id: PipelineId,
+ origin: ImmutableOrigin,
+ script_url: ServoUrl,
+ credentials: RequestCredentials,
+ pending_tasks_struct: PendingTasksStruct,
+ promise: TrustedPromise,
+ ) {
+ debug!("Fetching from {}.", script_url);
+ // Step 1.
+ // TODO: Settings object?
+
+ // Step 2.
+ // TODO: Fetch a module graph, not just a single script.
+ // TODO: Fetch the script asynchronously?
+ // TODO: Caching.
+ let resource_fetcher = self.global_init.resource_threads.sender();
+ let request = RequestBuilder::new(
+ script_url,
+ global_scope.upcast::<GlobalScope>().get_referrer(),
+ )
+ .destination(Destination::Script)
+ .mode(RequestMode::CorsMode)
+ .credentials_mode(credentials.into())
+ .origin(origin);
+
+ let script = load_whole_resource(
+ request,
+ &resource_fetcher,
+ &global_scope.upcast::<GlobalScope>(),
+ )
+ .ok()
+ .and_then(|(_, bytes)| String::from_utf8(bytes).ok());
+
+ // Step 4.
+ // NOTE: the spec parses and executes the script in separate steps,
+ // but our JS API doesn't separate these, so we do the steps out of order.
+ // Also, the spec currently doesn't allow exceptions to be propagated
+ // to the main script thread.
+ // https://github.com/w3c/css-houdini-drafts/issues/407
+ let ok = script
+ .map(|script| global_scope.evaluate_js(&*script))
+ .unwrap_or(false);
+
+ if !ok {
+ // Step 3.
+ debug!("Failed to load script.");
+ let old_counter = pending_tasks_struct.set_counter_to(-1);
+ if old_counter > 0 {
+ self.run_in_script_thread(promise.reject_task(Error::Abort));
+ }
+ } else {
+ // Step 5.
+ debug!("Finished adding script.");
+ let old_counter = pending_tasks_struct.decrement_counter_by(1);
+ if old_counter == 1 {
+ debug!("Resolving promise.");
+ let msg = MainThreadScriptMsg::WorkletLoaded(pipeline_id);
+ self.global_init
+ .to_script_thread_sender
+ .send(msg)
+ .expect("Worklet thread outlived script thread.");
+ self.run_in_script_thread(promise.resolve_task(()));
+ }
+ }
+ }
+
+ /// Perform a task.
+ fn perform_a_worklet_task(&self, worklet_id: WorkletId, task: WorkletTask) {
+ match self.global_scopes.get(&worklet_id) {
+ Some(global) => global.perform_a_worklet_task(task),
+ None => return warn!("No such worklet as {:?}.", worklet_id),
+ }
+ }
+
+ /// Process a control message.
+ fn process_control(&mut self, control: WorkletControl) {
+ match control {
+ WorkletControl::ExitWorklet(worklet_id) => {
+ self.global_scopes.remove(&worklet_id);
+ },
+ WorkletControl::FetchAndInvokeAWorkletScript {
+ pipeline_id,
+ worklet_id,
+ global_type,
+ origin,
+ base_url,
+ script_url,
+ credentials,
+ pending_tasks_struct,
+ promise,
+ } => {
+ let global =
+ self.get_worklet_global_scope(pipeline_id, worklet_id, global_type, base_url);
+ self.fetch_and_invoke_a_worklet_script(
+ &*global,
+ pipeline_id,
+ origin,
+ script_url,
+ credentials,
+ pending_tasks_struct,
+ promise,
+ )
+ },
+ }
+ }
+
+ /// Run a task in the main script thread.
+ fn run_in_script_thread<T>(&self, task: T)
+ where
+ T: TaskBox + 'static,
+ {
+ // NOTE: It's unclear which task source should be used here:
+ // https://drafts.css-houdini.org/worklets/#dom-worklet-addmodule
+ let msg = CommonScriptMsg::Task(
+ ScriptThreadEventCategory::WorkletEvent,
+ Box::new(task),
+ None,
+ TaskSourceName::DOMManipulation,
+ );
+ let msg = MainThreadScriptMsg::Common(msg);
+ self.global_init
+ .to_script_thread_sender
+ .send(msg)
+ .expect("Worklet thread outlived script thread.");
+ }
+}
+
+/// An executor of worklet tasks
+#[derive(Clone, JSTraceable, MallocSizeOf)]
+pub struct WorkletExecutor {
+ worklet_id: WorkletId,
+ #[ignore_malloc_size_of = "channels are hard"]
+ primary_sender: Sender<WorkletData>,
+}
+
+impl WorkletExecutor {
+ fn new(worklet_id: WorkletId, primary_sender: Sender<WorkletData>) -> WorkletExecutor {
+ WorkletExecutor {
+ worklet_id: worklet_id,
+ primary_sender: primary_sender,
+ }
+ }
+
+ /// Schedule a worklet task to be peformed by the worklet thread pool.
+ pub fn schedule_a_worklet_task(&self, task: WorkletTask) {
+ let _ = self
+ .primary_sender
+ .send(WorkletData::Task(self.worklet_id, task));
+ }
+}