fix(fonts): settle document fonts ready after loading and layout invalidation

Validation on this branch: cargo fmt --all; cargo clippy --workspace --all-targets --all-features -- -D warnings.
Nextest and WPT reruns omitted at the user's request.

Includes the ablation-confirmed cleanup from 0e3ce99dcf.
This commit is contained in:
ldm0
2026-10-10 03:39:54 +08:00
committed by Donough Liu
parent e87d039017
commit 658bc8fcb4
9 changed files with 249 additions and 74 deletions
+3 -1
View File
@@ -146,7 +146,9 @@ pub(crate) use crypto::{
CryptoKeyAlgorithmClonePayload, CryptoKeyClonePayload, WebCryptoRejection, WebCryptoTaskResult,
crypto_key_clone_payload_from_object, crypto_key_object_from_clone_payload,
};
pub(crate) use css_fontface_runtime::{new_font_face_set, rebuild_font_face_set_faces};
pub(crate) use css_fontface_runtime::{
new_font_face_set, rebuild_font_face_set_faces, settle_document_font_face_set_ready,
};
pub(crate) use location_navigation::{
LocationNavigationKind, dispatch_top_level_form_navigation_event,
dispatch_top_level_navigation_event_with_source_element, is_native_location,
@@ -56,3 +56,4 @@ pub(super) use font_face_set::{
pub(super) use storage::install_font_face_set_template_accessors;
pub(crate) use storage::new_font_face_set;
pub(crate) use storage::rebuild_font_face_set_faces;
pub(crate) use storage::settle_document_font_face_set_ready;
@@ -86,6 +86,12 @@ fn font_face_set_attribute_getter_callback<'s>(
rv.set_undefined();
return;
};
if matches!(
attribute,
FontFaceSetAttribute::Status | FontFaceSetAttribute::Ready
) {
reconcile_document_font_face_set_ready(scope, args.this());
}
match attribute {
FontFaceSetAttribute::Status => rv.set(
font_face_set_slot_value(scope, args.this(), FONT_FACE_SET_STATUS_SLOT)
@@ -105,6 +111,86 @@ fn font_face_set_attribute_getter_callback<'s>(
}
}
const DOCUMENT_FONT_FACE_SET_OWNER: &str = "__moliFontFaceSetOwnerDocument";
const DOCUMENT_FONT_FACE_SET_READY_RESOLVER: &str = "__moliDocumentFontFaceSetReadyResolver";
fn reconcile_document_font_face_set_ready<'s>(
scope: &mut v8::PinScope<'s, '_>,
fonts: v8::Local<'s, v8::Object>,
) {
let Some(document) = get_private_value(scope, fonts, DOCUMENT_FONT_FACE_SET_OWNER)
.and_then(|value| v8::Local::<v8::Object>::try_from(value).ok())
else {
return;
};
let Ok((host_ptr, handle)) =
crate::native_bridge::node_runtime_and_handle_from_object(scope, document)
else {
return;
};
if unsafe { &*host_ptr }.document_handle() != handle {
return;
}
crate::document_runtime::DocumentRuntime::reconcile_document_web_fonts_in_current_scope(
scope, host_ptr,
);
if unsafe { &*host_ptr }.document_web_fonts_have_pending_requests() {
if get_private_value(scope, fonts, DOCUMENT_FONT_FACE_SET_READY_RESOLVER)
.is_some_and(|value| value.is_object())
{
return;
}
if let Some(resolver) = v8::PromiseResolver::new(scope) {
let promise = resolver.get_promise(scope);
set_private_value(
scope,
fonts,
DOCUMENT_FONT_FACE_SET_READY_RESOLVER,
resolver.into(),
);
set_font_face_set_slot_value(scope, fonts, FONT_FACE_SET_READY_SLOT, promise.into());
set_font_face_set_status(scope, fonts, "loading");
}
} else {
settle_document_font_face_set_ready(scope, host_ptr);
}
}
pub(crate) fn settle_document_font_face_set_ready(
scope: &mut v8::PinScope<'_, '_>,
host_ptr: *mut crate::native_bridge::JsContextHost,
) {
let host = unsafe { &*host_ptr };
if host.document_web_fonts_have_pending_requests() {
return;
}
let Some(document) = crate::util::node_wrapper_from_handle(scope, host.document_handle())
else {
return;
};
let Some(fonts) = document
.get(scope, crate::util::v8str(scope, "__moliFonts").into())
.and_then(|value| v8::Local::<v8::Object>::try_from(value).ok())
else {
return;
};
let Some(resolver) = get_private_value(scope, fonts, DOCUMENT_FONT_FACE_SET_READY_RESOLVER)
.filter(|value| value.is_object())
else {
return;
};
// Only a native PromiseResolver can occupy this private slot.
let resolver = unsafe { v8::Local::<v8::PromiseResolver>::cast_unchecked(resolver) };
set_font_face_set_status(scope, fonts, "loaded");
set_private_value(
scope,
fonts,
DOCUMENT_FONT_FACE_SET_READY_RESOLVER,
v8::undefined(scope).into(),
);
let _ = resolver.resolve(scope, fonts.into());
}
pub(crate) fn new_font_face_set<'s>(
scope: &mut v8::PinScope<'s, '_>,
) -> Option<v8::Local<'s, v8::Object>> {
@@ -589,7 +589,21 @@ impl JsContextHost {
&self,
terminal: CompletedStylesheetWebFont,
) -> DocumentWebFontCompletion {
self.document_layout_state.borrow_mut().complete(terminal)
let completion = self.document_layout_state.borrow_mut().complete(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.
self.document_layout_state
.borrow_mut()
.clear_latest_layout();
}
completion
}
pub(crate) fn document_web_fonts_have_pending_requests(&self) -> bool {
self.document_layout_state
.borrow()
.web_fonts_have_pending_requests()
}
#[cfg(test)]
@@ -257,6 +257,10 @@ impl DocumentLayoutState {
completion
}
pub(super) fn web_fonts_have_pending_requests(&self) -> bool {
self.web_fonts.has_pending_requests()
}
#[cfg(test)]
pub(super) fn web_font_counts(&self) -> (usize, usize, usize) {
(
+11 -72
View File
@@ -239,79 +239,12 @@ impl ScriptVm {
}
pub(super) fn reconcile_document_web_fonts_for_layout(&mut self) {
let font_fetch_enabled = self
.document_runtime
.current_document_resource_loader()
.is_some_and(|loader| {
loader
.request_client()
.optional_resource_fetch_enabled(crate::types::SubresourceResourceType::Font)
});
if !font_fetch_enabled {
return;
if let Err(error) = self.with_default_context_scope(|scope, host_ptr| {
DocumentRuntime::reconcile_document_web_fonts_in_current_scope(scope, host_ptr);
Ok(())
}) {
tracing::warn!(%error, "failed to reconcile current document web fonts");
}
let root = {
let host = self._context_host.borrow();
host.dom_host().document_element_handle()
};
let Some(root) = root else {
return;
};
let skip_pristine_document = {
let host = self._context_host.borrow();
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 skip_pristine_document {
return;
}
let Some(resources) = crate::layout_renderer::current_native_stylesheet_resources(
&self._context_host.borrow(),
root,
) else {
return;
};
tracing::trace!(
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 self
._context_host
.borrow()
.document_web_font_resources_are_current(generation)
{
return;
}
let resources = resources.web_fonts().to_vec();
self._context_host
.borrow()
.retain_document_web_font_slots(resources.iter());
if resources.is_empty() {
self._context_host
.borrow()
.publish_document_web_font_resource_generation(generation);
return;
}
let resource_count = resources.len();
let bound = {
let mut host = self._context_host.borrow_mut();
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
};
self.start_stylesheet_subresource_fetches(bound);
}
pub(super) fn start_css_images_discovered_by_layout(
@@ -446,6 +379,12 @@ impl ScriptVm {
tracing::debug!("discarded superseded document web font response")
}
}
if let Err(error) = self.with_default_context_scope(|scope, host_ptr| {
crate::context_bootstrap::settle_document_font_face_set_ready(scope, host_ptr);
Ok(())
}) {
tracing::warn!(%error, "failed to settle current document font readiness");
}
}
#[cfg(test)]
@@ -1,5 +1,63 @@
use super::*;
#[tokio::test]
async fn document_fonts_ready_waits_for_shadow_font_and_refreshes_cached_geometry() {
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()).expect("loader");
loader.set_optional_resource_fetch_mask(crate::protocol_types::OptionalResourceFetchMask::FONT);
let (mut vm, mut completions) = new_parsed_test_vm_with_loader_and_resource_completion_queue(
&document_url,
"<!doctype html><html><head></head><body></body></html>",
&loader,
);
vm.eval(&format!(r#"
const host=document.createElement('span');host.id='font-host';document.body.append(host);
const shadow=host.attachShadow({{mode:'open'}});
shadow.innerHTML='<style>@font-face{{font-family:ShadowReady;src:url({font_url})}}span{{display:inline-block;font:20px ShadowReady}}</style><span id="font-probe">AAAA</span>';
globalThis.fontProbe=shadow.getElementById('font-probe');
globalThis.fontReadyResolved=false;
globalThis.fontReady=document.fonts.ready;
fontReady.then(fonts=>fontReadyResolved=fonts===document.fonts);
'installed'
"#)).expect("install shadow font without a layout request");
tokio::time::timeout(std::time::Duration::from_secs(2), request_rx)
.await
.expect("fonts.ready starts the font request")
.expect("request signal");
assert_eq!(vm.eval("JSON.stringify([fontReadyResolved,document.fonts.status,fontReady===document.fonts.ready])").unwrap(),r#"[false,"loading",true]"#);
let fallback_width = vm
.eval("fontProbe.getBoundingClientRect().width")
.expect("cache fallback font geometry")
.parse::<f64>()
.expect("numeric fallback width");
// The project fixture has a 600-unit advance in a 1000-unit em:
// four glyphs at 20px must measure 48px after loading.
assert!((fallback_width - 48.0).abs() > 0.5);
release_tx
.send(())
.expect("release the exact font response");
server.await.expect("font server completes");
assert!(
tokio::time::timeout(
std::time::Duration::from_secs(2),
completions.wait_for_arrival_without_timeout()
)
.await
.expect("font completion")
);
let completion = completions
.pop_next_async_subresource_event()
.expect("typed font completion");
let _ = vm
.complete_async_subresource_fetch_event_body(completion)
.expect("register the current font");
vm.perform_owner_lane_task_microtask_checkpoints()
.expect("font completion checkpoint");
assert_eq!(vm.document_web_font_counts_for_test(), (1, 1, 1));
assert_eq!(vm.eval("JSON.stringify([fontReadyResolved,document.fonts.status,fontProbe.getBoundingClientRect().width])").unwrap(),r#"[true,"loaded",48]"#);
}
#[tokio::test(flavor = "current_thread")]
async fn linked_stylesheet_client_terminal_installs_source_before_its_load_event() {
let loader = ResourceRequestClient::new(&moli_fetch::FetchConfig::default()).expect("loader");
@@ -48,6 +48,12 @@ impl DocumentWebFontState {
self.slots.is_empty()
}
pub(crate) fn has_pending_requests(&self) -> bool {
self.slots
.values()
.any(|slot| slot.status == WebFontRequestStatus::Pending)
}
/// Removes registrations whose declarations are no longer in the current
/// Stylo source set. Pending network work may still finish, but its exact
/// request identity will no longer be accepted by [`Self::complete`].
@@ -7,6 +7,71 @@
use super::*;
impl DocumentRuntime {
pub(crate) fn reconcile_document_web_fonts_in_current_scope(
scope: &mut v8::PinScope<'_, '_>,
host_ptr: *mut JsContextHost,
) {
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 {
return;
}
let root = host.dom_host().document_element_handle();
let Some(root) = root 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 skip_pristine_document {
return;
}
let Some(resources) =
crate::layout_renderer::current_native_stylesheet_resources(host, root)
else {
return;
};
tracing::trace!(
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) {
return;
}
let resources = resources.web_fonts().to_vec();
host.retain_document_web_font_slots(resources.iter());
if resources.is_empty() {
host.publish_document_web_font_resource_generation(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
};
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<'_, '_>,