Implemented Houdini worklets.

This commit is contained in:
Alan Jeffrey 2017-04-03 14:35:57 -05:00
parent abb2985ffe
commit af8436c9be
34 changed files with 1209 additions and 17 deletions

46
Cargo.lock generated
View file

@ -194,6 +194,11 @@ name = "bit-vec"
version = "0.4.3"
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"
@ -2142,6 +2147,15 @@ dependencies = [
"time 0.1.37 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[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"
@ -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)",
@ -2738,6 +2752,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
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"
@ -2882,6 +2905,14 @@ dependencies = [
"style_traits 0.0.1",
]
[[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"
@ -2968,6 +2999,14 @@ dependencies = [
"gcc 0.3.45 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[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"
@ -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"

View file

@ -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)

View file

@ -89,6 +89,7 @@ pub enum ProfilerCategory {
ScriptEnterFullscreen = 0x77,
ScriptExitFullscreen = 0x78,
ScriptWebVREvent = 0x79,
ScriptWorkletEvent = 0x7a,
ApplicationHeartbeat = 0x90,
}

View file

@ -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"]}

View file

@ -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

View file

@ -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!();
}

View file

@ -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;

View file

@ -296,3 +296,4 @@ fn create_native_handler_function(cx: *mut JSContext,
obj.get()
}
}

View file

@ -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)
}
}

View file

@ -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>>),
}

View file

@ -10,7 +10,7 @@
*/
[ClassString="Console",
Exposed=(Window,Worker),
Exposed=(Window,Worker,Worklet),
ProtoObjectHack]
namespace console {
// These should be DOMString message, DOMString message2, ...

View file

@ -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,

View file

@ -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 {};

View file

@ -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);
};

View file

@ -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);
};

View file

@ -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 ();

View file

@ -201,3 +201,4 @@ partial interface Window {
readonly attribute TestRunner testRunner;
//readonly attribute EventSender eventSender;
};

View file

@ -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";
};

View file

@ -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 {
};

View file

@ -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 {

View file

@ -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.");
}
}

View file

@ -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),
}

View file

@ -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;

View file

@ -74,6 +74,7 @@ pub enum ScriptThreadEventCategory {
UpdateReplacedElement,
WebSocketEvent,
WorkerEvent,
WorkletEvent,
ServiceWorkerEvent,
EnterFullscreen,
ExitFullscreen,

View file

@ -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)) =>

View file

@ -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"

View file

@ -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",

View file

@ -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/",

View file

@ -0,0 +1,3 @@
[test_worklet.html]
type: testharness
prefs: [dom.worklet.testing.enabled:true]

View file

@ -200,6 +200,7 @@ test_interfaces([
"WebSocket",
"Window",
"Worker",
"Worklet",
"XMLDocument",
"XMLHttpRequest",
"XMLHttpRequestEventTarget",

View file

@ -0,0 +1 @@
{];

View file

@ -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>

View file

@ -0,0 +1 @@
registerKeyValue("hello", "world");

View file

@ -0,0 +1 @@
throw new TypeError();