diff options
author | Alan Jeffrey <ajeffrey@mozilla.com> | 2017-04-03 14:35:57 -0500 |
---|---|---|
committer | Alan Jeffrey <ajeffrey@mozilla.com> | 2017-05-17 09:01:05 -0500 |
commit | af8436c9be4c69c07265ab1095f89982b48cdd00 (patch) | |
tree | 0a4dbfaeb69c7a1741840e94ffb88139317ef295 | |
parent | abb2985ffe96485b58f6b9e5f8b2dd3641d987b7 (diff) | |
download | servo-af8436c9be4c69c07265ab1095f89982b48cdd00.tar.gz servo-af8436c9be4c69c07265ab1095f89982b48cdd00.zip |
Implemented Houdini worklets.
34 files changed, 1209 insertions, 17 deletions
diff --git a/Cargo.lock b/Cargo.lock index 3610f0cf2cb..d0eec9a4440 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -196,6 +196,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] name = "bitflags" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "bitflags" version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2143,6 +2148,15 @@ dependencies = [ ] [[package]] +name = "pulldown-cmark" +version = "0.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "bitflags 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", + "getopts 0.2.14 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] name = "quasi" version = "0.32.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2351,6 +2365,7 @@ dependencies = [ "smallvec 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)", "style 0.0.1", "style_traits 0.0.1", + "swapper 0.0.4 (registry+https://github.com/rust-lang/crates.io-index)", "time 0.1.37 (registry+https://github.com/rust-lang/crates.io-index)", "tinyfiledialogs 2.5.9 (registry+https://github.com/rust-lang/crates.io-index)", "unicode-segmentation 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)", @@ -2407,7 +2422,6 @@ dependencies = [ name = "script_traits" version = "0.0.1" dependencies = [ - "app_units 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", "bluetooth_traits 0.0.1", "canvas_traits 0.0.1", "cookie 0.6.1 (registry+https://github.com/rust-lang/crates.io-index)", @@ -2739,6 +2753,15 @@ name = "size_of_test" version = "0.0.1" [[package]] +name = "skeptic" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "pulldown-cmark 0.0.8 (registry+https://github.com/rust-lang/crates.io-index)", + "tempdir 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] name = "slab" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2883,6 +2906,14 @@ dependencies = [ ] [[package]] +name = "swapper" +version = "0.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "skeptic 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] name = "syn" version = "0.11.11" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2969,6 +3000,14 @@ dependencies = [ ] [[package]] +name = "tempdir" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "rand 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] name = "tendril" version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -3395,6 +3434,7 @@ dependencies = [ "checksum bindgen 0.25.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ccaf8958532d7e570e905266ee2dc1094c3e5c3c3cfc2c299368747a30a5e654" "checksum bit-set 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d9bf6104718e80d7b26a68fdbacff3481cfc05df670821affc7e9cbc1884400c" "checksum bit-vec 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)" = "5b97c2c8e8bbb4251754f559df8af22fb264853c7d009084a576cdf12565089d" +"checksum bitflags 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "4f67931368edf3a9a51d29886d245f1c3db2f1ef0dcc9e35ff70341b78c10d23" "checksum bitflags 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "aad18937a628ec6abcd26d1489012cc0e18c21798210f491af69ded9b881106d" "checksum bitflags 0.8.2 (registry+https://github.com/rust-lang/crates.io-index)" = "1370e9fc2a6ae53aea8b7a5110edbd08836ed87c88736dfabccade1c2b44bff4" "checksum bitreader 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "80b13e2ab064ff3aa0bdbf1eff533f9822dc37899821f5f98c67f263eab51707" @@ -3550,6 +3590,7 @@ dependencies = [ "checksum png 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)" = "3cb773e9a557edb568ce9935cf783e3cdcabe06a9449d41b3e5506d88e582c82" "checksum precomputed-hash 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "cdf1fc3616b3ef726a847f2cd2388c646ef6a1f1ba4835c2629004da48184150" "checksum procedural-masquerade 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "9f566249236c6ca4340f7ca78968271f0ed2b0f234007a61b66f9ecd0af09260" +"checksum pulldown-cmark 0.0.8 (registry+https://github.com/rust-lang/crates.io-index)" = "1058d7bb927ca067656537eec4e02c2b4b70eaaa129664c5b90c111e20326f41" "checksum quasi 0.32.0 (registry+https://github.com/rust-lang/crates.io-index)" = "18c45c4854d6d1cf5d531db97c75880feb91c958b0720f4ec1057135fec358b3" "checksum quasi_codegen 0.32.0 (registry+https://github.com/rust-lang/crates.io-index)" = "51b9e25fa23c044c1803f43ca59c98dac608976dd04ce799411edd58ece776d4" "checksum quote 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)" = "7a6e920b65c65f10b2ae65c831a81a073a89edd28c7cce89475bff467ab4167a" @@ -3586,12 +3627,14 @@ dependencies = [ "checksum signpost 0.1.0 (git+https://github.com/pcwalton/signpost.git)" = "<none>" "checksum simd 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "7a94d14a2ae1f1f110937de5fb69e494372560181c7e1739a097fcc2cee37ba0" "checksum siphasher 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "0df90a788073e8d0235a67e50441d47db7c8ad9debd91cbf43736a2a92d36537" +"checksum skeptic 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "dd7d8dc1315094150052d0ab767840376335a98ac66ef313ff911cdf439a5b69" "checksum slab 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "17b4fcaed89ab08ef143da37bc52adbcc04d4a69014f4c1208d6b51f0c47bc23" "checksum smallvec 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "4f8266519bc1d17d0b5b16f6c21295625d562841c708f6376f49028a43e9c11e" "checksum string_cache 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "f55fba06c5e294108f22e8512eb598cb13388a117991e411a8df8f41a1219a75" "checksum string_cache_codegen 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "479cde50c3539481f33906a387f2bd17c8e87cb848c35b6021d41fb81ff9b4d7" "checksum string_cache_shared 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b1884d1bc09741d466d9b14e6d37ac89d6909cbcac41dd9ae982d4d063bbedfc" "checksum strsim 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b4d15c810519a91cf877e7e36e63fe068815c678181439f2f29e2562147c3694" +"checksum swapper 0.0.4 (registry+https://github.com/rust-lang/crates.io-index)" = "3ca610b32bb8bfc5e7f705480c3a1edfeb70b6582495d343872c8bee0dcf758c" "checksum syn 0.11.11 (registry+https://github.com/rust-lang/crates.io-index)" = "d3b891b9015c88c576343b9b3e41c2c11a51c219ef067b264bd9c8aa9b441dad" "checksum synom 0.11.3 (registry+https://github.com/rust-lang/crates.io-index)" = "a393066ed9010ebaed60b9eafa373d4b1baac186dd7e008555b0f702b51945b6" "checksum synstructure 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "5ccc9780bf1aa601943988c2876ab22413c01ad1739689aa6af18d0aa0b3f38b" @@ -3600,6 +3643,7 @@ dependencies = [ "checksum syntex_pos 0.58.1 (registry+https://github.com/rust-lang/crates.io-index)" = "13ad4762fe52abc9f4008e85c4fb1b1fe3aa91ccb99ff4826a439c7c598e1047" "checksum syntex_syntax 0.58.1 (registry+https://github.com/rust-lang/crates.io-index)" = "6e0e4dbae163dd98989464c23dd503161b338790640e11537686f2ef0f25c791" "checksum target_build_utils 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "f42dc058080c19c6a58bdd1bf962904ee4f5ef1fe2a81b529f31dacc750c679f" +"checksum tempdir 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)" = "87974a6f5c1dfb344d733055601650059a3363de2a6104819293baff662132d6" "checksum tendril 0.2.4 (registry+https://github.com/rust-lang/crates.io-index)" = "4ce04c250d202db8004921e3d3bc95eaa4f2126c6937a428ae39d12d0e38df62" "checksum term 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)" = "d168af3930b369cfe245132550579d47dfd873d69470755a19c2c6568dbbd989" "checksum term_size 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "07b6c1ac5b3fffd75073276bca1ceed01f67a28537097a2a9539e116e50fb21a" diff --git a/components/profile/time.rs b/components/profile/time.rs index 928eca4a263..dfe5c37068d 100644 --- a/components/profile/time.rs +++ b/components/profile/time.rs @@ -150,6 +150,7 @@ impl Formattable for ProfilerCategory { ProfilerCategory::ScriptEnterFullscreen => "Script Enter Fullscreen", ProfilerCategory::ScriptExitFullscreen => "Script Exit Fullscreen", ProfilerCategory::ScriptWebVREvent => "Script WebVR Event", + ProfilerCategory::ScriptWorkletEvent => "Script Worklet Event", ProfilerCategory::ApplicationHeartbeat => "Application Heartbeat", }; format!("{}{}", padding, name) diff --git a/components/profile_traits/time.rs b/components/profile_traits/time.rs index 7bbebf5f465..369c2fa366d 100644 --- a/components/profile_traits/time.rs +++ b/components/profile_traits/time.rs @@ -89,6 +89,7 @@ pub enum ProfilerCategory { ScriptEnterFullscreen = 0x77, ScriptExitFullscreen = 0x78, ScriptWebVREvent = 0x79, + ScriptWorkletEvent = 0x7a, ApplicationHeartbeat = 0x90, } diff --git a/components/script/Cargo.toml b/components/script/Cargo.toml index 306dd2a748d..388801aa011 100644 --- a/components/script/Cargo.toml +++ b/components/script/Cargo.toml @@ -83,6 +83,7 @@ servo_url = {path = "../url"} smallvec = "0.3" style = {path = "../style"} style_traits = {path = "../style_traits"} +swapper = "0.0.4" time = "0.1.12" unicode-segmentation = "1.1.0" url = {version = "1.2", features = ["heap_size", "query_encoding"]} diff --git a/components/script/dom/bindings/refcounted.rs b/components/script/dom/bindings/refcounted.rs index dcf11626543..f22074d5fef 100644 --- a/components/script/dom/bindings/refcounted.rs +++ b/components/script/dom/bindings/refcounted.rs @@ -23,12 +23,17 @@ //! as JS roots. use core::nonzero::NonZero; +use dom::bindings::conversions::ToJSValConvertible; +use dom::bindings::error::Error; use dom::bindings::js::Root; use dom::bindings::reflector::{DomObject, Reflector}; use dom::bindings::trace::trace_reflector; use dom::promise::Promise; +use js::jsapi::JSAutoCompartment; use js::jsapi::JSTracer; use libc; +use script_thread::Runnable; +use script_thread::ScriptThread; use std::cell::RefCell; use std::collections::hash_map::Entry::{Occupied, Vacant}; use std::collections::hash_map::HashMap; @@ -115,6 +120,40 @@ impl TrustedPromise { promise }) } + + /// A runnable which will reject the promise. + #[allow(unrooted_must_root)] + pub fn reject_runnable(self, error: Error) -> impl Runnable + Send { + struct RejectPromise(TrustedPromise, Error); + impl Runnable for RejectPromise { + fn main_thread_handler(self: Box<Self>, script_thread: &ScriptThread) { + let this = *self; + let cx = script_thread.get_cx(); + let promise = this.0.root(); + let _ac = JSAutoCompartment::new(cx, promise.reflector().get_jsobject().get()); + promise.reject_error(cx, this.1); + } + } + RejectPromise(self, error) + } + + /// A runnable which will resolve the promise. + #[allow(unrooted_must_root)] + pub fn resolve_runnable<T>(self, value: T) -> impl Runnable + Send where + T: ToJSValConvertible + Send + { + struct ResolvePromise<T>(TrustedPromise, T); + impl<T: ToJSValConvertible> Runnable for ResolvePromise<T> { + fn main_thread_handler(self: Box<Self>, script_thread: &ScriptThread) { + let this = *self; + let cx = script_thread.get_cx(); + let promise = this.0.root(); + let _ac = JSAutoCompartment::new(cx, promise.reflector().get_jsobject().get()); + promise.resolve_native(cx, &this.1); + } + } + ResolvePromise(self, value) + } } /// A safe wrapper around a raw pointer to a DOM object that can be diff --git a/components/script/dom/globalscope.rs b/components/script/dom/globalscope.rs index 931d1f576aa..ce81958a2cb 100644 --- a/components/script/dom/globalscope.rs +++ b/components/script/dom/globalscope.rs @@ -19,6 +19,7 @@ use dom::event::{Event, EventBubbles, EventCancelable, EventStatus}; use dom::eventtarget::EventTarget; use dom::window::Window; use dom::workerglobalscope::WorkerGlobalScope; +use dom::workletglobalscope::WorkletGlobalScope; use dom_struct::dom_struct; use ipc_channel::ipc::IpcSender; use js::{JSCLASS_IS_DOMJSCLASS, JSCLASS_IS_GLOBAL}; @@ -259,6 +260,10 @@ impl GlobalScope { // https://html.spec.whatwg.org/multipage/#script-settings-for-workers:api-base-url return worker.get_url().clone(); } + if let Some(worker) = self.downcast::<WorkletGlobalScope>() { + // https://drafts.css-houdini.org/worklets/#script-settings-for-worklets + return worker.base_url(); + } unreachable!(); } @@ -270,6 +275,10 @@ impl GlobalScope { if let Some(worker) = self.downcast::<WorkerGlobalScope>() { return worker.get_url().clone(); } + if let Some(worker) = self.downcast::<WorkletGlobalScope>() { + // TODO: is this the right URL to return? + return worker.base_url(); + } unreachable!(); } @@ -349,14 +358,14 @@ impl GlobalScope { /// Evaluate JS code on this global scope. pub fn evaluate_js_on_global_with_result( - &self, code: &str, rval: MutableHandleValue) { + &self, code: &str, rval: MutableHandleValue) -> bool { self.evaluate_script_on_global_with_result(code, "", rval, 1) } /// Evaluate a JS script on this global scope. #[allow(unsafe_code)] pub fn evaluate_script_on_global_with_result( - &self, code: &str, filename: &str, rval: MutableHandleValue, line_number: u32) { + &self, code: &str, filename: &str, rval: MutableHandleValue, line_number: u32) -> bool { let metadata = time::TimerMetadata { url: if filename.is_empty() { self.get_url().as_str().into() @@ -379,16 +388,21 @@ impl GlobalScope { let _ac = JSAutoCompartment::new(cx, globalhandle.get()); let _aes = AutoEntryScript::new(self); let options = CompileOptionsWrapper::new(cx, filename.as_ptr(), line_number); - unsafe { - if !Evaluate2(cx, options.ptr, code.as_ptr(), - code.len() as libc::size_t, - rval) { - debug!("error evaluating JS string"); - report_pending_exception(cx, true); - } + + debug!("evaluating JS string"); + let result = unsafe { + Evaluate2(cx, options.ptr, code.as_ptr(), + code.len() as libc::size_t, + rval) + }; + + if !result { + debug!("error evaluating JS string"); + unsafe { report_pending_exception(cx, true) }; } maybe_resume_unwind(); + result } ) } @@ -468,6 +482,9 @@ impl GlobalScope { if let Some(worker) = self.downcast::<WorkerGlobalScope>() { return worker.perform_a_microtask_checkpoint(); } + if let Some(worker) = self.downcast::<WorkletGlobalScope>() { + return worker.perform_a_microtask_checkpoint(); + } unreachable!(); } @@ -479,6 +496,9 @@ impl GlobalScope { if let Some(worker) = self.downcast::<WorkerGlobalScope>() { return worker.enqueue_microtask(job); } + if let Some(worker) = self.downcast::<WorkletGlobalScope>() { + return worker.enqueue_microtask(job); + } unreachable!(); } diff --git a/components/script/dom/mod.rs b/components/script/dom/mod.rs index d155b8b6a47..62bffbdcbcd 100644 --- a/components/script/dom/mod.rs +++ b/components/script/dom/mod.rs @@ -423,6 +423,8 @@ pub mod testbindingiterable; pub mod testbindingpairiterable; pub mod testbindingproxy; pub mod testrunner; +pub mod testworklet; +pub mod testworkletglobalscope; pub mod text; pub mod textdecoder; pub mod textencoder; @@ -469,6 +471,8 @@ pub mod worker; pub mod workerglobalscope; pub mod workerlocation; pub mod workernavigator; +pub mod worklet; +pub mod workletglobalscope; pub mod xmldocument; pub mod xmlhttprequest; pub mod xmlhttprequesteventtarget; diff --git a/components/script/dom/promise.rs b/components/script/dom/promise.rs index 13689023462..5595e999c9d 100644 --- a/components/script/dom/promise.rs +++ b/components/script/dom/promise.rs @@ -296,3 +296,4 @@ fn create_native_handler_function(cx: *mut JSContext, obj.get() } } + diff --git a/components/script/dom/testworklet.rs b/components/script/dom/testworklet.rs new file mode 100644 index 00000000000..ea032e66faa --- /dev/null +++ b/components/script/dom/testworklet.rs @@ -0,0 +1,61 @@ +/* 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 http://mozilla.org/MPL/2.0/. */ + +// check-tidy: no specs after this line + +use dom::bindings::codegen::Bindings::TestWorkletBinding::TestWorkletMethods; +use dom::bindings::codegen::Bindings::TestWorkletBinding::Wrap; +use dom::bindings::codegen::Bindings::WorkletBinding::WorkletBinding::WorkletMethods; +use dom::bindings::codegen::Bindings::WorkletBinding::WorkletOptions; +use dom::bindings::error::Fallible; +use dom::bindings::js::JS; +use dom::bindings::js::Root; +use dom::bindings::reflector::Reflector; +use dom::bindings::reflector::reflect_dom_object; +use dom::bindings::str::DOMString; +use dom::bindings::str::USVString; +use dom::promise::Promise; +use dom::window::Window; +use dom::worklet::Worklet; +use dom::workletglobalscope::WorkletGlobalScopeType; +use dom_struct::dom_struct; +use script_thread::ScriptThread; +use std::rc::Rc; + +#[dom_struct] +pub struct TestWorklet { + reflector: Reflector, + worklet: JS<Worklet>, +} + +impl TestWorklet { + fn new_inherited(worklet: &Worklet) -> TestWorklet { + TestWorklet { + reflector: Reflector::new(), + worklet: JS::from_ref(worklet), + } + } + + fn new(window: &Window) -> Root<TestWorklet> { + let worklet = Worklet::new(window, WorkletGlobalScopeType::Test); + reflect_dom_object(box TestWorklet::new_inherited(&*worklet), window, Wrap) + } + + pub fn Constructor(window: &Window) -> Fallible<Root<TestWorklet>> { + Ok(TestWorklet::new(window)) + } +} + +impl TestWorkletMethods for TestWorklet { + #[allow(unrooted_must_root)] + fn AddModule(&self, moduleURL: USVString, options: &WorkletOptions) -> Rc<Promise> { + self.worklet.AddModule(moduleURL, options) + } + + fn Lookup(&self, key: DOMString) -> Option<DOMString> { + let id = self.worklet.worklet_id(); + let pool = ScriptThread::worklet_thread_pool(); + pool.test_worklet_lookup(id, String::from(key)).map(DOMString::from) + } +} diff --git a/components/script/dom/testworkletglobalscope.rs b/components/script/dom/testworkletglobalscope.rs new file mode 100644 index 00000000000..dfd000ac5c1 --- /dev/null +++ b/components/script/dom/testworkletglobalscope.rs @@ -0,0 +1,66 @@ +/* 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 http://mozilla.org/MPL/2.0/. */ + +use dom::bindings::cell::DOMRefCell; +use dom::bindings::codegen::Bindings::TestWorkletGlobalScopeBinding; +use dom::bindings::codegen::Bindings::TestWorkletGlobalScopeBinding::TestWorkletGlobalScopeMethods; +use dom::bindings::js::Root; +use dom::bindings::str::DOMString; +use dom::workletglobalscope::WorkletGlobalScope; +use dom::workletglobalscope::WorkletGlobalScopeInit; +use dom_struct::dom_struct; +use js::rust::Runtime; +use msg::constellation_msg::PipelineId; +use servo_url::ServoUrl; +use std::collections::HashMap; +use std::sync::mpsc::Sender; + +// check-tidy: no specs after this line + +#[dom_struct] +pub struct TestWorkletGlobalScope { + // The worklet global for this object + worklet_global: WorkletGlobalScope, + // The key/value pairs + lookup_table: DOMRefCell<HashMap<String, String>>, +} + +impl TestWorkletGlobalScope { + #[allow(unsafe_code)] + pub fn new(runtime: &Runtime, + pipeline_id: PipelineId, + base_url: ServoUrl, + init: &WorkletGlobalScopeInit) + -> Root<TestWorkletGlobalScope> + { + debug!("Creating test worklet global scope for pipeline {}.", pipeline_id); + let global = box TestWorkletGlobalScope { + worklet_global: WorkletGlobalScope::new_inherited(pipeline_id, base_url, init), + lookup_table: Default::default(), + }; + unsafe { TestWorkletGlobalScopeBinding::Wrap(runtime.cx(), global) } + } + + pub fn perform_a_worklet_task(&self, task: TestWorkletTask) { + match task { + TestWorkletTask::Lookup(key, sender) => { + debug!("Looking up key {}.", key); + let result = self.lookup_table.borrow().get(&key).cloned(); + let _ = sender.send(result); + } + } + } +} + +impl TestWorkletGlobalScopeMethods for TestWorkletGlobalScope { + fn RegisterKeyValue(&self, key: DOMString, value: DOMString) { + debug!("Registering test worklet key/value {}/{}.", key, value); + self.lookup_table.borrow_mut().insert(String::from(key), String::from(value)); + } +} + +/// Tasks which can be performed by test worklets. +pub enum TestWorkletTask { + Lookup(String, Sender<Option<String>>), +} diff --git a/components/script/dom/webidls/Console.webidl b/components/script/dom/webidls/Console.webidl index 90f9bb9f58e..7c4c6906a27 100644 --- a/components/script/dom/webidls/Console.webidl +++ b/components/script/dom/webidls/Console.webidl @@ -10,7 +10,7 @@ */ [ClassString="Console", - Exposed=(Window,Worker), + Exposed=(Window,Worker,Worklet), ProtoObjectHack] namespace console { // These should be DOMString message, DOMString message2, ... diff --git a/components/script/dom/webidls/EventTarget.webidl b/components/script/dom/webidls/EventTarget.webidl index ee6e5d722a8..ad25712122a 100644 --- a/components/script/dom/webidls/EventTarget.webidl +++ b/components/script/dom/webidls/EventTarget.webidl @@ -5,7 +5,7 @@ * https://dom.spec.whatwg.org/#interface-eventtarget */ -[Abstract, Exposed=(Window,Worker)] +[Abstract, Exposed=(Window,Worker,Worklet)] interface EventTarget { void addEventListener(DOMString type, EventListener? listener, diff --git a/components/script/dom/webidls/GlobalScope.webidl b/components/script/dom/webidls/GlobalScope.webidl index 7dab4f3afa7..2681d236dbc 100644 --- a/components/script/dom/webidls/GlobalScope.webidl +++ b/components/script/dom/webidls/GlobalScope.webidl @@ -5,6 +5,6 @@ // This interface is entirely internal to Servo, and should not be accessible to // web pages. -[Exposed=(Window,Worker), +[Exposed=(Window,Worker,Worklet), Inline] interface GlobalScope : EventTarget {}; diff --git a/components/script/dom/webidls/TestWorklet.webidl b/components/script/dom/webidls/TestWorklet.webidl new file mode 100644 index 00000000000..c1f1965a1e0 --- /dev/null +++ b/components/script/dom/webidls/TestWorklet.webidl @@ -0,0 +1,12 @@ +/* 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 http://mozilla.org/MPL/2.0/. */ + +// This interface is entirely internal to Servo, and should not be accessible to +// web pages. + +[Pref="dom.worklet.testing.enabled", Exposed=(Window), Constructor] +interface TestWorklet { + [NewObject] Promise<void> addModule(USVString moduleURL, optional WorkletOptions options); + DOMString? lookup(DOMString key); +}; diff --git a/components/script/dom/webidls/TestWorkletGlobalScope.webidl b/components/script/dom/webidls/TestWorkletGlobalScope.webidl new file mode 100644 index 00000000000..44027ab8dc6 --- /dev/null +++ b/components/script/dom/webidls/TestWorkletGlobalScope.webidl @@ -0,0 +1,11 @@ +/* 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 http://mozilla.org/MPL/2.0/. */ + +// This interface is entirely internal to Servo, and should not be accessible to +// web pages. + +[Global=(Worklet,TestWorklet), Exposed=TestWorklet] +interface TestWorkletGlobalScope : WorkletGlobalScope { + void registerKeyValue(DOMString key, DOMString value); +}; diff --git a/components/script/dom/webidls/VoidFunction.webidl b/components/script/dom/webidls/VoidFunction.webidl new file mode 100644 index 00000000000..82d4a666c51 --- /dev/null +++ b/components/script/dom/webidls/VoidFunction.webidl @@ -0,0 +1,13 @@ +/* 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 http://mozilla.org/MPL/2.0/. */ +/* + * The origin of this IDL file is + * https://heycam.github.io/webidl/#VoidFunction + * + * © Copyright 2004-2011 Apple Computer, Inc., Mozilla Foundation, and + * Opera Software ASA. You are granted a license to use, reproduce + * and create derivative works of this document. + */ + +callback VoidFunction = void (); diff --git a/components/script/dom/webidls/Window.webidl b/components/script/dom/webidls/Window.webidl index 47c753f43b1..548821ac971 100644 --- a/components/script/dom/webidls/Window.webidl +++ b/components/script/dom/webidls/Window.webidl @@ -201,3 +201,4 @@ partial interface Window { readonly attribute TestRunner testRunner; //readonly attribute EventSender eventSender; }; + diff --git a/components/script/dom/webidls/Worklet.webidl b/components/script/dom/webidls/Worklet.webidl new file mode 100644 index 00000000000..5bb39bebd96 --- /dev/null +++ b/components/script/dom/webidls/Worklet.webidl @@ -0,0 +1,13 @@ +/* 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 http://mozilla.org/MPL/2.0/. */ + +// https://drafts.css-houdini.org/worklets/#worklet +[Exposed=(Window)] +interface Worklet { + [NewObject] Promise<void> addModule(USVString moduleURL, optional WorkletOptions options); +}; + +dictionary WorkletOptions { + RequestCredentials credentials = "omit"; +}; diff --git a/components/script/dom/webidls/WorkletGlobalScope.webidl b/components/script/dom/webidls/WorkletGlobalScope.webidl new file mode 100644 index 00000000000..ca29296a10e --- /dev/null +++ b/components/script/dom/webidls/WorkletGlobalScope.webidl @@ -0,0 +1,10 @@ +/* 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 http://mozilla.org/MPL/2.0/. */ + +// https://drafts.css-houdini.org/worklets/#workletglobalscope +// TODO: The spec IDL doesn't make this a subclass of EventTarget +// https://github.com/whatwg/html/issues/2611 +[Exposed=Worklet] +interface WorkletGlobalScope: GlobalScope { +}; diff --git a/components/script/dom/window.rs b/components/script/dom/window.rs index 65d1a591adc..097f136e29c 100644 --- a/components/script/dom/window.rs +++ b/components/script/dom/window.rs @@ -49,6 +49,7 @@ use dom::screen::Screen; use dom::storage::Storage; use dom::testrunner::TestRunner; use dom::windowproxy::WindowProxy; +use dom::worklet::Worklet; use dom_struct::dom_struct; use euclid::{Point2D, Rect, Size2D}; use fetch; @@ -273,6 +274,9 @@ pub struct Window { /// Directory to store unminified scripts for this window if unminify-js /// opt is enabled. unminified_js_dir: DOMRefCell<Option<String>>, + + /// Worklets + test_worklet: MutNullableJS<Worklet>, } impl Window { @@ -1830,6 +1834,7 @@ impl Window { permission_state_invocation_results: DOMRefCell::new(HashMap::new()), pending_layout_images: DOMRefCell::new(HashMap::new()), unminified_js_dir: DOMRefCell::new(None), + test_worklet: Default::default(), }; unsafe { diff --git a/components/script/dom/worklet.rs b/components/script/dom/worklet.rs new file mode 100644 index 00000000000..fa5b3950b51 --- /dev/null +++ b/components/script/dom/worklet.rs @@ -0,0 +1,637 @@ +/* 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 http://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 dom::bindings::codegen::Bindings::RequestBinding::RequestCredentials; +use dom::bindings::codegen::Bindings::WindowBinding::WindowBinding::WindowMethods; +use dom::bindings::codegen::Bindings::WorkletBinding::WorkletMethods; +use dom::bindings::codegen::Bindings::WorkletBinding::WorkletOptions; +use dom::bindings::codegen::Bindings::WorkletBinding::Wrap; +use dom::bindings::error::Error; +use dom::bindings::inheritance::Castable; +use dom::bindings::js::JS; +use dom::bindings::js::Root; +use dom::bindings::js::RootCollection; +use dom::bindings::refcounted::TrustedPromise; +use dom::bindings::reflector::Reflector; +use dom::bindings::reflector::reflect_dom_object; +use dom::bindings::str::USVString; +use dom::bindings::trace::JSTraceable; +use dom::bindings::trace::RootedTraceableBox; +use dom::globalscope::GlobalScope; +use dom::promise::Promise; +use dom::testworkletglobalscope::TestWorkletTask; +use dom::window::Window; +use dom::workletglobalscope::WorkletGlobalScope; +use dom::workletglobalscope::WorkletGlobalScopeInit; +use dom::workletglobalscope::WorkletGlobalScopeType; +use dom::workletglobalscope::WorkletTask; +use dom_struct::dom_struct; +use js::jsapi::JSGCParamKey; +use js::jsapi::JSTracer; +use js::jsapi::JS_GC; +use js::jsapi::JS_GetGCParameter; +use js::rust::Runtime; +use msg::constellation_msg::PipelineId; +use net_traits::IpcSend; +use net_traits::load_whole_resource; +use net_traits::request::Destination; +use net_traits::request::RequestInit; +use net_traits::request::RequestMode; +use net_traits::request::Type as RequestType; +use script_runtime::CommonScriptMsg; +use script_runtime::ScriptThreadEventCategory; +use script_runtime::StackRootTLS; +use script_runtime::new_rt_and_cx; +use script_thread::MainThreadScriptMsg; +use script_thread::Runnable; +use script_thread::ScriptThread; +use servo_rand; +use servo_url::ImmutableOrigin; +use servo_url::ServoUrl; +use std::cmp::max; +use std::collections::HashMap; +use std::collections::hash_map; +use std::rc::Rc; +use std::sync::Arc; +use std::sync::atomic::AtomicIsize; +use std::sync::atomic::Ordering; +use std::sync::mpsc; +use std::sync::mpsc::Receiver; +use std::sync::mpsc::Sender; +use std::thread; +use style::thread_state; +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: JS<Window>, + worklet_id: WorkletId, + global_type: WorkletGlobalScopeType, +} + +impl Worklet { + fn new_inherited(window: &Window, global_type: WorkletGlobalScopeType) -> Worklet { + Worklet { + reflector: Reflector::new(), + window: JS::from_ref(window), + worklet_id: WorkletId::new(), + global_type: global_type, + } + } + + pub fn new(window: &Window, global_type: WorkletGlobalScopeType) -> Root<Worklet> { + debug!("Creating worklet {:?}.", global_type); + reflect_dom_object(box Worklet::new_inherited(window, global_type), window, Wrap) + } + + 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 { + #[allow(unrooted_must_root)] + /// https://drafts.css-houdini.org/worklets/#dom-worklet-addmodule + fn AddModule(&self, module_url: USVString, options: &WorkletOptions) -> Rc<Promise> { + // Step 1. + let promise = Promise::new(self.window.upcast()); + + // 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(self.window.get_cx(), 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. + promise + } +} + +/// A guid for worklets. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, JSTraceable)] +pub struct WorkletId(Uuid); + +known_heap_size!(0, WorkletId); + +impl WorkletId { + fn new() -> WorkletId { + WorkletId(servo_rand::random()) + } +} + +/// 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. + +#[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(script_sender: Sender<MainThreadScriptMsg>, 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 { + hot_backup_sender: hot_backup_sender.clone(), + cold_backup_sender: cold_backup_sender.clone(), + script_sender: script_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()), + }); + } + // 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); + } + + /// For testing. + pub fn test_worklet_lookup(&self, id: WorkletId, key: String) -> Option<String> { + let (sender, receiver) = mpsc::channel(); + let msg = WorkletData::Task(id, WorkletTask::Test(TestWorkletTask::Lookup(key, sender))); + let _ = self.primary_sender.send(msg); + receiver.recv().expect("Test worklet has died?") + } +} + +/// 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 { + 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) = mpsc::channel(); + 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 + hot_backup_sender: Sender<WorkletData>, + cold_backup_sender: Sender<WorkletData>, + script_sender: Sender<MainThreadScriptMsg>, + + /// Data for initializing new worklet global scopes + global_init: WorkletGlobalScopeInit, +} + +/// A thread for executing worklets. +#[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 + hot_backup_sender: Sender<WorkletData>, + cold_backup_sender: Sender<WorkletData>, + script_sender: Sender<MainThreadScriptMsg>, + + /// Data for initializing new worklet global scopes + global_init: WorkletGlobalScopeInit, + + /// The global scopes created by this thread + global_scopes: HashMap<WorkletId, JS<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) = mpsc::channel(); + // 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(thread_state::SCRIPT | thread_state::IN_WORKER); + let roots = RootCollection::new(); + let _stack_roots_tls = StackRootTLS::new(&roots); + let mut thread = RootedTraceableBox::new(WorkletThread { + role: role, + control_receiver: control_receiver, + hot_backup_sender: init.hot_backup_sender, + cold_backup_sender: init.cold_backup_sender, + script_sender: init.script_sender, + global_init: init.global_init, + global_scopes: HashMap::new(), + control_buffer: None, + runtime: unsafe { new_rt_and_cx() }, + 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.rt(), 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.rt()) }; + 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) + -> Root<WorkletGlobalScope> + { + match self.global_scopes.entry(worklet_id) { + hash_map::Entry::Occupied(entry) => Root::from_ref(entry.get()), + hash_map::Entry::Vacant(entry) => { + let result = global_type.new(&self.runtime, pipeline_id, base_url, &self.global_init); + entry.insert(JS::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, + 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. + // TODO: Avoid re-parsing the origin as a URL. + let resource_fetcher = self.global_init.resource_threads.sender(); + let origin_url = ServoUrl::parse(&*origin.unicode_serialization()).expect("Failed to parse origin as URL."); + let request = RequestInit { + url: script_url, + type_: RequestType::Script, + destination: Destination::Script, + mode: RequestMode::CorsMode, + origin: origin_url, + credentials_mode: credentials.into(), + .. RequestInit::default() + }; + let script = load_whole_resource(request, &resource_fetcher).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. + 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_runnable(Error::Abort)); + } + } else { + // Step 5. + debug!("Finished adding script."); + let old_counter = pending_tasks_struct.decrement_counter_by(1); + if old_counter == 1 { + // TODO: trigger a reflow? + self.run_in_script_thread(promise.resolve_runnable(())); + } + } + } + + /// 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::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, + origin, + script_url, + credentials, + pending_tasks_struct, + promise) + } + } + } + + /// Run a runnable in the main script thread. + fn run_in_script_thread<R>(&self, runnable: R) where + R: 'static + Send + Runnable, + { + let msg = CommonScriptMsg::RunnableMsg(ScriptThreadEventCategory::WorkletEvent, box runnable); + let msg = MainThreadScriptMsg::Common(msg); + self.script_sender.send(msg).expect("Worklet thread outlived script thread."); + } +} diff --git a/components/script/dom/workletglobalscope.rs b/components/script/dom/workletglobalscope.rs new file mode 100644 index 00000000000..a2e2463ca27 --- /dev/null +++ b/components/script/dom/workletglobalscope.rs @@ -0,0 +1,143 @@ +/* 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 http://mozilla.org/MPL/2.0/. */ + +use devtools_traits::ScriptToDevtoolsControlMsg; +use dom::bindings::inheritance::Castable; +use dom::bindings::js::Root; +use dom::globalscope::GlobalScope; +use dom::testworkletglobalscope::TestWorkletGlobalScope; +use dom::testworkletglobalscope::TestWorkletTask; +use dom_struct::dom_struct; +use ipc_channel::ipc; +use ipc_channel::ipc::IpcSender; +use js::jsval::UndefinedValue; +use js::rust::Runtime; +use microtask::Microtask; +use microtask::MicrotaskQueue; +use msg::constellation_msg::PipelineId; +use net_traits::ResourceThreads; +use profile_traits::mem; +use profile_traits::time; +use script_traits::ScriptMsg; +use script_traits::TimerSchedulerMsg; +use servo_url::ImmutableOrigin; +use servo_url::MutableOrigin; +use servo_url::ServoUrl; + +#[dom_struct] +/// https://drafts.css-houdini.org/worklets/#workletglobalscope +pub struct WorkletGlobalScope { + /// The global for this worklet. + globalscope: GlobalScope, + /// The base URL for this worklet. + base_url: ServoUrl, + /// The microtask queue for this worklet + microtask_queue: MicrotaskQueue, +} + +impl WorkletGlobalScope { + /// Create a new stack-allocated `WorkletGlobalScope`. + pub fn new_inherited(pipeline_id: PipelineId, + base_url: ServoUrl, + init: &WorkletGlobalScopeInit) + -> WorkletGlobalScope { + // Any timer events fired on this global are ignored. + let (timer_event_chan, _) = ipc::channel().unwrap(); + WorkletGlobalScope { + globalscope: GlobalScope::new_inherited(pipeline_id, + init.devtools_chan.clone(), + init.mem_profiler_chan.clone(), + init.time_profiler_chan.clone(), + init.constellation_chan.clone(), + init.scheduler_chan.clone(), + init.resource_threads.clone(), + timer_event_chan, + MutableOrigin::new(ImmutableOrigin::new_opaque())), + base_url: base_url, + microtask_queue: MicrotaskQueue::default(), + } + } + + /// Evaluate a JS script in this global. + pub fn evaluate_js(&self, script: &str) -> bool { + debug!("Evaluating JS."); + rooted!(in (self.globalscope.get_cx()) let mut rval = UndefinedValue()); + self.globalscope.evaluate_js_on_global_with_result(&*script, rval.handle_mut()) + } + + /// The base URL of this global. + pub fn base_url(&self) -> ServoUrl { + self.base_url.clone() + } + + /// Queue up a microtask to be executed in this global. + pub fn enqueue_microtask(&self, job: Microtask) { + self.microtask_queue.enqueue(job); + } + + /// Perform any queued microtasks. + pub fn perform_a_microtask_checkpoint(&self) { + self.microtask_queue.checkpoint(|id| { + let global = self.upcast::<GlobalScope>(); + assert_eq!(global.pipeline_id(), id); + Some(Root::from_ref(global)) + }); + } + + /// Perform a worklet task + pub fn perform_a_worklet_task(&self, task: WorkletTask) { + match task { + WorkletTask::Test(task) => match self.downcast::<TestWorkletGlobalScope>() { + Some(global) => global.perform_a_worklet_task(task), + None => warn!("This is not a test worklet."), + }, + } + } +} + +/// Resources required by workletglobalscopes +#[derive(Clone)] +pub struct WorkletGlobalScopeInit { + /// Channel to a resource thread + pub resource_threads: ResourceThreads, + /// Channel to the memory profiler + pub mem_profiler_chan: mem::ProfilerChan, + /// Channel to the time profiler + pub time_profiler_chan: time::ProfilerChan, + /// Channel to devtools + pub devtools_chan: Option<IpcSender<ScriptToDevtoolsControlMsg>>, + /// Messages to send to constellation + pub constellation_chan: IpcSender<ScriptMsg>, + /// Message to send to the scheduler + pub scheduler_chan: IpcSender<TimerSchedulerMsg>, +} + +/// https://drafts.css-houdini.org/worklets/#worklet-global-scope-type +#[derive(Clone, Copy, Debug, HeapSizeOf, JSTraceable)] +pub enum WorkletGlobalScopeType { + /// https://drafts.css-houdini.org/worklets/#examples + Test, +} + +impl WorkletGlobalScopeType { + /// Create a new heap-allocated `WorkletGlobalScope`. + pub fn new(&self, + runtime: &Runtime, + pipeline_id: PipelineId, + base_url: ServoUrl, + init: &WorkletGlobalScopeInit) + -> Root<WorkletGlobalScope> + { + match *self { + WorkletGlobalScopeType::Test => + Root::upcast(TestWorkletGlobalScope::new(runtime, pipeline_id, base_url, init)), + } + } +} + +/// A task which can be performed in the context of a worklet global. +pub enum WorkletTask { + Test(TestWorkletTask), +} + diff --git a/components/script/lib.rs b/components/script/lib.rs index c1bd886b927..703209604b0 100644 --- a/components/script/lib.rs +++ b/components/script/lib.rs @@ -10,10 +10,12 @@ #![feature(nonzero)] #![feature(on_unimplemented)] #![feature(optin_builtin_traits)] +#![feature(option_entry)] #![feature(plugin)] #![feature(proc_macro)] #![feature(stmt_expr_attributes)] #![feature(try_from)] +#![feature(unboxed_closures)] #![feature(untagged_unions)] #![deny(unsafe_code)] @@ -46,7 +48,7 @@ extern crate encoding; extern crate euclid; extern crate fnv; extern crate gfx_traits; -extern crate heapsize; +#[macro_use] extern crate heapsize; #[macro_use] extern crate heapsize_derive; #[macro_use] extern crate html5ever; #[macro_use] @@ -92,6 +94,7 @@ extern crate smallvec; #[macro_use] extern crate style; extern crate style_traits; +extern crate swapper; extern crate time; #[cfg(any(target_os = "macos", target_os = "linux", target_os = "windows"))] extern crate tinyfiledialogs; diff --git a/components/script/script_runtime.rs b/components/script/script_runtime.rs index c4f90edf8ea..8c243b18695 100644 --- a/components/script/script_runtime.rs +++ b/components/script/script_runtime.rs @@ -74,6 +74,7 @@ pub enum ScriptThreadEventCategory { UpdateReplacedElement, WebSocketEvent, WorkerEvent, + WorkletEvent, ServiceWorkerEvent, EnterFullscreen, ExitFullscreen, diff --git a/components/script/script_thread.rs b/components/script/script_thread.rs index a8b20b6cf98..8a6efa946ad 100644 --- a/components/script/script_thread.rs +++ b/components/script/script_thread.rs @@ -56,6 +56,8 @@ use dom::uievent::UIEvent; use dom::window::{ReflowReason, Window}; use dom::windowproxy::WindowProxy; use dom::worker::TrustedWorkerAddress; +use dom::worklet::WorkletThreadPool; +use dom::workletglobalscope::WorkletGlobalScopeInit; use euclid::Rect; use euclid::point::Point2D; use hyper::header::{ContentType, HttpDate, LastModified, Headers}; @@ -490,6 +492,9 @@ pub struct ScriptThread { /// A handle to the webvr thread, if available webvr_thread: Option<IpcSender<WebVRMsg>>, + /// The worklet thread pool + worklet_thread_pool: DOMRefCell<Option<Rc<WorkletThreadPool>>>, + /// A list of pipelines containing documents that finished loading all their blocking /// resources during a turn of the event loop. docs_with_no_blocking_loads: DOMRefCell<HashSet<JS<Document>>>, @@ -703,6 +708,24 @@ impl ScriptThread { })) } + pub fn worklet_thread_pool() -> Rc<WorkletThreadPool> { + SCRIPT_THREAD_ROOT.with(|root| { + let script_thread = unsafe { &*root.get().unwrap() }; + script_thread.worklet_thread_pool.borrow_mut().get_or_insert_with(|| { + let chan = script_thread.chan.0.clone(); + let init = WorkletGlobalScopeInit { + resource_threads: script_thread.resource_threads.clone(), + mem_profiler_chan: script_thread.mem_profiler_chan.clone(), + time_profiler_chan: script_thread.time_profiler_chan.clone(), + devtools_chan: script_thread.devtools_chan.clone(), + constellation_chan: script_thread.constellation_chan.clone(), + scheduler_chan: script_thread.scheduler_chan.clone(), + }; + Rc::new(WorkletThreadPool::spawn(chan, init)) + }).clone() + }) + } + /// Creates a new script thread. pub fn new(state: InitialScriptState, port: Receiver<MainThreadScriptMsg>, @@ -782,6 +805,8 @@ impl ScriptThread { webvr_thread: state.webvr_thread, + worklet_thread_pool: Default::default(), + docs_with_no_blocking_loads: Default::default(), transitioning_nodes: Default::default(), @@ -1065,6 +1090,7 @@ impl ScriptThread { ScriptThreadEventCategory::WebSocketEvent => ProfilerCategory::ScriptWebSocketEvent, ScriptThreadEventCategory::WebVREvent => ProfilerCategory::ScriptWebVREvent, ScriptThreadEventCategory::WorkerEvent => ProfilerCategory::ScriptWorkerEvent, + ScriptThreadEventCategory::WorkletEvent => ProfilerCategory::ScriptWorkletEvent, ScriptThreadEventCategory::ServiceWorkerEvent => ProfilerCategory::ScriptServiceWorkerEvent, ScriptThreadEventCategory::EnterFullscreen => ProfilerCategory::ScriptEnterFullscreen, ScriptThreadEventCategory::ExitFullscreen => ProfilerCategory::ScriptExitFullscreen, @@ -1149,7 +1175,7 @@ impl ScriptThread { // The category of the runnable is ignored by the pattern, however // it is still respected by profiling (see categorize_msg). if !runnable.is_cancelled() { - runnable.handler() + runnable.main_thread_handler(self) } } MainThreadScriptMsg::Common(CommonScriptMsg::CollectReports(reports_chan)) => diff --git a/components/script_traits/Cargo.toml b/components/script_traits/Cargo.toml index da5fcc78704..993c14c43b0 100644 --- a/components/script_traits/Cargo.toml +++ b/components/script_traits/Cargo.toml @@ -10,7 +10,6 @@ name = "script_traits" path = "lib.rs" [dependencies] -app_units = "0.4" bluetooth_traits = {path = "../bluetooth_traits"} canvas_traits = {path = "../canvas_traits"} cookie = "0.6" diff --git a/python/tidy/servo_tidy/tidy.py b/python/tidy/servo_tidy/tidy.py index ccca233db9c..b8229e67d96 100644 --- a/python/tidy/servo_tidy/tidy.py +++ b/python/tidy/servo_tidy/tidy.py @@ -67,6 +67,7 @@ WEBIDL_STANDARDS = [ "//dom.spec.whatwg.org", "//domparsing.spec.whatwg.org", "//drafts.csswg.org", + "//drafts.css-houdini.org", "//drafts.fxtf.org", "//encoding.spec.whatwg.org", "//fetch.spec.whatwg.org", diff --git a/tests/wpt/mozilla/meta/MANIFEST.json b/tests/wpt/mozilla/meta/MANIFEST.json index ce611aee8f1..c94175fdce8 100644 --- a/tests/wpt/mozilla/meta/MANIFEST.json +++ b/tests/wpt/mozilla/meta/MANIFEST.json @@ -11185,6 +11185,21 @@ [ {} ] + ], + "mozilla/worklets/syntax_error.js": [ + [ + {} + ] + ], + "mozilla/worklets/test_worklet.js": [ + [ + {} + ] + ], + "mozilla/worklets/throw_exception.js": [ + [ + {} + ] ] }, "testharness": { @@ -20027,6 +20042,12 @@ "/_mozilla/mozilla/windowproxy.html", {} ] + ], + "mozilla/worklets/test_worklet.html": [ + [ + "/_mozilla/mozilla/worklets/test_worklet.html", + {} + ] ] } }, @@ -25796,7 +25817,7 @@ "testharness" ], "mozilla/interfaces.html": [ - "21e18bafdbfe5f3aa0ee71766bdc3b6a7e334226", + "49dd9f6ef449813f2ce943d4c9fac351357e5c74", "testharness" ], "mozilla/interfaces.js": [ @@ -31714,6 +31735,22 @@ "mozilla/windowproxy.html": [ "128cd0aa5cf80f60078979039036d32b470b0616", "testharness" + ], + "mozilla/worklets/syntax_error.js": [ + "f3a9b8c78346507bc0b3190c8000ccf80cc133f6", + "support" + ], + "mozilla/worklets/test_worklet.html": [ + "fe9c93a5307c616f878b6623155e1b04c86dd994", + "testharness" + ], + "mozilla/worklets/test_worklet.js": [ + "9d5f8a07cd62a10f4f5ff93744672e5a6fdbc2b0", + "support" + ], + "mozilla/worklets/throw_exception.js": [ + "ebfdae19db68fed8e69142ef73842ac9921e4463", + "support" ] }, "url_base": "/_mozilla/", diff --git a/tests/wpt/mozilla/meta/mozilla/worklets/test_worklet.html.ini b/tests/wpt/mozilla/meta/mozilla/worklets/test_worklet.html.ini new file mode 100644 index 00000000000..37762a513dd --- /dev/null +++ b/tests/wpt/mozilla/meta/mozilla/worklets/test_worklet.html.ini @@ -0,0 +1,3 @@ +[test_worklet.html] + type: testharness + prefs: [dom.worklet.testing.enabled:true] diff --git a/tests/wpt/mozilla/tests/mozilla/interfaces.html b/tests/wpt/mozilla/tests/mozilla/interfaces.html index a29f439a7c6..ee987ae9322 100644 --- a/tests/wpt/mozilla/tests/mozilla/interfaces.html +++ b/tests/wpt/mozilla/tests/mozilla/interfaces.html @@ -200,6 +200,7 @@ test_interfaces([ "WebSocket", "Window", "Worker", + "Worklet", "XMLDocument", "XMLHttpRequest", "XMLHttpRequestEventTarget", diff --git a/tests/wpt/mozilla/tests/mozilla/worklets/syntax_error.js b/tests/wpt/mozilla/tests/mozilla/worklets/syntax_error.js new file mode 100644 index 00000000000..4adade8939c --- /dev/null +++ b/tests/wpt/mozilla/tests/mozilla/worklets/syntax_error.js @@ -0,0 +1 @@ +{]; diff --git a/tests/wpt/mozilla/tests/mozilla/worklets/test_worklet.html b/tests/wpt/mozilla/tests/mozilla/worklets/test_worklet.html new file mode 100644 index 00000000000..d7a9efa04fe --- /dev/null +++ b/tests/wpt/mozilla/tests/mozilla/worklets/test_worklet.html @@ -0,0 +1,35 @@ +<!doctype html> +<meta charset="utf-8"> +<title>Test worklet loading</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/common/get-host-info.sub.js"></script> +<script> +var testWorklet = new TestWorklet(); +var host_info = get_host_info(); + +promise_test(function() { + return testWorklet.addModule("test_worklet.js") + .then(function () { + assert_equals(testWorklet.lookup("hello"), "world"); + }); +}, "Loading a test worklet."); + +promise_test(function(t) { + var path = new URL("test_worklet.js", document.location).pathname; + var url = new URL(path, host_info.HTTP_REMOTE_ORIGIN); + return promise_rejects(t, "AbortError", testWorklet.addModule(url)); +}, "Loading a cross-origin test worklet."); + +promise_test(function(t) { + return promise_rejects(t, "AbortError", testWorklet.addModule("nonexistent_worklet.js")); +}, "Loading a nonexistent test worklet."); + +promise_test(function(t) { + return promise_rejects(t, "AbortError", testWorklet.addModule("syntax_error.js")); +}, "Loading a syntactically incorrect test worklet."); + +promise_test(function(t) { + return promise_rejects(t, "AbortError", testWorklet.addModule("throw_exception.js")); +}, "Loading an exception-throwing test worklet."); +</script> diff --git a/tests/wpt/mozilla/tests/mozilla/worklets/test_worklet.js b/tests/wpt/mozilla/tests/mozilla/worklets/test_worklet.js new file mode 100644 index 00000000000..9c0b392a6ab --- /dev/null +++ b/tests/wpt/mozilla/tests/mozilla/worklets/test_worklet.js @@ -0,0 +1 @@ +registerKeyValue("hello", "world"); diff --git a/tests/wpt/mozilla/tests/mozilla/worklets/throw_exception.js b/tests/wpt/mozilla/tests/mozilla/worklets/throw_exception.js new file mode 100644 index 00000000000..6ca4f80fc27 --- /dev/null +++ b/tests/wpt/mozilla/tests/mozilla/worklets/throw_exception.js @@ -0,0 +1 @@ +throw new TypeError(); |