Make script thread initiate requests for images needed by layout.

In support of this goal, the layout thread collects information about
CSS images that are missing image data and hands it off to the script
thread after layout completes. The script thread stores a list of
nodes that will need to be reflowed after the associated network
request is complete. The script thread ensures that the nodes are
not GCed while a request is ongoing, which the layout thread is
incapable of guaranteeing.

The image cache's API has also been redesigned in support of this
work. No network requests are made by the new image cache, since it
does not possess the document-specific information necessary to
initiate them. Instead, there is now a single, synchronous
query operation that optionally reserves a slot when a cache
entry for a URL cannot be found. This reserved slot is then
the responsibility of the queryer to populate with the contents
of the network response for the URL once it is complete. Any
subsequent queries for the same URL will be informed that the
response is pending until that occurs.

The changes to layout also remove the synchronous image loading
code path, which means that reftests now test the same code
that non-test binaries execute. The decision to take a screenshot
now considers whether there are any outstanding image
requests for layout in order to avoid intermittent failures in
reftests that use CSS images.
This commit is contained in:
Josh Matthews 2016-11-21 18:13:40 -05:00
parent 78e8c31a4d
commit c890c9143c
18 changed files with 528 additions and 388 deletions

View file

@ -62,7 +62,7 @@ use msg::constellation_msg::{FrameId, FrameType, PipelineId};
use net_traits::{Metadata, NetworkError, ReferrerPolicy, ResourceThreads};
use net_traits::filemanager_thread::RelativePos;
use net_traits::image::base::{Image, ImageMetadata};
use net_traits::image_cache_thread::{ImageCacheChan, ImageCacheThread};
use net_traits::image_cache_thread::{ImageCacheThread, PendingImageId};
use net_traits::request::{Request, RequestInit};
use net_traits::response::{Response, ResponseBody};
use net_traits::response::HttpsState;
@ -320,7 +320,7 @@ unsafe_no_jsmanaged_fields!(bool, f32, f64, String, AtomicBool, AtomicUsize, Uui
unsafe_no_jsmanaged_fields!(usize, u8, u16, u32, u64);
unsafe_no_jsmanaged_fields!(isize, i8, i16, i32, i64);
unsafe_no_jsmanaged_fields!(ServoUrl, ImmutableOrigin, MutableOrigin);
unsafe_no_jsmanaged_fields!(Image, ImageMetadata, ImageCacheChan, ImageCacheThread);
unsafe_no_jsmanaged_fields!(Image, ImageMetadata, ImageCacheThread, PendingImageId);
unsafe_no_jsmanaged_fields!(Metadata);
unsafe_no_jsmanaged_fields!(NetworkError);
unsafe_no_jsmanaged_fields!(Atom, Prefix, LocalName, Namespace, QualName);

View file

@ -429,7 +429,9 @@ impl CanvasRenderingContext2D {
let img = match self.request_image_from_cache(url) {
ImageResponse::Loaded(img) => img,
ImageResponse::PlaceholderLoaded(_) | ImageResponse::None | ImageResponse::MetadataLoaded(_) => {
ImageResponse::PlaceholderLoaded(_) |
ImageResponse::None |
ImageResponse::MetadataLoaded(_) => {
return None;
}
};

View file

@ -337,15 +337,18 @@ impl<'a> From<&'a WebGLContextAttributes> for GLContextAttributes {
pub mod utils {
use dom::window::Window;
use ipc_channel::ipc;
use net_traits::image_cache_thread::{ImageCacheChan, ImageResponse};
use net_traits::image_cache_thread::ImageResponse;
use servo_url::ServoUrl;
pub fn request_image_from_cache(window: &Window, url: ServoUrl) -> ImageResponse {
let image_cache = window.image_cache_thread();
panic!()
/*let image_cache = window.image_cache_thread();
let (response_chan, response_port) = ipc::channel().unwrap();
image_cache.request_image(url.into(), ImageCacheChan(response_chan), None);
let result = response_port.recv().unwrap();
result.image_response
match result {
ImageCacheResult::InitiateRequest(..) => panic!("unexpected image request initiator"),
ImageCacheResult::Response(result) => result.image_response,
}*/
}
}

View file

@ -3,6 +3,7 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
use app_units::{Au, AU_PER_PX};
use document_loader::{LoadType, LoadBlocker};
use dom::activation::Activatable;
use dom::attr::Attr;
use dom::bindings::cell::DOMRefCell;
@ -16,6 +17,7 @@ use dom::bindings::error::Fallible;
use dom::bindings::inheritance::Castable;
use dom::bindings::js::{LayoutJS, Root};
use dom::bindings::refcounted::Trusted;
use dom::bindings::reflector::DomObject;
use dom::bindings::str::DOMString;
use dom::document::Document;
use dom::element::{AttributeMutation, Element, RawLayoutElementHelpers};
@ -34,13 +36,17 @@ use euclid::point::Point2D;
use html5ever_atoms::LocalName;
use ipc_channel::ipc;
use ipc_channel::router::ROUTER;
use net_traits::{FetchResponseListener, FetchMetadata, Metadata, NetworkError};
use net_traits::image::base::{Image, ImageMetadata};
use net_traits::image_cache_thread::{ImageResponder, ImageResponse};
use net_traits::image_cache_thread::{ImageResponder, ImageResponse, PendingImageId, ImageState};
use net_traits::image_cache_thread::{UsePlaceholder, ImageOrMetadataAvailable};
use net_traits::request::{RequestInit, Type as RequestType};
use network_listener::{NetworkListener, PreInvoke};
use num_traits::ToPrimitive;
use script_thread::Runnable;
use servo_url::ServoUrl;
use std::i32;
use std::sync::Arc;
use std::sync::{Arc, Mutex};
use style::attr::{AttrValue, LengthOrPercentageOrAuto};
use task_source::TaskSource;
@ -53,10 +59,12 @@ enum State {
Broken,
}
#[derive(JSTraceable, HeapSizeOf)]
#[must_root]
struct ImageRequest {
state: State,
parsed_url: Option<ServoUrl>,
source_url: Option<DOMString>,
blocker: Option<LoadBlocker>,
#[ignore_heap_size_of = "Arc"]
image: Option<Arc<Image>>,
metadata: Option<ImageMetadata>,
@ -74,6 +82,64 @@ impl HTMLImageElement {
}
}
struct ImageRequestRunnable {
element: Trusted<HTMLImageElement>,
img_url: ServoUrl,
id: PendingImageId,
}
impl ImageRequestRunnable {
fn new(element: Trusted<HTMLImageElement>,
img_url: ServoUrl,
id: PendingImageId)
-> ImageRequestRunnable {
ImageRequestRunnable {
element: element,
img_url: img_url,
id: id,
}
}
}
impl Runnable for ImageRequestRunnable {
fn handler(self: Box<Self>) {
let this = *self;
let trusted_node = this.element.clone();
let element = this.element.root();
let document = document_from_node(&*element);
let window = window_from_node(&*element);
let context = Arc::new(Mutex::new(ImageContext {
elem: trusted_node,
data: vec!(),
metadata: None,
url: this.img_url.clone(),
status: Ok(()),
id: this.id,
}));
let (action_sender, action_receiver) = ipc::channel().unwrap();
let listener = NetworkListener {
context: context,
task_source: window.networking_task_source(),
wrapper: Some(window.get_runnable_wrapper()),
};
ROUTER.add_route(action_receiver.to_opaque(), box move |message| {
listener.notify_fetch(message.to().unwrap());
});
let request = RequestInit {
url: this.img_url.clone(),
origin: document.url().clone(),
type_: RequestType::Image,
pipeline_id: Some(document.global().pipeline_id()),
.. RequestInit::default()
};
document.fetch_async(LoadType::Image(this.img_url), request, action_sender);
}
}
struct ImageResponseHandlerRunnable {
element: Trusted<HTMLImageElement>,
@ -122,23 +188,83 @@ impl Runnable for ImageResponseHandlerRunnable {
element.upcast::<EventTarget>().fire_event(atom!("error"));
}
LoadBlocker::terminate(&mut element.current_request.borrow_mut().blocker);
// Trigger reflow
let window = window_from_node(&*document);
window.add_pending_reflow();
}
}
/// The context required for asynchronously loading an external image.
struct ImageContext {
/// The element that initiated the request.
elem: Trusted<HTMLImageElement>,
/// The response body received to date.
data: Vec<u8>,
/// The response metadata received to date.
metadata: Option<Metadata>,
/// The initial URL requested.
url: ServoUrl,
/// Indicates whether the request failed, and why
status: Result<(), NetworkError>,
/// The cache ID for this request.
id: PendingImageId,
}
impl FetchResponseListener for ImageContext {
fn process_request_body(&mut self) {}
fn process_request_eof(&mut self) {}
fn process_response(&mut self, metadata: Result<FetchMetadata, NetworkError>) {
self.metadata = metadata.ok().map(|meta| match meta {
FetchMetadata::Unfiltered(m) => m,
FetchMetadata::Filtered { unsafe_, .. } => unsafe_
});
let status_code = self.metadata.as_ref().and_then(|m| {
match m.status {
Some((c, _)) => Some(c),
_ => None,
}
}).unwrap_or(0);
self.status = match status_code {
0 => Err(NetworkError::Internal("No http status code received".to_owned())),
200...299 => Ok(()), // HTTP ok status codes
_ => Err(NetworkError::Internal(format!("HTTP error code {}", status_code)))
};
}
fn process_response_chunk(&mut self, mut payload: Vec<u8>) {
if self.status.is_ok() {
self.data.append(&mut payload);
}
}
fn process_response_eof(&mut self, _response: Result<(), NetworkError>) {
let elem = self.elem.root();
let document = document_from_node(&*elem);
let window = document.window();
let image_cache = window.image_cache_thread();
image_cache.store_complete_image_bytes(self.id, self.data.clone());
document.finish_load(LoadType::Image(self.url.clone()));
}
}
impl PreInvoke for ImageContext {}
impl HTMLImageElement {
/// Makes the local `image` member match the status of the `src` attribute and starts
/// prefetching the image. This method must be called after `src` is changed.
fn update_image(&self, value: Option<(DOMString, ServoUrl)>) {
let document = document_from_node(self);
let window = document.window();
let image_cache = window.image_cache_thread();
match value {
None => {
self.current_request.borrow_mut().parsed_url = None;
self.current_request.borrow_mut().source_url = None;
LoadBlocker::terminate(&mut self.current_request.borrow_mut().blocker);
self.current_request.borrow_mut().image = None;
}
Some((src, base_url)) => {
@ -147,22 +273,57 @@ impl HTMLImageElement {
self.current_request.borrow_mut().parsed_url = Some(img_url.clone());
self.current_request.borrow_mut().source_url = Some(src);
LoadBlocker::terminate(&mut self.current_request.borrow_mut().blocker);
self.current_request.borrow_mut().blocker =
Some(LoadBlocker::new(&*document, LoadType::Image(img_url.clone())));
let trusted_node = Trusted::new(self);
let (responder_sender, responder_receiver) = ipc::channel().unwrap();
let task_source = window.networking_task_source();
let wrapper = window.get_runnable_wrapper();
let img_url_cloned = img_url.clone();
let trusted_node_clone = trusted_node.clone();
ROUTER.add_route(responder_receiver.to_opaque(), box move |message| {
// Return the image via a message to the script thread, which marks the element
// as dirty and triggers a reflow.
let image_response = message.to().unwrap();
let runnable = box ImageResponseHandlerRunnable::new(
trusted_node.clone(), image_response);
let _ = task_source.queue_with_wrapper(runnable, &wrapper);
let runnable = ImageResponseHandlerRunnable::new(
trusted_node_clone.clone(), message.to().unwrap());
let _ = task_source.queue_with_wrapper(box runnable, &wrapper);
});
image_cache.request_image_and_metadata(img_url.into(),
window.image_cache_chan(),
Some(ImageResponder::new(responder_sender)));
let image_cache = window.image_cache_thread();
let response =
image_cache.find_image_or_metadata(img_url_cloned.into(), UsePlaceholder::Yes);
match response {
Ok(ImageOrMetadataAvailable::ImageAvailable(image)) => {
let event = box ImageResponseHandlerRunnable::new(
trusted_node, ImageResponse::Loaded(image));
event.handler();
}
Ok(ImageOrMetadataAvailable::MetadataAvailable(m)) => {
let event = box ImageResponseHandlerRunnable::new(
trusted_node, ImageResponse::MetadataLoaded(m));
event.handler();
}
Err(ImageState::Pending(id)) => {
image_cache.add_listener(id, ImageResponder::new(responder_sender, id));
}
Err(ImageState::LoadError) => {
let event = box ImageResponseHandlerRunnable::new(
trusted_node, ImageResponse::None);
event.handler();
}
Err(ImageState::NotRequested(id)) => {
image_cache.add_listener(id, ImageResponder::new(responder_sender, id));
let runnable = box ImageRequestRunnable::new(
Trusted::new(self), img_url, id);
runnable.handler();
}
}
} else {
// https://html.spec.whatwg.org/multipage/#update-the-image-data
// Step 11 (error substeps)
@ -202,6 +363,7 @@ impl HTMLImageElement {
}
}
}
fn new_inherited(local_name: LocalName, prefix: Option<DOMString>, document: &Document) -> HTMLImageElement {
HTMLImageElement {
htmlelement: HTMLElement::new_inherited(local_name, prefix, document),
@ -210,14 +372,16 @@ impl HTMLImageElement {
parsed_url: None,
source_url: None,
image: None,
metadata: None
metadata: None,
blocker: None,
}),
pending_request: DOMRefCell::new(ImageRequest {
state: State::Unavailable,
parsed_url: None,
source_url: None,
image: None,
metadata: None
metadata: None,
blocker: None,
}),
}
}

View file

@ -6,6 +6,7 @@ use app_units::Au;
use bluetooth_traits::BluetoothRequest;
use cssparser::Parser;
use devtools_traits::{ScriptToDevtoolsControlMsg, TimelineMarker, TimelineMarkerType};
use document_loader::LoadType;
use dom::bindings::cell::DOMRefCell;
use dom::bindings::codegen::Bindings::DocumentBinding::{DocumentMethods, DocumentReadyState};
use dom::bindings::codegen::Bindings::EventHandlerBinding::EventHandlerNonNull;
@ -42,7 +43,7 @@ use dom::location::Location;
use dom::mediaquerylist::{MediaQueryList, WeakMediaQueryListVec};
use dom::messageevent::MessageEvent;
use dom::navigator::Navigator;
use dom::node::{Node, from_untrusted_node_address, window_from_node};
use dom::node::{Node, from_untrusted_node_address, window_from_node, document_from_node, NodeDamage};
use dom::performance::Performance;
use dom::promise::Promise;
use dom::screen::Screen;
@ -52,20 +53,25 @@ use euclid::{Point2D, Rect, Size2D};
use fetch;
use gfx_traits::ScrollRootId;
use ipc_channel::ipc::{self, IpcSender};
use ipc_channel::router::ROUTER;
use js::jsapi::{HandleObject, HandleValue, JSAutoCompartment, JSContext};
use js::jsapi::{JS_GC, JS_GetRuntime};
use js::jsval::UndefinedValue;
use js::rust::Runtime;
use msg::constellation_msg::{FrameType, PipelineId};
use net_traits::{ResourceThreads, ReferrerPolicy};
use net_traits::image_cache_thread::{ImageCacheChan, ImageCacheThread};
use net_traits::{ResourceThreads, ReferrerPolicy, FetchResponseListener, FetchMetadata};
use net_traits::NetworkError;
use net_traits::image_cache_thread::{ImageResponder, ImageResponse};
use net_traits::image_cache_thread::{PendingImageResponse, ImageCacheThread, PendingImageId};
use net_traits::request::{Type as RequestType, RequestInit as FetchRequestInit};
use net_traits::storage_thread::StorageType;
use network_listener::{NetworkListener, PreInvoke};
use num_traits::ToPrimitive;
use open;
use profile_traits::mem::ProfilerChan as MemProfilerChan;
use profile_traits::time::ProfilerChan as TimeProfilerChan;
use rustc_serialize::base64::{FromBase64, STANDARD, ToBase64};
use script_layout_interface::TrustedNodeAddress;
use script_layout_interface::{TrustedNodeAddress, PendingImageState};
use script_layout_interface::message::{Msg, Reflow, ReflowQueryType, ScriptReflow};
use script_layout_interface::reporter::CSSErrorReporter;
use script_layout_interface::rpc::{ContentBoxResponse, ContentBoxesResponse, LayoutRPC};
@ -73,7 +79,7 @@ use script_layout_interface::rpc::{MarginStyleResponse, NodeScrollRootIdResponse
use script_layout_interface::rpc::{ResolvedStyleResponse, TextIndexResponse};
use script_runtime::{CommonScriptMsg, ScriptChan, ScriptPort, ScriptThreadEventCategory};
use script_thread::{MainThreadScriptChan, MainThreadScriptMsg, Runnable, RunnableWrapper};
use script_thread::SendableMainThreadScriptChan;
use script_thread::{SendableMainThreadScriptChan, ImageCacheMsg};
use script_traits::{ConstellationControlMsg, LoadData, MozBrowserEvent, UntrustedNodeAddress};
use script_traits::{DocumentState, TimerEvent, TimerEventId};
use script_traits::{ScriptMsg as ConstellationMsg, TimerEventRequest, WindowSizeData, WindowSizeType};
@ -87,6 +93,7 @@ use std::ascii::AsciiExt;
use std::borrow::ToOwned;
use std::cell::Cell;
use std::collections::{HashMap, HashSet};
use std::collections::hash_map::Entry;
use std::default::Default;
use std::io::{Write, stderr, stdout};
use std::mem;
@ -165,7 +172,7 @@ pub struct Window {
#[ignore_heap_size_of = "channels are hard"]
image_cache_thread: ImageCacheThread,
#[ignore_heap_size_of = "channels are hard"]
image_cache_chan: ImageCacheChan,
image_cache_chan: Sender<ImageCacheMsg>,
browsing_context: MutNullableJS<BrowsingContext>,
document: MutNullableJS<Document>,
history: MutNullableJS<History>,
@ -253,7 +260,13 @@ pub struct Window {
webvr_thread: Option<IpcSender<WebVRMsg>>,
/// A map for storing the previous permission state read results.
permission_state_invocation_results: DOMRefCell<HashMap<String, PermissionState>>
permission_state_invocation_results: DOMRefCell<HashMap<String, PermissionState>>,
/// All of the elements that have an outstanding image request that was
/// initiated by layout during a reflow. They are stored in the script thread
/// to ensure that the element can be marked dirty when the image data becomes
/// available at some point in the future.
pending_layout_images: DOMRefCell<HashMap<PendingImageId, Vec<JS<Node>>>>,
}
impl Window {
@ -295,10 +308,6 @@ impl Window {
&self.script_chan.0
}
pub fn image_cache_chan(&self) -> ImageCacheChan {
self.image_cache_chan.clone()
}
pub fn parent_info(&self) -> Option<(PipelineId, FrameType)> {
self.parent_info
}
@ -346,6 +355,28 @@ impl Window {
pub fn permission_state_invocation_results(&self) -> &DOMRefCell<HashMap<String, PermissionState>> {
&self.permission_state_invocation_results
}
pub fn pending_image_notification(&self, response: PendingImageResponse) {
//XXXjdm could be more efficient to send the responses to the layout thread,
// rather than making the layout thread talk to the image cache to
// obtain the same data.
let mut images = self.pending_layout_images.borrow_mut();
let nodes = images.entry(response.id);
let nodes = match nodes {
Entry::Occupied(nodes) => nodes,
Entry::Vacant(_) => return,
};
for node in nodes.get() {
node.dirty(NodeDamage::OtherNodeDamage);
}
match response.response {
ImageResponse::MetadataLoaded(_) => {}
ImageResponse::Loaded(_) |
ImageResponse::PlaceholderLoaded(_) |
ImageResponse::None => { nodes.remove(); }
}
self.add_pending_reflow();
}
}
#[cfg(any(target_os = "macos", target_os = "linux", target_os = "windows"))]
@ -1168,6 +1199,31 @@ impl Window {
self.emit_timeline_marker(marker.end());
}
let pending_images = self.layout_rpc.pending_images();
for image in pending_images {
let id = image.id;
let js_runtime = self.js_runtime.borrow();
let js_runtime = js_runtime.as_ref().unwrap();
let node = from_untrusted_node_address(js_runtime.rt(), image.node);
if let PendingImageState::Unrequested(ref url) = image.state {
fetch_image_for_layout(url.clone(), &*node, id);
}
let mut images = self.pending_layout_images.borrow_mut();
let nodes = images.entry(id).or_insert(vec![]);
if nodes.iter().find(|n| &***n as *const _ == &*node as *const _).is_none() {
let (responder, responder_listener) = ipc::channel().unwrap();
let pipeline = self.upcast::<GlobalScope>().pipeline_id();
let image_cache_chan = self.image_cache_chan.clone();
ROUTER.add_route(responder_listener.to_opaque(), box move |message| {
let _ = image_cache_chan.send((pipeline, message.to().unwrap()));
});
self.image_cache_thread.add_listener(id, ImageResponder::new(responder, id));
nodes.push(JS::from_ref(&*node));
}
}
true
}
@ -1227,7 +1283,8 @@ impl Window {
let ready_state = document.ReadyState();
if ready_state == DocumentReadyState::Complete && !reftest_wait {
let pending_images = self.pending_layout_images.borrow().is_empty();
if ready_state == DocumentReadyState::Complete && !reftest_wait && pending_images {
let global_scope = self.upcast::<GlobalScope>();
let event = ConstellationMsg::SetDocumentState(global_scope.pipeline_id(), DocumentState::Idle);
global_scope.constellation_chan().send(event).unwrap();
@ -1635,7 +1692,7 @@ impl Window {
network_task_source: NetworkingTaskSource,
history_task_source: HistoryTraversalTaskSource,
file_task_source: FileReadingTaskSource,
image_cache_chan: ImageCacheChan,
image_cache_chan: Sender<ImageCacheMsg>,
image_cache_thread: ImageCacheThread,
resource_threads: ResourceThreads,
bluetooth_thread: IpcSender<BluetoothRequest>,
@ -1717,6 +1774,7 @@ impl Window {
test_runner: Default::default(),
webvr_thread: webvr_thread,
permission_state_invocation_results: DOMRefCell::new(HashMap::new()),
pending_layout_images: DOMRefCell::new(HashMap::new()),
};
unsafe {
@ -1836,3 +1894,64 @@ impl Runnable for PostMessageHandler {
message.handle());
}
}
struct LayoutImageContext {
node: Trusted<Node>,
data: Vec<u8>,
id: PendingImageId,
url: ServoUrl,
}
impl FetchResponseListener for LayoutImageContext {
fn process_request_body(&mut self) {}
fn process_request_eof(&mut self) {}
fn process_response(&mut self, _metadata: Result<FetchMetadata, NetworkError>) {/*XXXjdm*/}
fn process_response_chunk(&mut self, mut payload: Vec<u8>) {
self.data.append(&mut payload);
}
fn process_response_eof(&mut self, _response: Result<(), NetworkError>) {
let node = self.node.root();
let document = document_from_node(&*node);
let window = document.window();
let image_cache = window.image_cache_thread();
image_cache.store_complete_image_bytes(self.id, self.data.clone());
document.finish_load(LoadType::Image(self.url.clone()));
}
}
impl PreInvoke for LayoutImageContext {}
fn fetch_image_for_layout(url: ServoUrl, node: &Node, id: PendingImageId) {
let context = Arc::new(Mutex::new(LayoutImageContext {
node: Trusted::new(node),
data: vec![],
id: id,
url: url.clone(),
}));
let document = document_from_node(node);
let window = window_from_node(node);
let (action_sender, action_receiver) = ipc::channel().unwrap();
let listener = NetworkListener {
context: context,
task_source: window.networking_task_source(),
wrapper: Some(window.get_runnable_wrapper()),
};
ROUTER.add_route(action_receiver.to_opaque(), box move |message| {
listener.notify_fetch(message.to().unwrap());
});
let request = FetchRequestInit {
url: url.clone(),
origin: document.url().clone(),
type_: RequestType::Image,
pipeline_id: Some(document.global().pipeline_id()),
.. FetchRequestInit::default()
};
//XXXjdm should not block load event
document.fetch_async(LoadType::Image(url), request, action_sender);
}