Skip to content

Commit

Permalink
Improve spec-compliance of script loading and execution during docume…
Browse files Browse the repository at this point in the history
…nt startup

Including proper support for async and deferred scripts.
  • Loading branch information
tschneidereit committed Oct 26, 2015
1 parent 6b95c39 commit a0c5d47
Show file tree
Hide file tree
Showing 9 changed files with 298 additions and 128 deletions.
31 changes: 10 additions & 21 deletions components/script/document_loader.rs
Expand Up @@ -8,9 +8,7 @@
use msg::constellation_msg::{PipelineId};
use net_traits::AsyncResponseTarget;
use net_traits::{Metadata, PendingAsyncLoad, ResourceTask, load_whole_resource};
use script_task::MainThreadScriptMsg;
use std::sync::Arc;
use std::sync::mpsc::Sender;
use url::Url;

#[derive(JSTraceable, PartialEq, Clone, Debug, HeapSizeOf)]
Expand Down Expand Up @@ -40,15 +38,9 @@ pub struct DocumentLoader {
/// are lots of iframes.
#[ignore_heap_size_of = "channels are hard"]
pub resource_task: Arc<ResourceTask>,
notifier_data: Option<NotifierData>,
pipeline: Option<PipelineId>,
blocking_loads: Vec<LoadType>,
}

#[derive(JSTraceable, HeapSizeOf)]
pub struct NotifierData {
#[ignore_heap_size_of = "trait objects are hard"]
pub script_chan: Sender<MainThreadScriptMsg>,
pub pipeline: PipelineId,
events_inhibited: bool,
}

impl DocumentLoader {
Expand All @@ -59,15 +51,16 @@ impl DocumentLoader {
/// We use an `Arc<ResourceTask>` here in order to avoid file descriptor exhaustion when there
/// are lots of iframes.
pub fn new_with_task(resource_task: Arc<ResourceTask>,
data: Option<NotifierData>,
pipeline: Option<PipelineId>,
initial_load: Option<Url>)
-> DocumentLoader {
let initial_loads = initial_load.into_iter().map(LoadType::PageSource).collect();

DocumentLoader {
resource_task: resource_task,
notifier_data: data,
pipeline: pipeline,
blocking_loads: initial_loads,
events_inhibited: false,
}
}

Expand All @@ -76,8 +69,7 @@ impl DocumentLoader {
pub fn prepare_async_load(&mut self, load: LoadType) -> PendingAsyncLoad {
let url = load.url().clone();
self.blocking_loads.push(load);
let pipeline = self.notifier_data.as_ref().map(|data| data.pipeline);
PendingAsyncLoad::new((*self.resource_task).clone(), url, pipeline)
PendingAsyncLoad::new((*self.resource_task).clone(), url, self.pipeline)
}

/// Create and initiate a new network request.
Expand All @@ -98,12 +90,6 @@ impl DocumentLoader {
pub fn finish_load(&mut self, load: LoadType) {
let idx = self.blocking_loads.iter().position(|unfinished| *unfinished == load);
self.blocking_loads.remove(idx.expect(&format!("unknown completed load {:?}", load)));

if let Some(NotifierData { ref script_chan, pipeline }) = self.notifier_data {
if !self.is_blocked() {
script_chan.send(MainThreadScriptMsg::DocumentLoadsComplete(pipeline)).unwrap();
}
}
}

pub fn is_blocked(&self) -> bool {
Expand All @@ -112,6 +98,9 @@ impl DocumentLoader {
}

pub fn inhibit_events(&mut self) {
self.notifier_data = None;
self.events_inhibited = true;
}
pub fn events_inhibited(&self) -> bool {
self.events_inhibited
}
}
2 changes: 2 additions & 0 deletions components/script/dom/bindings/trace.rs
Expand Up @@ -53,6 +53,7 @@ use layout_interface::{LayoutChan, LayoutRPC};
use libc;
use msg::constellation_msg::ConstellationChan;
use msg::constellation_msg::{PipelineId, SubpageId, WindowSizeData, WorkerId};
use net_traits::Metadata;
use net_traits::image::base::Image;
use net_traits::image_cache_task::{ImageCacheChan, ImageCacheTask};
use net_traits::storage_task::StorageType;
Expand Down Expand Up @@ -277,6 +278,7 @@ no_jsmanaged_fields!(Rect<T>);
no_jsmanaged_fields!(Size2D<T>);
no_jsmanaged_fields!(Arc<T>);
no_jsmanaged_fields!(Image, ImageCacheChan, ImageCacheTask);
no_jsmanaged_fields!(Metadata);
no_jsmanaged_fields!(Atom, Namespace);
no_jsmanaged_fields!(Trusted<T: Reflectable>);
no_jsmanaged_fields!(PropertyDeclarationBlock);
Expand Down
206 changes: 177 additions & 29 deletions components/script/dom/document.rs
Expand Up @@ -81,9 +81,9 @@ use msg::constellation_msg::{ALT, CONTROL, SHIFT, SUPER};
use msg::constellation_msg::{ConstellationChan, FocusType, Key, KeyModifiers, KeyState, MozBrowserEvent, SubpageId};
use net_traits::ControlMsg::{GetCookiesForUrl, SetCookiesForUrl};
use net_traits::CookieSource::NonHTTP;
use net_traits::{AsyncResponseTarget, Metadata, PendingAsyncLoad};
use net_traits::{AsyncResponseTarget, PendingAsyncLoad};
use num::ToPrimitive;
use script_task::Runnable;
use script_task::{MainThreadScriptMsg, Runnable};
use script_traits::{MouseButton, UntrustedNodeAddress};
use std::ascii::AsciiExt;
use std::borrow::ToOwned;
Expand All @@ -107,6 +107,12 @@ pub enum IsHTMLDocument {
NonHTMLDocument,
}

#[derive(PartialEq)]
enum ParserBlockedByScript {
Blocked,
Unblocked,
}

// https://dom.spec.whatwg.org/#document
#[dom_struct]
pub struct Document {
Expand All @@ -129,12 +135,24 @@ pub struct Document {
anchors: MutNullableHeap<JS<HTMLCollection>>,
applets: MutNullableHeap<JS<HTMLCollection>>,
ready_state: Cell<DocumentReadyState>,
/// Whether the DOMContentLoaded event has already been dispatched.
domcontentloaded_dispatched: Cell<bool>,
/// The element that has most recently requested focus for itself.
possibly_focused: MutNullableHeap<JS<Element>>,
/// The element that currently has the document focus context.
focused: MutNullableHeap<JS<Element>>,
/// The script element that is currently executing.
current_script: MutNullableHeap<JS<HTMLScriptElement>>,
/// https://html.spec.whatwg.org/multipage/#pending-parsing-blocking-script
pending_parsing_blocking_script: MutNullableHeap<JS<HTMLScriptElement>>,
/// Number of stylesheets that block executing the next parser-inserted script
script_blocking_stylesheets_count: Cell<u32>,
/// https://html.spec.whatwg.org/multipage/#list-of-scripts-that-will-execute-when-the-document-has-finished-parsing
deferred_scripts: DOMRefCell<Vec<JS<HTMLScriptElement>>>,
/// https://html.spec.whatwg.org/multipage/#list-of-scripts-that-will-execute-in-order-as-soon-as-possible
asap_in_order_scripts_list: DOMRefCell<Vec<JS<HTMLScriptElement>>>,
/// https://html.spec.whatwg.org/multipage/#set-of-scripts-that-will-execute-as-soon-as-possible
asap_scripts_set: DOMRefCell<Vec<JS<HTMLScriptElement>>>,
/// https://html.spec.whatwg.org/multipage/#concept-n-noscript
/// True if scripting is enabled for all scripts in this document
scripting_enabled: Cell<bool>,
Expand Down Expand Up @@ -892,6 +910,42 @@ impl Document {
self.current_script.set(script);
}

pub fn get_script_blocking_stylesheets_count(&self) -> u32 {
self.script_blocking_stylesheets_count.get()
}

pub fn increment_script_blocking_stylesheet_count(&self) {
let count_cell = &self.script_blocking_stylesheets_count;
count_cell.set(count_cell.get() + 1);
}

pub fn decrement_script_blocking_stylesheet_count(&self) {
let count_cell = &self.script_blocking_stylesheets_count;
assert!(count_cell.get() > 0);
count_cell.set(count_cell.get() - 1);
}

pub fn set_pending_parsing_blocking_script(&self, script: Option<&HTMLScriptElement>) {
assert!(self.get_pending_parsing_blocking_script().is_none() || script.is_none());
self.pending_parsing_blocking_script.set(script);
}

pub fn get_pending_parsing_blocking_script(&self) -> Option<Root<HTMLScriptElement>> {
self.pending_parsing_blocking_script.get()
}

pub fn add_deferred_script(&self, script: &HTMLScriptElement) {
self.deferred_scripts.borrow_mut().push(JS::from_ref(script));
}

pub fn add_asap_script(&self, script: &HTMLScriptElement) {
self.asap_scripts_set.borrow_mut().push(JS::from_ref(script));
}

pub fn push_asap_in_order_script(&self, script: &HTMLScriptElement) {
self.asap_in_order_scripts_list.borrow_mut().push(JS::from_ref(script));
}

pub fn trigger_mozbrowser_event(&self, event: MozBrowserEvent) {
if htmliframeelement::mozbrowser_enabled() {
if let Some((containing_pipeline_id, subpage_id)) = self.window.parent_info() {
Expand Down Expand Up @@ -966,14 +1020,120 @@ impl Document {
loader.load_async(load, listener)
}

pub fn load_sync(&self, load: LoadType) -> Result<(Metadata, Vec<u8>), String> {
let mut loader = self.loader.borrow_mut();
loader.load_sync(load)
pub fn finish_load(&self, load: LoadType) {
// The parser might need the loader, so restrict the lifetime of the borrow.
{
let mut loader = self.loader.borrow_mut();
loader.finish_load(load.clone());
}

if let LoadType::Script(_) = load {
self.process_deferred_scripts();
self.process_asap_scripts();
}

if self.maybe_execute_parser_blocking_script() == ParserBlockedByScript::Blocked {
return;
}

// A finished resource load can potentially unblock parsing. In that case, resume the
// parser so its loop can find out.
if let Some(parser) = self.current_parser.get_rooted() {
if parser.is_suspended() {
parser.resume();
}
}

let loader = self.loader.borrow();
if !loader.is_blocked() && !loader.events_inhibited() {
let win = self.window();
let msg = MainThreadScriptMsg::DocumentLoadsComplete(win.pipeline());
win.main_thread_script_chan().send(msg).unwrap();
}
}

pub fn finish_load(&self, load: LoadType) {
let mut loader = self.loader.borrow_mut();
loader.finish_load(load);
/// If document parsing is blocked on a script, and that script is ready to run,
/// execute it.
/// https://html.spec.whatwg.org/multipage/#ready-to-be-parser-executed
fn maybe_execute_parser_blocking_script(&self) -> ParserBlockedByScript {
let script = match self.pending_parsing_blocking_script.get_rooted() {
None => return ParserBlockedByScript::Unblocked,
Some(script) => script,
};

if self.script_blocking_stylesheets_count.get() == 0 &&
script.r().is_ready_to_be_executed() {
script.r().execute();
self.pending_parsing_blocking_script.set(None);
return ParserBlockedByScript::Unblocked;
}
ParserBlockedByScript::Blocked
}

/// https://html.spec.whatwg.org/multipage/#the-end step 3
pub fn process_deferred_scripts(&self) {
if self.ready_state.get() != DocumentReadyState::Interactive {
return;
}
// Part of substep 1.
if self.script_blocking_stylesheets_count.get() > 0 {
return;
}
let mut deferred_scripts = self.deferred_scripts.borrow_mut();
while !deferred_scripts.is_empty() {
let script = deferred_scripts[0].root();
// Part of substep 1.
if !script.is_ready_to_be_executed() {
return;
}
// Substep 2.
script.execute();
// Substep 3.
deferred_scripts.remove(0);
// Substep 4 (implicit).
}
// https://html.spec.whatwg.org/multipage/#the-end step 4.
self.maybe_dispatch_dom_content_loaded();
}

/// https://html.spec.whatwg.org/multipage/#the-end step 5 and the latter parts of
/// https://html.spec.whatwg.org/multipage/#prepare-a-script 15.d and 15.e.
pub fn process_asap_scripts(&self) {
// Execute the first in-order asap-executed script if it's ready, repeat as required.
// Re-borrowing the list for each step because it can also be borrowed under execute.
while self.asap_in_order_scripts_list.borrow().len() > 0 {
let script = self.asap_in_order_scripts_list.borrow()[0].root();
if !script.r().is_ready_to_be_executed() {
break;
}
script.r().execute();
self.asap_in_order_scripts_list.borrow_mut().remove(0);
}

let mut idx = 0;
// Re-borrowing the set for each step because it can also be borrowed under execute.
while idx < self.asap_scripts_set.borrow().len() {
let script = self.asap_scripts_set.borrow()[idx].root();
if !script.r().is_ready_to_be_executed() {
idx += 1;
continue;
}
script.r().execute();
self.asap_scripts_set.borrow_mut().swap_remove(idx);
}
}

pub fn maybe_dispatch_dom_content_loaded(&self) {
if self.domcontentloaded_dispatched.get() {
return;
}
self.domcontentloaded_dispatched.set(true);
let event = Event::new(GlobalRef::Window(self.window()), "DOMContentLoaded".to_owned(),
EventBubbles::DoesNotBubble,
EventCancelable::NotCancelable);
let doctarget = self.upcast::<EventTarget>();
let _ = doctarget.DispatchEvent(event.r());
self.window().reflow(ReflowGoal::ForDisplay, ReflowQueryType::NoQuery, ReflowReason::DOMContentLoaded);
}

pub fn notify_constellation_load(&self) {
Expand Down Expand Up @@ -1039,10 +1199,10 @@ impl Document {
doc_loader: DocumentLoader) -> Document {
let url = url.unwrap_or_else(|| Url::parse("about:blank").unwrap());

let ready_state = if source == DocumentSource::FromParser {
DocumentReadyState::Loading
let (ready_state, domcontentloaded_dispatched) = if source == DocumentSource::FromParser {
(DocumentReadyState::Loading, false)
} else {
DocumentReadyState::Complete
(DocumentReadyState::Complete, true)
};

Document {
Expand Down Expand Up @@ -1075,9 +1235,15 @@ impl Document {
anchors: Default::default(),
applets: Default::default(),
ready_state: Cell::new(ready_state),
domcontentloaded_dispatched: Cell::new(domcontentloaded_dispatched),
possibly_focused: Default::default(),
focused: Default::default(),
current_script: Default::default(),
pending_parsing_blocking_script: Default::default(),
script_blocking_stylesheets_count: Cell::new(0u32),
deferred_scripts: DOMRefCell::new(vec!()),
asap_in_order_scripts_list: DOMRefCell::new(vec!()),
asap_scripts_set: DOMRefCell::new(vec!()),
scripting_enabled: Cell::new(true),
animation_frame_ident: Cell::new(0),
animation_frame_list: RefCell::new(HashMap::new()),
Expand Down Expand Up @@ -1875,7 +2041,6 @@ fn is_scheme_host_port_tuple(url: &Url) -> bool {

#[derive(HeapSizeOf)]
pub enum DocumentProgressTask {
DOMContentLoaded,
Load,
}

Expand All @@ -1892,20 +2057,6 @@ impl DocumentProgressHandler {
}
}

fn dispatch_dom_content_loaded(&self) {
let document = self.addr.root();
let window = document.r().window();
let event = Event::new(GlobalRef::Window(window), "DOMContentLoaded".to_owned(),
EventBubbles::DoesNotBubble,
EventCancelable::NotCancelable);
let doctarget = document.upcast::<EventTarget>();
let _ = doctarget.DispatchEvent(event.r());

window.reflow(ReflowGoal::ForDisplay,
ReflowQueryType::NoQuery,
ReflowReason::DOMContentLoaded);
}

fn set_ready_state_complete(&self) {
let document = self.addr.root();
document.r().set_ready_state(DocumentReadyState::Complete);
Expand Down Expand Up @@ -1949,9 +2100,6 @@ impl Runnable for DocumentProgressHandler {
let window = document.r().window();
if window.is_alive() {
match self.task {
DocumentProgressTask::DOMContentLoaded => {
self.dispatch_dom_content_loaded();
}
DocumentProgressTask::Load => {
self.set_ready_state_complete();
self.dispatch_load();
Expand Down

0 comments on commit a0c5d47

Please sign in to comment.