mirror of
https://github.com/lexmount/moli.git
synced 2026-10-06 00:00:56 +00:00
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
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<serde_json::Value> {
|
||||
let mut target = markup_url(server, markup);
|
||||
if child {
|
||||
target = markup_url(
|
||||
server,
|
||||
&format!(
|
||||
"<!doctype html><iframe id=target src=\"{}\"></iframe>",
|
||||
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#"<!doctype html><body><script>
|
||||
window.executed = [];
|
||||
window.preparationErrors = 0;
|
||||
addEventListener('error', () => preparationErrors++, true);
|
||||
new MutationObserver(records => {
|
||||
for (const record of records) {
|
||||
for (const node of record.addedNodes) {
|
||||
if (node.localName !== 'script' || !node.id) continue;
|
||||
if (node.id === 'promoted') node.type = 'text/javascript';
|
||||
else if (node.id === 'rewritten') node.text = "executed.push('rewritten')";
|
||||
else node.type = 'text/plain';
|
||||
}
|
||||
}
|
||||
}).observe(document.body, {childList: true, subtree: true});
|
||||
</script>
|
||||
<script id=classic>executed.push('classic')</script>
|
||||
<script id=module type=module>executed.push('module')</script>
|
||||
<script id=async async src="data:text/javascript,executed.push('async')"></script>
|
||||
<script id=defer defer src="data:text/javascript,executed.push('defer')"></script>
|
||||
<script id=asyncmodule type=module async
|
||||
src="data:text/javascript,executed.push('asyncmodule')"></script>
|
||||
<script id=invalid src=""></script>
|
||||
<script id=promoted type=text/plain>executed.push('promoted')</script>
|
||||
<script id=rewritten>executed.push('stale source')</script><p id=late>late</p>"#;
|
||||
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#"<!doctype html><body><script>
|
||||
window.staleScriptRan = false;
|
||||
new MutationObserver(records => {{
|
||||
for (const record of records) {{
|
||||
for (const node of record.addedNodes) {{
|
||||
if (node.localName !== 'script' || node.id !== 'probe') continue;
|
||||
document.write('<body><p id=replacement>replacement</p>');
|
||||
document.close();
|
||||
}}
|
||||
}}
|
||||
}}).observe(document.body, {{childList: true}});
|
||||
</script><p>old body</p>
|
||||
<script id=probe {attributes}>window.staleScriptRan = true;</script>
|
||||
<p id=late>old parser tail</p>"#
|
||||
);
|
||||
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#"<!doctype html><body><script>
|
||||
window.order = [];
|
||||
new MutationObserver(records => {
|
||||
for (const record of records) {
|
||||
for (const node of record.addedNodes) {
|
||||
if (node.localName !== 'script' || node.id !== 'nested') continue;
|
||||
order.push('observer');
|
||||
node.type = 'text/plain';
|
||||
}
|
||||
}
|
||||
}).observe(document.body, {childList: true});
|
||||
document.write('<script id=nested>order.push("nested")<' + '/script>');
|
||||
order.push('outer-end');
|
||||
</script><p id=late>late</p>"#;
|
||||
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(())
|
||||
}
|
||||
@@ -94,6 +94,22 @@ pub struct ParserFinishDiscoverySignals {
|
||||
pub discovered_blocking_stylesheet_inputs: Vec<DocumentOwnedBlockingStylesheetDiscoveryInput>,
|
||||
}
|
||||
|
||||
#[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<DocumentBlockingStylesheetSignature>,
|
||||
}
|
||||
|
||||
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<ParserScriptHandoff>),
|
||||
/// The runtime must release the parser borrow and perform the HTML parser
|
||||
/// microtask checkpoint before reading the script's preparation inputs.
|
||||
ScriptPreparation(Box<ParserScriptPreparationRequest>),
|
||||
CustomElementConstruction(Box<ParserCustomElementConstructionHandoff>),
|
||||
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<T>(
|
||||
&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<T>(
|
||||
&self,
|
||||
max_bytes: usize,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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<HashMap<NativeNodeId, usize>>,
|
||||
next_parser_script_position: Cell<usize>,
|
||||
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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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<RefCell<DocumentStream>>;
|
||||
type XmlDocumentParserStreamHandle = Rc<RefCell<XmlDocumentStream>>;
|
||||
|
||||
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<ParserScriptHandoff>),
|
||||
/// HTML script preparation must observe mutations from the parser's
|
||||
/// microtask checkpoint, after every parser borrow has been released.
|
||||
ScriptPreparation(Box<ParserScriptPreparationRequest>),
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<LiveDocumentParserStepOutcome> {
|
||||
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;
|
||||
|
||||
@@ -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<Url>,
|
||||
) -> 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,
|
||||
|
||||
@@ -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<Option<LiveDocumentParserStepOutcome>> {
|
||||
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),
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user