fix(renderer): load and retire web fonts by child Document owner

This commit is contained in:
ldm0
2026-10-21 02:12:39 +08:00
committed by Donough Liu
parent b8ca3b075c
commit 41e608f91c
13 changed files with 559 additions and 151 deletions
@@ -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
@@ -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(
@@ -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,
));
@@ -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,
@@ -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<Item = &'a StylesheetLoadBlockingResource>,
) {
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<StylesheetLoadBlockingResource> {
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<DomHandle> {
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)]
@@ -40,7 +40,13 @@ pub(super) struct DocumentLayoutState {
services: DocumentLayoutServices,
embedded_document_services: HashMap<DomHandle, DocumentLayoutServices>,
web_fonts: DocumentWebFontState,
web_font_resource_generation: Option<StylesheetResourceGeneration>,
// 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<Box<HashMap<DomHandle, DocumentWebFontState>>>,
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<T>(
&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<Item = &'a StylesheetLoadBlockingResource>,
) {
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<StylesheetLoadBlockingResource> {
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)]
@@ -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<crate::css_resource_urls::StylesheetWebFont>,
) -> 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
+12 -2
View File
@@ -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");
@@ -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",
@@ -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/",
"<!doctype html><span id=parentTarget style='display:inline-block;font:20px ChildDocumentSourceFont,serif'>iiii</span><iframe id=frame></iframe>",
);
vm.set_layout_policy(LayoutPolicy::OnDemand);
let rewrite = "globalThis.child=frame.contentWindow;child.document.open();\
child.document.write('<!doctype html><style>@font-face{font-family:ChildDocumentSourceFont;src:url(font.woff2)}</style><span id=target style=\"display:inline-block;font:20px ChildDocumentSourceFont,serif\">iiii</span>');\
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,
"<!doctype html><span id=parentTarget style='display:inline-block;font:20px ChildReady,serif'>iiii</span><iframe id=frame></iframe>",
&loader,
);
vm.set_layout_policy(LayoutPolicy::OnDemand);
vm.eval(&format!(r#"
globalThis.child=frame.contentWindow;
child.document.open();
child.document.write('<!doctype html><style>@font-face{{font-family:ChildReady;src:url({font_url})}}</style><span id=target style="display:inline-block;font:20px ChildReady,serif">iiii</span>');
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/",
"<!doctype html><span id=parentTarget style='display:inline-block;font:20px ChildPolicy,serif'>iiii</span><iframe id=frame></iframe>",
&loader,
);
vm.set_layout_policy(policy);
vm.eval(&format!(r#"
globalThis.child=frame.contentWindow;
child.document.open();
child.document.write('<!doctype html><style>@font-face{{font-family:ChildPolicy;src:url(data:font/woff2;base64,{font})}}</style><span id=target style="display:inline-block;font:20px ChildPolicy,serif">iiii</span>');
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"
);
}
}
}
}
@@ -1,4 +1,5 @@
mod beforeinput_target_validation;
mod child_document_web_fonts;
mod credential_container;
mod credential_json_options;
mod cross_origin_navigation;
+18 -3
View File
@@ -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<String, WebFontSlot>,
resource_generation: Option<crate::style_engine::StylesheetResourceGeneration>,
}
#[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 {
@@ -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::<Vec<_>>();
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::<Vec<_>>();
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,