diff options
Diffstat (limited to 'components/script/script_task.rs')
-rw-r--r-- | components/script/script_task.rs | 854 |
1 files changed, 475 insertions, 379 deletions
diff --git a/components/script/script_task.rs b/components/script/script_task.rs index fd608fe4284..238763f3035 100644 --- a/components/script/script_task.rs +++ b/components/script/script_task.rs @@ -3,7 +3,19 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ //! The script task is the task that owns the DOM in memory, runs JavaScript, and spawns parsing -//! and layout tasks. +//! and layout tasks. It's in charge of processing events for all same-origin pages in a frame +//! tree, and manages the entire lifetime of pages in the frame tree from initial request to +//! teardown. +//! +//! Page loads follow a two-step process. When a request for a new page load is received, the +//! network request is initiated and the relevant data pertaining to the new page is stashed. +//! While the non-blocking request is ongoing, the script task is free to process further events, +//! noting when they pertain to ongoing loads (such as resizes/viewport adjustments). When the +//! initial response is received for an ongoing load, the second phase starts - the frame tree +//! entry is created, along with the Window and Document objects, and the appropriate parser +//! takes over the response body. Once parsing is complete, the document lifecycle for loading +//! a page runs its course and the script task returns to processing events in the main event +//! loop. #![allow(unsafe_blocks)] @@ -20,13 +32,12 @@ use dom::bindings::trace::JSTraceable; use dom::bindings::utils::{wrap_for_same_compartment, pre_wrap}; use dom::document::{Document, IsHTMLDocument, DocumentHelpers, DocumentProgressHandler, DocumentProgressTask, DocumentSource}; use dom::element::{Element, AttributeHandlers}; -use dom::event::{Event, EventHelpers}; +use dom::event::{Event, EventHelpers, EventBubbles, EventCancelable}; +use dom::htmliframeelement::HTMLIFrameElementHelpers; use dom::uievent::UIEvent; use dom::eventtarget::EventTarget; -use dom::mouseevent::MouseEvent; -use dom::node::{self, Node, NodeHelpers, NodeDamage}; -use dom::window::{Window, WindowHelpers, ScriptHelpers}; -use dom::worker::{Worker, TrustedWorkerAddress}; +use dom::node::{self, Node, NodeHelpers, NodeDamage, window_from_node}; +use dom::window::{Window, WindowHelpers, ScriptHelpers, ReflowReason}; use parse::html::{HTMLInput, parse_html}; use layout_interface::{ScriptLayoutChan, LayoutChan, ReflowGoal, ReflowQueryType}; use layout_interface; @@ -50,20 +61,21 @@ use msg::constellation_msg::{LoadData, PipelineId, SubpageId}; use msg::constellation_msg::{Failure, Msg, WindowSizeData, PipelineExitType}; use msg::constellation_msg::Msg as ConstellationMsg; use net::image_cache_task::ImageCacheTask; -use net::resource_task::{ResourceTask, ControlMsg}; +use net::resource_task::{ResourceTask, ControlMsg, LoadResponse}; use net::resource_task::LoadData as NetLoadData; use net::storage_task::StorageTask; use string_cache::Atom; use util::geometry::to_frac_px; use util::smallvec::SmallVec; use util::str::DOMString; -use util::task::spawn_named_with_send_on_failure; +use util::task::{spawn_named, spawn_named_with_send_on_failure}; use util::task_state; +use geom::Rect; use geom::point::Point2D; use hyper::header::{LastModified, Headers}; -use js::jsapi::{JS_SetWrapObjectCallbacks, JS_SetGCZeal, JS_DEFAULT_ZEAL_FREQ, JS_GC}; -use js::jsapi::{JSContext, JSRuntime, JSObject}; +use js::jsapi::{JS_SetWrapObjectCallbacks, JS_SetGCZeal, JS_SetExtraGCRootsTracer, JS_DEFAULT_ZEAL_FREQ}; +use js::jsapi::{JSContext, JSRuntime, JSObject, JSTracer}; use js::jsapi::{JS_SetGCParameter, JSGC_MAX_BYTES}; use js::jsapi::{JS_SetGCCallback, JSGCStatus, JSGC_BEGIN, JSGC_END}; use js::rust::{Cx, RtUtils}; @@ -71,11 +83,13 @@ use js; use url::Url; use libc; +use std::ascii::AsciiExt; use std::any::Any; use std::borrow::ToOwned; -use std::cell::Cell; -use std::mem::replace; +use std::cell::{Cell, RefCell}; use std::num::ToPrimitive; +use std::option::Option; +use std::ptr; use std::rc::Rc; use std::result::Result; use std::sync::mpsc::{channel, Sender, Receiver, Select}; @@ -83,6 +97,53 @@ use std::u32; use time::Tm; thread_local!(pub static STACK_ROOTS: Cell<Option<RootCollectionPtr>> = Cell::new(None)); +thread_local!(static SCRIPT_TASK_ROOT: RefCell<Option<*const ScriptTask>> = RefCell::new(None)); + +unsafe extern fn trace_script_tasks(tr: *mut JSTracer, _data: *mut libc::c_void) { + SCRIPT_TASK_ROOT.with(|root| { + if let Some(script_task) = *root.borrow() { + (*script_task).trace(tr); + } + }); +} + +/// A document load that is in the process of fetching the requested resource. Contains +/// data that will need to be present when the document and frame tree entry are created, +/// but is only easily available at initiation of the load and on a push basis (so some +/// data will be updated according to future resize events, viewport changes, etc.) +#[jstraceable] +struct InProgressLoad { + /// The pipeline which requested this load. + pipeline_id: PipelineId, + /// The parent pipeline and child subpage associated with this load, if any. + subpage_id: Option<(PipelineId, SubpageId)>, + /// The current window size associated with this pipeline. + window_size: Option<WindowSizeData>, + /// Channel to the layout task associated with this pipeline. + layout_chan: LayoutChan, + /// The current viewport clipping rectangle applying to this pipelie, if any. + clip_rect: Option<Rect<f32>>, + /// The requested URL of the load. + url: Url, +} + +impl InProgressLoad { + /// Create a new InProgressLoad object. + fn new(id: PipelineId, + subpage_id: Option<(PipelineId, SubpageId)>, + layout_chan: LayoutChan, + window_size: Option<WindowSizeData>, + url: Url) -> InProgressLoad { + InProgressLoad { + pipeline_id: id, + subpage_id: subpage_id, + layout_chan: layout_chan, + window_size: window_size, + clip_rect: None, + url: url, + } + } +} #[derive(Copy)] pub enum TimerSource { @@ -102,7 +163,7 @@ pub enum ScriptMsg { TriggerFragment(PipelineId, String), /// Begins a content-initiated load on the specified pipeline (only /// dispatched to ScriptTask). - TriggerLoad(PipelineId, LoadData), + Navigate(PipelineId, LoadData), /// Fires a JavaScript timeout /// TimerSource must be FromWindow when dispatched to ScriptTask and /// must be FromWorker when dispatched to a DedicatedGlobalWorkerScope @@ -113,12 +174,12 @@ pub enum ScriptMsg { /// Message sent through Worker.postMessage (only dispatched to /// DedicatedWorkerGlobalScope). DOMMessage(StructuredCloneData), - /// Sends a message to the Worker object (dispatched to all tasks) regarding error. - WorkerDispatchErrorEvent(TrustedWorkerAddress, DOMString, DOMString, u32, u32), /// Generic message that encapsulates event handling. RunnableMsg(Box<Runnable+Send>), /// A DOM object's last pinned reference was removed (dispatched to all tasks). RefcountCleanup(TrustedReference), + /// The final network response for a page has arrived. + PageFetchComplete(PipelineId, Option<SubpageId>, LoadResponse), } /// A cloneable interface for communicating with an event loop. @@ -174,13 +235,18 @@ impl Drop for StackRootTLS { /// frames. /// /// FIXME: Rename to `Page`, following WebKit? +#[jstraceable] pub struct ScriptTask { /// A handle to the information pertaining to page layout - page: DOMRefCell<Rc<Page>>, + page: DOMRefCell<Option<Rc<Page>>>, + /// A list of data pertaining to loads that have not yet received a network response + incomplete_loads: DOMRefCell<Vec<InProgressLoad>>, /// A handle to the image cache task. image_cache_task: ImageCacheTask, /// A handle to the resource task. resource_task: ResourceTask, + /// A handle to the storage task. + storage_task: StorageTask, /// The port on which the script task receives messages (load URL, exit, etc.) port: Receiver<ScriptMsg>, @@ -243,7 +309,8 @@ impl<'a> Drop for ScriptMemoryFailsafe<'a> { unsafe { let page = owner.page.borrow_for_script_deallocation(); for page in page.iter() { - *page.unsafe_mut_js_info() = None; + let window = page.window().root(); + window.r().clear_js_context_for_script_deallocation(); } *owner.js_context.borrow_for_script_deallocation() = None; } @@ -275,15 +342,14 @@ impl ScriptTaskFactory for ScriptTask { storage_task: StorageTask, image_cache_task: ImageCacheTask, devtools_chan: Option<DevtoolsControlChan>, - window_size: WindowSizeData) + window_size: Option<WindowSizeData>, + load_data: LoadData) where C: ScriptListener + Send + 'static { let ConstellationChan(const_chan) = constellation_chan.clone(); let (script_chan, script_port) = channel(); let layout_chan = LayoutChan(layout_chan.sender()); spawn_named_with_send_on_failure("ScriptTask", task_state::SCRIPT, move || { - let script_task = ScriptTask::new(id, - box compositor as Box<ScriptListener>, - layout_chan, + let script_task = ScriptTask::new(box compositor as Box<ScriptListener>, script_port, NonWorkerScriptChan(script_chan), control_chan, @@ -292,9 +358,17 @@ impl ScriptTaskFactory for ScriptTask { resource_task, storage_task, image_cache_task, - devtools_chan, - window_size); + devtools_chan); + + SCRIPT_TASK_ROOT.with(|root| { + *root.borrow_mut() = Some(&script_task as *const _); + }); let mut failsafe = ScriptMemoryFailsafe::new(&script_task); + + let new_load = InProgressLoad::new(id, None, layout_chan, window_size, + load_data.url.clone()); + script_task.start_page_load(new_load, load_data); + script_task.start(); // This must always be the very last operation performed before the task completes @@ -313,9 +387,7 @@ unsafe extern "C" fn debug_gc_callback(_rt: *mut JSRuntime, status: JSGCStatus) impl ScriptTask { /// Creates a new script task. - pub fn new(id: PipelineId, - compositor: Box<ScriptListener+'static>, - layout_chan: LayoutChan, + pub fn new(compositor: Box<ScriptListener+'static>, port: Receiver<ScriptMsg>, chan: NonWorkerScriptChan, control_chan: ScriptControlChan, @@ -324,8 +396,7 @@ impl ScriptTask { resource_task: ResourceTask, storage_task: StorageTask, img_cache_task: ImageCacheTask, - devtools_chan: Option<DevtoolsControlChan>, - window_size: WindowSizeData) + devtools_chan: Option<DevtoolsControlChan>) -> ScriptTask { let (js_runtime, js_context) = ScriptTask::new_rt_and_cx(); let wrap_for_same_compartment = wrap_for_same_compartment as @@ -349,19 +420,14 @@ impl ScriptTask { Some(pre_wrap)); } - let page = Page::new(id, None, layout_chan, window_size, - resource_task.clone(), - storage_task, - constellation_chan.clone(), - js_context.clone(), - devtools_chan.clone()); - let (devtools_sender, devtools_receiver) = channel(); ScriptTask { - page: DOMRefCell::new(Rc::new(page)), + page: DOMRefCell::new(None), + incomplete_loads: DOMRefCell::new(vec!()), image_cache_task: img_cache_task, resource_task: resource_task, + storage_task: storage_task, port: port, chan: chan, @@ -405,6 +471,7 @@ impl ScriptTask { js_context.set_logging_error_reporter(); unsafe { JS_SetGCZeal((*js_context).ptr, 0, JS_DEFAULT_ZEAL_FREQ); + JS_SetExtraGCRootsTracer((*js_runtime).ptr, Some(trace_script_tasks), ptr::null_mut()); } // Needed for debug assertions about whether GC is running. @@ -418,6 +485,11 @@ impl ScriptTask { (js_runtime, js_context) } + // Return the root page in the frame tree. Panics if it doesn't exist. + fn root_page(&self) -> Rc<Page> { + self.page.borrow().as_ref().unwrap().clone() + } + pub fn get_cx(&self) -> *mut JSContext { (**self.js_context.borrow().as_ref().unwrap()).ptr } @@ -440,17 +512,18 @@ impl ScriptTask { let mut resizes = vec!(); { - let page = self.page.borrow_mut(); - for page in page.iter() { - // Only process a resize if layout is idle. - let layout_join_port = page.layout_join_port.borrow(); - if layout_join_port.is_none() { - let mut resize_event = page.resize_event.get(); - match resize_event.take() { - Some(size) => resizes.push((page.id, size)), - None => () + let page = self.page.borrow(); + if let Some(page) = page.as_ref() { + for page in page.iter() { + // Only process a resize if layout is idle. + let window = page.window().root(); + if window.r().layout_is_idle() { + let resize_event = window.r().steal_resize_event(); + match resize_event { + Some(size) => resizes.push((window.r().pipeline(), size)), + None => () + } } - page.resize_event.set(None); } } } @@ -503,17 +576,10 @@ impl ScriptTask { self.handle_new_layout(new_layout_info); } MixedMessage::FromConstellation(ConstellationControlMsg::Resize(id, size)) => { - let page = self.page.borrow_mut(); - let page = page.find(id).expect("resize sent to nonexistent pipeline"); - page.resize_event.set(Some(size)); + self.handle_resize(id, size); } MixedMessage::FromConstellation(ConstellationControlMsg::Viewport(id, rect)) => { - let page = self.page.borrow_mut(); - let inner_page = page.find(id).expect("Page rect message sent to nonexistent pipeline"); - if inner_page.set_page_clip_rect_with_new_viewport(rect) { - let page = get_page(&*page, id); - self.force_reflow(&*page); - } + self.handle_viewport(id, rect); } _ => { sequential.push(event); @@ -556,8 +622,8 @@ impl ScriptTask { match msg { ConstellationControlMsg::AttachLayout(_) => panic!("should have handled AttachLayout already"), - ConstellationControlMsg::Load(id, parent, load_data) => - self.load(id, parent, load_data), + ConstellationControlMsg::Navigate(pipeline_id, subpage_id, load_data) => + self.handle_navigate(pipeline_id, Some(subpage_id), load_data), ConstellationControlMsg::SendEvent(id, event) => self.handle_event(id, event), ConstellationControlMsg::ReflowComplete(id, reflow_id) => @@ -581,8 +647,8 @@ impl ScriptTask { fn handle_msg_from_script(&self, msg: ScriptMsg) { match msg { - ScriptMsg::TriggerLoad(id, load_data) => - self.trigger_load(id, load_data), + ScriptMsg::Navigate(id, load_data) => + self.handle_navigate(id, None, load_data), ScriptMsg::TriggerFragment(id, fragment) => self.trigger_fragment(id, fragment), ScriptMsg::FireTimer(TimerSource::FromWindow(id), timer_id) => @@ -593,104 +659,141 @@ impl ScriptTask { self.handle_exit_window_msg(id), ScriptMsg::DOMMessage(..) => panic!("unexpected message"), - ScriptMsg::WorkerDispatchErrorEvent(addr, msg, file_name,line_num, col_num) => - Worker::handle_error_message(addr, msg, file_name, line_num, col_num), ScriptMsg::RunnableMsg(runnable) => runnable.handler(), ScriptMsg::RefcountCleanup(addr) => LiveDOMReferences::cleanup(self.get_cx(), addr), + ScriptMsg::PageFetchComplete(id, subpage, response) => + self.handle_page_fetch_complete(id, subpage, response), } } fn handle_msg_from_devtools(&self, msg: DevtoolScriptControlMsg) { + let page = self.root_page(); match msg { DevtoolScriptControlMsg::EvaluateJS(id, s, reply) => - devtools::handle_evaluate_js(&*self.page.borrow(), id, s, reply), + devtools::handle_evaluate_js(&page, id, s, reply), DevtoolScriptControlMsg::GetRootNode(id, reply) => - devtools::handle_get_root_node(&*self.page.borrow(), id, reply), + devtools::handle_get_root_node(&page, id, reply), DevtoolScriptControlMsg::GetDocumentElement(id, reply) => - devtools::handle_get_document_element(&*self.page.borrow(), id, reply), + devtools::handle_get_document_element(&page, id, reply), DevtoolScriptControlMsg::GetChildren(id, node_id, reply) => - devtools::handle_get_children(&*self.page.borrow(), id, node_id, reply), + devtools::handle_get_children(&page, id, node_id, reply), DevtoolScriptControlMsg::GetLayout(id, node_id, reply) => - devtools::handle_get_layout(&*self.page.borrow(), id, node_id, reply), + devtools::handle_get_layout(&page, id, node_id, reply), DevtoolScriptControlMsg::ModifyAttribute(id, node_id, modifications) => - devtools::handle_modify_attribute(&*self.page.borrow(), id, node_id, modifications), + devtools::handle_modify_attribute(&page, id, node_id, modifications), DevtoolScriptControlMsg::WantsLiveNotifications(pipeline_id, to_send) => - devtools::handle_wants_live_notifications(&*self.page.borrow(), pipeline_id, to_send), + devtools::handle_wants_live_notifications(&page, pipeline_id, to_send), } } + fn handle_resize(&self, id: PipelineId, size: WindowSizeData) { + let page = self.page.borrow(); + if let Some(ref page) = page.as_ref() { + if let Some(ref page) = page.find(id) { + let window = page.window().root(); + window.r().set_resize_event(size); + return; + } + } + let mut loads = self.incomplete_loads.borrow_mut(); + if let Some(ref mut load) = loads.iter_mut().find(|load| load.pipeline_id == id) { + load.window_size = Some(size); + return; + } + panic!("resize sent to nonexistent pipeline"); + } + + fn handle_viewport(&self, id: PipelineId, rect: Rect<f32>) { + let page = self.page.borrow(); + if let Some(page) = page.as_ref() { + if let Some(ref inner_page) = page.find(id) { + let window = inner_page.window().root(); + if window.r().set_page_clip_rect_with_new_viewport(rect) { + let page = get_page(page, id); + self.force_reflow(&*page, ReflowReason::Viewport); + } + return; + } + } + let mut loads = self.incomplete_loads.borrow_mut(); + if let Some(ref mut load) = loads.iter_mut().find(|load| load.pipeline_id == id) { + load.clip_rect = Some(rect); + return; + } + panic!("Page rect message sent to nonexistent pipeline"); + } + + /// Handle a request to load a page in a new child frame of an existing page. fn handle_new_layout(&self, new_layout_info: NewLayoutInfo) { let NewLayoutInfo { - old_pipeline_id, + containing_pipeline_id, new_pipeline_id, subpage_id, - layout_chan + layout_chan, + load_data, } = new_layout_info; - let page = self.page.borrow_mut(); - let parent_page = page.find(old_pipeline_id).expect("ScriptTask: received a layout + let page = self.root_page(); + let parent_page = page.find(containing_pipeline_id).expect("ScriptTask: received a layout whose parent has a PipelineId which does not correspond to a pipeline in the script task's page tree. This is a bug."); - let new_page = { - let window_size = parent_page.window_size.get(); - Page::new(new_pipeline_id, Some(subpage_id), - LayoutChan(layout_chan.downcast_ref::<Sender<layout_interface::Msg>>().unwrap().clone()), - window_size, - parent_page.resource_task.clone(), - parent_page.storage_task.clone(), - self.constellation_chan.clone(), - self.js_context.borrow().as_ref().unwrap().clone(), - self.devtools_chan.clone()) - }; - parent_page.children.borrow_mut().push(Rc::new(new_page)); + + let parent_window = parent_page.window().root(); + let chan = layout_chan.downcast_ref::<Sender<layout_interface::Msg>>().unwrap(); + let layout_chan = LayoutChan(chan.clone()); + // Kick off the fetch for the new resource. + let new_load = InProgressLoad::new(new_pipeline_id, Some((containing_pipeline_id, subpage_id)), + layout_chan, parent_window.r().window_size(), + load_data.url.clone()); + self.start_page_load(new_load, load_data); } /// Handles a timer that fired. fn handle_fire_timer_msg(&self, id: PipelineId, timer_id: TimerId) { - let page = self.page.borrow_mut(); + let page = self.root_page(); let page = page.find(id).expect("ScriptTask: received fire timer msg for a pipeline ID not associated with this script task. This is a bug."); - let frame = page.frame(); - let window = frame.as_ref().unwrap().window.root(); + let window = page.window().root(); window.r().handle_fire_timer(timer_id); } /// Handles freeze message fn handle_freeze_msg(&self, id: PipelineId) { - let page = self.page.borrow_mut(); + let page = self.root_page(); let page = page.find(id).expect("ScriptTask: received freeze msg for a pipeline ID not associated with this script task. This is a bug."); - let frame = page.frame(); - let window = frame.as_ref().unwrap().window.root(); + let window = page.window().root(); window.r().freeze(); } /// Handles thaw message fn handle_thaw_msg(&self, id: PipelineId) { - let page = self.page.borrow_mut(); - let page = page.find(id).expect("ScriptTask: received thaw msg for a - pipeline ID not associated with this script task. This is a bug."); - let frame = page.frame(); - let window = frame.as_ref().unwrap().window.root(); + // We should only get this message when moving in history, so all pages requested + // should exist. + let page = self.root_page().find(id).unwrap(); + + let needed_reflow = page.set_reflow_status(false); + if needed_reflow { + self.force_reflow(&*page, ReflowReason::CachedPageNeededReflow); + } + + let window = page.window().root(); window.r().thaw(); } /// Handles a notification that reflow completed. fn handle_reflow_complete_msg(&self, pipeline_id: PipelineId, reflow_id: uint) { debug!("Script: Reflow {:?} complete for {:?}", reflow_id, pipeline_id); - let page = self.page.borrow_mut(); + let page = self.root_page(); let page = page.find(pipeline_id).expect( "ScriptTask: received a load message for a layout channel that is not associated \ with this script task. This is a bug."); - let last_reflow_id = page.last_reflow_id.get(); - if last_reflow_id == reflow_id { - let mut layout_join_port = page.layout_join_port.borrow_mut(); - *layout_join_port = None; - } + let window = page.window().root(); + window.r().handle_reflow_complete_msg(reflow_id); - let doc = page.frame().as_ref().unwrap().document.root(); + let doc = page.document().root(); let html_element = doc.r().GetDocumentElement().root(); let reftest_wait = html_element.r().map_or(false, |elem| elem.has_class(&Atom::from_slice("reftest-wait"))); @@ -701,14 +804,12 @@ impl ScriptTask { /// Window was resized, but this script was not active, so don't reflow yet fn handle_resize_inactive_msg(&self, id: PipelineId, new_size: WindowSizeData) { - let page = self.page.borrow_mut(); + let page = self.root_page(); let page = page.find(id).expect("Received resize message for PipelineId not associated with a page in the page tree. This is a bug."); - page.window_size.set(new_size); - match &mut *page.mut_url() { - &mut Some((_, ref mut needs_reflow)) => *needs_reflow = true, - &mut None => (), - } + let window = page.window().root(); + window.r().set_window_size(new_size); + page.set_reflow_status(true); } /// We have gotten a window.close from script, which we pass on to the compositor. @@ -725,182 +826,206 @@ impl ScriptTask { self.compositor.borrow_mut().close(); } + /// We have received notification that the response associated with a load has completed. + /// Kick off the document and frame tree creation process using the result. + fn handle_page_fetch_complete(&self, id: PipelineId, subpage: Option<SubpageId>, + response: LoadResponse) { + // Any notification received should refer to an existing, in-progress load that is tracked. + let idx = self.incomplete_loads.borrow().iter().position(|load| { + load.pipeline_id == id && load.subpage_id.map(|sub| sub.1) == subpage + }).unwrap(); + let load = self.incomplete_loads.borrow_mut().remove(idx); + self.load(response, load); + } + /// Handles a request for the window title. fn handle_get_title_msg(&self, pipeline_id: PipelineId) { - get_page(&*self.page.borrow(), pipeline_id).send_title_to_compositor(); + let page = get_page(&self.root_page(), pipeline_id); + let document = page.document().root(); + document.r().send_title_to_compositor(); } /// Handles a request to exit the script task and shut down layout. /// Returns true if the script task should shut down and false otherwise. fn handle_exit_pipeline_msg(&self, id: PipelineId, exit_type: PipelineExitType) -> bool { // If root is being exited, shut down all pages - let page = self.page.borrow_mut(); - if page.id == id { + let page = self.root_page(); + let window = page.window().root(); + if window.r().pipeline() == id { debug!("shutting down layout for root page {:?}", id); + // To ensure the elements of the DOM tree remain usable (such as the window global), + // don't free the JS context until all interactions with it are finished. + shut_down_layout(&page, exit_type); *self.js_context.borrow_mut() = None; - shut_down_layout(&*page, (*self.js_runtime).ptr, exit_type); return true } // otherwise find just the matching page and exit all sub-pages - match page.find(id) { - Some(ref mut page) => { - shut_down_layout(&*page, (*self.js_runtime).ptr, exit_type); - page.remove(id); - false - } - // TODO(tkuehn): pipeline closing is currently duplicated across - // script and constellation, which can cause this to happen. Constellation - // needs to be smarter about exiting pipelines. - None => false, + if let Some(ref mut child_page) = page.remove(id) { + shut_down_layout(&*child_page, exit_type); } - + return false; } /// The entry point to document loading. Defines bindings, sets up the window and document /// objects, parses HTML and CSS, and kicks off initial layout. - fn load(&self, pipeline_id: PipelineId, - parent: Option<(PipelineId, SubpageId)>, load_data: LoadData) { - let url = load_data.url.clone(); - debug!("ScriptTask: loading {} on page {:?}", url.serialize(), pipeline_id); - - let borrowed_page = self.page.borrow_mut(); - - let frame_element = parent.and_then(|(parent_id, subpage_id)| { - // In the case a parent id exists but the matching page - // cannot be found, this means the page exists in a different - // script task (due to origin) so it shouldn't be returned. - // TODO: window.parent will continue to return self in that - // case, which is wrong. We should be returning an object that - // denies access to most properties (per - // https://github.com/servo/servo/issues/3939#issuecomment-62287025). - borrowed_page.find(parent_id).and_then(|page| { - let doc = page.frame().as_ref().unwrap().document.root(); - let doc: JSRef<Node> = NodeCast::from_ref(doc.r()); - - doc.traverse_preorder() - .filter_map(HTMLIFrameElementCast::to_ref) - .find(|node| node.subpage_id() == Some(subpage_id)) - .map(ElementCast::from_ref) - .map(Temporary::from_rooted) - }) + fn load(&self, response: LoadResponse, incomplete: InProgressLoad) { + let final_url = response.metadata.final_url.clone(); + debug!("ScriptTask: loading {} on page {:?}", incomplete.url.serialize(), incomplete.pipeline_id); + + // We should either be initializing a root page or loading a child page of an + // existing one. + let root_page_exists = self.page.borrow().is_some(); + assert!(incomplete.subpage_id.is_none() || root_page_exists); + + let frame_element = incomplete.subpage_id.and_then(|(parent_id, subpage_id)| { + let borrowed_page = self.root_page(); + // In the case a parent id exists but the matching page + // cannot be found, this means the page exists in a different + // script task (due to origin) so it shouldn't be returned. + // TODO: window.parent will continue to return self in that + // case, which is wrong. We should be returning an object that + // denies access to most properties (per + // https://github.com/servo/servo/issues/3939#issuecomment-62287025). + borrowed_page.find(parent_id).and_then(|page| { + let doc = page.document().root(); + let doc: JSRef<Node> = NodeCast::from_ref(doc.r()); + + doc.traverse_preorder() + .filter_map(HTMLIFrameElementCast::to_ref) + .find(|node| node.subpage_id() == Some(subpage_id)) + .map(ElementCast::from_ref) + .map(Temporary::from_rooted) + }) }).root(); - let page = borrowed_page.find(pipeline_id).expect("ScriptTask: received a load - message for a layout channel that is not associated with this script task. This - is a bug."); + self.compositor.borrow_mut().set_ready_state(incomplete.pipeline_id, Loading); - // Are we reloading? - let reloading = match *page.url() { - Some((ref loaded, _)) => *loaded == url, - _ => false, - }; - if reloading { - // Pull out the `needs_reflow` flag explicitly because `reflow` can ask for the page's - // URL, and we can't be holding a borrow on that URL (#4402). - let needed_reflow = match &mut *page.mut_url() { - &mut Some((_, ref mut needs_reflow)) => replace(needs_reflow, false), - _ => panic!("can't reload a page with no URL!") - }; - if needed_reflow { - self.force_reflow(&*page); - } - return - } - let last_url = replace(&mut *page.mut_url(), None).map(|(last_url, _)| last_url); - - let is_javascript = url.scheme.as_slice() == "javascript"; - - self.compositor.borrow_mut().set_ready_state(pipeline_id, Loading); - - let (mut parser_input, final_url, last_modified) = if !is_javascript { - // Wait for the LoadResponse so that the parser knows the final URL. - let (input_chan, input_port) = channel(); - self.resource_task.send(ControlMsg::Load(NetLoadData { - url: url, - method: load_data.method, - headers: Headers::new(), - preserved_headers: load_data.headers, - data: load_data.data, - cors: None, - consumer: input_chan, - })).unwrap(); + let cx = self.js_context.borrow(); + let cx = cx.as_ref().unwrap(); - let load_response = input_port.recv().unwrap(); + // Create a new frame tree entry. + let page = Rc::new(Page::new(incomplete.pipeline_id, final_url.clone())); + if !root_page_exists { + // We have a new root frame tree. + *self.page.borrow_mut() = Some(page.clone()); + } else if let Some((parent, _)) = incomplete.subpage_id { + // We have a new child frame. + let parent_page = self.root_page(); + parent_page.find(parent).expect("received load for subpage with missing parent"); + parent_page.children.borrow_mut().push(page.clone()); + } - let last_modified = load_response.metadata.headers.as_ref().and_then(|headers| { - headers.get().map(|&LastModified(ref tm)| tm.clone()) - }); + enum PageToRemove { + Root, + Child(PipelineId), + } + struct AutoPageRemover<'a> { + page: PageToRemove, + script_task: &'a ScriptTask, + neutered: bool, + } + impl<'a> AutoPageRemover<'a> { + fn new(script_task: &'a ScriptTask, page: PageToRemove) -> AutoPageRemover<'a> { + AutoPageRemover { + page: page, + script_task: script_task, + neutered: false, + } + } - let final_url = load_response.metadata.final_url.clone(); + fn neuter(&mut self) { + self.neutered = true; + } + } + #[unsafe_destructor] + impl<'a> Drop for AutoPageRemover<'a> { + fn drop(&mut self) { + if !self.neutered { + match self.page { + PageToRemove::Root => *self.script_task.page.borrow_mut() = None, + PageToRemove::Child(id) => { + let _ = self.script_task.root_page().remove(id); + } + } + } + } + } - (Some(HTMLInput::InputUrl(load_response)), final_url, last_modified) + let page_to_remove = if !root_page_exists { + PageToRemove::Root } else { - let doc_url = last_url.unwrap_or_else(|| { - Url::parse("about:blank").unwrap() - }); - (None, doc_url, None) + PageToRemove::Child(incomplete.pipeline_id) }; + let mut page_remover = AutoPageRemover::new(self, page_to_remove); - // Store the final URL before we start parsing, so that DOM routines - // (e.g. HTMLImageElement::update_image) can resolve relative URLs - // correctly. - { - *page.mut_url() = Some((final_url.clone(), true)); - } - - let cx = self.js_context.borrow(); - let cx = cx.as_ref().unwrap(); // Create the window and document objects. - let window = Window::new(cx.ptr, + let window = Window::new(cx.clone(), page.clone(), self.chan.clone(), self.control_chan.clone(), self.compositor.borrow_mut().dup(), - self.image_cache_task.clone()).root(); + self.image_cache_task.clone(), + self.resource_task.clone(), + self.storage_task.clone(), + self.devtools_chan.clone(), + self.constellation_chan.clone(), + incomplete.layout_chan, + incomplete.pipeline_id, + incomplete.subpage_id.map(|s| s.1), + incomplete.window_size).root(); + + let last_modified: Option<DOMString> = response.metadata.headers.as_ref().and_then(|headers| { + headers.get().map(|&LastModified(ref tm)| dom_last_modified(tm)) + }); - let document = Document::new(window.r(), Some(final_url.clone()), - IsHTMLDocument::HTMLDocument, None, + let content_type = match response.metadata.content_type { + Some((ref t, ref st)) if t.as_slice().eq_ignore_ascii_case("text") && + st.as_slice().eq_ignore_ascii_case("plain") => { + Some("text/plain".to_owned()) + } + _ => None + }; + + let document = Document::new(window.r(), + Some(final_url.clone()), + IsHTMLDocument::HTMLDocument, + content_type, + last_modified, DocumentSource::FromParser).root(); - if let Some(tm) = last_modified { - document.r().set_last_modified(dom_last_modified(&tm)); - } - window.r().init_browser_context(document.r(), frame_element.r()); + window.r().init_browser_context(document.r(), frame_element.r()); - { - // Create the root frame. - let mut frame = page.mut_frame(); - *frame = Some(Frame { - document: JS::from_rooted(document.r()), - window: JS::from_rooted(window.r()), - }); - } + // Create the root frame + page.set_frame(Some(Frame { + document: JS::from_rooted(document.r()), + window: JS::from_rooted(window.r()), + })); - if is_javascript { - let evalstr = load_data.url.non_relative_scheme_data().unwrap(); + let is_javascript = incomplete.url.scheme.as_slice() == "javascript"; + let parse_input = if is_javascript { + let evalstr = incomplete.url.non_relative_scheme_data().unwrap(); let jsval = window.r().evaluate_js_on_global_with_result(evalstr); let strval = FromJSValConvertible::from_jsval(self.get_cx(), jsval, StringificationBehavior::Empty); - parser_input = Some(HTMLInput::InputString(strval.unwrap_or("".to_owned()))); + HTMLInput::InputString(strval.unwrap_or("".to_owned())) + } else { + HTMLInput::InputUrl(response) }; - parse_html(document.r(), parser_input.unwrap(), &final_url); + parse_html(document.r(), parse_input, &final_url); document.r().set_ready_state(DocumentReadyState::Interactive); - self.compositor.borrow_mut().set_ready_state(pipeline_id, PerformingLayout); + self.compositor.borrow_mut().set_ready_state(incomplete.pipeline_id, PerformingLayout); // Kick off the initial reflow of the page. debug!("kicking off initial reflow of {:?}", final_url); document.r().content_changed(NodeCast::from_ref(document.r()), NodeDamage::OtherNodeDamage); - window.r().flush_layout(ReflowGoal::ForDisplay, ReflowQueryType::NoQuery); + window.r().reflow(ReflowGoal::ForDisplay, ReflowQueryType::NoQuery, ReflowReason::FirstLoad); - { - // No more reflow required - let mut page_url = page.mut_url(); - *page_url = Some((final_url.clone(), false)); - } + // No more reflow required + page.set_reflow_status(false); // https://html.spec.whatwg.org/multipage/#the-end step 4 let addr: Trusted<Document> = Trusted::new(self.get_cx(), document.r(), self.chan.clone()); @@ -915,7 +1040,7 @@ impl ScriptTask { let handler = Box::new(DocumentProgressHandler::new(addr, DocumentProgressTask::Load)); self.chan.send(ScriptMsg::RunnableMsg(handler)).unwrap(); - *page.fragment_name.borrow_mut() = final_url.fragment.clone(); + window.r().set_fragment_name(final_url.fragment.clone()); let ConstellationChan(ref chan) = self.constellation_chan; chan.send(ConstellationMsg::LoadComplete).unwrap(); @@ -928,11 +1053,13 @@ impl ScriptTask { title: document.r().Title(), url: final_url }; - chan.send(DevtoolsControlMsg::NewGlobal(pipeline_id, + chan.send(DevtoolsControlMsg::NewGlobal(incomplete.pipeline_id, self.devtools_sender.clone(), page_info)).unwrap(); } } + + page_remover.neuter(); } fn scroll_fragment_point(&self, pipeline_id: PipelineId, node: JSRef<Element>) { @@ -948,12 +1075,11 @@ impl ScriptTask { } /// Reflows non-incrementally. - fn force_reflow(&self, page: &Page) { - page.dirty_all_nodes(); - page.reflow(ReflowGoal::ForDisplay, - self.control_chan.clone(), - &mut **self.compositor.borrow_mut(), - ReflowQueryType::NoQuery); + fn force_reflow(&self, page: &Page, reason: ReflowReason) { + let document = page.document().root(); + document.r().dirty_all_nodes(); + let window = window_from_node(document.r()).root(); + window.r().reflow(ReflowGoal::ForDisplay, ReflowQueryType::NoQuery, reason); } /// This is the main entry point for receiving and dispatching DOM events. @@ -979,9 +1105,8 @@ impl ScriptTask { for node in nodes.iter() { let node_to_dirty = node::from_untrusted_node_address(self.js_runtime.ptr, *node).root(); - let page = get_page(&*self.page.borrow(), pipeline_id); - let frame = page.frame(); - let document = frame.as_ref().unwrap().document.root(); + let page = get_page(&self.root_page(), pipeline_id); + let document = page.document().root(); document.r().content_changed(node_to_dirty.r(), NodeDamage::OtherNodeDamage); } @@ -990,40 +1115,65 @@ impl ScriptTask { } ClickEvent(_button, point) => { - let page = get_page(&*self.page.borrow(), pipeline_id); - let frame = page.frame(); - let document = frame.as_ref().unwrap().document.root(); + let page = get_page(&self.root_page(), pipeline_id); + let document = page.document().root(); document.r().handle_click_event(self.js_runtime.ptr, _button, point); } MouseDownEvent(..) => {} MouseUpEvent(..) => {} MouseMoveEvent(point) => { - self.handle_mouse_move_event(pipeline_id, point); + let page = get_page(&self.root_page(), pipeline_id); + let document = page.document().root(); + let mouse_over_targets = &mut *self.mouse_over_targets.borrow_mut(); + + if document.r().handle_mouse_move_event(self.js_runtime.ptr, point, mouse_over_targets) { + self.force_reflow(&page, ReflowReason::MouseEvent) + } } KeyEvent(key, state, modifiers) => { - let page = get_page(&*self.page.borrow(), pipeline_id); - let frame = page.frame(); - let document = frame.as_ref().unwrap().document.root(); + let page = get_page(&self.root_page(), pipeline_id); + let document = page.document().root(); document.r().dispatch_key_event( key, state, modifiers, &mut *self.compositor.borrow_mut()); } } } + /// https://html.spec.whatwg.org/multipage/browsers.html#navigating-across-documents /// The entry point for content to notify that a new load has been requested - /// for the given pipeline. - fn trigger_load(&self, pipeline_id: PipelineId, load_data: LoadData) { - let ConstellationChan(ref const_chan) = self.constellation_chan; - const_chan.send(ConstellationMsg::LoadUrl(pipeline_id, load_data)).unwrap(); + /// for the given pipeline (specifically the "navigate" algorithm). + fn handle_navigate(&self, pipeline_id: PipelineId, subpage_id: Option<SubpageId>, load_data: LoadData) { + match subpage_id { + Some(subpage_id) => { + let borrowed_page = self.root_page(); + let iframe = borrowed_page.find(pipeline_id).and_then(|page| { + let doc = page.document().root(); + let doc: JSRef<Node> = NodeCast::from_ref(doc.r()); + + doc.traverse_preorder() + .filter_map(HTMLIFrameElementCast::to_ref) + .find(|node| node.subpage_id() == Some(subpage_id)) + .map(Temporary::from_rooted) + }).root(); + if let Some(iframe) = iframe.r() { + iframe.navigate_child_browsing_context(load_data.url); + } + } + None => { + let ConstellationChan(ref const_chan) = self.constellation_chan; + const_chan.send(ConstellationMsg::LoadUrl(pipeline_id, load_data)).unwrap(); + } + } } /// The entry point for content to notify that a fragment url has been requested /// for the given pipeline. fn trigger_fragment(&self, pipeline_id: PipelineId, fragment: String) { - let page = get_page(&*self.page.borrow(), pipeline_id); - match page.find_fragment_node(fragment).root() { + let page = get_page(&self.root_page(), pipeline_id); + let document = page.document().root(); + match document.r().find_fragment_node(fragment).root() { Some(node) => { self.scroll_fragment_point(pipeline_id, node.r()); } @@ -1033,159 +1183,105 @@ impl ScriptTask { fn handle_resize_event(&self, pipeline_id: PipelineId, new_size: WindowSizeData) { - let window = { - let page = get_page(&*self.page.borrow(), pipeline_id); - page.window_size.set(new_size); - - let frame = page.frame(); - if frame.is_some() { - self.force_reflow(&*page); - } - - let fragment_node = - page.fragment_name - .borrow_mut() - .take() - .and_then(|name| page.find_fragment_node(name)) - .root(); - match fragment_node { - Some(node) => self.scroll_fragment_point(pipeline_id, node.r()), - None => {} - } + let page = get_page(&self.root_page(), pipeline_id); + let window = page.window().root(); + window.r().set_window_size(new_size); + self.force_reflow(&*page, ReflowReason::WindowResize); + + let document = page.document().root(); + let fragment_node = window.r().steal_fragment_name() + .and_then(|name| document.r().find_fragment_node(name)) + .root(); + match fragment_node { + Some(node) => self.scroll_fragment_point(pipeline_id, node.r()), + None => {} + } - frame.as_ref().map(|frame| Temporary::new(frame.window.clone())) - }; + // http://dev.w3.org/csswg/cssom-view/#resizing-viewports + // https://dvcs.w3.org/hg/dom3events/raw-file/tip/html/DOM3-Events.html#event-type-resize + let uievent = UIEvent::new(window.r(), + "resize".to_owned(), EventBubbles::DoesNotBubble, + EventCancelable::NotCancelable, Some(window.r()), + 0i32).root(); + let event: JSRef<Event> = EventCast::from_ref(uievent.r()); - match window.root() { - Some(window) => { - // http://dev.w3.org/csswg/cssom-view/#resizing-viewports - // https://dvcs.w3.org/hg/dom3events/raw-file/tip/html/DOM3-Events.html#event-type-resize - let uievent = UIEvent::new(window.r(), - "resize".to_owned(), false, - false, Some(window.r()), - 0i32).root(); - let event: JSRef<Event> = EventCast::from_ref(uievent.r()); - - let wintarget: JSRef<EventTarget> = EventTargetCast::from_ref(window.r()); - event.fire(wintarget); - } - None => () - } + let wintarget: JSRef<EventTarget> = EventTargetCast::from_ref(window.r()); + event.fire(wintarget); } fn handle_reflow_event(&self, pipeline_id: PipelineId) { debug!("script got reflow event"); - let page = get_page(&*self.page.borrow(), pipeline_id); - let frame = page.frame(); - if frame.is_some() { - self.force_reflow(&*page); - } + let page = get_page(&self.root_page(), pipeline_id); + self.force_reflow(&*page, ReflowReason::ReceivedReflowEvent); } + /// Initiate a non-blocking fetch for a specified resource. Stores the InProgressLoad + /// argument until a notification is received that the fetch is complete. + fn start_page_load(&self, incomplete: InProgressLoad, mut load_data: LoadData) { + let id = incomplete.pipeline_id.clone(); + let subpage = incomplete.subpage_id.clone().map(|p| p.1); - fn handle_mouse_move_event(&self, pipeline_id: PipelineId, point: Point2D<f32>) { - let page = get_page(&*self.page.borrow(), pipeline_id); - let mut needs_reflow = false; - - // Build a list of elements that are currently under the mouse. - let mouse_over_addresses = page.get_nodes_under_mouse(&point); - let mouse_over_targets: Vec<JS<Node>> = mouse_over_addresses.iter() - .filter_map(|node_address| { - let node = node::from_untrusted_node_address(self.js_runtime.ptr, *node_address); - node.root().r().inclusive_ancestors().find(|node| node.is_element()).map(JS::from_rooted) - }).collect(); - - // Remove hover from any elements in the previous list that are no longer - // under the mouse. - let prev_mouse_over_targets = &mut *self.mouse_over_targets.borrow_mut(); - for target in prev_mouse_over_targets.iter() { - if !mouse_over_targets.contains(target) { - target.root().r().set_hover_state(false); - needs_reflow = true; - } - } + let script_chan = self.chan.clone(); + let resource_task = self.resource_task.clone(); - // Set hover state for any elements in the current mouse over list. - // Check if any of them changed state to determine whether to - // force a reflow below. - for target in mouse_over_targets.iter() { - let target = target.root(); - let target_ref = target.r(); - if !target_ref.get_hover_state() { - target_ref.set_hover_state(true); - needs_reflow = true; + spawn_named(format!("fetch for {:?}", load_data.url.serialize()), move || { + if load_data.url.scheme.as_slice() == "javascript" { + load_data.url = Url::parse("about:blank").unwrap(); } - } - // Send mousemove event to topmost target - if mouse_over_addresses.len() > 0 { - let top_most_node = - node::from_untrusted_node_address(self.js_runtime.ptr, mouse_over_addresses[0]).root(); - - if let Some(ref frame) = *page.frame() { - let window = frame.window.root(); - - let x = point.x.to_i32().unwrap_or(0); - let y = point.y.to_i32().unwrap_or(0); - - let mouse_event = MouseEvent::new(window.r(), - "mousemove".to_owned(), - true, - true, - Some(window.r()), - 0i32, - x, y, x, y, - false, false, false, false, - 0i16, - None).root(); - - let event: JSRef<Event> = EventCast::from_ref(mouse_event.r()); - let target: JSRef<EventTarget> = EventTargetCast::from_ref(top_most_node.r()); - event.fire(target); - } - } + let (input_chan, input_port) = channel(); + resource_task.send(ControlMsg::Load(NetLoadData { + url: load_data.url, + method: load_data.method, + headers: Headers::new(), + preserved_headers: load_data.headers, + data: load_data.data, + cors: None, + consumer: input_chan, + })).unwrap(); - // Store the current mouse over targets for next frame - *prev_mouse_over_targets = mouse_over_targets; + let load_response = input_port.recv().unwrap(); + script_chan.send(ScriptMsg::PageFetchComplete(id, subpage, load_response)).unwrap(); + }); - // Reflow if hover state changed - if needs_reflow { - self.force_reflow(&*page); - } + self.incomplete_loads.borrow_mut().push(incomplete); + } +} + +impl Drop for ScriptTask { + fn drop(&mut self) { + SCRIPT_TASK_ROOT.with(|root| { + *root.borrow_mut() = None; + }); } } /// Shuts down layout for the given page tree. -fn shut_down_layout(page_tree: &Rc<Page>, rt: *mut JSRuntime, exit_type: PipelineExitType) { +fn shut_down_layout(page_tree: &Rc<Page>, exit_type: PipelineExitType) { + let mut channels = vec!(); + for page in page_tree.iter() { // Tell the layout task to begin shutting down, and wait until it // processed this message. let (response_chan, response_port) = channel(); - let LayoutChan(ref chan) = page.layout_chan; + let window = page.window().root(); + let LayoutChan(chan) = window.r().layout_chan(); if chan.send(layout_interface::Msg::PrepareToExit(response_chan)).is_ok() { - response_port.recv().unwrap(); + channels.push(chan); + response_port.recv().unwrap(); } } - // Remove our references to the DOM objects in this page tree. - for page in page_tree.iter() { - *page.mut_frame() = None; - } - - // Drop our references to the JSContext, potentially triggering a GC. + // Drop our references to the JSContext and DOM objects. for page in page_tree.iter() { - *page.mut_js_info() = None; - } - - // Force a GC to make sure that our DOM reflectors are released before we tell - // layout to exit. - unsafe { - JS_GC(rt); + let window = page.window().root(); + window.r().clear_js_context(); + // Sever the connection between the global and the DOM tree + page.set_frame(None); } // Destroy the layout task. If there were node leaks, layout will now crash safely. - for page in page_tree.iter() { - let LayoutChan(ref chan) = page.layout_chan; + for chan in channels.into_iter() { chan.send(layout_interface::Msg::ExitNow(exit_type)).ok(); } } |