servo: Merge #16814 - Implement Houdini worklets (from asajeffrey:script-worklets); r=jdm

<!-- Please describe your changes on the following line: -->

This PR implements the current draft Houdini Worklets specification (https://drafts.css-houdini.org/worklets/).

The implementation is intended to provide a responsive environment for worklet execution, and in particular to ensure that the primary worklet executor does not garbage collect, and does not block loading module code. The implementation does this by providing a thread pool, and performing GC and module loading in a backup thread, not in the primary thread.

---
<!-- Thank you for contributing to Servo! Please replace each `[ ]` by `[X]` when the step is complete, and replace `__` with appropriate data: -->
- [x] `./mach build -d` does not report any errors
- [x] `./mach test-tidy` does not report any errors
- [x] These changes fix #16206
- [x] There are tests for these changes

<!-- Also, please make sure that "Allow edits from maintainers" checkbox is checked, so that we can help you if you get stuck somewhere along the way.-->

<!-- Pull requests that do not address these steps are welcome, but they will require additional verification as part of the review process. -->

Source-Repo: https://github.com/servo/servo
Source-Revision: ac99a48aeaa184d3acdb39d249636a140c4b7393

--HG--
rename : servo/components/script/dom/webidls/Function.webidl => servo/components/script/dom/webidls/VoidFunction.webidl
extra : subtree_source : https%3A//hg.mozilla.org/projects/converted-servo-linear
extra : subtree_revision : d4069e68d5b007ec258bf5821d6386a3ec63c490
This commit is contained in:
Alan Jeffrey 2017-05-17 16:20:42 -05:00
parent 6995e1d59d
commit 33d16ecbe0
27 changed files with 1129 additions and 16 deletions

46
servo/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"
@ -2883,6 +2906,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"
@ -2969,6 +3000,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"
@ -3396,6 +3435,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"
@ -3551,6 +3591,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"
@ -3587,12 +3628,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"
@ -3601,6 +3644,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",