diff --git a/moli-parser/src/html.rs b/moli-parser/src/html.rs index 4091fb6d93..ce351cf5ed 100644 --- a/moli-parser/src/html.rs +++ b/moli-parser/src/html.rs @@ -338,9 +338,6 @@ pub(super) struct ParseHandle { #[derive(Debug, Clone, Copy, PartialEq, Eq)] enum ParseHandleIdentity { DomNode(NativeNodeId), - // html5ever exposes only Script(handle) as an embedder yield. This marker - // is translated to OwnerInterrupted before any DOM handle is inspected. - OwnerInterrupted, // Standalone fragment parsing, like `Element.innerHTML` staging, only has a // context element name. Chromium keeps a real `context_element` next to the // `DocumentFragment` target; our detached staging parser uses this @@ -1363,18 +1360,6 @@ impl Drop for ParserInputContext { } impl ParseHandle { - pub(super) fn owner_interrupted() -> Self { - Self { - identity: ParseHandleIdentity::OwnerInterrupted, - element_name: None, - parser_flags: ParserElementFlags::default(), - } - } - - pub(super) fn is_owner_interrupted(&self) -> bool { - self.identity == ParseHandleIdentity::OwnerInterrupted - } - pub(super) fn new(node_id: NativeNodeId, element_name: Option>) -> Self { Self { identity: ParseHandleIdentity::DomNode(node_id), @@ -1406,8 +1391,7 @@ impl ParseHandle { pub(super) fn dom_node_id(&self) -> Option { match self.identity { ParseHandleIdentity::DomNode(node_id) => Some(node_id), - ParseHandleIdentity::SyntheticFragmentContext - | ParseHandleIdentity::OwnerInterrupted => None, + ParseHandleIdentity::SyntheticFragmentContext => None, } } diff --git a/moli-parser/src/session.rs b/moli-parser/src/session.rs index ebb9dc58a2..3025906a43 100644 --- a/moli-parser/src/session.rs +++ b/moli-parser/src/session.rs @@ -34,6 +34,13 @@ pub(super) enum HtmlParserSessionResult { Script(ParseHandle), } +// html5ever carries tokenizer pauses through Script(Handle). Keep the pause +// reason separate from the tree builder's DOM handles at this adapter boundary. +enum TokenizerPause { + Handoff(ParseHandle), + OwnerInterrupted, +} + struct EmbedderPausingTreeBuilder { inner: TreeBuilder, } @@ -128,16 +135,24 @@ impl EmbedderPausingTreeBuilder { } impl TokenSink for EmbedderPausingTreeBuilder { - type Handle = ParseHandle; + type Handle = TokenizerPause; fn process_token(&self, token: Token, line_number: u64) -> TokenSinkResult { let result = self.process_token_before_callbacks(token, line_number); if self.sink().finish_parser_dom_mutations().is_break() { // A nested parser invocation already handed its blocker to the // owner. Stop this outer feed before it consumes another token. - return TokenSinkResult::Script(ParseHandle::owner_interrupted()); + return TokenSinkResult::Script(TokenizerPause::OwnerInterrupted); + } + match result { + TokenSinkResult::Continue => TokenSinkResult::Continue, + TokenSinkResult::Script(handle) => { + TokenSinkResult::Script(TokenizerPause::Handoff(handle)) + } + TokenSinkResult::Plaintext => TokenSinkResult::Plaintext, + TokenSinkResult::RawData(kind) => TokenSinkResult::RawData(kind), + TokenSinkResult::EncodingIndicator(label) => TokenSinkResult::EncodingIndicator(label), } - result } fn end(&self) { @@ -325,12 +340,11 @@ fn feed_with_definitive_encoding( // advisory notification without exposing a false parser pause. TokenizerResult::EncodingIndicator(_) => {} TokenizerResult::Done => return HtmlParserSessionResult::InputDrained, - TokenizerResult::Script(handle) => { - return if handle.is_owner_interrupted() { - HtmlParserSessionResult::OwnerInterrupted - } else { - HtmlParserSessionResult::Script(handle) - }; + TokenizerResult::Script(TokenizerPause::Handoff(handle)) => { + return HtmlParserSessionResult::Script(handle); + } + TokenizerResult::Script(TokenizerPause::OwnerInterrupted) => { + return HtmlParserSessionResult::OwnerInterrupted; } } } diff --git a/moli-renderer-v8/src/document_runtime/dom_facade.rs b/moli-renderer-v8/src/document_runtime/dom_facade.rs index 86ec4dad8f..1d72630331 100644 --- a/moli-renderer-v8/src/document_runtime/dom_facade.rs +++ b/moli-renderer-v8/src/document_runtime/dom_facade.rs @@ -1,8 +1,9 @@ use super::mutation_commands::{ - apply_runtime_mutation_effects_to_dom_host, finish_runtime_mutation_effects, + RuntimeMutationApplyResult, finish_runtime_mutation_effects, prepare_runtime_mutation_effects, }; use super::*; use crate::parser::{ParserPlanningReadView, ParserScriptRead}; +use crate::parser_mutation_effects::{ParserMutationEffectsOwner, apply_parser_mutation_effects}; use crate::stylesheet_blocking::StylesheetElementRead; use html5ever::tree_builder::QuirksMode; // This slice collects the remaining low-level facade methods that mostly forward into `DomHost` @@ -97,39 +98,8 @@ impl DocumentRuntime { host_ptr: *mut JsContextHost, effects: DomMutationEffects, ) { - let connected_roots = effects.tree().connected_roots().to_vec(); - let form_owner_effects = effects.clone(); self.assert_active_parser_document_incarnation(); - let result = { - let dom_host = self.dom_host.borrow_mut(); - apply_runtime_mutation_effects_to_dom_host( - &mut self.mutations, - &self.document, - self.script_lifecycle.scripts_mut(), - &mut self.events, - scope, - host_ptr, - dom_host, - effects, - RuntimeMutationOptions::parser_tree_sink(), - ) - }; - let _ = finish_runtime_mutation_effects(self, scope, host_ptr, result); - if !connected_roots.is_empty() { - self.ensure_parser_custom_element_reaction_queue(host_ptr); - crate::custom_elements::enqueue_connected_and_form_callbacks_for_already_upgraded_subtrees( - scope, - host_ptr, - &connected_roots, - ); - } - if crate::custom_elements::form_owner_mutation_effects_touch_html_form( - self.dom_host(), - &form_owner_effects, - ) { - self.ensure_parser_custom_element_reaction_queue(host_ptr); - crate::custom_elements::enqueue_form_association_callbacks_for_all(scope, host_ptr); - } + apply_parser_mutation_effects(scope, host_ptr, self, &effects); } pub(crate) fn parser_runtime_dom_node_exists(&mut self, node_id: DomHandle) -> bool { @@ -843,14 +813,30 @@ impl DocumentRuntime { } } -pub(super) fn sync_style_sources_from_dom_mutation_effects( - host_ptr: *mut JsContextHost, - effects: &DomMutationEffects, -) { - if effects.stylesheet_owners().changes().is_empty() { - return; +impl ParserMutationEffectsOwner for DocumentRuntime { + type Prepared = RuntimeMutationApplyResult; + + fn prepare_parser_mutation_effects(&mut self, effects: &DomMutationEffects) -> Self::Prepared { + prepare_runtime_mutation_effects( + self.dom_host(), + self.document.url(), + effects, + RuntimeMutationOptions::parser_tree_sink(), + ) + } + + fn ensure_parser_reaction_queue(&mut self, host_ptr: *mut JsContextHost) { + self.ensure_parser_custom_element_reaction_queue(host_ptr); + } + + fn finish_parser_mutation_effects( + &mut self, + scope: &mut v8::PinScope<'_, '_>, + host_ptr: *mut JsContextHost, + prepared: Self::Prepared, + ) { + let _ = finish_runtime_mutation_effects(self, scope, host_ptr, prepared); } - unsafe { &mut *host_ptr }.apply_stylesheet_owner_changes(effects.stylesheet_owners().changes()); } #[cfg(test)] diff --git a/moli-renderer-v8/src/document_runtime/mutation_commands.rs b/moli-renderer-v8/src/document_runtime/mutation_commands.rs index cdd29e1978..13ce9879e1 100644 --- a/moli-renderer-v8/src/document_runtime/mutation_commands.rs +++ b/moli-renderer-v8/src/document_runtime/mutation_commands.rs @@ -9,7 +9,6 @@ use crate::{ }; use moli_dom::native::{Element, Node, NodeType}; -use super::dom_facade::sync_style_sources_from_dom_mutation_effects; use super::*; mod details; @@ -1709,7 +1708,7 @@ impl DocumentRuntime { } } -pub(super) struct RuntimeMutationApplyResult { +pub(crate) struct RuntimeMutationApplyResult { changed: bool, meta_refresh_candidates: Vec, devtools_dom_mutations: Vec, @@ -1919,25 +1918,17 @@ fn execute_committed_inline_classic_script( } } -pub(super) fn apply_runtime_mutation_effects_to_dom_host( - mutations: &mut MutationCoordinator, - document: &HostDocumentState, - scripts: &mut HostScriptScheduler, - events: &mut HostEventTargetRegistry, - scope: &mut v8::PinScope<'_, '_>, - host_ptr: *mut JsContextHost, - dom_host: &mut DomHost, - effects: DomMutationEffects, +pub(super) fn prepare_runtime_mutation_effects( + dom_host: &DomHost, + document_url: &Url, + effects: &DomMutationEffects, options: RuntimeMutationOptions, ) -> RuntimeMutationApplyResult { - let cpu_profile_enabled = moli_trace::cpu_profile_enabled(); - let total_started = cpu_profile_enabled.then(Instant::now); - let style_sources_started = cpu_profile_enabled.then(Instant::now); let stylesheet_owner_changes = effects.stylesheet_owners().changes().to_vec(); let meta_refresh_candidates = super::meta_refresh::meta_refresh_navigations_from_mutation( dom_host, - &effects, - document.url(), + effects, + document_url, ); let inline_style_attribute_csp_mutations = if options.check_inline_style_csp { effects @@ -1975,11 +1966,36 @@ pub(super) fn apply_runtime_mutation_effects_to_dom_host( Vec::new() }; let devtools_dom_mutations = - super::devtools_mutations::capture_devtools_dom_mutation_facts(dom_host, &effects); - if effects.did_change() { - sync_style_sources_from_dom_mutation_effects(host_ptr, &effects); + super::devtools_mutations::capture_devtools_dom_mutation_facts(dom_host, effects); + RuntimeMutationApplyResult { + changed: effects.did_change(), + meta_refresh_candidates, + devtools_dom_mutations, + runtime_script_start_candidates: Vec::new(), + removed_open_popovers: effects.tree().removed_open_popovers().to_vec(), + changed_slots: effects.slots().changed_slots().to_vec(), + stylesheet_owner_changes, + inline_style_attribute_csp_mutations, + connected_style_csp_roots, } - let style_sources_us = style_sources_started +} + +pub(super) fn apply_runtime_mutation_effects_to_dom_host( + mutations: &mut MutationCoordinator, + document: &HostDocumentState, + scripts: &mut HostScriptScheduler, + events: &mut HostEventTargetRegistry, + scope: &mut v8::PinScope<'_, '_>, + host_ptr: *mut JsContextHost, + dom_host: &mut DomHost, + effects: DomMutationEffects, + options: RuntimeMutationOptions, +) -> RuntimeMutationApplyResult { + let cpu_profile_enabled = moli_trace::cpu_profile_enabled(); + let total_started = cpu_profile_enabled.then(Instant::now); + let preparation_started = cpu_profile_enabled.then(Instant::now); + let mut result = prepare_runtime_mutation_effects(dom_host, document.url(), &effects, options); + let preparation_us = preparation_started .map(|started| started.elapsed().as_micros()) .unwrap_or_default(); let started = dom_binding_timing_started(); @@ -1992,24 +2008,16 @@ pub(super) fn apply_runtime_mutation_effects_to_dom_host( tracing::info!( target: "moli_cpu_profile", stage = "apply_runtime_mutation_effects", - style_sources_us, - coordinator_us = total_us.saturating_sub(style_sources_us), + preparation_us, + coordinator_us = total_us.saturating_sub(preparation_us), total_us, ); } } record_dom_binding_timing("mutation.apply", started); - RuntimeMutationApplyResult { - changed: mutation_result.changed, - meta_refresh_candidates, - devtools_dom_mutations, - runtime_script_start_candidates: mutation_result.runtime_script_start_candidates, - removed_open_popovers: mutation_result.removed_open_popovers, - changed_slots: mutation_result.changed_slots, - stylesheet_owner_changes, - inline_style_attribute_csp_mutations, - connected_style_csp_roots, - } + result.changed = mutation_result.changed; + result.runtime_script_start_candidates = mutation_result.runtime_script_start_candidates; + result } fn should_dispatch_attribute_changed_for_set( diff --git a/moli-renderer-v8/src/lib.rs b/moli-renderer-v8/src/lib.rs index 86fccc943f..c7f7359912 100644 --- a/moli-renderer-v8/src/lib.rs +++ b/moli-renderer-v8/src/lib.rs @@ -80,6 +80,7 @@ mod page_resource_completion; mod page_task_queue; mod parser_module_evaluation; mod parser_module_pending; +mod parser_mutation_effects; mod parser_script; mod permissions_policy; mod queue_microtask; diff --git a/moli-renderer-v8/src/mutation_coordinator.rs b/moli-renderer-v8/src/mutation_coordinator.rs index 7e8e3dacd2..2e18e3e112 100644 --- a/moli-renderer-v8/src/mutation_coordinator.rs +++ b/moli-renderer-v8/src/mutation_coordinator.rs @@ -1,7 +1,9 @@ use std::time::Instant; use crate::dom::native::{DomHost, DomMutationEffects, NativeNodeId, ScriptPrepareTriggerKind}; -use crate::style_engine::StyleMutationEffect; + +mod notifications; +pub(crate) use notifications::notify_dom_mutation; use super::{ host::{ @@ -9,7 +11,6 @@ use super::{ RuntimeScriptStartDecision, ScriptElementLoader, ScriptElementLoaderOptions, }, native_bridge::{self, JsContextHost}, - observer_runtime, util::v8str, }; @@ -19,8 +20,6 @@ pub(super) struct MutationCoordinator; pub(super) struct MutationCoordinatorApplyResult { pub(super) changed: bool, pub(super) runtime_script_start_candidates: Vec, - pub(super) removed_open_popovers: Vec, - pub(super) changed_slots: Vec, } #[derive(Debug)] @@ -175,14 +174,11 @@ impl MutationCoordinator { return MutationCoordinatorApplyResult { changed: false, runtime_script_start_candidates: Vec::new(), - removed_open_popovers: Vec::new(), - changed_slots: Vec::new(), }; } if options.source == DomMutationSource::JsDomApi { Self::note_script_children_changed_by_api(dom_host, &effects); } - unsafe { &mut *host_ptr }.note_app_manifest_link_mutation(dom_host, &effects); let cpu_profile_enabled = moli_trace::cpu_profile_enabled(); let total_started = cpu_profile_enabled.then(Instant::now); tracing::trace!( @@ -194,19 +190,12 @@ impl MutationCoordinator { dispatch_atomic_move_callbacks = options.dispatch_atomic_move_callbacks, "applying runtime mutation effects" ); - let style_effects_started = cpu_profile_enabled.then(Instant::now); - let style_effects = StyleMutationEffect::from_dom_mutation_effects(dom_host, &effects); - let style_effect_count = style_effects.len(); - let style_effects_us = style_effects_started - .map(|started| started.elapsed().as_micros()) - .unwrap_or_default(); - let style_invalidation_started = cpu_profile_enabled.then(Instant::now); - if !style_effects.is_empty() { - unsafe { &mut *host_ptr }.note_style_mutation_effects(&style_effects); - } - let style_invalidation_us = style_invalidation_started - .map(|started| started.elapsed().as_micros()) - .unwrap_or_default(); + let notifications::MutationNotificationTimings { + style_effect_count, + style_effects_us, + style_invalidation_us, + observer_us, + } = notify_dom_mutation(scope, host_ptr, dom_host, &effects); let timing_started = moli_trace::cdp_nav_timing_enabled().then(Instant::now); let prepare_connected_scripts = options.prepares_connected_scripts(); @@ -262,12 +251,6 @@ impl MutationCoordinator { let script_planning_us = script_planning_started .map(|started| started.elapsed().as_micros()) .unwrap_or_default(); - let observer_started = cpu_profile_enabled.then(Instant::now); - observer_runtime::queue_mutation_records(scope, host_ptr, dom_host, &effects); - let observer_us = observer_started - .map(|started| started.elapsed().as_micros()) - .unwrap_or_default(); - let script_start_started = cpu_profile_enabled.then(Instant::now); let mut runtime_script_start_candidates = Vec::new(); for request in script_start_requests { @@ -328,8 +311,6 @@ impl MutationCoordinator { MutationCoordinatorApplyResult { changed: true, runtime_script_start_candidates, - removed_open_popovers: effects.tree().removed_open_popovers().to_vec(), - changed_slots: effects.slots().changed_slots().to_vec(), } } diff --git a/moli-renderer-v8/src/mutation_coordinator/notifications.rs b/moli-renderer-v8/src/mutation_coordinator/notifications.rs new file mode 100644 index 0000000000..2ced50f76e --- /dev/null +++ b/moli-renderer-v8/src/mutation_coordinator/notifications.rs @@ -0,0 +1,57 @@ +use std::time::Instant; + +use crate::{ + dom::native::{DomHost, DomMutationEffects}, + native_bridge::JsContextHost, + observer_runtime, + style_engine::StyleMutationEffect, +}; + +pub(crate) struct MutationNotificationTimings { + pub(super) style_effect_count: usize, + pub(super) style_effects_us: u128, + pub(super) style_invalidation_us: u128, + pub(super) observer_us: u128, +} + +/// Notifications derived entirely from the committed DOM changes. This layer +/// does not need a DocumentRuntime or prepare scripts for any document owner. +pub(crate) fn notify_dom_mutation( + scope: &mut v8::PinScope<'_, '_>, + host_ptr: *mut JsContextHost, + dom_host: &DomHost, + effects: &DomMutationEffects, +) -> MutationNotificationTimings { + let host = unsafe { &mut *host_ptr }; + if !effects.stylesheet_owners().changes().is_empty() { + host.apply_stylesheet_owner_changes(effects.stylesheet_owners().changes()); + } + host.note_app_manifest_link_mutation(dom_host, effects); + + let profile = moli_trace::cpu_profile_enabled(); + let started = profile.then(Instant::now); + let style_effects = StyleMutationEffect::from_dom_mutation_effects(dom_host, effects); + let style_effect_count = style_effects.len(); + let style_effects_us = started + .map(|started| started.elapsed().as_micros()) + .unwrap_or_default(); + let started = profile.then(Instant::now); + if !style_effects.is_empty() { + host.note_style_mutation_effects(&style_effects); + } + let style_invalidation_us = started + .map(|started| started.elapsed().as_micros()) + .unwrap_or_default(); + + let started = profile.then(Instant::now); + observer_runtime::queue_mutation_records(scope, host_ptr, dom_host, effects); + let observer_us = started + .map(|started| started.elapsed().as_micros()) + .unwrap_or_default(); + MutationNotificationTimings { + style_effect_count, + style_effects_us, + style_invalidation_us, + observer_us, + } +} diff --git a/moli-renderer-v8/src/native_bridge/context_host/child_documents/live_parser.rs b/moli-renderer-v8/src/native_bridge/context_host/child_documents/live_parser.rs index 787759578b..d0aad86740 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/child_documents/live_parser.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/child_documents/live_parser.rs @@ -27,6 +27,7 @@ use crate::{ ParserElementCreationConsumer, ParserElementCreationRequest, ParserMutationEffectConsumer, ParserPlanningReadView, ParserScriptHandoff, ParserScriptRead, PreparedImportMapSource, }, + parser_mutation_effects::{ParserMutationEffectsOwner, apply_parser_mutation_effects}, planning::ScriptSource, types::{ScriptKind, ScriptSourceKind}, }; @@ -60,9 +61,36 @@ struct ChildFrameLiveParserOwner<'a, 'scope, 'pin> { child_document_handle: DomHandle, document_owner: Option, parser_control: Option, + mutation_effects: ChildParserMutationEffects, +} + +struct ChildParserMutationEffects { + document_handle: DomHandle, reaction_queue_active: bool, } +impl ParserMutationEffectsOwner for ChildParserMutationEffects { + type Prepared = (); + + fn prepare_parser_mutation_effects(&mut self, _effects: &DomMutationEffects) {} + + fn ensure_parser_reaction_queue(&mut self, host_ptr: *mut JsContextHost) { + if !self.reaction_queue_active { + custom_elements::push_parser_custom_element_reaction_queue(host_ptr); + self.reaction_queue_active = true; + } + } + + fn finish_parser_mutation_effects( + &mut self, + scope: &mut v8::PinScope<'_, '_>, + host_ptr: *mut JsContextHost, + _prepared: (), + ) { + unsafe { &mut *host_ptr }.sync_child_browsing_context_subtree(scope, self.document_handle); + } +} + impl<'a, 'scope, 'pin> ChildFrameLiveParserOwner<'a, 'scope, 'pin> { fn new( host: &'a mut JsContextHost, @@ -81,7 +109,10 @@ impl<'a, 'scope, 'pin> ChildFrameLiveParserOwner<'a, 'scope, 'pin> { child_document_handle, document_owner, parser_control, - reaction_queue_active: false, + mutation_effects: ChildParserMutationEffects { + document_handle: child_document_handle, + reaction_queue_active: false, + }, } } @@ -95,44 +126,6 @@ impl<'a, 'scope, 'pin> ChildFrameLiveParserOwner<'a, 'scope, 'pin> { }) == self.document_owner } - - fn ensure_reaction_queue(&mut self) { - if !self.reaction_queue_active { - custom_elements::push_parser_custom_element_reaction_queue(self.host); - self.reaction_queue_active = true; - } - } - - fn apply_child_parser_mutation_effects(&mut self, effects: &DomMutationEffects) { - if !effects.did_change() { - return; - } - let host_ptr = self.host as *mut JsContextHost; - crate::observer_runtime::queue_mutation_records( - self.scope, - host_ptr, - self.host.dom_host(), - effects, - ); - for &root in effects.tree().disconnected_roots() { - custom_elements::enqueue_disconnected_callbacks_for_subtree(self.scope, host_ptr, root); - } - custom_elements::enqueue_connected_and_form_callbacks_for_already_upgraded_subtrees( - self.scope, - host_ptr, - effects.tree().connected_roots(), - ); - if custom_elements::form_owner_mutation_effects_touch_html_form( - self.host.dom_host(), - effects, - ) { - custom_elements::enqueue_form_association_callbacks_for_all(self.scope, host_ptr); - } - self.host - .sync_owner_style_sheet_texts_for_document_tree_scopes(self.child_document_handle); - self.host - .sync_child_browsing_context_subtree(self.scope, self.child_document_handle); - } } impl LiveDocumentParserOwner for ChildFrameLiveParserOwner<'_, '_, '_> {} @@ -190,12 +183,11 @@ impl ParserMutationEffectConsumer for ChildFrameLiveParserOwner<'_, '_, '_> { if !self.targets_current_document() { return; } - self.ensure_reaction_queue(); - self.apply_child_parser_mutation_effects(&effects); + apply_parser_mutation_effects(self.scope, self.host, &mut self.mutation_effects, &effects); } fn finish_parser_dom_mutations(&mut self) -> std::ops::ControlFlow<()> { - if std::mem::take(&mut self.reaction_queue_active) { + if std::mem::take(&mut self.mutation_effects.reaction_queue_active) { custom_elements::flush_parser_custom_element_reaction_queue(self.scope, self.host); } if !self.targets_current_document() @@ -337,9 +329,10 @@ impl ParserDomMutationConsumer for ChildFrameLiveParserOwner<'_, '_, '_> { // The tree sink invokes this queue after releasing its structural // borrow, before returning to the tree builder. Nested document.write // can then use the insertion point preceding this element's children. - self.ensure_reaction_queue(); + self.mutation_effects + .ensure_parser_reaction_queue(self.host); let effects = mutation.apply_to_dom_host(self.host.dom_host_mut()); - self.apply_child_parser_mutation_effects(&effects); + self.consume_parser_mutation_effects(effects); } fn create_parser_element_without_attributes( diff --git a/moli-renderer-v8/src/parser_mutation_effects.rs b/moli-renderer-v8/src/parser_mutation_effects.rs new file mode 100644 index 0000000000..9f011b62df --- /dev/null +++ b/moli-renderer-v8/src/parser_mutation_effects.rs @@ -0,0 +1,59 @@ +use crate::{ + custom_elements, dom::native::DomMutationEffects, mutation_coordinator::notify_dom_mutation, + native_bridge::JsContextHost, +}; + +/// Document-specific resource and lifecycle work surrounding the shared parser +/// notifications. Owners keep their runtime state; they do not choose which +/// observer or custom-element notifications a mutation produces. +pub(crate) trait ParserMutationEffectsOwner { + type Prepared; + + fn prepare_parser_mutation_effects(&mut self, effects: &DomMutationEffects) -> Self::Prepared; + + fn ensure_parser_reaction_queue(&mut self, host_ptr: *mut JsContextHost); + + fn finish_parser_mutation_effects( + &mut self, + scope: &mut v8::PinScope<'_, '_>, + host_ptr: *mut JsContextHost, + prepared: Self::Prepared, + ); +} + +pub(crate) fn apply_parser_mutation_effects( + scope: &mut v8::PinScope<'_, '_>, + host_ptr: *mut JsContextHost, + owner: &mut impl ParserMutationEffectsOwner, + effects: &DomMutationEffects, +) { + if !effects.did_change() { + return; + } + let prepared = owner.prepare_parser_mutation_effects(effects); + notify_dom_mutation(scope, host_ptr, unsafe { &*host_ptr }.dom_host(), effects); + + let form_owner_changed = custom_elements::form_owner_mutation_effects_touch_html_form( + unsafe { &*host_ptr }.dom_host(), + effects, + ); + let tree = effects.tree(); + if !tree.disconnected_roots().is_empty() + || !tree.connected_roots().is_empty() + || form_owner_changed + { + owner.ensure_parser_reaction_queue(host_ptr); + for &root in tree.disconnected_roots() { + custom_elements::enqueue_disconnected_callbacks_for_subtree(scope, host_ptr, root); + } + custom_elements::enqueue_connected_and_form_callbacks_for_already_upgraded_subtrees( + scope, + host_ptr, + tree.connected_roots(), + ); + if form_owner_changed { + custom_elements::enqueue_form_association_callbacks_for_all(scope, host_ptr); + } + } + owner.finish_parser_mutation_effects(scope, host_ptr, prepared); +} diff --git a/moli-renderer-v8/src/script_vm/tests/dom_elements/custom_elements/parser.rs b/moli-renderer-v8/src/script_vm/tests/dom_elements/custom_elements/parser.rs index 38081b0233..f70f21bbe6 100644 --- a/moli-renderer-v8/src/script_vm/tests/dom_elements/custom_elements/parser.rs +++ b/moli-renderer-v8/src/script_vm/tests/dom_elements/custom_elements/parser.rs @@ -1,5 +1,114 @@ use super::*; +#[test] +fn main_and_child_parser_mutation_notifications_preserve_order_and_identity() { + let mut vm = new_storage_test_vm("https://parser-mutation-parity.test/"); + let result = vm.eval(r#" + (() => { + function exercise(w) { + const d = w.document; + d.open(); d.write(''); + const log = [], records = []; + const collect = entries => records.push(...entries.map(r => [ + r.target.nodeName, Array.from(r.addedNodes, n => n.nodeName), r.removedNodes.length + ])); + const observer = new w.MutationObserver(collect); + observer.observe(d.body, {childList:true, subtree:true}); + class Probe extends w.HTMLElement { + static get observedAttributes() { return ['title']; } + constructor() { super(); log.push('construct'); } + attributeChangedCallback(name, oldValue, value) { log.push(name + ':' + value); } + connectedCallback() { log.push('connected:' + this.childNodes.length); } + } + let internals; + class Face extends w.HTMLElement { + static formAssociated = true; + constructor() { super(); internals = this.attachInternals(); } + connectedCallback() { log.push('face:' + (internals.form?.id || 'null')); } + formAssociatedCallback(form) { log.push('form:' + (form?.id || 'null')); } + } + w.customElements.define('x-mutation-probe', Probe); + w.customElements.define('x-mutation-face', Face); + d.write('text' + + '
'); + collect(observer.takeRecords()); + observer.disconnect(); + const nested = d.getElementById('nested'); + const result = {log, records, form:internals.form === d.getElementById('owner'), + childIdentity:nested.contentDocument === nested.contentWindow.document}; + d.close(); + return result; + } + const main = exercise(window); + const frame = document.createElement('iframe'); + document.body.appendChild(frame); + const child = exercise(frame.contentWindow); + frame.remove(); + return JSON.stringify([main, child]); + })() + "#).unwrap(); + let expected = serde_json::json!({ + "log": ["construct", "title:parsed", "connected:0", "face:null", "form:owner"], + "records": [ + ["BODY", ["X-MUTATION-PROBE"], 0], + ["X-MUTATION-PROBE", ["#text"], 0], + ["BODY", ["X-MUTATION-FACE"], 0], + ["BODY", ["FORM"], 0], + ["BODY", ["IFRAME"], 0] + ], + "form": true, + "childIdentity": true + }); + assert_eq!( + serde_json::from_str::(&result).unwrap(), + serde_json::json!([expected, expected]), + ); +} + +#[test] +fn main_and_child_parser_styles_are_current_in_reactions_and_preserve_cssom_edits() { + let mut vm = new_storage_test_vm("https://parser-style-mutation-parity.test/"); + let result = vm.eval(r#" + (() => { + function exercise(w) { + const d = w.document; + d.open(); d.write(''); + const first = d.getElementById('first').sheet; + first.insertRule('.kept { color: green; }', first.cssRules.length); + const log = []; + class Probe extends w.HTMLElement { + connectedCallback() { + log.push({color:w.getComputedStyle(this).color, + selectors:Array.from(d.getElementById('second').sheet.cssRules, r => r.selectorText)}); + } + } + w.customElements.define('x-style-probe', Probe); + d.write(''); + d.write('
unrelated insertion
'); + const result = {log, identity:first === d.getElementById('first').sheet, + firstSelectors:Array.from(first.cssRules, r => r.selectorText)}; + d.close(); + return result; + } + const main = exercise(window); + const frame = document.createElement('iframe'); + document.body.appendChild(frame); + const child = exercise(frame.contentWindow); + frame.remove(); + return JSON.stringify([main, child]); + })() + "#).unwrap(); + let expected = serde_json::json!({ + "log": [{"color": "rgb(1, 2, 3)", "selectors": ["x-style-probe"]}], + "identity": true, + "firstSelectors": [".first", ".kept"] + }); + assert_eq!( + serde_json::from_str::(&result).unwrap(), + serde_json::json!([expected, expected]), + ); +} + #[test] fn child_parser_write_constructs_elements_and_runs_reactions_synchronously() { let mut vm = new_storage_test_vm("https://child-parser-custom-elements.test/");