perf(fonts): batch membership retirement and update priorities in place

Clear and CSS manifest replacement rebuilt the physical font collection once per removed face; changing matching order also re-registered every font. Retire each Document membership change in one batch, rebuild at most once for actual removals, and update candidate priorities/caches in place for order or membership-only changes. Byte/descriptor replacements still rebuild to preserve other sources with identical Fontique attributes.

Validation: cargo fmt --all; strict all-feature workspace Clippy;
cargo nextest run --no-fail-fast (20683 tests run: 20683 passed (9 slow), 19 skipped).
This commit is contained in:
ldm0
2026-10-24 05:03:25 +08:00
committed by Donough Liu
parent 2593e76394
commit a9e54b4a0a
5 changed files with 352 additions and 111 deletions
+150 -80
View File
@@ -863,6 +863,25 @@ pub struct DocumentLayoutServices {
web_font_order: BTreeMap<String, (bool, usize)>,
has_font_face_set_order: bool,
pub(crate) text_layout_passes: u64,
#[cfg(test)]
font_collection_rebuilds: usize,
}
/// Restores a temporary native pass's candidate order, including unwinding.
/// Physical Fontique families and both text contexts remain untouched.
struct ScopedWebFontOrder<'a> {
services: &'a mut DocumentLayoutServices,
previous: BTreeMap<String, (bool, usize)>,
}
impl Drop for ScopedWebFontOrder<'_> {
fn drop(&mut self) {
self.services.web_font_order = std::mem::take(&mut self.previous);
self.services.refresh_web_font_priorities();
if let Some(parley) = self.services.parley.as_mut() {
parley.invalidate_font_selection();
}
}
}
impl Default for DocumentLayoutServices {
@@ -882,6 +901,8 @@ impl DocumentLayoutServices {
web_font_order: BTreeMap::new(),
has_font_face_set_order: false,
text_layout_passes: 0,
#[cfg(test)]
font_collection_rebuilds: 0,
}
}
@@ -895,6 +916,8 @@ impl DocumentLayoutServices {
web_font_order: BTreeMap::new(),
has_font_face_set_order: false,
text_layout_passes: 0,
#[cfg(test)]
font_collection_rebuilds: 0,
}
}
@@ -963,37 +986,54 @@ impl DocumentLayoutServices {
.into_iter()
.map(|(slot, _)| slot.clone())
.collect::<Vec<_>>();
if current == previous {
// Unloaded members can shift indices without changing the current
// registered order. Future incremental arrivals use the new indices.
if let Some(parley) = self.parley.as_mut() {
for family in parley.web_font_families.values_mut() {
for group in &mut family.groups {
for face in &mut group.faces {
face.source_priority = self
.web_font_order
.get(&face.source_slot)
.copied()
.unwrap_or((
!self.web_fonts[&face.source_slot]
.membership
.is_stylesheet_only(),
usize::MAX,
));
}
}
}
}
return false;
// Physical families are private per slot. Changing CSS selection order
// only updates candidate priorities, never Fontique registrations.
self.refresh_web_font_priorities();
let changed = current != previous;
if changed && let Some(parley) = self.parley.as_mut() {
parley.invalidate_font_selection();
}
changed
}
fn refresh_web_font_priorities(&mut self) {
let Some(parley) = self.parley.as_mut() else {
return;
};
for family in parley.web_font_families.values_mut() {
for group in &mut family.groups {
for face in &mut group.faces {
face.source_priority = self
.web_font_order
.get(&face.source_slot)
.copied()
.unwrap_or((
!self.web_fonts[&face.source_slot]
.membership
.is_stylesheet_only(),
usize::MAX,
));
}
group.faces.sort_by(|first, second| {
(first.source_priority, first.source_slot.as_str())
.cmp(&(second.source_priority, second.source_slot.as_str()))
});
}
}
}
fn rebuild_font_collection(&mut self) {
if self.parley.is_some() {
self.parley = Some(Box::new(build_parley_services(
self.system_font_policy,
&self.web_fonts,
&self.web_font_order,
)));
#[cfg(test)]
{
self.font_collection_rebuilds += 1;
}
}
true
}
/// Adds or replaces one owner-validated font face.
@@ -1024,21 +1064,27 @@ impl DocumentLayoutServices {
source_key: registration.source_key,
membership,
};
let outcome = match self.web_fonts.get(&registration.slot) {
Some(current)
if current.face == font.face
&& current.membership == font.membership
&& current.source_key == font.source_key
&& current.sfnt_bytes.as_ref() == font.sfnt_bytes.as_ref() =>
{
WebFontRegistrationOutcome::Unchanged
if let Some(current) = self.web_fonts.get_mut(&registration.slot)
&& current.face == font.face
&& (current.sfnt_bytes.id() == font.sfnt_bytes.id()
|| current.sfnt_bytes.as_ref() == font.sfnt_bytes.as_ref())
{
if current.membership == font.membership && current.source_key == font.source_key {
return Ok(WebFontRegistrationOutcome::Unchanged);
}
Some(_) => WebFontRegistrationOutcome::Replaced,
None => WebFontRegistrationOutcome::Added,
};
if outcome == WebFontRegistrationOutcome::Unchanged {
return Ok(outcome);
current.membership = font.membership;
current.source_key = font.source_key;
self.refresh_web_font_priorities();
if let Some(parley) = self.parley.as_mut() {
parley.invalidate_font_selection();
}
return Ok(WebFontRegistrationOutcome::Replaced);
}
let outcome = if self.web_fonts.contains_key(&registration.slot) {
WebFontRegistrationOutcome::Replaced
} else {
WebFontRegistrationOutcome::Added
};
// Fontique's successful registration is the validation for an added
// face. Keep it in the live collection instead of trial-registering in
// a throwaway collection and then rebuilding every existing family.
@@ -1057,16 +1103,9 @@ impl DocumentLayoutServices {
validate_registered_font(&font.face, font.sfnt_bytes.clone())?;
}
self.web_fonts.insert(registration.slot, font);
if self.parley.is_some() {
// Replacement still needs a rebuild: Fontique can unregister by
// family/attributes, not by our exact source slot. Removing that
// match would also remove other unicode-range faces sharing it.
self.parley = Some(Box::new(build_parley_services(
self.system_font_policy,
&self.web_fonts,
&self.web_font_order,
)));
}
// Replacing bytes/descriptors must rebuild: unregistering by Fontique
// attributes could also remove another slot with the same attributes.
self.rebuild_font_collection();
Ok(outcome)
}
@@ -1075,52 +1114,69 @@ impl DocumentLayoutServices {
if self.web_fonts.remove(slot).is_none() {
return false;
}
if self.parley.is_some() {
self.parley = Some(Box::new(build_parley_services(
self.system_font_policy,
&self.web_fonts,
&self.web_font_order,
)));
}
self.rebuild_font_collection();
true
}
pub fn remove_stylesheet_web_font(&mut self, slot: &str) -> bool {
self.remove_web_font_membership(slot, WebFontRegistrationScope::Stylesheet)
self.remove_stylesheet_web_fonts([slot])
}
/// Retires a CSS manifest update in one collection transaction.
pub fn remove_stylesheet_web_fonts<'a>(
&mut self,
slots: impl IntoIterator<Item = &'a str>,
) -> bool {
self.remove_web_font_memberships(slots, WebFontRegistrationScope::Stylesheet)
}
pub fn remove_font_face_set_web_font(&mut self, slot: &str) -> bool {
self.remove_web_font_membership(slot, WebFontRegistrationScope::FontFaceSetManual)
self.remove_font_face_set_web_fonts([slot])
}
fn remove_web_font_membership(&mut self, slot: &str, scope: WebFontRegistrationScope) -> bool {
let Some(font) = self.web_fonts.get_mut(slot) else {
return false;
};
match scope {
WebFontRegistrationScope::Stylesheet => {
if !std::mem::take(&mut font.membership.stylesheet) {
return false;
/// Retires a FontFaceSet membership update in one collection transaction.
pub fn remove_font_face_set_web_fonts<'a>(
&mut self,
slots: impl IntoIterator<Item = &'a str>,
) -> bool {
self.remove_web_font_memberships(slots, WebFontRegistrationScope::FontFaceSetManual)
}
fn remove_web_font_memberships<'a>(
&mut self,
slots: impl IntoIterator<Item = &'a str>,
scope: WebFontRegistrationScope,
) -> bool {
let mut changed = false;
let mut removed = false;
for slot in slots {
let Some(font) = self.web_fonts.get_mut(slot) else {
continue;
};
let retired = match scope {
WebFontRegistrationScope::Stylesheet => {
std::mem::take(&mut font.membership.stylesheet)
}
}
WebFontRegistrationScope::FontFaceSetConnected
| WebFontRegistrationScope::FontFaceSetManual => {
if font.membership.font_face_set.take().is_none() {
return false;
WebFontRegistrationScope::FontFaceSetConnected
| WebFontRegistrationScope::FontFaceSetManual => {
font.membership.font_face_set.take().is_some()
}
};
changed |= retired;
if retired && font.membership.is_empty() {
self.web_fonts.remove(slot);
removed = true;
}
}
if font.membership.is_empty() {
return self.remove_web_font(slot);
if removed {
self.rebuild_font_collection();
} else if changed {
self.refresh_web_font_priorities();
if let Some(parley) = self.parley.as_mut() {
parley.invalidate_font_selection();
}
}
if self.parley.is_some() {
self.parley = Some(Box::new(build_parley_services(
self.system_font_policy,
&self.web_fonts,
&self.web_font_order,
)));
}
true
changed
}
pub fn has_stylesheet_web_fonts(&self) -> bool {
@@ -1200,8 +1256,20 @@ impl DocumentLayoutServices {
(font.membership.is_stylesheet_only() && !is_active(slot)).then_some(slot.as_str())
})
.collect::<Vec<_>>();
if excluded.is_empty() && additions.is_empty() && order == self.web_font_order {
return consume(self);
if excluded.is_empty() && additions.is_empty() {
if order == self.web_font_order {
return consume(self);
}
let previous = std::mem::replace(&mut self.web_font_order, order);
self.refresh_web_font_priorities();
if let Some(parley) = self.parley.as_mut() {
parley.invalidate_font_selection();
}
let scoped = ScopedWebFontOrder {
services: self,
previous,
};
return consume(scoped.services);
}
// BTreeMap iteration keeps the excluded slots sorted. Font bytes are
// shared Arcs; a temporary selection never copies their payloads.
@@ -1220,6 +1288,8 @@ impl DocumentLayoutServices {
web_font_order: order,
has_font_face_set_order: self.has_font_face_set_order,
text_layout_passes: 0,
#[cfg(test)]
font_collection_rebuilds: 0,
};
let result = consume(&mut temporary);
self.text_layout_passes = self
+161
View File
@@ -453,3 +453,164 @@ fn unchanged_bytes_do_not_depend_on_blob_allocation_identity() {
assert_eq!(source_id(services.parley_mut(), "Same"), source);
}
}
#[test]
fn reordered_overlapping_fonts_keep_sources_contexts_and_invalidate_selection() {
let mut services = DocumentLayoutServices::with_system_font_policy(SystemFontPolicy::Disabled);
let narrow = include_bytes!("../../../tests/fixtures/moli-ahem-narrow.ttf");
let wide = include_bytes!("../../../tests/fixtures/moli-ahem-wide.ttf");
for (slot, bytes, weight) in [
("wide", wide.as_slice(), "100 900"),
("narrow", narrow.as_slice(), "500 800"),
] {
services
.register_web_font(WebFontRegistration::new(
slot,
WebFontFace::new("Ordered").with_css_descriptors(weight, "normal", "normal"),
bytes.to_vec(),
))
.unwrap();
}
services
.register_web_font(registration("seed", "Seed", TEST_TTF))
.unwrap();
let original_source = source_id(services.parley_mut(), "Seed");
let context = std::ptr::from_ref(&services.parley_mut().font_context);
let layout = std::ptr::from_ref(&services.parley_mut().layout_context);
for (order, bytes) in [
(["wide", "narrow"], narrow.as_slice()),
(["narrow", "wide"], wide.as_slice()),
(["wide", "narrow"], narrow.as_slice()),
] {
services.set_web_font_order(&[], &order.map(str::to_owned));
assert_eq!(
shape_one_character(services.parley_mut(), web_font_style("Ordered", 700.0), 'A'),
bytes
);
assert_eq!(source_id(services.parley_mut(), "Seed"), original_source);
assert_eq!(
std::ptr::from_ref(&services.parley_mut().font_context),
context
);
assert_eq!(
std::ptr::from_ref(&services.parley_mut().layout_context),
layout
);
assert_eq!(services.font_collection_rebuilds, 0);
}
}
#[test]
fn batch_membership_retirement_rebuilds_once_and_preserves_other_owners() {
for scope in [
WebFontRegistrationScope::Stylesheet,
WebFontRegistrationScope::FontFaceSetManual,
] {
let mut services =
DocumentLayoutServices::with_system_font_policy(SystemFontPolicy::Disabled);
for index in 0..64 {
services
.register_web_font(
registration(&format!("slot-{index}"), "Shared", TEST_TTF).with_scope(scope),
)
.unwrap();
}
services
.register_web_font(registration("retained", "Shared", TEST_TTF).with_stylesheet_scope())
.unwrap();
services
.register_web_font(registration("retained", "Shared", TEST_TTF))
.unwrap();
services.parley_mut();
let mut slots = (0..64)
.map(|index| format!("slot-{index}"))
.collect::<Vec<_>>();
slots.extend(["retained".into(), "missing".into(), "slot-0".into()]);
let retire = |services: &mut DocumentLayoutServices| match scope {
WebFontRegistrationScope::Stylesheet => {
services.remove_stylesheet_web_fonts(slots.iter().map(String::as_str))
}
_ => services.remove_font_face_set_web_fonts(slots.iter().map(String::as_str)),
};
assert!(retire(&mut services));
assert_eq!(services.font_collection_rebuilds, 1);
assert_eq!(services.web_font_count(), 1);
assert_eq!(
shape_one_character(services.parley_mut(), web_font_style("Shared", 625.0), 'R'),
TEST_TTF
);
assert!(!retire(&mut services));
assert_eq!(services.font_collection_rebuilds, 1);
let source = source_id(services.parley_mut(), "Shared");
// Adding and retiring an independent relation must retain the source.
services
.register_web_font(registration("retained", "Shared", TEST_TTF).with_scope(scope))
.unwrap();
assert!(retire(&mut services));
assert_eq!(source_id(services.parley_mut(), "Shared"), source);
assert_eq!(services.font_collection_rebuilds, 1);
assert!(services.remove_web_font("retained"));
assert_eq!(services.font_collection_rebuilds, 2);
assert_eq!(services.web_font_count(), 0);
}
}
#[test]
fn temporary_css_order_keeps_sources_and_restores_selection_after_an_error() {
let mut services = DocumentLayoutServices::with_system_font_policy(SystemFontPolicy::Disabled);
let narrow = include_bytes!("../../../tests/fixtures/moli-ahem-narrow.ttf");
let wide = include_bytes!("../../../tests/fixtures/moli-ahem-wide.ttf");
let face = WebFontFace::new("ScopedOrder").with_css_descriptors("100 900", "normal", "normal");
for (slot, bytes) in [("wide", wide.as_slice()), ("narrow", narrow.as_slice())] {
services
.register_web_font(
WebFontRegistration::new(slot, face.clone(), bytes.to_vec())
.with_stylesheet_scope(),
)
.unwrap();
}
services
.register_web_font(registration("seed", "Seed", TEST_TTF))
.unwrap();
services.set_stylesheet_web_font_order(&["wide".into(), "narrow".into()]);
let source = source_id(services.parley_mut(), "Seed");
let context = std::ptr::from_ref(&services.parley_mut().font_context);
let result: Result<(), ()> = services.with_stylesheet_font_declarations(
["narrow", "wide"].map(|slot| WebFontDeclaration {
slot,
source_key: "fixture",
face: &face,
}),
|temporary| {
assert_eq!(
shape_one_character(
temporary.parley_mut(),
web_font_style("ScopedOrder", 700.0),
'A'
),
wide
);
assert_eq!(source_id(temporary.parley_mut(), "Seed"), source);
assert_eq!(
std::ptr::from_ref(&temporary.parley_mut().font_context),
context
);
Err(())
},
);
assert!(result.is_err());
assert_eq!(
shape_one_character(
services.parley_mut(),
web_font_style("ScopedOrder", 700.0),
'A'
),
narrow
);
assert_eq!(source_id(services.parley_mut(), "Seed"), source);
assert_eq!(
std::ptr::from_ref(&services.parley_mut().font_context),
context
);
assert_eq!(services.font_collection_rebuilds, 0);
}
@@ -352,7 +352,7 @@ pub(crate) fn finish_font_face_url_load<'s>(
pub(super) fn sync_owners<'s>(scope: &mut v8::PinScope<'s, '_>, face: v8::Local<'s, v8::Object>) {
for owner in super::storage::font_face_set_owner_snapshot(scope, face) {
sync_registration(scope, owner, face, true);
sync_registration(scope, owner, face);
}
}
@@ -360,7 +360,6 @@ pub(super) fn sync_registration<'s>(
scope: &mut v8::PinScope<'s, '_>,
owner: v8::Local<'s, v8::Object>,
face: v8::Local<'s, v8::Object>,
present: bool,
) {
let Some(document) = get_private_value(scope, owner, OWNER_DOCUMENT)
.and_then(|value| crate::native_bridge::callback_value_dom_handle(scope, value))
@@ -383,20 +382,16 @@ pub(super) fn sync_registration<'s>(
let host = unsafe { &*host_ptr };
let changed = host.with_font_services(document, |services| {
let load = load.borrow();
if present {
match load.register(services, registration_scope) {
Ok(Some(
moli_layout::WebFontRegistrationOutcome::Added
| moli_layout::WebFontRegistrationOutcome::Replaced,
)) => true,
Ok(_) => false,
Err(error) => {
tracing::warn!(%error, "failed to register FontFace");
false
}
match load.register(services, registration_scope) {
Ok(Some(
moli_layout::WebFontRegistrationOutcome::Added
| moli_layout::WebFontRegistrationOutcome::Replaced,
)) => true,
Ok(_) => false,
Err(error) => {
tracing::warn!(%error, "failed to register FontFace");
false
}
} else {
services.remove_font_face_set_web_font(load.slot())
}
});
if changed {
@@ -346,21 +346,36 @@ fn sync_font_face_set_owners<'s>(
retained_slots.insert(load.borrow().slot().to_owned());
}
}
let mut removed_faces = Vec::new();
let mut removed_slots = Vec::new();
for index in 0..previous.length() {
let Some(face) = previous.get_index(scope, index) else {
continue;
};
if !array_contains_value(scope, current, face) {
if let Ok(face) = v8::Local::<v8::Object>::try_from(face) {
let retained = super::state::get(scope, face)
.is_some_and(|load| retained_slots.contains(load.borrow().slot()));
if !retained {
super::loading::sync_registration(scope, owner, face, false);
}
if let Ok(face) = v8::Local::<v8::Object>::try_from(face)
&& let Some(load) = super::state::get(scope, face)
&& !retained_slots.contains(load.borrow().slot())
{
removed_slots.push(load.borrow().slot().to_owned());
}
remove_font_face_set_owner(scope, face, owner);
removed_faces.push(face);
}
}
if !removed_slots.is_empty()
&& let Some((host_ptr, document)) = font_face_set_document_context(scope, owner)
{
// SAFETY: native font membership updates cannot call into page code.
let host = unsafe { &*host_ptr };
if host.with_font_services(document, |services| {
services.remove_font_face_set_web_fonts(removed_slots.iter().map(String::as_str))
}) {
host.invalidate_document_font_layout(document);
}
}
for face in removed_faces {
remove_font_face_set_owner(scope, face, owner);
}
for index in 0..current.length() {
let Some(face) = current.get_index(scope, index) else {
continue;
@@ -368,7 +383,7 @@ fn sync_font_face_set_owners<'s>(
if !array_contains_value(scope, previous, face) {
add_font_face_set_owner(scope, face, owner);
if let Ok(face) = v8::Local::<v8::Object>::try_from(face) {
super::loading::sync_registration(scope, owner, face, true);
super::loading::sync_registration(scope, owner, face);
if super::loading::status(scope, face)
== crate::font_loading::FontFaceStatus::Loading
{
+8 -8
View File
@@ -135,14 +135,14 @@ impl DocumentWebFontState {
.map(|font| font.registration_slot().to_owned())
.collect::<Vec<_>>();
let active = order.iter().collect::<BTreeSet<_>>();
let mut changed = false;
self.slots.retain(|slot, _| {
if active.contains(slot) {
return true;
}
changed |= services.remove_stylesheet_web_font(slot);
false
});
let removed = self
.slots
.keys()
.filter(|slot| !active.contains(slot))
.cloned()
.collect::<Vec<_>>();
self.slots.retain(|slot, _| active.contains(slot));
let changed = services.remove_stylesheet_web_fonts(removed.iter().map(String::as_str));
self.loads.retain(|_, load| load.strong_count() != 0);
self.sources.retain(|_, source| source.strong_count() != 0);
changed | services.set_stylesheet_web_font_order(&order)