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/");