Auto merge of #15189 - jdm:microtasks, r=nox

Implement microtask checkpoints

This generalizes the work previously done for Promise job callbacks. There is now a microtask queue that correctly processes all queued microtasks after each turn of the event loop, as well as after a scripted callback finishes executing, and after a classic script executes.

---
- [X] `./mach build -d` does not report any errors
- [X] `./mach test-tidy` does not report any errors
- [X] These changes fix #4283
- [X] There are tests for these changes

<!-- Reviewable:start -->
---
This change is [<img src="https://reviewable.io/review_button.svg" height="34" align="absmiddle" alt="Reviewable"/>](https://reviewable.io/reviews/servo/servo/15189)
<!-- Reviewable:end -->
This commit is contained in:
bors-servo 2017-02-03 07:53:17 -08:00 committed by GitHub
commit cbcafd18f4
14 changed files with 340 additions and 307 deletions

View file

@ -11,6 +11,7 @@ use js::jsapi::JSTracer;
use js::jsapi::UnhideScriptedCaller;
use js::rust::Runtime;
use std::cell::RefCell;
use std::thread;
thread_local!(static STACK: RefCell<Vec<StackEntry>> = RefCell::new(Vec::new()));
@ -36,7 +37,7 @@ pub unsafe fn trace(tracer: *mut JSTracer) {
/// RAII struct that pushes and pops entries from the script settings stack.
pub struct AutoEntryScript {
global: usize,
global: Root<GlobalScope>,
}
impl AutoEntryScript {
@ -50,7 +51,7 @@ impl AutoEntryScript {
kind: StackEntryKind::Entry,
});
AutoEntryScript {
global: global as *const _ as usize,
global: Root::from_ref(global),
}
})
}
@ -62,12 +63,17 @@ impl Drop for AutoEntryScript {
STACK.with(|stack| {
let mut stack = stack.borrow_mut();
let entry = stack.pop().unwrap();
assert_eq!(&*entry.global as *const GlobalScope as usize,
self.global,
assert_eq!(&*entry.global as *const GlobalScope,
&*self.global as *const GlobalScope,
"Dropped AutoEntryScript out of order.");
assert_eq!(entry.kind, StackEntryKind::Entry);
trace!("Clean up after running script with {:p}", &*entry.global);
})
});
// Step 5
if !thread::panicking() && incumbent_global().is_none() {
self.global.perform_a_microtask_checkpoint();
}
}
}

View file

@ -230,17 +230,23 @@ impl DedicatedWorkerGlobalScope {
}
{
let _ar = AutoWorkerReset::new(&global, worker);
let _ar = AutoWorkerReset::new(&global, worker.clone());
scope.execute_script(DOMString::from(source));
}
let reporter_name = format!("dedicated-worker-reporter-{}", random::<u64>());
scope.upcast::<GlobalScope>().mem_profiler_chan().run_with_memory_reporting(|| {
// https://html.spec.whatwg.org/multipage/#event-loop-processing-model
// Step 1
while let Ok(event) = global.receive_event() {
if scope.is_closing() {
break;
}
// Step 3
global.handle_event(event);
// Step 6
let _ar = AutoWorkerReset::new(&global, worker.clone());
global.upcast::<WorkerGlobalScope>().perform_a_microtask_checkpoint();
}
}, reporter_name, parent_sender, CommonScriptMsg::CollectReports);
}).expect("Thread spawning failed");

View file

@ -29,10 +29,11 @@ use js::jsapi::{JS_GetObjectRuntime, MutableHandleValue};
use js::panic::maybe_resume_unwind;
use js::rust::{CompileOptionsWrapper, Runtime, get_object_class};
use libc;
use microtask::Microtask;
use msg::constellation_msg::PipelineId;
use net_traits::{CoreResourceThread, ResourceThreads, IpcSend};
use profile_traits::{mem, time};
use script_runtime::{CommonScriptMsg, EnqueuedPromiseCallback, ScriptChan, ScriptPort};
use script_runtime::{CommonScriptMsg, ScriptChan, ScriptPort};
use script_thread::{MainThreadScriptChan, RunnableWrapper, ScriptThread};
use script_traits::{MsDuration, ScriptMsg as ConstellationMsg, TimerEvent};
use script_traits::{TimerEventId, TimerEventRequest, TimerSource};
@ -448,25 +449,24 @@ impl GlobalScope {
unreachable!();
}
/// Start the process of executing the pending promise callbacks. They will be invoked
/// in FIFO order, synchronously, at some point in the future.
pub fn flush_promise_jobs(&self) {
/// Perform a microtask checkpoint.
pub fn perform_a_microtask_checkpoint(&self) {
if self.is::<Window>() {
return ScriptThread::flush_promise_jobs(self);
return ScriptThread::invoke_perform_a_microtask_checkpoint();
}
if let Some(worker) = self.downcast::<WorkerGlobalScope>() {
return worker.flush_promise_jobs();
return worker.perform_a_microtask_checkpoint();
}
unreachable!();
}
/// Enqueue a promise callback for subsequent execution.
pub fn enqueue_promise_job(&self, job: EnqueuedPromiseCallback) {
/// Enqueue a microtask for subsequent execution.
pub fn enqueue_microtask(&self, job: Microtask) {
if self.is::<Window>() {
return ScriptThread::enqueue_promise_job(job, self);
return ScriptThread::enqueue_microtask(job);
}
if let Some(worker) = self.downcast::<WorkerGlobalScope>() {
return worker.enqueue_promise_job(job);
return worker.enqueue_microtask(job);
}
unreachable!();
}

View file

@ -486,11 +486,7 @@ impl HTMLScriptElement {
document.set_current_script(Some(self));
// Step 5.a.2.
let window = window_from_node(self);
let line_number = if script.external { 1 } else { self.line_number as u32 };
rooted!(in(window.get_cx()) let mut rval = UndefinedValue());
window.upcast::<GlobalScope>().evaluate_script_on_global_with_result(
&script.text, script.url.as_str(), rval.handle_mut(), line_number);
self.run_a_classic_script(&script);
// Step 6.
document.set_current_script(old_script.r());
@ -506,6 +502,24 @@ impl HTMLScriptElement {
}
}
// https://html.spec.whatwg.org/multipage/#run-a-classic-script
pub fn run_a_classic_script(&self, script: &ClassicScript) {
// TODO use a settings object rather than this element's document/window
// Step 2
let document = document_from_node(self);
if !document.is_fully_active() || !document.is_scripting_enabled() {
return;
}
// Steps 4-10
let window = window_from_node(self);
let line_number = if script.external { 1 } else { self.line_number as u32 };
rooted!(in(window.get_cx()) let mut rval = UndefinedValue());
let global = window.upcast::<GlobalScope>();
global.evaluate_script_on_global_with_result(
&script.text, script.url.as_str(), rval.handle_mut(), line_number);
}
pub fn queue_error_event(&self) {
let window = window_from_node(self);
window.dom_manipulation_task_source().queue_simple_event(self.upcast(), atom!("error"), &window);

View file

@ -210,10 +210,15 @@ impl ServiceWorkerGlobalScope {
global.dispatch_activate();
let reporter_name = format!("service-worker-reporter-{}", random::<u64>());
scope.upcast::<GlobalScope>().mem_profiler_chan().run_with_memory_reporting(|| {
// https://html.spec.whatwg.org/multipage/#event-loop-processing-model
// Step 1
while let Ok(event) = global.receive_event() {
// Step 3
if !global.handle_event(event) {
break;
}
// Step 6
global.upcast::<WorkerGlobalScope>().perform_a_microtask_checkpoint();
}
}, reporter_name, scope.script_chan(), CommonScriptMsg::CollectReports);
}).expect("Thread spawning failed");

View file

@ -11,7 +11,6 @@ use dom::bindings::codegen::UnionTypes::RequestOrUSVString;
use dom::bindings::error::{Error, ErrorResult, Fallible, report_pending_exception};
use dom::bindings::inheritance::Castable;
use dom::bindings::js::{MutNullableJS, Root};
use dom::bindings::refcounted::Trusted;
use dom::bindings::reflector::DomObject;
use dom::bindings::settings_stack::AutoEntryScript;
use dom::bindings::str::DOMString;
@ -29,11 +28,11 @@ use js::jsapi::{HandleValue, JSAutoCompartment, JSContext, JSRuntime};
use js::jsval::UndefinedValue;
use js::panic::maybe_resume_unwind;
use js::rust::Runtime;
use microtask::{MicrotaskQueue, Microtask};
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};
use script_runtime::{ScriptThreadEventCategory, PromiseJobQueue, EnqueuedPromiseCallback};
use script_thread::{Runnable, RunnableWrapper};
use script_thread::RunnableWrapper;
use script_traits::{TimerEvent, TimerEventId};
use script_traits::WorkerGlobalScopeInit;
use servo_url::ServoUrl;
@ -87,7 +86,7 @@ pub struct WorkerGlobalScope {
/// `IpcSender` doesn't exist
from_devtools_receiver: Receiver<DevtoolScriptControlMsg>,
promise_job_queue: PromiseJobQueue,
microtask_queue: MicrotaskQueue,
}
impl WorkerGlobalScope {
@ -117,7 +116,7 @@ impl WorkerGlobalScope {
navigator: Default::default(),
from_devtools_sender: init.from_devtools_sender,
from_devtools_receiver: from_devtools_receiver,
promise_job_queue: PromiseJobQueue::new(),
microtask_queue: MicrotaskQueue::default(),
}
}
@ -159,20 +158,12 @@ impl WorkerGlobalScope {
}
}
pub fn enqueue_promise_job(&self, job: EnqueuedPromiseCallback) {
self.promise_job_queue.enqueue(job, self.upcast());
pub fn enqueue_microtask(&self, job: Microtask) {
self.microtask_queue.enqueue(job);
}
pub fn flush_promise_jobs(&self) {
self.script_chan().send(CommonScriptMsg::RunnableMsg(
ScriptThreadEventCategory::WorkerEvent,
box FlushPromiseJobs {
global: Trusted::new(self),
})).unwrap();
}
fn do_flush_promise_jobs(&self) {
self.promise_job_queue.flush_promise_jobs(|id| {
pub fn perform_a_microtask_checkpoint(&self) {
self.microtask_queue.checkpoint(|id| {
let global = self.upcast::<GlobalScope>();
assert_eq!(global.pipeline_id(), id);
Some(Root::from_ref(global))
@ -393,6 +384,8 @@ impl WorkerGlobalScope {
} else {
panic!("need to implement a sender for SharedWorker")
}
//XXXjdm should we do a microtask checkpoint here?
}
pub fn handle_fire_timer(&self, timer_id: TimerEventId) {
@ -405,14 +398,3 @@ impl WorkerGlobalScope {
}
}
}
struct FlushPromiseJobs {
global: Trusted<WorkerGlobalScope>,
}
impl Runnable for FlushPromiseJobs {
fn handler(self: Box<FlushPromiseJobs>) {
let global = self.global.root();
global.do_flush_promise_jobs();
}
}

View file

@ -109,6 +109,7 @@ mod dom;
pub mod fetch;
pub mod layout_wrapper;
mod mem;
mod microtask;
mod network_listener;
pub mod origin;
pub mod script_runtime;

View file

@ -0,0 +1,83 @@
/* 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/. */
//! Implementation of [microtasks](https://html.spec.whatwg.org/multipage/#microtask) and
//! microtask queues. It is up to implementations of event loops to store a queue and
//! perform checkpoints at appropriate times, as well as enqueue microtasks as required.
use dom::bindings::callback::ExceptionHandling;
use dom::bindings::cell::DOMRefCell;
use dom::bindings::codegen::Bindings::PromiseBinding::PromiseJobCallback;
use dom::bindings::js::Root;
use dom::globalscope::GlobalScope;
use msg::constellation_msg::PipelineId;
use std::cell::Cell;
use std::mem;
use std::rc::Rc;
/// A collection of microtasks in FIFO order.
#[derive(JSTraceable, HeapSizeOf, Default)]
pub struct MicrotaskQueue {
/// The list of enqueued microtasks that will be invoked at the next microtask checkpoint.
microtask_queue: DOMRefCell<Vec<Microtask>>,
/// https://html.spec.whatwg.org/multipage/#performing-a-microtask-checkpoint
performing_a_microtask_checkpoint: Cell<bool>,
}
#[derive(JSTraceable, HeapSizeOf)]
pub enum Microtask {
Promise(EnqueuedPromiseCallback),
}
/// A promise callback scheduled to run during the next microtask checkpoint (#4283).
#[derive(JSTraceable, HeapSizeOf)]
pub struct EnqueuedPromiseCallback {
#[ignore_heap_size_of = "Rc has unclear ownership"]
pub callback: Rc<PromiseJobCallback>,
pub pipeline: PipelineId,
}
impl MicrotaskQueue {
/// Add a new microtask to this queue. It will be invoked as part of the next
/// microtask checkpoint.
pub fn enqueue(&self, job: Microtask) {
self.microtask_queue.borrow_mut().push(job);
}
/// https://html.spec.whatwg.org/multipage/#perform-a-microtask-checkpoint
/// Perform a microtask checkpoint, executing all queued microtasks until the queue is empty.
pub fn checkpoint<F>(&self, target_provider: F)
where F: Fn(PipelineId) -> Option<Root<GlobalScope>>
{
if self.performing_a_microtask_checkpoint.get() {
return;
}
// Step 1
self.performing_a_microtask_checkpoint.set(true);
// Steps 2-7
while !self.microtask_queue.borrow().is_empty() {
rooted_vec!(let mut pending_queue);
mem::swap(
&mut *pending_queue,
&mut *self.microtask_queue.borrow_mut());
for job in pending_queue.iter() {
match *job {
Microtask::Promise(ref job) => {
if let Some(target) = target_provider(job.pipeline) {
let _ = job.callback.Call_(&*target, ExceptionHandling::Report);
}
}
}
}
}
//TODO: Step 8 - notify about rejected promises
// Step 9
self.performing_a_microtask_checkpoint.set(false);
}
}

View file

@ -5,10 +5,8 @@
//! The script runtime contains common traits and structs commonly used by the
//! script thread, the dom, and the worker threads.
use dom::bindings::callback::ExceptionHandling;
use dom::bindings::cell::DOMRefCell;
use dom::bindings::codegen::Bindings::PromiseBinding::PromiseJobCallback;
use dom::bindings::js::{Root, RootCollection, RootCollectionPtr, trace_roots};
use dom::bindings::js::{RootCollection, RootCollectionPtr, trace_roots};
use dom::bindings::refcounted::{LiveDOMReferences, trace_refcounted_objects};
use dom::bindings::settings_stack;
use dom::bindings::trace::{JSTraceable, trace_traceables};
@ -23,7 +21,7 @@ use js::jsapi::{JSJitCompilerOption, JS_SetOffthreadIonCompilationEnabled, JS_Se
use js::jsapi::{JSObject, RuntimeOptionsRef, SetPreserveWrapperCallback, SetEnqueuePromiseJobCallback};
use js::panic::wrap_panic;
use js::rust::Runtime;
use msg::constellation_msg::PipelineId;
use microtask::{EnqueuedPromiseCallback, Microtask};
use profile_traits::mem::{Report, ReportKind, ReportsChan};
use script_thread::{Runnable, STACK_ROOTS, trace_thread};
use servo_config::opts;
@ -35,7 +33,6 @@ use std::os;
use std::os::raw::c_void;
use std::panic::AssertUnwindSafe;
use std::ptr;
use std::rc::Rc;
use style::thread_state;
use time::{Tm, now};
@ -107,86 +104,21 @@ impl<'a> Drop for StackRootTLS<'a> {
}
}
/// A promise callback scheduled to run during the next microtask checkpoint (#4283).
#[derive(JSTraceable, HeapSizeOf)]
pub struct EnqueuedPromiseCallback {
#[ignore_heap_size_of = "Rc has unclear ownership"]
callback: Rc<PromiseJobCallback>,
pipeline: PipelineId,
}
/// A collection of promise callbacks in FIFO order.
#[derive(JSTraceable, HeapSizeOf)]
pub struct PromiseJobQueue {
/// A snapshot of `promise_job_queue` that was taken at the start of the microtask checkpoint.
/// Used to work around mutability errors when appending new promise jobs while performing
/// a microtask checkpoint.
flushing_job_queue: DOMRefCell<Vec<EnqueuedPromiseCallback>>,
/// The list of enqueued promise callbacks that will be invoked at the next microtask checkpoint.
promise_job_queue: DOMRefCell<Vec<EnqueuedPromiseCallback>>,
/// True if there is an outstanding runnable responsible for evaluating the promise job queue.
/// This prevents runnables flooding the event queue needlessly, since the first one will
/// execute all pending runnables.
pending_promise_job_runnable: Cell<bool>,
}
impl PromiseJobQueue {
/// Create a new PromiseJobQueue instance.
pub fn new() -> PromiseJobQueue {
PromiseJobQueue {
promise_job_queue: DOMRefCell::new(vec![]),
flushing_job_queue: DOMRefCell::new(vec![]),
pending_promise_job_runnable: Cell::new(false),
}
}
/// Add a new promise job callback to this queue. It will be invoked as part of the next
/// microtask checkpoint.
pub fn enqueue(&self, job: EnqueuedPromiseCallback, global: &GlobalScope) {
self.promise_job_queue.borrow_mut().push(job);
if !self.pending_promise_job_runnable.get() {
self.pending_promise_job_runnable.set(true);
global.flush_promise_jobs();
}
}
/// Perform a microtask checkpoint, by invoking all of the pending promise job callbacks in
/// FIFO order (#4283).
pub fn flush_promise_jobs<F>(&self, target_provider: F)
where F: Fn(PipelineId) -> Option<Root<GlobalScope>>
{
self.pending_promise_job_runnable.set(false);
{
let mut pending_queue = self.promise_job_queue.borrow_mut();
*self.flushing_job_queue.borrow_mut() = pending_queue.drain(..).collect();
}
// N.B. borrowing this vector is safe w.r.t. mutability, since any promise job that
// is enqueued while invoking these callbacks will be placed in `pending_queue`;
// `flushing_queue` is a static snapshot during this checkpoint.
for job in &*self.flushing_job_queue.borrow() {
if let Some(target) = target_provider(job.pipeline) {
let _ = job.callback.Call_(&*target, ExceptionHandling::Report);
}
}
self.flushing_job_queue.borrow_mut().clear();
}
}
/// SM callback for promise job resolution. Adds a promise callback to the current global's
/// promise job queue, and enqueues a runnable to perform a microtask checkpoint if one
/// is not already pending.
/// SM callback for promise job resolution. Adds a promise callback to the current
/// global's microtask queue.
#[allow(unsafe_code)]
unsafe extern "C" fn enqueue_job(cx: *mut JSContext,
job: HandleObject,
_allocation_site: HandleObject,
_data: *mut c_void) -> bool {
wrap_panic(AssertUnwindSafe(|| {
//XXXjdm - use a different global now?
let global = GlobalScope::from_object(job.get());
let pipeline = global.pipeline_id();
global.enqueue_promise_job(EnqueuedPromiseCallback {
global.enqueue_microtask(Microtask::Promise(EnqueuedPromiseCallback {
callback: PromiseJobCallback::new(cx, job.get()),
pipeline: pipeline,
});
}));
true
}), false)
}

View file

@ -35,7 +35,6 @@ use dom::bindings::inheritance::Castable;
use dom::bindings::js::{JS, MutNullableJS, Root, RootCollection};
use dom::bindings::js::{RootCollectionPtr, RootedReference};
use dom::bindings::num::Finite;
use dom::bindings::refcounted::Trusted;
use dom::bindings::reflector::DomObject;
use dom::bindings::str::DOMString;
use dom::bindings::trace::JSTraceable;
@ -70,6 +69,7 @@ use js::jsval::UndefinedValue;
use js::rust::Runtime;
use layout_wrapper::ServoLayoutNode;
use mem::heap_size_of_self_and_children;
use microtask::{MicrotaskQueue, Microtask};
use msg::constellation_msg::{FrameId, FrameType, PipelineId, PipelineNamespace};
use net_traits::{CoreResourceMsg, FetchMetadata, FetchResponseListener};
use net_traits::{IpcSend, Metadata, ReferrerPolicy, ResourceThreads};
@ -81,8 +81,8 @@ use origin::Origin;
use profile_traits::mem::{self, OpaqueSender, Report, ReportKind, ReportsChan};
use profile_traits::time::{self, ProfilerCategory, profile};
use script_layout_interface::message::{self, NewLayoutThreadInfo, ReflowQueryType};
use script_runtime::{CommonScriptMsg, ScriptChan, ScriptThreadEventCategory, EnqueuedPromiseCallback};
use script_runtime::{ScriptPort, StackRootTLS, get_reports, new_rt_and_cx, PromiseJobQueue};
use script_runtime::{CommonScriptMsg, ScriptChan, ScriptThreadEventCategory};
use script_runtime::{ScriptPort, StackRootTLS, get_reports, new_rt_and_cx};
use script_traits::{CompositorEvent, ConstellationControlMsg};
use script_traits::{DocumentActivity, DiscardBrowsingContext, EventResult};
use script_traits::{InitialScriptState, LayoutMsg, LoadData, MouseButton, MouseEventType, MozBrowserEvent};
@ -93,11 +93,12 @@ use script_traits::CompositorEvent::{KeyEvent, MouseButtonEvent, MouseMoveEvent,
use script_traits::CompositorEvent::{TouchEvent, TouchpadPressureEvent};
use script_traits::WebVREventMsg;
use script_traits::webdriver_msg::WebDriverScriptCommand;
use serviceworkerjob::{Job, JobQueue, AsyncJobHandler, FinishJobHandler, InvokeType, SettleType};
use serviceworkerjob::{Job, JobQueue, AsyncJobHandler};
use servo_config::opts;
use servo_url::ServoUrl;
use std::cell::Cell;
use std::collections::{hash_map, HashMap, HashSet};
use std::default::Default;
use std::ops::Deref;
use std::option::Option;
use std::ptr;
@ -110,7 +111,6 @@ use std::thread;
use style::context::ReflowGoal;
use style::dom::{TNode, UnsafeNode};
use style::thread_state;
use task_source::TaskSource;
use task_source::dom_manipulation::{DOMManipulationTask, DOMManipulationTaskSource};
use task_source::file_reading::FileReadingTaskSource;
use task_source::history_traversal::HistoryTraversalTaskSource;
@ -477,7 +477,7 @@ pub struct ScriptThread {
content_process_shutdown_chan: IpcSender<()>,
promise_job_queue: PromiseJobQueue,
microtask_queue: MicrotaskQueue,
/// A handle to the webvr thread, if available
webvr_thread: Option<IpcSender<WebVRMsg>>,
@ -565,6 +565,13 @@ impl ScriptThreadFactory for ScriptThread {
}
impl ScriptThread {
pub fn invoke_perform_a_microtask_checkpoint() {
SCRIPT_THREAD_ROOT.with(|root| {
let script_thread = unsafe { &*root.get().unwrap() };
script_thread.perform_a_microtask_checkpoint()
})
}
pub fn page_headers_available(id: &PipelineId, metadata: Option<Metadata>)
-> Option<Root<ServoParser>> {
SCRIPT_THREAD_ROOT.with(|root| {
@ -694,7 +701,7 @@ impl ScriptThread {
content_process_shutdown_chan: state.content_process_shutdown_chan,
promise_job_queue: PromiseJobQueue::new(),
microtask_queue: MicrotaskQueue::default(),
layout_to_constellation_chan: state.layout_to_constellation_chan,
@ -776,6 +783,7 @@ impl ScriptThread {
let mut mouse_move_event_index = None;
let mut animation_ticks = HashSet::new();
loop {
// https://html.spec.whatwg.org/multipage/#event-loop-processing-model step 7
match event {
// This has to be handled before the ResizeMsg below,
// otherwise the page may not have been added to the
@ -788,6 +796,7 @@ impl ScriptThread {
})
}
FromConstellation(ConstellationControlMsg::Resize(id, size, size_type)) => {
// step 7.7
self.profile_event(ScriptThreadEventCategory::Resize, || {
self.handle_resize(id, size, size_type);
})
@ -804,6 +813,7 @@ impl ScriptThread {
}
FromConstellation(ConstellationControlMsg::TickAllAnimations(
pipeline_id)) => {
// step 7.8
if !animation_ticks.contains(&pipeline_id) {
animation_ticks.insert(pipeline_id);
sequential.push(event);
@ -868,11 +878,16 @@ impl ScriptThread {
None
});
// https://html.spec.whatwg.org/multipage/#event-loop-processing-model step 6
self.perform_a_microtask_checkpoint();
if let Some(retval) = result {
return retval
}
}
// https://html.spec.whatwg.org/multipage/#event-loop-processing-model step 7.12
// Issue batched reflows on any pages that require it (e.g. if images loaded)
// TODO(gw): In the future we could probably batch other types of reflows
// into this loop too, but for now it's only images.
@ -1452,49 +1467,11 @@ impl ScriptThread {
}
pub fn dispatch_job_queue(&self, job_handler: Box<AsyncJobHandler>) {
let scope_url = job_handler.scope_url.clone();
let queue_ref = self.job_queue_map.0.borrow();
let front_job = {
let job_vec = queue_ref.get(&scope_url);
job_vec.unwrap().first().unwrap()
};
match job_handler.invoke_type {
InvokeType::Run => (&*self.job_queue_map).run_job(job_handler, self),
InvokeType::Register => self.job_queue_map.run_register(front_job, job_handler, self),
InvokeType::Update => self.job_queue_map.update(front_job, &*front_job.client.global(), self),
InvokeType::Settle(settle_type) => {
let promise = &front_job.promise;
let global = &*front_job.client.global();
let trusted_global = Trusted::new(global);
let _ac = JSAutoCompartment::new(global.get_cx(), promise.reflector().get_jsobject().get());
match settle_type {
SettleType::Resolve(reg) => promise.resolve_native(global.get_cx(), &*reg.root()),
SettleType::Reject(err) => promise.reject_error(global.get_cx(), err)
}
let finish_job_handler = box FinishJobHandler::new(scope_url, trusted_global);
self.queue_finish_job(finish_job_handler, global);
}
}
self.job_queue_map.run_job(job_handler, self);
}
pub fn queue_serviceworker_job(&self, async_job_handler: Box<AsyncJobHandler>, global: &GlobalScope) {
let _ = self.dom_manipulation_task_source.queue(async_job_handler, &*global);
}
pub fn queue_finish_job(&self, finish_job_handler: Box<FinishJobHandler>, global: &GlobalScope) {
let _ = self.dom_manipulation_task_source.queue(finish_job_handler, global);
}
pub fn invoke_finish_job(&self, finish_job_handler: Box<FinishJobHandler>) {
let job_queue = &*self.job_queue_map;
let global = &*finish_job_handler.global.root();
let scope_url = (*finish_job_handler).scope_url;
job_queue.finish_job(scope_url, global, self);
}
pub fn invoke_job_update(&self, job: &Job, global: &GlobalScope) {
let job_queue = &*self.job_queue_map;
job_queue.update(job, global, self);
pub fn dom_manipulation_task_source(&self) -> &DOMManipulationTaskSource {
&self.dom_manipulation_task_source
}
/// Handles a request for the window title.
@ -2108,30 +2085,15 @@ impl ScriptThread {
}
}
pub fn enqueue_promise_job(job: EnqueuedPromiseCallback, global: &GlobalScope) {
pub fn enqueue_microtask(job: Microtask) {
SCRIPT_THREAD_ROOT.with(|root| {
let script_thread = unsafe { &*root.get().unwrap() };
script_thread.promise_job_queue.enqueue(job, global);
script_thread.microtask_queue.enqueue(job);
});
}
pub fn flush_promise_jobs(global: &GlobalScope) {
SCRIPT_THREAD_ROOT.with(|root| {
let script_thread = unsafe { &*root.get().unwrap() };
let _ = script_thread.dom_manipulation_task_source.queue(
box FlushPromiseJobs, global);
})
}
fn do_flush_promise_jobs(&self) {
self.promise_job_queue.flush_promise_jobs(|id| self.documents.borrow().find_global(id))
}
}
struct FlushPromiseJobs;
impl Runnable for FlushPromiseJobs {
fn main_thread_handler(self: Box<FlushPromiseJobs>, script_thread: &ScriptThread) {
script_thread.do_flush_promise_jobs();
fn perform_a_microtask_checkpoint(&self) {
self.microtask_queue.checkpoint(|id| self.documents.borrow().find_global(id))
}
}

View file

@ -10,20 +10,23 @@
use dom::bindings::cell::DOMRefCell;
use dom::bindings::error::Error;
use dom::bindings::js::JS;
use dom::bindings::refcounted::Trusted;
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 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(PartialEq, Clone, Debug, JSTraceable)]
#[derive(PartialEq, Copy, Clone, Debug, JSTraceable)]
pub enum JobType {
Register,
Unregister,
@ -36,14 +39,6 @@ pub enum SettleType {
Reject(Error)
}
// This encapsulates what operation to invoke of JobQueue from script thread
pub enum InvokeType {
Settle(SettleType),
Run,
Register,
Update
}
#[must_root]
#[derive(JSTraceable)]
pub struct Job {
@ -98,36 +93,14 @@ impl PartialEq for Job {
}
}
pub struct FinishJobHandler {
pub scope_url: ServoUrl,
pub global: Trusted<GlobalScope>,
}
impl FinishJobHandler {
pub fn new(scope_url: ServoUrl, global: Trusted<GlobalScope>) -> FinishJobHandler {
FinishJobHandler {
scope_url: scope_url,
global: global
}
}
}
impl Runnable for FinishJobHandler {
fn main_thread_handler(self: Box<FinishJobHandler>, script_thread: &ScriptThread) {
script_thread.invoke_finish_job(self);
}
}
pub struct AsyncJobHandler {
pub scope_url: ServoUrl,
pub invoke_type: InvokeType
}
impl AsyncJobHandler {
fn new(scope_url: ServoUrl, invoke_type: InvokeType) -> AsyncJobHandler {
fn new(scope_url: ServoUrl) -> AsyncJobHandler {
AsyncJobHandler {
scope_url: scope_url,
invoke_type: invoke_type
}
}
}
@ -153,25 +126,29 @@ impl JobQueue {
job: Job,
global: &GlobalScope,
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 = AsyncJobHandler::new(scope_url, InvokeType::Run);
script_thread.queue_serviceworker_job(box run_job_handler, global);
let run_job_handler = box AsyncJobHandler::new(scope_url);
let _ = script_thread.dom_manipulation_task_source().queue(run_job_handler, global);
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");
} else {
// restore the popped last_job
job_queue.push(last_job);
// and push this new job to job queue
job_queue.push(job);
debug!("pushed onto job queue job");
}
}
}
@ -179,41 +156,46 @@ impl JobQueue {
#[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) {
let queue_ref = &*self.0.borrow();
let front_job = {
let job_vec = queue_ref.get(&run_job_handler.scope_url);
job_vec.unwrap().first().unwrap()
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);
job_vec.unwrap().first().unwrap()
};
let 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::Update => self.update(front_job, script_thread),
JobType::Unregister => unreachable!(),
};
scope_url
};
let global = &*front_job.client.global();
let handler = *run_job_handler;
match front_job.job_type {
JobType::Register => {
let register_job_handler = AsyncJobHandler::new(handler.scope_url, InvokeType::Register);
script_thread.queue_serviceworker_job(box register_job_handler, global);
},
JobType::Update => {
let update_job_handler = AsyncJobHandler::new(handler.scope_url, InvokeType::Update);
script_thread.queue_serviceworker_job(box update_job_handler, global);
}
_ => { /* TODO implement Unregister */ }
}
self.finish_job(url, script_thread);
}
#[allow(unrooted_must_root)]
// https://w3c.github.io/ServiceWorker/#register-algorithm
pub fn run_register(&self, job: &Job, register_job_handler: Box<AsyncJobHandler>, script_thread: &ScriptThread) {
let global = &*job.client.global();
fn run_register(&self, job: &Job, register_job_handler: Box<AsyncJobHandler>, 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) {
let settle_type = SettleType::Reject(Error::Type("Invalid script ServoURL".to_owned()));
let async_job_handler = AsyncJobHandler::new(scope_url, InvokeType::Settle(settle_type));
return script_thread.queue_serviceworker_job(box async_job_handler, global);
// 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;
} else if job.script_url.origin() != job.referrer.origin() || job.scope_url.origin() != job.referrer.origin() {
let settle_type = SettleType::Reject(Error::Security);
let async_job_handler = AsyncJobHandler::new(scope_url, InvokeType::Settle(settle_type));
return script_thread.queue_serviceworker_job(box async_job_handler, global);
// Step 2.1/3.1
reject_job_promise(job,
Error::Security,
script_thread.dom_manipulation_task_source());
// Step 2.2/3.2 (see run_job)
return;
}
// Step 4-5
if let Some(reg) = script_thread.handle_get_registration(&job.scope_url) {
// Step 5.1
@ -223,70 +205,144 @@ impl JobQueue {
// Step 5.3
if let Some(ref newest_worker) = reg.get_newest_worker() {
if (&*newest_worker).get_script_url() == job.script_url {
let settle_type = SettleType::Resolve(Trusted::new(&*reg));
let async_job_handler = AsyncJobHandler::new(scope_url, InvokeType::Settle(settle_type));
script_thread.queue_serviceworker_job(box async_job_handler, global);
let finish_job_handler = box FinishJobHandler::new(job.scope_url.clone(), Trusted::new(&*global));
script_thread.queue_finish_job(finish_job_handler, &*global);
// Step 5.3.1
resolve_job_promise(job, &*reg, script_thread.dom_manipulation_task_source());
// Step 5.3.2 (see run_job)
return;
}
}
} else {
// Step 6.1
let global = &*job.client.global();
let pipeline = global.pipeline_id();
let new_reg = ServiceWorkerRegistration::new(&*global, &job.script_url, scope_url);
script_thread.handle_serviceworker_registration(&job.scope_url, &*new_reg, pipeline);
}
// Step 7
script_thread.invoke_job_update(job, &*global);
self.update(job, script_thread)
}
#[allow(unrooted_must_root)]
// https://w3c.github.io/ServiceWorker/#finish-job-algorithm
pub fn finish_job(&self, scope_url: ServoUrl, global: &GlobalScope, script_thread: &ScriptThread) {
if let Some(job_vec) = (*self.0.borrow_mut()).get_mut(&scope_url) {
if job_vec.first().map_or(false, |job| job.scope_url == scope_url) {
let _ = job_vec.remove(0);
}
if !job_vec.is_empty() {
let run_job_handler = AsyncJobHandler::new(scope_url, InvokeType::Run);
script_thread.queue_serviceworker_job(box run_job_handler, global);
}
pub fn finish_job(&self, scope_url: ServoUrl, script_thread: &ScriptThread) {
debug!("finishing previous job");
let run_job = if let Some(job_vec) = (*self.0.borrow_mut()).get_mut(&scope_url) {
assert_eq!(job_vec.first().as_ref().unwrap().scope_url, scope_url);
let _ = job_vec.remove(0);
!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);
}
}
// https://w3c.github.io/ServiceWorker/#update-algorithm
pub fn update(&self, job: &Job, global: &GlobalScope, script_thread: &ScriptThread) {
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) {
Some(reg) => reg,
None => return
};
// Step 1
if reg.get_uninstalling() {
let err_type = Error::Type("Update called on an uninstalling registration".to_owned());
let settle_type = SettleType::Reject(err_type);
let async_job_handler = AsyncJobHandler::new(job.scope_url.clone(), InvokeType::Settle(settle_type));
return script_thread.queue_serviceworker_job(box async_job_handler, global);
}
let newest_worker = match reg.get_newest_worker() {
Some(worker) => worker,
None => return
None => {
let err_type = Error::Type("No registration to update".to_owned());
// Step 2.1
reject_job_promise(job, err_type, script_thread.dom_manipulation_task_source());
// Step 2.2 (see run_job)
return;
}
};
// Step 2
if (&*newest_worker).get_script_url() == job.script_url && job.job_type == JobType::Update {
// Step 4
let err_type = Error::Type("Invalid script ServoURL".to_owned());
let settle_type = SettleType::Reject(err_type);
let async_job_handler = AsyncJobHandler::new(job.scope_url.clone(), InvokeType::Settle(settle_type));
script_thread.queue_serviceworker_job(box async_job_handler, global);
} else {
job.client.set_controller(&*newest_worker);
let settle_type = SettleType::Resolve(Trusted::new(&*reg));
let async_job_handler = AsyncJobHandler::new(job.scope_url.clone(), InvokeType::Settle(settle_type));
script_thread.queue_serviceworker_job(box async_job_handler, global);
if reg.get_uninstalling() {
let err_type = Error::Type("Update called on an uninstalling registration".to_owned());
// Step 2.1
reject_job_promise(job, err_type, script_thread.dom_manipulation_task_source());
// Step 2.2 (see run_job)
return;
}
let finish_job_handler = box FinishJobHandler::new(job.scope_url.clone(), Trusted::new(global));
script_thread.queue_finish_job(finish_job_handler, global);
// Step 3
let newest_worker = reg.get_newest_worker();
let newest_worker_url = newest_worker.as_ref().map(|w| w.get_script_url());
// Step 4
if newest_worker_url.as_ref() == Some(&job.script_url) && job.job_type == JobType::Update {
let err_type = Error::Type("Invalid script ServoURL".to_owned());
// Step 4.1
reject_job_promise(job, err_type, script_thread.dom_manipulation_task_source());
// Step 4.2 (see run_job)
return;
}
// Step 8
if let Some(newest_worker) = newest_worker {
job.client.set_controller(&*newest_worker);
// 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),
};
}
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);
}
// 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
for job in &job.equivalent_jobs {
queue_settle_promise_for_job(job, settle.clone(), task_source);
}
}
fn reject_job_promise(job: &Job, err: Error, task_source: &DOMManipulationTaskSource) {
queue_settle_promise(job, SettleType::Reject(err), task_source)
}
fn resolve_job_promise(job: &Job, reg: &ServiceWorkerRegistration, task_source: &DOMManipulationTaskSource) {
queue_settle_promise(job, SettleType::Resolve(Trusted::new(reg)), task_source)
}

View file

@ -1,6 +0,0 @@
[microtask_after_raf.html]
type: testharness
disabled: https://github.com/servo/servo/issues/13501
[Microtask execute immediately after script]
expected: FAIL

View file

@ -1,5 +0,0 @@
[microtask_after_script.html]
type: testharness
[Microtask immediately after script]
expected: FAIL

View file

@ -1,8 +1,5 @@
[task_microtask_ordering.html]
type: testharness
[Basic task and microtask ordering]
expected: FAIL
[Level 1 bossfight (synthetic click)]
expected: FAIL
bug: https://github.com/servo/servo/issues/1980