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