From 07368cea9a72a4f90c2becfa0de7483a37eddddc Mon Sep 17 00:00:00 2001 From: ldm0 Date: Fri, 16 Oct 2026 18:32:30 +0800 Subject: [PATCH] perf(layout): reuse prepared context for anonymous styles --- moli-layout/src/anonymous_style.rs | 33 ++ moli-layout/src/builder.rs | 20 +- moli-layout/src/form.rs | 10 +- moli-layout/src/lib.rs | 2 + moli-layout/src/source.rs | 8 +- moli-layout/src/style.rs | 16 + moli-layout/tests/box_builder_contract.rs | 10 +- moli-layout/tests/main_layout_contract.rs | 8 +- .../src/layout_renderer/style_resolver.rs | 328 +++++++++++++++++- .../context_host/host_environment.rs | 4 +- moli-renderer-v8/src/native_bridge/element.rs | 4 +- .../src/native_bridge/element/styles.rs | 2 +- .../element/styles/declaration.rs | 2 +- .../element/styles/declaration/observation.rs | 57 ++- .../element/styles/declaration/values.rs | 25 +- moli-renderer-v8/src/style_engine/computed.rs | 5 +- 16 files changed, 470 insertions(+), 64 deletions(-) create mode 100644 moli-layout/src/anonymous_style.rs diff --git a/moli-layout/src/anonymous_style.rs b/moli-layout/src/anonymous_style.rs new file mode 100644 index 0000000000..36ed97a1a8 --- /dev/null +++ b/moli-layout/src/anonymous_style.rs @@ -0,0 +1,33 @@ +use crate::ResolvedLayoutStyle; + +/// Renderer-assigned provenance of a projected style. Pass identities must +/// remain unique while any of their styles can still be used as parents. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct LayoutStyleOrigin { + pass: u64, + owner: u64, +} + +impl LayoutStyleOrigin { + pub const fn new(pass: u64, owner: u64) -> Self { + Self { pass, owner } + } +} + +/// Inheritance input for an anonymous box in the resolver's prepared pass. +/// The owner supplies provenance and diagnostics; it does not select a style +/// world or request another element-style observation. +#[derive(Clone, Copy, Debug)] +pub struct LayoutAnonymousStyleContext<'a, N> { + pub owner: N, + pub parent_style: &'a ResolvedLayoutStyle, +} + +impl<'a, N> LayoutAnonymousStyleContext<'a, N> { + pub const fn new(owner: N, parent_style: &'a ResolvedLayoutStyle) -> Self { + Self { + owner, + parent_style, + } + } +} diff --git a/moli-layout/src/builder.rs b/moli-layout/src/builder.rs index 2215854bc4..386760fca6 100644 --- a/moli-layout/src/builder.rs +++ b/moli-layout/src/builder.rs @@ -13,10 +13,10 @@ use std::{ }; use crate::{ - LayoutAnonymousReason, LayoutBoxId, LayoutBoxKind, LayoutCapabilityDiagnostic, LayoutDisplay, - LayoutElementCategory, LayoutElementSemantics, LayoutError, LayoutPseudo, LayoutSource, - LayoutSourceKind, LayoutStyleResolver, LayoutWorld, ResolvedLayoutPseudoStyle, - ResolvedLayoutStyle, + LayoutAnonymousReason, LayoutAnonymousStyleContext, LayoutBoxId, LayoutBoxKind, + LayoutCapabilityDiagnostic, LayoutDisplay, LayoutElementCategory, LayoutElementSemantics, + LayoutError, LayoutPseudo, LayoutSource, LayoutSourceKind, LayoutStyleResolver, LayoutWorld, + ResolvedLayoutPseudoStyle, ResolvedLayoutStyle, form::form_control_context, replaced::ReplacedContext, style::{InlineWhiteSpaceCollapse, LayoutOverflowMode}, @@ -885,9 +885,10 @@ where run.clear(); return Ok(()); } - let style = self - .styles - .anonymous_style(owner, parent_style, LayoutDisplay::Block)?; + let style = self.styles.anonymous_style( + LayoutAnonymousStyleContext::new(owner, parent_style), + LayoutDisplay::Block, + )?; let mut anonymous = LayoutWorld::new_box( None, Some(owner), @@ -1146,7 +1147,10 @@ where return Ok(last); } - let style = self.styles.anonymous_style(owner, parent_style, display)?; + let style = self.styles.anonymous_style( + LayoutAnonymousStyleContext::new(owner, parent_style), + display, + )?; let id = world.allocate(LayoutWorld::new_box( None, Some(owner), diff --git a/moli-layout/src/form.rs b/moli-layout/src/form.rs index 37e85e97b4..8ff749b1ed 100644 --- a/moli-layout/src/form.rs +++ b/moli-layout/src/form.rs @@ -5,9 +5,10 @@ use std::{fmt::Debug, hash::Hash, sync::Arc}; use taffy::Size; use crate::{ - LayoutAnonymousReason, LayoutBoxId, LayoutBoxKind, LayoutDisplay, LayoutElementCategory, - LayoutElementSemantics, LayoutError, LayoutFormControlKind, LayoutInputControlKind, - LayoutStyleResolver, LayoutWorld, ResolvedLayoutStyle, replaced::ReplacedContext, + LayoutAnonymousReason, LayoutAnonymousStyleContext, LayoutBoxId, LayoutBoxKind, LayoutDisplay, + LayoutElementCategory, LayoutElementSemantics, LayoutError, LayoutFormControlKind, + LayoutInputControlKind, LayoutStyleResolver, LayoutWorld, ResolvedLayoutStyle, + replaced::ReplacedContext, }; pub(crate) fn prepare_form_controls( @@ -52,8 +53,7 @@ where None }; let editor_style = styles.anonymous_style( - owner, - &world.boxes[control.index()].style, + LayoutAnonymousStyleContext::new(owner, &world.boxes[control.index()].style), LayoutDisplay::Block, )?; let selection = world.boxes[control.index()].text_selection; diff --git a/moli-layout/src/lib.rs b/moli-layout/src/lib.rs index 268c476d92..15a878a8cb 100644 --- a/moli-layout/src/lib.rs +++ b/moli-layout/src/lib.rs @@ -9,6 +9,7 @@ /// `LayoutUnit` precision. pub(crate) const LAYOUT_SUBPIXELS_PER_CSS_PIXEL: f32 = 64.0; +mod anonymous_style; mod builder; mod capture; mod containment; @@ -40,6 +41,7 @@ mod taffy_tree; mod text; mod world; +pub use anonymous_style::{LayoutAnonymousStyleContext, LayoutStyleOrigin}; pub use builder::build_layout_world; pub use capture::{ FULL_DOCUMENT_CAPTURE_CSS_DIMENSION_LIMIT, PaintCaptureRegion, PaintCaptureRequest, diff --git a/moli-layout/src/source.rs b/moli-layout/src/source.rs index d097a4f2aa..4a93eb6b0e 100644 --- a/moli-layout/src/source.rs +++ b/moli-layout/src/source.rs @@ -662,10 +662,12 @@ where fn anonymous_style( &mut self, - _owner: N, - parent: &ResolvedLayoutStyle, + context: crate::LayoutAnonymousStyleContext<'_, N>, display: crate::LayoutDisplay, ) -> Result { - Ok(ResolvedLayoutStyle::anonymous_from(parent, display)) + Ok(ResolvedLayoutStyle::anonymous_from( + context.parent_style, + display, + )) } } diff --git a/moli-layout/src/style.rs b/moli-layout/src/style.rs index f0904f61ce..117f88a5b4 100644 --- a/moli-layout/src/style.rs +++ b/moli-layout/src/style.rs @@ -482,6 +482,7 @@ impl ResolvedLayoutTransform { /// invalid. #[derive(Clone)] pub struct ResolvedLayoutStyle { + source_origin: Option, pub(crate) computed: Option>, pub(crate) taffy: Style, preferred_aspect_ratio: PreferredAspectRatio, @@ -865,6 +866,7 @@ impl ResolvedLayoutStyle { }; } Self { + source_origin: None, computed: Some(computed), taffy, preferred_aspect_ratio, @@ -942,6 +944,7 @@ impl ResolvedLayoutStyle { }; let sticky_inset = taffy.inset; Self { + source_origin: None, computed: None, taffy, preferred_aspect_ratio, @@ -1167,6 +1170,17 @@ impl ResolvedLayoutStyle { self.computed.as_ref() } + /// Binds a renderer projection to its pass and source. Cloning and + /// deriving anonymous/text styles preserve this identity. + pub fn with_source_origin(mut self, origin: crate::LayoutStyleOrigin) -> Self { + self.source_origin = Some(origin); + self + } + + pub const fn source_origin(&self) -> Option { + self.source_origin + } + pub fn background_color(&self) -> PaintColor { self.background_color } @@ -1979,6 +1993,7 @@ impl ResolvedLayoutStyle { pub(crate) fn text_leaf_from(parent: &Self) -> Self { Self { + source_origin: parent.source_origin, computed: parent.computed.clone(), taffy: Style { display: TaffyDisplay::Block, @@ -2046,6 +2061,7 @@ impl ResolvedLayoutStyle { /// Stylo allocation (primarily DOM-free tests and conservative fallback). pub fn anonymous_from(parent: &Self, display: LayoutDisplay) -> Self { Self { + source_origin: parent.source_origin, computed: parent.computed.clone(), taffy: Style { display: taffy_display(display), diff --git a/moli-layout/tests/box_builder_contract.rs b/moli-layout/tests/box_builder_contract.rs index ca152e0b1f..b9b4dcb3da 100644 --- a/moli-layout/tests/box_builder_contract.rs +++ b/moli-layout/tests/box_builder_contract.rs @@ -193,12 +193,14 @@ impl LayoutStyleResolver for TestStyles { fn anonymous_style( &mut self, - owner: usize, - parent: &ResolvedLayoutStyle, + context: moli_layout::LayoutAnonymousStyleContext<'_, usize>, display: LayoutDisplay, ) -> Result { - self.anonymous_queries.push((owner, display)); - Ok(ResolvedLayoutStyle::anonymous_from(parent, display)) + self.anonymous_queries.push((context.owner, display)); + Ok(ResolvedLayoutStyle::anonymous_from( + context.parent_style, + display, + )) } } diff --git a/moli-layout/tests/main_layout_contract.rs b/moli-layout/tests/main_layout_contract.rs index da684601cc..8beff80a0c 100644 --- a/moli-layout/tests/main_layout_contract.rs +++ b/moli-layout/tests/main_layout_contract.rs @@ -141,11 +141,13 @@ impl LayoutStyleResolver for FixtureStyles { fn anonymous_style( &mut self, - _owner: usize, - parent: &ResolvedLayoutStyle, + context: moli_layout::LayoutAnonymousStyleContext<'_, usize>, display: LayoutDisplay, ) -> Result { - Ok(ResolvedLayoutStyle::anonymous_from(parent, display)) + Ok(ResolvedLayoutStyle::anonymous_from( + context.parent_style, + display, + )) } } diff --git a/moli-renderer-v8/src/layout_renderer/style_resolver.rs b/moli-renderer-v8/src/layout_renderer/style_resolver.rs index ded3173a2e..defccdfba2 100644 --- a/moli-renderer-v8/src/layout_renderer/style_resolver.rs +++ b/moli-renderer-v8/src/layout_renderer/style_resolver.rs @@ -1,18 +1,29 @@ use moli_layout::{ - LayoutDisplay, LayoutError, LayoutStyleResolver, LayoutViewport, ResolvedLayoutElementStyles, - ResolvedLayoutPseudoStyle, ResolvedLayoutStyle, + LayoutAnonymousStyleContext, LayoutDisplay, LayoutError, LayoutStyleOrigin, + LayoutStyleResolver, LayoutViewport, ResolvedLayoutElementStyles, ResolvedLayoutPseudoStyle, + ResolvedLayoutStyle, +}; +use std::{ + sync::atomic::{AtomicU64, Ordering}, + time::Instant, }; -use std::time::Instant; use crate::{ document_runtime::DomHandle, - native_bridge::{JsContextHost, element::StyleObservation}, + native_bridge::{ + JsContextHost, + element::{PreparedAnonymousStyleContext, StyleObservation}, + }, style_engine::{StyleViewport, StyloAnonymousBoxKind}, }; +static NEXT_STYLE_PASS: AtomicU64 = AtomicU64::new(1); + pub(super) struct NativeLayoutStyleResolver<'a> { runtime: &'a JsContextHost, reads: StyleObservation<'a>, + anonymous_context: Option>, + pass_id: u64, scripting_enabled: bool, profile: Option, } @@ -41,16 +52,28 @@ impl<'a> NativeLayoutStyleResolver<'a> { viewport: LayoutViewport, ) -> Self { let mut reads = layout_style_read_scope(runtime, document, viewport); - let _ = reads.read(root).computed_values(); + // Drain pending invalidations and update the retained world before + // freezing the anonymous-style context for this synchronous pass. + let anonymous_context = reads.read(root).prepare_anonymous_style_context(); Self { runtime, reads, + anonymous_context, + pass_id: NEXT_STYLE_PASS + .fetch_update(Ordering::Relaxed, Ordering::Relaxed, |next| { + next.checked_add(1) + }) + .expect("layout style pass identity exhausted"), scripting_enabled: runtime.document_scripting_enabled(document), profile: moli_trace::cpu_profile_enabled() .then(NativeLayoutStyleResolverProfile::default), } } + fn style_origin(&self, owner: DomHandle) -> LayoutStyleOrigin { + LayoutStyleOrigin::new(self.pass_id, u64::from(owner.index_u32())) + } + pub(super) fn trace_profile(&self, document: crate::document_runtime::DomHandle) { let Some(profile) = self.profile.as_ref() else { return; @@ -129,7 +152,8 @@ impl LayoutStyleResolver for NativeLayoutStyleResolver<'_> { ); } let phase_started = self.profile.as_ref().map(|_| Instant::now()); - let mut resolved = ResolvedLayoutStyle::from_stylo(computed); + let origin = self.style_origin(node); + let mut resolved = ResolvedLayoutStyle::from_stylo(computed).with_source_origin(origin); if let Some(profile) = self.profile.as_mut() { profile.primary_projection_ns = profile.primary_projection_ns.saturating_add( phase_started @@ -138,8 +162,10 @@ impl LayoutStyleResolver for NativeLayoutStyleResolver<'_> { ); } let phase_started = self.profile.as_ref().map(|_| Instant::now()); - let before = before.map(ResolvedLayoutStyle::from_stylo); - let after = after.map(ResolvedLayoutStyle::from_stylo); + let before = before + .map(|computed| ResolvedLayoutStyle::from_stylo(computed).with_source_origin(origin)); + let after = after + .map(|computed| ResolvedLayoutStyle::from_stylo(computed).with_source_origin(origin)); if let Some(profile) = self.profile.as_mut() { profile.eager_pseudo_count = profile .eager_pseudo_count @@ -191,8 +217,11 @@ impl LayoutStyleResolver for NativeLayoutStyleResolver<'_> { ); } let phase_started = self.profile.as_ref().map(|_| Instant::now()); + let origin = self.style_origin(node); let resolved = computed.map(|computed| { - ResolvedLayoutPseudoStyle::new(ResolvedLayoutStyle::from_stylo(computed)) + ResolvedLayoutPseudoStyle::new( + ResolvedLayoutStyle::from_stylo(computed).with_source_origin(origin), + ) }); if let Some(profile) = self.profile.as_mut() { profile.marker_projection_ns = profile.marker_projection_ns.saturating_add( @@ -208,21 +237,25 @@ impl LayoutStyleResolver for NativeLayoutStyleResolver<'_> { &mut self, node: DomHandle, ) -> Result, LayoutError> { + let origin = self.style_origin(node); Ok(self .reads .read(node) .pseudo_computed_values("placeholder") .map(|computed| { - ResolvedLayoutPseudoStyle::new(ResolvedLayoutStyle::from_stylo(computed)) + ResolvedLayoutPseudoStyle::new( + ResolvedLayoutStyle::from_stylo(computed).with_source_origin(origin), + ) })) } fn anonymous_style( &mut self, - owner: DomHandle, - parent: &ResolvedLayoutStyle, + context: LayoutAnonymousStyleContext<'_, DomHandle>, display: LayoutDisplay, ) -> Result { + let owner = context.owner; + let parent = context.parent_style; let parent_computed = parent.stylo_computed_values().ok_or_else(|| { LayoutError::style_resolution( format!("{owner:?}"), @@ -231,9 +264,27 @@ impl LayoutStyleResolver for NativeLayoutStyleResolver<'_> { })?; let anonymous_kind = anonymous_box_kind(display); let phase_started = self.profile.as_ref().map(|_| Instant::now()); - let read = self.reads.read(owner); - let computed = read - .anonymous_computed_values(parent_computed.as_ref(), anonymous_kind) + let prepared = self.anonymous_context.as_ref().ok_or_else(|| { + LayoutError::style_resolution( + format!("{owner:?}"), + "anonymous style pass has no prepared document world", + ) + })?; + if self.runtime.dom_host().owner_document_handle(owner) != Some(prepared.document()) { + return Err(LayoutError::style_resolution( + format!("{owner:?}"), + "anonymous owner document does not match the prepared style context", + )); + } + let origin = self.style_origin(owner); + if parent.source_origin() != Some(origin) { + return Err(LayoutError::style_resolution( + format!("{owner:?}"), + "anonymous parent style does not match its owner and layout pass", + )); + } + let computed = prepared + .computed_values(owner, parent_computed.as_ref(), anonymous_kind) .ok_or_else(|| { LayoutError::style_resolution( format!("{owner:?}"), @@ -249,7 +300,7 @@ impl LayoutStyleResolver for NativeLayoutStyleResolver<'_> { ); } let phase_started = self.profile.as_ref().map(|_| Instant::now()); - let mut resolved = ResolvedLayoutStyle::from_stylo(computed); + let mut resolved = ResolvedLayoutStyle::from_stylo(computed).with_source_origin(origin); resolved.force_layout_display(display); if let Some(profile) = self.profile.as_mut() { profile.anonymous_projection_ns = profile.anonymous_projection_ns.saturating_add( @@ -290,10 +341,253 @@ const fn anonymous_box_kind(display: LayoutDisplay) -> StyloAnonymousBoxKind { #[cfg(test)] mod tests { - use super::anonymous_box_kind; + use super::*; use crate::style_engine::StyloAnonymousBoxKind; use moli_layout::LayoutDisplay; + fn fixture() -> crate::script_vm::StandaloneScriptVmHarness { + use crate::{ + dom::native::DomHost, parser::HtmlParser, script_vm::ScriptVmDefaultWorldBootstrap, + }; + let _runtime = crate::JsRuntime::initialize(); + let document = HtmlParser::SCRIPTING_ENABLED.parse( + url::Url::parse("https://anonymous-style.test/").unwrap(), + "
one
two
".into(), + ); + let queue = crate::page_task_queue::PageTaskQueueTestHarness::new(); + let mut vm = ScriptVmDefaultWorldBootstrap::standalone_from_dom_host_for_test( + DomHost::from_dom(document), + queue.owner_attached_runtime_page_task_sender_for_test(), + queue.parser_boundary_sender(), + ) + .unwrap() + .finish() + .unwrap(); + vm.install_page_task_residence_for_executor_test(queue.residence()); + vm.set_layout_policy(crate::real_layout_test_policy()); + vm + } + + fn parent_style( + resolver: &mut NativeLayoutStyleResolver<'_>, + owner: DomHandle, + ) -> ResolvedLayoutStyle { + ResolvedLayoutStyle::from_stylo(resolver.reads.read(owner).computed_values().unwrap()) + .with_source_origin(resolver.style_origin(owner)) + } + + #[test] + fn anonymous_styles_reuse_one_prepared_observation() { + let vm = fixture(); + let host = vm.context_host_weak_for_test().upgrade().unwrap(); + let host = host.borrow(); + let document = host.document_handle(); + let root = host.dom_host().document_element_handle().unwrap(); + let owner = host.dom_host().element_handle_by_id("owner").unwrap(); + let viewport = host.layout_viewport_for_document(document); + let mut resolver = NativeLayoutStyleResolver::new(&host, root, document, viewport); + let parent = parent_style(&mut resolver, owner); + let reads = resolver.reads.read_count_for_test(); + for _ in 0..3 { + resolver.profile = Some(NativeLayoutStyleResolverProfile::default()); + for _ in 0..10_000 { + let style = resolver + .anonymous_style( + LayoutAnonymousStyleContext::new(owner, &parent), + LayoutDisplay::Block, + ) + .unwrap(); + assert_eq!(style.display(), LayoutDisplay::Block); + std::hint::black_box(style); + } + let profile = resolver.profile.as_ref().unwrap(); + assert_eq!(profile.anonymous_count, 10_000); + assert_eq!( + resolver.reads.read_count_for_test(), + reads, + "anonymous boxes must not observe element styles again" + ); + eprintln!( + "anonymous_count={} anonymous_read_ns={} anonymous_projection_ns={}", + profile.anonymous_count, profile.anonymous_read_ns, profile.anonymous_projection_ns + ); + } + } + + #[test] + fn anonymous_styles_reject_mismatched_owners_documents_and_passes() { + let mut vm = fixture(); + vm.eval("document.getElementById('frame').contentDocument.body.textContent = 'child'") + .unwrap(); + let host = vm.context_host_weak_for_test().upgrade().unwrap(); + let host = host.borrow(); + let document = host.document_handle(); + let root = host.dom_host().document_element_handle().unwrap(); + let owner = host.dom_host().element_handle_by_id("owner").unwrap(); + let other = host.dom_host().element_handle_by_id("other").unwrap(); + let viewport = host.layout_viewport_for_document(document); + let mut resolver = NativeLayoutStyleResolver::new(&host, root, document, viewport); + let parent = parent_style(&mut resolver, owner); + let other_parent = parent_style(&mut resolver, other); + for (owner, parent) in [(owner, &other_parent), (other, &parent)] { + let error = resolver + .anonymous_style( + LayoutAnonymousStyleContext::new(owner, parent), + LayoutDisplay::Block, + ) + .unwrap_err(); + assert!( + error.to_string().contains("owner and layout pass"), + "{error}" + ); + } + + let mut inherited = parent.clone(); + let reads = resolver.reads.read_count_for_test(); + for display in [ + LayoutDisplay::Table, + LayoutDisplay::TableRow, + LayoutDisplay::TableCell, + ] { + inherited = resolver + .anonymous_style(LayoutAnonymousStyleContext::new(owner, &inherited), display) + .unwrap(); + assert_eq!(inherited.font_size(), parent.font_size()); + assert_eq!(inherited.source_origin(), parent.source_origin()); + } + assert_eq!(resolver.reads.read_count_for_test(), reads); + + let frame = host.dom_host().element_handle_by_id("frame").unwrap(); + let foreign_document = host.child_browsing_context_document_handle(frame).unwrap(); + let foreign_root = host + .dom_host() + .dom() + .document_element_handle_for_document(foreign_document) + .unwrap(); + let mut foreign = + NativeLayoutStyleResolver::new(&host, foreign_root, foreign_document, viewport); + let foreign_parent = parent_style(&mut foreign, foreign_root); + assert!( + foreign + .anonymous_style( + LayoutAnonymousStyleContext::new(foreign_root, &foreign_parent), + LayoutDisplay::Block + ) + .is_ok() + ); + let error = resolver + .anonymous_style( + LayoutAnonymousStyleContext::new(foreign_root, &foreign_parent), + LayoutDisplay::Block, + ) + .unwrap_err(); + assert!(error.to_string().contains("owner document"), "{error}"); + assert!( + resolver + .anonymous_style( + LayoutAnonymousStyleContext::new(owner, &foreign_parent), + LayoutDisplay::Block + ) + .is_err() + ); + assert!( + resolver + .anonymous_context + .as_ref() + .unwrap() + .computed_values( + foreign_root, + parent.stylo_computed_values().unwrap(), + StyloAnonymousBoxKind::Generic + ) + .is_none() + ); + + let mut next = NativeLayoutStyleResolver::new(&host, root, document, viewport); + assert!( + next.anonymous_style( + LayoutAnonymousStyleContext::new(owner, &parent), + LayoutDisplay::Block + ) + .is_err() + ); + } + + #[test] + fn anonymous_styles_prepare_pending_invalidations_for_each_pass() { + let mut vm = fixture(); + vm.sync_live_document_style_sources(); + let initial = { + let host = vm.context_host_weak_for_test().upgrade().unwrap(); + let host = host.borrow(); + let document = host.document_handle(); + let root = host.dom_host().document_element_handle().unwrap(); + let owner = host.dom_host().element_handle_by_id("owner").unwrap(); + let mut resolver = NativeLayoutStyleResolver::new( + &host, + root, + document, + host.layout_viewport_for_document(document), + ); + let parent = parent_style(&mut resolver, owner); + assert_eq!(parent.font_size(), 23.0); + assert_eq!( + resolver + .anonymous_style( + LayoutAnonymousStyleContext::new(owner, &parent), + LayoutDisplay::Block + ) + .unwrap() + .font_size(), + 23.0 + ); + parent + }; + for (mutation, expected) in [ + ( + "document.getElementById('owner').style.fontSize = '37px'", + 37.0, + ), + ( + "document.querySelector('style').textContent += '#owner {font-size:51px!important}'", + 51.0, + ), + ] { + vm.eval(mutation).unwrap(); + vm.sync_live_document_style_sources(); + let host = vm.context_host_weak_for_test().upgrade().unwrap(); + let host = host.borrow(); + let document = host.document_handle(); + let root = host.dom_host().document_element_handle().unwrap(); + let owner = host.dom_host().element_handle_by_id("owner").unwrap(); + let mut resolver = NativeLayoutStyleResolver::new( + &host, + root, + document, + host.layout_viewport_for_document(document), + ); + let parent = parent_style(&mut resolver, owner); + assert_eq!(parent.font_size(), expected); + let reads = resolver.reads.read_count_for_test(); + let anonymous = resolver + .anonymous_style( + LayoutAnonymousStyleContext::new(owner, &parent), + LayoutDisplay::Block, + ) + .unwrap(); + assert_eq!(anonymous.font_size(), expected); + assert_eq!(resolver.reads.read_count_for_test(), reads); + assert!( + resolver + .anonymous_style( + LayoutAnonymousStyleContext::new(owner, &initial), + LayoutDisplay::Block + ) + .is_err() + ); + } + } + #[test] fn anonymous_table_roles_use_the_matching_servo_precomputed_pseudo() { assert_eq!( diff --git a/moli-renderer-v8/src/native_bridge/context_host/host_environment.rs b/moli-renderer-v8/src/native_bridge/context_host/host_environment.rs index 6de1275cb6..dbd3c4609a 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/host_environment.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/host_environment.rs @@ -1422,7 +1422,7 @@ impl JsContextHost { ) } - fn style_source_document_context_for_read_document( + pub(crate) fn style_source_document_context_for_read_document( &self, read_document: DomHandle, ) -> OwnedStyleSourceDocumentContext { @@ -1800,9 +1800,9 @@ impl JsContextHost { owner: DomHandle, parent_style: &style::properties::ComputedValues, anonymous_kind: crate::style_engine::StyloAnonymousBoxKind, + document_context: &OwnedStyleSourceDocumentContext, read_document: DomHandle, ) -> Option { - let document_context = self.style_source_document_context_for_read_document(read_document); self.style_engine .computed_anonymous_style_snapshot_from_current_observation( self.dom_host(), diff --git a/moli-renderer-v8/src/native_bridge/element.rs b/moli-renderer-v8/src/native_bridge/element.rs index c6c8173e8f..20d1a38357 100644 --- a/moli-renderer-v8/src/native_bridge/element.rs +++ b/moli-renderer-v8/src/native_bridge/element.rs @@ -166,8 +166,8 @@ pub(crate) use shadow_dom::{ ensure_shadow_root_adopted_style_sheets_initialized, }; pub(crate) use styles::{ - ComputedStyleRead, StyleMode, active_css_animation_transform_value, - computed_style_properties_for_inspector_handle, + ComputedStyleRead, PreparedAnonymousStyleContext, StyleMode, + active_css_animation_transform_value, computed_style_properties_for_inspector_handle, computed_style_property_values_for_document_snapshot, css_animation_start_applies, cssom_style_entry_requires_structured_parser, cssom_style_property_mutation_affected_names_with_pdb, diff --git a/moli-renderer-v8/src/native_bridge/element/styles.rs b/moli-renderer-v8/src/native_bridge/element/styles.rs index 85d4b00705..dea29b7240 100644 --- a/moli-renderer-v8/src/native_bridge/element/styles.rs +++ b/moli-renderer-v8/src/native_bridge/element/styles.rs @@ -18,7 +18,7 @@ pub(crate) use accessors::{ style_css_text_getter_callback, style_css_text_setter_callback, style_length_getter_callback, }; pub(in crate::native_bridge::element) use declaration::style_base_url; -pub(crate) use declaration::{ComputedStyleRead, StyleObservation}; +pub(crate) use declaration::{ComputedStyleRead, PreparedAnonymousStyleContext, StyleObservation}; use declaration::{ StyleComputationContext, style_property_count_with_context, style_property_value_with_context, style_runtime_and_handle_from_object, diff --git a/moli-renderer-v8/src/native_bridge/element/styles/declaration.rs b/moli-renderer-v8/src/native_bridge/element/styles/declaration.rs index 11d5d93005..dc2e686c0b 100644 --- a/moli-renderer-v8/src/native_bridge/element/styles/declaration.rs +++ b/moli-renderer-v8/src/native_bridge/element/styles/declaration.rs @@ -9,7 +9,7 @@ mod values; pub(crate) use identity::StyleMode; pub(super) use identity::style_runtime_and_handle_from_object; pub(super) use observation::StyleComputationContext; -pub(crate) use observation::{ComputedStyleRead, StyleObservation}; +pub(crate) use observation::{ComputedStyleRead, PreparedAnonymousStyleContext, StyleObservation}; pub(crate) use parse::{ cssom_style_entry_requires_structured_parser, cssom_style_property_uses_preferred_pdb_supplemental_entries, diff --git a/moli-renderer-v8/src/native_bridge/element/styles/declaration/observation.rs b/moli-renderer-v8/src/native_bridge/element/styles/declaration/observation.rs index 5e9cb6f1b7..490c6fd20f 100644 --- a/moli-renderer-v8/src/native_bridge/element/styles/declaration/observation.rs +++ b/moli-renderer-v8/src/native_bridge/element/styles/declaration/observation.rs @@ -3,8 +3,9 @@ use std::{cell::RefCell, rc::Rc}; use crate::{ document_runtime::DomHandle, style_engine::{ - FullStyleWorldSnapshot, PreparedStyleWorldUpdate, StyleObservationSnapshot, StyleViewport, - StyleWorldUpdatePlan, StyloComputedStyleSnapshot, StyloStyleEnvironment, + FullStyleWorldSnapshot, OwnedStyleSourceDocumentContext, PreparedStyleWorldUpdate, + StyleObservationSnapshot, StyleViewport, StyleWorldUpdatePlan, StyloComputedStyleSnapshot, + StyloStyleEnvironment, }, }; @@ -67,6 +68,8 @@ impl StyleComputationContext { /// query needs it, then shared by every read in this observation. pub(crate) struct StyleObservation<'a> { runtime: &'a JsContextHost, + #[cfg(test)] + read_count: usize, /// `None` resolves one stable context per touched Document. Layout and /// detached CSSOM snapshots use `Some` to pin an exact caller-owned /// viewport instead. @@ -269,6 +272,8 @@ impl<'a> StyleObservation<'a> { ) -> Self { Self { runtime, + #[cfg(test)] + read_count: 0, fixed_context, tracks_persistent_world, sizing_target, @@ -283,7 +288,16 @@ impl<'a> StyleObservation<'a> { self.runtime } + #[cfg(test)] + pub(crate) const fn read_count_for_test(&self) -> usize { + self.read_count + } + pub(crate) fn read(&mut self, handle: DomHandle) -> ComputedStyleRead<'a> { + #[cfg(test)] + { + self.read_count += 1; + } let read_document = self .fixed_context .unwrap_or_default() @@ -417,5 +431,44 @@ pub(crate) struct ComputedStyleRead<'a> { pub(super) stylo_style: Option, } +/// A document world already updated by the pass's initial style read. Layout +/// remains synchronous; subsequent anonymous boxes only inherit and cascade. +pub(crate) struct PreparedAnonymousStyleContext<'a> { + runtime: &'a JsContextHost, + document: DomHandle, + document_context: OwnedStyleSourceDocumentContext, +} + +impl<'a> PreparedAnonymousStyleContext<'a> { + pub(super) fn new(runtime: &'a JsContextHost, document: DomHandle) -> Self { + Self { + runtime, + document, + document_context: runtime.style_source_document_context_for_read_document(document), + } + } + + pub(crate) const fn document(&self) -> DomHandle { + self.document + } + + pub(crate) fn computed_values( + &self, + owner: DomHandle, + parent_style: &style::properties::ComputedValues, + anonymous_kind: crate::style_engine::StyloAnonymousBoxKind, + ) -> Option> { + self.runtime + .computed_anonymous_style_snapshot_from_current_observation( + owner, + parent_style, + anonymous_kind, + &self.document_context, + self.document, + ) + .map(|snapshot| snapshot.computed_values()) + } +} + #[cfg(test)] mod sizing_tests; diff --git a/moli-renderer-v8/src/native_bridge/element/styles/declaration/values.rs b/moli-renderer-v8/src/native_bridge/element/styles/declaration/values.rs index 28594b7095..5ef8b6817f 100644 --- a/moli-renderer-v8/src/native_bridge/element/styles/declaration/values.rs +++ b/moli-renderer-v8/src/native_bridge/element/styles/declaration/values.rs @@ -17,8 +17,8 @@ use crate::{ }, style_engine::{ ComputedRenderedStyleFacts, FullStyleWorldSnapshot, StyleViewport, - StylesheetResourceSnapshot, StyloAnonymousBoxKind, StyloComputedStyleSnapshot, - StyloStylesheetSource, computed_property_is_queryable, + StylesheetResourceSnapshot, StyloComputedStyleSnapshot, StyloStylesheetSource, + computed_property_is_queryable, }, }, }; @@ -29,7 +29,8 @@ use style::{properties::ComputedValues, servo_arc::Arc as ServoArc, stylesheets: use super::super::super::super::JsContextHost; use super::observation::{ - ComputedStyleRead, RetainedStyleObservation, StyleComputationContext, StyleObservation, + ComputedStyleRead, PreparedAnonymousStyleContext, RetainedStyleObservation, + StyleComputationContext, StyleObservation, }; use super::sizing::used_size_from_layout_snapshot; use super::style_world::{ @@ -271,23 +272,17 @@ impl<'a> ComputedStyleRead<'a> { .map(|snapshot| snapshot.computed_values()) } - pub(crate) fn anonymous_computed_values( + pub(crate) fn prepare_anonymous_style_context( &self, - parent_style: &ComputedValues, - anonymous_kind: StyloAnonymousBoxKind, - ) -> Option> { + ) -> Option> { self.stylo_style.as_ref()?; let read_document = self .context .resolved_read_document(self.runtime, self.handle); - self.runtime - .computed_anonymous_style_snapshot_from_current_observation( - self.handle, - parent_style, - anonymous_kind, - read_document, - ) - .map(|snapshot| snapshot.computed_values()) + Some(PreparedAnonymousStyleContext::new( + self.runtime, + read_document, + )) } fn property_in_prepared_scope(&self, property: &str) -> String { diff --git a/moli-renderer-v8/src/style_engine/computed.rs b/moli-renderer-v8/src/style_engine/computed.rs index be22111f4a..32e553ab24 100644 --- a/moli-renderer-v8/src/style_engine/computed.rs +++ b/moli-renderer-v8/src/style_engine/computed.rs @@ -637,6 +637,9 @@ pub(super) fn computed_anonymous_style_snapshot_from_current_observation( read_document: DomHandle, ) -> Option { let owner_document = owner_document_for_computed_style_read(host, owner)?; + if owner_document != read_document { + return None; + } trace_computed_style_read( document_url, owner, @@ -646,7 +649,7 @@ pub(super) fn computed_anonymous_style_snapshot_from_current_observation( Some(anonymous_kind.trace_name()), document_context, ); - let world = engine.world_for_document(owner_document); + let world = engine.world_for_document(read_document); computed_anonymous_style_snapshot_in_current_world( engine, host,