From 47a05b747bc8d6689ff0ced657e1c82d1828d8a2 Mon Sep 17 00:00:00 2001 From: ldm0 Date: Wed, 9 Sep 2026 20:20:40 +0800 Subject: [PATCH] fix(parser): checkpoint microtasks before preparing parser scripts Yield live HTML script preparation to the document owner after releasing parser borrows. Prepare from the current DOM after the guarded microtask checkpoint, and abandon stale handoffs when that checkpoint replaces the document. Apply the boundary to main documents, child documents, and document.write; preserve eager parsing when no runtime owner is installed. Admit parser async classic scripts after preparation and keep nested script cleanup from draining microtasks while an outer script is running. Add main/child integration coverage and update the two repaired WPT cases. Validation: - cargo fmt --all - cargo clippy --workspace --all-targets --all-features -- -D warnings - cargo nextest run --no-fail-fast: 17,846 passed, 13 skipped - 245 related WPT cases: all 231 previously passing cases remain passing - Final CLI build: both repaired WPT cases pass on the validated sources --- .../wpt-cross-current/failed-cases.txt | 2 - .../wpt-cross-current/passed-cases.txt | 2 + moli-core/tests/scripts.rs | 6 +- moli-core/tests/scripts/parser_preparation.rs | 170 ++++++++++++++++++ moli-parser/src/html.rs | 44 +++++ moli-parser/src/lib.rs | 3 +- moli-parser/src/live_target.rs | 10 +- moli-parser/src/stream.rs | 70 +++++++- .../src/context_bootstrap/trusted_types.rs | 5 +- .../src/document_runtime/document_write.rs | 33 ++++ .../document_write_scripts.rs | 4 +- moli-renderer-v8/src/live_document_parser.rs | 22 ++- .../src/live_document_parser/insertion.rs | 10 ++ .../child_documents/live_parser.rs | 64 +++++++ moli-renderer-v8/src/runtime/page_vm/mod.rs | 21 --- .../src/runtime/phase_one/parser_turn.rs | 160 +++++++++++------ moli-renderer-v8/src/script_cleanup.rs | 9 + moli-renderer-v8/src/script_vm.rs | 6 + 18 files changed, 548 insertions(+), 93 deletions(-) create mode 100644 moli-core/tests/scripts/parser_preparation.rs diff --git a/moli-benchmark/wpt-cross-current/failed-cases.txt b/moli-benchmark/wpt-cross-current/failed-cases.txt index 776ea98f7c..8f726f1ccf 100644 --- a/moli-benchmark/wpt-cross-current/failed-cases.txt +++ b/moli-benchmark/wpt-cross-current/failed-cases.txt @@ -2985,9 +2985,7 @@ html/syntax/parsing/cdata-in-integration-point-fragment.html html/webappapis/dynamic-markup-insertion/document-write/module-delayed.html html/webappapis/dynamic-markup-insertion/document-write/module-static-import-delayed.html html/webappapis/dynamic-markup-insertion/document-write/module-tla-delayed.html -html/webappapis/dynamic-markup-insertion/document-write/mutation-observer.html html/webappapis/dynamic-markup-insertion/document-write/write-active-document.html -html/webappapis/scripting/event-loops/microtask_before_prepare_the_script_element-01.html html/webappapis/scripting/processing-model-2/integration-with-the-javascript-job-queue/promise-job-detached-iframe.html html/webappapis/scripting/processing-model-2/integration-with-the-javascript-job-queue/promise-job-incumbent.html html/webappapis/scripting/processing-model-2/unhandled-promise-rejections/promise-rejection-event-during-parse.html diff --git a/moli-benchmark/wpt-cross-current/passed-cases.txt b/moli-benchmark/wpt-cross-current/passed-cases.txt index 3045b9cba2..4365627491 100644 --- a/moli-benchmark/wpt-cross-current/passed-cases.txt +++ b/moli-benchmark/wpt-cross-current/passed-cases.txt @@ -7027,6 +7027,7 @@ html/webappapis/dynamic-markup-insertion/document-write/module-tla-immediate-pro html/webappapis/dynamic-markup-insertion/document-write/module-tla-import.html html/webappapis/dynamic-markup-insertion/document-write/module-tla-promise.html html/webappapis/dynamic-markup-insertion/document-write/module.html +html/webappapis/dynamic-markup-insertion/document-write/mutation-observer.html html/webappapis/dynamic-markup-insertion/document-write/script_001.html html/webappapis/dynamic-markup-insertion/document-write/script_002.html html/webappapis/dynamic-markup-insertion/document-write/script_003.html @@ -7075,6 +7076,7 @@ html/webappapis/dynamic-markup-insertion/the-outerhtml-property/outerhtml-no-par html/webappapis/microtask-queuing/queue-microtask-cross-realm-callback-report-exception.html html/webappapis/scripting/event-loops/microtask_after_raf.html html/webappapis/scripting/event-loops/microtask_after_script.html +html/webappapis/scripting/event-loops/microtask_before_prepare_the_script_element-01.html html/webappapis/scripting/event-loops/task_microtask_ordering.html html/webappapis/scripting/event-loops/update-the-rendering-resize-autofocus.html html/webappapis/scripting/events/body-onload.html diff --git a/moli-core/tests/scripts.rs b/moli-core/tests/scripts.rs index 869179df6b..3bc0795fa7 100644 --- a/moli-core/tests/scripts.rs +++ b/moli-core/tests/scripts.rs @@ -28,15 +28,15 @@ mod child_readiness; #[path = "scripts/child_markup_insertion.rs"] mod child_markup_insertion; -#[path = "scripts/child_script_text.rs"] -mod child_script_text; - #[path = "scripts/module_document_write.rs"] mod module_document_write; #[path = "scripts/parser_cleanup.rs"] mod parser_cleanup; +#[path = "scripts/parser_preparation.rs"] +mod parser_preparation; + fn diagnostic_global<'a>( page: &'a moli_core::page::Page, name: &str, diff --git a/moli-core/tests/scripts/parser_preparation.rs b/moli-core/tests/scripts/parser_preparation.rs new file mode 100644 index 0000000000..ec0bbf6825 --- /dev/null +++ b/moli-core/tests/scripts/parser_preparation.rs @@ -0,0 +1,170 @@ +use super::*; + +fn markup_url(server: &FixtureServer, markup: &str) -> String { + let mut url = url::Url::parse(&server.url("/compat/child-dynamic-markup-document")).unwrap(); + url.query_pairs_mut().append_pair("markup", markup); + url.into() +} + +async fn observe_parser_document( + browser: &Browser, + server: &FixtureServer, + markup: &str, + child: bool, + expression: &str, +) -> Result { + let mut target = markup_url(server, markup); + if child { + target = markup_url( + server, + &format!( + "", + target.replace('&', "&") + ), + ); + } + let mut page = browser.fetch(&target).await?; + let result = page + .evaluate_runtime_expression_with_await_async( + &format!( + "JSON.stringify((() => {{ + const win = document.getElementById('target')?.contentWindow || window; + const doc = win.document; + return ({expression}); + }})())" + ), + true, + ) + .await?; + Ok(serde_json::from_str( + result["value"].as_str().expect("parser observation"), + )?) +} + +#[tokio::test(flavor = "multi_thread")] +async fn parser_preparation_observes_microtask_changes_to_type_and_source() -> Result<()> { + let server = FixtureServer::spawn().await?; + let browser = Browser::new(AppConfig::default())?; + let markup = r#" + + + + + + + +

late

"#; + for child in [false, true] { + let observed = observe_parser_document( + &browser, + &server, + markup, + child, + "{executed: win.executed, errors: win.preparationErrors, + late: !!doc.getElementById('late'), ready: doc.readyState}", + ) + .await?; + assert_eq!( + observed, + serde_json::json!({"executed": ["promoted", "rewritten"], "errors": 0, + "late": true, "ready": "complete"}), + "child={child}" + ); + } + server.shutdown().await; + Ok(()) +} + +#[tokio::test(flavor = "multi_thread")] +async fn parser_preparation_checkpoint_can_replace_the_document() -> Result<()> { + let server = FixtureServer::spawn().await?; + let browser = Browser::new(AppConfig::default())?; + for child in [false, true] { + for attributes in [ + "", + "type=module", + "async src=\"data:text/javascript,window.staleScriptRan=true\"", + "defer src=\"data:text/javascript,window.staleScriptRan=true\"", + ] { + let markup = format!( + r#"

old body

+ +

old parser tail

"# + ); + let observed = observe_parser_document( + &browser, + &server, + &markup, + child, + "{ran: win.staleScriptRan, text: doc.body.textContent, + late: !!doc.getElementById('late'), ready: doc.readyState}", + ) + .await?; + assert_eq!( + observed, + serde_json::json!({"ran": false, "text": "replacement", "late": false, + "ready": "complete"}), + "child={child}, attributes={attributes}" + ); + } + } + server.shutdown().await; + Ok(()) +} + +#[tokio::test(flavor = "multi_thread")] +async fn parser_preparation_does_not_checkpoint_inside_document_write_script() -> Result<()> { + let server = FixtureServer::spawn().await?; + let browser = Browser::new(AppConfig::default())?; + let markup = r#"

late

"#; + for child in [false, true] { + let observed = + observe_parser_document(&browser, &server, markup, child, "win.order").await?; + assert_eq!( + observed, + serde_json::json!(["nested", "outer-end", "observer"]), + "child={child}" + ); + } + server.shutdown().await; + Ok(()) +} diff --git a/moli-parser/src/html.rs b/moli-parser/src/html.rs index 7f85e69d78..e5ca5d4e93 100644 --- a/moli-parser/src/html.rs +++ b/moli-parser/src/html.rs @@ -94,6 +94,22 @@ pub struct ParserFinishDiscoverySignals { pub discovered_blocking_stylesheet_inputs: Vec, } +#[derive(Debug, Clone)] +pub struct ParserScriptPreparationRequest { + pub(super) node_id: NativeNodeId, + pub(super) start_line: u64, + pub(super) start_column: u64, + pub(super) position: usize, + pub(super) needs_microtask_checkpoint: bool, + pub(super) blocking_signatures_before: HashSet, +} + +impl ParserScriptPreparationRequest { + pub fn needs_microtask_checkpoint(&self) -> bool { + self.needs_microtask_checkpoint + } +} + #[derive(Debug, Clone)] pub enum ParserScriptHandoff { BlockingClassic { @@ -333,6 +349,9 @@ pub enum ParserYield { /// already holds the blocker; this yield transfers no new work. OwnerInterrupted, Script(Box), + /// The runtime must release the parser borrow and perform the HTML parser + /// microtask checkpoint before reading the script's preparation inputs. + ScriptPreparation(Box), CustomElementConstruction(Box), BlockingStylesheet(ParserBlockingStylesheetPause), } @@ -873,6 +892,31 @@ impl DocumentStream { self.pump_parser_step_with_runtime_dom_sinks(chunk, sinks) } + /// Live document owners can execute JavaScript at a script boundary. Keep + /// the offline parser's eager planning separate from that runtime boundary. + pub fn defer_script_preparation_to_owner(&mut self) { + self.inner.defer_script_preparation_to_owner(); + } + + pub fn prepare_script_with_runtime_dom_consumer( + &self, + request: ParserScriptPreparationRequest, + consumer: &mut T, + ) -> ParserScriptHandoff + where + T: ParserDomReadConsumer + + ParserDomMutationConsumer + + ParserMutationEffectConsumer + + ParserElementCreationConsumer, + { + // SAFETY: the owner remains borrowed until the guard clears the scoped + // callbacks. Preparation only reads the DOM; it never executes script. + let sinks = unsafe { ParserRuntimeDomSinks::from_consumer(consumer) }; + self.inner.enter_runtime_dom_sinks_parse_step(sinks); + let step = RuntimeDomSinksParserStep { stream: self }; + step.stream.inner.prepare_script(request) + } + pub fn pump_next_parser_step_with_runtime_dom_consumer( &self, max_bytes: usize, diff --git a/moli-parser/src/lib.rs b/moli-parser/src/lib.rs index 0d2675a753..4ec1853de9 100644 --- a/moli-parser/src/lib.rs +++ b/moli-parser/src/lib.rs @@ -28,7 +28,8 @@ pub use html::{ ParserCustomElementConstructionHandoff, ParserFinishDiscoverySignals, ParserInputContext, ParserInputQueue, ParserInputSession, ParserPumpOutcome, ParserPumpStep, ParserScriptElementStateTransition, ParserScriptHandoff, ParserScriptNoExecutionOutcome, - ParserScriptPreparationFailure, ParserStreamDocumentSnapshot, ParserYield, + ParserScriptPreparationFailure, ParserScriptPreparationRequest, ParserStreamDocumentSnapshot, + ParserYield, }; pub use live_target::{ ParserDomMutation, ParserDomMutationConsumer, ParserDomReadConsumer, diff --git a/moli-parser/src/live_target.rs b/moli-parser/src/live_target.rs index 4fd4dcbb59..bea9cf1e1b 100644 --- a/moli-parser/src/live_target.rs +++ b/moli-parser/src/live_target.rs @@ -2062,6 +2062,10 @@ impl ParserStreamHtmlTreeSinkTarget { Some(move || sink.finish()) } + pub(super) fn has_runtime_dom_consumer(&self) -> bool { + self.runtime_dom_sinks.is_some() + } + pub(super) fn new_with_declarative_shadow_roots_and_scripting( final_url: Url, allow_declarative_shadow_roots: bool, @@ -2295,7 +2299,11 @@ impl ParserStreamHtmlTreeSinkTarget { } } - fn read_is_html_element_named(&self, node_id: NativeNodeId, local_name: &str) -> bool { + pub(super) fn read_is_html_element_named( + &self, + node_id: NativeNodeId, + local_name: &str, + ) -> bool { if let Some(owner) = &self.runtime_dom_sinks { owner .dom_read_sink() diff --git a/moli-parser/src/stream.rs b/moli-parser/src/stream.rs index ced5d427b6..94b9ab97da 100644 --- a/moli-parser/src/stream.rs +++ b/moli-parser/src/stream.rs @@ -25,7 +25,7 @@ use super::{ ParserBlockingStylesheetPause, ParserFinishDiscoverySignals, ParserInputQueue, ParserInputSession, ParserPumpOutcome, ParserPumpStep, ParserScriptElementStateTransition, ParserScriptHandoff, ParserScriptNoExecutionOutcome, ParserScriptPreparationFailure, - ParserYield, + ParserScriptPreparationRequest, ParserYield, }, live_target::{ParserRuntimeDomSinks, ParserStreamHtmlTreeSinkTarget}, session::{ @@ -39,6 +39,7 @@ pub(super) struct HtmlTreeSinkStream { script_input: ParserInputQueue, parser_script_positions: RefCell>, next_parser_script_position: Cell, + defer_script_preparation: bool, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -437,9 +438,24 @@ impl HtmlTreeSinkStream { script_input: session.script_input, parser_script_positions: RefCell::default(), next_parser_script_position: Cell::new(0), + defer_script_preparation: false, } } + pub fn defer_script_preparation_to_owner(&mut self) { + self.defer_script_preparation = true; + } + + pub fn prepare_script(&self, request: ParserScriptPreparationRequest) -> ParserScriptHandoff { + let target = self.parser.sink().borrow_target(); + prepare_parser_script(&*target, request.node_id, Some(request.position)).into_handoff( + request.node_id, + request.start_line, + request.start_column, + request.blocking_signatures_before, + ) + } + pub(super) fn from_target_with_scripting( target: ParserStreamHtmlTreeSinkTarget, scripting_enabled: bool, @@ -600,6 +616,56 @@ impl HtmlTreeSinkStream { | RawParserStep::InputDrained => None, }; + let defer_script_preparation = self.defer_script_preparation + && self + .parser + .sink() + .borrow_target() + .has_runtime_dom_consumer(); + if defer_script_preparation { + // A speculative fetch is distinct from preparing a parser script. + // The live owner admits async execution only after its checkpoint; + // ordinary preload discovery continues on the separate preload lane. + let result = match result { + RawParserStep::Script(node_id) => { + let target = self.parser.sink().borrow_target(); + let (start_line, start_column) = + target.script_start_position(node_id).unwrap_or((0, 0)); + ParserPumpStep::Yield(ParserYield::ScriptPreparation(Box::new( + ParserScriptPreparationRequest { + node_id, + start_line, + start_column, + position: handoff_parser_position.expect("script has parser position"), + needs_microtask_checkpoint: target + .read_is_html_element_named(node_id, "script"), + blocking_signatures_before: captured_blocking_stylesheet_signatures, + }, + ))) + } + RawParserStep::BlockingStylesheet(node_id) => { + assert_eq!(self.pop_pending_blocking_stylesheet_pause(), Some(node_id)); + ParserPumpStep::Yield(ParserYield::BlockingStylesheet( + ParserBlockingStylesheetPause { node_id }, + )) + } + RawParserStep::CustomElementConstruction => { + ParserPumpStep::Yield(ParserYield::CustomElementConstruction(Box::new( + self.pop_pending_custom_element_construction_handoff() + .expect("custom element boundary has a construction handoff"), + ))) + } + RawParserStep::InputDrained => ParserPumpStep::InputDrained, + }; + return ParserPumpOutcome { + result, + discovered_async_prefetch_scripts: Vec::new(), + discovered_modulepreload_link_candidates: + discovered_modulepreload_link_candidate_node_ids, + discovered_blocking_stylesheet_inputs, + }; + } + let ( result, discovered_async_prefetch_scripts, @@ -710,6 +776,7 @@ impl HtmlTreeSinkStream { script_input: _, parser_script_positions: _, next_parser_script_position: _, + defer_script_preparation: _, } = self; parser.finish_live_runtime_dom_sink_parser() } @@ -868,6 +935,7 @@ impl HtmlTreeSinkStream { script_input: _, parser_script_positions: _, next_parser_script_position: _, + defer_script_preparation: _, } = self; let mut target = parser.finish(); let signals = ParserFinishDiscoverySignals { diff --git a/moli-renderer-v8/src/context_bootstrap/trusted_types.rs b/moli-renderer-v8/src/context_bootstrap/trusted_types.rs index 4e8e4de4e5..aa89539fbd 100644 --- a/moli-renderer-v8/src/context_bootstrap/trusted_types.rs +++ b/moli-renderer-v8/src/context_bootstrap/trusted_types.rs @@ -873,9 +873,8 @@ fn trusted_type_to_string_callback<'s>( mut rv: v8::ReturnValue<'_, v8::Value>, ) { let this = args.this(); - if !trusted_type_kind(scope, this.into()).is_some_and(|kind| { - v8str(scope, kind.constructor_name()).strict_equals(args.data()) - }) + if !trusted_type_kind(scope, this.into()) + .is_some_and(|kind| v8str(scope, kind.constructor_name()).strict_equals(args.data())) { throw_type_error(scope, "Illegal invocation"); return; diff --git a/moli-renderer-v8/src/document_runtime/document_write.rs b/moli-renderer-v8/src/document_runtime/document_write.rs index ea42f2e0b3..75da41a7e2 100644 --- a/moli-renderer-v8/src/document_runtime/document_write.rs +++ b/moli-renderer-v8/src/document_runtime/document_write.rs @@ -2429,7 +2429,40 @@ impl DocumentRuntime { completed_stylesheet_clients, ); + let result = if let ParserPumpStep::Yield(ParserYield::ScriptPreparation(request)) = + result + { + let owner = unsafe { &*host_ptr }.current_main_document_task_owner(); + if request.needs_microtask_checkpoint() + && let Err(error) = + crate::script_cleanup::perform_parser_script_preparation_checkpoint(scope) + { + tracing::warn!(%error, "document.write parser preparation checkpoint failed"); + } + if unsafe { &*host_ptr }.current_main_document_task_owner() != owner { + return true; + } + let handoff = insertion_controller.with_parser_stream(|stream| { + self.with_dom_host_parse_step(|runtime| { + let mut mutation_owner = DocumentWriteParserMutationOwner { + runtime, + scope, + host_ptr, + target: DocumentWriteParserMutationTarget::LiveDocument, + }; + stream + .prepare_script_with_runtime_dom_consumer(*request, &mut mutation_owner) + }) + }); + ParserPumpStep::Yield(ParserYield::Script(Box::new(handoff))) + } else { + result + }; + match result { + ParserPumpStep::Yield(ParserYield::ScriptPreparation(_)) => { + unreachable!("document.write parser preparation was resolved before dispatch") + } ParserPumpStep::InputDrained | ParserPumpStep::Yield(ParserYield::OwnerInterrupted) => { return true; diff --git a/moli-renderer-v8/src/document_runtime/script_scheduling/document_write_scripts.rs b/moli-renderer-v8/src/document_runtime/script_scheduling/document_write_scripts.rs index 59d8f110f2..00d0738cb0 100644 --- a/moli-renderer-v8/src/document_runtime/script_scheduling/document_write_scripts.rs +++ b/moli-renderer-v8/src/document_runtime/script_scheduling/document_write_scripts.rs @@ -20,7 +20,9 @@ pub(in crate::document_runtime) enum DocumentWriteCurrentScriptEventBehavior { } fn perform_document_write_microtask_checkpoints(scope: &mut v8::PinScope<'_, '_>) { - perform_microtask_checkpoint_and_report_pending_promise_rejections(scope); + if crate::script_cleanup::can_perform_script_cleanup_checkpoint(scope) { + perform_microtask_checkpoint_and_report_pending_promise_rejections(scope); + } } impl DocumentRuntime { diff --git a/moli-renderer-v8/src/live_document_parser.rs b/moli-renderer-v8/src/live_document_parser.rs index 9eacbd7037..6bb20099a5 100644 --- a/moli-renderer-v8/src/live_document_parser.rs +++ b/moli-renderer-v8/src/live_document_parser.rs @@ -5,7 +5,8 @@ use crate::{ DocumentStream, HtmlParser, ParserBlockingStylesheetPause, ParserCustomElementConstructionHandoff, ParserDomMutationConsumer, ParserDomReadConsumer, ParserElementCreationConsumer, ParserMutationEffectConsumer, ParserPumpOutcome, - ParserPumpStep, ParserScriptHandoff, ParserYield, PreparedScript, XmlDocumentStream, + ParserPumpStep, ParserScriptHandoff, ParserScriptPreparationRequest, ParserYield, + PreparedScript, XmlDocumentStream, }, }; use std::{ @@ -21,7 +22,8 @@ pub(crate) use insertion::ParserInsertionHandle; pub(crate) type DocumentParserStreamHandle = Rc>; type XmlDocumentParserStreamHandle = Rc>; -fn new_document_parser_stream_handle(stream: DocumentStream) -> DocumentParserStreamHandle { +fn new_document_parser_stream_handle(mut stream: DocumentStream) -> DocumentParserStreamHandle { + stream.defer_script_preparation_to_owner(); Rc::new(RefCell::new(stream)) } @@ -64,6 +66,9 @@ pub(crate) enum LiveDocumentParserStepOutcome { /// The tree builder reached a parser-connected script boundary. The owner /// decides whether this executes immediately or blocks on source/resources. ScriptHandoff(Box), + /// HTML script preparation must observe mutations from the parser's + /// microtask checkpoint, after every parser borrow has been released. + ScriptPreparation(Box), } struct LiveDocumentParserStepAdvance { @@ -167,6 +172,9 @@ fn live_document_parser_advance_from_outcome( ParserPumpStep::Yield(ParserYield::Script(handoff)) => { LiveDocumentParserStepOutcome::ScriptHandoff(handoff) } + ParserPumpStep::Yield(ParserYield::ScriptPreparation(request)) => { + LiveDocumentParserStepOutcome::ScriptPreparation(request) + } }; LiveDocumentParserStepAdvance { outcome, @@ -837,6 +845,16 @@ impl DocumentParserSession { } } + pub(crate) fn prepare_script( + &self, + request: ParserScriptPreparationRequest, + owner: &mut impl LiveDocumentParserOwner, + ) -> ParserScriptHandoff { + self.stream_handle() + .borrow() + .prepare_script_with_runtime_dom_consumer(request, owner) + } + pub(crate) fn lifetime(&self) -> DocumentParserLifetime { self.control.lifetime() } diff --git a/moli-renderer-v8/src/live_document_parser/insertion.rs b/moli-renderer-v8/src/live_document_parser/insertion.rs index 7bcffdaa1d..9f07e94444 100644 --- a/moli-renderer-v8/src/live_document_parser/insertion.rs +++ b/moli-renderer-v8/src/live_document_parser/insertion.rs @@ -88,4 +88,14 @@ impl ParserInsertionHandle { pub(crate) fn take_discovery_signals(&self) -> LiveDocumentParserDiscoverySignals { std::mem::take(&mut *self.discovery_signals.borrow_mut()) } + + pub(crate) fn prepare_script( + &self, + request: super::ParserScriptPreparationRequest, + owner: &mut impl LiveDocumentParserOwner, + ) -> super::ParserScriptHandoff { + self.controller.with_parser_stream(|stream| { + stream.prepare_script_with_runtime_dom_consumer(request, owner) + }) + } } 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 03d57c27d6..5f02183119 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 @@ -611,6 +611,44 @@ impl JsContextHost { self.child_browsing_context_host_for_document_handle(document_handle) == Some(child_handle) } + fn resolve_live_child_parser_script_preparation( + &mut self, + scope: &mut v8::PinScope<'_, '_>, + child_handle: DomHandle, + document_handle: DomHandle, + prepare_script: impl FnOnce( + crate::parser::ParserScriptPreparationRequest, + &mut ChildFrameLiveParserOwner<'_, '_, '_>, + ) -> ParserScriptHandoff, + outcome: LiveDocumentParserStepOutcome, + ) -> Option { + let LiveDocumentParserStepOutcome::ScriptPreparation(request) = outcome else { + return Some(outcome); + }; + let document_owner = self + .frame_owner_store + .current_child_document_owner(child_handle); + if request.needs_microtask_checkpoint() + && let Err(error) = + crate::script_cleanup::perform_parser_script_preparation_checkpoint(scope) + { + tracing::warn!(%error, "child parser preparation checkpoint failed"); + } + if self + .frame_owner_store + .current_child_document_owner(child_handle) + != document_owner + || !self.live_child_parser_document_is_current(child_handle, document_handle) + { + return None; + } + let mut owner = ChildFrameLiveParserOwner::new(self, scope, document_handle); + let handoff = prepare_script(*request, &mut owner); + Some(LiveDocumentParserStepOutcome::ScriptHandoff(Box::new( + handoff, + ))) + } + fn recover_current_child_parser_script_admission_failure( &mut self, child_handle: DomHandle, @@ -699,7 +737,20 @@ impl JsContextHost { _ => ParserProgress::BlockedOnParserScript { ready_work: None }, }; } + let Some(outcome) = self.resolve_live_child_parser_script_preparation( + scope, + child_handle, + document_handle, + |request, owner| parser.prepare_script(request, owner), + outcome, + ) else { + parser.stop(ParserStopReason::DocumentReplacement); + return ParserProgress::Stopped; + }; match outcome { + LiveDocumentParserStepOutcome::ScriptPreparation(_) => { + unreachable!("child parser preparation was resolved before dispatch") + } LiveDocumentParserStepOutcome::InputBoundary => { if parser.input_is_empty() { return if parser.finishes_on_empty_input() { @@ -1346,7 +1397,20 @@ impl JsContextHost { if insertion.is_suspended() { return true; } + let Some(outcome) = self.resolve_live_child_parser_script_preparation( + scope, + child_handle, + document_handle, + |request, owner| insertion.prepare_script(request, owner), + outcome, + ) else { + insertion.stop(ParserStopReason::DocumentReplacement); + return false; + }; match outcome { + LiveDocumentParserStepOutcome::ScriptPreparation(_) => { + unreachable!("child parser preparation was resolved before dispatch") + } LiveDocumentParserStepOutcome::InputBoundary => { if parser_insertion_only { return true; diff --git a/moli-renderer-v8/src/runtime/page_vm/mod.rs b/moli-renderer-v8/src/runtime/page_vm/mod.rs index bafbbadf93..aeb6f902df 100644 --- a/moli-renderer-v8/src/runtime/page_vm/mod.rs +++ b/moli-renderer-v8/src/runtime/page_vm/mod.rs @@ -2412,27 +2412,6 @@ impl PageVm { .await } - pub(super) async fn perform_script_task_checkpoint_on_named_owner_local_task( - &mut self, - script_url: Option, - ) -> Result<()> { - let local_executor = self.local_executor.clone(); - let mut page_vm_ref = AwaitedOwnerLocalPageVm::new(self); - run_named_owner_local_task( - local_executor, - "phase-one script-task checkpoint local task channel closed", - async move { - let page_vm = page_vm_ref.get_mut(); - page_vm - .vm_mut() - .perform_script_task_checkpoint(script_url.as_ref())?; - page_vm.absorb_parser_no_execution_runs(); - Ok(()) - }, - ) - .await - } - pub(super) async fn construct_parser_custom_element_handoff_on_named_owner_local_task( &mut self, handoff: crate::parser::ParserCustomElementConstructionHandoff, diff --git a/moli-renderer-v8/src/runtime/phase_one/parser_turn.rs b/moli-renderer-v8/src/runtime/phase_one/parser_turn.rs index 71ef6bdfd9..96faad6856 100644 --- a/moli-renderer-v8/src/runtime/phase_one/parser_turn.rs +++ b/moli-renderer-v8/src/runtime/phase_one/parser_turn.rs @@ -1236,18 +1236,6 @@ impl<'loader, 'state> ParserDriver<'loader, 'state> { start_column, outcome, } => { - // HTML's parser script processing performs a microtask - // checkpoint before PrepareScript, including for data blocks - // and other non-executable script elements. The parser crate - // has already classified the element, but classification does - // not run JavaScript, so this is the equivalent observable - // boundary on the renderer owner lane. - page_vm - .perform_script_task_checkpoint_on_named_owner_local_task(None) - .await?; - if page_vm.vm().current_main_document_task_owner() != Some(parser_document_owner) { - return Ok(ScriptHandoffOutcome::StoppedCurrentDocument); - } crate::host::apply_parser_script_element_state_transition( page_vm.vm_mut().document_runtime.dom_host_mut(), handle, @@ -1279,12 +1267,6 @@ impl<'loader, 'state> ParserDriver<'loader, 'state> { start_column, failure, } => { - page_vm - .perform_script_task_checkpoint_on_named_owner_local_task(None) - .await?; - if page_vm.vm().current_main_document_task_owner() != Some(parser_document_owner) { - return Ok(ScriptHandoffOutcome::StoppedCurrentDocument); - } crate::host::apply_parser_script_element_state_transition( page_vm.vm_mut().document_runtime.dom_host_mut(), handle, @@ -1345,12 +1327,15 @@ impl<'loader, 'state> ParserDriver<'loader, 'state> { &mut parser_owner, ) }); - self.finish_parser_pump_step( + let outcome = self.finish_parser_pump_step( page_vm, parser_document_owner, outcome, null_custom_element_registry_elements, - ) + ); + self.resolve_parser_script_preparation(page_vm, parser_document_owner, outcome) + .expect("parser preparation checkpoint") + .unwrap_or(LiveDocumentParserStepOutcome::InputBoundary) } fn pump_next_parse_step_with_signals( @@ -1518,44 +1503,8 @@ impl<'loader, 'state> ParserDriver<'loader, 'state> { .vm_mut() .accept_parser_discovered_native_modulepreloads(modulepreload_link_candidates); if page_vm.main_document_scripting_enabled() { - for mut script in async_prefetch_scripts { - bind_parser_owned_script_handle(page_vm, &mut script); - self.buffered_document_preloads - .claim_pending_script_preload_for_parser(&script); - let shared_preload = self - .buffered_document_preloads - .shared_preload_for_script(&script); - let document_character_set = page_vm - .vm() - .document_runtime - .document_character_set() - .to_owned(); - let resource_task_runner = page_vm.resource_task_runner(); - let _ = self.scheduler.accept_parser_discovered_async_candidate( - script, - self.loader, - page_vm.vm().current_main_document_resource_loader().expect("parser script requires its Document authority").fetch_context().request_origin(), - resource_task_runner, - shared_preload, - Some(&document_character_set), - |script| { - let binding = page_vm - .vm_mut() - .accept_main_document_script_load_delay_binding( - parser_document_owner, - crate::frame_owner_model::MainDocumentScriptLoadDelayKind::Classic, - ) - .expect("current parser async discovery must bind lifecycle ownership"); - tracing::debug!( - ?parser_document_owner, - script_node_id = ?script.node_id, - script_url = %script.url, - load_delay_token = ?binding.load_delay_token(), - "accepted main parser async classic lifecycle binding before source work" - ); - binding - }, - ); + for script in async_prefetch_scripts { + self.admit_prepared_parser_async_script(page_vm, parser_document_owner, script); } } page_vm @@ -1566,6 +1515,56 @@ impl<'loader, 'state> ParserDriver<'loader, 'state> { ); } + fn admit_prepared_parser_async_script( + &mut self, + page_vm: &mut PageVm, + parser_document_owner: crate::frame_owner_model::FrameDocumentTaskOwner, + mut script: PreparedScript, + ) { + bind_parser_owned_script_handle(page_vm, &mut script); + self.buffered_document_preloads + .claim_pending_script_preload_for_parser(&script); + let shared_preload = self + .buffered_document_preloads + .shared_preload_for_script(&script); + let document_character_set = page_vm + .vm() + .document_runtime + .document_character_set() + .to_owned(); + let resource_task_runner = page_vm.resource_task_runner(); + let _ = self.scheduler.accept_parser_discovered_async_candidate( + script, + self.loader, + page_vm + .vm() + .current_main_document_resource_loader() + .expect("parser script requires its Document authority") + .fetch_context() + .request_origin(), + resource_task_runner, + shared_preload, + Some(&document_character_set), + |script| { + let binding = page_vm + .vm_mut() + .accept_main_document_script_load_delay_binding( + parser_document_owner, + crate::frame_owner_model::MainDocumentScriptLoadDelayKind::Classic, + ) + .expect("current parser async discovery must bind lifecycle ownership"); + tracing::debug!( + ?parser_document_owner, + script_node_id = ?script.node_id, + script_url = %script.url, + load_delay_token = ?binding.load_delay_token(), + "accepted main parser async classic lifecycle binding before source work" + ); + binding + }, + ); + } + async fn advance_next_parser_step_for_owner( &mut self, page_vm: &mut PageVm, @@ -1599,7 +1598,16 @@ impl<'loader, 'state> ParserDriver<'loader, 'state> { break ParserStepAdvanceOutcome::StoppedCurrentDocument; } + let Some(outcome) = + self.resolve_parser_script_preparation(page_vm, parser_document_owner, outcome)? + else { + break ParserStepAdvanceOutcome::StoppedCurrentDocument; + }; + match outcome { + LiveDocumentParserStepOutcome::ScriptPreparation(_) => { + unreachable!("parser preparation was resolved before dispatch") + } LiveDocumentParserStepOutcome::InputBoundary => { break ParserStepAdvanceOutcome::Continue; } @@ -1710,6 +1718,9 @@ impl<'loader, 'state> ParserDriver<'loader, 'state> { } match outcome { + LiveDocumentParserStepOutcome::ScriptPreparation(_) => { + unreachable!("test parser pump resolves preparation before dispatch") + } LiveDocumentParserStepOutcome::InputBoundary => { break ParserStepAdvanceOutcome::Continue; } @@ -1795,4 +1806,37 @@ impl<'loader, 'state> ParserDriver<'loader, 'state> { | None => None, } } + + fn resolve_parser_script_preparation( + &mut self, + page_vm: &mut PageVm, + parser_document_owner: crate::frame_owner_model::FrameDocumentTaskOwner, + outcome: LiveDocumentParserStepOutcome, + ) -> Result> { + let LiveDocumentParserStepOutcome::ScriptPreparation(request) = outcome else { + return Ok(Some(outcome)); + }; + if request.needs_microtask_checkpoint() { + page_vm + .vm_mut() + .perform_parser_script_preparation_checkpoint()?; + page_vm.absorb_parser_no_execution_runs(); + } + if page_vm.vm().current_main_document_task_owner() != Some(parser_document_owner) { + return Ok(None); + } + let handoff = page_vm.vm_mut().with_dom_host_parse_step(|vm| { + let mut owner = PhaseOneParserOwner { vm }; + self.parser_session.prepare_script(*request, &mut owner) + }); + if page_vm.main_document_scripting_enabled() + && let ParserScriptHandoff::AsyncPostParse { script, .. } = &handoff + && script.kind == crate::types::ScriptKind::Classic + { + self.admit_prepared_parser_async_script(page_vm, parser_document_owner, script.clone()); + } + Ok(Some(LiveDocumentParserStepOutcome::ScriptHandoff( + Box::new(handoff), + ))) + } } diff --git a/moli-renderer-v8/src/script_cleanup.rs b/moli-renderer-v8/src/script_cleanup.rs index bcf7a39bd9..e230114fac 100644 --- a/moli-renderer-v8/src/script_cleanup.rs +++ b/moli-renderer-v8/src/script_cleanup.rs @@ -97,3 +97,12 @@ pub(crate) fn perform_callback_cleanup_checkpoint(scope: &mut v8::PinScope<'_, ' tracing::warn!(%error, "callback cleanup microtask checkpoint failed"); } } + +pub(crate) fn perform_parser_script_preparation_checkpoint( + scope: &mut v8::PinScope<'_, '_>, +) -> anyhow::Result<()> { + if can_perform_script_cleanup_checkpoint(scope) { + crate::script_vm::ScriptVm::perform_microtask_checkpoints(scope, None)?; + } + Ok(()) +} diff --git a/moli-renderer-v8/src/script_vm.rs b/moli-renderer-v8/src/script_vm.rs index 922bf6f12e..6982f3668f 100644 --- a/moli-renderer-v8/src/script_vm.rs +++ b/moli-renderer-v8/src/script_vm.rs @@ -6461,6 +6461,12 @@ impl ScriptVm { false } + pub(crate) fn perform_parser_script_preparation_checkpoint(&mut self) -> Result<()> { + self.with_default_context_scope(|scope, _| { + crate::script_cleanup::perform_parser_script_preparation_checkpoint(scope) + }) + } + /// Run one explicit page-task microtask checkpoint before a queued script task. /// /// This does not make the runtime a full browser task queue yet. The point is