servo/components/script/dom/paintworkletglobalscope.rs
Yati Sagade aa48a2c2e3 Paint worklets: Implement timeout for worklet painter threads
When a paint worklet thread takes too long, we would like to move on,
since we have a ~16ms budget for rendering @60fps. At the moment, there
is no provision in the paintworklet spec to signal such timeouts to the
developer. ajeffrey opened an [issue][1] for this, but it got punted to
v2 of the spec. Hence we are silently timing out unresponsive paint
scripts.

The timeout value is chosen to be 10ms by default, and can be overridden
by setting the `dom.worklet.timeout_ms` pref.

In the absence of such a timeout, the reftest in this commit would fail
by timing out the testrunner itself, since the paint script never
returns. From my discussions with ajeffrey, this should do until we spec
out a way to signal timeouts to the script developer.

Since we did not have a better way to trigger a timeout than a busy
waiting loop (which would hog one core of the test machine until the
timeout was reached), we decided to implement a test only blocking
sleep, available to the PaintWorkletGlobalScope. Since
`dom.worklet.enabled` enables worklets in general, we also decided to
have another pref `dom.worklet.blockingsleep.enabled`, which, in
addition to `dom.worklet.enabled`, would be required for the blocking
sleep to be available.

This fixes #17370.

[1]: https://github.com/w3c/css-houdini-drafts/issues/507
2017-12-22 10:47:23 +01:00

522 lines
23 KiB
Rust

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
use dom::bindings::callback::CallbackContainer;
use dom::bindings::cell::DomRefCell;
use dom::bindings::codegen::Bindings::PaintWorkletGlobalScopeBinding;
use dom::bindings::codegen::Bindings::PaintWorkletGlobalScopeBinding::PaintWorkletGlobalScopeMethods;
use dom::bindings::codegen::Bindings::VoidFunctionBinding::VoidFunction;
use dom::bindings::conversions::get_property;
use dom::bindings::conversions::get_property_jsval;
use dom::bindings::error::Error;
use dom::bindings::error::Fallible;
use dom::bindings::inheritance::Castable;
use dom::bindings::reflector::DomObject;
use dom::bindings::root::{Dom, DomRoot};
use dom::bindings::str::DOMString;
use dom::cssstylevalue::CSSStyleValue;
use dom::paintrenderingcontext2d::PaintRenderingContext2D;
use dom::paintsize::PaintSize;
use dom::stylepropertymapreadonly::StylePropertyMapReadOnly;
use dom::worklet::WorkletExecutor;
use dom::workletglobalscope::WorkletGlobalScope;
use dom::workletglobalscope::WorkletGlobalScopeInit;
use dom::workletglobalscope::WorkletTask;
use dom_struct::dom_struct;
use euclid::TypedScale;
use euclid::TypedSize2D;
use ipc_channel::ipc;
use js::jsapi::Call;
use js::jsapi::Construct1;
use js::jsapi::HandleValue;
use js::jsapi::HandleValueArray;
use js::jsapi::Heap;
use js::jsapi::IsCallable;
use js::jsapi::IsConstructor;
use js::jsapi::JSAutoCompartment;
use js::jsapi::JS_ClearPendingException;
use js::jsapi::JS_IsExceptionPending;
use js::jsapi::JS_NewArrayObject;
use js::jsval::JSVal;
use js::jsval::ObjectValue;
use js::jsval::UndefinedValue;
use js::rust::Runtime;
use msg::constellation_msg::PipelineId;
use net_traits::image::base::PixelFormat;
use net_traits::image_cache::ImageCache;
use script_traits::{DrawAPaintImageResult, PaintWorkletError};
use script_traits::Painter;
use servo_atoms::Atom;
use servo_config::prefs::PREFS;
use servo_url::ServoUrl;
use std::cell::Cell;
use std::collections::HashMap;
use std::collections::hash_map::Entry;
use std::ptr::null_mut;
use std::rc::Rc;
use std::sync::Arc;
use std::sync::Mutex;
use std::sync::mpsc;
use std::sync::mpsc::Sender;
use std::thread;
use std::time::Duration;
use style_traits::CSSPixel;
use style_traits::DevicePixel;
use style_traits::SpeculativePainter;
/// <https://drafts.css-houdini.org/css-paint-api/#paintworkletglobalscope>
#[dom_struct]
pub struct PaintWorkletGlobalScope {
/// The worklet global for this object
worklet_global: WorkletGlobalScope,
/// The image cache
#[ignore_malloc_size_of = "Arc"]
image_cache: Arc<ImageCache>,
/// <https://drafts.css-houdini.org/css-paint-api/#paint-definitions>
paint_definitions: DomRefCell<HashMap<Atom, Box<PaintDefinition>>>,
/// <https://drafts.css-houdini.org/css-paint-api/#paint-class-instances>
paint_class_instances: DomRefCell<HashMap<Atom, Box<Heap<JSVal>>>>,
/// The most recent name the worklet was called with
cached_name: DomRefCell<Atom>,
/// The most recent size the worklet was drawn at
cached_size: Cell<TypedSize2D<f32, CSSPixel>>,
/// The most recent device pixel ratio the worklet was drawn at
cached_device_pixel_ratio: Cell<TypedScale<f32, CSSPixel, DevicePixel>>,
/// The most recent properties the worklet was drawn at
cached_properties: DomRefCell<Vec<(Atom, String)>>,
/// The most recent arguments the worklet was drawn at
cached_arguments: DomRefCell<Vec<String>>,
/// The most recent result
cached_result: DomRefCell<DrawAPaintImageResult>,
}
impl PaintWorkletGlobalScope {
#[allow(unsafe_code)]
pub fn new(runtime: &Runtime,
pipeline_id: PipelineId,
base_url: ServoUrl,
executor: WorkletExecutor,
init: &WorkletGlobalScopeInit)
-> DomRoot<PaintWorkletGlobalScope> {
debug!("Creating paint worklet global scope for pipeline {}.", pipeline_id);
let global = Box::new(PaintWorkletGlobalScope {
worklet_global: WorkletGlobalScope::new_inherited(pipeline_id, base_url, executor, init),
image_cache: init.image_cache.clone(),
paint_definitions: Default::default(),
paint_class_instances: Default::default(),
cached_name: DomRefCell::new(Atom::from("")),
cached_size: Cell::new(TypedSize2D::zero()),
cached_device_pixel_ratio: Cell::new(TypedScale::new(1.0)),
cached_properties: Default::default(),
cached_arguments: Default::default(),
cached_result: DomRefCell::new(DrawAPaintImageResult {
width: 0,
height: 0,
format: PixelFormat::BGRA8,
image_key: None,
missing_image_urls: Vec::new(),
}),
});
unsafe { PaintWorkletGlobalScopeBinding::Wrap(runtime.cx(), global) }
}
pub fn image_cache(&self) -> Arc<ImageCache> {
self.image_cache.clone()
}
pub fn perform_a_worklet_task(&self, task: PaintWorkletTask) {
match task {
PaintWorkletTask::DrawAPaintImage(name, size, device_pixel_ratio, properties, arguments, sender) => {
let cache_hit = (&*self.cached_name.borrow() == &name) &&
(self.cached_size.get() == size) &&
(self.cached_device_pixel_ratio.get() == device_pixel_ratio) &&
(&*self.cached_properties.borrow() == &properties) &&
(&*self.cached_arguments.borrow() == &arguments);
let result = if cache_hit {
debug!("Cache hit on paint worklet {}!", name);
self.cached_result.borrow().clone()
} else {
debug!("Cache miss on paint worklet {}!", name);
let map = StylePropertyMapReadOnly::from_iter(self.upcast(), properties.iter().cloned());
let result = self.draw_a_paint_image(&name, size, device_pixel_ratio, &*map, &*arguments);
if (result.image_key.is_some()) && (result.missing_image_urls.is_empty()) {
*self.cached_name.borrow_mut() = name;
self.cached_size.set(size);
self.cached_device_pixel_ratio.set(device_pixel_ratio);
*self.cached_properties.borrow_mut() = properties;
*self.cached_arguments.borrow_mut() = arguments;
*self.cached_result.borrow_mut() = result.clone();
}
result
};
let _ = sender.send(result);
}
PaintWorkletTask::SpeculativelyDrawAPaintImage(name, properties, arguments) => {
let should_speculate = (&*self.cached_name.borrow() != &name) ||
(&*self.cached_properties.borrow() != &properties) ||
(&*self.cached_arguments.borrow() != &arguments);
if should_speculate {
let size = self.cached_size.get();
let device_pixel_ratio = self.cached_device_pixel_ratio.get();
let map = StylePropertyMapReadOnly::from_iter(self.upcast(), properties.iter().cloned());
let result = self.draw_a_paint_image(&name, size, device_pixel_ratio, &*map, &*arguments);
if (result.image_key.is_some()) && (result.missing_image_urls.is_empty()) {
*self.cached_name.borrow_mut() = name;
*self.cached_properties.borrow_mut() = properties;
*self.cached_arguments.borrow_mut() = arguments;
*self.cached_result.borrow_mut() = result;
}
}
}
}
}
/// <https://drafts.css-houdini.org/css-paint-api/#draw-a-paint-image>
fn draw_a_paint_image(&self,
name: &Atom,
size_in_px: TypedSize2D<f32, CSSPixel>,
device_pixel_ratio: TypedScale<f32, CSSPixel, DevicePixel>,
properties: &StylePropertyMapReadOnly,
arguments: &[String])
-> DrawAPaintImageResult
{
let size_in_dpx = size_in_px * device_pixel_ratio;
let size_in_dpx = TypedSize2D::new(size_in_dpx.width.abs() as u32, size_in_dpx.height.abs() as u32);
// TODO: Steps 1-5.
// TODO: document paint definitions.
self.invoke_a_paint_callback(name, size_in_px, size_in_dpx, device_pixel_ratio, properties, arguments)
}
/// <https://drafts.css-houdini.org/css-paint-api/#invoke-a-paint-callback>
#[allow(unsafe_code)]
fn invoke_a_paint_callback(&self,
name: &Atom,
size_in_px: TypedSize2D<f32, CSSPixel>,
size_in_dpx: TypedSize2D<u32, DevicePixel>,
device_pixel_ratio: TypedScale<f32, CSSPixel, DevicePixel>,
properties: &StylePropertyMapReadOnly,
arguments: &[String])
-> DrawAPaintImageResult
{
debug!("Invoking a paint callback {}({},{}) at {}.",
name, size_in_px.width, size_in_px.height, device_pixel_ratio);
let cx = self.worklet_global.get_cx();
let _ac = JSAutoCompartment::new(cx, self.worklet_global.reflector().get_jsobject().get());
// TODO: Steps 1-2.1.
// Step 2.2-5.1.
rooted!(in(cx) let mut class_constructor = UndefinedValue());
rooted!(in(cx) let mut paint_function = UndefinedValue());
let rendering_context = match self.paint_definitions.borrow().get(name) {
None => {
// Step 2.2.
warn!("Drawing un-registered paint definition {}.", name);
return self.invalid_image(size_in_dpx, vec![]);
}
Some(definition) => {
// Step 5.1
if !definition.constructor_valid_flag.get() {
debug!("Drawing invalid paint definition {}.", name);
return self.invalid_image(size_in_dpx, vec![]);
}
class_constructor.set(definition.class_constructor.get());
paint_function.set(definition.paint_function.get());
DomRoot::from_ref(&*definition.context)
}
};
// Steps 5.2-5.4
// TODO: the spec requires calling the constructor now, but we might want to
// prepopulate the paint instance in `RegisterPaint`, to avoid calling it in
// the primary worklet thread.
// https://github.com/servo/servo/issues/17377
rooted!(in(cx) let mut paint_instance = UndefinedValue());
match self.paint_class_instances.borrow_mut().entry(name.clone()) {
Entry::Occupied(entry) => paint_instance.set(entry.get().get()),
Entry::Vacant(entry) => {
// Step 5.2-5.3
let args = HandleValueArray::new();
rooted!(in(cx) let mut result = null_mut());
unsafe { Construct1(cx, class_constructor.handle(), &args, result.handle_mut()); }
paint_instance.set(ObjectValue(result.get()));
if unsafe { JS_IsExceptionPending(cx) } {
debug!("Paint constructor threw an exception {}.", name);
unsafe { JS_ClearPendingException(cx); }
self.paint_definitions.borrow_mut().get_mut(name)
.expect("Vanishing paint definition.")
.constructor_valid_flag.set(false);
return self.invalid_image(size_in_dpx, vec![]);
}
// Step 5.4
entry.insert(Box::new(Heap::default())).set(paint_instance.get());
}
};
// TODO: Steps 6-7
// Step 8
// TODO: the spec requires creating a new paint rendering context each time,
// this code recycles the same one.
rendering_context.set_bitmap_dimensions(size_in_px, device_pixel_ratio);
// Step 9
let paint_size = PaintSize::new(self, size_in_px);
// TODO: Step 10
// Steps 11-12
debug!("Invoking paint function {}.", name);
rooted_vec!(let arguments_values <- arguments.iter().cloned()
.map(|argument| CSSStyleValue::new(self.upcast(), argument)));
let arguments_value_vec: Vec<JSVal> = arguments_values.iter()
.map(|argument| ObjectValue(argument.reflector().get_jsobject().get()))
.collect();
let arguments_value_array = unsafe { HandleValueArray::from_rooted_slice(&*arguments_value_vec) };
rooted!(in(cx) let argument_object = unsafe { JS_NewArrayObject(cx, &arguments_value_array) });
let args_slice = [
ObjectValue(rendering_context.reflector().get_jsobject().get()),
ObjectValue(paint_size.reflector().get_jsobject().get()),
ObjectValue(properties.reflector().get_jsobject().get()),
ObjectValue(argument_object.get()),
];
let args = unsafe { HandleValueArray::from_rooted_slice(&args_slice) };
rooted!(in(cx) let mut result = UndefinedValue());
unsafe { Call(cx, paint_instance.handle(), paint_function.handle(), &args, result.handle_mut()); }
let missing_image_urls = rendering_context.take_missing_image_urls();
// Step 13.
if unsafe { JS_IsExceptionPending(cx) } {
debug!("Paint function threw an exception {}.", name);
unsafe { JS_ClearPendingException(cx); }
return self.invalid_image(size_in_dpx, missing_image_urls);
}
let (sender, receiver) = ipc::channel().expect("IPC channel creation.");
rendering_context.send_data(sender);
let image_key = match receiver.recv() {
Ok(data) => Some(data.image_key),
_ => None,
};
DrawAPaintImageResult {
width: size_in_dpx.width,
height: size_in_dpx.height,
format: PixelFormat::BGRA8,
image_key: image_key,
missing_image_urls: missing_image_urls,
}
}
// https://drafts.csswg.org/css-images-4/#invalid-image
fn invalid_image(&self, size: TypedSize2D<u32, DevicePixel>, missing_image_urls: Vec<ServoUrl>)
-> DrawAPaintImageResult {
debug!("Returning an invalid image.");
DrawAPaintImageResult {
width: size.width as u32,
height: size.height as u32,
format: PixelFormat::BGRA8,
image_key: None,
missing_image_urls: missing_image_urls,
}
}
fn painter(&self, name: Atom) -> Box<Painter> {
// Rather annoyingly we have to use a mutex here to make the painter Sync.
struct WorkletPainter {
name: Atom,
executor: Mutex<WorkletExecutor>,
}
impl SpeculativePainter for WorkletPainter {
fn speculatively_draw_a_paint_image(&self,
properties: Vec<(Atom, String)>,
arguments: Vec<String>) {
let name = self.name.clone();
let task = PaintWorkletTask::SpeculativelyDrawAPaintImage(name, properties, arguments);
self.executor.lock().expect("Locking a painter.")
.schedule_a_worklet_task(WorkletTask::Paint(task));
}
}
impl Painter for WorkletPainter {
fn draw_a_paint_image(&self,
size: TypedSize2D<f32, CSSPixel>,
device_pixel_ratio: TypedScale<f32, CSSPixel, DevicePixel>,
properties: Vec<(Atom, String)>,
arguments: Vec<String>)
-> Result<DrawAPaintImageResult, PaintWorkletError> {
let name = self.name.clone();
let (sender, receiver) = mpsc::channel();
let task = PaintWorkletTask::DrawAPaintImage(name,
size,
device_pixel_ratio,
properties,
arguments,
sender);
self.executor.lock().expect("Locking a painter.")
.schedule_a_worklet_task(WorkletTask::Paint(task));
let timeout = PREFS.get("dom.worklet.timeout_ms")
.as_u64()
.unwrap_or(10u64);
let timeout_duration = Duration::from_millis(timeout);
receiver.recv_timeout(timeout_duration)
.map_err(|e| PaintWorkletError::from(e))
}
}
Box::new(WorkletPainter {
name: name,
executor: Mutex::new(self.worklet_global.executor()),
})
}
}
/// Tasks which can be peformed by a paint worklet
pub enum PaintWorkletTask {
DrawAPaintImage(Atom,
TypedSize2D<f32, CSSPixel>,
TypedScale<f32, CSSPixel, DevicePixel>,
Vec<(Atom, String)>,
Vec<String>,
Sender<DrawAPaintImageResult>),
SpeculativelyDrawAPaintImage(Atom,
Vec<(Atom, String)>,
Vec<String>),
}
/// A paint definition
/// <https://drafts.css-houdini.org/css-paint-api/#paint-definition>
/// This type is dangerous, because it contains uboxed `Heap<JSVal>` values,
/// which can't be moved.
#[derive(JSTraceable, MallocSizeOf)]
#[must_root]
struct PaintDefinition {
class_constructor: Heap<JSVal>,
paint_function: Heap<JSVal>,
constructor_valid_flag: Cell<bool>,
context_alpha_flag: bool,
// TODO: this should be a list of CSS syntaxes.
input_arguments_len: usize,
// TODO: the spec calls for fresh rendering contexts each time a paint image is drawn,
// but to avoid having the primary worklet thread create a new renering context,
// we recycle them.
context: Dom<PaintRenderingContext2D>,
}
impl PaintDefinition {
fn new(class_constructor: HandleValue,
paint_function: HandleValue,
alpha: bool,
input_arguments_len: usize,
context: &PaintRenderingContext2D)
-> Box<PaintDefinition>
{
let result = Box::new(PaintDefinition {
class_constructor: Heap::default(),
paint_function: Heap::default(),
constructor_valid_flag: Cell::new(true),
context_alpha_flag: alpha,
input_arguments_len: input_arguments_len,
context: Dom::from_ref(context),
});
result.class_constructor.set(class_constructor.get());
result.paint_function.set(paint_function.get());
result
}
}
impl PaintWorkletGlobalScopeMethods for PaintWorkletGlobalScope {
#[allow(unsafe_code)]
#[allow(unrooted_must_root)]
/// <https://drafts.css-houdini.org/css-paint-api/#dom-paintworkletglobalscope-registerpaint>
fn RegisterPaint(&self, name: DOMString, paint_ctor: Rc<VoidFunction>) -> Fallible<()> {
let name = Atom::from(name);
let cx = self.worklet_global.get_cx();
rooted!(in(cx) let paint_obj = paint_ctor.callback_holder().get());
rooted!(in(cx) let paint_val = ObjectValue(paint_obj.get()));
debug!("Registering paint image name {}.", name);
// Step 1.
if name.is_empty() {
return Err(Error::Type(String::from("Empty paint name."))) ;
}
// Step 2-3.
if self.paint_definitions.borrow().contains_key(&name) {
return Err(Error::InvalidModification);
}
// Step 4-6.
let mut property_names: Vec<String> =
unsafe { get_property(cx, paint_obj.handle(), "inputProperties", ()) }?
.unwrap_or_default();
let properties = property_names.drain(..).map(Atom::from).collect();
// Step 7-9.
let input_arguments: Vec<String> =
unsafe { get_property(cx, paint_obj.handle(), "inputArguments", ()) }?
.unwrap_or_default();
// TODO: Steps 10-11.
// Steps 12-13.
let alpha: bool =
unsafe { get_property(cx, paint_obj.handle(), "alpha", ()) }?
.unwrap_or(true);
// Step 14
if unsafe { !IsConstructor(paint_obj.get()) } {
return Err(Error::Type(String::from("Not a constructor.")));
}
// Steps 15-16
rooted!(in(cx) let mut prototype = UndefinedValue());
unsafe { get_property_jsval(cx, paint_obj.handle(), "prototype", prototype.handle_mut())?; }
if !prototype.is_object() {
return Err(Error::Type(String::from("Prototype is not an object.")));
}
rooted!(in(cx) let prototype = prototype.to_object());
// Steps 17-18
rooted!(in(cx) let mut paint_function = UndefinedValue());
unsafe { get_property_jsval(cx, prototype.handle(), "paint", paint_function.handle_mut())?; }
if !paint_function.is_object() || unsafe { !IsCallable(paint_function.to_object()) } {
return Err(Error::Type(String::from("Paint function is not callable.")));
}
// Step 19.
let context = PaintRenderingContext2D::new(self);
let definition = PaintDefinition::new(paint_val.handle(),
paint_function.handle(),
alpha,
input_arguments.len(),
&*context);
// Step 20.
debug!("Registering definition {}.", name);
self.paint_definitions.borrow_mut().insert(name.clone(), definition);
// TODO: Step 21.
// Inform layout that there is a registered paint worklet.
// TODO: layout will end up getting this message multiple times.
let painter = self.painter(name.clone());
self.worklet_global.register_paint_worklet(name, properties, painter);
Ok(())
}
/// This is a blocking sleep function available in the paint worklet
/// global scope behind the dom.worklet.enabled +
/// dom.worklet.blockingsleep.enabled prefs. It is to be used only for
/// testing, e.g., timeouts, where otherwise one would need busy waiting
/// to make sure a certain timeout is triggered.
/// check-tidy: no specs after this line
fn Sleep(&self, ms: u64) {
thread::sleep(Duration::from_millis(ms));
}
}