From d4a4fa6974021d408e12b5d5fa9d09ff663f1de2 Mon Sep 17 00:00:00 2001 From: ldm0 Date: Mon, 28 Sep 2026 19:23:35 +0800 Subject: [PATCH] perf(css): defer font projections until collections are exposed Carry owner refresh intent through CSS synchronization and prepare font projections only after finding an existing FontFaceSet in its creation realm. Native CSS updates continue normally and held collections update without rereading document.fonts. A test-only counter verifies 64 unobserved stylesheet mutations prepare no font projections, first observation prepares current state once, and later mutations and removals update held collections. Validation: cargo fmt --all; cargo clippy --workspace --all-targets --all-features -- -D warnings; cargo nextest run --no-fail-fast (18833 passed, 16 skipped on the final change set). --- .../src/native_bridge/document.rs | 2 + .../src/native_bridge/document/css_state.rs | 2 + .../document/css_state/font_faces.rs | 22 ++++++- .../document/css_state/projection.rs | 42 ++++++-------- .../tests/browser_api/misc/extracted/fonts.rs | 58 +++++++++++++++++++ 5 files changed, 99 insertions(+), 27 deletions(-) diff --git a/moli-renderer-v8/src/native_bridge/document.rs b/moli-renderer-v8/src/native_bridge/document.rs index b1957e898f..8c99d15be0 100644 --- a/moli-renderer-v8/src/native_bridge/document.rs +++ b/moli-renderer-v8/src/native_bridge/document.rs @@ -129,6 +129,8 @@ pub(super) use construction::{ }; pub(crate) use cookies::{document_cookie_for_receiver, set_document_cookie_for_receiver}; pub(crate) use css_state::install_adopted_style_sheets_array_primordials; +#[cfg(test)] +pub(crate) use css_state::take_owner_font_face_projection_count_for_test; pub(in crate::native_bridge) use css_state::{ AdoptedStyleSheetsArrayOwner, install_adopted_style_sheets_array_mutation_methods, normalize_adopted_style_sheets_assignment, diff --git a/moli-renderer-v8/src/native_bridge/document/css_state.rs b/moli-renderer-v8/src/native_bridge/document/css_state.rs index c33a41142c..f1e64eb287 100644 --- a/moli-renderer-v8/src/native_bridge/document/css_state.rs +++ b/moli-renderer-v8/src/native_bridge/document/css_state.rs @@ -3,6 +3,8 @@ mod font_faces; mod projection; mod shared; mod style_sheets; +#[cfg(test)] +pub(crate) use font_faces::take_owner_font_face_projection_count_for_test; pub(super) use accessors::detached_document_fonts_getter; pub(in crate::native_bridge) use accessors::{ diff --git a/moli-renderer-v8/src/native_bridge/document/css_state/font_faces.rs b/moli-renderer-v8/src/native_bridge/document/css_state/font_faces.rs index 29b9a77afe..44b97db6c0 100644 --- a/moli-renderer-v8/src/native_bridge/document/css_state/font_faces.rs +++ b/moli-renderer-v8/src/native_bridge/document/css_state/font_faces.rs @@ -23,6 +23,16 @@ const FONT_FACE_STYLESHEET_RULE_IDENTITY_SLOT: &str = "__moliFontFaceStylesheetR const FONT_FACE_STYLESHEET_RULE_FINGERPRINT_SLOT: &str = "__moliFontFaceStylesheetRuleFingerprint"; const FONT_FACE_STYLESHEET_ID_SLOT: &str = "__moliFontFaceStylesheetId"; +#[cfg(test)] +thread_local! { + static OWNER_FONT_FACE_PROJECTION_COUNT: std::cell::Cell = const { std::cell::Cell::new(0) }; +} + +#[cfg(test)] +pub(crate) fn take_owner_font_face_projection_count_for_test() -> usize { + OWNER_FONT_FACE_PROJECTION_COUNT.with(|count| count.replace(0)) +} + #[derive(Clone, Debug)] pub(super) enum OwnerFontFaceProjection { Descriptors(std::sync::Arc<[StylesheetFontFaceDescriptor]>), @@ -36,6 +46,8 @@ pub(super) fn owner_font_face_projection( host: &JsContextHost, owner: DomHandle, ) -> Option { + #[cfg(test)] + OWNER_FONT_FACE_PROJECTION_COUNT.with(|count| count.set(count.get() + 1)); let dom_host = host.dom_host(); let element = dom_host.node(owner)?.as_element()?; if !dom_host.is_connected(owner) @@ -144,13 +156,19 @@ pub(super) fn sync_document_fonts<'s>( pub(super) fn apply_font_face_owner_projection<'s>( scope: &mut v8::PinScope<'s, '_>, holder: v8::Local<'s, v8::Object>, + host: &JsContextHost, owner: DomHandle, - projection: Option<&OwnerFontFaceProjection>, + refresh: bool, ) -> bool { let Some(fonts) = object_property_as_object(scope, holder, FONTS_SLOT) else { return false; }; - set_owner_font_face_contribution(scope, fonts, owner, projection); + // Native CSS preparation and resource discovery have already run. Only + // construct the JS collection's projection when that collection exists. + let projection = refresh + .then(|| owner_font_face_projection(host, owner)) + .flatten(); + set_owner_font_face_contribution(scope, fonts, owner, projection.as_ref()); true } diff --git a/moli-renderer-v8/src/native_bridge/document/css_state/projection.rs b/moli-renderer-v8/src/native_bridge/document/css_state/projection.rs index 7516d95759..1a27bb0ba1 100644 --- a/moli-renderer-v8/src/native_bridge/document/css_state/projection.rs +++ b/moli-renderer-v8/src/native_bridge/document/css_state/projection.rs @@ -1,8 +1,5 @@ use super::{ - font_faces::{ - OwnerFontFaceProjection, apply_font_face_owner_projection, - finish_font_face_owner_projections, owner_font_face_projection, - }, + font_faces::{apply_font_face_owner_projection, finish_font_face_owner_projections}, style_sheets::sync_document_style_sheets, }; use crate::{ @@ -21,7 +18,7 @@ enum DocumentCssProjection { FontFaceOwner { document: DomHandle, owner: DomHandle, - projection: Option, + refresh: bool, }, } @@ -41,18 +38,18 @@ impl DocumentCssProjections { let owner = change.owner(); let scopes = change.tree_scopes(); if let Some(old_scope) = scopes.old() { - projections.push_for_scope(host, old_scope, owner, None); + projections.push_for_scope(host, old_scope, owner, false); } - let current_projection = match change.kind() { + let refresh = !matches!( + change.kind(), DomStylesheetOwnerChangeKind::TreeConnectionChanged { connected: false } - | DomStylesheetOwnerChangeKind::Unregistered => None, - _ => owner_font_face_projection(host, owner), - }; + | DomStylesheetOwnerChangeKind::Unregistered + ); if let Some(current_scope) = scopes.current_scope() { - projections.push_for_scope(host, current_scope, owner, current_projection.clone()); + projections.push_for_scope(host, current_scope, owner, refresh); } if let Some(new_scope) = scopes.new_scope() { - projections.push_for_scope(host, new_scope, owner, current_projection.clone()); + projections.push_for_scope(host, new_scope, owner, refresh); } } projections @@ -63,8 +60,7 @@ impl DocumentCssProjections { let Some(tree_scope) = host.dom_host().root_node_handle(owner) else { return projections; }; - let projection = owner_font_face_projection(host, owner); - projections.push_for_scope(host, tree_scope, owner, projection); + projections.push_for_scope(host, tree_scope, owner, true); projections } @@ -73,7 +69,7 @@ impl DocumentCssProjections { host: &JsContextHost, scope: DomHandle, owner: DomHandle, - projection: Option, + refresh: bool, ) { if !host.dom_host().node(scope).is_some_and(Node::is_document) { return; @@ -94,20 +90,20 @@ impl DocumentCssProjections { { let existing = &mut self.projections[index]; let DocumentCssProjection::FontFaceOwner { - projection: existing_projection, + refresh: existing_refresh, .. } = existing else { unreachable!(); }; - *existing_projection = projection; + *existing_refresh = refresh; return; } let index = self.projections.len(); self.projections.push(DocumentCssProjection::FontFaceOwner { document: scope, owner, - projection, + refresh, }); self.font_face_projection_indices .insert((scope, owner), index); @@ -133,14 +129,10 @@ impl DocumentCssProjections { DocumentCssProjection::FontFaceOwner { document, owner, - projection, + refresh, } => { - if apply_font_face_owner_projection( - scope, - holder, - owner, - projection.as_ref(), - ) && !font_face_documents.contains(&document) + if apply_font_face_owner_projection(scope, holder, host, owner, refresh) + && !font_face_documents.contains(&document) { font_face_documents.push(document); } diff --git a/moli-renderer-v8/src/script_vm/tests/browser_api/misc/extracted/fonts.rs b/moli-renderer-v8/src/script_vm/tests/browser_api/misc/extracted/fonts.rs index a13b7ed911..fc8965907d 100644 --- a/moli-renderer-v8/src/script_vm/tests/browser_api/misc/extracted/fonts.rs +++ b/moli-renderer-v8/src/script_vm/tests/browser_api/misc/extracted/fonts.rs @@ -1323,3 +1323,61 @@ fn inline_stylesheet_copy_on_write_preserves_unmodified_font_face_wrappers() { r#"{"initialCount":2,"insertPreservedFirst":true,"insertPreservedSecond":true,"descriptorReplacedFirst":true,"descriptorPreservedSecond":true}"# ); } + +#[test] +fn css_owner_changes_prepare_font_projections_only_for_exposed_collections() { + use crate::native_bridge::document::take_owner_font_face_projection_count_for_test; + let mut vm = new_storage_test_vm("https://lazy-font-projection.test/"); + take_owner_font_face_projection_count_for_test(); + vm.eval( + r#" + globalThis.lazyStyle = document.createElement('style'); + (document.head || document.documentElement || document).appendChild(lazyStyle); + globalThis.heldSheets = document.styleSheets; + for (let i = 0; i < 64; i++) { + lazyStyle.textContent = `@font-face { font-family: Face${i}; src: local(Face${i}); } + #styled { color: rgb(1, 2, 3); }`; + } + globalThis.styled = document.createElement('div'); styled.id = 'styled'; + (document.body || document.documentElement || document).appendChild(styled); + 'ready' + "#, + ) + .unwrap(); + assert_eq!( + take_owner_font_face_projection_count_for_test(), + 0, + "unobserved FontFaceSets must not prepare owner projections" + ); + assert_eq!( + vm.eval("[heldSheets.length, getComputedStyle(styled).color].join('|')") + .unwrap(), + "1|rgb(1, 2, 3)" + ); + assert_eq!( + take_owner_font_face_projection_count_for_test(), + 0, + "native styling and exposed StyleSheetLists do not consume JS font projections" + ); + assert_eq!(vm.eval("globalThis.heldFonts = document.fonts; Array.from(heldFonts, face => face.family).join(',')").unwrap(), "Face63"); + assert_eq!( + take_owner_font_face_projection_count_for_test(), + 1, + "first observation prepares the current native state once" + ); + assert_eq!( + vm.eval( + r#" + lazyStyle.textContent = '@font-face { font-family: UpdatedFace; src: local(UpdatedFace); }'; + Array.from(heldFonts, face => face.family).join(',') + "# + ) + .unwrap(), + "UpdatedFace" + ); + assert!( + take_owner_font_face_projection_count_for_test() > 0, + "already held FontFaceSets must update without rereading Document.fonts" + ); + assert_eq!(vm.eval("lazyStyle.remove(); [heldFonts.size, heldSheets.length, heldFonts === document.fonts].join('|')").unwrap(), "0|0|true"); +}