fix(canvas): settle ImageBitmap promises through bitmap tasks

Reject Web IDL conversion errors through the returned Promise and decode
raster Blobs from their bytes on the blocking pool. Deliver results through
a dedicated bitmap task source bound to the exact Page and Window realm.

Retain canvas, ImageData and ImageBitmap pixel snapshots for crop, resize,
flip and alpha processing. Clear pixels on close and reject closed sources
in drawImage. Read HTML canvas dimensions from native attributes.

Cover Promise timing, pixels, intrinsic prototypes, child realm retirement,
document.open and real navigation identity collisions with Page task tests.
This commit is contained in:
ldm0
2026-09-27 06:43:21 +08:00
parent 44d72f947d
commit 38e4ea85db
33 changed files with 1683 additions and 159 deletions
+1 -1
View File
@@ -295,7 +295,7 @@ pub(super) fn build_blob_object<'s>(
Some(object)
}
fn blob_id_from_object<'s>(
pub(crate) fn blob_id_from_object<'s>(
scope: &mut v8::PinScope<'s, '_>,
object: v8::Local<'s, v8::Object>,
) -> Option<BlobId> {
+4 -3
View File
@@ -226,9 +226,10 @@ pub(crate) use self::broadcast_channel::{
dispatch_authorized_page_broadcast_channel_event, dispatch_broadcast_channel_events_for_channel,
};
pub(crate) use self::canvas::{
CanvasContextKind, attach_canvas_like_context_object, build_canvas_rendering_context_2d_object,
build_offscreen_canvas_object, build_webgl_context_object, build_webgl2_context_object,
canvas_like_to_data_url, reset_html_canvas_backing_store_for_dimension_assignment,
BitmapRejection, BitmapTaskResult, CanvasContextKind, attach_canvas_like_context_object,
build_canvas_rendering_context_2d_object, build_offscreen_canvas_object,
build_webgl_context_object, build_webgl2_context_object, canvas_like_to_data_url,
reset_html_canvas_backing_store_for_dimension_assignment, settle_bitmap_task_result,
};
use self::constructors::illegal_constructor_callback;
pub(crate) use self::constructors::{
@@ -242,7 +242,10 @@ pub(crate) use context2d::{
canvas_context_transform_callback, canvas_context_translate_callback,
canvas_gradient_add_color_stop_callback,
};
pub(crate) use image_bitmap::window_create_image_bitmap_callback;
pub(crate) use image_bitmap::{
BitmapRejection, BitmapTaskResult, settle_bitmap_task_result,
window_create_image_bitmap_callback,
};
pub(crate) use objects::{
build_canvas_rendering_context_2d_object, build_offscreen_canvas_object,
build_webgl_context_object, build_webgl2_context_object,
@@ -43,14 +43,6 @@ pub(crate) fn attach_canvas_like_context_object<'s>(
let _ = ensure_canvas_like_backing_store(scope, canvas);
}
pub(super) fn canvas_like_has_context<'s>(
scope: &mut v8::PinScope<'s, '_>,
canvas: v8::Local<'s, v8::Object>,
) -> bool {
get_private_value(scope, canvas, CANVAS_HAS_CONTEXT_SLOT)
.is_some_and(|value| value.boolean_value(scope))
}
pub(crate) fn reset_canvas_like_backing_store<'s>(
scope: &mut v8::PinScope<'s, '_>,
canvas: v8::Local<'s, v8::Object>,
@@ -221,6 +213,13 @@ pub(super) fn canvas_like_dimensions<'s>(
scope: &mut v8::PinScope<'s, '_>,
canvas: v8::Local<'s, v8::Object>,
) -> Option<(u32, u32)> {
if html_canvas_identity(scope, canvas).is_some() {
let width =
crate::native_bridge::element::canvas_dimension_value(scope, canvas, "width", 300);
let height =
crate::native_bridge::element::canvas_dimension_value(scope, canvas, "height", 150);
return Some((width, height));
}
let width = canvas_like_dimension(scope, canvas, OFFSCREEN_CANVAS_WIDTH_SLOT, "width")?;
let height = canvas_like_dimension(scope, canvas, OFFSCREEN_CANVAS_HEIGHT_SLOT, "height")?;
Some((width, height))
@@ -1657,8 +1657,22 @@ pub(crate) fn canvas_context_draw_image_callback<'s>(
let Ok(source) = v8::Local::<v8::Object>::try_from(args.get(0)) else {
return;
};
let Some((source_pixels, source_width, source_height)) =
html_image_pixels_copy(scope, source).or_else(|| canvas_like_pixels_copy(scope, source))
let bitmap_pixels = if super::image_bitmap::image_bitmap_receiver_branded(scope, source) {
let Some(pixels) = super::image_bitmap::image_bitmap_pixels_copy(scope, source) else {
crate::context_bootstrap::throw_dom_exception_value(
scope,
"The ImageBitmap is detached.",
"InvalidStateError",
);
return;
};
Some(pixels)
} else {
None
};
let Some((source_pixels, source_width, source_height)) = bitmap_pixels
.or_else(|| html_image_pixels_copy(scope, source))
.or_else(|| canvas_like_pixels_copy(scope, source))
else {
return;
};
@@ -1,15 +1,34 @@
use super::backing_store::{canvas_like_has_context, canvas_like_pixels_copy};
use super::backing_store::canvas_like_pixels_copy;
use super::offscreen::offscreen_canvas_receiver_branded;
use super::*;
use crate::context_bootstrap::new_dom_exception_value;
use crate::util::{
callback_data_index_value, callback_data_item, get_private_value, set_private_value,
callback_data_index_value, callback_data_item, context_host_ptr_from_global_bridge,
get_private_value, set_private_value,
};
use crate::web_api_interfaces;
use moli_webapi_declare::{WebApiFunctionTemplate, WebApiObject};
mod options;
use options::{BitmapOptions, BitmapParameters};
#[derive(Debug)]
pub(crate) struct BitmapTaskResult {
width: u32,
height: u32,
pixels: Vec<u8>,
premultiplied: bool,
}
#[derive(Clone, Copy, Debug)]
pub(crate) enum BitmapRejection {
InvalidState,
}
const IMAGE_BITMAP_WIDTH_SLOT: &str = "__moliImageBitmapWidth";
const IMAGE_BITMAP_HEIGHT_SLOT: &str = "__moliImageBitmapHeight";
const IMAGE_BITMAP_PIXELS_SLOT: &str = "__moliImageBitmapPixels";
const IMAGE_BITMAP_PREMULTIPLIED_SLOT: &str = "__moliImageBitmapPremultiplied";
const IMAGE_BITMAP_DIMENSION_SLOTS: &[&str] = &[IMAGE_BITMAP_WIDTH_SLOT, IMAGE_BITMAP_HEIGHT_SLOT];
@@ -60,66 +79,278 @@ pub(crate) fn window_create_image_bitmap_callback<'s>(
throw_type_error(scope, "createImageBitmap is not a constructor");
return;
}
if args.length() == 0 {
throw_type_error(
scope,
"Failed to execute 'createImageBitmap' on 'Window': 1 argument required, but only 0 present.",
);
return;
}
let Ok(source) = v8::Local::<v8::Object>::try_from(args.get(0)) else {
throw_type_error(
scope,
"Failed to execute 'createImageBitmap' on 'Window': The provided value is not a valid image source.",
);
return;
};
if !offscreen_canvas_receiver_branded(scope, source) {
throw_type_error(
scope,
"Failed to execute 'createImageBitmap' on 'Window': The provided value is not a valid image source.",
);
return;
}
let Some(resolver) = v8::PromiseResolver::new(scope) else {
rv.set_undefined();
return;
};
let promise = resolver.get_promise(scope);
if !canvas_like_has_context(scope, source) {
let error = new_dom_exception_value(
scope,
"Failed to execute 'createImageBitmap' on 'Window': The ImageBitmap could not be allocated.",
"InvalidStateError",
);
rv.set(promise.into());
// Promise-returning Web IDL operations turn conversion failures (including
// arbitrary getter exceptions) into rejections of the returned Promise.
let parsed = {
let try_catch = std::pin::pin!(v8::TryCatch::new(scope));
let mut conversion_scope = try_catch.init();
match parse_bitmap_arguments(&mut conversion_scope, &args) {
Ok(parsed) => Ok(parsed),
Err(error) => {
webidl::throw_error(&mut conversion_scope, &error);
let exception = conversion_scope
.exception()
.unwrap_or_else(|| v8::undefined(&conversion_scope).into());
conversion_scope.reset();
Err(exception)
}
}
};
let (source, parameters) = match parsed {
Ok(parsed) => parsed,
Err(exception) => {
let _ = resolver.reject(scope, exception);
return;
}
};
if parameters
.crop
.is_some_and(|[_, _, width, height]| width == 0 || height == 0)
{
let message = crate::util::v8str(scope, "The crop rectangle has a zero dimension.");
let error = v8::Exception::range_error(scope, message);
let _ = resolver.reject(scope, error);
rv.set(promise.into());
return;
}
if parameters.options.resize_width == Some(0) || parameters.options.resize_height == Some(0) {
reject_bitmap(scope, resolver);
return;
}
// Snapshot only after all observable conversions, which may mutate or
// detach the source. No V8 handles or live DOM state cross the task boundary.
let input = match source.snapshot(scope) {
Some(input) => input,
None => {
reject_bitmap(scope, resolver);
return;
}
};
let producer = context_host_ptr_from_global_bridge(scope).and_then(|host_ptr| {
// SAFETY: the bridge points at this callback's live Window host.
unsafe { &mut *host_ptr }.register_pending_bitmap_task(scope, resolver)
});
let Some(producer) = producer else {
reject_bitmap(scope, resolver);
return;
};
match input {
BitmapInput::Pixels(image) => {
let _ = producer.send(parameters.apply(image));
}
BitmapInput::Blob(bytes) => {
let decode = move || {
let result = moli_image::decode_raster_image(&bytes)
.map_err(|_| BitmapRejection::InvalidState)
.and_then(|decoded| parameters.apply(decoded.image));
let _ = producer.send(result);
};
if let Ok(handle) = tokio::runtime::Handle::try_current() {
handle.spawn_blocking(decode);
} else {
std::thread::spawn(decode);
}
}
}
}
let Some((_pixels, width, height)) = canvas_like_pixels_copy(scope, source) else {
let error = new_dom_exception_value(
scope,
"Failed to execute 'createImageBitmap' on 'Window': The ImageBitmap could not be allocated.",
"InvalidStateError",
);
let _ = resolver.reject(scope, error);
rv.set(promise.into());
return;
#[derive(Clone, Copy)]
enum BitmapSourceKind {
Blob,
Canvas,
ImageData,
ImageBitmap,
}
struct BitmapSource<'s> {
object: v8::Local<'s, v8::Object>,
kind: BitmapSourceKind,
}
impl<'s> webidl::WebIdlConverter<'s> for BitmapSource<'s> {
type Options = ();
fn convert(
scope: &mut v8::PinScope<'s, '_>,
value: v8::Local<'s, v8::Value>,
context: webidl::Context,
_options: &(),
) -> Result<Self, webidl::WebIdlError> {
let object = webidl::convert::<v8::Local<'s, v8::Object>>(scope, value, context)?;
let kind = if crate::blob::blob_id_from_object(scope, object).is_some() {
BitmapSourceKind::Blob
} else if offscreen_canvas_receiver_branded(scope, object) || is_html_canvas(scope, object)
{
BitmapSourceKind::Canvas
} else if crate::context_bootstrap::image_data::is_image_data_object(scope, object) {
BitmapSourceKind::ImageData
} else if image_bitmap_receiver_branded(scope, object) {
BitmapSourceKind::ImageBitmap
} else {
return Err(webidl::WebIdlError::custom_message(
"The value is not a supported ImageBitmapSource.",
));
};
Ok(Self { object, kind })
}
}
fn is_html_canvas<'s>(scope: &mut v8::PinScope<'s, '_>, object: v8::Local<'s, v8::Object>) -> bool {
crate::native_bridge::node_runtime_and_handle_from_object_or_detached(scope, object)
.ok()
.is_some_and(|(host, handle)| {
unsafe { &*host }
.dom_host()
.is_html_element_named(handle, "canvas")
})
}
enum BitmapInput {
Blob(Vec<u8>),
Pixels(moli_image::RgbaImage),
}
impl<'s> BitmapSource<'s> {
fn snapshot(self, scope: &mut v8::PinScope<'s, '_>) -> Option<BitmapInput> {
let (pixels, width, height) = match self.kind {
BitmapSourceKind::Blob => {
return crate::blob::blob_bytes_from_object(scope, self.object)
.map(BitmapInput::Blob);
}
BitmapSourceKind::Canvas => canvas_like_pixels_copy(scope, self.object)?,
BitmapSourceKind::ImageData => {
let data =
crate::context_bootstrap::image_data::image_data_clone_payload_from_object(
scope,
self.object,
)?;
(data.bytes, data.width, data.height)
}
BitmapSourceKind::ImageBitmap => image_bitmap_pixels_copy(scope, self.object)?,
};
if width == 0 || height == 0 {
return None;
}
moli_image::RgbaImage::try_new(width, height, pixels)
.ok()
.map(BitmapInput::Pixels)
}
}
fn parse_bitmap_arguments<'s>(
scope: &mut v8::PinScope<'s, '_>,
args: &v8::FunctionCallbackArguments<'s>,
) -> Result<(BitmapSource<'s>, BitmapParameters), webidl::WebIdlError> {
let context = |index| webidl::Context::argument("createImageBitmap", index);
if args.length() == 0 {
return Err(webidl::WebIdlError::missing_required(context(1)));
}
if matches!(args.length(), 3 | 4) {
return Err(webidl::WebIdlError::custom_message(
"The crop overload requires five arguments.",
));
}
let source = webidl::argument::<BitmapSource>(scope, args, 0, context(1))?;
let crop = if args.length() >= 5 {
let mut crop = [0; 4];
for (index, coordinate) in crop.iter_mut().enumerate() {
*coordinate = webidl::argument::<webidl::Long>(
scope,
args,
index as i32 + 1,
context(index + 2),
)?
.0;
}
Some(crop)
} else {
None
};
let Some(bitmap) = build_image_bitmap_object(scope, width, height) else {
let error = new_dom_exception_value(
let options_index = if crop.is_some() { 5 } else { 1 };
let options = webidl::parse_dictionary::<BitmapOptions>(
scope,
args.get(options_index),
context(options_index as usize + 1),
)?
.unwrap_or_default();
Ok((source, BitmapParameters { crop, options }))
}
pub(crate) fn settle_bitmap_task_result<'s>(
scope: &mut v8::PinScope<'s, '_>,
resolver: v8::Local<'s, v8::PromiseResolver>,
result: Result<BitmapTaskResult, BitmapRejection>,
) {
let bitmap = result.ok().and_then(|result| {
let bitmap = build_image_bitmap_object(scope, result.width, result.height)?;
let pixels =
super::super::image_data::new_uint8_clamped_array_from_bytes(scope, result.pixels)?;
set_private_value(scope, bitmap, IMAGE_BITMAP_PIXELS_SLOT, pixels.into());
set_private_value(
scope,
"Failed to execute 'createImageBitmap' on 'Window': The ImageBitmap could not be allocated.",
"InvalidStateError",
bitmap,
IMAGE_BITMAP_PREMULTIPLIED_SLOT,
v8::Boolean::new(scope, result.premultiplied).into(),
);
let _ = resolver.reject(scope, error);
rv.set(promise.into());
return;
};
let _ = resolver.resolve(scope, bitmap.into());
rv.set(promise.into());
Some(bitmap)
});
match bitmap {
Some(bitmap) => {
let _ = resolver.resolve(scope, bitmap.into());
}
None => reject_bitmap(scope, resolver),
}
}
fn reject_bitmap<'s>(
scope: &mut v8::PinScope<'s, '_>,
resolver: v8::Local<'s, v8::PromiseResolver>,
) {
let error = new_dom_exception_value(
scope,
"The image source could not be decoded or allocated.",
"InvalidStateError",
);
let _ = resolver.reject(scope, error);
}
pub(super) fn image_bitmap_pixels_copy<'s>(
scope: &mut v8::PinScope<'s, '_>,
bitmap: v8::Local<'s, v8::Object>,
) -> Option<(Vec<u8>, u32, u32)> {
if !image_bitmap_receiver_branded(scope, bitmap) {
return None;
}
let width = get_private_value(scope, bitmap, IMAGE_BITMAP_WIDTH_SLOT)?.uint32_value(scope)?;
let height = get_private_value(scope, bitmap, IMAGE_BITMAP_HEIGHT_SLOT)?.uint32_value(scope)?;
let view = v8::Local::<v8::Uint8ClampedArray>::try_from(get_private_value(
scope,
bitmap,
IMAGE_BITMAP_PIXELS_SLOT,
)?)
.ok()?;
let mut pixels = vec![0; view.byte_length()];
view.copy_contents(&mut pixels);
if get_private_value(scope, bitmap, IMAGE_BITMAP_PREMULTIPLIED_SLOT)
.is_some_and(|value| value.boolean_value(scope))
{
for pixel in pixels.chunks_exact_mut(4) {
let alpha = u32::from(pixel[3]);
for channel in &mut pixel[..3] {
*channel = (u32::from(*channel) * 255 + alpha / 2)
.checked_div(alpha)
.unwrap_or(0)
.min(255) as u8;
}
}
}
Some((pixels, width, height))
}
fn build_image_bitmap_object<'s>(
@@ -174,10 +405,16 @@ fn image_bitmap_close_callback<'s>(
for slot in IMAGE_BITMAP_DIMENSION_SLOTS {
set_private_value(scope, args.this(), slot, v8::Number::new(scope, 0.0).into());
}
set_private_value(
scope,
args.this(),
IMAGE_BITMAP_PIXELS_SLOT,
v8::undefined(scope).into(),
);
rv.set_undefined();
}
fn image_bitmap_receiver_branded<'s>(
pub(super) fn image_bitmap_receiver_branded<'s>(
scope: &mut v8::PinScope<'s, '_>,
receiver: v8::Local<'s, v8::Object>,
) -> bool {
@@ -0,0 +1,191 @@
use super::{BitmapRejection, BitmapTaskResult};
use crate::webidl;
use moli_canvas::{DrawImageBlit, ScaleFilter, blit_draw_image_filtered, byte_len};
use moli_image::RgbaImage;
use std::str::FromStr;
#[derive(Clone, Copy, Default, strum::EnumString, webidl::WebIdlEnum)]
#[webidl(name = "ImageOrientation", parse_with = Self::parse)]
enum ImageOrientation {
#[default]
#[strum(serialize = "from-image")]
FromImage,
#[strum(serialize = "flipY")]
FlipY,
}
impl ImageOrientation {
fn parse(value: &str) -> Option<Self> {
Self::from_str(value).ok()
}
}
#[derive(Clone, Copy, Default, strum::EnumString, webidl::WebIdlEnum)]
#[webidl(name = "PremultiplyAlpha", parse_with = Self::parse)]
#[strum(serialize_all = "lowercase")]
enum PremultiplyAlpha {
None,
Premultiply,
#[default]
Default,
}
impl PremultiplyAlpha {
fn parse(value: &str) -> Option<Self> {
Self::from_str(value).ok()
}
}
#[derive(Clone, Copy, Default, strum::EnumString, webidl::WebIdlEnum)]
#[webidl(name = "ColorSpaceConversion", parse_with = Self::parse)]
#[strum(serialize_all = "lowercase")]
enum ColorSpaceConversion {
None,
#[default]
Default,
}
impl ColorSpaceConversion {
fn parse(value: &str) -> Option<Self> {
Self::from_str(value).ok()
}
}
#[derive(Clone, Copy, Default, strum::EnumString, webidl::WebIdlEnum)]
#[webidl(name = "ResizeQuality", parse_with = Self::parse)]
#[strum(serialize_all = "lowercase")]
enum ResizeQuality {
Pixelated,
#[default]
Low,
Medium,
High,
}
impl ResizeQuality {
fn parse(value: &str) -> Option<Self> {
Self::from_str(value).ok()
}
}
#[derive(Default, webidl::WebIdlDictionary)]
#[webidl(prefix = "ImageBitmapOptions")]
pub(super) struct BitmapOptions {
// The raster backend leaves color profiles unconverted in both modes.
// Members are declared in Web IDL's observable lexicographic read order.
#[webidl(name = "colorSpaceConversion", converter = "enum", default = ColorSpaceConversion::Default)]
_color_space_conversion: ColorSpaceConversion,
#[webidl(converter = "enum", default = ImageOrientation::FromImage)]
image_orientation: ImageOrientation,
#[webidl(converter = "enum", default = PremultiplyAlpha::Default)]
premultiply_alpha: PremultiplyAlpha,
#[webidl(converter = "enforce_range_unsigned_long")]
pub(super) resize_height: Option<u32>,
#[webidl(converter = "enum", default = ResizeQuality::Low)]
resize_quality: ResizeQuality,
#[webidl(converter = "enforce_range_unsigned_long")]
pub(super) resize_width: Option<u32>,
}
pub(super) struct BitmapParameters {
pub(super) crop: Option<[i32; 4]>,
pub(super) options: BitmapOptions,
}
impl BitmapParameters {
pub(super) fn apply(self, image: RgbaImage) -> Result<BitmapTaskResult, BitmapRejection> {
if image.width == 0 || image.height == 0 {
return Err(BitmapRejection::InvalidState);
}
let [mut x, mut y, mut width, mut height] = self
.crop
.map(|crop| crop.map(f64::from))
.unwrap_or([0.0, 0.0, f64::from(image.width), f64::from(image.height)]);
// Negative extents select the rectangle on the other side of the
// origin; they do not mirror its pixels.
if width < 0.0 {
x += width;
width = -width;
}
if height < 0.0 {
y += height;
height = -height;
}
let (output_width, output_height) =
match (self.options.resize_width, self.options.resize_height) {
(Some(w), Some(h)) => (w, h),
(Some(w), None) => (w, (height * f64::from(w) / width).ceil() as u32),
(None, Some(h)) => ((width * f64::from(h) / height).ceil() as u32, h),
(None, None) => (width as u32, height as u32),
};
if output_width == 0 || output_height == 0 {
return Err(BitmapRejection::InvalidState);
}
let len = byte_len(output_width, output_height).ok_or(BitmapRejection::InvalidState)?;
let mut pixels = if x == 0.0
&& y == 0.0
&& width == f64::from(image.width)
&& height == f64::from(image.height)
&& output_width == image.width
&& output_height == image.height
{
image.rgba
} else {
let mut pixels = vec![0; len];
let blit = DrawImageBlit::new(
x,
y,
width,
height,
0.0,
0.0,
f64::from(output_width),
f64::from(output_height),
)
.ok_or(BitmapRejection::InvalidState)?;
let filter = if matches!(self.options.resize_quality, ResizeQuality::Pixelated) {
ScaleFilter::Nearest
} else {
ScaleFilter::Bilinear
};
blit_draw_image_filtered(
&mut pixels,
output_width,
output_height,
&image.rgba,
image.width,
image.height,
blit,
filter,
);
pixels
};
if matches!(self.options.image_orientation, ImageOrientation::FlipY) {
let row_len = output_width as usize * 4;
for y in 0..output_height as usize / 2 {
let bottom = (output_height as usize - y - 1) * row_len;
let (top_rows, bottom_rows) = pixels.split_at_mut(bottom);
top_rows[y * row_len..(y + 1) * row_len]
.swap_with_slice(&mut bottom_rows[..row_len]);
}
}
let premultiplied = matches!(
self.options.premultiply_alpha,
PremultiplyAlpha::Premultiply
);
if premultiplied {
for pixel in pixels.chunks_exact_mut(4) {
let alpha = u32::from(pixel[3]);
for channel in &mut pixel[..3] {
*channel = ((u32::from(*channel) * alpha + 127) / 255) as u8;
}
}
}
Ok(BitmapTaskResult {
width: output_width,
height: output_height,
pixels,
premultiplied,
})
}
}
@@ -0,0 +1,110 @@
use super::*;
pub(crate) struct PendingBitmapTask {
pub(crate) execution_context: super::WindowExecutionContextIdentity,
pub(crate) relevant_context: super::WindowExecutionContextBinding,
pub(crate) resolver: v8::Global<v8::PromiseResolver>,
}
impl JsContextHost {
pub(crate) fn register_pending_bitmap_task(
&mut self,
scope: &mut v8::PinScope<'_, '_>,
resolver: v8::Local<'_, v8::PromiseResolver>,
) -> Option<crate::page_task_queue::RendererPageBitmapTaskProducer> {
let task_id = self.next_bitmap_task_id;
self.next_bitmap_task_id = self
.next_bitmap_task_id
.checked_next()
.expect("Page Bitmap task id overflow");
let execution_context = self.current_runtime_window_execution_context_identity(scope)?;
let relevant_context = super::WindowExecutionContextBinding::new(
execution_context.owner(),
execution_context.dispatch_scope(),
execution_context.realm_token(),
v8::Global::new(scope, scope.get_current_context()),
);
let producer = self
.page_bitmap_task_sender()
.bind_task(execution_context, task_id);
let replaced = self.pending_bitmap_tasks.insert(
task_id,
PendingBitmapTask {
execution_context,
relevant_context,
resolver: v8::Global::new(scope, resolver),
},
);
assert!(
replaced.is_none(),
"Page Bitmap task ids must never be reused"
);
tracing::debug!(
task_id = task_id.task_id(),
?execution_context,
"registered Bitmap task with Window execution context"
);
Some(producer)
}
pub(crate) fn current_pending_bitmap_task_execution_context(
&self,
task: crate::page_task_queue::RendererPageBitmapTaskId,
) -> Option<super::WindowExecutionContextIdentity> {
let pending = self.pending_bitmap_tasks.get(&task)?;
if !self.window_execution_context_identity_is_current(pending.execution_context) {
return None;
}
Some(pending.execution_context)
}
pub(crate) fn take_pending_bitmap_task_for_exact_owner(
&mut self,
execution_context: super::WindowExecutionContextIdentity,
task: crate::page_task_queue::RendererPageBitmapTaskId,
) -> Option<PendingBitmapTask> {
let pending = self.pending_bitmap_tasks.get(&task)?;
if pending.execution_context != execution_context {
return None;
}
self.pending_bitmap_tasks.remove(&task)
}
pub(crate) fn retire_bitmap_execution_context_owner(
&mut self,
retired_owner: super::WindowExecutionContextOwner,
) -> usize {
let count_before = self.pending_bitmap_tasks.len();
self.pending_bitmap_tasks
.retain(|_, pending| pending.relevant_context.owner() != retired_owner);
let retired_count = count_before - self.pending_bitmap_tasks.len();
tracing::debug!(
?retired_owner,
retired_count,
"retired Bitmap tasks with Window execution context"
);
retired_count
}
pub(crate) fn retire_bitmap_context_token(
&mut self,
context_token: super::RuntimeObservableContextToken,
) -> usize {
let count_before = self.pending_bitmap_tasks.len();
self.pending_bitmap_tasks
.retain(|_, pending| pending.relevant_context.realm_token() != context_token);
let retired_count = count_before - self.pending_bitmap_tasks.len();
if retired_count > 0 {
tracing::debug!(
?context_token,
retired_count,
"retired Bitmap tasks with destroyed V8 context"
);
}
retired_count
}
pub(crate) fn has_pending_bitmap_tasks(&self) -> bool {
!self.pending_bitmap_tasks.is_empty()
}
}
@@ -330,7 +330,9 @@ impl JsContextHost {
next_image_decode_id: 1,
pending_image_decode_requests: HashMap::new(),
resource_timing_buffers: SharedResourceTimingBufferRegistry::new(),
next_bitmap_task_id: crate::page_task_queue::RendererPageBitmapTaskId::first(),
next_webcrypto_task_id: crate::page_task_queue::RendererPageWebCryptoTaskId::first(),
pending_bitmap_tasks: HashMap::new(),
pending_webcrypto_tasks: HashMap::new(),
opfs_owner_state: None,
history_queue: HistoryQueueState::default(),
@@ -966,6 +968,16 @@ impl JsContextHost {
.worker_host_bridge()
}
pub(crate) fn page_bitmap_task_sender(
&self,
) -> &crate::page_task_queue::RendererPageBitmapTaskSender {
self.page_task_capabilities
.get()
.expect(
"a live Page Window must install its complete Page task capabilities before Bitmap registration",
)
.bitmap_task()
}
pub(crate) fn page_webcrypto_task_sender(
&self,
) -> &crate::page_task_queue::RendererPageWebCryptoTaskSender {
@@ -148,6 +148,7 @@ mod security_policy;
mod selection_records;
mod service_workers;
pub(crate) use service_workers::ServiceWorkerWindowOwner;
mod bitmap_tasks;
mod shared_workers;
mod signal_bridge;
mod storage_events;
@@ -945,7 +946,10 @@ pub(crate) struct JsContextHost {
next_image_decode_id: u64,
pending_image_decode_requests: HashMap<ImageDecodeRequestId, PendingImageDecodeRequest>,
resource_timing_buffers: SharedResourceTimingBufferRegistry,
next_bitmap_task_id: crate::page_task_queue::RendererPageBitmapTaskId,
next_webcrypto_task_id: crate::page_task_queue::RendererPageWebCryptoTaskId,
pending_bitmap_tasks:
HashMap<crate::page_task_queue::RendererPageBitmapTaskId, bitmap_tasks::PendingBitmapTask>,
pending_webcrypto_tasks: HashMap<
crate::page_task_queue::RendererPageWebCryptoTaskId,
webcrypto_tasks::PendingWebCryptoTask,
@@ -2075,6 +2075,8 @@ impl JsContextHost {
.retire_owner(execution_context_owner);
let retired_timer_count = unsafe { &mut *self.runtime }
.cancel_window_execution_context_timers(execution_context_owner);
let retired_bitmap_count =
self.retire_bitmap_execution_context_owner(execution_context_owner);
let retired_webcrypto_count =
self.retire_webcrypto_execution_context_owner(execution_context_owner);
self.retire_opfs_execution_context_owner(execution_context_owner);
@@ -2102,6 +2104,7 @@ impl JsContextHost {
tracing::debug!(
?execution_context_owner,
retired_timer_count,
retired_bitmap_count,
retired_webcrypto_count,
retired_worker_count,
retired_shared_worker_count,
@@ -161,7 +161,7 @@ pub(super) use attributes::{
node_toggle_attribute_callback,
};
pub(crate) use canvas::{
canvas_get_context_callback, canvas_to_data_url_callback,
canvas_dimension_value, canvas_get_context_callback, canvas_to_data_url_callback,
canvas_transfer_control_to_offscreen_callback,
};
pub(crate) use canvas::{
@@ -125,7 +125,7 @@ fn set_canvas_dimension_attribute<'s>(
true
}
fn canvas_dimension_value<'s>(
pub(crate) fn canvas_dimension_value<'s>(
scope: &mut v8::PinScope<'s, '_>,
object: v8::Local<'s, v8::Object>,
name: &str,
+6
View File
@@ -1,3 +1,4 @@
mod bitmap_task;
mod broadcast_channel_delivery;
mod child_frame_task;
mod child_module_dependency_fetch_start;
@@ -155,6 +156,11 @@ pub(crate) struct PageWindowDocumentTaskTurnAction<I, K> {
impl<I, K> PageWindowDocumentTaskTurnAction<I, K> {}
pub(crate) use self::bitmap_task::{
PageBitmapTaskTargetEffect, PageBitmapTaskTurnAction, PageBitmapTaskTurnOutcome,
RendererPageBitmapTask, RendererPageBitmapTaskId, RendererPageBitmapTaskOwner,
RendererPageBitmapTaskProducer, RendererPageBitmapTaskSender,
};
pub(crate) use self::broadcast_channel_delivery::{
PageBroadcastChannelDeliveryDocumentEffect, PageBroadcastChannelDeliveryTurnAction,
PageBroadcastChannelDeliveryTurnOutcome, RendererPageBroadcastChannelDeliveryOwner,
@@ -0,0 +1,257 @@
use moli_owner_queue::{OwnerReadyTaskRoute, OwnerReadyTaskSource};
use crate::{
context_bootstrap::{BitmapRejection, BitmapTaskResult},
native_bridge::WindowExecutionContextIdentity,
resource_ready::{ReadyPageTask, RendererPageTaskReadyMetadata},
runtime::{PageOwnerTurnOutcome, RendererDocumentToken},
};
use super::{RendererOwnerWakeSender, RendererOwnerWakeSource, RendererPageTaskReadySignal};
/// PageVm-local identity of one pending Bitmap Promise.
///
/// The id is never reused within a PageVm. The enclosing task owner carries
/// the root Page and Window-realm identities, so `document.open()` can preserve
/// Window-owned Bitmap work without projecting a Document identity into
/// the task identity.
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub(crate) struct RendererPageBitmapTaskId(u64);
impl RendererPageBitmapTaskId {
pub(crate) const fn first() -> Self {
Self(1)
}
#[cfg(test)]
pub(crate) const fn new(task_id: u64) -> Self {
assert!(task_id != 0, "Bitmap task id must be non-zero");
Self(task_id)
}
pub(crate) const fn task_id(self) -> u64 {
self.0
}
pub(crate) const fn checked_next(self) -> Option<Self> {
match self.0.checked_add(1) {
Some(task_id) => Some(Self(task_id)),
None => None,
}
}
}
/// Exact owner of one page-side Bitmap completion.
///
/// The root token prevents PageVm-local ids from colliding across navigation.
/// The Window identity binds the Promise relevant realm. The task id then
/// identifies the pending resolver within that PageVm/realm.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) struct RendererPageBitmapTaskOwner {
root_document: RendererDocumentToken,
execution_context: WindowExecutionContextIdentity,
task: RendererPageBitmapTaskId,
}
impl RendererPageBitmapTaskOwner {
pub(crate) const fn new(
root_document: RendererDocumentToken,
execution_context: WindowExecutionContextIdentity,
task: RendererPageBitmapTaskId,
) -> Self {
Self {
root_document,
execution_context,
task,
}
}
pub(crate) const fn root_document(self) -> RendererDocumentToken {
self.root_document
}
pub(crate) const fn execution_context(self) -> WindowExecutionContextIdentity {
self.execution_context
}
pub(crate) const fn task(self) -> RendererPageBitmapTaskId {
self.task
}
}
#[derive(Debug)]
pub(crate) struct RendererPageBitmapTask {
owner: RendererPageBitmapTaskOwner,
result: Result<BitmapTaskResult, BitmapRejection>,
}
impl RendererPageBitmapTask {
fn new(
owner: RendererPageBitmapTaskOwner,
result: Result<BitmapTaskResult, BitmapRejection>,
) -> Self {
Self { owner, result }
}
pub(crate) const fn owner(&self) -> RendererPageBitmapTaskOwner {
self.owner
}
pub(crate) fn into_result(self) -> Result<BitmapTaskResult, BitmapRejection> {
self.result
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) struct RendererPageBitmapTaskRouteClosed;
#[derive(Clone, Debug)]
pub(crate) struct RendererPageBitmapTaskRoute {
task_route:
OwnerReadyTaskRoute<ReadyPageTask<RendererPageBitmapTask>, RendererPageTaskReadySignal>,
}
impl RendererPageBitmapTaskRoute {
pub(crate) fn sender(
&self,
root_document: RendererDocumentToken,
) -> RendererPageBitmapTaskSender {
RendererPageBitmapTaskSender {
task_route: self.task_route.clone(),
root_document,
}
}
fn same_route_as(&self, source: &RendererPageBitmapTaskSource) -> bool {
self.task_route.same_source_as(&source.source)
}
}
/// PageVm-stamped route used only while a Bitmap Promise is registered.
#[derive(Clone, Debug)]
pub(crate) struct RendererPageBitmapTaskSender {
task_route:
OwnerReadyTaskRoute<ReadyPageTask<RendererPageBitmapTask>, RendererPageTaskReadySignal>,
root_document: RendererDocumentToken,
}
impl RendererPageBitmapTaskSender {
pub(crate) fn bind_task(
&self,
execution_context: WindowExecutionContextIdentity,
task: RendererPageBitmapTaskId,
) -> RendererPageBitmapTaskProducer {
RendererPageBitmapTaskProducer {
task_route: self.task_route.clone(),
owner: RendererPageBitmapTaskOwner::new(self.root_document, execution_context, task),
}
}
}
/// Single-use completion capability retained by one blocking bitmap job.
///
/// Consuming `self` makes duplicate delivery impossible without cloning and
/// rebuilding the exact task at registration time.
#[derive(Debug)]
pub(crate) struct RendererPageBitmapTaskProducer {
task_route:
OwnerReadyTaskRoute<ReadyPageTask<RendererPageBitmapTask>, RendererPageTaskReadySignal>,
owner: RendererPageBitmapTaskOwner,
}
impl RendererPageBitmapTaskProducer {
#[cfg(test)]
pub(crate) const fn owner(&self) -> RendererPageBitmapTaskOwner {
self.owner
}
pub(crate) fn send(
self,
result: Result<BitmapTaskResult, BitmapRejection>,
) -> Result<(), RendererPageBitmapTaskRouteClosed> {
self.task_route
.send_and_signal_if_newly_ready(ReadyPageTask::new(RendererPageBitmapTask::new(
self.owner, result,
)))
.map_err(|_| RendererPageBitmapTaskRouteClosed)
}
}
/// Unique Page-lifetime consumer for completed Bitmap operations.
#[derive(Debug)]
pub(crate) struct RendererPageBitmapTaskSource {
source:
OwnerReadyTaskSource<ReadyPageTask<RendererPageBitmapTask>, RendererPageTaskReadySignal>,
}
impl RendererPageBitmapTaskSource {
pub(crate) fn new(owner_wake: RendererOwnerWakeSender) -> Self {
Self {
source: OwnerReadyTaskSource::new(RendererPageTaskReadySignal::new(
owner_wake,
RendererOwnerWakeSource::BitmapTask,
)),
}
}
pub(crate) fn route(&self) -> RendererPageBitmapTaskRoute {
RendererPageBitmapTaskRoute {
task_route: self.source.route(),
}
}
pub(crate) fn next_ready_metadata(&mut self) -> Option<RendererPageTaskReadyMetadata> {
self.source.front().map(ReadyPageTask::metadata)
}
pub(crate) fn next_ready_owner(&mut self) -> Option<RendererPageBitmapTaskOwner> {
self.source.front().map(|ready| ready.value().owner())
}
pub(crate) fn pop_front(
&mut self,
) -> Option<(RendererPageTaskReadyMetadata, RendererPageBitmapTask)> {
self.source.pop_front().map(ReadyPageTask::into_parts)
}
pub(crate) fn has_ready_task(&mut self) -> bool {
!self.source.is_empty()
}
pub(crate) fn clear(&mut self) {
self.source.clear_local();
}
pub(crate) fn route_matches(&self, route: &RendererPageBitmapTaskRoute) -> bool {
route.same_route_as(self)
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) enum PageBitmapTaskTargetEffect {
SettledCurrentOwner,
IgnoredStaleOwner {
current_owner: Option<RendererPageBitmapTaskOwner>,
},
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) struct PageBitmapTaskTurnAction {
pub(crate) owner: RendererPageBitmapTaskOwner,
pub(crate) target_effect: PageBitmapTaskTargetEffect,
}
impl PageBitmapTaskTurnAction {
/// Whether the exact pending Promise was settled in its relevant realm.
///
/// This reports the domain effect only. The selected-task dispatcher
/// decides which task-end checkpoint that effect requires.
pub(crate) const fn settled_current_owner(self) -> bool {
matches!(
self.target_effect,
PageBitmapTaskTargetEffect::SettledCurrentOwner
)
}
}
pub(crate) type PageBitmapTaskTurnOutcome = PageOwnerTurnOutcome<PageBitmapTaskTurnAction>;
@@ -17,6 +17,10 @@ use std::{cell::RefCell, rc::Rc};
use super::{
PageRuntimeWakeSignal, RendererOwnerWakeSender, RendererOwnerWakeSource,
bitmap_task::{
RendererPageBitmapTask, RendererPageBitmapTaskOwner, RendererPageBitmapTaskRoute,
RendererPageBitmapTaskSender, RendererPageBitmapTaskSource,
},
child_frame_task::{
RendererPageChildFrameTask, RendererPageChildFrameTaskOwner,
RendererPageChildFrameTaskRoute, RendererPageChildFrameTaskSender,
@@ -177,6 +181,7 @@ pub(crate) struct RendererPageOwnedTaskSources {
shared_worker_client_event: RendererPageSharedWorkerClientEventSource,
service_worker_internal: RendererPageServiceWorkerInternalSource,
service_worker_client_message: RendererPageServiceWorkerClientMessageSource,
bitmap_task: RendererPageBitmapTaskSource,
webcrypto_task: RendererPageWebCryptoTaskSource,
indexed_db_task: RendererPageIndexedDbTaskSource,
opfs_task: RendererPageOpfsTaskSource,
@@ -213,6 +218,7 @@ pub(crate) struct RendererPageTaskProducerRoutes {
shared_worker_client_event: RendererPageSharedWorkerClientEventRoute,
service_worker_internal: RendererPageServiceWorkerInternalRoute,
service_worker_client_message: RendererPageServiceWorkerClientMessageRoute,
bitmap_task: RendererPageBitmapTaskRoute,
webcrypto_task: RendererPageWebCryptoTaskRoute,
indexed_db_task: RendererPageIndexedDbTaskRoute,
opfs_task: RendererPageOpfsTaskRoute,
@@ -293,6 +299,10 @@ pub(crate) enum RendererPageReadyDescriptor {
ready: RendererPageTaskReadyMetadata,
owner: RendererPageServiceWorkerClientMessageOwner,
},
BitmapTask {
ready: RendererPageTaskReadyMetadata,
owner: RendererPageBitmapTaskOwner,
},
WebCryptoTask {
ready: RendererPageTaskReadyMetadata,
owner: RendererPageWebCryptoTaskOwner,
@@ -382,6 +392,7 @@ impl RendererPageReadyDescriptor {
| Self::SharedWorkerClientEvent { ready, .. }
| Self::ServiceWorkerInternal { ready, .. }
| Self::ServiceWorkerClientMessage { ready, .. }
| Self::BitmapTask { ready, .. }
| Self::WebCryptoTask { ready, .. }
| Self::IndexedDbTask { ready, .. }
| Self::OpfsTask { ready, .. }
@@ -424,6 +435,7 @@ impl RendererPageReadyDescriptor {
Self::ServiceWorkerClientMessage { .. } => {
RendererPageTaskSourceKind::ServiceWorkerClientMessage
}
Self::BitmapTask { .. } => RendererPageTaskSourceKind::BitmapTask,
Self::WebCryptoTask { .. } => RendererPageTaskSourceKind::WebCryptoTask,
Self::IndexedDbTask { .. } => RendererPageTaskSourceKind::IndexedDbTask,
Self::OpfsTask { .. } => RendererPageTaskSourceKind::OpfsTask,
@@ -468,6 +480,7 @@ impl RendererPageReadyDescriptor {
| Self::SharedWorkerClientEvent { ready, .. }
| Self::ServiceWorkerInternal { ready, .. }
| Self::ServiceWorkerClientMessage { ready, .. }
| Self::BitmapTask { ready, .. }
| Self::WebCryptoTask { ready, .. }
| Self::IndexedDbTask { ready, .. }
| Self::OpfsTask { ready, .. }
@@ -512,6 +525,7 @@ pub(crate) enum RendererPageTaskSourceKind {
SharedWorkerClientEvent,
ServiceWorkerInternal,
ServiceWorkerClientMessage,
BitmapTask,
WebCryptoTask,
IndexedDbTask,
OpfsTask,
@@ -531,7 +545,7 @@ pub(crate) enum RendererPageTaskSourceKind {
}
impl RendererPageTaskSourceKind {
pub(crate) const ALL: [Self; 29] = [
pub(crate) const ALL: [Self; 30] = [
Self::ActionWindow,
Self::Timer,
Self::DomManipulation,
@@ -545,6 +559,7 @@ impl RendererPageTaskSourceKind {
Self::SharedWorkerClientEvent,
Self::ServiceWorkerInternal,
Self::ServiceWorkerClientMessage,
Self::BitmapTask,
Self::WebCryptoTask,
Self::IndexedDbTask,
Self::OpfsTask,
@@ -584,6 +599,7 @@ pub(crate) enum RendererPageSchedulerTask {
SharedWorkerClientEvent(RendererPageSharedWorkerClientEventTask),
ServiceWorkerInternal(RendererPageServiceWorkerInternalTask),
ServiceWorkerClientMessage(RendererPageServiceWorkerClientMessageTask),
BitmapTask(RendererPageBitmapTask),
WebCryptoTask(RendererPageWebCryptoTask),
IndexedDbTask(RendererPageIndexedDbTask),
OpfsTask(RendererPageOpfsTask),
@@ -646,6 +662,7 @@ impl RendererPageOwnedTaskSources {
RendererPageServiceWorkerInternalSource::new(owner_wake.clone());
let service_worker_client_message =
RendererPageServiceWorkerClientMessageSource::new(owner_wake.clone());
let bitmap_task = RendererPageBitmapTaskSource::new(owner_wake.clone());
let webcrypto_task = RendererPageWebCryptoTaskSource::new(owner_wake.clone());
let indexed_db_task = RendererPageIndexedDbTaskSource::new(owner_wake.clone());
let opfs_task = RendererPageOpfsTaskSource::new(owner_wake.clone());
@@ -680,6 +697,7 @@ impl RendererPageOwnedTaskSources {
shared_worker_client_event: shared_worker_client_event.route(),
service_worker_internal: service_worker_internal.route(),
service_worker_client_message: service_worker_client_message.route(),
bitmap_task: bitmap_task.route(),
webcrypto_task: webcrypto_task.route(),
indexed_db_task: indexed_db_task.route(),
opfs_task: opfs_task.route(),
@@ -711,6 +729,7 @@ impl RendererPageOwnedTaskSources {
shared_worker_client_event,
service_worker_internal,
service_worker_client_message,
bitmap_task,
webcrypto_task,
indexed_db_task,
opfs_task,
@@ -764,6 +783,7 @@ impl RendererPageOwnedTaskSources {
&& self
.service_worker_client_message
.route_matches(&routes.service_worker_client_message)
&& self.bitmap_task.route_matches(&routes.bitmap_task)
&& self.webcrypto_task.route_matches(&routes.webcrypto_task)
&& self.indexed_db_task.route_matches(&routes.indexed_db_task)
&& self.opfs_task.route_matches(&routes.opfs_task)
@@ -832,6 +852,7 @@ impl RendererPageOwnedTaskSources {
shared_worker_client_event: self.shared_worker_client_event.route(),
service_worker_internal: self.service_worker_internal.route(),
service_worker_client_message: self.service_worker_client_message.route(),
bitmap_task: self.bitmap_task.route(),
webcrypto_task: self.webcrypto_task.route(),
indexed_db_task: self.indexed_db_task.route(),
opfs_task: self.opfs_task.route(),
@@ -942,6 +963,15 @@ impl RendererPageOwnedTaskSources {
.expect("ready SharedWorker client event must retain its exact owner"),
},
);
let bitmap_task = self.bitmap_task.next_ready_metadata().map(|ready| {
RendererPageReadyDescriptor::BitmapTask {
ready,
owner: self
.bitmap_task
.next_ready_owner()
.expect("ready Bitmap task must retain its exact owner"),
}
});
let webcrypto_task = self.webcrypto_task.next_ready_metadata().map(|ready| {
RendererPageReadyDescriptor::WebCryptoTask {
ready,
@@ -1145,6 +1175,7 @@ impl RendererPageOwnedTaskSources {
shared_worker_client_event,
service_worker_internal,
service_worker_client_message,
bitmap_task,
webcrypto_task,
indexed_db_task,
opfs_task,
@@ -1297,6 +1328,14 @@ impl RendererPageOwnedTaskSources {
);
RendererPageSchedulerTask::ServiceWorkerClientMessage(task)
}
RendererPageReadyDescriptor::BitmapTask { ready, .. } => {
let (actual, task) = self
.bitmap_task
.pop_front()
.expect("selected Bitmap task must remain queued");
assert_eq!(actual, ready, "selected Bitmap head changed before dequeue");
RendererPageSchedulerTask::BitmapTask(task)
}
RendererPageReadyDescriptor::WebCryptoTask { ready, .. } => {
let (actual, task) = self
.webcrypto_task
@@ -1522,6 +1561,7 @@ impl RendererPageOwnedTaskSources {
|| self.shared_worker_client_event.has_ready_task()
|| self.service_worker_internal.has_ready_task()
|| self.service_worker_client_message.has_ready_task()
|| self.bitmap_task.has_ready_task()
|| self.webcrypto_task.has_ready_task()
|| self.indexed_db_task.has_ready_task()
|| self.opfs_task.has_ready_task()
@@ -1566,6 +1606,7 @@ impl RendererPageOwnedTaskSources {
self.shared_worker_client_event.clear();
self.service_worker_internal.clear();
self.service_worker_client_message.clear();
self.bitmap_task.clear();
self.webcrypto_task.clear();
self.indexed_db_task.clear();
self.opfs_task.clear();
@@ -1681,6 +1722,12 @@ impl RendererPageTaskProducerRoutes {
RendererWorkerHostBridgeEventSender::new(self.networking.clone(), root_document)
}
pub(crate) fn bitmap_task_sender(
&self,
root_document: RendererDocumentToken,
) -> RendererPageBitmapTaskSender {
self.bitmap_task.sender(root_document)
}
pub(crate) fn webcrypto_task_sender(
&self,
root_document: RendererDocumentToken,
@@ -9,7 +9,7 @@ use url::Url;
use crate::{
PageId,
context_bootstrap::{IndexedDbTaskId, WebCryptoTaskResult},
context_bootstrap::{BitmapRejection, IndexedDbTaskId, WebCryptoTaskResult},
document_runtime::DomHandle,
frame_owner_model::{
ChildDocumentModuleFetchTarget, DocumentId,
@@ -38,7 +38,8 @@ use crate::{
page_task_queue::{
MainDocumentMetaRefreshNavigationTask, PageOwnedInternalLoadingTask,
RendererDedicatedWorkerClientEvent, RendererDedicatedWorkerMessageEvent, RendererOwnerWake,
RendererPageChildFrameTaskSender, RendererPageChildRealmMaterializationTarget,
RendererPageBitmapTaskId, RendererPageBitmapTaskSender, RendererPageChildFrameTaskSender,
RendererPageChildRealmMaterializationTarget,
RendererPageDedicatedWorkerClientEventProducer, RendererPageHistoryTraversalSender,
RendererPageHistoryTraversalTaskId, RendererPageHistoryTraversalTaskKind,
RendererPageIndexedDbTaskKind, RendererPageIndexedDbTaskSender,
@@ -65,7 +66,8 @@ use crate::{
use super::{
PageRuntimeWakeSignal, RendererOwnerWakeSender, RendererOwnerWakeSource,
RendererPageChildFrameTaskSource, RendererPageChildModuleDependencyFetchStartSender,
RendererPageBitmapTaskSource, RendererPageChildFrameTaskSource,
RendererPageChildModuleDependencyFetchStartSender,
RendererPageChildModuleDependencyFetchStartSource, RendererPageChildModuleScriptTerminalSender,
RendererPageChildModuleScriptTerminalSource, RendererPageChildModulepreloadEventActionSender,
RendererPageChildModulepreloadEventActionSource, RendererPageDedicatedWorkerClientEventSource,
@@ -261,6 +263,14 @@ struct SharedWorkerClientEventLane {
wake_rx: tokio::sync::mpsc::UnboundedReceiver<RendererOwnerWake>,
}
struct BitmapTaskLane {
source: Option<RendererPageBitmapTaskSource>,
initial_sender: RendererPageBitmapTaskSender,
replacement_sender: RendererPageBitmapTaskSender,
execution_context: WindowExecutionContextIdentity,
wake_rx: tokio::sync::mpsc::UnboundedReceiver<RendererOwnerWake>,
}
struct WebCryptoTaskLane {
source: Option<RendererPageWebCryptoTaskSource>,
initial_sender: RendererPageWebCryptoTaskSender,
@@ -729,6 +739,58 @@ impl TypedPageSourceConformance for IndexedDbTaskLane {
}
}
impl BitmapTaskLane {
fn new() -> Self {
let (wake_tx, wake_rx) = tokio::sync::mpsc::unbounded_channel();
let source = RendererPageBitmapTaskSource::new(RendererOwnerWakeSender::new(
wake_tx,
RendererPageToken::new_for_testing(document_token(1).page_id),
));
let route = source.route();
let initial_sender = route.sender(document_token(1));
let replacement_sender = route.sender(document_token(2));
Self {
source: Some(source),
initial_sender,
replacement_sender,
execution_context: window_execution_context(76),
wake_rx,
}
}
fn enqueue_with(
sender: &RendererPageBitmapTaskSender,
execution_context: WindowExecutionContextIdentity,
sequence: u64,
) -> bool {
let producer = sender.bind_task(execution_context, RendererPageBitmapTaskId::new(sequence));
assert_eq!(producer.owner().task().task_id(), sequence);
producer.send(Err(BitmapRejection::InvalidState)).is_ok()
}
}
impl TypedPageSourceConformance for BitmapTaskLane {
fn enqueue_initial(&mut self, sequence: u64) -> bool {
Self::enqueue_with(&self.initial_sender, self.execution_context, sequence)
}
fn enqueue_replacement(&mut self, sequence: u64) -> bool {
Self::enqueue_with(&self.replacement_sender, self.execution_context, sequence)
}
fn pop_ready_metadata(&mut self) -> Option<RendererPageTaskReadyMetadata> {
self.source.as_mut()?.pop_front().map(|(ready, _)| ready)
}
fn take_wake(&mut self) -> Option<RendererOwnerWake> {
self.wake_rx.try_recv().ok()
}
fn retire_consumer(&mut self) {
drop(self.source.take());
}
}
impl WebCryptoTaskLane {
fn new() -> Self {
let (wake_tx, wake_rx) = tokio::sync::mpsc::unbounded_channel();
@@ -2206,6 +2268,11 @@ fn shared_worker_client_event_source_conforms_to_page_queue_contract() {
assert_fifo_replacement_and_route_retirement(SharedWorkerClientEventLane::new());
}
#[test]
fn bitmap_task_source_conforms_to_page_queue_contract() {
assert_fifo_replacement_and_route_retirement(BitmapTaskLane::new());
}
#[test]
fn webcrypto_task_source_conforms_to_page_queue_contract() {
assert_fifo_replacement_and_route_retirement(WebCryptoTaskLane::new());
@@ -2438,6 +2505,14 @@ fn unified_ready_descriptors_expose_one_fifo_head_per_typed_source() {
.bind_client(allocated_shared_worker_client_id())
.send(SharedWorkerClientEvent::Closed)
.expect("SharedWorker client event should enter the unified source set");
routes
.bitmap_task_sender(document_token(1))
.bind_task(
window_execution_context(88),
RendererPageBitmapTaskId::new(13),
)
.send(Err(BitmapRejection::InvalidState))
.expect("Bitmap task should enter the unified source set");
routes
.webcrypto_task_sender(document_token(1))
.bind_task(
@@ -2536,7 +2611,7 @@ fn unified_ready_descriptors_expose_one_fifo_head_per_typed_source() {
.expect("dynamic-import action should enter the unified source set");
let descriptors = sources.ready_descriptors();
assert_eq!(descriptors.len(), 18);
assert_eq!(descriptors.len(), 19);
assert!(descriptors.iter().any(|descriptor| matches!(
descriptor,
RendererPageReadyDescriptor::DomManipulation { .. }
@@ -2557,6 +2632,11 @@ fn unified_ready_descriptors_expose_one_fifo_head_per_typed_source() {
descriptor,
RendererPageReadyDescriptor::SharedWorkerClientEvent { .. }
)));
assert!(
descriptors
.iter()
.any(|descriptor| matches!(descriptor, RendererPageReadyDescriptor::BitmapTask { .. }))
);
assert!(descriptors.iter().any(|descriptor| matches!(
descriptor,
RendererPageReadyDescriptor::WebCryptoTask { .. }
@@ -2626,7 +2706,7 @@ fn unified_ready_descriptors_expose_one_fifo_head_per_typed_source() {
));
assert_eq!(
sources.ready_descriptors().len(),
17,
18,
"one Page turn must remove only the selected source head"
);
}
@@ -200,6 +200,7 @@ pub(crate) enum RendererOwnerWakeSource {
ServiceWorkerClientMessage,
/// A completed WebCrypto operation entered its exact Page/Window task
/// source. The queued task, not this admission hint, owns identity.
BitmapTask,
WebCryptoTask,
/// A Page-side IndexedDB task entered its exact Window-realm source.
IndexedDbTask,
@@ -17,6 +17,7 @@ use super::{
resource_completions::RendererOwnerWakeSender,
};
use crate::page_resource_completion::RendererPageResourceCompletionSender;
use crate::page_task_queue::RendererPageBitmapTaskSender;
use crate::page_task_queue::RendererPageChildFrameTaskSender;
use crate::page_task_queue::RendererPageChildModuleDependencyFetchStartSender;
use crate::page_task_queue::RendererPageChildModuleScriptTerminalSender;
@@ -80,6 +81,7 @@ pub(crate) struct RendererPageJsContextTaskSenders {
dedicated_worker_client_event: RendererPageDedicatedWorkerClientEventSender,
shared_worker_client_event: RendererPageSharedWorkerClientEventSender,
worker_host_bridge: RendererWorkerHostBridgeEventSender,
bitmap_task: RendererPageBitmapTaskSender,
webcrypto_task: RendererPageWebCryptoTaskSender,
indexed_db_task: RendererPageIndexedDbTaskSender,
opfs_task: RendererPageOpfsTaskSender,
@@ -173,6 +175,9 @@ impl RendererPageJsContextTaskSenders {
&self.worker_host_bridge
}
pub(crate) fn bitmap_task(&self) -> &RendererPageBitmapTaskSender {
&self.bitmap_task
}
pub(crate) fn webcrypto_task(&self) -> &RendererPageWebCryptoTaskSender {
&self.webcrypto_task
}
@@ -346,6 +351,7 @@ impl PageRuntimeTaskSource {
.dedicated_worker_client_event_sender(root_document),
shared_worker_client_event: routes.shared_worker_client_event_sender(root_document),
worker_host_bridge: routes.worker_host_bridge_event_sender(root_document),
bitmap_task: routes.bitmap_task_sender(root_document),
webcrypto_task: routes.webcrypto_task_sender(root_document),
indexed_db_task: routes.indexed_db_task_sender(root_document),
opfs_task: routes.opfs_task_sender(root_document),
+1
View File
@@ -93,6 +93,7 @@ pub(crate) use self::page_turn_scheduler::{
PageOwnerBlockedReason, PageOwnerTurnOutcome, PageOwnerTurnReadiness,
};
pub(crate) use self::page_vm::AuthorizedCurrentBroadcastChannelDelivery;
pub(crate) use self::page_vm::AuthorizedCurrentPageBitmapTask;
pub(crate) use self::page_vm::AuthorizedCurrentPageChildClassicScriptSourceLoad;
pub(crate) use self::page_vm::AuthorizedCurrentPageChildDocumentLifecycle;
pub(crate) use self::page_vm::AuthorizedCurrentPageChildDocumentScriptReady;
+1
View File
@@ -1067,6 +1067,7 @@ fn page_turn_trigger_log_label(trigger: PageTurnTrigger) -> &'static str {
Some(RendererOwnerWakeSource::ServiceWorkerClientMessage) => {
"service-worker-client-message-wake"
}
Some(RendererOwnerWakeSource::BitmapTask) => "bitmap-task-wake",
Some(RendererOwnerWakeSource::WebCryptoTask) => "webcrypto-task-wake",
Some(RendererOwnerWakeSource::IndexedDbTask) => "indexed-db-task-wake",
Some(RendererOwnerWakeSource::OpfsTask) => "opfs-task-wake",
+1
View File
@@ -1612,6 +1612,7 @@ impl PageVm {
let pending_runtime_work = self
.vm_mut()
.has_post_domcontentloaded_runtime_work_for_wait()
|| self.vm().has_pending_bitmap_tasks()
|| self.vm().has_pending_webcrypto_tasks()
|| self.vm().has_pending_opfs_tasks();
state.saw_post_domcontentloaded_runtime_work |= pending_runtime_work;
@@ -482,6 +482,7 @@ mod tests {
},
page_resource_completion::RendererPageResourceCompletionOwner,
page_task_queue::{
RendererPageBitmapTaskId, RendererPageBitmapTaskOwner,
RendererPageBroadcastChannelDeliveryOwner, RendererPageChildFrameTaskOwner,
RendererPageChildFrameTaskTarget, RendererPageChildModuleDependencyFetchStartOwner,
RendererPageChildModuleScriptTerminalOwner,
@@ -762,6 +763,22 @@ mod tests {
}
}
fn bitmap_descriptor(ready_at: Instant, order: u64) -> RendererPageReadyDescriptor {
RendererPageReadyDescriptor::BitmapTask {
ready: ready_metadata(ready_at, order),
owner: RendererPageBitmapTaskOwner::new(
RendererDocumentToken::new_for_testing(crate::PageId::new_for_testing(1), 1),
WindowExecutionContextIdentity::new(
WindowExecutionContextOwner::Frame(LocalWindowId(7)),
OwnerDispatchScope::Top,
RuntimeObservableContextToken::from_raw(11),
WindowExecutionContextAccessPolicy::EnforceWebOrigin,
),
RendererPageBitmapTaskId::new(order),
),
}
}
fn webcrypto_descriptor(ready_at: Instant, order: u64) -> RendererPageReadyDescriptor {
RendererPageReadyDescriptor::WebCryptoTask {
ready: ready_metadata(ready_at, order),
@@ -1045,6 +1062,7 @@ mod tests {
RendererPageTaskSourceKind::ServiceWorkerClientMessage => {
service_worker_client_message_descriptor(runnable_since, order)
}
RendererPageTaskSourceKind::BitmapTask => bitmap_descriptor(runnable_since, order),
RendererPageTaskSourceKind::WebCryptoTask => {
webcrypto_descriptor(runnable_since, order)
}
@@ -172,6 +172,7 @@ pub(crate) use page_selected_task_test_harness::{
ClaimedPageSelectedTaskForTest, PageSelectedTaskTestSelector,
};
pub(crate) use page_task_completion::{IntoPageTaskCompletion, PageTaskCompletion};
mod page_bitmap_task;
mod page_text_track_default_mode;
mod page_text_track_default_mode_task_completion;
mod page_text_track_load;
@@ -197,6 +198,7 @@ mod parser_owned_module_completion;
mod parser_task_completion;
mod selected_page_task;
pub(crate) use page_bitmap_task::AuthorizedCurrentPageBitmapTask;
pub(crate) use page_broadcast_channel_delivery::AuthorizedCurrentBroadcastChannelDelivery;
pub(crate) use page_child_frame_task::{
AuthorizedCurrentPageChildClassicScriptSourceLoad, AuthorizedCurrentPageChildDocumentLifecycle,
@@ -0,0 +1,68 @@
use crate::page_task_queue::{
PageBitmapTaskTargetEffect, PageBitmapTaskTurnAction, PageBitmapTaskTurnOutcome,
RendererPageBitmapTask, RendererPageBitmapTaskOwner,
};
use super::PageVm;
/// Proof that the Page arbiter matched a selected completion against the exact
/// root PageVm, Window realm, and pending Promise entry.
pub(crate) struct AuthorizedCurrentPageBitmapTask(RendererPageBitmapTask);
impl AuthorizedCurrentPageBitmapTask {
fn new(task: RendererPageBitmapTask) -> Self {
Self(task)
}
pub(crate) fn into_task(self) -> RendererPageBitmapTask {
self.0
}
}
impl PageVm {
fn current_page_bitmap_task_owner(
&self,
expected: RendererPageBitmapTaskOwner,
) -> Option<RendererPageBitmapTaskOwner> {
let execution_context = self
.vm()
.current_pending_bitmap_task_execution_context(expected.task())?;
Some(RendererPageBitmapTaskOwner::new(
self.document_lifecycle.identity().document,
execution_context,
expected.task(),
))
}
pub(in crate::runtime) fn apply_selected_page_bitmap_task_turn(
&mut self,
task: RendererPageBitmapTask,
) -> anyhow::Result<PageBitmapTaskTurnOutcome> {
let owner = task.owner();
let current_owner = self.current_page_bitmap_task_owner(owner);
let target_effect = if current_owner == Some(owner) {
self.vm_mut()
.apply_current_bitmap_task_body(AuthorizedCurrentPageBitmapTask::new(task))?;
PageBitmapTaskTargetEffect::SettledCurrentOwner
} else {
// A root mismatch means `task_id` belongs to another PageVm
// namespace and must not be used to touch this PageVm's pending
// map. Within the same root, exact cleanup is safe and prevents a
// retired realm from retaining its resolver.
if owner.root_document() == self.document_lifecycle.identity().document {
self.vm_mut().discard_stale_bitmap_task(owner);
}
tracing::debug!(
?owner,
?current_owner,
"ignored stale exact-owner Bitmap task"
);
PageBitmapTaskTargetEffect::IgnoredStaleOwner { current_owner }
};
let action = PageBitmapTaskTurnAction {
owner,
target_effect,
};
Ok(PageBitmapTaskTurnOutcome::new(action))
}
}
@@ -76,6 +76,7 @@ pub(crate) enum PageSelectedTaskTestSelector {
StylesheetCompletion,
TextTrackNetworking,
UserInteraction,
BitmapTask,
WebCryptoTask,
WebSocket,
WindowMessage,
@@ -281,6 +282,9 @@ impl PageSelectedTaskTestSelector {
RendererPageReadyDescriptor::MiscPlatformApi { .. }
)
}
Self::BitmapTask => {
matches!(descriptor, RendererPageReadyDescriptor::BitmapTask { .. })
}
Self::WebCryptoTask => matches!(
descriptor,
RendererPageReadyDescriptor::WebCryptoTask { .. }
@@ -406,6 +410,7 @@ impl PageSelectedTaskTestSelector {
RendererPageSchedulerTask::SharedWorkerClientEvent(_),
)
| (Self::UserInteraction, RendererPageSchedulerTask::UserInteraction(_))
| (Self::BitmapTask, RendererPageSchedulerTask::BitmapTask(_))
| (Self::WebCryptoTask, RendererPageSchedulerTask::WebCryptoTask(_))
| (Self::WindowMessage, RendererPageSchedulerTask::WindowMessage(_)) => true,
(Self::WebSocket, RendererPageSchedulerTask::WebSocket(_)) => true,
@@ -83,6 +83,7 @@ impl PageVm {
| RendererPageReadyDescriptor::SharedWorkerClientEvent { .. }
| RendererPageReadyDescriptor::ServiceWorkerInternal { .. }
| RendererPageReadyDescriptor::ServiceWorkerClientMessage { .. }
| RendererPageReadyDescriptor::BitmapTask { .. }
| RendererPageReadyDescriptor::WebCryptoTask { .. }
| RendererPageReadyDescriptor::IndexedDbTask { .. }
| RendererPageReadyDescriptor::OpfsTask { .. }
@@ -142,6 +142,13 @@ impl PageVm {
.await?;
Ok(())
}
RendererPageSchedulerTask::BitmapTask(task) => {
let outcome = self.apply_selected_page_bitmap_task_turn(task)?;
if outcome.action.settled_current_owner() {
self.finish_selected_page_task_checkpoint()?;
}
Ok(())
}
RendererPageSchedulerTask::WebCryptoTask(task) => {
let outcome = self.apply_selected_page_webcrypto_task_turn(task)?;
if outcome.action.settled_current_owner() {
@@ -0,0 +1,431 @@
use super::*;
#[tokio::test(flavor = "current_thread")]
async fn bitmap_argument_errors_reject_promises_without_queuing_decode_tasks() {
run_page_vm_async_test(async move {
let mut page_vm = test_page_vm();
page_vm.vm_mut().eval(r#"
globalThis.outcomes = [];
globalThis.getters = [];
const source = new OffscreenCanvas(2, 3);
const marker = {};
const probes = [
() => createImageBitmap(),
() => createImageBitmap({width: 2, height: 3}),
() => createImageBitmap(source, 0, 0),
() => createImageBitmap(source, 0, 0, 0, 1),
() => createImageBitmap(source, {resizeWidth: 0}),
() => createImageBitmap(source, {resizeHeight: Infinity}),
() => createImageBitmap(source, {resizeWidth: -1}),
() => createImageBitmap(source, {imageOrientation: 'invalid'}),
() => createImageBitmap(new OffscreenCanvas(0, 3)),
() => createImageBitmap(source, {
get colorSpaceConversion() { getters.push('colorSpaceConversion'); },
get imageOrientation() { getters.push('imageOrientation'); },
get premultiplyAlpha() { getters.push('premultiplyAlpha'); throw marker; },
get resizeHeight() { getters.push('unexpected'); }
}),
];
for (const probe of probes) {
const promise = probe();
if (!(promise instanceof Promise)) throw new Error('missing Promise');
promise.then(() => outcomes.push('unexpected success'), error => outcomes.push(error === marker ? 'marker' : error.name));
}
'scheduled'
"#)?;
assert!(!page_vm.vm().has_pending_bitmap_tasks());
assert_eq!(page_vm.vm_mut().eval("JSON.stringify(outcomes)")?, r#"["TypeError","TypeError","TypeError","RangeError","InvalidStateError","TypeError","TypeError","TypeError","InvalidStateError","marker"]"#);
assert_eq!(page_vm.vm_mut().eval("JSON.stringify(getters)")?, r#"["colorSpaceConversion","imageOrientation","premultiplyAlpha"]"#);
Ok::<_, anyhow::Error>(())
}).await.expect("bitmap Web IDL rejections");
}
async fn run_ready_bitmap_task(page_vm: &mut PageVm) -> anyhow::Result<()> {
let loader = page_vm.request_client.clone();
let deadline = Instant::now() + Duration::from_secs(5);
while !page_vm
.run_exact_selected_page_task_for_test(PageSelectedTaskTestSelector::BitmapTask, &loader)
.await?
{
anyhow::ensure!(
Instant::now() < deadline,
"bitmap decode did not enqueue its result"
);
tokio::time::sleep(Duration::from_millis(1)).await;
}
Ok(())
}
#[tokio::test(flavor = "current_thread")]
async fn bitmap_blob_decodes_bytes_and_settles_in_a_later_task_in_the_intrinsic_realm() {
run_page_vm_async_test(async move {
let mut page_vm = test_page_vm();
let pixels = moli_image::RgbaImage::try_new(1, 2, vec![255, 0, 0, 255, 0, 255, 0, 255])?;
let encoded = moli_image::encode_png(&pixels)?.bytes;
page_vm.vm_mut().eval(&format!("globalThis.pngBytes = new Uint8Array({encoded:?}); 'ready'"))?;
page_vm.vm_mut().eval(r#"
globalThis.outcomes = [];
const prototype = ImageBitmap.prototype;
const valid = createImageBitmap(new Blob([pngBytes], {type: 'text/plain'}), {imageOrientation: 'flipY'});
const invalid = createImageBitmap(new Blob(['not an image'], {type: 'image/png'}));
const empty = createImageBitmap(new Blob());
globalThis.ImageBitmap = function ReplacedImageBitmap() {};
pngBytes.fill(0);
valid.then(bitmap => {
const canvas = new OffscreenCanvas(1, 2);
const ctx = canvas.getContext('2d');
ctx.drawImage(bitmap, 0, 0);
outcomes.push([Object.getPrototypeOf(bitmap) === prototype, bitmap.width, bitmap.height, Array.from(ctx.getImageData(0, 0, 1, 2).data)]);
}, error => outcomes.push(error.name));
invalid.catch(error => outcomes.push('invalid:' + error.name));
empty.catch(error => outcomes.push('empty:' + error.name));
Promise.resolve().then(() => outcomes.push('microtask'));
'scheduled'
"#)?;
assert_eq!(page_vm.vm_mut().eval("JSON.stringify(outcomes)")?, r#"["microtask"]"#);
for _ in 0..3 { run_ready_bitmap_task(&mut page_vm).await?; }
assert!(!page_vm.vm().has_pending_bitmap_tasks());
assert_eq!(page_vm.vm_mut().eval("JSON.stringify(outcomes.filter(Array.isArray))")?, "[[true,1,2,[0,255,0,255,255,0,0,255]]]");
assert_eq!(page_vm.vm_mut().eval("JSON.stringify(outcomes.filter(value => typeof value === 'string').sort())")?, r#"["empty:InvalidStateError","invalid:InvalidStateError","microtask"]"#);
Ok::<_, anyhow::Error>(())
}).await.expect("bitmap Blob decode and Promise task timing");
}
#[tokio::test(flavor = "current_thread")]
async fn bitmap_snapshots_pixels_crops_resizes_and_rejects_closed_sources() {
run_page_vm_async_test(async move {
let mut page_vm = test_page_vm();
page_vm.vm_mut().eval(r#"
globalThis.results = [];
const source = new OffscreenCanvas(2, 1);
const ctx = source.getContext('2d');
ctx.fillStyle = 'red'; ctx.fillRect(0, 0, 2, 1);
const pending = createImageBitmap(source, 2, 0, -2, 1, {resizeWidth: 4, resizeQuality: 'pixelated'});
ctx.fillStyle = 'blue'; ctx.fillRect(0, 0, 2, 1);
pending.then(async bitmap => {
const dest = new OffscreenCanvas(4, 2);
const destCtx = dest.getContext('2d');
destCtx.drawImage(bitmap, 0, 0);
results.push([bitmap.width, bitmap.height, Array.from(destCtx.getImageData(0, 0, 1, 1).data)]);
const copyPromise = createImageBitmap(bitmap);
bitmap.close(); bitmap.close();
try { destCtx.drawImage(bitmap, 0, 0); } catch(error) { results.push(error.name); }
await createImageBitmap(bitmap).catch(error => results.push(error.name));
const copy = await copyPromise;
results.push([copy.width, copy.height]);
});
const data = new ImageData(new Uint8ClampedArray([0, 255, 0, 255]), 1, 1);
createImageBitmap(data, -1, 0, 2, 1, {resizeQuality: 'pixelated'}).then(bitmap => {
const dest = new OffscreenCanvas(2, 1);
const ctx = dest.getContext('2d');
ctx.drawImage(bitmap, 0, 0);
globalThis.cropPixels = Array.from(ctx.getImageData(0, 0, 2, 1).data);
});
data.data.fill(0);
const htmlCanvas = document.createElement('canvas');
htmlCanvas.width = 5; htmlCanvas.height = 3;
Object.defineProperty(htmlCanvas, 'width', {get() { throw new Error('observable width getter'); }});
createImageBitmap(htmlCanvas).then(bitmap => globalThis.htmlDimensions = [bitmap.width, bitmap.height]);
'scheduled'
"#)?;
for _ in 0..4 { run_ready_bitmap_task(&mut page_vm).await?; }
assert_eq!(page_vm.vm_mut().eval("JSON.stringify(results)")?, r#"[[4,2,[255,0,0,255]],"InvalidStateError","InvalidStateError",[4,2]]"#);
assert_eq!(page_vm.vm_mut().eval("JSON.stringify(cropPixels)")?, "[0,0,0,0,0,255,0,255]");
assert_eq!(page_vm.vm_mut().eval("JSON.stringify(htmlDimensions)")?, "[5,3]");
Ok::<_, anyhow::Error>(())
}).await.expect("bitmap snapshots and close");
}
#[tokio::test(flavor = "current_thread")]
async fn bitmap_task_survives_document_open_in_the_same_window() {
run_page_vm_async_test(async move {
let mut page_vm = test_page_vm();
page_vm.vm_mut().eval(r#"
globalThis.outcome = 'pending';
createImageBitmap(new OffscreenCanvas(3, 2)).then(bitmap => outcome = [bitmap.width, bitmap.height]);
document.open();
'replaced'
"#)?;
assert!(page_vm.vm().has_pending_bitmap_tasks());
run_ready_bitmap_task(&mut page_vm).await?;
assert_eq!(page_vm.vm_mut().eval("JSON.stringify(outcome)")?, "[3,2]");
assert!(!page_vm.vm().has_pending_bitmap_tasks());
Ok::<_, anyhow::Error>(())
}).await.expect("bitmap Window lifetime");
}
#[tokio::test(flavor = "current_thread")]
async fn bitmap_task_uses_child_intrinsics_and_retires_when_the_child_window_is_removed() {
run_page_vm_async_test(async move {
let mut page_vm = test_page_vm();
let loader = page_vm.request_client.clone();
page_vm.vm_mut().eval(r#"
const frame = document.createElement('iframe');
globalThis.bitmapFrame = frame;
globalThis.bitmapChildOutcome = 'pending';
(document.body || document.documentElement || document).appendChild(frame);
void frame.contentWindow.Function;
const script = frame.contentDocument.createElement('script');
script.textContent = "createImageBitmap(new OffscreenCanvas(3, 4)).then(bitmap => { parent.bitmapChildOutcome = [bitmap.width, bitmap.height, Object.getPrototypeOf(bitmap) === ImageBitmap.prototype]; createImageBitmap(new OffscreenCanvas(1, 1)).then(() => parent.bitmapChildOutcome = 'stale'); });";
frame.contentDocument.body.appendChild(script);
'scheduled'
"#)?;
run_expected_child_realm_materialization_for_wait(&mut page_vm, "child bitmap realm").await;
assert!(page_vm.run_exact_selected_page_task_for_test(PageSelectedTaskTestSelector::ChildDocumentScriptReady, &loader).await?);
run_ready_bitmap_task(&mut page_vm).await?;
assert_eq!(page_vm.vm_mut().eval("JSON.stringify(bitmapChildOutcome)")?, "[3,4,true]");
assert!(page_vm.vm().has_pending_bitmap_tasks());
page_vm.vm_mut().eval("document.open(); 'retired'")?;
assert!(!page_vm.vm().has_pending_bitmap_tasks());
run_ready_bitmap_task(&mut page_vm).await?;
assert_eq!(page_vm.vm_mut().eval("JSON.stringify(bitmapChildOutcome)")?, "[3,4,true]");
Ok::<_, anyhow::Error>(())
}).await.expect("bitmap child realm settlement and retirement");
}
use crate::context_bootstrap::BitmapRejection;
use crate::page_task_queue::{
MainDocumentMetaRefreshNavigationTask, PageInternalLoadingTargetEffect,
PageOwnedInternalLoadingTask,
};
#[tokio::test(flavor = "current_thread")]
async fn create_image_bitmap_offscreen_canvas_and_close_contract() {
run_page_vm_async_test(async move {
let mut page_vm = test_page_vm();
let loader = page_vm.request_client.clone();
page_vm.vm_mut().exec(
r#"
(() => {
const caughtName = callback => {
try {
callback();
return "none";
} catch (error) {
return error.name;
}
};
const descriptor = Object.getOwnPropertyDescriptor(window, "createImageBitmap");
const blank = new OffscreenCanvas(16, 9);
const drawn = new OffscreenCanvas(16, 9);
drawn.getContext("2d").fillRect(0, 0, 1, 1);
globalThis.__imageBitmapProbe = {
functionShape: [
typeof createImageBitmap,
createImageBitmap.name,
createImageBitmap.length,
Object.prototype.hasOwnProperty.call(createImageBitmap, "prototype"),
descriptor.enumerable,
descriptor.configurable,
descriptor.writable,
caughtName(() => new createImageBitmap(drawn)),
caughtName(() => createImageBitmap().catch(() => {})),
],
constructorShape: [
typeof ImageBitmap,
ImageBitmap.name,
ImageBitmap.length,
Object.prototype.toString.call(ImageBitmap.prototype),
Object.getPrototypeOf(ImageBitmap.prototype) === Object.prototype,
caughtName(() => new ImageBitmap()),
],
settled: false,
};
Promise.all([
createImageBitmap(blank).then(
() => "resolved",
error => `rejected:${error.name}`,
),
createImageBitmap(drawn).then(bitmap => {
const before = [
Object.prototype.toString.call(bitmap),
bitmap instanceof ImageBitmap,
Object.getPrototypeOf(bitmap) === ImageBitmap.prototype,
Object.getOwnPropertyNames(bitmap).length,
bitmap.width,
bitmap.height,
typeof bitmap.close,
];
const closeResult = bitmap.close();
return [before, typeof closeResult, bitmap.width, bitmap.height];
}),
]).then(([blankOutcome, bitmapOutcome]) => {
__imageBitmapProbe.blankOutcome = blankOutcome;
__imageBitmapProbe.bitmapOutcome = bitmapOutcome;
__imageBitmapProbe.settled = true;
});
})()
"#,
None,
)
.expect("createImageBitmap probe should execute");
assert_eq!(page_vm.vm_mut().eval("String(__imageBitmapProbe.settled)")?, "false");
for _ in 0..2 {
assert!(page_vm.run_exact_selected_page_task_for_test(PageSelectedTaskTestSelector::BitmapTask, &loader).await?);
}
let result = page_vm.vm_mut()
.eval("JSON.stringify(globalThis.__imageBitmapProbe)")
.expect("createImageBitmap probe should be readable");
assert_eq!(
result,
r#"{"functionShape":["function","createImageBitmap",1,false,true,true,true,"TypeError","none"],"constructorShape":["function","ImageBitmap",0,"[object ImageBitmap]",true,"TypeError"],"settled":true,"blankOutcome":"resolved","bitmapOutcome":[["[object ImageBitmap]",true,true,0,16,9,"function"],"undefined",0,0]}"#
);
Ok::<_, anyhow::Error>(())
}).await.expect("bitmap task surface");
}
#[test]
fn bitmap_task_rejects_a_real_page_vm_replacement_identity_collision() {
run_page_vm_large_stack_async_test(
"bitmap-real-page-vm-replacement-collision",
|| async move {
let (base_url, server) = spawn_path_response_http_server(vec![(
"/replacement.html",
"HTTP/1.1 200 OK",
"<!doctype html><body>replacement</body>".to_owned(),
Duration::ZERO,
)])
.await;
let loader = crate::network::ResourceRequestClient::new(&FetchConfig::default())
.expect("loader");
let document_url = Url::parse(&format!("{base_url}/initial.html")).unwrap();
let (page_vm, _resource_source, _owner_wake_rx) =
page_vm_with_bound_task_sources_and_owner_wake(&loader, document_url);
let local_executor = page_vm.local_executor.clone();
local_executor
.run(async move {
let mut page_vm = page_vm;
let retired_producer = page_vm
.vm_mut()
.register_pending_bitmap_task_producer_for_executor_test()?;
let retired_owner = retired_producer.owner();
assert_eq!(
retired_owner.root_document(),
page_vm.document_lifecycle.identity().document
);
retired_producer
.send(Err(BitmapRejection::InvalidState))
.expect("retired Bitmap task should enter the stable Page source");
let replacement_url = format!("{base_url}/replacement.html");
page_vm
.vm_mut()
.eval(&format!("location.href = {replacement_url:?}; 'queued'"))?;
let mut pending_document_lifecycle_turn = None;
let navigation = page_vm
.follow_pending_location_navigation_one_turn_async(
&mut pending_document_lifecycle_turn,
PageVmInitStage::Load,
)
.await?;
assert!(matches!(
navigation,
crate::runtime::PageVmFollowNavigationTurnOutcome::Completed
| crate::runtime::PageVmFollowNavigationTurnOutcome::PostParseLifecycle {
..
}
));
let current_producer = page_vm
.vm_mut()
.register_pending_bitmap_task_producer_for_executor_test()?;
let current_owner = current_producer.owner();
assert_eq!(
retired_owner.task(),
current_owner.task(),
"fresh PageVm counters should naturally reuse the first Bitmap task id and transport generation"
);
assert_eq!(
retired_owner.execution_context(),
current_owner.execution_context(),
"fresh PageVm counters should naturally reuse the top Window/realm identity"
);
assert_ne!(
retired_owner.root_document(),
current_owner.root_document(),
"the stable Page queue must namespace identical local owners by root Document"
);
assert_eq!(
current_owner.root_document(),
page_vm.document_lifecycle.identity().document
);
current_producer
.send(Err(BitmapRejection::InvalidState))
.expect("replacement Bitmap task should enter the same stable Page source");
let current_document_owner = page_vm
.vm()
.current_main_document_task_owner()
.expect("replacement main Document owner");
page_vm
.vm()
.schedule_page_internal_loading_task(
PageOwnedInternalLoadingTask::MetaRefreshNavigation(
MainDocumentMetaRefreshNavigationTask::new(
current_document_owner,
0,
Url::parse("https://example.test/refresh").unwrap(),
),
),
Instant::now(),
)
.expect("internal-loading task should enter the stable Page source");
park_current_document_websocket_for_test(
&mut page_vm,
moli_websocket::Event::TextMessage {
socket_id: 41,
data: "blocked".to_owned(),
},
)
.await;
assert!(
page_vm
.run_exact_selected_page_task_for_test(PageSelectedTaskTestSelector::BitmapTask, &loader)
.await?,
"retired Bitmap task should remain runnable beside independent internal-loading and backpressured WebSocket work"
);
assert_eq!(
page_vm
.vm()
.current_pending_bitmap_task_execution_context(current_owner.task()),
Some(current_owner.execution_context()),
"discarding the old completion must not remove the colliding replacement Promise"
);
assert!(
page_vm
.run_exact_selected_page_task_for_test(PageSelectedTaskTestSelector::BitmapTask, &loader)
.await?,
"replacement Bitmap task should consume the following turn"
);
assert_eq!(
page_vm
.vm()
.current_pending_bitmap_task_execution_context(current_owner.task()),
None,
"the current completion must settle exactly the replacement Promise"
);
let internal_loading = page_vm
.run_internal_loading_body_for_test()
.expect("the independent internal-loading task should remain queued");
assert_eq!(
internal_loading.action.target_effect,
PageInternalLoadingTargetEffect::AppliedToCurrentOwner {
effect: crate::page_task_queue::PageOwnedInternalLoadingTaskEffect::MetaRefreshNavigationNotActivated,
},
"the synthetic refresh must still enforce its own post-load prerequisite"
);
Ok::<_, anyhow::Error>(())
})
.await
.expect("Bitmap replacement should run through the typed task executor");
server
.await
.expect("Bitmap PageVm replacement server should finish");
},
);
}
@@ -1,3 +1,4 @@
mod bitmap;
use std::{
sync::Arc,
time::{Duration, Instant},
+19 -6
View File
@@ -782,6 +782,7 @@ mod subresource_command_completion;
mod subresource_fetch;
pub(crate) use subresource_command_completion::AsyncSubresourceCommandExecution;
pub(crate) use subresource_fetch::AsyncSubresourceFetchBodyActivity;
mod bitmap_tasks;
mod page_resource_completion_task_completion;
mod text_search;
mod text_track_default_mode;
@@ -3850,6 +3851,8 @@ impl ScriptVm {
let retired_image_decode_count = host.retire_image_decode_requests_for_context_token(
context.runtime_observable_context_token,
);
let retired_bitmap_count =
host.retire_bitmap_context_token(context.runtime_observable_context_token);
let retired_webcrypto_count =
host.retire_webcrypto_context_token(context.runtime_observable_context_token);
host.retire_opfs_context_token(context.runtime_observable_context_token);
@@ -3880,6 +3883,7 @@ impl ScriptVm {
retired_message_port_count,
retired_window_message_count,
retired_window_execution_context_count,
retired_bitmap_count,
retired_webcrypto_count,
retired_worker_count,
retired_shared_worker_count,
@@ -3896,12 +3900,13 @@ impl ScriptVm {
retired_message_port_count = runtime_binding_retirement.2,
retired_window_message_count = runtime_binding_retirement.3,
retired_window_execution_context_count = runtime_binding_retirement.4,
retired_webcrypto_count = runtime_binding_retirement.5,
retired_worker_count = runtime_binding_retirement.6,
retired_shared_worker_count = runtime_binding_retirement.7,
retired_xhr_count = runtime_binding_retirement.8,
aborted_fetch_count = runtime_binding_retirement.9.0,
detached_keepalive_fetch_count = runtime_binding_retirement.9.1,
retired_bitmap_count = runtime_binding_retirement.5,
retired_webcrypto_count = runtime_binding_retirement.6,
retired_worker_count = runtime_binding_retirement.7,
retired_shared_worker_count = runtime_binding_retirement.8,
retired_xhr_count = runtime_binding_retirement.9,
aborted_fetch_count = runtime_binding_retirement.10.0,
detached_keepalive_fetch_count = runtime_binding_retirement.10.1,
retired_timer_count,
"retired child Runtime binding context"
);
@@ -3961,6 +3966,7 @@ impl ScriptVm {
runtime_binding_retirement,
retired_image_decode_count,
retired_message_port_count,
retired_bitmap_count,
retired_webcrypto_count,
retired_worker_count,
retired_shared_worker_count,
@@ -3973,6 +3979,8 @@ impl ScriptVm {
let retired_image_decode_count = host.retire_image_decode_requests_for_context_token(
context.runtime_observable_context_token,
);
let retired_bitmap_count =
host.retire_bitmap_context_token(context.runtime_observable_context_token);
let retired_webcrypto_count =
host.retire_webcrypto_context_token(context.runtime_observable_context_token);
host.retire_opfs_context_token(context.runtime_observable_context_token);
@@ -3995,6 +4003,7 @@ impl ScriptVm {
runtime_binding_retirement,
retired_image_decode_count,
retired_message_port_count,
retired_bitmap_count,
retired_webcrypto_count,
retired_worker_count,
retired_shared_worker_count,
@@ -4023,6 +4032,7 @@ impl ScriptVm {
.retired_execution_context_count(),
retired_image_decode_count,
retired_message_port_count,
retired_bitmap_count,
retired_webcrypto_count,
retired_worker_count,
retired_shared_worker_count,
@@ -7349,6 +7359,9 @@ impl ScriptVm {
.dedicated_worker_running_worker_isolate_count_for_diagnostics()
}
pub(crate) fn has_pending_bitmap_tasks(&self) -> bool {
self._context_host.borrow().has_pending_bitmap_tasks()
}
pub(crate) fn has_pending_webcrypto_tasks(&self) -> bool {
self._context_host.borrow().has_pending_webcrypto_tasks()
}
@@ -0,0 +1,83 @@
use anyhow::Result;
use super::ScriptVm;
use crate::{
context_bootstrap::settle_bitmap_task_result, page_task_queue::RendererPageBitmapTaskOwner,
runtime::AuthorizedCurrentPageBitmapTask,
};
impl ScriptVm {
#[cfg(test)]
pub(crate) fn register_pending_bitmap_task_producer_for_executor_test(
&mut self,
) -> Result<crate::page_task_queue::RendererPageBitmapTaskProducer> {
self.with_default_context_scope_and_checkpoint_for_test(|scope, host_ptr| {
let resolver = v8::PromiseResolver::new(scope)
.expect("Bitmap executor test resolver should exist");
unsafe { &mut *host_ptr }
.register_pending_bitmap_task(scope, resolver)
.ok_or_else(|| {
anyhow::anyhow!("Bitmap executor test must capture the current Window realm")
})
})
}
pub(crate) fn current_pending_bitmap_task_execution_context(
&self,
task: crate::page_task_queue::RendererPageBitmapTaskId,
) -> Option<crate::native_bridge::WindowExecutionContextIdentity> {
self._context_host
.borrow()
.current_pending_bitmap_task_execution_context(task)
}
/// Settle one page-side Bitmap Promise body only after the Page arbiter
/// has authorized its exact PageVm and Window realm.
///
/// The selected Page-task dispatcher owns the enclosing task's microtask
/// checkpoint. This method deliberately leaves reactions queued after
/// resolving or rejecting the Promise.
pub(crate) fn apply_current_bitmap_task_body(
&mut self,
authorization: AuthorizedCurrentPageBitmapTask,
) -> Result<()> {
let task = authorization.into_task();
let owner = task.owner();
let pending = self
._context_host
.borrow_mut()
.take_pending_bitmap_task_for_exact_owner(owner.execution_context(), owner.task())
.ok_or_else(|| {
anyhow::anyhow!("authorized Bitmap task lost its exact pending Promise")
})?;
let (bound_owner, bound_dispatch_scope, _realm_token, context) =
pending.relevant_context.into_parts();
debug_assert_eq!(bound_owner, owner.execution_context().owner());
debug_assert_eq!(
bound_dispatch_scope,
owner.execution_context().dispatch_scope()
);
let context_ptr: *const v8::Global<v8::Context> = &context;
let resolver = pending.resolver;
self.with_context_scope_by_ptr(context_ptr, move |scope, _host_ptr| {
let previous_dispatch_scope = bound_dispatch_scope.enter(scope);
let resolver = v8::Local::new(scope, &resolver);
settle_bitmap_task_result(scope, resolver, task.into_result());
bound_dispatch_scope.defer_restore(scope, previous_dispatch_scope);
tracing::debug!(
task_id = owner.task().task_id(),
execution_context = ?owner.execution_context(),
"settled Bitmap task body in relevant Window execution context"
);
Ok(())
})
}
pub(crate) fn discard_stale_bitmap_task(&mut self, owner: RendererPageBitmapTaskOwner) {
let _ = self
._context_host
.borrow_mut()
.take_pending_bitmap_task_for_exact_owner(owner.execution_context(), owner.task());
}
}
@@ -29,85 +29,6 @@ async fn run_one_canvas_image_load_event_task(
);
}
#[test]
fn create_image_bitmap_matches_chromium_offscreen_canvas_and_close_contract() {
let mut vm = new_storage_test_vm("https://image-bitmap-surface.test/");
vm.exec(
r#"
(() => {
const caughtName = callback => {
try {
callback();
return "none";
} catch (error) {
return error.name;
}
};
const descriptor = Object.getOwnPropertyDescriptor(window, "createImageBitmap");
const blank = new OffscreenCanvas(16, 9);
const drawn = new OffscreenCanvas(16, 9);
drawn.getContext("2d").fillRect(0, 0, 1, 1);
globalThis.__imageBitmapProbe = {
functionShape: [
typeof createImageBitmap,
createImageBitmap.name,
createImageBitmap.length,
Object.prototype.hasOwnProperty.call(createImageBitmap, "prototype"),
descriptor.enumerable,
descriptor.configurable,
descriptor.writable,
caughtName(() => new createImageBitmap(drawn)),
caughtName(() => createImageBitmap()),
],
constructorShape: [
typeof ImageBitmap,
ImageBitmap.name,
ImageBitmap.length,
Object.prototype.toString.call(ImageBitmap.prototype),
Object.getPrototypeOf(ImageBitmap.prototype) === Object.prototype,
caughtName(() => new ImageBitmap()),
],
settled: false,
};
Promise.all([
createImageBitmap(blank).then(
() => "resolved",
error => `rejected:${error.name}`,
),
createImageBitmap(drawn).then(bitmap => {
const before = [
Object.prototype.toString.call(bitmap),
bitmap instanceof ImageBitmap,
Object.getPrototypeOf(bitmap) === ImageBitmap.prototype,
Object.getOwnPropertyNames(bitmap).length,
bitmap.width,
bitmap.height,
typeof bitmap.close,
];
const closeResult = bitmap.close();
return [before, typeof closeResult, bitmap.width, bitmap.height];
}),
]).then(([blankOutcome, bitmapOutcome]) => {
__imageBitmapProbe.blankOutcome = blankOutcome;
__imageBitmapProbe.bitmapOutcome = bitmapOutcome;
__imageBitmapProbe.settled = true;
});
})()
"#,
None,
)
.expect("createImageBitmap probe should execute");
let result = vm
.eval("JSON.stringify(globalThis.__imageBitmapProbe)")
.expect("createImageBitmap probe should be readable");
assert_eq!(
result,
r#"{"functionShape":["function","createImageBitmap",1,false,true,true,true,"TypeError","TypeError"],"constructorShape":["function","ImageBitmap",0,"[object ImageBitmap]",true,"TypeError"],"settled":true,"blankOutcome":"rejected:InvalidStateError","bitmapOutcome":[["[object ImageBitmap]",true,true,0,16,9,"function"],"undefined",0,0]}"#
);
}
#[test]
fn webgl_viewport_state_matches_chromium_on_html_and_offscreen_contexts() {
let mut vm = new_storage_test_vm("https://webgl-viewport.test/");