From 41e608f91cd4ec44cb054a8094598fd44cf42402 Mon Sep 17 00:00:00 2001 From: ldm0 Date: Sat, 17 Oct 2026 21:54:48 +0800 Subject: [PATCH] fix(renderer): load and retire web fonts by child Document owner --- .../css_fontface_runtime/storage.rs | 19 +- .../child_frame_runtime/document.rs | 1 + .../context_host/child_frames/registry.rs | 1 + .../context_host/child_frames/stylesheets.rs | 44 +--- .../src/native_bridge/context_host/layout.rs | 54 +++- .../context_host/layout_state.rs | 128 ++++++++- .../network_host/stylesheet_subresource.rs | 25 +- moli-renderer-v8/src/script_vm/layout.rs | 14 +- .../subresource_fetch/fetch_dispatch.rs | 41 ++- .../tests/child_document_web_fonts.rs | 249 ++++++++++++++++++ moli-renderer-v8/src/script_vm/tests/mod.rs | 1 + moli-renderer-v8/src/script_vm/web_fonts.rs | 21 +- .../src/stylesheet_runtime/settlement.rs | 112 +++++--- 13 files changed, 559 insertions(+), 151 deletions(-) create mode 100644 moli-renderer-v8/src/script_vm/tests/child_document_web_fonts.rs diff --git a/moli-renderer-v8/src/context_bootstrap/css_fontface_runtime/storage.rs b/moli-renderer-v8/src/context_bootstrap/css_fontface_runtime/storage.rs index 18667df6de..ab5e7029c9 100644 --- a/moli-renderer-v8/src/context_bootstrap/css_fontface_runtime/storage.rs +++ b/moli-renderer-v8/src/context_bootstrap/css_fontface_runtime/storage.rs @@ -128,13 +128,16 @@ fn reconcile_document_font_face_set_ready<'s>( else { return; }; - if unsafe { &*host_ptr }.document_handle() != handle { + if unsafe { &*host_ptr } + .owner_dispatch_scope_for_node(handle) + .is_none() + { return; } - crate::document_runtime::DocumentRuntime::reconcile_document_web_fonts_in_current_scope( - scope, host_ptr, + crate::document_runtime::DocumentRuntime::reconcile_web_fonts_for_document_in_current_scope( + scope, host_ptr, handle, ); - if unsafe { &*host_ptr }.document_web_fonts_have_pending_requests() { + if unsafe { &*host_ptr }.document_web_fonts_have_pending_requests(handle) { if get_private_value(scope, fonts, DOCUMENT_FONT_FACE_SET_READY_RESOLVER) .is_some_and(|value| value.is_object()) { @@ -152,20 +155,20 @@ fn reconcile_document_font_face_set_ready<'s>( set_font_face_set_status(scope, fonts, "loading"); } } else { - settle_document_font_face_set_ready(scope, host_ptr); + settle_document_font_face_set_ready(scope, host_ptr, handle); } } pub(crate) fn settle_document_font_face_set_ready( scope: &mut v8::PinScope<'_, '_>, host_ptr: *mut crate::native_bridge::JsContextHost, + document: crate::document_runtime::DomHandle, ) { let host = unsafe { &*host_ptr }; - if host.document_web_fonts_have_pending_requests() { + if host.document_web_fonts_have_pending_requests(document) { return; } - let Some(document) = crate::util::node_wrapper_from_handle(scope, host.document_handle()) - else { + let Some(document) = crate::util::node_wrapper_from_handle(scope, document) else { return; }; let Some(fonts) = document diff --git a/moli-renderer-v8/src/native_bridge/context_host/child_frame_runtime/document.rs b/moli-renderer-v8/src/native_bridge/context_host/child_frame_runtime/document.rs index db551794df..20e2dcd52d 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/child_frame_runtime/document.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/child_frame_runtime/document.rs @@ -704,6 +704,7 @@ impl JsContextHost { let current_owner = transition .current_owner() .expect("committed child document-open replacement must install an owner"); + host.retire_embedded_document_fonts(document_handle); host.replace_document_resource_loader_for_document_open( crate::native_bridge::WindowDocumentOwner::Frame(retired_owner), crate::network::context::DocumentFetchContext::new( diff --git a/moli-renderer-v8/src/native_bridge/context_host/child_frames/registry.rs b/moli-renderer-v8/src/native_bridge/context_host/child_frames/registry.rs index 23b1bc606d..7e912d856c 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/child_frames/registry.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/child_frames/registry.rs @@ -105,6 +105,7 @@ impl JsContextHost { retired_owner: crate::frame_owner_model::FrameDocumentTaskOwner, document_handle: DomHandle, ) { + self.retire_embedded_document_fonts(document_handle); self.retire_web_mcp_document(crate::native_bridge::WindowDocumentOwner::Frame( retired_owner, )); diff --git a/moli-renderer-v8/src/native_bridge/context_host/child_frames/stylesheets.rs b/moli-renderer-v8/src/native_bridge/context_host/child_frames/stylesheets.rs index ed9e0dc75f..94e8067a36 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/child_frames/stylesheets.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/child_frames/stylesheets.rs @@ -285,45 +285,11 @@ impl JsContextHost { ); return; } - for (binding, resource) in stylesheet_subresources { - let request_url = resource.request_url().clone(); - let kind = resource.kind(); - let css_image = if kind - == crate::css_resource_urls::StylesheetLoadBlockingResourceKind::Image - && self.layout_policy().uses_real_layout() - { - match self.admit_stylesheet_css_image(binding, request_url.as_str().to_owned()) { - crate::native_bridge::CssImageResourceAdmission::Fetch(identity) => { - Some(identity) - } - crate::native_bridge::CssImageResourceAdmission::Reused => { - self.settle_stylesheet_subresource_load_delay(binding); - continue; - } - crate::native_bridge::CssImageResourceAdmission::Untracked => None, - } - } else { - None - }; - let failed_css_image = css_image.clone(); - if let Err(error) = crate::network_host::start_stylesheet_subresource_fetch( - scope, self, binding, resource, css_image, - ) { - if let Some(identity) = failed_css_image.as_ref() { - let _ = self.fail_stylesheet_css_image(identity); - } - let settlement = self.settle_stylesheet_subresource_load_delay(binding); - tracing::warn!( - child_handle = ?completion.child_handle, - owner = ?completion.owner, - url = %request_url, - ?kind, - settled = settlement.settled(), - %error, - "child stylesheet subresource failed before network scheduling" - ); - } - } + crate::document_runtime::DocumentRuntime::start_stylesheet_subresource_fetches_in_current_scope( + scope, + self, + stylesheet_subresources, + ); tracing::debug!( child_handle = ?completion.child_handle, owner = ?completion.owner, diff --git a/moli-renderer-v8/src/native_bridge/context_host/layout.rs b/moli-renderer-v8/src/native_bridge/context_host/layout.rs index 5c9ff1e765..648e26e1fd 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/layout.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/layout.rs @@ -568,49 +568,62 @@ impl JsContextHost { pub(crate) fn document_web_font_resources_are_current( &self, + document: DomHandle, generation: crate::style_engine::StylesheetResourceGeneration, ) -> bool { self.document_layout_state .borrow() - .web_font_resources_are_current(generation) + .web_font_resources_are_current(document, self.document_handle(), generation) } - pub(crate) fn document_web_font_sidecar_is_pristine(&self) -> bool { + pub(crate) fn document_web_font_sidecar_is_pristine(&self, document: DomHandle) -> bool { self.document_layout_state .borrow() - .web_font_sidecar_is_pristine() + .web_font_sidecar_is_pristine(document, self.document_handle()) } pub(crate) fn publish_document_web_font_resource_generation( &self, + document: DomHandle, generation: crate::style_engine::StylesheetResourceGeneration, ) { self.document_layout_state .borrow_mut() - .publish_web_font_resource_generation(generation); + .publish_web_font_resource_generation(document, self.document_handle(), generation); } pub(crate) fn retain_document_web_font_slots<'a>( &self, + document: DomHandle, resources: impl IntoIterator, ) { - self.document_layout_state - .borrow_mut() - .retain_active_slots(resources); + self.document_layout_state.borrow_mut().retain_active_slots( + document, + self.document_handle(), + resources, + ); } pub(crate) fn admit_document_web_font( &self, + document: DomHandle, resource: StylesheetLoadBlockingResource, ) -> Option { - self.document_layout_state.borrow_mut().admit(resource) + self.document_layout_state + .borrow_mut() + .admit(document, self.document_handle(), resource) } pub(crate) fn complete_document_web_font( &self, + document: DomHandle, terminal: CompletedStylesheetWebFont, ) -> DocumentWebFontCompletion { - let completion = self.document_layout_state.borrow_mut().complete(terminal); + let completion = self.document_layout_state.borrow_mut().complete( + document, + self.document_handle(), + terminal, + ); if matches!(&completion, DocumentWebFontCompletion::Registered(_)) { // Geometry sampled while the request was pending contains fallback // glyph advances. Retire that tree before fonts.ready observers run. @@ -621,10 +634,29 @@ impl JsContextHost { completion } - pub(crate) fn document_web_fonts_have_pending_requests(&self) -> bool { + pub(crate) fn document_web_fonts_have_pending_requests(&self, document: DomHandle) -> bool { self.document_layout_state .borrow() - .web_fonts_have_pending_requests() + .web_fonts_have_pending_requests(document, self.document_handle()) + } + + pub(crate) fn stylesheet_web_font_document( + &self, + binding: crate::frame_owner_model::StylesheetSubresourceLoadDelayBinding, + ) -> Option { + if !self.stylesheet_subresource_load_delay_is_current(binding) { + return None; + } + match binding.child_handle() { + Some(child) => self.child_browsing_context_document_handle(child), + None => Some(self.document_handle()), + } + } + + pub(super) fn retire_embedded_document_fonts(&self, document: DomHandle) { + self.document_layout_state + .borrow_mut() + .retire_embedded_document_fonts(document); } #[cfg(test)] diff --git a/moli-renderer-v8/src/native_bridge/context_host/layout_state.rs b/moli-renderer-v8/src/native_bridge/context_host/layout_state.rs index 884a07e28e..1022192d46 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/layout_state.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/layout_state.rs @@ -40,7 +40,13 @@ pub(super) struct DocumentLayoutState { services: DocumentLayoutServices, embedded_document_services: HashMap, web_fonts: DocumentWebFontState, - web_font_resource_generation: Option, + // Allocated only when a child has enabled font resource work. The existing + // child Parley service remains the sole owner of its registered fonts. + #[expect( + clippy::box_collection, + reason = "Keep the per-Page field pointer-sized when child font loading is unused" + )] + embedded_web_fonts: Option>>, visual_state_generation: u64, latest_layout: LatestLayoutTreeCache, /// Last used content viewport published by each live iframe owner's @@ -62,22 +68,82 @@ impl DocumentLayoutState { /// Both conditions are required: a request can own a slot before its /// stylesheet generation is published, while an empty published manifest /// still records reconciliation state that later mutations must advance. - pub(super) fn web_font_sidecar_is_pristine(&self) -> bool { - self.web_font_resource_generation.is_none() && self.web_fonts.is_empty() + pub(super) fn web_font_sidecar_is_pristine( + &self, + document: DomHandle, + main_document: DomHandle, + ) -> bool { + self.web_fonts_for_document(document, main_document) + .is_none_or(DocumentWebFontState::is_pristine) } pub(super) fn web_font_resources_are_current( &self, + document: DomHandle, + main_document: DomHandle, generation: StylesheetResourceGeneration, ) -> bool { - self.web_font_resource_generation == Some(generation) + self.web_fonts_for_document(document, main_document) + .is_some_and(|fonts| fonts.resources_are_current(generation)) } pub(super) fn publish_web_font_resource_generation( &mut self, + document: DomHandle, + main_document: DomHandle, generation: StylesheetResourceGeneration, ) { - self.web_font_resource_generation = Some(generation); + let fonts = if document == main_document { + &mut self.web_fonts + } else { + self.embedded_web_fonts + .get_or_insert_with(Default::default) + .entry(document) + .or_default() + }; + // An empty manifest needs a revision, but no text/layout service. + fonts.publish_resource_generation(generation); + } + + fn web_fonts_for_document( + &self, + document: DomHandle, + main_document: DomHandle, + ) -> Option<&DocumentWebFontState> { + if document == main_document { + Some(&self.web_fonts) + } else { + self.embedded_web_fonts.as_ref()?.get(&document) + } + } + + fn with_web_fonts_for_document( + &mut self, + document: DomHandle, + main_document: DomHandle, + consume: impl FnOnce(&mut DocumentWebFontState, &mut DocumentLayoutServices) -> T, + ) -> T { + if document == main_document { + return consume(&mut self.web_fonts, &mut self.services); + } + let fonts = self + .embedded_web_fonts + .get_or_insert_with(Default::default) + .entry(document) + .or_default(); + let services = self.embedded_document_services.entry(document).or_default(); + consume(fonts, services) + } + + pub(super) fn retire_embedded_document_fonts(&mut self, document: DomHandle) { + if let Some(fonts) = &mut self.embedded_web_fonts { + if fonts.remove(&document).is_some() { + self.embedded_document_services.remove(&document); + } + if fonts.is_empty() { + self.embedded_web_fonts = None; + } + } } pub(super) const fn visual_state_generation(&self) -> u64 { @@ -119,6 +185,12 @@ impl DocumentLayoutState { ) { self.embedded_document_services .retain(|document, _| is_live(*document)); + if let Some(fonts) = &mut self.embedded_web_fonts { + fonts.retain(|document, _| is_live(*document)); + if fonts.is_empty() { + self.embedded_web_fonts = None; + } + } } pub(super) fn latest_layout( @@ -233,32 +305,66 @@ impl DocumentLayoutState { pub(super) fn retain_active_slots<'a>( &mut self, + document: DomHandle, + main_document: DomHandle, resources: impl IntoIterator, ) { - self.web_fonts - .retain_active_slots(resources, &mut self.services); + if document == main_document { + return self + .web_fonts + .retain_active_slots(resources, &mut self.services); + } + if let Some(fonts) = self + .embedded_web_fonts + .as_mut() + .and_then(|fonts| fonts.get_mut(&document)) + && let Some(services) = self.embedded_document_services.get_mut(&document) + { + fonts.retain_active_slots(resources, services); + } } pub(super) fn admit( &mut self, + document: DomHandle, + main_document: DomHandle, resource: StylesheetLoadBlockingResource, ) -> Option { - self.web_fonts.admit(resource, &mut self.services) + self.with_web_fonts_for_document(document, main_document, |fonts, services| { + fonts.admit(resource, services) + }) } pub(super) fn complete( &mut self, + document: DomHandle, + main_document: DomHandle, terminal: CompletedStylesheetWebFont, ) -> DocumentWebFontCompletion { - let completion = self.web_fonts.complete(terminal, &mut self.services); + // A stale completion must not recreate an already-retired sidecar. + if self + .web_fonts_for_document(document, main_document) + .is_none() + { + return DocumentWebFontCompletion::Stale; + } + let completion = + self.with_web_fonts_for_document(document, main_document, |fonts, services| { + fonts.complete(terminal, services) + }); if !matches!(&completion, DocumentWebFontCompletion::Stale) { self.mark_visual_state_dirty(); } completion } - pub(super) fn web_fonts_have_pending_requests(&self) -> bool { - self.web_fonts.has_pending_requests() + pub(super) fn web_fonts_have_pending_requests( + &self, + document: DomHandle, + main_document: DomHandle, + ) -> bool { + self.web_fonts_for_document(document, main_document) + .is_some_and(DocumentWebFontState::has_pending_requests) } #[cfg(test)] diff --git a/moli-renderer-v8/src/network_host/stylesheet_subresource.rs b/moli-renderer-v8/src/network_host/stylesheet_subresource.rs index 0d766a7ebd..3c46337cce 100644 --- a/moli-renderer-v8/src/network_host/stylesheet_subresource.rs +++ b/moli-renderer-v8/src/network_host/stylesheet_subresource.rs @@ -23,10 +23,10 @@ use moli_fetch::{ #[derive(Debug)] pub(crate) enum StylesheetSubresourceFetchStart { Pending, - /// The request reached a synchronous terminal and has no main-Document + /// The request reached a synchronous terminal and has no Document /// font state to commit. Settled, - /// A main-Document font reached a synchronous terminal. Network failure + /// A Document font reached a synchronous terminal. Network failure /// is represented by `CompletedStylesheetWebFont` with no body bytes. WebFontSettled(CompletedStylesheetWebFont), } @@ -60,15 +60,14 @@ pub(crate) fn start_stylesheet_subresource_fetch( if css_image.is_some() && resource_kind != StylesheetLoadBlockingResourceKind::Image { return Err("a stylesheet CSS image identity was bound to a non-image resource".to_owned()); } - let is_main_web_font = binding.child_handle().is_none() - && resource_kind == StylesheetLoadBlockingResourceKind::Font; - if is_main_web_font { + let is_web_font = resource_kind == StylesheetLoadBlockingResourceKind::Font; + if is_web_font { let web_font = resource.web_font().ok_or_else(|| { - "main Document stylesheet font is missing parsed @font-face metadata".to_owned() + "Document stylesheet font is missing parsed @font-face metadata".to_owned() })?; if web_font.request_id().is_none() { return Err( - "main Document stylesheet font is missing its admitted request identity".to_owned(), + "Document stylesheet font is missing its admitted request identity".to_owned(), ); } } @@ -129,8 +128,8 @@ pub(crate) fn start_stylesheet_subresource_fetch( .clone(), ); } - let terminal = if is_main_web_font { - let font = web_font.expect("main web-font metadata was validated before consumption"); + let terminal = if is_web_font { + let font = web_font.expect("web-font metadata was validated before consumption"); StylesheetSubresourceFetchStart::WebFontSettled( if (200..=299).contains(&response.status) { CompletedStylesheetWebFont::response(font, response.clone_body_bytes()) @@ -172,7 +171,7 @@ pub(crate) fn start_stylesheet_subresource_fetch( let _ = host.fail_stylesheet_css_image(identity); } host.settle_stylesheet_subresource_load_delay(binding); - return Ok(synchronous_failure_terminal(is_main_web_font, web_font)); + return Ok(synchronous_failure_terminal(is_web_font, web_font)); } let network_partition_key = active_subresource_network_partition_key(host, owner); let policy_context = effective_subresource_policy_context(scope, host, owner); @@ -306,12 +305,12 @@ pub(crate) fn start_stylesheet_subresource_fetch( } fn synchronous_failure_terminal( - is_main_web_font: bool, + is_web_font: bool, web_font: Option, ) -> StylesheetSubresourceFetchStart { - if is_main_web_font { + if is_web_font { StylesheetSubresourceFetchStart::WebFontSettled(CompletedStylesheetWebFont::failure( - web_font.expect("main web-font metadata was validated before consumption"), + web_font.expect("web-font metadata was validated before consumption"), )) } else { StylesheetSubresourceFetchStart::Settled diff --git a/moli-renderer-v8/src/script_vm/layout.rs b/moli-renderer-v8/src/script_vm/layout.rs index 8e584f5533..0d94f92172 100644 --- a/moli-renderer-v8/src/script_vm/layout.rs +++ b/moli-renderer-v8/src/script_vm/layout.rs @@ -426,12 +426,20 @@ impl ScriptVm { pub(super) fn complete_document_web_font( &mut self, + binding: crate::frame_owner_model::StylesheetSubresourceLoadDelayBinding, terminal: crate::css_resource_urls::CompletedStylesheetWebFont, ) { + let Some(document) = self + ._context_host + .borrow() + .stylesheet_web_font_document(binding) + else { + return; + }; match self ._context_host .borrow() - .complete_document_web_font(terminal) + .complete_document_web_font(document, terminal) { web_fonts::DocumentWebFontCompletion::Registered(outcome) => tracing::debug!( ?outcome, @@ -449,7 +457,9 @@ impl ScriptVm { } } if let Err(error) = self.with_default_context_scope(|scope, host_ptr| { - crate::context_bootstrap::settle_document_font_face_set_ready(scope, host_ptr); + crate::context_bootstrap::settle_document_font_face_set_ready( + scope, host_ptr, document, + ); Ok(()) }) { tracing::warn!(%error, "failed to settle current document font readiness"); diff --git a/moli-renderer-v8/src/script_vm/subresource_fetch/fetch_dispatch.rs b/moli-renderer-v8/src/script_vm/subresource_fetch/fetch_dispatch.rs index 007b0a198f..53be0bba52 100644 --- a/moli-renderer-v8/src/script_vm/subresource_fetch/fetch_dispatch.rs +++ b/moli-renderer-v8/src/script_vm/subresource_fetch/fetch_dispatch.rs @@ -783,22 +783,19 @@ impl ScriptVm { .clone(), ); } - if binding.child_handle().is_none() { - completed_web_font = web_font.map(|font| { - if (200..=299).contains(&response_status) - && !opaque_response_blocked - { - crate::css_resource_urls::CompletedStylesheetWebFont::response( - font, - observable_response.clone_body_bytes(), - ) - } else { - crate::css_resource_urls::CompletedStylesheetWebFont::failure( - font, - ) - } - }); - } + completed_web_font = web_font.map(|font| { + let terminal = if (200..=299).contains(&response_status) + && !opaque_response_blocked + { + crate::css_resource_urls::CompletedStylesheetWebFont::response( + font, + observable_response.clone_body_bytes(), + ) + } else { + crate::css_resource_urls::CompletedStylesheetWebFont::failure(font) + }; + (binding, terminal) + }); apply_stylesheet_subresource_terminal(&context_host, binding); } PendingSubresourceContinuation::Beacon @@ -912,11 +909,9 @@ impl ScriptVm { .borrow_mut() .fail_stylesheet_css_image(identity); } - if binding.child_handle().is_none() { - completed_web_font = web_font.map( - crate::css_resource_urls::CompletedStylesheetWebFont::failure, - ); - } + completed_web_font = web_font.map(|font| { + (binding, crate::css_resource_urls::CompletedStylesheetWebFont::failure(font)) + }); apply_stylesheet_subresource_terminal(&context_host, binding); } PendingSubresourceContinuation::Beacon @@ -946,8 +941,8 @@ impl ScriptVm { ); Ok(AsyncSubresourceFetchBodyActivity::WindowRealmEntered) }); - if let Some(web_font) = completed_web_font { - self.complete_document_web_font(web_font); + if let Some((binding, web_font)) = completed_web_font { + self.complete_document_web_font(binding, web_font); } trace_async_subresource_stage( "async_subresource_resolve_pending_done", diff --git a/moli-renderer-v8/src/script_vm/tests/child_document_web_fonts.rs b/moli-renderer-v8/src/script_vm/tests/child_document_web_fonts.rs new file mode 100644 index 0000000000..1e58fc9045 --- /dev/null +++ b/moli-renderer-v8/src/script_vm/tests/child_document_web_fonts.rs @@ -0,0 +1,249 @@ +use super::*; +use crate::{ + css_resource_urls::{ + CompletedStylesheetWebFont, StylesheetLoadBlockingResource, + stylesheet_load_blocking_font_resources, + }, + protocol_types::OptionalResourceFetchMask, +}; +use moli_page_types::LayoutPolicy; + +fn font_resource() -> StylesheetLoadBlockingResource { + stylesheet_load_blocking_font_resources( + "@font-face{font-family:ChildDocumentSourceFont;src:url(font.woff2)}", + &Url::parse("https://child-document-font.test/style.css").unwrap(), + OptionalResourceFetchMask::FONT, + ) + .into_iter() + .next() + .unwrap() +} + +fn font_response(resource: &StylesheetLoadBlockingResource) -> CompletedStylesheetWebFont { + let (_, request) = resource.clone().into_parts(); + CompletedStylesheetWebFont::response( + request.unwrap(), + include_bytes!("../../../../moli-layout/tests/fixtures/moli-ahem.woff2").to_vec(), + ) +} + +// These regressions exercise explicit OnDemand layout, rather than changing +// the legacy snapshot freshness contract to satisfy a Realtime fixture. +fn refresh_child_geometry_without_paint(vm: &StandaloneScriptVmHarness) { + let host = vm._context_host.borrow(); + let document = host.document_handle(); + let pass = host + .build_layout_pass_for_document( + document, + moli_layout::LayoutPassRequest::new( + host.layout_viewport_for_document(document), + moli_layout::LayoutFlushReason::Explicit, + ), + ) + .unwrap() + .unwrap(); + assert_eq!(pass.metrics.paint_operation_count, 0); + host.publish_layout_pass_for_document(document, pass); +} + +#[test] +fn child_web_font_document_open_retires_registrations_and_rejects_old_owner() { + let mut vm = new_parsed_test_vm( + "https://child-document-font.test/", + "iiii", + ); + vm.set_layout_policy(LayoutPolicy::OnDemand); + let rewrite = "globalThis.child=frame.contentWindow;child.document.open();\ + child.document.write('iiii');\ + child.document.close();globalThis.childTarget=child.document.getElementById('target')"; + vm.eval(rewrite).unwrap(); + let (document, binding, resource) = { + let mut host = vm._context_host.borrow_mut(); + let frame = host.dom_host().element_handle_by_id("frame").unwrap(); + let document = host.child_browsing_context_document_handle(frame).unwrap(); + let binding = host + .accept_current_stylesheet_subresource_load_delay_for_document(document) + .unwrap(); + let resource = host + .admit_document_web_font(document, font_resource()) + .unwrap(); + (document, binding, resource) + }; + refresh_child_geometry_without_paint(&vm); + let fallback = vm.eval("String(childTarget.offsetWidth)").unwrap(); + assert_ne!(fallback, "48"); + let parent = vm.eval("String(parentTarget.offsetWidth)").unwrap(); + vm.complete_document_web_font(binding, font_response(&resource)); + refresh_child_geometry_without_paint(&vm); + assert_eq!(vm.eval("String(childTarget.offsetWidth)").unwrap(), "48"); + assert_eq!(vm.eval("String(parentTarget.offsetWidth)").unwrap(), parent); + + vm.eval(rewrite).unwrap(); + refresh_child_geometry_without_paint(&vm); + assert_eq!( + vm.eval("String(childTarget.offsetWidth)").unwrap(), + fallback + ); + let (replacement_binding, replacement_resource) = { + let mut host = vm._context_host.borrow_mut(); + let frame = host.dom_host().element_handle_by_id("frame").unwrap(); + // document.open reuses the DOM handle; the exact task owner differs. + assert_eq!( + host.child_browsing_context_document_handle(frame), + Some(document) + ); + assert!(host.stylesheet_web_font_document(binding).is_none()); + let binding = host + .accept_current_stylesheet_subresource_load_delay_for_document(document) + .unwrap(); + let resource = host + .admit_document_web_font(document, font_resource()) + .unwrap(); + (binding, resource) + }; + assert_eq!( + resource.web_font().unwrap().request_id(), + replacement_resource.web_font().unwrap().request_id() + ); + let before = vm.layout_pass_observability_for_test().1; + vm.complete_document_web_font(binding, font_response(&resource)); + assert_eq!( + vm.eval("String(childTarget.offsetWidth)").unwrap(), + fallback + ); + assert_eq!(vm.layout_pass_observability_for_test().1, before); + vm.complete_document_web_font(replacement_binding, font_response(&replacement_resource)); + refresh_child_geometry_without_paint(&vm); + assert_eq!(vm.eval("String(childTarget.offsetWidth)").unwrap(), "48"); + vm.eval("frame.remove()").unwrap(); + assert!( + vm._context_host + .borrow() + .stylesheet_web_font_document(replacement_binding) + .is_none() + ); + vm.complete_document_web_font(replacement_binding, font_response(&replacement_resource)); + refresh_child_geometry_without_paint(&vm); + assert_eq!(vm.eval("String(parentTarget.offsetWidth)").unwrap(), parent); +} + +#[tokio::test] +async fn child_document_fonts_ready_waits_for_its_own_resource() { + let (font_url, request_rx, release_tx, server) = spawn_gated_font_resource_server().await; + let document_url = font_url.replace("/print-only.woff2", "/page"); + let loader = ResourceRequestClient::new(&moli_fetch::FetchConfig::default()).unwrap(); + loader.set_optional_resource_fetch_mask(OptionalResourceFetchMask::FONT); + let (mut vm, mut completions) = new_parsed_test_vm_with_loader_and_resource_completion_queue( + &document_url, + "iiii", + &loader, + ); + vm.set_layout_policy(LayoutPolicy::OnDemand); + vm.eval(&format!(r#" + globalThis.child=frame.contentWindow; + child.document.open(); + child.document.write('iiii'); + child.document.close(); + globalThis.childTarget=child.document.getElementById('target'); + globalThis.rootReady=false;globalThis.childReady=false; + document.fonts.ready.then(fonts=>rootReady=fonts===document.fonts); + globalThis.childPromise=child.document.fonts.ready; + childPromise.then(fonts=>childReady=fonts===child.document.fonts); + 'installed' + "#)).unwrap(); + tokio::time::timeout(std::time::Duration::from_secs(2), request_rx) + .await + .unwrap() + .unwrap(); + assert_eq!(vm.eval("JSON.stringify([rootReady,childReady,document.fonts.status,child.document.fonts.status,childPromise===child.document.fonts.ready])").unwrap(), r#"[true,false,"loaded","loading",true]"#); + refresh_child_geometry_without_paint(&vm); + let fallback = vm.eval("String(childTarget.offsetWidth)").unwrap(); + assert_ne!(fallback, "48"); + let parent = vm.eval("String(parentTarget.offsetWidth)").unwrap(); + release_tx.send(()).unwrap(); + server.await.unwrap(); + assert!( + tokio::time::timeout( + std::time::Duration::from_secs(2), + completions.wait_for_arrival_without_timeout() + ) + .await + .unwrap() + ); + let completion = completions.pop_next_async_subresource_event().unwrap(); + let _ = vm + .complete_async_subresource_fetch_event_body(completion) + .unwrap(); + vm.perform_owner_lane_task_microtask_checkpoints().unwrap(); + refresh_child_geometry_without_paint(&vm); + assert_eq!(vm.eval("JSON.stringify([rootReady,childReady,child.document.fonts.status,childTarget.offsetWidth])").unwrap(), r#"[true,true,"loaded",48]"#); + assert_eq!(vm.eval("String(parentTarget.offsetWidth)").unwrap(), parent); + assert_eq!(vm.document_web_font_counts_for_test(), (0, 0, 0)); + assert_eq!( + vm.layout_pass_observability_for_test() + .3 + .unwrap() + .paint_operation_count, + 0 + ); +} + +#[tokio::test] +async fn child_web_fonts_share_the_resource_policy_and_preserve_document_isolation() { + use base64::Engine; + + let font = base64::engine::general_purpose::STANDARD.encode(include_bytes!( + "../../../../moli-layout/tests/fixtures/moli-ahem.woff2" + )); + for policy in [LayoutPolicy::Mock, LayoutPolicy::OnDemand] { + for mask in [ + OptionalResourceFetchMask::NONE, + OptionalResourceFetchMask::FONT, + ] { + let loader = ResourceRequestClient::new(&moli_fetch::FetchConfig::default()).unwrap(); + loader.set_optional_resource_fetch_mask(mask); + let (mut vm, _completions) = + new_parsed_test_vm_with_loader_and_resource_completion_queue( + "https://child-font-policy.test/", + "iiii", + &loader, + ); + vm.set_layout_policy(policy); + vm.eval(&format!(r#" + globalThis.child=frame.contentWindow; + child.document.open(); + child.document.write('iiii'); + child.document.close(); + globalThis.childTarget=child.document.getElementById('target'); + "#)).unwrap(); + vm.reconcile_document_web_fonts_for_layout(); + let fonts_enabled = mask == OptionalResourceFetchMask::FONT; + { + let host = vm._context_host.borrow(); + let frame = host.dom_host().element_handle_by_id("frame").unwrap(); + let document = host.child_browsing_context_document_handle(frame).unwrap(); + assert_eq!( + host.document_web_font_sidecar_is_pristine(document), + !fonts_enabled, + "{policy:?}, {mask:?}" + ); + assert!(!host.document_web_fonts_have_pending_requests(document)); + } + assert_eq!(vm.document_web_font_counts_for_test(), (0, 0, 0)); + if policy == LayoutPolicy::OnDemand { + refresh_child_geometry_without_paint(&vm); + let child = vm.eval("String(childTarget.offsetWidth)").unwrap(); + assert_eq!( + child == "48", + fonts_enabled, + "{policy:?}, {mask:?}: {child}" + ); + let parent = vm.eval("String(parentTarget.offsetWidth)").unwrap(); + assert_ne!( + parent, "48", + "a child font must not become a parent registration" + ); + } + } + } +} diff --git a/moli-renderer-v8/src/script_vm/tests/mod.rs b/moli-renderer-v8/src/script_vm/tests/mod.rs index b1b1094649..4b39e95e91 100644 --- a/moli-renderer-v8/src/script_vm/tests/mod.rs +++ b/moli-renderer-v8/src/script_vm/tests/mod.rs @@ -1,4 +1,5 @@ mod beforeinput_target_validation; +mod child_document_web_fonts; mod credential_container; mod credential_json_options; mod cross_origin_navigation; diff --git a/moli-renderer-v8/src/script_vm/web_fonts.rs b/moli-renderer-v8/src/script_vm/web_fonts.rs index ad9b8b2e89..065dd73f23 100644 --- a/moli-renderer-v8/src/script_vm/web_fonts.rs +++ b/moli-renderer-v8/src/script_vm/web_fonts.rs @@ -22,7 +22,7 @@ struct WebFontSlot { status: WebFontRequestStatus, } -/// Resource state for the current main Document's downloadable fonts. +/// Resource state for one Document's downloadable fonts. /// /// A request id is an identity for one concrete fetch, not a generation or a /// layout consistency fence. It prevents an old response from committing after @@ -33,6 +33,7 @@ struct WebFontSlot { pub(crate) struct DocumentWebFontState { next_request_id: u64, slots: BTreeMap, + resource_generation: Option, } #[derive(Debug)] @@ -44,8 +45,22 @@ pub(crate) enum DocumentWebFontCompletion { } impl DocumentWebFontState { - pub(crate) fn is_empty(&self) -> bool { - self.slots.is_empty() + pub(crate) fn is_pristine(&self) -> bool { + self.resource_generation.is_none() && self.slots.is_empty() + } + + pub(crate) fn resources_are_current( + &self, + generation: crate::style_engine::StylesheetResourceGeneration, + ) -> bool { + self.resource_generation == Some(generation) + } + + pub(crate) fn publish_resource_generation( + &mut self, + generation: crate::style_engine::StylesheetResourceGeneration, + ) { + self.resource_generation = Some(generation); } pub(crate) fn has_pending_requests(&self) -> bool { diff --git a/moli-renderer-v8/src/stylesheet_runtime/settlement.rs b/moli-renderer-v8/src/stylesheet_runtime/settlement.rs index f24f7294c4..09490cbeee 100644 --- a/moli-renderer-v8/src/stylesheet_runtime/settlement.rs +++ b/moli-renderer-v8/src/stylesheet_runtime/settlement.rs @@ -13,27 +13,53 @@ impl DocumentRuntime { ) { let host = unsafe { &mut *host_ptr }; host.apply_pending_stylesheet_source_css_projections(scope, host_ptr); - let font_fetch_enabled = - host.current_main_document_resource_loader() - .is_some_and(|loader| { - loader.request_client().optional_resource_fetch_enabled( - crate::types::SubresourceResourceType::Font, - ) - }); - if !font_fetch_enabled { + // The shared resource mask is checked before allocating a document + // list. Default resource-free pages keep their existing cheap path. + if !host + .current_main_document_resource_loader() + .is_some_and(|loader| { + loader + .request_client() + .optional_resource_fetch_enabled(crate::types::SubresourceResourceType::Font) + }) + { return; } - let root = host.dom_host().document_element_handle(); - let Some(root) = root else { + for document in host.active_layout_document_handles() { + Self::reconcile_web_fonts_for_document_in_current_scope(scope, host_ptr, document); + } + } + + pub(crate) fn reconcile_web_fonts_for_document_in_current_scope( + scope: &mut v8::PinScope<'_, '_>, + host_ptr: *mut JsContextHost, + document: crate::document_runtime::DomHandle, + ) { + let host = unsafe { &mut *host_ptr }; + let Some(dispatch_scope) = host.owner_dispatch_scope_for_node(document) else { return; }; - let skip_pristine_document = { - let document = host.document_handle(); - host.document_web_font_sidecar_is_pristine() - && !host.document_has_style_state(document) - && !host.document_has_active_author_stylesheet_sources(document) + if !host + .document_resource_loader_for_dispatch_scope(dispatch_scope) + .is_some_and(|loader| { + loader + .request_client() + .optional_resource_fetch_enabled(crate::types::SubresourceResourceType::Font) + }) + { + return; + } + let Some(root) = host + .dom_host() + .dom() + .document_element_handle_for_document(document) + else { + return; }; - if skip_pristine_document { + if host.document_web_font_sidecar_is_pristine(document) + && !host.document_has_style_state(document) + && !host.document_has_active_author_stylesheet_sources(document) + { return; } let Some(resources) = @@ -42,36 +68,35 @@ impl DocumentRuntime { return; }; tracing::trace!( + ?document, stylesheet_import_dependency_count = resources.imports().len(), stylesheet_web_font_dependency_count = resources.web_fonts().len(), "observed typed stylesheet resource manifest" ); let generation = resources.generation(); - if host.document_web_font_resources_are_current(generation) { + if host.document_web_font_resources_are_current(document, generation) { return; } let resources = resources.web_fonts().to_vec(); - host.retain_document_web_font_slots(resources.iter()); + host.retain_document_web_font_slots(document, resources.iter()); if resources.is_empty() { - host.publish_document_web_font_resource_generation(generation); + host.publish_document_web_font_resource_generation(document, generation); return; } let resource_count = resources.len(); - let bound = { - let bound = resources - .into_iter() - .filter_map(|resource| { - host.accept_current_main_stylesheet_subresource_load_delay() - .map(|binding| (binding, resource)) - }) - .collect::>(); - if bound.len() == resource_count { - host.publish_document_web_font_resource_generation(generation); - } - bound - }; + let bound = resources + .into_iter() + .filter_map(|resource| { + host.accept_current_stylesheet_subresource_load_delay_for_document(document) + .map(|binding| (binding, resource)) + }) + .collect::>(); + if bound.len() == resource_count { + host.publish_document_web_font_resource_generation(document, generation); + } Self::start_stylesheet_subresource_fetches_in_current_scope(scope, host_ptr, bound); } + pub(crate) fn settle_stylesheet_link_clients_in_current_scope( &mut self, scope: &mut v8::PinScope<'_, '_>, @@ -275,10 +300,11 @@ impl DocumentRuntime { let mut admitted = Vec::with_capacity(resources.len()); for (binding, resource) in resources { match resource.kind() { - crate::css_resource_urls::StylesheetLoadBlockingResourceKind::Font - if binding.child_handle().is_none() => - { - match host.admit_document_web_font(resource) { + crate::css_resource_urls::StylesheetLoadBlockingResourceKind::Font => { + let Some(document) = host.stylesheet_web_font_document(binding) else { + continue; + }; + match host.admit_document_web_font(document, resource) { Some(resource) => admitted.push((binding, resource, None)), None => { host.settle_stylesheet_subresource_load_delay(binding); @@ -308,8 +334,8 @@ impl DocumentRuntime { let request_url = resource.request_url().clone(); let kind = resource.kind(); let failed_css_image = css_image.clone(); - let failed_web_font = (binding.child_handle().is_none() - && kind == crate::css_resource_urls::StylesheetLoadBlockingResourceKind::Font) + let failed_web_font = (kind + == crate::css_resource_urls::StylesheetLoadBlockingResourceKind::Font) .then(|| { resource .web_font() @@ -323,7 +349,7 @@ impl DocumentRuntime { Ok(crate::network_host::StylesheetSubresourceFetchStart::WebFontSettled( web_font, )) => { - Self::complete_document_web_font(host, web_font); + Self::complete_document_web_font(host, binding, web_font); } Ok( crate::network_host::StylesheetSubresourceFetchStart::Pending @@ -335,7 +361,7 @@ impl DocumentRuntime { } let settlement = host.settle_stylesheet_subresource_load_delay(binding); if let Some(web_font) = failed_web_font { - Self::complete_document_web_font(host, web_font); + Self::complete_document_web_font(host, binding, web_font); } tracing::warn!( url = %request_url, @@ -357,9 +383,13 @@ impl DocumentRuntime { fn complete_document_web_font( host: &JsContextHost, + binding: crate::frame_owner_model::StylesheetSubresourceLoadDelayBinding, terminal: crate::css_resource_urls::CompletedStylesheetWebFont, ) { - match host.complete_document_web_font(terminal) { + let Some(document) = host.stylesheet_web_font_document(binding) else { + return; + }; + match host.complete_document_web_font(document, terminal) { crate::script_vm::web_fonts::DocumentWebFontCompletion::Registered(outcome) => { tracing::debug!( ?outcome,