servo: Merge #18533 - Rename Runnable to Task and other improvements (from servo:TASKS); r=SimonSapin
authorAnthony Ramine <n.oxyde@gmail.com>
Mon, 18 Sep 2017 14:42:50 -0500
changeset 666675 e368699e1e511972a1a95499d98663e06d4adf89
parent 666674 0ad97b0a82c4341736eb3aad6f155cc6d30aec5c
child 666676 206b2ca14b91789922bd3079c08ee4888974bcdc
push id80486
push userbmo:tlin@mozilla.com
push dateTue, 19 Sep 2017 03:52:30 +0000
reviewersSimonSapin
milestone57.0a1
servo: Merge #18533 - Rename Runnable to Task and other improvements (from servo:TASKS); r=SimonSapin Source-Repo: https://github.com/servo/servo Source-Revision: 23701f865988c420425f46386e4106f2f3fb95fc
servo/components/script/dom/bindings/refcounted.rs
servo/components/script/dom/bluetooth.rs
servo/components/script/dom/dedicatedworkerglobalscope.rs
servo/components/script/dom/document.rs
servo/components/script/dom/element.rs
servo/components/script/dom/event.rs
servo/components/script/dom/eventsource.rs
servo/components/script/dom/filereader.rs
servo/components/script/dom/globalscope.rs
servo/components/script/dom/htmldetailselement.rs
servo/components/script/dom/htmlformelement.rs
servo/components/script/dom/htmliframeelement.rs
servo/components/script/dom/htmlimageelement.rs
servo/components/script/dom/htmlmediaelement.rs
servo/components/script/dom/htmlscriptelement.rs
servo/components/script/dom/paintworkletglobalscope.rs
servo/components/script/dom/serviceworker.rs
servo/components/script/dom/serviceworkercontainer.rs
servo/components/script/dom/storage.rs
servo/components/script/dom/vrdisplay.rs
servo/components/script/dom/websocket.rs
servo/components/script/dom/window.rs
servo/components/script/dom/worker.rs
servo/components/script/dom/workerglobalscope.rs
servo/components/script/dom/worklet.rs
servo/components/script/dom/workletglobalscope.rs
servo/components/script/dom/xmlhttprequest.rs
servo/components/script/fetch.rs
servo/components/script/layout_image.rs
servo/components/script/lib.rs
servo/components/script/network_listener.rs
servo/components/script/script_runtime.rs
servo/components/script/script_thread.rs
servo/components/script/serviceworkerjob.rs
servo/components/script/stylesheet_loader.rs
servo/components/script/task.rs
servo/components/script/task_source/dom_manipulation.rs
servo/components/script/task_source/file_reading.rs
servo/components/script/task_source/mod.rs
servo/components/script/task_source/networking.rs
servo/components/script/task_source/performance_timeline.rs
servo/components/script/task_source/user_interaction.rs
servo/components/script_traits/lib.rs
--- a/servo/components/script/dom/bindings/refcounted.rs
+++ b/servo/components/script/dom/bindings/refcounted.rs
@@ -27,26 +27,25 @@ use dom::bindings::conversions::ToJSValC
 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;
 use std::hash::Hash;
 use std::marker::PhantomData;
 use std::os;
 use std::rc::Rc;
 use std::sync::{Arc, Weak};
+use task::Task;
 
 
 #[allow(missing_docs)]  // FIXME
 mod dummy {  // Attributes don’t apply through the macro.
     use std::cell::RefCell;
     use std::rc::Rc;
     use super::LiveDOMReferences;
     thread_local!(pub static LIVE_REFERENCES: Rc<RefCell<Option<LiveDOMReferences>>> =
@@ -116,50 +115,43 @@ impl TrustedPromise {
                     promise
                 }
                 Vacant(_) => unreachable!(),
             };
             promise
         })
     }
 
-    /// A runnable which will reject the promise.
+    /// A task 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) {
-                debug!("Rejecting promise.");
-                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)
+    pub fn reject_task(self, error: Error) -> impl Send + Task {
+        let this = self;
+        task!(reject_promise: move || {
+            debug!("Rejecting promise.");
+            let this = this.root();
+            let cx = this.global().get_cx();
+            let _ac = JSAutoCompartment::new(cx, this.reflector().get_jsobject().get());
+            this.reject_error(cx, error);
+        })
     }
 
-    /// A runnable which will resolve the promise.
+    /// A task which will resolve the promise.
     #[allow(unrooted_must_root)]
-    pub fn resolve_runnable<T>(self, value: T) -> impl Runnable + Send where
-        T: ToJSValConvertible + Send
+    pub fn resolve_task<T>(self, value: T) -> impl Send + Task
+    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) {
-                debug!("Resolving promise.");
-                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)
+        let this = self;
+        task!(resolve_promise: move || {
+            debug!("Resolving promise.");
+            let this = this.root();
+            let cx = this.global().get_cx();
+            let _ac = JSAutoCompartment::new(cx, this.reflector().get_jsobject().get());
+            this.resolve_native(cx, &value);
+        })
     }
 }
 
 /// A safe wrapper around a raw pointer to a DOM object that can be
 /// shared among threads for use in asynchronous operations. The underlying
 /// DOM object is guaranteed to live at least as long as the last outstanding
 /// `Trusted<T>` instance.
 #[allow_unrooted_interior]
--- a/servo/components/script/dom/bluetooth.rs
+++ b/servo/components/script/dom/bluetooth.rs
@@ -30,22 +30,22 @@ use dom::globalscope::GlobalScope;
 use dom::permissions::{get_descriptor_permission_state, PermissionAlgorithm};
 use dom::promise::Promise;
 use dom_struct::dom_struct;
 use ipc_channel::ipc::{self, IpcSender};
 use ipc_channel::router::ROUTER;
 use js::conversions::ConversionResult;
 use js::jsapi::{JSAutoCompartment, JSContext, JSObject};
 use js::jsval::{ObjectValue, UndefinedValue};
-use script_thread::Runnable;
 use std::cell::Ref;
 use std::collections::HashMap;
 use std::rc::Rc;
 use std::str::FromStr;
 use std::sync::{Arc, Mutex};
+use task::Task;
 
 const KEY_CONVERSION_ERROR: &'static str = "This `manufacturerData` key can not be parsed as unsigned short:";
 const FILTER_EMPTY_ERROR: &'static str = "'filters' member, if present, must be nonempty to find any devices.";
 const FILTER_ERROR: &'static str = "A filter must restrict the devices in some way.";
 const MANUFACTURER_DATA_ERROR: &'static str = "'manufacturerData', if present, must be non-empty to filter devices.";
 const MASK_LENGTH_ERROR: &'static str = "`mask`, if present, must have the same length as `dataPrefix`.";
 // 248 is the maximum number of UTF-8 code units in a Bluetooth Device Name.
 const MAX_DEVICE_NAME_LENGTH: usize = 248;
@@ -219,35 +219,38 @@ pub fn response_async<T: AsyncBluetoothL
         receiver: &T) -> IpcSender<BluetoothResponseResult> {
     let (action_sender, action_receiver) = ipc::channel().unwrap();
     let task_source = receiver.global().networking_task_source();
     let context = Arc::new(Mutex::new(BluetoothContext {
         promise: Some(TrustedPromise::new(promise.clone())),
         receiver: Trusted::new(receiver),
     }));
     ROUTER.add_route(action_receiver.to_opaque(), box move |message| {
-        struct ListenerRunnable<T: AsyncBluetoothListener + DomObject> {
+        struct ListenerTask<T: AsyncBluetoothListener + DomObject> {
             context: Arc<Mutex<BluetoothContext<T>>>,
             action: BluetoothResponseResult,
         }
 
-        impl<T: AsyncBluetoothListener + DomObject> Runnable for ListenerRunnable<T> {
-            fn handler(self: Box<Self>) {
+        impl<T> Task for ListenerTask<T>
+        where
+            T: AsyncBluetoothListener + DomObject,
+        {
+            fn run(self: Box<Self>) {
                 let this = *self;
                 let mut context = this.context.lock().unwrap();
                 context.response(this.action);
             }
         }
 
-        let runnable = box ListenerRunnable {
+        let task = box ListenerTask {
             context: context.clone(),
             action: message.to().unwrap(),
         };
 
-        let result = task_source.queue_wrapperless(runnable);
+        let result = task_source.queue_unconditionally(task);
         if let Err(err) = result {
             warn!("failed to deliver network data: {:?}", err);
         }
     });
     action_sender
 }
 
 #[allow(unrooted_must_root)]
--- a/servo/components/script/dom/dedicatedworkerglobalscope.rs
+++ b/servo/components/script/dom/dedicatedworkerglobalscope.rs
@@ -10,25 +10,28 @@ use dom::bindings::cell::DOMRefCell;
 use dom::bindings::codegen::Bindings::DedicatedWorkerGlobalScopeBinding;
 use dom::bindings::codegen::Bindings::DedicatedWorkerGlobalScopeBinding::DedicatedWorkerGlobalScopeMethods;
 use dom::bindings::error::{ErrorInfo, ErrorResult};
 use dom::bindings::inheritance::Castable;
 use dom::bindings::js::{Root, RootCollection};
 use dom::bindings::reflector::DomObject;
 use dom::bindings::str::DOMString;
 use dom::bindings::structuredclone::StructuredCloneData;
+use dom::errorevent::ErrorEvent;
+use dom::event::{Event, EventBubbles, EventCancelable, EventStatus};
+use dom::eventtarget::EventTarget;
 use dom::globalscope::GlobalScope;
 use dom::messageevent::MessageEvent;
-use dom::worker::{TrustedWorkerAddress, WorkerErrorHandler, WorkerMessageHandler};
+use dom::worker::{TrustedWorkerAddress, Worker};
 use dom::workerglobalscope::WorkerGlobalScope;
 use dom_struct::dom_struct;
 use ipc_channel::ipc::{self, IpcReceiver, IpcSender};
 use ipc_channel::router::ROUTER;
 use js::jsapi::{HandleValue, JS_SetInterruptCallback};
-use js::jsapi::{JSAutoCompartment, JSContext};
+use js::jsapi::{JSAutoCompartment, JSContext, NullHandleValue};
 use js::jsval::UndefinedValue;
 use js::rust::Runtime;
 use msg::constellation_msg::TopLevelBrowsingContextId;
 use net_traits::{IpcSend, load_whole_resource};
 use net_traits::request::{CredentialsMode, Destination, RequestInit, Type as RequestType};
 use script_runtime::{CommonScriptMsg, ScriptChan, ScriptPort, StackRootTLS, new_rt_and_cx};
 use script_runtime::ScriptThreadEventCategory::WorkerEvent;
 use script_traits::{TimerEvent, TimerSource, WorkerGlobalScopeInit, WorkerScriptLoadOrigin};
@@ -185,17 +188,17 @@ impl DedicatedWorkerGlobalScope {
                 origin,
                 .. RequestInit::default()
             };
 
             let (metadata, bytes) = match load_whole_resource(request,
                                                               &init.resource_threads.sender()) {
                 Err(_) => {
                     println!("error loading script {}", serialized_worker_url);
-                    parent_sender.send(CommonScriptMsg::RunnableMsg(WorkerEvent,
+                    parent_sender.send(CommonScriptMsg::Task(WorkerEvent,
                         box SimpleWorkerErrorHandler::new(worker))).unwrap();
                     return;
                 }
                 Ok((metadata, bytes)) => (metadata, bytes)
             };
             let url = metadata.final_url;
             let source = String::from_utf8_lossy(&bytes);
 
@@ -343,23 +346,46 @@ impl DedicatedWorkerGlobalScope {
             }
             MixedMessage::FromWorker((linked_worker, msg)) => {
                 let _ar = AutoWorkerReset::new(self, linked_worker);
                 self.handle_script_event(msg);
             }
         }
     }
 
+    // https://html.spec.whatwg.org/multipage/#runtime-script-errors-2
+    #[allow(unsafe_code)]
     pub fn forward_error_to_worker_object(&self, error_info: ErrorInfo) {
         let worker = self.worker.borrow().as_ref().unwrap().clone();
+        let task = box task!(forward_error_to_worker_object: move || {
+            let worker = worker.root();
+            let global = worker.global();
+
+            // Step 1.
+            let event = ErrorEvent::new(
+                &global,
+                atom!("error"),
+                EventBubbles::DoesNotBubble,
+                EventCancelable::Cancelable,
+                error_info.message.as_str().into(),
+                error_info.filename.as_str().into(),
+                error_info.lineno,
+                error_info.column,
+                unsafe { NullHandleValue },
+            );
+            let event_status =
+                event.upcast::<Event>().fire(worker.upcast::<EventTarget>());
+
+            // Step 2.
+            if event_status == EventStatus::NotCanceled {
+                global.report_an_error(error_info, unsafe { NullHandleValue });
+            }
+        });
         // TODO: Should use the DOM manipulation task source.
-        self.parent_sender
-            .send(CommonScriptMsg::RunnableMsg(WorkerEvent,
-                                               box WorkerErrorHandler::new(worker, error_info)))
-            .unwrap();
+        self.parent_sender.send(CommonScriptMsg::Task(WorkerEvent, task)).unwrap();
     }
 }
 
 #[allow(unsafe_code)]
 unsafe extern "C" fn interrupt_callback(cx: *mut JSContext) -> bool {
     let worker =
         Root::downcast::<WorkerGlobalScope>(GlobalScope::from_context(cx))
             .expect("global is not a worker scope");
@@ -370,20 +396,20 @@ unsafe extern "C" fn interrupt_callback(
 }
 
 impl DedicatedWorkerGlobalScopeMethods for DedicatedWorkerGlobalScope {
     #[allow(unsafe_code)]
     // https://html.spec.whatwg.org/multipage/#dom-dedicatedworkerglobalscope-postmessage
     unsafe fn PostMessage(&self, cx: *mut JSContext, message: HandleValue) -> ErrorResult {
         let data = StructuredCloneData::write(cx, message)?;
         let worker = self.worker.borrow().as_ref().unwrap().clone();
-        self.parent_sender
-            .send(CommonScriptMsg::RunnableMsg(WorkerEvent,
-                                               box WorkerMessageHandler::new(worker, data)))
-            .unwrap();
+        let task = box task!(post_worker_message: move || {
+            Worker::handle_message(worker, data);
+        });
+        self.parent_sender.send(CommonScriptMsg::Task(WorkerEvent, task)).unwrap();
         Ok(())
     }
 
     // https://html.spec.whatwg.org/multipage/#dom-dedicatedworkerglobalscope-close
     fn Close(&self) {
         // Step 2
         self.upcast::<WorkerGlobalScope>().close();
     }
--- a/servo/components/script/dom/document.rs
+++ b/servo/components/script/dom/document.rs
@@ -108,17 +108,17 @@ use net_traits::{FetchResponseMsg, IpcSe
 use net_traits::CookieSource::NonHTTP;
 use net_traits::CoreResourceMsg::{GetCookiesForUrl, SetCookiesForUrl};
 use net_traits::pub_domains::is_pub_domain;
 use net_traits::request::RequestInit;
 use net_traits::response::HttpsState;
 use num_traits::ToPrimitive;
 use script_layout_interface::message::{Msg, ReflowQueryType};
 use script_runtime::{CommonScriptMsg, ScriptThreadEventCategory};
-use script_thread::{MainThreadScriptMsg, Runnable, ScriptThread};
+use script_thread::{MainThreadScriptMsg, ScriptThread};
 use script_traits::{AnimationState, CompositorEvent, DocumentActivity};
 use script_traits::{MouseButton, MouseEventType, MozBrowserEvent};
 use script_traits::{MsDuration, ScriptMsg, TouchpadPressurePhase};
 use script_traits::{TouchEventType, TouchId};
 use script_traits::UntrustedNodeAddress;
 use servo_arc::Arc;
 use servo_atoms::Atom;
 use servo_config::prefs::PREFS;
@@ -1709,18 +1709,67 @@ impl Document {
         }
 
         assert!(!self.loader.borrow().events_inhibited());
         self.loader.borrow_mut().inhibit_events();
 
         // The rest will ever run only once per document.
         // Step 7.
         debug!("Document loads are complete.");
-        let handler = box DocumentProgressHandler::new(Trusted::new(self));
-        self.window.dom_manipulation_task_source().queue(handler, self.window.upcast()).unwrap();
+        let document = Trusted::new(self);
+        self.window.dom_manipulation_task_source().queue(
+            box task!(fire_load_event: move || {
+                let document = document.root();
+                let window = document.window();
+                if !window.is_alive() {
+                    return;
+                }
+
+                // Step 7.1.
+                document.set_ready_state(DocumentReadyState::Complete);
+
+                // Step 7.2.
+                if document.browsing_context().is_none() {
+                    return;
+                }
+                let event = Event::new(
+                    window.upcast(),
+                    atom!("load"),
+                    EventBubbles::DoesNotBubble,
+                    EventCancelable::NotCancelable,
+                );
+                event.set_trusted(true);
+
+                // http://w3c.github.io/navigation-timing/#widl-PerformanceNavigationTiming-loadEventStart
+                update_with_current_time_ms(&document.load_event_start);
+
+                debug!("About to dispatch load for {:?}", document.url());
+                // FIXME(nox): Why are errors silenced here?
+                let _ = window.upcast::<EventTarget>().dispatch_event_with_target(
+                    document.upcast(),
+                    &event,
+                );
+
+                // http://w3c.github.io/navigation-timing/#widl-PerformanceNavigationTiming-loadEventEnd
+                update_with_current_time_ms(&document.load_event_end);
+
+                window.reflow(
+                    ReflowGoal::ForDisplay,
+                    ReflowQueryType::NoQuery,
+                    ReflowReason::DocumentLoaded,
+                );
+
+                document.notify_constellation_load();
+
+                if let Some(fragment) = document.url().fragment() {
+                    document.check_and_scroll_fragment(fragment);
+                }
+            }),
+            self.window.upcast(),
+        ).unwrap();
 
         // Step 8.
         // TODO: pageshow event.
 
         // Step 9.
         // TODO: pending application cache download process tasks.
 
         // Step 10.
@@ -2604,17 +2653,17 @@ impl Document {
             let event = ScriptMsg::SetFullscreenState(true);
             self.send_to_constellation(event);
         }
 
         // Step 7
         let trusted_pending = Trusted::new(pending);
         let trusted_promise = TrustedPromise::new(promise.clone());
         let handler = ElementPerformFullscreenEnter::new(trusted_pending, trusted_promise, error);
-        let script_msg = CommonScriptMsg::RunnableMsg(ScriptThreadEventCategory::EnterFullscreen, handler);
+        let script_msg = CommonScriptMsg::Task(ScriptThreadEventCategory::EnterFullscreen, handler);
         let msg = MainThreadScriptMsg::Common(script_msg);
         window.main_thread_script_chan().send(msg).unwrap();
 
         promise
     }
 
     // https://fullscreen.spec.whatwg.org/#exit-fullscreen
     #[allow(unrooted_must_root)]
@@ -2636,17 +2685,17 @@ impl Document {
         // Step 8
         let event = ScriptMsg::SetFullscreenState(false);
         self.send_to_constellation(event);
 
         // Step 9
         let trusted_element = Trusted::new(element.r());
         let trusted_promise = TrustedPromise::new(promise.clone());
         let handler = ElementPerformFullscreenExit::new(trusted_element, trusted_promise);
-        let script_msg = CommonScriptMsg::RunnableMsg(ScriptThreadEventCategory::ExitFullscreen, handler);
+        let script_msg = CommonScriptMsg::Task(ScriptThreadEventCategory::ExitFullscreen, handler);
         let msg = MainThreadScriptMsg::Common(script_msg);
         window.main_thread_script_chan().send(msg).unwrap();
 
         promise
     }
 
     pub fn set_fullscreen_element(&self, element: Option<&Element>) {
         self.fullscreen_element.set(element);
@@ -3965,76 +4014,16 @@ pub fn determine_policy_for_token(token:
         "strict-origin-when-cross-origin" => Some(ReferrerPolicy::StrictOriginWhenCrossOrigin),
         "origin-when-cross-origin" => Some(ReferrerPolicy::OriginWhenCrossOrigin),
         "always" | "unsafe-url" => Some(ReferrerPolicy::UnsafeUrl),
         "" => Some(ReferrerPolicy::NoReferrer),
         _ => None,
     }
 }
 
-pub struct DocumentProgressHandler {
-    addr: Trusted<Document>
-}
-
-impl DocumentProgressHandler {
-     pub fn new(addr: Trusted<Document>) -> DocumentProgressHandler {
-        DocumentProgressHandler {
-            addr: addr
-        }
-    }
-
-    fn set_ready_state_complete(&self) {
-        let document = self.addr.root();
-        document.set_ready_state(DocumentReadyState::Complete);
-    }
-
-    fn dispatch_load(&self) {
-        let document = self.addr.root();
-        if document.browsing_context().is_none() {
-            return;
-        }
-        let window = document.window();
-        let event = Event::new(window.upcast(),
-                               atom!("load"),
-                               EventBubbles::DoesNotBubble,
-                               EventCancelable::NotCancelable);
-        let wintarget = window.upcast::<EventTarget>();
-        event.set_trusted(true);
-
-        // http://w3c.github.io/navigation-timing/#widl-PerformanceNavigationTiming-loadEventStart
-        update_with_current_time_ms(&document.load_event_start);
-
-        debug!("About to dispatch load for {:?}", document.url());
-        let _ = wintarget.dispatch_event_with_target(document.upcast(), &event);
-
-        // http://w3c.github.io/navigation-timing/#widl-PerformanceNavigationTiming-loadEventEnd
-        update_with_current_time_ms(&document.load_event_end);
-
-        window.reflow(ReflowGoal::ForDisplay,
-                      ReflowQueryType::NoQuery,
-                      ReflowReason::DocumentLoaded);
-
-        document.notify_constellation_load();
-    }
-}
-
-impl Runnable for DocumentProgressHandler {
-    fn handler(self: Box<DocumentProgressHandler>) {
-        let document = self.addr.root();
-        let window = document.window();
-        if window.is_alive() {
-            self.set_ready_state_complete();
-            self.dispatch_load();
-            if let Some(fragment) = document.url().fragment() {
-                document.check_and_scroll_fragment(fragment);
-            }
-        }
-    }
-}
-
 /// Specifies the type of focus event that is sent to a pipeline
 #[derive(Clone, Copy, PartialEq)]
 pub enum FocusType {
     Element,    // The first focus message - focus the element itself
     Parent,     // Focusing a parent element (an iframe)
 }
 
 /// Focus events
--- a/servo/components/script/dom/element.rs
+++ b/servo/components/script/dom/element.rs
@@ -82,17 +82,17 @@ use html5ever::serialize;
 use html5ever::serialize::SerializeOpts;
 use html5ever::serialize::TraversalScope;
 use html5ever::serialize::TraversalScope::{ChildrenOnly, IncludeNode};
 use js::jsapi::{HandleValue, Heap, JSAutoCompartment};
 use js::jsval::JSVal;
 use net_traits::request::CorsSettings;
 use ref_filter_map::ref_filter_map;
 use script_layout_interface::message::ReflowQueryType;
-use script_thread::{Runnable, ScriptThread};
+use script_thread::ScriptThread;
 use selectors::attr::{AttrSelectorOperation, NamespaceConstraint, CaseSensitivity};
 use selectors::matching::{ElementSelectorFlags, LocalMatchingContext, MatchingContext, MatchingMode};
 use selectors::matching::{HAS_EDGE_CHILD_SELECTOR, HAS_SLOW_SELECTOR, HAS_SLOW_SELECTOR_LATER_SIBLINGS};
 use selectors::matching::{RelevantLinkStatus, matches_selector_list};
 use selectors::sink::Push;
 use servo_arc::Arc;
 use servo_atoms::Atom;
 use std::ascii::AsciiExt;
@@ -114,16 +114,17 @@ use style::properties::longhands::{self,
 use style::rule_tree::CascadeLevel;
 use style::selector_parser::{NonTSPseudoClass, PseudoElement, RestyleDamage, SelectorImpl, SelectorParser};
 use style::selector_parser::extended_filtering;
 use style::shared_lock::{SharedRwLock, Locked};
 use style::thread_state;
 use style::values::{CSSFloat, Either};
 use style::values::{specified, computed};
 use stylesheet_loader::StylesheetOwner;
+use task::Task;
 
 // TODO: Update focus state when the top-level browsing context gains or loses system focus,
 // and when the element enters or leaves a browsing context container.
 // https://html.spec.whatwg.org/multipage/#selector-focus
 
 #[dom_struct]
 pub struct Element {
     node: Node,
@@ -3041,19 +3042,19 @@ impl ElementPerformFullscreenEnter {
         box ElementPerformFullscreenEnter {
             element: element,
             promise: promise,
             error: error,
         }
     }
 }
 
-impl Runnable for ElementPerformFullscreenEnter {
+impl Task for ElementPerformFullscreenEnter {
     #[allow(unrooted_must_root)]
-    fn handler(self: Box<ElementPerformFullscreenEnter>) {
+    fn run(self: Box<Self>) {
         let element = self.element.root();
         let document = document_from_node(element.r());
 
         // Step 7.1
         if self.error || !element.fullscreen_element_ready_check() {
             // JSAutoCompartment needs to be manually made.
             // Otherwise, Servo will crash.
             let promise = self.promise.root();
@@ -3094,19 +3095,19 @@ impl ElementPerformFullscreenExit {
     pub fn new(element: Trusted<Element>, promise: TrustedPromise) -> Box<ElementPerformFullscreenExit> {
         box ElementPerformFullscreenExit {
             element: element,
             promise: promise,
         }
     }
 }
 
-impl Runnable for ElementPerformFullscreenExit {
+impl Task for ElementPerformFullscreenExit {
     #[allow(unrooted_must_root)]
-    fn handler(self: Box<ElementPerformFullscreenExit>) {
+    fn run(self: Box<Self>) {
         let element = self.element.root();
         let document = document_from_node(element.r());
         // TODO Step 9.1-5
         // Step 9.6
         element.set_fullscreen_state(false);
 
         document.window().reflow(ReflowGoal::ForDisplay,
                                  ReflowQueryType::NoQuery,
--- a/servo/components/script/dom/event.rs
+++ b/servo/components/script/dom/event.rs
@@ -15,20 +15,20 @@ use dom::bindings::reflector::{DomObject
 use dom::bindings::str::DOMString;
 use dom::document::Document;
 use dom::eventtarget::{CompiledEventListener, EventTarget, ListenerPhase};
 use dom::globalscope::GlobalScope;
 use dom::node::Node;
 use dom::virtualmethods::vtable_for;
 use dom::window::Window;
 use dom_struct::dom_struct;
-use script_thread::Runnable;
 use servo_atoms::Atom;
 use std::cell::Cell;
 use std::default::Default;
+use task::Task;
 use time;
 
 #[dom_struct]
 pub struct Event {
     reflector_: Reflector,
     current_target: MutNullableJS<EventTarget>,
     target: MutNullableJS<EventTarget>,
     type_: DOMRefCell<Atom>,
@@ -376,40 +376,40 @@ pub enum EventDefault {
 
 #[derive(PartialEq)]
 pub enum EventStatus {
     Canceled,
     NotCanceled
 }
 
 // https://dom.spec.whatwg.org/#concept-event-fire
-pub struct EventRunnable {
+pub struct EventTask {
     pub target: Trusted<EventTarget>,
     pub name: Atom,
     pub bubbles: EventBubbles,
     pub cancelable: EventCancelable,
 }
 
-impl Runnable for EventRunnable {
-    fn handler(self: Box<EventRunnable>) {
+impl Task for EventTask {
+    fn run(self: Box<Self>) {
         let target = self.target.root();
         let bubbles = self.bubbles;
         let cancelable = self.cancelable;
         target.fire_event_with_params(self.name, bubbles, cancelable);
     }
 }
 
 // https://html.spec.whatwg.org/multipage/#fire-a-simple-event
-pub struct SimpleEventRunnable {
+pub struct SimpleEventTask {
     pub target: Trusted<EventTarget>,
     pub name: Atom,
 }
 
-impl Runnable for SimpleEventRunnable {
-    fn handler(self: Box<SimpleEventRunnable>) {
+impl Task for SimpleEventTask {
+    fn run(self: Box<Self>) {
         let target = self.target.root();
         target.fire_event(self.name);
     }
 }
 
 // See dispatch_event.
 // https://dom.spec.whatwg.org/#concept-event-dispatch
 fn dispatch_to_listeners(event: &Event, target: &EventTarget, event_path: &[&EventTarget]) {
--- a/servo/components/script/dom/eventsource.rs
+++ b/servo/components/script/dom/eventsource.rs
@@ -22,17 +22,16 @@ use ipc_channel::router::ROUTER;
 use js::conversions::ToJSValConvertible;
 use js::jsapi::JSAutoCompartment;
 use js::jsval::UndefinedValue;
 use mime::{Mime, TopLevel, SubLevel};
 use net_traits::{CoreResourceMsg, FetchMetadata, FetchResponseMsg, FetchResponseListener, NetworkError};
 use net_traits::request::{CacheMode, CorsSettings, CredentialsMode};
 use net_traits::request::{RequestInit, RequestMode};
 use network_listener::{NetworkListener, PreInvoke};
-use script_thread::Runnable;
 use servo_atoms::Atom;
 use servo_url::ServoUrl;
 use std::cell::Cell;
 use std::mem;
 use std::str::{Chars, FromStr};
 use std::sync::{Arc, Mutex};
 use task_source::TaskSource;
 use timers::OneshotTimerCallback;
@@ -86,52 +85,103 @@ struct EventSourceContext {
     origin: String,
 
     event_type: String,
     data: String,
     last_event_id: String,
 }
 
 impl EventSourceContext {
+    /// https://html.spec.whatwg.org/multipage/#announce-the-connection
     fn announce_the_connection(&self) {
         let event_source = self.event_source.root();
         if self.gen_id != event_source.generation_id.get() {
             return;
         }
-        let runnable = box AnnounceConnectionRunnable {
-            event_source: self.event_source.clone()
-        };
-        let _ = event_source.global().networking_task_source().queue(runnable, &*event_source.global());
+        let global = event_source.global();
+        let event_source = self.event_source.clone();
+        // FIXME(nox): Why are errors silenced here?
+        let _ = global.networking_task_source().queue(
+            box task!(announce_the_event_source_connection: move || {
+                let event_source = event_source.root();
+                if event_source.ready_state.get() != ReadyState::Closed {
+                    event_source.ready_state.set(ReadyState::Open);
+                    event_source.upcast::<EventTarget>().fire_event(atom!("open"));
+                }
+            }),
+            &global,
+        );
     }
 
+    /// https://html.spec.whatwg.org/multipage/#fail-the-connection
     fn fail_the_connection(&self) {
         let event_source = self.event_source.root();
         if self.gen_id != event_source.generation_id.get() {
             return;
         }
-        let runnable = box FailConnectionRunnable {
-            event_source: self.event_source.clone()
-        };
-        let _ = event_source.global().networking_task_source().queue(runnable, &*event_source.global());
+        let global = event_source.global();
+        let event_source = self.event_source.clone();
+        // FIXME(nox): Why are errors silenced here?
+        let _ = global.networking_task_source().queue(
+            box task!(fail_the_event_source_connection: move || {
+                let event_source = event_source.root();
+                if event_source.ready_state.get() != ReadyState::Closed {
+                    event_source.ready_state.set(ReadyState::Closed);
+                    event_source.upcast::<EventTarget>().fire_event(atom!("error"));
+                }
+            }),
+            &global,
+        );
     }
 
     // https://html.spec.whatwg.org/multipage/#reestablish-the-connection
     fn reestablish_the_connection(&self) {
         let event_source = self.event_source.root();
 
         if self.gen_id != event_source.generation_id.get() {
             return;
         }
 
-        // Step 1
-        let runnable = box ReestablishConnectionRunnable {
-            event_source: self.event_source.clone(),
-            action_sender: self.action_sender.clone()
-        };
-        let _ = event_source.global().networking_task_source().queue(runnable, &*event_source.global());
+        let trusted_event_source = self.event_source.clone();
+        let action_sender = self.action_sender.clone();
+        let global = event_source.global();
+        // FIXME(nox): Why are errors silenced here?
+        let _ = global.networking_task_source().queue(
+            box task!(reestablish_the_event_source_onnection: move || {
+                let event_source = trusted_event_source.root();
+
+                // Step 1.1.
+                if event_source.ready_state.get() == ReadyState::Closed {
+                    return;
+                }
+
+                // Step 1.2.
+                event_source.ready_state.set(ReadyState::Connecting);
+
+                // Step 1.3.
+                event_source.upcast::<EventTarget>().fire_event(atom!("error"));
+
+                // Step 2.
+                let duration = Length::new(event_source.reconnection_time.get());
+
+                // Step 3.
+                // TODO: Optionally wait some more.
+
+                // Steps 4-5.
+                let callback = OneshotTimerCallback::EventSourceTimeout(
+                    EventSourceTimeoutCallback {
+                        event_source: trusted_event_source,
+                        action_sender,
+                    }
+                );
+                // FIXME(nox): Why are errors silenced here?
+                let _ = event_source.global().schedule_callback(callback, duration);
+            }),
+            &global,
+        );
     }
 
     // https://html.spec.whatwg.org/multipage/#processField
     fn process_field(&mut self) {
         match &*self.field {
             "event" => mem::swap(&mut self.event_type, &mut self.value),
             "data" => {
                 self.data.push_str(&self.value);
@@ -180,22 +230,31 @@ impl EventSourceContext {
             unsafe { self.data.to_jsval(event_source.global().get_cx(), data.handle_mut()) };
             MessageEvent::new(&*event_source.global(), type_, false, false, data.handle(),
                               DOMString::from(self.origin.clone()),
                               event_source.last_event_id.borrow().clone())
         };
         // Step 7
         self.event_type.clear();
         self.data.clear();
-        // Step 8
-        let runnable = box DispatchEventRunnable {
-            event_source: self.event_source.clone(),
-            event: Trusted::new(&event)
-        };
-        let _ = event_source.global().networking_task_source().queue(runnable, &*event_source.global());
+
+        // Step 8.
+        let global = event_source.global();
+        let event_source = self.event_source.clone();
+        let event = Trusted::new(&*event);
+        // FIXME(nox): Why are errors silenced here?
+        let _ = global.networking_task_source().queue(
+            box task!(dispatch_the_event_source_event: move || {
+                let event_source = event_source.root();
+                if event_source.ready_state.get() != ReadyState::Closed {
+                    event.root().upcast::<Event>().fire(&event_source.upcast());
+                }
+            }),
+            &global,
+        );
     }
 
     // https://html.spec.whatwg.org/multipage/#event-stream-interpretation
     fn parse(&mut self, stream: Chars) {
         let mut stream = stream.peekable();
 
         while let Some(ch) = stream.next() {
             match (ch, &self.parser_state) {
@@ -420,17 +479,17 @@ impl EventSource {
 
             event_type: String::new(),
             data: String::new(),
             last_event_id: String::new(),
         };
         let listener = NetworkListener {
             context: Arc::new(Mutex::new(context)),
             task_source: global.networking_task_source(),
-            wrapper: Some(global.get_runnable_wrapper())
+            canceller: Some(global.task_canceller())
         };
         ROUTER.add_route(action_receiver.to_opaque(), box move |message| {
             listener.notify_fetch(message.to().unwrap());
         });
         global.core_resource_thread().send(CoreResourceMsg::Fetch(request, action_sender)).unwrap();
         // Step 13
         Ok(ev)
     }
@@ -464,75 +523,16 @@ impl EventSourceMethods for EventSource 
     // https://html.spec.whatwg.org/multipage/#dom-eventsource-close
     fn Close(&self) {
         let GenerationId(prev_id) = self.generation_id.get();
         self.generation_id.set(GenerationId(prev_id + 1));
         self.ready_state.set(ReadyState::Closed);
     }
 }
 
-pub struct AnnounceConnectionRunnable {
-    event_source: Trusted<EventSource>,
-}
-
-impl Runnable for AnnounceConnectionRunnable {
-    // https://html.spec.whatwg.org/multipage/#announce-the-connection
-    fn handler(self: Box<AnnounceConnectionRunnable>) {
-        let event_source = self.event_source.root();
-        if event_source.ready_state.get() != ReadyState::Closed {
-            event_source.ready_state.set(ReadyState::Open);
-            event_source.upcast::<EventTarget>().fire_event(atom!("open"));
-        }
-    }
-}
-
-pub struct FailConnectionRunnable {
-    event_source: Trusted<EventSource>,
-}
-
-impl Runnable for FailConnectionRunnable {
-    // https://html.spec.whatwg.org/multipage/#fail-the-connection
-    fn handler(self: Box<FailConnectionRunnable>) {
-        let event_source = self.event_source.root();
-        if event_source.ready_state.get() != ReadyState::Closed {
-            event_source.ready_state.set(ReadyState::Closed);
-            event_source.upcast::<EventTarget>().fire_event(atom!("error"));
-        }
-    }
-}
-
-pub struct ReestablishConnectionRunnable {
-    event_source: Trusted<EventSource>,
-    action_sender: ipc::IpcSender<FetchResponseMsg>,
-}
-
-impl Runnable for ReestablishConnectionRunnable {
-    // https://html.spec.whatwg.org/multipage/#reestablish-the-connection
-    fn handler(self: Box<ReestablishConnectionRunnable>) {
-        let event_source = self.event_source.root();
-        // Step 1.1
-        if event_source.ready_state.get() == ReadyState::Closed {
-            return;
-        }
-        // Step 1.2
-        event_source.ready_state.set(ReadyState::Connecting);
-        // Step 1.3
-        event_source.upcast::<EventTarget>().fire_event(atom!("error"));
-        // Step 2
-        let duration = Length::new(event_source.reconnection_time.get());
-        // TODO Step 3: Optionally wait some more
-        // Steps 4-5
-        let callback = OneshotTimerCallback::EventSourceTimeout(EventSourceTimeoutCallback {
-            event_source: self.event_source.clone(),
-            action_sender: self.action_sender.clone()
-        });
-        let _ = event_source.global().schedule_callback(callback, duration);
-    }
-}
-
 #[derive(HeapSizeOf, JSTraceable)]
 pub struct EventSourceTimeoutCallback {
     #[ignore_heap_size_of = "Because it is non-owning"]
     event_source: Trusted<EventSource>,
     #[ignore_heap_size_of = "Because it is non-owning"]
     action_sender: ipc::IpcSender<FetchResponseMsg>,
 }
 
@@ -550,24 +550,8 @@ impl EventSourceTimeoutCallback {
         // Step 5.3
         if !event_source.last_event_id.borrow().is_empty() {
             request.headers.set(LastEventId(String::from(event_source.last_event_id.borrow().clone())));
         }
         // Step 5.4
         global.core_resource_thread().send(CoreResourceMsg::Fetch(request, self.action_sender)).unwrap();
     }
 }
-
-pub struct DispatchEventRunnable {
-    event_source: Trusted<EventSource>,
-    event: Trusted<MessageEvent>,
-}
-
-impl Runnable for DispatchEventRunnable {
-    // https://html.spec.whatwg.org/multipage/#dispatchMessage
-    fn handler(self: Box<DispatchEventRunnable>) {
-        let event_source = self.event_source.root();
-        // Step 8
-        if event_source.ready_state.get() != ReadyState::Closed {
-            self.event.root().upcast::<Event>().fire(&event_source.upcast());
-        }
-    }
-}
--- a/servo/components/script/dom/filereader.rs
+++ b/servo/components/script/dom/filereader.rs
@@ -24,24 +24,24 @@ use encoding::all::UTF_8;
 use encoding::label::encoding_from_whatwg_label;
 use encoding::types::{DecoderTrap, EncodingRef};
 use hyper::mime::{Attr, Mime};
 use js::jsapi::Heap;
 use js::jsapi::JSAutoCompartment;
 use js::jsapi::JSContext;
 use js::jsval::{self, JSVal};
 use js::typedarray::{ArrayBuffer, CreateWith};
-use script_thread::RunnableWrapper;
 use servo_atoms::Atom;
 use std::cell::Cell;
 use std::ptr;
 use std::sync::Arc;
 use std::thread;
+use task::TaskCanceller;
 use task_source::TaskSource;
-use task_source::file_reading::{FileReadingTaskSource, FileReadingRunnable, FileReadingTask};
+use task_source::file_reading::{FileReadingTask, FileReadingTaskSource};
 
 #[derive(Clone, Copy, HeapSizeOf, JSTraceable, PartialEq)]
 pub enum FileReaderFunction {
     ReadAsText,
     ReadAsDataUrl,
     ReadAsArrayBuffer,
 }
 
@@ -378,40 +378,49 @@ impl FileReader {
         let type_ = blob.Type();
 
         let load_data = ReadMetaData::new(String::from(type_), label.map(String::from), function);
 
         let fr = Trusted::new(self);
         let gen_id = self.generation_id.get();
 
         let global = self.global();
-        let wrapper = global.get_runnable_wrapper();
+        let canceller = global.task_canceller();
         let task_source = global.file_reading_task_source();
 
         thread::Builder::new().name("file reader async operation".to_owned()).spawn(move || {
-            perform_annotated_read_operation(gen_id, load_data, blob_contents, fr, task_source, wrapper)
+            perform_annotated_read_operation(
+                gen_id,
+                load_data,
+                blob_contents,
+                fr,
+                task_source,
+                canceller,
+            )
         }).expect("Thread spawning failed");
 
         Ok(())
     }
 
     fn change_ready_state(&self, state: FileReaderReadyState) {
         self.ready_state.set(state);
     }
 }
 
 // https://w3c.github.io/FileAPI/#thread-read-operation
-fn perform_annotated_read_operation(gen_id: GenerationId,
-                                    data: ReadMetaData,
-                                    blob_contents: Arc<Vec<u8>>,
-                                    filereader: TrustedFileReader,
-                                    task_source: FileReadingTaskSource,
-                                    wrapper: RunnableWrapper) {
+fn perform_annotated_read_operation(
+    gen_id: GenerationId,
+    data: ReadMetaData,
+    blob_contents: Arc<Vec<u8>>,
+    filereader: TrustedFileReader,
+    task_source: FileReadingTaskSource,
+    canceller: TaskCanceller,
+) {
     // Step 4
-    let task = FileReadingRunnable::new(FileReadingTask::ProcessRead(filereader.clone(), gen_id));
-    task_source.queue_with_wrapper(task, &wrapper).unwrap();
+    let task = FileReadingTask::ProcessRead(filereader.clone(), gen_id);
+    task_source.queue_with_canceller(box task, &canceller).unwrap();
 
-    let task = FileReadingRunnable::new(FileReadingTask::ProcessReadData(filereader.clone(), gen_id));
-    task_source.queue_with_wrapper(task, &wrapper).unwrap();
+    let task = FileReadingTask::ProcessReadData(filereader.clone(), gen_id);
+    task_source.queue_with_canceller(box task, &canceller).unwrap();
 
-    let task = FileReadingRunnable::new(FileReadingTask::ProcessReadEOF(filereader, gen_id, data, blob_contents));
-    task_source.queue_with_wrapper(task, &wrapper).unwrap();
+    let task = FileReadingTask::ProcessReadEOF(filereader, gen_id, data, blob_contents);
+    task_source.queue_with_canceller(box task, &canceller).unwrap();
 }
--- a/servo/components/script/dom/globalscope.rs
+++ b/servo/components/script/dom/globalscope.rs
@@ -33,25 +33,26 @@ use js::jsapi::{JS_GetObjectRuntime, Mut
 use js::panic::maybe_resume_unwind;
 use js::rust::{CompileOptionsWrapper, Runtime, get_object_class};
 use libc;
 use microtask::{Microtask, MicrotaskQueue};
 use msg::constellation_msg::PipelineId;
 use net_traits::{CoreResourceThread, ResourceThreads, IpcSend};
 use profile_traits::{mem, time};
 use script_runtime::{CommonScriptMsg, ScriptChan, ScriptPort};
-use script_thread::{MainThreadScriptChan, RunnableWrapper, ScriptThread};
+use script_thread::{MainThreadScriptChan, ScriptThread};
 use script_traits::{MsDuration, ScriptToConstellationChan, TimerEvent};
 use script_traits::{TimerEventId, TimerSchedulerMsg, TimerSource};
 use servo_url::{MutableOrigin, ServoUrl};
 use std::cell::Cell;
 use std::collections::HashMap;
 use std::collections::hash_map::Entry;
 use std::ffi::CString;
 use std::rc::Rc;
+use task::TaskCanceller;
 use task_source::file_reading::FileReadingTaskSource;
 use task_source::networking::NetworkingTaskSource;
 use task_source::performance_timeline::PerformanceTimelineTaskSource;
 use time::{Timespec, get_time};
 use timers::{IsInterval, OneshotTimerCallback, OneshotTimerHandle};
 use timers::{OneshotTimers, TimerCallback};
 
 #[dom_struct]
@@ -307,40 +308,44 @@ impl GlobalScope {
         // Step 1.
         if self.in_error_reporting_mode.get() {
             return;
         }
 
         // Step 2.
         self.in_error_reporting_mode.set(true);
 
-        // Steps 3-12.
+        // Steps 3-6.
         // FIXME(#13195): muted errors.
-        let event = ErrorEvent::new(self,
-                                    atom!("error"),
-                                    EventBubbles::DoesNotBubble,
-                                    EventCancelable::Cancelable,
-                                    error_info.message.as_str().into(),
-                                    error_info.filename.as_str().into(),
-                                    error_info.lineno,
-                                    error_info.column,
-                                    value);
+        let event = ErrorEvent::new(
+            self,
+            atom!("error"),
+            EventBubbles::DoesNotBubble,
+            EventCancelable::Cancelable,
+            error_info.message.as_str().into(),
+            error_info.filename.as_str().into(),
+            error_info.lineno,
+            error_info.column,
+            value,
+        );
 
-        // Step 13.
+        // Step 7.
         let event_status = event.upcast::<Event>().fire(self.upcast::<EventTarget>());
 
-        // Step 15
+        // Step 8.
+        self.in_error_reporting_mode.set(false);
+
+        // Step 9.
         if event_status == EventStatus::NotCanceled {
+            // https://html.spec.whatwg.org/multipage/#runtime-script-errors-2
             if let Some(dedicated) = self.downcast::<DedicatedWorkerGlobalScope>() {
                 dedicated.forward_error_to_worker_object(error_info);
             }
         }
 
-        // Step 14
-        self.in_error_reporting_mode.set(false);
     }
 
     /// Get the `&ResourceThreads` for this global scope.
     pub fn resource_threads(&self) -> &ResourceThreads {
         &self.resource_threads
     }
 
     /// Get the `CoreResourceThread` for this global scope.
@@ -472,24 +477,24 @@ impl GlobalScope {
             return TimerSource::FromWindow(self.pipeline_id());
         }
         if self.is::<WorkerGlobalScope>() {
             return TimerSource::FromWorker;
         }
         unreachable!();
     }
 
-    /// Returns a wrapper for runnables to ensure they are cancelled if
-    /// the global scope is being destroyed.
-    pub fn get_runnable_wrapper(&self) -> RunnableWrapper {
+    /// Returns the task canceller of this global to ensure that everything is
+    /// properly cancelled when the global scope is destroyed.
+    pub fn task_canceller(&self) -> TaskCanceller {
         if let Some(window) = self.downcast::<Window>() {
-            return window.get_runnable_wrapper();
+            return window.task_canceller();
         }
         if let Some(worker) = self.downcast::<WorkerGlobalScope>() {
-            return worker.get_runnable_wrapper();
+            return worker.task_canceller();
         }
         unreachable!();
     }
 
     /// Perform a microtask checkpoint.
     pub fn perform_a_microtask_checkpoint(&self) {
         self.microtask_queue.checkpoint(|_| Some(Root::from_ref(self)));
     }
--- a/servo/components/script/dom/htmldetailselement.rs
+++ b/servo/components/script/dom/htmldetailselement.rs
@@ -11,17 +11,16 @@ use dom::bindings::refcounted::Trusted;
 use dom::document::Document;
 use dom::element::AttributeMutation;
 use dom::eventtarget::EventTarget;
 use dom::htmlelement::HTMLElement;
 use dom::node::{Node, window_from_node};
 use dom::virtualmethods::VirtualMethods;
 use dom_struct::dom_struct;
 use html5ever::{LocalName, Prefix};
-use script_thread::Runnable;
 use std::cell::Cell;
 use task_source::TaskSource;
 
 #[dom_struct]
 pub struct HTMLDetailsElement {
     htmlelement: HTMLElement,
     toggle_counter: Cell<u32>
 }
@@ -40,20 +39,16 @@ impl HTMLDetailsElement {
     #[allow(unrooted_must_root)]
     pub fn new(local_name: LocalName,
                prefix: Option<Prefix>,
                document: &Document) -> Root<HTMLDetailsElement> {
         Node::reflect_node(box HTMLDetailsElement::new_inherited(local_name, prefix, document),
                            document,
                            HTMLDetailsElementBinding::Wrap)
     }
-
-    pub fn check_toggle_count(&self, number: u32) -> bool {
-        number == self.toggle_counter.get()
-    }
 }
 
 impl HTMLDetailsElementMethods for HTMLDetailsElement {
     // https://html.spec.whatwg.org/multipage/#dom-details-open
     make_bool_getter!(Open, "open");
 
     // https://html.spec.whatwg.org/multipage/#dom-details-open
     make_bool_setter!(SetOpen, "open");
@@ -67,32 +62,22 @@ impl VirtualMethods for HTMLDetailsEleme
     fn attribute_mutated(&self, attr: &Attr, mutation: AttributeMutation) {
         self.super_type().unwrap().attribute_mutated(attr, mutation);
 
         if attr.local_name() == &local_name!("open") {
             let counter = self.toggle_counter.get() + 1;
             self.toggle_counter.set(counter);
 
             let window = window_from_node(self);
-            let task_source = window.dom_manipulation_task_source();
-            let details = Trusted::new(self);
-            let runnable = box DetailsNotificationRunnable {
-                element: details,
-                toggle_number: counter
-            };
-            let _ = task_source.queue(runnable, window.upcast());
+            let this = Trusted::new(self);
+            // FIXME(nox): Why are errors silenced here?
+            let _ = window.dom_manipulation_task_source().queue(
+                box task!(details_notification_task_steps: move || {
+                    let this = this.root();
+                    if counter == this.toggle_counter.get() {
+                        this.upcast::<EventTarget>().fire_event(atom!("toggle"));
+                    }
+                }),
+                window.upcast(),
+            );
         }
     }
 }
-
-pub struct DetailsNotificationRunnable {
-    element: Trusted<HTMLDetailsElement>,
-    toggle_number: u32
-}
-
-impl Runnable for DetailsNotificationRunnable {
-    fn handler(self: Box<DetailsNotificationRunnable>) {
-        let target = self.element.root();
-        if target.check_toggle_count(self.toggle_number) {
-            target.upcast::<EventTarget>().fire_event(atom!("toggle"));
-        }
-    }
-}
--- a/servo/components/script/dom/htmlformelement.rs
+++ b/servo/components/script/dom/htmlformelement.rs
@@ -43,23 +43,21 @@ use dom::validitystate::ValidationFlags;
 use dom::virtualmethods::VirtualMethods;
 use dom_struct::dom_struct;
 use encoding::EncodingRef;
 use encoding::all::UTF_8;
 use encoding::label::encoding_from_whatwg_label;
 use html5ever::{LocalName, Prefix};
 use hyper::header::{Charset, ContentDisposition, ContentType, DispositionParam, DispositionType};
 use hyper::method::Method;
-use msg::constellation_msg::PipelineId;
-use script_thread::{MainThreadScriptMsg, Runnable};
+use script_thread::MainThreadScriptMsg;
 use script_traits::LoadData;
 use servo_rand::random;
 use std::borrow::ToOwned;
 use std::cell::Cell;
-use std::sync::mpsc::Sender;
 use style::attr::AttrValue;
 use style::str::split_html_space_chars;
 use task_source::TaskSource;
 
 #[derive(Clone, Copy, HeapSizeOf, JSTraceable, PartialEq)]
 pub struct GenerationId(u32);
 
 #[dom_struct]
@@ -427,33 +425,39 @@ impl HTMLFormElement {
         self.plan_to_navigate(load_data);
     }
 
     /// [Planned navigation](https://html.spec.whatwg.org/multipage/#planned-navigation)
     fn plan_to_navigate(&self, load_data: LoadData) {
         let window = window_from_node(self);
 
         // Step 1
-        // Each planned navigation runnable is tagged with a generation ID, and
-        // before the runnable is handled, it first checks whether the HTMLFormElement's
+        // Each planned navigation task is tagged with a generation ID, and
+        // before the task is handled, it first checks whether the HTMLFormElement's
         // generation ID is the same as its own generation ID.
-        let GenerationId(prev_id) = self.generation_id.get();
-        self.generation_id.set(GenerationId(prev_id + 1));
+        let generation_id = GenerationId(self.generation_id.get().0 + 1);
+        self.generation_id.set(generation_id);
 
-        // Step 2
-        let nav = box PlannedNavigation {
-            load_data: load_data,
-            pipeline_id: window.upcast::<GlobalScope>().pipeline_id(),
-            script_chan: window.main_thread_script_chan().clone(),
-            generation_id: self.generation_id.get(),
-            form: Trusted::new(self)
-        };
+        // Step 2.
+        let pipeline_id = window.upcast::<GlobalScope>().pipeline_id();
+        let script_chan = window.main_thread_script_chan().clone();
+        let this = Trusted::new(self);
+        let task = box task!(navigate_to_form_planned_navigation: move || {
+            if generation_id != this.root().generation_id.get() {
+                return;
+            }
+            script_chan.send(MainThreadScriptMsg::Navigate(
+                pipeline_id,
+                load_data,
+                false,
+            )).unwrap();
+        });
 
-        // Step 3
-        window.dom_manipulation_task_source().queue(nav, window.upcast()).unwrap();
+        // Step 3.
+        window.dom_manipulation_task_source().queue(task, window.upcast()).unwrap();
     }
 
     /// Interactively validate the constraints of form elements
     /// https://html.spec.whatwg.org/multipage/#interactively-validate-the-constraints
     fn interactive_validation(&self) -> Result<(), ()> {
         // Step 1-3
         let _unhandled_invalid_controls = match self.static_validation() {
             Ok(()) => return Ok(()),
@@ -1102,34 +1106,16 @@ impl FormControlElementHelpers for Eleme
             },
             _ => {
                 None
             }
         }
     }
 }
 
-struct PlannedNavigation {
-    load_data: LoadData,
-    pipeline_id: PipelineId,
-    script_chan: Sender<MainThreadScriptMsg>,
-    generation_id: GenerationId,
-    form: Trusted<HTMLFormElement>
-}
-
-impl Runnable for PlannedNavigation {
-    fn handler(self: Box<PlannedNavigation>) {
-        if self.generation_id == self.form.root().generation_id.get() {
-            let script_chan = self.script_chan.clone();
-            script_chan.send(MainThreadScriptMsg::Navigate(self.pipeline_id, self.load_data, false)).unwrap();
-        }
-    }
-}
-
-
 // https://html.spec.whatwg.org/multipage/#multipart/form-data-encoding-algorithm
 pub fn encode_multipart_form_data(form_data: &mut Vec<FormDatum>,
                                   boundary: String, encoding: EncodingRef) -> Vec<u8> {
     // Step 1
     let mut result = vec![];
 
     // Step 2
     let charset = &*encoding.whatwg_name().unwrap_or("UTF-8");
--- a/servo/components/script/dom/htmliframeelement.rs
+++ b/servo/components/script/dom/htmliframeelement.rs
@@ -38,17 +38,17 @@ use dom::windowproxy::WindowProxy;
 use dom_struct::dom_struct;
 use html5ever::{LocalName, Prefix};
 use ipc_channel::ipc;
 use js::jsapi::{JSAutoCompartment, JSContext, MutableHandleValue};
 use js::jsval::{NullValue, UndefinedValue};
 use msg::constellation_msg::{FrameType, BrowsingContextId, PipelineId, TopLevelBrowsingContextId, TraversalDirection};
 use net_traits::response::HttpsState;
 use script_layout_interface::message::ReflowQueryType;
-use script_thread::{ScriptThread, Runnable};
+use script_thread::ScriptThread;
 use script_traits::{IFrameLoadInfo, IFrameLoadInfoWithData, JsEvalResult, LoadData, UpdatePipelineIdReason};
 use script_traits::{MozBrowserEvent, NewLayoutInfo, ScriptMsg};
 use script_traits::IFrameSandboxState::{IFrameSandboxed, IFrameUnsandboxed};
 use servo_atoms::Atom;
 use servo_config::prefs::PREFS;
 use servo_config::servo_version;
 use servo_url::ServoUrl;
 use std::cell::Cell;
@@ -227,19 +227,25 @@ impl HTMLIFrameElement {
     /// https://html.spec.whatwg.org/multipage/#process-the-iframe-attributes
     fn process_the_iframe_attributes(&self, mode: ProcessingMode) {
         // TODO: srcdoc
 
         let window = window_from_node(self);
 
         // https://github.com/whatwg/html/issues/490
         if mode == ProcessingMode::FirstTime && !self.upcast::<Element>().has_attribute(&local_name!("src")) {
-            let event_loop = window.dom_manipulation_task_source();
-            let _ = event_loop.queue(box IFrameLoadEventSteps::new(self),
-                                     window.upcast());
+            let this = Trusted::new(self);
+            let pipeline_id = self.pipeline_id().unwrap();
+            // FIXME(nox): Why are errors silenced here?
+            let _ = window.dom_manipulation_task_source().queue(
+                box task!(iframe_load_event_steps: move || {
+                    this.root().iframe_load_event_steps(pipeline_id);
+                }),
+                window.upcast(),
+            );
             return;
         }
 
         let url = self.get_url();
 
         // TODO: check ancestor browsing contexts for same URL
 
         let creator_pipeline_id = if url.as_str() == "about:blank" {
@@ -829,29 +835,8 @@ impl VirtualMethods for HTMLIFrameElemen
         // Resetting the pipeline_id to None is required here so that
         // if this iframe is subsequently re-added to the document
         // the load doesn't think that it's a navigation, but instead
         // a new iframe. Without this, the constellation gets very
         // confused.
         self.destroy_nested_browsing_context();
     }
 }
-
-struct IFrameLoadEventSteps {
-    frame_element: Trusted<HTMLIFrameElement>,
-    pipeline_id: PipelineId,
-}
-
-impl IFrameLoadEventSteps {
-    fn new(frame_element: &HTMLIFrameElement) -> IFrameLoadEventSteps {
-        IFrameLoadEventSteps {
-            frame_element: Trusted::new(frame_element),
-            pipeline_id: frame_element.pipeline_id().unwrap(),
-        }
-    }
-}
-
-impl Runnable for IFrameLoadEventSteps {
-    fn handler(self: Box<IFrameLoadEventSteps>) {
-        let this = self.frame_element.root();
-        this.iframe_load_event_steps(self.pipeline_id);
-    }
-}
--- a/servo/components/script/dom/htmlimageelement.rs
+++ b/servo/components/script/dom/htmlimageelement.rs
@@ -43,17 +43,17 @@ use microtask::{Microtask, MicrotaskRunn
 use net_traits::{FetchResponseListener, FetchMetadata, NetworkError, FetchResponseMsg};
 use net_traits::image::base::{Image, ImageMetadata};
 use net_traits::image_cache::{CanRequestImages, ImageCache, ImageOrMetadataAvailable};
 use net_traits::image_cache::{ImageResponder, ImageResponse, ImageState, PendingImageId};
 use net_traits::image_cache::UsePlaceholder;
 use net_traits::request::{RequestInit, Type as RequestType};
 use network_listener::{NetworkListener, PreInvoke};
 use num_traits::ToPrimitive;
-use script_thread::{Runnable, ScriptThread};
+use script_thread::ScriptThread;
 use servo_url::ServoUrl;
 use servo_url::origin::ImmutableOrigin;
 use std::cell::{Cell, RefMut};
 use std::default::Default;
 use std::i32;
 use std::sync::{Arc, Mutex};
 use style::attr::{AttrValue, LengthOrPercentageOrAuto};
 use task_source::TaskSource;
@@ -94,43 +94,16 @@ pub struct HTMLImageElement {
 }
 
 impl HTMLImageElement {
     pub fn get_url(&self) -> Option<ServoUrl> {
         self.current_request.borrow().parsed_url.clone()
     }
 }
 
-struct ImageResponseHandlerRunnable {
-    element: Trusted<HTMLImageElement>,
-    image: ImageResponse,
-    generation: u32,
-}
-
-impl ImageResponseHandlerRunnable {
-    fn new(element: Trusted<HTMLImageElement>, image: ImageResponse, generation: u32)
-           -> ImageResponseHandlerRunnable {
-        ImageResponseHandlerRunnable {
-            element: element,
-            image: image,
-            generation: generation,
-        }
-    }
-}
-
-impl Runnable for ImageResponseHandlerRunnable {
-    fn handler(self: Box<Self>) {
-        let element = self.element.root();
-        // Ignore any image response for a previous request that has been discarded.
-        if element.generation.get() == self.generation {
-            element.process_image_response(self.image);
-        }
-    }
-}
-
 /// The context required for asynchronously loading an external image.
 struct ImageContext {
     /// Reference to the script thread image cache.
     image_cache: Arc<ImageCache>,
     /// Indicates whether the request failed, and why
     status: Result<(), NetworkError>,
     /// The cache ID for this request.
     id: PendingImageId,
@@ -186,25 +159,35 @@ impl HTMLImageElement {
         fn add_cache_listener_for_element(image_cache: Arc<ImageCache>,
                                           id: PendingImageId,
                                           elem: &HTMLImageElement) {
             let trusted_node = Trusted::new(elem);
             let (responder_sender, responder_receiver) = ipc::channel().unwrap();
 
             let window = window_from_node(elem);
             let task_source = window.networking_task_source();
-            let wrapper = window.get_runnable_wrapper();
+            let task_canceller = window.task_canceller();
             let generation = elem.generation.get();
             ROUTER.add_route(responder_receiver.to_opaque(), box move |message| {
                 debug!("Got image {:?}", message);
                 // Return the image via a message to the script thread, which marks
                 // the element as dirty and triggers a reflow.
-                let runnable = ImageResponseHandlerRunnable::new(
-                    trusted_node.clone(), message.to().unwrap(), generation);
-                let _ = task_source.queue_with_wrapper(box runnable, &wrapper);
+                let element = trusted_node.clone();
+                let image = message.to().unwrap();
+                // FIXME(nox): Why are errors silenced here?
+                let _ = task_source.queue_with_canceller(
+                    box task!(process_image_response: move || {
+                        let element = element.root();
+                        // Ignore any image response for a previous request that has been discarded.
+                        if generation == element.generation.get() {
+                            element.process_image_response(image);
+                        }
+                    }),
+                    &task_canceller,
+                );
             });
 
             image_cache.add_listener(id, ImageResponder::new(responder_sender, id));
         }
 
         let window = window_from_node(self);
         let image_cache = window.image_cache();
         let response =
@@ -244,17 +227,17 @@ impl HTMLImageElement {
             status: Ok(()),
             id: id,
         }));
 
         let (action_sender, action_receiver) = ipc::channel().unwrap();
         let listener = NetworkListener {
             context: context,
             task_source: window.networking_task_source(),
-            wrapper: Some(window.get_runnable_wrapper()),
+            canceller: Some(window.task_canceller()),
         };
         ROUTER.add_route(action_receiver.to_opaque(), box move |message| {
             listener.notify_fetch(message.to().unwrap());
         });
 
         let request = RequestInit {
             url: img_url.clone(),
             origin: document.origin().immutable().clone(),
@@ -347,68 +330,16 @@ impl HTMLImageElement {
             ImageRequestPhase::Pending => self.pending_request.borrow_mut(),
         };
         LoadBlocker::terminate(&mut request.blocker);
         request.state = state;
         request.image = None;
         request.metadata = None;
     }
 
-    /// Step 11.4 of https://html.spec.whatwg.org/multipage/#update-the-image-data
-    fn set_current_request_url_to_selected_fire_error_and_loadend(&self, src: DOMString) {
-        struct Task {
-            img: Trusted<HTMLImageElement>,
-            src: String,
-        }
-        impl Runnable for Task {
-            fn handler(self: Box<Self>) {
-                let img = self.img.root();
-                {
-                    let mut current_request = img.current_request.borrow_mut();
-                    current_request.source_url = Some(DOMString::from_string(self.src));
-                }
-                img.upcast::<EventTarget>().fire_event(atom!("error"));
-                img.upcast::<EventTarget>().fire_event(atom!("loadend"));
-                img.abort_request(State::Broken, ImageRequestPhase::Current);
-                img.abort_request(State::Broken, ImageRequestPhase::Pending);
-            }
-        }
-
-        let task = box Task {
-            img: Trusted::new(self),
-            src: src.into()
-        };
-        let document = document_from_node(self);
-        let window = document.window();
-        let task_source = window.dom_manipulation_task_source();
-        let _ = task_source.queue(task, window.upcast());
-    }
-
-    /// Step 10 of html.spec.whatwg.org/multipage/#update-the-image-data
-    fn dispatch_loadstart_progress_event(&self) {
-        struct FireprogressEventTask {
-            img: Trusted<HTMLImageElement>,
-        }
-        impl Runnable for FireprogressEventTask {
-            fn handler(self: Box<Self>) {
-                let progressevent = ProgressEvent::new(&self.img.root().global(),
-                    atom!("loadstart"), EventBubbles::DoesNotBubble, EventCancelable::NotCancelable,
-                    false, 0, 0);
-                progressevent.upcast::<Event>().fire(self.img.root().upcast());
-            }
-        }
-        let runnable = box FireprogressEventTask {
-            img: Trusted::new(self),
-        };
-        let document = document_from_node(self);
-        let window = document.window();
-        let task = window.dom_manipulation_task_source();
-        let _ = task.queue(runnable, window.upcast());
-    }
-
     /// https://html.spec.whatwg.org/multipage/#update-the-source-set
     fn update_source_set(&self) -> Vec<DOMString> {
         let elem = self.upcast::<Element>();
         // TODO: follow the algorithm
         let src = elem.get_string_attribute(&local_name!("src"));
         if src.is_empty() {
             return vec![]
         }
@@ -416,77 +347,16 @@ impl HTMLImageElement {
     }
 
     /// https://html.spec.whatwg.org/multipage/#select-an-image-source
     fn select_image_source(&self) -> Option<DOMString> {
         // TODO: select an image source from source set
         self.update_source_set().first().cloned()
     }
 
-    /// Step 9.2 of https://html.spec.whatwg.org/multipage/#update-the-image-data
-    fn set_current_request_url_to_none_fire_error(&self) {
-        struct SetUrlToNoneTask {
-            img: Trusted<HTMLImageElement>,
-        }
-        impl Runnable for SetUrlToNoneTask {
-            fn handler(self: Box<Self>) {
-                let img = self.img.root();
-                {
-                    let mut current_request = img.current_request.borrow_mut();
-                    current_request.source_url = None;
-                    current_request.parsed_url = None;
-                }
-                let elem = img.upcast::<Element>();
-                if elem.has_attribute(&local_name!("src")) {
-                    img.upcast::<EventTarget>().fire_event(atom!("error"));
-                }
-                img.abort_request(State::Broken, ImageRequestPhase::Current);
-                img.abort_request(State::Broken, ImageRequestPhase::Pending);
-            }
-        }
-
-        let task = box SetUrlToNoneTask {
-            img: Trusted::new(self),
-        };
-        let document = document_from_node(self);
-        let window = document.window();
-        let task_source = window.dom_manipulation_task_source();
-        let _ = task_source.queue(task, window.upcast());
-    }
-
-    /// Step 5.3.7 of https://html.spec.whatwg.org/multipage/#update-the-image-data
-    fn set_current_request_url_to_string_and_fire_load(&self, src: DOMString, url: ServoUrl) {
-        struct SetUrlToStringTask {
-            img: Trusted<HTMLImageElement>,
-            src: String,
-            url: ServoUrl
-        }
-        impl Runnable for SetUrlToStringTask {
-            fn handler(self: Box<Self>) {
-                let img = self.img.root();
-                {
-                    let mut current_request = img.current_request.borrow_mut();
-                    current_request.parsed_url = Some(self.url.clone());
-                    current_request.source_url = Some(self.src.into());
-                }
-                // TODO: restart animation, if set
-                img.upcast::<EventTarget>().fire_event(atom!("load"));
-            }
-        }
-        let runnable = box SetUrlToStringTask {
-            img: Trusted::new(self),
-            src: src.into(),
-            url: url
-        };
-        let document = document_from_node(self);
-        let window = document.window();
-        let task = window.dom_manipulation_task_source();
-        let _ = task.queue(runnable, window.upcast());
-    }
-
     fn init_image_request(&self,
                           request: &mut RefMut<ImageRequest>,
                           url: &ServoUrl,
                           src: &DOMString) {
         request.parsed_url = Some(url.clone());
         request.source_url = Some(src.clone());
         request.image = None;
         request.metadata = None;
@@ -537,40 +407,103 @@ impl HTMLImageElement {
             }
         }
         self.fetch_image(&url);
     }
 
     /// Step 8-12 of html.spec.whatwg.org/multipage/#update-the-image-data
     fn update_the_image_data_sync_steps(&self) {
         let document = document_from_node(self);
-        // Step 8
-        // TODO: take pixel density into account
-        match self.select_image_source() {
+        let window = document.window();
+        let task_source = window.dom_manipulation_task_source();
+        let this = Trusted::new(self);
+        let src = match self.select_image_source() {
             Some(src) => {
-                // Step 10
-                self.dispatch_loadstart_progress_event();
-                // Step 11
-                let base_url = document.base_url();
-                let parsed_url = base_url.join(&src);
-                match parsed_url {
-                    Ok(url) => {
-                         // Step 12
-                        self.prepare_image_request(&url, &src);
-                    },
-                    Err(_) => {
-                        // Step 11.1-11.5
-                        self.set_current_request_url_to_selected_fire_error_and_loadend(src);
-                    }
-                }
+                // Step 8.
+                // TODO: Handle pixel density.
+                src
             },
             None => {
-                // Step 9
-                self.set_current_request_url_to_none_fire_error();
+                // Step 9.
+                // FIXME(nox): Why are errors silenced here?
+                let _ = task_source.queue(
+                    box task!(image_null_source_error: move || {
+                        let this = this.root();
+                        {
+                            let mut current_request =
+                                this.current_request.borrow_mut();
+                            current_request.source_url = None;
+                            current_request.parsed_url = None;
+                        }
+                        if this.upcast::<Element>().has_attribute(&local_name!("src")) {
+                            this.upcast::<EventTarget>().fire_event(atom!("error"));
+                        }
+                        // FIXME(nox): According to the spec, setting the current
+                        // request to the broken state is done prior to queuing a
+                        // task, why is this here?
+                        this.abort_request(State::Broken, ImageRequestPhase::Current);
+                        this.abort_request(State::Broken, ImageRequestPhase::Pending);
+                    }),
+                    window.upcast(),
+                );
+                return;
             },
+        };
+        // Step 10.
+        let target = Trusted::new(self.upcast::<EventTarget>());
+        // FIXME(nox): Why are errors silenced here?
+        let _ = task_source.queue(
+            box task!(fire_progress_event: move || {
+                let target = target.root();
+
+                let event = ProgressEvent::new(
+                    &target.global(),
+                    atom!("loadstart"),
+                    EventBubbles::DoesNotBubble,
+                    EventCancelable::NotCancelable,
+                    false,
+                    0,
+                    0,
+                );
+                event.upcast::<Event>().fire(&target);
+            }),
+            window.upcast(),
+        );
+        // Step 11
+        let base_url = document.base_url();
+        let parsed_url = base_url.join(&src);
+        match parsed_url {
+            Ok(url) => {
+                    // Step 12
+                self.prepare_image_request(&url, &src);
+            },
+            Err(_) => {
+                // Step 11.1-11.5.
+                let src = String::from(src);
+                // FIXME(nox): Why are errors silenced here?
+                let _ = task_source.queue(
+                    box task!(image_selected_source_error: move || {
+                        let this = this.root();
+                        {
+                            let mut current_request =
+                                this.current_request.borrow_mut();
+                            current_request.source_url = Some(src.into());
+                        }
+                        this.upcast::<EventTarget>().fire_event(atom!("error"));
+                        this.upcast::<EventTarget>().fire_event(atom!("loadend"));
+
+                        // FIXME(nox): According to the spec, setting the current
+                        // request to the broken state is done prior to queuing a
+                        // task, why is this here?
+                        this.abort_request(State::Broken, ImageRequestPhase::Current);
+                        this.abort_request(State::Broken, ImageRequestPhase::Pending);
+                    }),
+                    window.upcast(),
+                );
+            }
         }
     }
 
     /// https://html.spec.whatwg.org/multipage/#update-the-image-data
     fn update_the_image_data(&self) {
         let document = document_from_node(self);
         let window = document.window();
         let elem = self.upcast::<Element>();
@@ -608,18 +541,33 @@ impl HTMLImageElement {
                     let metadata = ImageMetadata { height: image.height, width: image.width };
                     // Step 5.3.2 abort requests
                     self.abort_request(State::CompletelyAvailable, ImageRequestPhase::Current);
                     self.abort_request(State::CompletelyAvailable, ImageRequestPhase::Pending);
                     let mut current_request = self.current_request.borrow_mut();
                     current_request.final_url = Some(url);
                     current_request.image = Some(image.clone());
                     current_request.metadata = Some(metadata);
-                    self.set_current_request_url_to_string_and_fire_load(src, img_url);
-                    return
+                    let this = Trusted::new(self);
+                    let src = String::from(src);
+                    let _ = window.dom_manipulation_task_source().queue(
+                        box task!(image_load_event: move || {
+                            let this = this.root();
+                            {
+                                let mut current_request =
+                                    this.current_request.borrow_mut();
+                                current_request.parsed_url = Some(img_url);
+                                current_request.source_url = Some(src.into());
+                            }
+                            // TODO: restart animation, if set.
+                            this.upcast::<EventTarget>().fire_event(atom!("load"));
+                        }),
+                        window.upcast(),
+                    );
+                    return;
                 }
             }
         }
         // step 6, await a stable state.
         self.generation.set(self.generation.get() + 1);
         let task = ImageElementMicrotask::StableStateUpdateImageDataTask {
             elem: Root::from_ref(self),
             generation: self.generation.get(),
--- a/servo/components/script/dom/htmlmediaelement.rs
+++ b/servo/components/script/dom/htmlmediaelement.rs
@@ -30,17 +30,17 @@ use dom::virtualmethods::VirtualMethods;
 use dom_struct::dom_struct;
 use html5ever::{LocalName, Prefix};
 use ipc_channel::ipc;
 use ipc_channel::router::ROUTER;
 use microtask::{Microtask, MicrotaskRunnable};
 use net_traits::{FetchResponseListener, FetchMetadata, Metadata, NetworkError};
 use net_traits::request::{CredentialsMode, Destination, RequestInit, Type as RequestType};
 use network_listener::{NetworkListener, PreInvoke};
-use script_thread::{Runnable, ScriptThread};
+use script_thread::ScriptThread;
 use servo_url::ServoUrl;
 use std::cell::Cell;
 use std::sync::{Arc, Mutex};
 use task_source::TaskSource;
 use time::{self, Timespec, Duration};
 
 #[dom_struct]
 pub struct HTMLMediaElement {
@@ -134,68 +134,62 @@ impl HTMLMediaElement {
             self.paused.set(true);
 
             // Step 2.2.
             // FIXME(nox): Take pending play promises and let promises be the
             // result.
 
             // Step 2.3.
             let window = window_from_node(self);
+            let target = Trusted::new(self.upcast::<EventTarget>());
             // FIXME(nox): Why are errors silenced here?
             let _ = window.dom_manipulation_task_source().queue(
-                box InternalPauseStepsTask(Trusted::new(self.upcast())),
-                window.upcast(),
-            );
-            struct InternalPauseStepsTask(Trusted<EventTarget>);
-            impl Runnable for InternalPauseStepsTask {
-                fn handler(self: Box<Self>) {
-                    let target = self.0.root();
+                box task!(internal_pause_steps: move || {
+                    let target = target.root();
 
                     // Step 2.3.1.
                     target.fire_event(atom!("timeupdate"));
 
                     // Step 2.3.2.
                     target.fire_event(atom!("pause"));
 
                     // Step 2.3.3.
                     // FIXME(nox): Reject pending play promises with promises
                     // and an "AbortError" DOMException.
-                }
-            }
+                }),
+                window.upcast(),
+            );
 
             // Step 2.4.
             // FIXME(nox): Set the official playback position to the current
             // playback position.
         }
     }
 
     // https://html.spec.whatwg.org/multipage/#notify-about-playing
     fn notify_about_playing(&self) {
         // Step 1.
         // TODO(nox): Take pending play promises and let promises be the result.
 
         // Step 2.
+        let target = Trusted::new(self.upcast::<EventTarget>());
         let window = window_from_node(self);
         // FIXME(nox): Why are errors silenced here?
         let _ = window.dom_manipulation_task_source().queue(
-            box NotifyAboutPlayingTask(Trusted::new(self.upcast())),
-            window.upcast(),
-        );
-        struct NotifyAboutPlayingTask(Trusted<EventTarget>);
-        impl Runnable for NotifyAboutPlayingTask {
-            fn handler(self: Box<Self>) {
-                let target = self.0.root();
+            box task!(notify_about_playing: move || {
+                let target = target.root();
 
                 // Step 2.1.
                 target.fire_event(atom!("playing"));
 
                 // Step 2.2.
                 // FIXME(nox): Resolve pending play promises with promises.
-            }
-        }
+            }),
+            window.upcast(),
+        );
     }
 
     // https://html.spec.whatwg.org/multipage/#ready-states
     fn change_ready_state(&self, ready_state: ReadyState) {
         let old_ready_state = self.ready_state.get();
         self.ready_state.set(ready_state);
 
         if self.network_state.get() == NetworkState::Empty {
@@ -423,17 +417,17 @@ impl HTMLMediaElement {
 
             // 4.2
             let context = Arc::new(Mutex::new(HTMLMediaElementContext::new(self, url.clone())));
             let (action_sender, action_receiver) = ipc::channel().unwrap();
             let window = window_from_node(self);
             let listener = NetworkListener {
                 context: context,
                 task_source: window.networking_task_source(),
-                wrapper: Some(window.get_runnable_wrapper())
+                canceller: Some(window.task_canceller())
             };
 
             ROUTER.add_route(action_receiver.to_opaque(), box move |message| {
                 listener.notify_fetch(message.to().unwrap());
             });
 
             // FIXME: we're supposed to block the load event much earlier than now
             let document = document_from_node(self);
@@ -461,40 +455,53 @@ impl HTMLMediaElement {
 
             document.fetch_async(LoadType::Media(url), request, action_sender);
         } else {
             // TODO local resource fetch
             self.queue_dedicated_media_source_failure_steps();
         }
     }
 
+    /// Queues the [dedicated media source failure steps][steps].
+    ///
+    /// [steps]: https://html.spec.whatwg.org/multipage/#dedicated-media-source-failure-steps
     fn queue_dedicated_media_source_failure_steps(&self) {
+        let this = Trusted::new(self);
         let window = window_from_node(self);
+        // FIXME(nox): Why are errors silenced here?
         let _ = window.dom_manipulation_task_source().queue(
-            box DedicatedMediaSourceFailureTask::new(self), window.upcast());
-    }
+            box task!(dedicated_media_source_failure_steps: move || {
+                let this = this.root();
 
-    // https://html.spec.whatwg.org/multipage/#dedicated-media-source-failure-steps
-    fn dedicated_media_source_failure(&self) {
-        // Step 1
-        self.error.set(Some(&*MediaError::new(&*window_from_node(self),
-                                              MEDIA_ERR_SRC_NOT_SUPPORTED)));
+                // Step 1.
+                this.error.set(Some(&*MediaError::new(
+                    &window_from_node(&*this),
+                    MEDIA_ERR_SRC_NOT_SUPPORTED,
+                )));
 
-        // TODO step 2 (forget resource tracks)
+                // Step 2.
+                // FIXME(nox): Forget the media-resource-specific tracks.
 
-        // Step 3
-        self.network_state.set(NetworkState::NoSource);
+                // Step 3.
+                this.network_state.set(NetworkState::NoSource);
+
+                // Step 4.
+                // FIXME(nox): Set show poster flag to true.
 
-        // TODO step 4 (show poster)
+                // Step 5.
+                this.upcast::<EventTarget>().fire_event(atom!("error"));
+
+                // Step 6.
+                // FIXME(nox): Reject pending play promises.
 
-        // Step 5
-        self.upcast::<EventTarget>().fire_event(atom!("error"));
-
-        // TODO step 6 (resolve pending play promises)
-        // TODO step 7 (delay load event)
+                // Step 7.
+                // FIXME(nox): Set the delaying-the-load-event flag to false.
+            }),
+            window.upcast(),
+        );
     }
 
     // https://html.spec.whatwg.org/multipage/#media-element-load-algorithm
     fn media_element_load_algorithm(&self) {
         // Reset the flag that signals whether loadeddata was ever fired for
         // this invokation of the load algorithm.
         self.fired_loadeddata_event.set(false);
 
@@ -733,34 +740,16 @@ impl MicrotaskRunnable for MediaElementM
                 if !elem.upcast::<Node>().is_in_doc() {
                     elem.internal_pause_steps();
                 }
             },
         }
     }
 }
 
-struct DedicatedMediaSourceFailureTask {
-    elem: Trusted<HTMLMediaElement>,
-}
-
-impl DedicatedMediaSourceFailureTask {
-    fn new(elem: &HTMLMediaElement) -> DedicatedMediaSourceFailureTask {
-        DedicatedMediaSourceFailureTask {
-            elem: Trusted::new(elem),
-        }
-    }
-}
-
-impl Runnable for DedicatedMediaSourceFailureTask {
-    fn handler(self: Box<DedicatedMediaSourceFailureTask>) {
-        self.elem.root().dedicated_media_source_failure();
-    }
-}
-
 enum ResourceSelectionMode {
     Object,
     Attribute(String),
     Children(Root<HTMLSourceElement>),
 }
 
 enum Resource {
     Object,
--- a/servo/components/script/dom/htmlscriptelement.rs
+++ b/servo/components/script/dom/htmlscriptelement.rs
@@ -272,17 +272,17 @@ fn fetch_a_classic_script(script: &HTMLS
         url: url.clone(),
         status: Ok(())
     }));
 
     let (action_sender, action_receiver) = ipc::channel().unwrap();
     let listener = NetworkListener {
         context: context,
         task_source: doc.window().networking_task_source(),
-        wrapper: Some(doc.window().get_runnable_wrapper())
+        canceller: Some(doc.window().task_canceller())
     };
 
     ROUTER.add_route(action_receiver.to_opaque(), box move |message| {
         listener.notify_fetch(message.to().unwrap());
     });
     doc.fetch_async(LoadType::Script(url), request, action_sender);
 }
 
--- a/servo/components/script/dom/paintworkletglobalscope.rs
+++ b/servo/components/script/dom/paintworkletglobalscope.rs
@@ -41,17 +41,16 @@ use js::jsapi::JS_IsExceptionPending;
 use js::jsapi::JS_NewArrayObject;
 use js::jsval::JSVal;
 use js::jsval::ObjectValue;
 use js::jsval::UndefinedValue;
 use js::rust::Runtime;
 use msg::constellation_msg::PipelineId;
 use net_traits::image::base::PixelFormat;
 use net_traits::image_cache::ImageCache;
-use script_layout_interface::message::Msg;
 use script_traits::DrawAPaintImageResult;
 use script_traits::Painter;
 use servo_atoms::Atom;
 use servo_url::ServoUrl;
 use std::cell::Cell;
 use std::collections::HashMap;
 use std::collections::hash_map::Entry;
 use std::ptr::null_mut;
@@ -438,18 +437,17 @@ impl PaintWorkletGlobalScopeMethods for 
         debug!("Registering definition {}.", name);
         self.paint_definitions.borrow_mut().insert(name.clone(), definition);
 
         // TODO: Step 21.
 
         // Inform layout that there is a registered paint worklet.
         // TODO: layout will end up getting this message multiple times.
         let painter = self.painter(name.clone());
-        let msg = Msg::RegisterPaint(name, properties, painter);
-        self.worklet_global.send_to_layout(msg);
+        self.worklet_global.register_paint_worklet(name, properties, painter);
 
         Ok(())
     }
 }
 
 /// Tasks which can be peformed by a paint worklet
 pub enum PaintWorkletTask {
     DrawAPaintImage(Atom,
--- a/servo/components/script/dom/serviceworker.rs
+++ b/servo/components/script/dom/serviceworker.rs
@@ -11,20 +11,20 @@ use dom::bindings::js::Root;
 use dom::bindings::refcounted::Trusted;
 use dom::bindings::reflector::{DomObject, reflect_dom_object};
 use dom::bindings::str::USVString;
 use dom::bindings::structuredclone::StructuredCloneData;
 use dom::eventtarget::EventTarget;
 use dom::globalscope::GlobalScope;
 use dom_struct::dom_struct;
 use js::jsapi::{HandleValue, JSContext};
-use script_thread::Runnable;
 use script_traits::{ScriptMsg, DOMMessage};
 use servo_url::ServoUrl;
 use std::cell::Cell;
+use task::Task;
 
 pub type TrustedServiceWorkerAddress = Trusted<ServiceWorker>;
 
 #[dom_struct]
 pub struct ServiceWorker {
     eventtarget: EventTarget,
     script_url: DOMRefCell<String>,
     scope_url: ServoUrl,
@@ -99,15 +99,15 @@ impl ServiceWorkerMethods for ServiceWor
 
     // https://w3c.github.io/ServiceWorker/#service-worker-container-onerror-attribute
     event_handler!(error, GetOnerror, SetOnerror);
 
     // https://w3c.github.io/ServiceWorker/#ref-for-service-worker-onstatechange-attribute-1
     event_handler!(statechange, GetOnstatechange, SetOnstatechange);
 }
 
-impl Runnable for SimpleWorkerErrorHandler<ServiceWorker> {
+impl Task for SimpleWorkerErrorHandler<ServiceWorker> {
     #[allow(unrooted_must_root)]
-    fn handler(self: Box<SimpleWorkerErrorHandler<ServiceWorker>>) {
+    fn run(self: Box<Self>) {
         let this = *self;
         ServiceWorker::dispatch_simple_error(this.addr);
     }
 }
--- a/servo/components/script/dom/serviceworkercontainer.rs
+++ b/servo/components/script/dom/serviceworkercontainer.rs
@@ -109,12 +109,12 @@ impl ServiceWorkerContainerMethods for S
         if scope.path().to_ascii_lowercase().contains("%2f") ||
         scope.path().to_ascii_lowercase().contains("%5c") {
             promise.reject_error(ctx, Error::Type("Scope URL contains forbidden characters".to_owned()));
             return promise;
         }
 
         // B: Step 8
         let job = Job::create_job(JobType::Register, scope, script_url, promise.clone(), &*self.client);
-        ScriptThread::schedule_job(job, &*self.global());
+        ScriptThread::schedule_job(job);
         promise
     }
 }
--- a/servo/components/script/dom/storage.rs
+++ b/servo/components/script/dom/storage.rs
@@ -12,17 +12,16 @@ use dom::bindings::reflector::{DomObject
 use dom::bindings::str::DOMString;
 use dom::event::{Event, EventBubbles, EventCancelable};
 use dom::storageevent::StorageEvent;
 use dom::window::Window;
 use dom_struct::dom_struct;
 use ipc_channel::ipc::{self, IpcSender};
 use net_traits::IpcSend;
 use net_traits::storage_thread::{StorageThreadMsg, StorageType};
-use script_thread::{Runnable, ScriptThread};
 use script_traits::ScriptMsg;
 use servo_url::ServoUrl;
 use task_source::TaskSource;
 
 #[dom_struct]
 pub struct Storage {
     reflector_: Reflector,
     storage_type: StorageType
@@ -153,55 +152,38 @@ impl Storage {
                                      new_value: Option<String>) {
         let storage = self.storage_type;
         let url = self.get_url();
         let msg = ScriptMsg::BroadcastStorageEvent(storage, url, key, old_value, new_value);
         self.global().script_to_constellation_chan().send(msg).unwrap();
     }
 
     /// https://html.spec.whatwg.org/multipage/#send-a-storage-notification
-    pub fn queue_storage_event(&self, url: ServoUrl,
-                               key: Option<String>, old_value: Option<String>, new_value: Option<String>) {
+    pub fn queue_storage_event(
+        &self,
+        url: ServoUrl,
+        key: Option<String>,
+        old_value: Option<String>,
+        new_value: Option<String>,
+    ) {
         let global = self.global();
-        let window = global.as_window();
-        let task_source = window.dom_manipulation_task_source();
-        let trusted_storage = Trusted::new(self);
-        task_source
-            .queue(
-                box StorageEventRunnable::new(trusted_storage, url, key, old_value, new_value), &global)
-            .unwrap();
+        let this = Trusted::new(self);
+        global.as_window().dom_manipulation_task_source().queue(
+            box task!(send_storage_notification: move || {
+                let this = this.root();
+                let global = this.global();
+                let event = StorageEvent::new(
+                    global.as_window(),
+                    atom!("storage"),
+                    EventBubbles::DoesNotBubble,
+                    EventCancelable::NotCancelable,
+                    key.map(DOMString::from),
+                    old_value.map(DOMString::from),
+                    new_value.map(DOMString::from),
+                    DOMString::from(url.into_string()),
+                    Some(&this),
+                );
+                event.upcast::<Event>().fire(global.upcast());
+            }),
+            global.upcast(),
+        ).unwrap();
     }
 }
-
-pub struct StorageEventRunnable {
-    element: Trusted<Storage>,
-    url: ServoUrl,
-    key: Option<String>,
-    old_value: Option<String>,
-    new_value: Option<String>
-}
-
-impl StorageEventRunnable {
-    fn new(storage: Trusted<Storage>, url: ServoUrl,
-           key: Option<String>, old_value: Option<String>, new_value: Option<String>) -> StorageEventRunnable {
-        StorageEventRunnable { element: storage, url: url, key: key, old_value: old_value, new_value: new_value }
-    }
-}
-
-impl Runnable for StorageEventRunnable {
-    fn main_thread_handler(self: Box<StorageEventRunnable>, _: &ScriptThread) {
-        let this = *self;
-        let storage = this.element.root();
-        let global = storage.global();
-        let window = global.as_window();
-
-        let storage_event = StorageEvent::new(
-            &window,
-            atom!("storage"),
-            EventBubbles::DoesNotBubble, EventCancelable::NotCancelable,
-            this.key.map(DOMString::from), this.old_value.map(DOMString::from), this.new_value.map(DOMString::from),
-            DOMString::from(this.url.into_string()),
-            Some(&storage)
-        );
-
-        storage_event.upcast::<Event>().fire(window.upcast());
-    }
-}
--- a/servo/components/script/dom/vrdisplay.rs
+++ b/servo/components/script/dom/vrdisplay.rs
@@ -31,17 +31,16 @@ use dom::vrframedata::VRFrameData;
 use dom::vrpose::VRPose;
 use dom::vrstageparameters::VRStageParameters;
 use dom::webglrenderingcontext::WebGLRenderingContext;
 use dom_struct::dom_struct;
 use ipc_channel::ipc::{self, IpcSender};
 use js::jsapi::JSContext;
 use script_runtime::CommonScriptMsg;
 use script_runtime::ScriptThreadEventCategory::WebVREvent;
-use script_thread::Runnable;
 use std::cell::Cell;
 use std::mem;
 use std::rc::Rc;
 use std::sync::mpsc;
 use std::thread;
 use webvr_traits::{WebVRDisplayData, WebVRDisplayEvent, WebVRFrameData, WebVRLayer, WebVRMsg};
 
 #[dom_struct]
@@ -506,21 +505,22 @@ impl VRDisplay {
             let (raf_sender, raf_receiver) = mpsc::channel();
             let mut near = near_init;
             let mut far = far_init;
 
             // Initialize compositor
             api_sender.send_vr(WebVRCommand::Create(display_id)).unwrap();
             loop {
                 // Run RAF callbacks on JavaScript thread
-                let msg = box NotifyDisplayRAF {
-                    address: address.clone(),
-                    sender: raf_sender.clone()
-                };
-                js_sender.send(CommonScriptMsg::RunnableMsg(WebVREvent, msg)).unwrap();
+                let this = address.clone();
+                let sender = raf_sender.clone();
+                let task = box task!(handle_vrdisplay_raf: move || {
+                    this.root().handle_raf(&sender);
+                });
+                js_sender.send(CommonScriptMsg::Task(WebVREvent, task)).unwrap();
 
                 // Run Sync Poses in parallell on Render thread
                 let msg = WebVRCommand::SyncPoses(display_id, near, far, sync_sender.clone());
                 api_sender.send_vr(msg).unwrap();
 
                 // Wait until both SyncPoses & RAF ends
                 if let Ok(depth) = raf_receiver.recv().unwrap() {
                     near = depth.0;
@@ -603,29 +603,16 @@ impl VRDisplay {
                 // ExitPresent called or some error ocurred.
                 // Notify VRDisplay RAF thread to stop.
                 end_sender.send(Err(())).unwrap();
             }
         }
     }
 }
 
-struct NotifyDisplayRAF {
-    address: Trusted<VRDisplay>,
-    sender: mpsc::Sender<Result<(f64, f64), ()>>
-}
-
-impl Runnable for NotifyDisplayRAF {
-    fn handler(self: Box<Self>) {
-        let display = self.address.root();
-        display.handle_raf(&self.sender);
-    }
-}
-
-
 // WebVR Spec: If the number of values in the leftBounds/rightBounds arrays
 // is not 0 or 4 for any of the passed layers the promise is rejected
 fn parse_bounds(src: &Option<Vec<Finite<f32>>>, dst: &mut [f32; 4]) -> Result<(), &'static str> {
     match *src {
         Some(ref values) => {
             if values.len() == 0 {
                 return Ok(())
             }
--- a/servo/components/script/dom/websocket.rs
+++ b/servo/components/script/dom/websocket.rs
@@ -26,23 +26,23 @@ use ipc_channel::ipc::{self, IpcReceiver
 use js::jsapi::JSAutoCompartment;
 use js::jsval::UndefinedValue;
 use js::typedarray::{ArrayBuffer, CreateWith};
 use net_traits::{WebSocketCommunicate, WebSocketConnectData, WebSocketDomAction, WebSocketNetworkEvent};
 use net_traits::CoreResourceMsg::WebsocketConnect;
 use net_traits::MessageData;
 use script_runtime::CommonScriptMsg;
 use script_runtime::ScriptThreadEventCategory::WebSocketEvent;
-use script_thread::{Runnable, RunnableWrapper};
 use servo_url::ServoUrl;
 use std::ascii::AsciiExt;
 use std::borrow::ToOwned;
 use std::cell::Cell;
 use std::ptr;
 use std::thread;
+use task::{Task, TaskCanceller};
 use task_source::TaskSource;
 use task_source::networking::NetworkingTaskSource;
 
 #[derive(Clone, Copy, Debug, HeapSizeOf, JSTraceable, PartialEq)]
 enum WebSocketRequestState {
     Connecting = 0,
     Open = 1,
     Closing = 2,
@@ -62,40 +62,44 @@ mod close_code {
     pub const INVALID_PAYLOAD: u16 = 1007;
     pub const POLICY_VIOLATION: u16 = 1008;
     pub const TOO_LARGE: u16 = 1009;
     pub const EXTENSION_MISSING: u16 = 1010;
     pub const INTERNAL_ERROR: u16 = 1011;
     pub const TLS_FAILED: u16 = 1015;
 }
 
-pub fn close_the_websocket_connection(address: Trusted<WebSocket>,
-                                      task_source: &NetworkingTaskSource,
-                                      wrapper: &RunnableWrapper,
-                                      code: Option<u16>,
-                                      reason: String) {
+pub fn close_the_websocket_connection(
+    address: Trusted<WebSocket>,
+    task_source: &NetworkingTaskSource,
+    canceller: &TaskCanceller,
+    code: Option<u16>,
+    reason: String,
+) {
     let close_task = box CloseTask {
         address: address,
         failed: false,
         code: code,
         reason: Some(reason),
     };
-    task_source.queue_with_wrapper(close_task, &wrapper).unwrap();
+    task_source.queue_with_canceller(close_task, &canceller).unwrap();
 }
 
-pub fn fail_the_websocket_connection(address: Trusted<WebSocket>,
-                                     task_source: &NetworkingTaskSource,
-                                     wrapper: &RunnableWrapper) {
+pub fn fail_the_websocket_connection(
+    address: Trusted<WebSocket>,
+    task_source: &NetworkingTaskSource,
+    canceller: &TaskCanceller,
+) {
     let close_task = box CloseTask {
         address: address,
         failed: true,
         code: Some(close_code::ABNORMAL),
         reason: None,
     };
-    task_source.queue_with_wrapper(close_task, &wrapper).unwrap();
+    task_source.queue_with_canceller(close_task, &canceller).unwrap();
 }
 
 #[dom_struct]
 pub struct WebSocket {
     eventtarget: EventTarget,
     url: ServoUrl,
     ready_state: Cell<WebSocketRequestState>,
     buffered_amount: Cell<u64>,
@@ -192,41 +196,41 @@ impl WebSocket {
         };
 
         // Step 8.
         let _ = global.core_resource_thread().send(WebsocketConnect(connect, connect_data));
 
         *ws.sender.borrow_mut() = Some(dom_action_sender);
 
         let task_source = global.networking_task_source();
-        let wrapper = global.get_runnable_wrapper();
+        let canceller = global.task_canceller();
         thread::spawn(move || {
             while let Ok(event) = dom_event_receiver.recv() {
                 match event {
                     WebSocketNetworkEvent::ConnectionEstablished { protocol_in_use } => {
                         let open_thread = box ConnectionEstablishedTask {
                             address: address.clone(),
                             protocol_in_use,
                         };
-                        task_source.queue_with_wrapper(open_thread, &wrapper).unwrap();
+                        task_source.queue_with_canceller(open_thread, &canceller).unwrap();
                     },
                     WebSocketNetworkEvent::MessageReceived(message) => {
                         let message_thread = box MessageReceivedTask {
                             address: address.clone(),
                             message: message,
                         };
-                        task_source.queue_with_wrapper(message_thread, &wrapper).unwrap();
+                        task_source.queue_with_canceller(message_thread, &canceller).unwrap();
                     },
                     WebSocketNetworkEvent::Fail => {
                         fail_the_websocket_connection(address.clone(),
-                            &task_source, &wrapper);
+                            &task_source, &canceller);
                     },
                     WebSocketNetworkEvent::Close(code, reason) => {
                         close_the_websocket_connection(address.clone(),
-                            &task_source, &wrapper, code, reason);
+                            &task_source, &canceller, code, reason);
                     },
                 }
             }
         });
 
         // Step 7.
         Ok(ws)
     }
@@ -256,17 +260,17 @@ impl WebSocket {
             self.clearing_buffer.set(true);
 
             let task = box BufferedAmountTask {
                 address: address,
             };
 
             self.global()
                 .script_chan()
-                .send(CommonScriptMsg::RunnableMsg(WebSocketEvent, task))
+                .send(CommonScriptMsg::Task(WebSocketEvent, task))
                 .unwrap();
         }
 
         Ok(true)
     }
 }
 
 impl WebSocketMethods for WebSocket {
@@ -363,17 +367,17 @@ impl WebSocketMethods for WebSocket {
             WebSocketRequestState::Closing | WebSocketRequestState::Closed  => {} //Do nothing
             WebSocketRequestState::Connecting => { //Connection is not yet established
                 /*By setting the state to closing, the open function
                   will abort connecting the websocket*/
                 self.ready_state.set(WebSocketRequestState::Closing);
 
                 let address = Trusted::new(self);
                 let task_source = self.global().networking_task_source();
-                fail_the_websocket_connection(address, &task_source, &self.global().get_runnable_wrapper());
+                fail_the_websocket_connection(address, &task_source, &self.global().task_canceller());
             }
             WebSocketRequestState::Open => {
                 self.ready_state.set(WebSocketRequestState::Closing);
 
                 // Kick off _Start the WebSocket Closing Handshake_
                 // https://tools.ietf.org/html/rfc6455#section-7.1.2
                 let reason = reason.map(|reason| reason.0);
                 let mut other_sender = self.sender.borrow_mut();
@@ -388,19 +392,19 @@ impl WebSocketMethods for WebSocket {
 
 /// Task queued when *the WebSocket connection is established*.
 /// https://html.spec.whatwg.org/multipage/#feedback-from-the-protocol:concept-websocket-established
 struct ConnectionEstablishedTask {
     address: Trusted<WebSocket>,
     protocol_in_use: Option<String>,
 }
 
-impl Runnable for ConnectionEstablishedTask {
+impl Task for ConnectionEstablishedTask {
     /// https://html.spec.whatwg.org/multipage/#feedback-from-the-protocol:concept-websocket-established
-    fn handler(self: Box<Self>) {
+    fn run(self: Box<Self>) {
         let ws = self.address.root();
 
         // Step 1.
         ws.ready_state.set(WebSocketRequestState::Open);
 
         // Step 2: Extensions.
         // TODO: Set extensions to extensions in use.
 
@@ -413,39 +417,39 @@ impl Runnable for ConnectionEstablishedT
         ws.upcast().fire_event(atom!("open"));
     }
 }
 
 struct BufferedAmountTask {
     address: Trusted<WebSocket>,
 }
 
-impl Runnable for BufferedAmountTask {
+impl Task for BufferedAmountTask {
     // See https://html.spec.whatwg.org/multipage/#dom-websocket-bufferedamount
     //
     // To be compliant with standards, we need to reset bufferedAmount only when the event loop
     // reaches step 1.  In our implementation, the bytes will already have been sent on a background
     // thread.
-    fn handler(self: Box<Self>) {
+    fn run(self: Box<Self>) {
         let ws = self.address.root();
 
         ws.buffered_amount.set(0);
         ws.clearing_buffer.set(false);
     }
 }
 
 struct CloseTask {
     address: Trusted<WebSocket>,
     failed: bool,
     code: Option<u16>,
     reason: Option<String>,
 }
 
-impl Runnable for CloseTask {
-    fn handler(self: Box<Self>) {
+impl Task for CloseTask {
+    fn run(self: Box<Self>) {
         let ws = self.address.root();
 
         if ws.ready_state.get() == WebSocketRequestState::Closed {
             // Do nothing if already closed.
             return;
         }
 
         // Perform _the WebSocket connection is closed_ steps.
@@ -474,19 +478,19 @@ impl Runnable for CloseTask {
     }
 }
 
 struct MessageReceivedTask {
     address: Trusted<WebSocket>,
     message: MessageData,
 }
 
-impl Runnable for MessageReceivedTask {
+impl Task for MessageReceivedTask {
     #[allow(unsafe_code)]
-    fn handler(self: Box<Self>) {
+    fn run(self: Box<Self>) {
         let ws = self.address.root();
         debug!("MessageReceivedTask::handler({:p}): readyState={:?}", &*ws,
                ws.ready_state.get());
 
         // Step 1.
         if ws.ready_state.get() != WebSocketRequestState::Open {
             return;
         }
--- a/servo/components/script/dom/window.rs
+++ b/servo/components/script/dom/window.rs
@@ -71,18 +71,18 @@ use profile_traits::mem::ProfilerChan as
 use profile_traits::time::ProfilerChan as TimeProfilerChan;
 use script_layout_interface::{TrustedNodeAddress, PendingImageState};
 use script_layout_interface::message::{Msg, Reflow, ReflowQueryType, ScriptReflow};
 use script_layout_interface::reporter::CSSErrorReporter;
 use script_layout_interface::rpc::{ContentBoxResponse, ContentBoxesResponse, LayoutRPC};
 use script_layout_interface::rpc::{MarginStyleResponse, NodeScrollRootIdResponse};
 use script_layout_interface::rpc::{ResolvedStyleResponse, TextIndexResponse};
 use script_runtime::{CommonScriptMsg, ScriptChan, ScriptPort, ScriptThreadEventCategory};
-use script_thread::{ImageCacheMsg, MainThreadScriptChan, MainThreadScriptMsg, Runnable};
-use script_thread::{RunnableWrapper, ScriptThread, SendableMainThreadScriptChan};
+use script_thread::{ImageCacheMsg, MainThreadScriptChan, MainThreadScriptMsg};
+use script_thread::{ScriptThread, SendableMainThreadScriptChan};
 use script_traits::{ConstellationControlMsg, DocumentState, LoadData, MozBrowserEvent};
 use script_traits::{ScriptToConstellationChan, ScriptMsg, ScrollState, TimerEvent, TimerEventId};
 use script_traits::{TimerSchedulerMsg, UntrustedNodeAddress, WindowSizeData, WindowSizeType};
 use script_traits::webdriver_msg::{WebDriverJSError, WebDriverJSResult};
 use selectors::attr::CaseSensitivity;
 use servo_config::opts;
 use servo_config::prefs::PREFS;
 use servo_geometry::{f32_rect_to_au_rect, max_rect};
@@ -106,16 +106,17 @@ use style::context::ReflowGoal;
 use style::media_queries;
 use style::parser::ParserContext as CssParserContext;
 use style::properties::PropertyId;
 use style::properties::longhands::overflow_x;
 use style::selector_parser::PseudoElement;
 use style::str::HTML_SPACE_CHARACTERS;
 use style::stylesheets::CssRuleType;
 use style_traits::PARSING_MODE_DEFAULT;
+use task::TaskCanceller;
 use task_source::dom_manipulation::DOMManipulationTaskSource;
 use task_source::file_reading::FileReadingTaskSource;
 use task_source::history_traversal::HistoryTraversalTaskSource;
 use task_source::networking::NetworkingTaskSource;
 use task_source::performance_timeline::PerformanceTimelineTaskSource;
 use task_source::user_interaction::UserInteractionTaskSource;
 use time;
 use timers::{IsInterval, TimerCallback};
@@ -1033,18 +1034,18 @@ impl WindowMethods for Window {
     }
 
     fn TestRunner(&self) -> Root<TestRunner> {
         self.test_runner.or_init(|| TestRunner::new(self.upcast()))
     }
 }
 
 impl Window {
-    pub fn get_runnable_wrapper(&self) -> RunnableWrapper {
-        RunnableWrapper {
+    pub fn task_canceller(&self) -> TaskCanceller {
+        TaskCanceller {
             cancelled: Some(self.ignore_further_async_events.borrow().clone()),
         }
     }
 
     /// Cancels all the tasks associated with that window.
     ///
     /// This sets the current `ignore_further_async_events` sentinel value to
     /// `true` and replaces it with a brand new one for future tasks.
@@ -1963,64 +1964,55 @@ fn debug_reflow_events(id: PipelineId, g
         ReflowReason::IFrameLoadEvent => "\tIFrameLoadEvent",
         ReflowReason::MissingExplicitReflow => "\tMissingExplicitReflow",
         ReflowReason::ElementStateChanged => "\tElementStateChanged",
     });
 
     println!("{}", debug_msg);
 }
 
-struct PostMessageHandler {
-    destination: Trusted<Window>,
-    origin: Option<ImmutableOrigin>,
-    message: StructuredCloneData,
-}
+impl Window {
+    // https://html.spec.whatwg.org/multipage/#dom-window-postmessage step 7.
+    pub fn post_message(
+        &self,
+        target_origin: Option<ImmutableOrigin>,
+        serialize_with_transfer_result: StructuredCloneData,
+    ) {
+        let this = Trusted::new(self);
+        let task = box task!(post_serialised_message: move || {
+            let this = this.root();
+
+            // Step 7.1.
+            if let Some(target_origin) = target_origin {
+                if !target_origin.same_origin(this.Document().origin()) {
+                    return;
+                }
+            }
 
-impl PostMessageHandler {
-    fn new(window: &Window,
-           origin: Option<ImmutableOrigin>,
-           message: StructuredCloneData) -> PostMessageHandler {
-        PostMessageHandler {
-            destination: Trusted::new(window),
-            origin: origin,
-            message: message,
-        }
+            // Steps 7.2.-7.5.
+            let cx = this.get_cx();
+            let obj = this.reflector().get_jsobject();
+            let _ac = JSAutoCompartment::new(cx, obj.get());
+            rooted!(in(cx) let mut message_clone = UndefinedValue());
+            serialize_with_transfer_result.read(
+                this.upcast(),
+                message_clone.handle_mut(),
+            );
+
+            // Step 7.6.
+            // TODO: MessagePort array.
+
+            // Step 7.7.
+            // TODO(#12719): Set the other attributes.
+            MessageEvent::dispatch_jsval(
+                this.upcast(),
+                this.upcast(),
+                message_clone.handle(),
+            );
+        });
+        // FIXME(nox): Why are errors silenced here?
+        // TODO(#12718): Use the "posted message task source".
+        let _ = self.script_chan.send(CommonScriptMsg::Task(
+            ScriptThreadEventCategory::DomEvent,
+            self.task_canceller().wrap_task(task),
+        ));
     }
 }
-
-impl Runnable for PostMessageHandler {
-    // https://html.spec.whatwg.org/multipage/#dom-window-postmessage steps 10-12.
-    fn handler(self: Box<PostMessageHandler>) {
-        let this = *self;
-        let window = this.destination.root();
-
-        // Step 10.
-        let doc = window.Document();
-        if let Some(source) = this.origin {
-            if !source.same_origin(doc.origin()) {
-                return;
-            }
-        }
-
-        let cx = window.get_cx();
-        let globalhandle = window.reflector().get_jsobject();
-        let _ac = JSAutoCompartment::new(cx, globalhandle.get());
-
-        rooted!(in(cx) let mut message = UndefinedValue());
-        this.message.read(window.upcast(), message.handle_mut());
-
-        // Step 11-12.
-        // TODO(#12719): set the other attributes.
-        MessageEvent::dispatch_jsval(window.upcast(),
-                                     window.upcast(),
-                                     message.handle());
-    }
-}
-
-impl Window {
-    pub fn post_message(&self, origin: Option<ImmutableOrigin>, data: StructuredCloneData) {
-        let runnable = PostMessageHandler::new(self, origin, data);
-        let runnable = self.get_runnable_wrapper().wrap_runnable(box runnable);
-        let msg = CommonScriptMsg::RunnableMsg(ScriptThreadEventCategory::DomEvent, runnable);
-        // TODO(#12718): Use the "posted message task source".
-        let _ = self.script_chan.send(msg);
-    }
-}
--- a/servo/components/script/dom/worker.rs
+++ b/servo/components/script/dom/worker.rs
@@ -2,40 +2,38 @@
  * 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::{DevtoolsPageInfo, ScriptToDevtoolsControlMsg};
 use dom::abstractworker::{SharedRt, SimpleWorkerErrorHandler};
 use dom::abstractworker::WorkerScriptMsg;
 use dom::bindings::codegen::Bindings::WorkerBinding;
 use dom::bindings::codegen::Bindings::WorkerBinding::WorkerMethods;
-use dom::bindings::error::{Error, ErrorResult, Fallible, ErrorInfo};
+use dom::bindings::error::{Error, ErrorResult, Fallible};
 use dom::bindings::inheritance::Castable;
 use dom::bindings::js::Root;
 use dom::bindings::refcounted::Trusted;
 use dom::bindings::reflector::{DomObject, reflect_dom_object};
 use dom::bindings::str::DOMString;
 use dom::bindings::structuredclone::StructuredCloneData;
 use dom::dedicatedworkerglobalscope::DedicatedWorkerGlobalScope;
-use dom::errorevent::ErrorEvent;
-use dom::event::{Event, EventBubbles, EventCancelable, EventStatus};
 use dom::eventtarget::EventTarget;
 use dom::globalscope::GlobalScope;
 use dom::messageevent::MessageEvent;
 use dom::workerglobalscope::prepare_workerscope_init;
 use dom_struct::dom_struct;
 use ipc_channel::ipc;
-use js::jsapi::{HandleValue, JSAutoCompartment, JSContext, NullHandleValue};
+use js::jsapi::{HandleValue, JSAutoCompartment, JSContext};
 use js::jsval::UndefinedValue;
-use script_thread::Runnable;
 use script_traits::WorkerScriptLoadOrigin;
 use std::cell::Cell;
 use std::sync::{Arc, Mutex};
 use std::sync::atomic::{AtomicBool, Ordering};
 use std::sync::mpsc::{Sender, channel};
+use task::Task;
 
 pub type TrustedWorkerAddress = Trusted<Worker>;
 
 // https://html.spec.whatwg.org/multipage/#worker
 #[dom_struct]
 pub struct Worker {
     eventtarget: EventTarget,
     #[ignore_heap_size_of = "Defined in std"]
@@ -134,37 +132,16 @@ impl Worker {
         data.read(&global, message.handle_mut());
         MessageEvent::dispatch_jsval(target, &global, message.handle());
     }
 
     pub fn dispatch_simple_error(address: TrustedWorkerAddress) {
         let worker = address.root();
         worker.upcast().fire_event(atom!("error"));
     }
-
-    #[allow(unsafe_code)]
-    fn dispatch_error(&self, error_info: ErrorInfo) {
-        let global = self.global();
-        let event = ErrorEvent::new(&global,
-                                    atom!("error"),
-                                    EventBubbles::DoesNotBubble,
-                                    EventCancelable::Cancelable,
-                                    error_info.message.as_str().into(),
-                                    error_info.filename.as_str().into(),
-                                    error_info.lineno,
-                                    error_info.column,
-                                    unsafe { NullHandleValue });
-
-        let event_status = event.upcast::<Event>().fire(self.upcast::<EventTarget>());
-        if event_status == EventStatus::Canceled {
-            return;
-        }
-
-        global.report_an_error(error_info, unsafe { NullHandleValue });
-    }
 }
 
 impl WorkerMethods for Worker {
     #[allow(unsafe_code)]
     // https://html.spec.whatwg.org/multipage/#dom-worker-postmessage
     unsafe fn PostMessage(&self, cx: *mut JSContext, message: HandleValue) -> ErrorResult {
         let data = StructuredCloneData::write(cx, message)?;
         let address = Trusted::new(self);
@@ -193,57 +170,15 @@ impl WorkerMethods for Worker {
 
     // https://html.spec.whatwg.org/multipage/#handler-worker-onmessage
     event_handler!(message, GetOnmessage, SetOnmessage);
 
     // https://html.spec.whatwg.org/multipage/#handler-workerglobalscope-onerror
     event_handler!(error, GetOnerror, SetOnerror);
 }
 
-pub struct WorkerMessageHandler {
-    addr: TrustedWorkerAddress,
-    data: StructuredCloneData,
-}
-
-impl WorkerMessageHandler {
-    pub fn new(addr: TrustedWorkerAddress, data: StructuredCloneData) -> WorkerMessageHandler {
-        WorkerMessageHandler {
-            addr: addr,
-            data: data,
-        }
-    }
-}
-
-impl Runnable for WorkerMessageHandler {
-    fn handler(self: Box<WorkerMessageHandler>) {
-        let this = *self;
-        Worker::handle_message(this.addr, this.data);
-    }
-}
-
-impl Runnable for SimpleWorkerErrorHandler<Worker> {
+impl Task for SimpleWorkerErrorHandler<Worker> {
     #[allow(unrooted_must_root)]
-    fn handler(self: Box<SimpleWorkerErrorHandler<Worker>>) {
+    fn run(self: Box<Self>) {
         let this = *self;
         Worker::dispatch_simple_error(this.addr);
     }
 }
-
-pub struct WorkerErrorHandler {
-    address: Trusted<Worker>,
-    error_info: ErrorInfo,
-}
-
-impl WorkerErrorHandler {
-    pub fn new(address: Trusted<Worker>, error_info: ErrorInfo) -> WorkerErrorHandler {
-        WorkerErrorHandler {
-            address: address,
-            error_info: error_info,
-        }
-    }
-}
-
-impl Runnable for WorkerErrorHandler {
-    fn handler(self: Box<Self>) {
-        let this = *self;
-        this.address.root().dispatch_error(this.error_info);
-    }
-}
--- a/servo/components/script/dom/workerglobalscope.rs
+++ b/servo/components/script/dom/workerglobalscope.rs
@@ -28,25 +28,25 @@ use fetch;
 use ipc_channel::ipc::IpcSender;
 use js::jsapi::{HandleValue, JSAutoCompartment, JSContext, JSRuntime};
 use js::jsval::UndefinedValue;
 use js::panic::maybe_resume_unwind;
 use js::rust::Runtime;
 use net_traits::{IpcSend, load_whole_resource};
 use net_traits::request::{CredentialsMode, Destination, RequestInit as NetRequestInit, Type as RequestType};
 use script_runtime::{CommonScriptMsg, ScriptChan, ScriptPort, get_reports};
-use script_thread::RunnableWrapper;
 use script_traits::{TimerEvent, TimerEventId};
 use script_traits::WorkerGlobalScopeInit;
 use servo_url::{MutableOrigin, ServoUrl};
 use std::default::Default;
 use std::rc::Rc;
 use std::sync::Arc;
 use std::sync::atomic::{AtomicBool, Ordering};
 use std::sync::mpsc::Receiver;
+use task::TaskCanceller;
 use task_source::file_reading::FileReadingTaskSource;
 use task_source::networking::NetworkingTaskSource;
 use task_source::performance_timeline::PerformanceTimelineTaskSource;
 use time::precise_time_ns;
 use timers::{IsInterval, TimerCallback};
 
 pub fn prepare_workerscope_init(global: &GlobalScope,
                                 devtools_sender: Option<IpcSender<DevtoolScriptControlMsg>>) -> WorkerGlobalScopeInit {
@@ -156,18 +156,18 @@ impl WorkerGlobalScope {
     pub fn get_url(&self) -> &ServoUrl {
         &self.worker_url
     }
 
     pub fn get_worker_id(&self) -> WorkerId {
         self.worker_id.clone()
     }
 
-    pub fn get_runnable_wrapper(&self) -> RunnableWrapper {
-        RunnableWrapper {
+    pub fn task_canceller(&self) -> TaskCanceller {
+        TaskCanceller {
             cancelled: self.closing.clone(),
         }
     }
 }
 
 impl WorkerGlobalScopeMethods for WorkerGlobalScope {
     // https://html.spec.whatwg.org/multipage/#dom-workerglobalscope-self
     fn Self_(&self) -> Root<WorkerGlobalScope> {
@@ -382,18 +382,18 @@ impl WorkerGlobalScope {
             return dedicated.new_script_pair();
         } else {
             panic!("need to implement a sender for SharedWorker/ServiceWorker")
         }
     }
 
     pub fn process_event(&self, msg: CommonScriptMsg) {
         match msg {
-            CommonScriptMsg::RunnableMsg(_, runnable) => {
-                runnable.handler()
+            CommonScriptMsg::Task(_, task) => {
+                task.run()
             },
             CommonScriptMsg::CollectReports(reports_chan) => {
                 let cx = self.get_cx();
                 let path_seg = format!("url({})", self.get_url());
                 let reports = get_reports(cx, path_seg);
                 reports_chan.send(reports);
             },
         }
--- a/servo/components/script/dom/worklet.rs
+++ b/servo/components/script/dom/worklet.rs
@@ -46,19 +46,17 @@ 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 script_thread::{MainThreadScriptMsg, 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;
@@ -66,16 +64,17 @@ 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 task::Task;
 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
@@ -595,27 +594,27 @@ impl WorkletThread {
         // https://github.com/w3c/css-houdini-drafts/issues/407
         let ok = script.map(|script| global_scope.evaluate_js(&*script)).unwrap_or(false);
 
         if !ok {
             // Step 3.
             debug!("Failed to load script.");
             let old_counter = pending_tasks_struct.set_counter_to(-1);
             if old_counter > 0 {
-                self.run_in_script_thread(promise.reject_runnable(Error::Abort));
+                self.run_in_script_thread(promise.reject_task(Error::Abort));
             }
         } else {
             // Step 5.
             debug!("Finished adding script.");
             let old_counter = pending_tasks_struct.decrement_counter_by(1);
             if old_counter == 1 {
                 debug!("Resolving promise.");
                 let msg = MainThreadScriptMsg::WorkletLoaded(pipeline_id);
                 self.global_init.to_script_thread_sender.send(msg).expect("Worklet thread outlived script thread.");
-                self.run_in_script_thread(promise.resolve_runnable(()));
+                self.run_in_script_thread(promise.resolve_task(()));
             }
         }
     }
 
     /// Perform a task.
     fn perform_a_worklet_task(&self, worklet_id: WorkletId, task: WorkletTask) {
         match self.global_scopes.get(&worklet_id) {
             Some(global) => global.perform_a_worklet_task(task),
@@ -640,21 +639,21 @@ impl WorkletThread {
                                                        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,
+    /// Run a task in the main script thread.
+    fn run_in_script_thread<T>(&self, task: T) where
+        T: 'static + Send + Task,
     {
-        let msg = CommonScriptMsg::RunnableMsg(ScriptThreadEventCategory::WorkletEvent, box runnable);
+        let msg = CommonScriptMsg::Task(ScriptThreadEventCategory::WorkletEvent, box task);
         let msg = MainThreadScriptMsg::Common(msg);
         self.global_init.to_script_thread_sender.send(msg).expect("Worklet thread outlived script thread.");
     }
 }
 
 /// An executor of worklet tasks
 #[derive(Clone, HeapSizeOf, JSTraceable)]
 pub struct WorkletExecutor {
--- a/servo/components/script/dom/workletglobalscope.rs
+++ b/servo/components/script/dom/workletglobalscope.rs
@@ -17,25 +17,20 @@ use ipc_channel::ipc::IpcSender;
 use js::jsapi::JSContext;
 use js::jsval::UndefinedValue;
 use js::rust::Runtime;
 use msg::constellation_msg::PipelineId;
 use net_traits::ResourceThreads;
 use net_traits::image_cache::ImageCache;
 use profile_traits::mem;
 use profile_traits::time;
-use script_layout_interface::message::Msg;
-use script_runtime::CommonScriptMsg;
-use script_runtime::ScriptThreadEventCategory;
 use script_thread::MainThreadScriptMsg;
-use script_thread::Runnable;
-use script_thread::ScriptThread;
-use script_traits::ScriptMsg;
-use script_traits::ScriptToConstellationChan;
-use script_traits::TimerSchedulerMsg;
+use script_traits::{Painter, ScriptMsg};
+use script_traits::{ScriptToConstellationChan, TimerSchedulerMsg};
+use servo_atoms::Atom;
 use servo_url::ImmutableOrigin;
 use servo_url::MutableOrigin;
 use servo_url::ServoUrl;
 use std::sync::Arc;
 use std::sync::mpsc::Sender;
 
 #[dom_struct]
 /// https://drafts.css-houdini.org/worklets/#workletglobalscope
@@ -91,35 +86,31 @@ impl WorkletGlobalScope {
 
     /// 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())
     }
 
-    /// Run a runnable in the main script thread.
-    pub 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.to_script_thread_sender.send(msg).expect("Worklet thread outlived script thread.");
-    }
-
-    /// Send a message to layout.
-    pub fn send_to_layout(&self, msg: Msg) {
-        struct RunnableMsg(PipelineId, Msg);
-        impl Runnable for RunnableMsg {
-            fn main_thread_handler(self: Box<Self>, script_thread: &ScriptThread) {
-                script_thread.send_to_layout(self.0, self.1);
-            }
-        }
-        let pipeline_id = self.globalscope.pipeline_id();
-        self.run_in_script_thread(RunnableMsg(pipeline_id, msg));
+    /// Register a paint worklet to the script thread.
+    pub fn register_paint_worklet(
+        &self,
+        name: Atom,
+        properties: Vec<Atom>,
+        painter: Box<Painter>,
+    ) {
+        self.to_script_thread_sender
+            .send(MainThreadScriptMsg::RegisterPaintWorklet {
+                pipeline_id: self.globalscope.pipeline_id(),
+                name,
+                properties,
+                painter,
+            })
+            .expect("Worklet thread outlived script thread.");
     }
 
     /// The base URL of this global.
     pub fn base_url(&self) -> ServoUrl {
         self.base_url.clone()
     }
 
     /// The worklet executor.
--- a/servo/components/script/dom/xmlhttprequest.rs
+++ b/servo/components/script/dom/xmlhttprequest.rs
@@ -256,17 +256,17 @@ impl XMLHttpRequest {
                 self.xhr.root().generation_id.get() == self.gen_id
             }
         }
 
         let (action_sender, action_receiver) = ipc::channel().unwrap();
         let listener = NetworkListener {
             context: context,
             task_source: task_source,
-            wrapper: Some(global.get_runnable_wrapper())
+            canceller: Some(global.task_canceller())
         };
         ROUTER.add_route(action_receiver.to_opaque(), box move |message| {
             listener.notify_fetch(message.to().unwrap());
         });
         global.core_resource_thread().send(Fetch(init, action_sender)).unwrap();
     }
 }
 
--- a/servo/components/script/fetch.rs
+++ b/servo/components/script/fetch.rs
@@ -97,17 +97,17 @@ pub fn Fetch(global: &GlobalScope, input
     let fetch_context = Arc::new(Mutex::new(FetchContext {
         fetch_promise: Some(TrustedPromise::new(promise.clone())),
         response_object: Trusted::new(&*response),
         body: vec![],
     }));
     let listener = NetworkListener {
         context: fetch_context,
         task_source: global.networking_task_source(),
-        wrapper: Some(global.get_runnable_wrapper())
+        canceller: Some(global.task_canceller())
     };
 
     ROUTER.add_route(action_receiver.to_opaque(), box move |message| {
         listener.notify_fetch(message.to().unwrap());
     });
     core_resource_thread.send(NetTraitsFetch(request_init, action_sender)).unwrap();
 
     promise
--- a/servo/components/script/layout_image.rs
+++ b/servo/components/script/layout_image.rs
@@ -57,17 +57,17 @@ pub fn fetch_image_for_layout(url: Servo
 
     let document = document_from_node(node);
     let window = document.window();
 
     let (action_sender, action_receiver) = ipc::channel().unwrap();
     let listener = NetworkListener {
         context: context,
         task_source: window.networking_task_source(),
-        wrapper: Some(window.get_runnable_wrapper()),
+        canceller: Some(window.task_canceller()),
     };
     ROUTER.add_route(action_receiver.to_opaque(), box move |message| {
         listener.notify_fetch(message.to().unwrap());
     });
 
     let request = FetchRequestInit {
         url: url,
         origin: document.origin().immutable().clone(),
--- a/servo/components/script/lib.rs
+++ b/servo/components/script/lib.rs
@@ -103,16 +103,19 @@ extern crate tinyfiledialogs;
 extern crate unicode_segmentation;
 extern crate url;
 extern crate utf8;
 extern crate uuid;
 extern crate webrender_api;
 extern crate webvr_traits;
 extern crate xml5ever;
 
+#[macro_use]
+mod task;
+
 mod body;
 pub mod clipboard_provider;
 mod devtools;
 pub mod document_loader;
 #[macro_use]
 mod dom;
 pub mod fetch;
 mod layout_image;
--- a/servo/components/script/network_listener.rs
+++ b/servo/components/script/network_listener.rs
@@ -1,66 +1,70 @@
 /* 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 net_traits::{Action, FetchResponseListener, FetchResponseMsg};
-use script_thread::{Runnable, RunnableWrapper};
 use std::sync::{Arc, Mutex};
+use task::{Task, TaskCanceller};
 use task_source::TaskSource;
 use task_source::networking::NetworkingTaskSource;
 
-/// An off-thread sink for async network event runnables. All such events are forwarded to
+/// An off-thread sink for async network event tasks. All such events are forwarded to
 /// a target thread, where they are invoked on the provided context object.
 pub struct NetworkListener<Listener: PreInvoke + Send + 'static> {
     pub context: Arc<Mutex<Listener>>,
     pub task_source: NetworkingTaskSource,
-    pub wrapper: Option<RunnableWrapper>,
+    pub canceller: Option<TaskCanceller>,
 }
 
 impl<Listener: PreInvoke + Send + 'static> NetworkListener<Listener> {
     pub fn notify<A: Action<Listener> + Send + 'static>(&self, action: A) {
-        let runnable = box ListenerRunnable {
+        let task = box ListenerTask {
             context: self.context.clone(),
             action: action,
         };
-        let result = if let Some(ref wrapper) = self.wrapper {
-            self.task_source.queue_with_wrapper(runnable, wrapper)
+        let result = if let Some(ref canceller) = self.canceller {
+            self.task_source.queue_with_canceller(task, canceller)
         } else {
-            self.task_source.queue_wrapperless(runnable)
+            self.task_source.queue_unconditionally(task)
         };
         if let Err(err) = result {
             warn!("failed to deliver network data: {:?}", err);
         }
     }
 }
 
 // helps type inference
 impl<Listener: FetchResponseListener + PreInvoke + Send + 'static> NetworkListener<Listener> {
     pub fn notify_fetch(&self, action: FetchResponseMsg) {
         self.notify(action);
     }
 }
 
-/// A gating mechanism that runs before invoking the runnable on the target thread.
-/// If the `should_invoke` method returns false, the runnable is discarded without
+/// A gating mechanism that runs before invoking the task on the target thread.
+/// If the `should_invoke` method returns false, the task is discarded without
 /// being invoked.
 pub trait PreInvoke {
     fn should_invoke(&self) -> bool {
         true
     }
 }
 
-/// A runnable for moving the async network events between threads.
-struct ListenerRunnable<A: Action<Listener> + Send + 'static, Listener: PreInvoke + Send> {
+/// A task for moving the async network events between threads.
+struct ListenerTask<A: Action<Listener> + Send + 'static, Listener: PreInvoke + Send> {
     context: Arc<Mutex<Listener>>,
     action: A,
 }
 
-impl<A: Action<Listener> + Send + 'static, Listener: PreInvoke + Send> Runnable for ListenerRunnable<A, Listener> {
-    fn handler(self: Box<ListenerRunnable<A, Listener>>) {
+impl<A, Listener> Task for ListenerTask<A, Listener>
+where
+    A: Action<Listener> + Send + 'static,
+    Listener: PreInvoke + Send,
+{
+    fn run(self: Box<Self>) {
         let this = *self;
         let mut context = this.context.lock().unwrap();
         if context.should_invoke() {
             this.action.process(&mut *context);
         }
     }
 }
--- a/servo/components/script/script_runtime.rs
+++ b/servo/components/script/script_runtime.rs
@@ -18,44 +18,47 @@ use js::jsapi::{JSContext, JS_GetRuntime
 use js::jsapi::{JSGCInvocationKind, JSGCStatus, JS_AddExtraGCRootsTracer, JS_SetGCCallback};
 use js::jsapi::{JSGCMode, JSGCParamKey, JS_SetGCParameter, JS_SetGlobalJitCompilerOption};
 use js::jsapi::{JSJitCompilerOption, JS_SetOffthreadIonCompilationEnabled, JS_SetParallelParsingEnabled};
 use js::jsapi::{JSObject, RuntimeOptionsRef, SetPreserveWrapperCallback, SetEnqueuePromiseJobCallback};
 use js::panic::wrap_panic;
 use js::rust::Runtime;
 use microtask::{EnqueuedPromiseCallback, Microtask};
 use profile_traits::mem::{Report, ReportKind, ReportsChan};
-use script_thread::{Runnable, STACK_ROOTS, trace_thread};
+use script_thread::{STACK_ROOTS, trace_thread};
 use servo_config::opts;
 use servo_config::prefs::PREFS;
 use std::cell::Cell;
 use std::fmt;
 use std::io::{Write, stdout};
 use std::marker::PhantomData;
 use std::os;
 use std::os::raw::c_void;
 use std::panic::AssertUnwindSafe;
 use std::ptr;
 use style::thread_state;
+use task::Task;
 use time::{Tm, now};
 
 /// Common messages used to control the event loops in both the script and the worker
 pub enum CommonScriptMsg {
     /// Requests that the script thread measure its memory usage. The results are sent back via the
     /// supplied channel.
     CollectReports(ReportsChan),
     /// Generic message that encapsulates event handling.
-    RunnableMsg(ScriptThreadEventCategory, Box<Runnable + Send>),
+    Task(ScriptThreadEventCategory, Box<Task + Send>),
 }
 
 impl fmt::Debug for CommonScriptMsg {
     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
         match *self {
             CommonScriptMsg::CollectReports(_) => write!(f, "CollectReports(...)"),
-            CommonScriptMsg::RunnableMsg(category, _) => write!(f, "RunnableMsg({:?}, ...)", category),
+            CommonScriptMsg::Task(ref category, ref task) => {
+                f.debug_tuple("Task").field(category).field(task).finish()
+            },
         }
     }
 }
 
 /// A cloneable interface for communicating with an event loop.
 pub trait ScriptChan: JSTraceable {
     /// Send a message to the associated event loop.
     fn send(&self, msg: CommonScriptMsg) -> Result<(), ()>;
--- a/servo/components/script/script_thread.rs
+++ b/servo/components/script/script_thread.rs
@@ -82,39 +82,40 @@ use net_traits::{Metadata, NetworkError,
 use net_traits::image_cache::{ImageCache, PendingImageResponse};
 use net_traits::request::{CredentialsMode, Destination, RedirectMode, RequestInit};
 use net_traits::storage_thread::StorageType;
 use profile_traits::mem::{self, OpaqueSender, Report, ReportKind, ReportsChan};
 use profile_traits::time::{self, ProfilerCategory, profile};
 use script_layout_interface::message::{self, Msg, NewLayoutThreadInfo, ReflowQueryType};
 use script_runtime::{CommonScriptMsg, ScriptChan, ScriptThreadEventCategory};
 use script_runtime::{ScriptPort, StackRootTLS, get_reports, new_rt_and_cx};
-use script_traits::{CompositorEvent, ConstellationControlMsg, PaintMetricType};
-use script_traits::{DocumentActivity, DiscardBrowsingContext, EventResult};
-use script_traits::{InitialScriptState, JsEvalResult, LayoutMsg, LoadData, MouseButton, MouseEventType};
-use script_traits::{MozBrowserEvent, NewLayoutInfo, ScriptToConstellationChan, ScriptMsg, UpdatePipelineIdReason};
-use script_traits::{ScriptThreadFactory, TimerEvent, TimerSchedulerMsg, TimerSource};
-use script_traits::{TouchEventType, TouchId, UntrustedNodeAddress, WindowSizeData, WindowSizeType};
+use script_traits::{CompositorEvent, ConstellationControlMsg};
+use script_traits::{DiscardBrowsingContext, DocumentActivity, EventResult};
+use script_traits::{InitialScriptState, JsEvalResult, LayoutMsg, LoadData};
+use script_traits::{MouseButton, MouseEventType, MozBrowserEvent, NewLayoutInfo};
+use script_traits::{PaintMetricType, Painter, ScriptMsg, ScriptThreadFactory};
+use script_traits::{ScriptToConstellationChan, TimerEvent, TimerSchedulerMsg};
+use script_traits::{TimerSource, TouchEventType, TouchId, UntrustedNodeAddress};
+use script_traits::{UpdatePipelineIdReason, WindowSizeData, WindowSizeType};
 use script_traits::CompositorEvent::{KeyEvent, MouseButtonEvent, MouseMoveEvent, ResizeEvent};
 use script_traits::CompositorEvent::{TouchEvent, TouchpadPressureEvent};
 use script_traits::webdriver_msg::WebDriverScriptCommand;
-use serviceworkerjob::{Job, JobQueue, AsyncJobHandler};
+use serviceworkerjob::{Job, JobQueue};
+use servo_atoms::Atom;
 use servo_config::opts;
 use servo_url::{ImmutableOrigin, MutableOrigin, ServoUrl};
 use std::cell::Cell;
 use std::collections::{hash_map, HashMap, HashSet};
 use std::default::Default;
-use std::intrinsics;
 use std::ops::Deref;
 use std::option::Option;
 use std::ptr;
 use std::rc::Rc;
 use std::result::Result;
 use std::sync::Arc;
-use std::sync::atomic::{AtomicBool, Ordering};
 use std::sync::mpsc::{Receiver, Select, Sender, channel};
 use std::thread;
 use style::context::ReflowGoal;
 use style::thread_state;
 use task_source::dom_manipulation::DOMManipulationTaskSource;
 use task_source::file_reading::FileReadingTaskSource;
 use task_source::history_traversal::HistoryTraversalTaskSource;
 use task_source::networking::NetworkingTaskSource;
@@ -197,96 +198,49 @@ impl InProgressLoad {
             url: url,
             origin: origin,
             navigation_start: (current_time.sec * 1000 + current_time.nsec as i64 / 1000000) as u64,
             navigation_start_precise: navigation_start_precise,
         }
     }
 }
 
-/// Encapsulated state required to create cancellable runnables from non-script threads.
-pub struct RunnableWrapper {
-    pub cancelled: Option<Arc<AtomicBool>>,
-}
-
-impl RunnableWrapper {
-    pub fn wrap_runnable<T: Runnable + Send + 'static>(&self, runnable: Box<T>) -> Box<Runnable + Send> {
-        box CancellableRunnable {
-            cancelled: self.cancelled.clone(),
-            inner: runnable,
-        }
-    }
-}
-
-/// A runnable that can be discarded by toggling a shared flag.
-pub struct CancellableRunnable<T: Runnable + Send> {
-    cancelled: Option<Arc<AtomicBool>>,
-    inner: Box<T>,
-}
-
-impl<T> CancellableRunnable<T>
-where
-    T: Runnable + Send,
-{
-    fn is_cancelled(&self) -> bool {
-        self.cancelled.as_ref().map_or(false, |cancelled| {
-            cancelled.load(Ordering::SeqCst)
-        })
-    }
-}
-
-impl<T> Runnable for CancellableRunnable<T>
-where
-    T: Runnable + Send,
-{
-    fn main_thread_handler(self: Box<CancellableRunnable<T>>, script_thread: &ScriptThread) {
-        if !self.is_cancelled() {
-            self.inner.main_thread_handler(script_thread);
-        }
-    }
-
-    fn handler(self: Box<CancellableRunnable<T>>) {
-        if !self.is_cancelled() {
-            self.inner.handler()
-        }
-    }
-}
-
-pub trait Runnable {
-    fn name(&self) -> &'static str { unsafe { intrinsics::type_name::<Self>() } }
-    fn handler(self: Box<Self>) {
-        panic!("This should probably be redefined.")
-    }
-    fn main_thread_handler(self: Box<Self>, _script_thread: &ScriptThread) { self.handler(); }
-}
-
 #[derive(Debug)]
 enum MixedMessage {
     FromConstellation(ConstellationControlMsg),
     FromScript(MainThreadScriptMsg),
     FromDevtools(DevtoolScriptControlMsg),
     FromImageCache((PipelineId, PendingImageResponse)),
     FromScheduler(TimerEvent),
 }
 
-/// Messages used to control the script event loop
+/// Messages used to control the script event loop.
 #[derive(Debug)]
 pub enum MainThreadScriptMsg {
     /// Common variants associated with the script messages
     Common(CommonScriptMsg),
     /// Notifies the script that a window associated with a particular pipeline
     /// should be closed (only dispatched to ScriptThread).
     ExitWindow(PipelineId),
     /// Begins a content-initiated load on the specified pipeline (only
     /// dispatched to ScriptThread). Allows for a replace bool to be passed. If true,
     /// the current entry will be replaced instead of a new entry being added.
     Navigate(PipelineId, LoadData, bool),
     /// Notifies the script thread that a new worklet has been loaded, and thus the page should be
     /// reflowed.
     WorkletLoaded(PipelineId),
+    /// Notifies the script thread that a new paint worklet has been registered.
+    RegisterPaintWorklet {
+        pipeline_id: PipelineId,
+        name: Atom,
+        properties: Vec<Atom>,
+        painter: Box<Painter>
+    },
+    /// Dispatches a job queue.
+    DispatchJobQueue { scope_url: ServoUrl },
 }
 
 impl OpaqueSender<CommonScriptMsg> for Box<ScriptChan + Send> {
     fn send(&self, msg: CommonScriptMsg) {
         ScriptChan::send(&**self, msg).unwrap();
     }
 }
 
@@ -689,21 +643,21 @@ impl ScriptThread {
                                   -> Option<Root<ServoParser>> {
         SCRIPT_THREAD_ROOT.with(|root| {
             let script_thread = unsafe { &*root.get().unwrap() };
             script_thread.handle_page_headers_available(id, metadata)
         })
     }
 
     #[allow(unrooted_must_root)]
-    pub fn schedule_job(job: Job, global: &GlobalScope) {
+    pub fn schedule_job(job: Job) {
         SCRIPT_THREAD_ROOT.with(|root| {
             let script_thread = unsafe { &*root.get().unwrap() };
             let job_queue = &*script_thread.job_queue_map;
-            job_queue.schedule_job(job, global, &script_thread);
+            job_queue.schedule_job(job, &script_thread);
         });
     }
 
     pub fn process_event(msg: CommonScriptMsg) {
         SCRIPT_THREAD_ROOT.with(|root| {
             if let Some(script_thread) = root.get() {
                 let script_thread = unsafe { &*script_thread };
                 script_thread.handle_msg_from_script(MainThreadScriptMsg::Common(msg));
@@ -761,23 +715,31 @@ impl ScriptThread {
                     scheduler_chan: script_thread.scheduler_chan.clone(),
                     image_cache: script_thread.image_cache.clone(),
                 };
                 Rc::new(WorkletThreadPool::spawn(init))
             }).clone()
         })
     }
 
-    pub fn send_to_layout(&self, pipeline_id: PipelineId, msg: Msg) {
+    fn handle_register_paint_worklet(
+        &self,
+        pipeline_id: PipelineId,
+        name: Atom,
+        properties: Vec<Atom>,
+        painter: Box<Painter>,
+    ) {
         let window = self.documents.borrow().find_window(pipeline_id);
         let window = match window {
             Some(window) => window,
-            None => return warn!("Message sent to layout after pipeline {} closed.", pipeline_id),
+            None => return warn!("Paint worklet registered after pipeline {} closed.", pipeline_id),
         };
-        let _ = window.layout_chan().send(msg);
+        let _ = window.layout_chan().send(
+            Msg::RegisterPaint(name, properties, painter),
+        );
     }
 
     pub fn push_new_element_queue() {
         SCRIPT_THREAD_ROOT.with(|root| {
             if let Some(script_thread) = root.get() {
                 let script_thread = unsafe { &*script_thread };
                 script_thread.custom_element_reaction_stack.push_new_element_queue();
             }
@@ -1155,19 +1117,23 @@ impl ScriptThread {
                         ScriptThreadEventCategory::DomEvent,
                     _ => ScriptThreadEventCategory::ConstellationMsg
                 }
             },
             MixedMessage::FromDevtools(_) => ScriptThreadEventCategory::DevtoolsMsg,
             MixedMessage::FromImageCache(_) => ScriptThreadEventCategory::ImageCacheMsg,
             MixedMessage::FromScript(ref inner_msg) => {
                 match *inner_msg {
-                    MainThreadScriptMsg::Common(CommonScriptMsg::RunnableMsg(ref category, _)) =>
-                        *category,
-                    _ => ScriptThreadEventCategory::ScriptEvent
+                    MainThreadScriptMsg::Common(CommonScriptMsg::Task(category, _)) => {
+                        category
+                    },
+                    MainThreadScriptMsg::RegisterPaintWorklet { .. } => {
+                        ScriptThreadEventCategory::WorkletEvent
+                    },
+                    _ => ScriptThreadEventCategory::ScriptEvent,
                 }
             },
             MixedMessage::FromScheduler(_) => ScriptThreadEventCategory::TimerEvent
         }
     }
 
     fn profile_event<F, R>(&self, category: ScriptThreadEventCategory, f: F) -> R
         where F: FnOnce() -> R {
@@ -1286,25 +1252,41 @@ impl ScriptThread {
     fn handle_msg_from_script(&self, msg: MainThreadScriptMsg) {
         match msg {
             MainThreadScriptMsg::Navigate(parent_pipeline_id, load_data, replace) => {
                 self.handle_navigate(parent_pipeline_id, None, load_data, replace)
             },
             MainThreadScriptMsg::ExitWindow(id) => {
                 self.handle_exit_window_msg(id)
             },
-            MainThreadScriptMsg::Common(CommonScriptMsg::RunnableMsg(_, runnable)) => {
-                runnable.main_thread_handler(self)
+            MainThreadScriptMsg::Common(CommonScriptMsg::Task(_, task)) => {
+                task.run()
             }
             MainThreadScriptMsg::Common(CommonScriptMsg::CollectReports(chan)) => {
                 self.collect_reports(chan)
             },
             MainThreadScriptMsg::WorkletLoaded(pipeline_id) => {
                 self.handle_worklet_loaded(pipeline_id)
             },
+            MainThreadScriptMsg::RegisterPaintWorklet {
+                pipeline_id,
+                name,
+                properties,
+                painter,
+            } => {
+                self.handle_register_paint_worklet(
+                    pipeline_id,
+                    name,
+                    properties,
+                    painter,
+                )
+            },
+            MainThreadScriptMsg::DispatchJobQueue { scope_url } => {
+                self.job_queue_map.run_job(scope_url, self)
+            }
         }
     }
 
     fn handle_timer_event(&self, timer_event: TimerEvent) {
         let TimerEvent(source, id) = timer_event;
 
         let pipeline_id = match source {
             TimerSource::FromWindow(pipeline_id) => pipeline_id,
@@ -1731,18 +1713,18 @@ impl ScriptThread {
             None => return warn!("Registration failed for {}", scope),
         };
 
         let script_url = maybe_registration.get_installed().get_script_url();
         let scope_things = ServiceWorkerRegistration::create_scope_things(window.upcast(), script_url);
         let _ = self.script_sender.send((pipeline_id, ScriptMsg::RegisterServiceWorker(scope_things, scope.clone())));
     }
 
-    pub fn dispatch_job_queue(&self, job_handler: Box<AsyncJobHandler>) {
-        self.job_queue_map.run_job(job_handler, self);
+    pub fn schedule_job_queue(&self, scope_url: ServoUrl) {
+        let _ = self.chan.0.send(MainThreadScriptMsg::DispatchJobQueue { scope_url });
     }
 
     pub fn dom_manipulation_task_source(&self) -> &DOMManipulationTaskSource {
         &self.dom_manipulation_task_source
     }
 
     pub fn performance_timeline_task_source(&self) -> &PerformanceTimelineTaskSource {
         &self.performance_timeline_task_source
--- a/servo/components/script/serviceworkerjob.rs
+++ b/servo/components/script/serviceworkerjob.rs
@@ -13,17 +13,17 @@ use dom::bindings::js::JS;
 use dom::bindings::refcounted::{Trusted, TrustedPromise};
 use dom::bindings::reflector::DomObject;
 use dom::client::Client;
 use dom::globalscope::GlobalScope;
 use dom::promise::Promise;
 use dom::serviceworkerregistration::ServiceWorkerRegistration;
 use dom::urlhelper::UrlHelper;
 use js::jsapi::JSAutoCompartment;
-use script_thread::{ScriptThread, Runnable};
+use script_thread::ScriptThread;
 use servo_url::ServoUrl;
 use std::cmp::PartialEq;
 use std::collections::HashMap;
 use std::rc::Rc;
 use task_source::TaskSource;
 use task_source::dom_manipulation::DOMManipulationTaskSource;
 
 #[derive(Clone, Copy, Debug, JSTraceable, PartialEq)]
@@ -88,58 +88,35 @@ impl PartialEq for Job {
                 JobType::Unregister => self.scope_url == other.scope_url
             }
         } else {
             false
         }
     }
 }
 
-pub struct AsyncJobHandler {
-    pub scope_url: ServoUrl,
-}
-
-impl AsyncJobHandler {
-    fn new(scope_url: ServoUrl) -> AsyncJobHandler {
-        AsyncJobHandler {
-            scope_url: scope_url,
-        }
-    }
-}
-
-impl Runnable for AsyncJobHandler {
-    #[allow(unrooted_must_root)]
-    fn main_thread_handler(self: Box<AsyncJobHandler>, script_thread: &ScriptThread) {
-        script_thread.dispatch_job_queue(self);
-    }
-}
-
 #[must_root]
 #[derive(JSTraceable)]
 pub struct JobQueue(pub DOMRefCell<HashMap<ServoUrl, Vec<Job>>>);
 
 impl JobQueue {
     pub fn new() -> JobQueue {
         JobQueue(DOMRefCell::new(HashMap::new()))
     }
     #[allow(unrooted_must_root)]
     // https://w3c.github.io/ServiceWorker/#schedule-job-algorithm
-    pub fn schedule_job(&self,
-                        job: Job,
-                        global: &GlobalScope,
-                        script_thread: &ScriptThread) {
+    pub fn schedule_job(&self, job: Job, script_thread: &ScriptThread) {
         debug!("scheduling {:?} job", job.job_type);
         let mut queue_ref = self.0.borrow_mut();
         let job_queue = queue_ref.entry(job.scope_url.clone()).or_insert(vec![]);
         // Step 1
         if job_queue.is_empty() {
             let scope_url = job.scope_url.clone();
             job_queue.push(job);
-            let run_job_handler = box AsyncJobHandler::new(scope_url);
-            let _ = script_thread.dom_manipulation_task_source().queue(run_job_handler, global);
+            let _ = script_thread.schedule_job_queue(scope_url);
             debug!("queued task to run newly-queued job");
         } else {
             // Step 2
             let mut last_job = job_queue.pop().unwrap();
             if job == last_job && !last_job.promise.is_settled() {
                 last_job.append_equivalent_job(job);
                 job_queue.push(last_job);
                 debug!("appended equivalent job");
@@ -150,40 +127,39 @@ impl JobQueue {
                 job_queue.push(job);
                 debug!("pushed onto job queue job");
             }
         }
     }
 
     #[allow(unrooted_must_root)]
     // https://w3c.github.io/ServiceWorker/#run-job-algorithm
-    pub fn run_job(&self, run_job_handler: Box<AsyncJobHandler>, script_thread: &ScriptThread) {
+    pub fn run_job(&self, scope_url: ServoUrl, script_thread: &ScriptThread) {
         debug!("running a job");
         let url = {
             let queue_ref = self.0.borrow();
             let front_job = {
-                let job_vec = queue_ref.get(&run_job_handler.scope_url);
+                let job_vec = queue_ref.get(&scope_url);
                 job_vec.unwrap().first().unwrap()
             };
-            let scope_url = front_job.scope_url.clone();
+            let front_scope_url = front_job.scope_url.clone();
             match front_job.job_type {
-                JobType::Register => self.run_register(front_job, run_job_handler, script_thread),
+                JobType::Register => self.run_register(front_job, scope_url, script_thread),
                 JobType::Update => self.update(front_job, script_thread),
                 JobType::Unregister => unreachable!(),
             };
-            scope_url
+            front_scope_url
         };
         self.finish_job(url, script_thread);
     }
 
     #[allow(unrooted_must_root)]
     // https://w3c.github.io/ServiceWorker/#register-algorithm
-    fn run_register(&self, job: &Job, register_job_handler: Box<AsyncJobHandler>, script_thread: &ScriptThread) {
+    fn run_register(&self, job: &Job, scope_url: ServoUrl, script_thread: &ScriptThread) {
         debug!("running register job");
-        let AsyncJobHandler { scope_url, .. } = *register_job_handler;
         // Step 1-3
         if !UrlHelper::is_origin_trustworthy(&job.script_url) {
             // Step 1.1
             reject_job_promise(job,
                                Error::Type("Invalid script ServoURL".to_owned()),
                                script_thread.dom_manipulation_task_source());
             // Step 1.2 (see run_job)
             return;
@@ -232,18 +208,17 @@ impl JobQueue {
             !job_vec.is_empty()
         } else {
             warn!("non-existent job vector for Servourl: {:?}", scope_url);
             false
         };
 
         if run_job {
             debug!("further jobs in queue after finishing");
-            let handler = box AsyncJobHandler::new(scope_url);
-            self.run_job(handler, script_thread);
+            self.run_job(scope_url, script_thread);
         }
     }
 
     // https://w3c.github.io/ServiceWorker/#update-algorithm
     fn update(&self, job: &Job, script_thread: &ScriptThread) {
         debug!("running update job");
         // Step 1
         let reg = match script_thread.handle_get_registration(&job.scope_url) {
@@ -281,56 +256,36 @@ impl JobQueue {
             // Step 8.1
             resolve_job_promise(job, &*reg, script_thread.dom_manipulation_task_source());
             // Step 8.2 present in run_job
         }
         // TODO Step 9 (create new service worker)
     }
 }
 
-struct AsyncPromiseSettle {
-    global: Trusted<GlobalScope>,
-    promise: TrustedPromise,
-    settle_type: SettleType,
-}
-
-impl Runnable for AsyncPromiseSettle {
-    #[allow(unrooted_must_root)]
-    fn handler(self: Box<AsyncPromiseSettle>) {
-        let global = self.global.root();
-        let settle_type = self.settle_type.clone();
-        let promise = self.promise.root();
-        settle_job_promise(&*global, &*promise, settle_type)
-    }
-}
-
-impl AsyncPromiseSettle {
-    #[allow(unrooted_must_root)]
-    fn new(promise: Rc<Promise>, settle_type: SettleType) -> AsyncPromiseSettle {
-        AsyncPromiseSettle {
-            global: Trusted::new(&*promise.global()),
-            promise: TrustedPromise::new(promise),
-            settle_type: settle_type,
-        }
-    }
-}
-
 fn settle_job_promise(global: &GlobalScope, promise: &Promise, settle: SettleType) {
     let _ac = JSAutoCompartment::new(global.get_cx(), promise.reflector().get_jsobject().get());
     match settle {
         SettleType::Resolve(reg) => promise.resolve_native(global.get_cx(), &*reg.root()),
         SettleType::Reject(err) => promise.reject_error(global.get_cx(), err),
     };
 }
 
+#[allow(unrooted_must_root)]
 fn queue_settle_promise_for_job(job: &Job, settle: SettleType, task_source: &DOMManipulationTaskSource) {
-    let task = box AsyncPromiseSettle::new(job.promise.clone(), settle);
     let global = job.client.global();
-    let _ = task_source.queue(task, &*global);
-
+    let promise = TrustedPromise::new(job.promise.clone());
+    // FIXME(nox): Why are errors silenced here?
+    let _ = task_source.queue(
+        box task!(settle_promise_for_job: move || {
+            let promise = promise.root();
+            settle_job_promise(&promise.global(), &promise, settle)
+        }),
+        &*global,
+    );
 }
 
 // https://w3c.github.io/ServiceWorker/#reject-job-promise-algorithm
 // https://w3c.github.io/ServiceWorker/#resolve-job-promise-algorithm
 fn queue_settle_promise(job: &Job, settle: SettleType, task_source: &DOMManipulationTaskSource) {
     // Step 1
     queue_settle_promise_for_job(job, settle.clone(), task_source);
     // Step 2
--- a/servo/components/script/stylesheet_loader.rs
+++ b/servo/components/script/stylesheet_loader.rs
@@ -224,17 +224,17 @@ impl<'a> StylesheetLoader<'a> {
             origin_clean: true,
             request_generation_id: gen,
         }));
 
         let (action_sender, action_receiver) = ipc::channel().unwrap();
         let listener = NetworkListener {
             context: context,
             task_source: document.window().networking_task_source(),
-            wrapper: Some(document.window().get_runnable_wrapper())
+            canceller: Some(document.window().task_canceller())
         };
         ROUTER.add_route(action_receiver.to_opaque(), box move |message| {
             listener.notify_fetch(message.to().unwrap());
         });
 
 
         let owner = self.elem.upcast::<Element>().as_stylesheet_owner()
             .expect("Stylesheet not loaded by <style> or <link> element!");
new file mode 100644
--- /dev/null
+++ b/servo/components/script/task.rs
@@ -0,0 +1,94 @@
+/* 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/. */
+
+//! Machinery for [tasks](https://html.spec.whatwg.org/multipage/#concept-task).
+
+use std::fmt;
+use std::intrinsics;
+use std::sync::Arc;
+use std::sync::atomic::{AtomicBool, Ordering};
+
+macro_rules! task {
+    ($name:ident: move || $body:tt) => {{
+        #[allow(non_camel_case_types)]
+        struct $name<F>(F);
+        impl<F> ::task::Task for $name<F>
+        where
+            F: ::std::ops::FnOnce(),
+        {
+            fn name(&self) -> &'static str {
+                stringify!($name)
+            }
+
+            fn run(self: Box<Self>) {
+                (self.0)();
+            }
+        }
+        $name(move || $body)
+    }};
+}
+
+/// A task that can be run. The name method is for profiling purposes.
+pub trait Task {
+    #[allow(unsafe_code)]
+    fn name(&self) -> &'static str { unsafe { intrinsics::type_name::<Self>() } }
+    fn run(self: Box<Self>);
+}
+
+impl fmt::Debug for Task + Send {
+    fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
+        fmt.debug_tuple(self.name()).field(&format_args!("...")).finish()
+    }
+}
+
+/// Encapsulated state required to create cancellable tasks from non-script threads.
+pub struct TaskCanceller {
+    pub cancelled: Option<Arc<AtomicBool>>,
+}
+
+impl TaskCanceller {
+    /// Returns a wrapped `task` that will be cancelled if the `TaskCanceller`
+    /// says so.
+    pub fn wrap_task<T>(&self, task: Box<T>) -> Box<Task + Send>
+    where
+        T: Send + Task + 'static,
+    {
+        box CancellableTask {
+            cancelled: self.cancelled.clone(),
+            inner: task,
+        }
+    }
+}
+
+/// A task that can be cancelled by toggling a shared flag.
+pub struct CancellableTask<T: Send + Task> {
+    cancelled: Option<Arc<AtomicBool>>,
+    inner: Box<T>,
+}
+
+impl<T> CancellableTask<T>
+where
+    T: Send + Task,
+{
+    fn is_cancelled(&self) -> bool {
+        self.cancelled.as_ref().map_or(false, |cancelled| {
+            cancelled.load(Ordering::SeqCst)
+        })
+    }
+}
+
+impl<T> Task for CancellableTask<T>
+where
+    T: Send + Task,
+{
+    fn name(&self) -> &'static str {
+        self.inner.name()
+    }
+
+    fn run(self: Box<Self>) {
+        if !self.is_cancelled() {
+            self.inner.run()
+        }
+    }
+}
--- a/servo/components/script/task_source/dom_manipulation.rs
+++ b/servo/components/script/task_source/dom_manipulation.rs
@@ -1,69 +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::inheritance::Castable;
 use dom::bindings::refcounted::Trusted;
-use dom::event::{EventBubbles, EventCancelable, EventRunnable, SimpleEventRunnable};
+use dom::event::{EventBubbles, EventCancelable, EventTask, SimpleEventTask};
 use dom::eventtarget::EventTarget;
 use dom::window::Window;
 use script_runtime::{CommonScriptMsg, ScriptThreadEventCategory};
-use script_thread::{MainThreadScriptMsg, Runnable, RunnableWrapper};
+use script_thread::MainThreadScriptMsg;
 use servo_atoms::Atom;
 use std::fmt;
 use std::result::Result;
 use std::sync::mpsc::Sender;
+use task::{Task, TaskCanceller};
 use task_source::TaskSource;
 
 #[derive(Clone, JSTraceable)]
 pub struct DOMManipulationTaskSource(pub Sender<MainThreadScriptMsg>);
 
 impl fmt::Debug for DOMManipulationTaskSource {
     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
         write!(f, "DOMManipulationTaskSource(...)")
     }
 }
 
 impl TaskSource for DOMManipulationTaskSource {
-    fn queue_with_wrapper<T>(
+    fn queue_with_canceller<T>(
         &self,
         msg: Box<T>,
-        wrapper: &RunnableWrapper,
+        canceller: &TaskCanceller,
     ) -> Result<(), ()>
     where
-        T: Runnable + Send + 'static,
+        T: Task + Send + 'static,
     {
-        let msg = MainThreadScriptMsg::Common(CommonScriptMsg::RunnableMsg(
+        let msg = MainThreadScriptMsg::Common(CommonScriptMsg::Task(
             ScriptThreadEventCategory::ScriptEvent,
-            wrapper.wrap_runnable(msg),
+            canceller.wrap_task(msg),
         ));
         self.0.send(msg).map_err(|_| ())
     }
 }
 
 impl DOMManipulationTaskSource {
     pub fn queue_event(&self,
                        target: &EventTarget,
                        name: Atom,
                        bubbles: EventBubbles,
                        cancelable: EventCancelable,
                        window: &Window) {
         let target = Trusted::new(target);
-        let runnable = box EventRunnable {
+        let task = box EventTask {
             target: target,
             name: name,
             bubbles: bubbles,
             cancelable: cancelable,
         };
-        let _ = self.queue(runnable, window.upcast());
+        let _ = self.queue(task, window.upcast());
     }
 
     pub fn queue_simple_event(&self, target: &EventTarget, name: Atom, window: &Window) {
         let target = Trusted::new(target);
-        let runnable = box SimpleEventRunnable {
-            target: target,
-            name: name,
-        };
-        let _ = self.queue(runnable, window.upcast());
+        let _ = self.queue(box SimpleEventTask { target, name }, window.upcast());
     }
 }
--- a/servo/components/script/task_source/file_reading.rs
+++ b/servo/components/script/task_source/file_reading.rs
@@ -1,54 +1,47 @@
 /* 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::domexception::DOMErrorName;
 use dom::filereader::{FileReader, TrustedFileReader, GenerationId, ReadMetaData};
 use script_runtime::{CommonScriptMsg, ScriptThreadEventCategory, ScriptChan};
-use script_thread::{Runnable, RunnableWrapper};
 use std::sync::Arc;
+use task::{Task, TaskCanceller};
 use task_source::TaskSource;
 
 #[derive(JSTraceable)]
 pub struct FileReadingTaskSource(pub Box<ScriptChan + Send + 'static>);
 
 impl Clone for FileReadingTaskSource {
     fn clone(&self) -> FileReadingTaskSource {
         FileReadingTaskSource(self.0.clone())
     }
 }
 
 impl TaskSource for FileReadingTaskSource {
-    fn queue_with_wrapper<T>(&self,
-                             msg: Box<T>,
-                             wrapper: &RunnableWrapper)
-                             -> Result<(), ()>
-                             where T: Runnable + Send + 'static {
-        self.0.send(CommonScriptMsg::RunnableMsg(ScriptThreadEventCategory::FileRead,
-                                                 wrapper.wrap_runnable(msg)))
+    fn queue_with_canceller<T>(
+        &self,
+        msg: Box<T>,
+        canceller: &TaskCanceller,
+    ) -> Result<(), ()>
+    where
+        T: Send + Task + 'static,
+    {
+        self.0.send(CommonScriptMsg::Task(
+            ScriptThreadEventCategory::FileRead,
+            canceller.wrap_task(msg),
+        ))
     }
 }
 
-pub struct FileReadingRunnable {
-    task: FileReadingTask,
-}
-
-impl FileReadingRunnable {
-    pub fn new(task: FileReadingTask) -> Box<FileReadingRunnable> {
-        box FileReadingRunnable {
-            task: task
-        }
-    }
-}
-
-impl Runnable for FileReadingRunnable {
-    fn handler(self: Box<FileReadingRunnable>) {
-        self.task.handle_task();
+impl Task for FileReadingTask {
+    fn run(self: Box<Self>) {
+        self.handle_task();
     }
 }
 
 #[allow(dead_code)]
 pub enum FileReadingTask {
     ProcessRead(TrustedFileReader, GenerationId),
     ProcessReadData(TrustedFileReader, GenerationId),
     ProcessReadError(TrustedFileReader, GenerationId, DOMErrorName),
--- a/servo/components/script/task_source/mod.rs
+++ b/servo/components/script/task_source/mod.rs
@@ -5,21 +5,24 @@
 pub mod dom_manipulation;
 pub mod file_reading;
 pub mod history_traversal;
 pub mod networking;
 pub mod performance_timeline;
 pub mod user_interaction;
 
 use dom::globalscope::GlobalScope;
-use script_thread::{Runnable, RunnableWrapper};
 use std::result::Result;
+use task::{Task, TaskCanceller};
 
 pub trait TaskSource {
-    fn queue_with_wrapper<T>(&self,
-                             msg: Box<T>,
-                             wrapper: &RunnableWrapper)
-                             -> Result<(), ()>
-                             where T: Runnable + Send + 'static;
-    fn queue<T: Runnable + Send + 'static>(&self, msg: Box<T>, global: &GlobalScope) -> Result<(), ()> {
-        self.queue_with_wrapper(msg, &global.get_runnable_wrapper())
+    fn queue_with_canceller<T>(
+        &self,
+        msg: Box<T>,
+        canceller: &TaskCanceller,
+    ) -> Result<(), ()>
+    where
+        T: Send + Task + 'static;
+
+    fn queue<T: Task + Send + 'static>(&self, msg: Box<T>, global: &GlobalScope) -> Result<(), ()> {
+        self.queue_with_canceller(msg, &global.task_canceller())
     }
 }
--- a/servo/components/script/task_source/networking.rs
+++ b/servo/components/script/task_source/networking.rs
@@ -1,33 +1,43 @@
 /* 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 script_runtime::{CommonScriptMsg, ScriptChan, ScriptThreadEventCategory};
-use script_thread::{Runnable, RunnableWrapper};
+use task::{Task, TaskCanceller};
 use task_source::TaskSource;
 
 #[derive(JSTraceable)]
 pub struct NetworkingTaskSource(pub Box<ScriptChan + Send + 'static>);
 
 impl Clone for NetworkingTaskSource {
     fn clone(&self) -> NetworkingTaskSource {
         NetworkingTaskSource(self.0.clone())
     }
 }
 
 impl TaskSource for NetworkingTaskSource {
-    fn queue_with_wrapper<T>(&self,
-                             msg: Box<T>,
-                             wrapper: &RunnableWrapper)
-                             -> Result<(), ()>
-                             where T: Runnable + Send + 'static {
-        self.0.send(CommonScriptMsg::RunnableMsg(ScriptThreadEventCategory::NetworkEvent,
-                                                 wrapper.wrap_runnable(msg)))
+    fn queue_with_canceller<T>(
+        &self,
+        msg: Box<T>,
+        canceller: &TaskCanceller,
+    ) -> Result<(), ()>
+    where
+        T: Send + Task + 'static,
+    {
+        self.0.send(CommonScriptMsg::Task(
+            ScriptThreadEventCategory::NetworkEvent,
+            canceller.wrap_task(msg),
+        ))
     }
 }
 
 impl NetworkingTaskSource {
-    pub fn queue_wrapperless<T: Runnable + Send + 'static>(&self, msg: Box<T>) -> Result<(), ()> {
-        self.0.send(CommonScriptMsg::RunnableMsg(ScriptThreadEventCategory::NetworkEvent, msg))
+    /// This queues a task that will not be cancelled when its associated
+    /// global scope gets destroyed.
+    pub fn queue_unconditionally<T>(&self, msg: Box<T>) -> Result<(), ()>
+    where
+        T: Task + Send + 'static,
+    {
+        self.0.send(CommonScriptMsg::Task(ScriptThreadEventCategory::NetworkEvent, msg))
     }
 }
--- a/servo/components/script/task_source/performance_timeline.rs
+++ b/servo/components/script/task_source/performance_timeline.rs
@@ -3,68 +3,58 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 // XXX The spec says that the performance timeline task source should be
 //     a low priority task and it should be processed during idle periods.
 //     We are currently treating this task queue as a normal priority queue.
 
 use dom::bindings::refcounted::Trusted;
 use dom::globalscope::GlobalScope;
-use dom::performance::Performance;
 use script_runtime::{CommonScriptMsg, ScriptChan, ScriptThreadEventCategory};
-use script_thread::{Runnable, RunnableWrapper};
 use std::fmt;
 use std::result::Result;
+use task::{Task, TaskCanceller};
 use task_source::TaskSource;
 
-pub struct NotifyPerformanceObserverRunnable {
-    owner: Trusted<Performance>,
-}
-
-impl NotifyPerformanceObserverRunnable {
-    pub fn new(owner: Trusted<Performance>) -> Self {
-        NotifyPerformanceObserverRunnable {
-            owner,
-        }
-    }
-}
-
-impl Runnable for NotifyPerformanceObserverRunnable {
-    fn handler(self: Box<NotifyPerformanceObserverRunnable>) {
-        self.owner.root().notify_observers();
-    }
-}
-
 #[derive(JSTraceable)]
 pub struct PerformanceTimelineTaskSource(pub Box<ScriptChan + Send + 'static>);
 
 impl Clone for PerformanceTimelineTaskSource {
     fn clone(&self) -> PerformanceTimelineTaskSource {
         PerformanceTimelineTaskSource(self.0.clone())
     }
 }
 
 impl fmt::Debug for PerformanceTimelineTaskSource {
     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
         write!(f, "PerformanceTimelineTaskSource(...)")
     }
 }
 
 impl TaskSource for PerformanceTimelineTaskSource {
-    fn queue_with_wrapper<T>(&self,
-                             msg: Box<T>,
-                             wrapper: &RunnableWrapper) -> Result<(), ()>
-                             where T: Runnable + Send + 'static {
-        let msg = CommonScriptMsg::RunnableMsg(
+    fn queue_with_canceller<T>(
+        &self,
+        msg: Box<T>,
+        canceller: &TaskCanceller,
+    ) -> Result<(), ()>
+    where
+        T: Send + Task + 'static,
+    {
+        let msg = CommonScriptMsg::Task(
             ScriptThreadEventCategory::PerformanceTimelineTask,
-            wrapper.wrap_runnable(msg)
+            canceller.wrap_task(msg)
         );
         self.0.send(msg).map_err(|_| ())
     }
 }
 
 impl PerformanceTimelineTaskSource {
     pub fn queue_notification(&self, global: &GlobalScope) {
         let owner = Trusted::new(&*global.performance());
-        let runnable = box NotifyPerformanceObserverRunnable::new(owner);
-        let _ = self.queue(runnable, global);
+        // FIXME(nox): Why are errors silenced here?
+        let _ = self.queue(
+            box task!(notify_performance_observers: move || {
+                owner.root().notify_observers();
+            }),
+            global,
+        );
     }
 }
--- a/servo/components/script/task_source/user_interaction.rs
+++ b/servo/components/script/task_source/user_interaction.rs
@@ -1,60 +1,56 @@
 /* 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::inheritance::Castable;
 use dom::bindings::refcounted::Trusted;
-use dom::event::{EventBubbles, EventCancelable, EventRunnable};
+use dom::event::{EventBubbles, EventCancelable, EventTask};
 use dom::eventtarget::EventTarget;
 use dom::window::Window;
 use script_runtime::{CommonScriptMsg, ScriptThreadEventCategory};
-use script_thread::{MainThreadScriptMsg, Runnable, RunnableWrapper};
+use script_thread::MainThreadScriptMsg;
 use servo_atoms::Atom;
 use std::fmt;
 use std::result::Result;
 use std::sync::mpsc::Sender;
+use task::{Task, TaskCanceller};
 use task_source::TaskSource;
 
 #[derive(Clone, JSTraceable)]
 pub struct UserInteractionTaskSource(pub Sender<MainThreadScriptMsg>);
 
 impl fmt::Debug for UserInteractionTaskSource {
     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
         write!(f, "UserInteractionTaskSource(...)")
     }
 }
 
 impl TaskSource for UserInteractionTaskSource {
-    fn queue_with_wrapper<T>(
+    fn queue_with_canceller<T>(
         &self,
         msg: Box<T>,
-        wrapper: &RunnableWrapper,
+        canceller: &TaskCanceller,
     ) -> Result<(), ()>
     where
-        T: Runnable + Send + 'static,
+        T: Task + Send + 'static,
     {
-        let msg = MainThreadScriptMsg::Common(CommonScriptMsg::RunnableMsg(
+        let msg = MainThreadScriptMsg::Common(CommonScriptMsg::Task(
             ScriptThreadEventCategory::InputEvent,
-            wrapper.wrap_runnable(msg),
+            canceller.wrap_task(msg),
         ));
         self.0.send(msg).map_err(|_| ())
     }
 }
 
 impl UserInteractionTaskSource {
     pub fn queue_event(&self,
                        target: &EventTarget,
                        name: Atom,
                        bubbles: EventBubbles,
                        cancelable: EventCancelable,
                        window: &Window) {
         let target = Trusted::new(target);
-        let runnable = box EventRunnable {
-            target: target,
-            name: name,
-            bubbles: bubbles,
-            cancelable: cancelable,
-        };
-        let _ = self.queue(runnable, window.upcast());
+        let task = box EventTask { target, name, bubbles, cancelable };
+        let _ = self.queue(task, window.upcast());
     }
 }
--- a/servo/components/script_traits/lib.rs
+++ b/servo/components/script_traits/lib.rs
@@ -855,16 +855,22 @@ pub trait Painter: SpeculativePainter {
     fn draw_a_paint_image(&self,
                           size: TypedSize2D<f32, CSSPixel>,
                           zoom: ScaleFactor<f32, CSSPixel, DevicePixel>,
                           properties: Vec<(Atom, String)>,
                           arguments: Vec<String>)
                           -> DrawAPaintImageResult;
 }
 
+impl fmt::Debug for Painter {
+    fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
+        fmt.debug_tuple("Painter").field(&format_args!("..")).finish()
+    }
+}
+
 /// The result of executing paint code: the image together with any image URLs that need to be loaded.
 /// TODO: this should return a WR display list. https://github.com/servo/servo/issues/17497
 #[derive(Clone, Debug, Deserialize, HeapSizeOf, Serialize)]
 pub struct DrawAPaintImageResult {
     /// The image height
     pub width: u32,
     /// The image width
     pub height: u32,