fix(html): queue document lifecycle transitions on the DOM source

Admit DOMContentLoaded and main Window load after their parser/defer and load
prerequisites so earlier DOM tasks keep their FIFO positions. Keep interactive
readiness at parser stop, preserve child realm prerequisites, and return a load
boundary to its driver if an earlier DOM callback adds a new load delay.

Queue parser-owned external import-map errors during preparation, including
child and document.write parsers, so later dynamic errors cannot overtake them.
Cover ordering, replacement and load-delay behavior in main and child documents.

Validation:
- cargo fmt --all
- cargo clippy --workspace --all-targets --all-features -- -D warnings
- cargo nextest run --no-fail-fast: 17,871 passed, 13 skipped
- 270 WPT cases: 254 pass, 10 fail, 5 timeout, 1 error; one new pass and
  no case-status or passing-subtest-count regressions
This commit is contained in:
ldm0
2026-09-27 06:43:21 +08:00
parent 38e4ea85db
commit 0cc94c4915
46 changed files with 1349 additions and 457 deletions
+3
View File
@@ -37,6 +37,9 @@ mod parser_cleanup;
#[path = "scripts/parser_preparation.rs"]
mod parser_preparation;
#[path = "scripts/lifecycle_task_order.rs"]
mod lifecycle_task_order;
fn diagnostic_global<'a>(
page: &'a moli_core::page::Page,
name: &str,
@@ -0,0 +1,230 @@
use super::*;
async fn parser_rejection_notification_precedes_lifecycle_events(child: bool) -> Result<()> {
let markup = r#"<!doctype html><body><script>
window.events = [];
document.addEventListener('readystatechange', () => events.push(document.readyState));
document.addEventListener('DOMContentLoaded', () => events.push('DOMContentLoaded'));
addEventListener('load', () => events.push('load'));
addEventListener('unhandledrejection', event => {
events.push(event.reason);
event.preventDefault();
});
Promise.reject('rejection');
</script><p id=tail>parser tail</p>"#;
let result = observe_lifecycle_markup(child, markup).await?;
assert_eq!(
result["value"].as_str(),
Some(r#"["interactive","rejection","DOMContentLoaded","complete","load"]"#),
"child={child}"
);
Ok(())
}
async fn observe_lifecycle_markup(child: bool, markup: &str) -> Result<serde_json::Value> {
let server = FixtureServer::spawn().await?;
let browser = Browser::new(AppConfig::default())?;
let mut target = url::Url::parse(&server.url("/compat/child-dynamic-markup-document"))?;
target.query_pairs_mut().append_pair("markup", markup);
if child {
let parent = format!(
"<!doctype html><iframe id=target src=\"{}\"></iframe>",
target.as_str().replace('&', "&amp;")
);
target.set_query(None);
target.query_pairs_mut().append_pair("markup", &parent);
}
let mut page = browser.fetch(target.as_str()).await?;
let result = page
.evaluate_runtime_expression_with_await_async(
"JSON.stringify((document.getElementById('target')?.contentWindow || window).events)",
true,
)
.await?;
server.shutdown().await;
Ok(result)
}
#[tokio::test(flavor = "multi_thread")]
async fn main_parser_rejection_notification_precedes_lifecycle_events() -> Result<()> {
parser_rejection_notification_precedes_lifecycle_events(false).await
}
#[tokio::test(flavor = "multi_thread")]
async fn child_parser_rejection_notification_precedes_lifecycle_events() -> Result<()> {
parser_rejection_notification_precedes_lifecycle_events(true).await
}
async fn deferred_rejection_keeps_dom_fifo_position(child: bool, module: bool) -> Result<()> {
let attribute = if module { "type=module" } else { "defer" };
let markup = format!(
r#"<!doctype html><body><script>
window.events = [];
document.addEventListener('readystatechange', () => events.push(document.readyState));
document.addEventListener('DOMContentLoaded', () => events.push('DOMContentLoaded'));
addEventListener('load', () => events.push('load'));
addEventListener('unhandledrejection', event => {{
event.preventDefault();
events.push(event.reason);
const later = document.createElement('script');
later.src = '';
later.onerror = () => events.push('later DOM task');
document.body.append(later);
}});
</script><script {attribute} src="data:text/javascript,Promise.reject('rejection')"></script>
<p id=tail>parser tail</p>"#
);
let result = observe_lifecycle_markup(child, &markup).await?;
let events: Vec<String> = serde_json::from_str(result["value"].as_str().unwrap())?;
let at = |name: &str| events.iter().position(|event| event == name).unwrap();
assert!(at("interactive") < at("rejection"), "{events:?}");
assert!(at("rejection") < at("DOMContentLoaded"), "{events:?}");
assert!(at("DOMContentLoaded") < at("later DOM task"), "{events:?}");
assert!(at("rejection") < at("complete"), "{events:?}");
assert!(at("complete") < at("load"), "{events:?}");
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn main_defer_dom_tasks_keep_their_positions_around_domcontentloaded() -> Result<()> {
deferred_rejection_keeps_dom_fifo_position(false, false).await
}
#[tokio::test(flavor = "multi_thread")]
async fn child_defer_dom_tasks_keep_their_positions_around_domcontentloaded() -> Result<()> {
deferred_rejection_keeps_dom_fifo_position(true, false).await
}
#[tokio::test(flavor = "multi_thread")]
async fn main_module_dom_tasks_keep_their_positions_around_domcontentloaded() -> Result<()> {
deferred_rejection_keeps_dom_fifo_position(false, true).await
}
#[tokio::test(flavor = "multi_thread")]
async fn child_module_dom_tasks_keep_their_positions_around_domcontentloaded() -> Result<()> {
deferred_rejection_keeps_dom_fifo_position(true, true).await
}
async fn earlier_dom_task_replaces_document_before_queued_dcl(child: bool) -> Result<()> {
let markup = r#"<!doctype html><body><script>
window.events = [];
document.addEventListener('DOMContentLoaded', () => events.push('old DCL'));
addEventListener('unhandledrejection', event => {
event.preventDefault();
document.open();
document.write("<!doctype html><script>document.addEventListener('DOMContentLoaded', () => events.push('new DCL'))<\/script><p>replacement</p>");
document.close();
}, {once: true});
Promise.reject('replace');
</script><p>old parser tail</p>"#;
let result = observe_lifecycle_markup(child, markup).await?;
assert_eq!(result["value"].as_str(), Some(r#"["new DCL"]"#));
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn main_earlier_dom_task_retires_queued_domcontentloaded() -> Result<()> {
earlier_dom_task_replaces_document_before_queued_dcl(false).await
}
#[tokio::test(flavor = "multi_thread")]
async fn child_earlier_dom_task_retires_queued_domcontentloaded() -> Result<()> {
earlier_dom_task_replaces_document_before_queued_dcl(true).await
}
async fn dom_callback_before_load_does_not_lose_load(child: bool) -> Result<()> {
let markup = r#"<!doctype html><body><script>
window.events = [];
document.addEventListener('DOMContentLoaded', () => {
events.push('DOMContentLoaded');
Promise.reject('before load');
});
addEventListener('unhandledrejection', event => {
event.preventDefault();
events.push('rejection');
const script = document.createElement('script');
script.async = true;
script.src = 'data:text/javascript,' + encodeURIComponent("events.push('async')");
document.body.append(script);
});
addEventListener('load', () => events.push('load'));
</script><p>parser tail</p>"#;
let result = observe_lifecycle_markup(child, markup).await?;
let events: Vec<String> = serde_json::from_str(result["value"].as_str().unwrap())?;
for expected in ["DOMContentLoaded", "rejection", "async", "load"] {
assert_eq!(
events
.iter()
.filter(|event| event.as_str() == expected)
.count(),
1,
"child={child}, {events:?}"
);
}
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn main_earlier_dom_callback_does_not_lose_load_after_starting_async_script() -> Result<()> {
dom_callback_before_load_does_not_lose_load(false).await
}
#[tokio::test(flavor = "multi_thread")]
async fn child_earlier_dom_callback_does_not_lose_load_after_starting_async_script() -> Result<()> {
dom_callback_before_load_does_not_lose_load(true).await
}
async fn parser_import_map_error_keeps_dom_fifo_position(
child: bool,
document_write: bool,
) -> Result<()> {
let import_map = r#"<script type=importmap src="data:application/json,%7B%7D"
onerror="events.push('parser error')"></script>"#;
let parser_markup = if document_write {
format!(
"<script>document.write({})</script>",
serde_json::to_string(import_map)?.replace("</script>", "<\\/script>")
)
} else {
import_map.to_owned()
};
let markup = format!(
r#"<!doctype html><head><script>
window.events = [];
document.addEventListener('DOMContentLoaded', () => events.push('DOMContentLoaded'));
</script>{parser_markup}<script>
const later = document.createElement('script');
later.type = 'importmap';
later.src = 'data:application/json,%7B%7D';
later.onerror = () => events.push('dynamic error');
document.head.append(later);
</script></head><body>parser tail"#
);
let result = observe_lifecycle_markup(child, &markup).await?;
assert_eq!(
result["value"].as_str(),
Some(r#"["parser error","dynamic error","DOMContentLoaded"]"#),
"child={child}, document_write={document_write}"
);
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn main_parser_import_map_error_precedes_later_dom_tasks() -> Result<()> {
parser_import_map_error_keeps_dom_fifo_position(false, false).await
}
#[tokio::test(flavor = "multi_thread")]
async fn child_parser_import_map_error_precedes_later_dom_tasks() -> Result<()> {
parser_import_map_error_keeps_dom_fifo_position(true, false).await
}
#[tokio::test(flavor = "multi_thread")]
async fn main_written_import_map_error_precedes_later_dom_tasks() -> Result<()> {
parser_import_map_error_keeps_dom_fifo_position(false, true).await
}
#[tokio::test(flavor = "multi_thread")]
async fn child_written_import_map_error_precedes_later_dom_tasks() -> Result<()> {
parser_import_map_error_keeps_dom_fifo_position(true, true).await
}
@@ -2129,10 +2129,7 @@ impl DocumentRuntime {
}
}
PreparedImportMapSource::ExternalUnsupported => {
let _ = self.enqueue_script_event_lifecycle_work(
ScriptEventKind::Error,
&host_script_handle,
);
unsafe { &mut *host_ptr }.queue_script_preparation_error(scope, node);
}
}
}
@@ -28,8 +28,9 @@ impl DocumentRuntime {
.unwrap_or(work.len());
// This is an owner-queue boundary marker, not yet the HTML event-loop
// queue point for DOMContentLoaded. When it reaches the front after all
// defer-like and parser-owned pre-DCL work has drained, the driver first
// runs ready timer tasks recorded during classic defer execution.
// defer-like and parser-owned pre-DCL work has drained, the driver
// admits the event to the shared DOM task source before yielding to
// ready timer tasks recorded during classic defer execution.
work.insert(
domcontentloaded_index,
PostParsePageOwnedWork::main_document_domcontentloaded(owner),
+5 -8
View File
@@ -151,11 +151,10 @@ pub(crate) fn accept_parser_owned_import_map_handoff(
start_line: u64,
start_column: u64,
import_map: PreparedImportMap,
) {
) -> anyhow::Result<()> {
vm.document_runtime
.note_parser_script_start_position(node_id, start_line, start_column);
let host_script_handle = vm
.document_runtime
vm.document_runtime
.bind_parser_owned_script_handle_for_node(import_map.node_id);
let _ = vm
.document_runtime
@@ -169,7 +168,7 @@ pub(crate) fn accept_parser_owned_import_map_handoff(
let Some(source) =
vm.inline_script_element_source_for_execution(node_id, &source, request)
else {
return;
return Ok(());
};
if let Err(error) =
register_parser_owned_import_map_source(vm, &source, &import_map.base_url)
@@ -181,12 +180,10 @@ pub(crate) fn accept_parser_owned_import_map_handoff(
}
}
PreparedImportMapSource::ExternalUnsupported => {
let _ = vm.document_runtime.enqueue_script_event_lifecycle_work(
crate::host::ScriptEventKind::Error,
&host_script_handle,
);
vm.queue_script_preparation_error(node_id)?;
}
}
Ok(())
}
pub(crate) fn resolve_module_specifier(
@@ -63,8 +63,21 @@ impl JsContextHost {
action: FrameDocumentLifecycleAction,
realm_id: FrameRealmId,
) -> bool {
if !matches!(action, FrameDocumentLifecycleAction::Interactive(_)) {
return self
.page_task_capabilities
.get()
.expect("a child Document must retain its Page task capabilities")
.dom_manipulation()
.send_child_document_lifecycle(
crate::page_task_queue::RendererPageChildDocumentLifecycleTarget::new(
action, realm_id,
),
)
.is_ok();
}
self.page_child_frame_task_sender()
.send_document_lifecycle(
.send_interactive_lifecycle(
crate::page_task_queue::RendererPageChildDocumentLifecycleTarget::new(
action, realm_id,
),
@@ -196,6 +209,35 @@ impl JsContextHost {
self.route_child_document_lifecycle_action(action.into(), realm_request.realm_id())
}
/// Parser stop changes readiness synchronously. Only an unmaterialized
/// child realm needs an owner continuation before it can dispatch events.
pub(crate) fn finish_child_document_parser_stop(
&mut self,
scope: &mut v8::PinScope<'_, '_>,
action: FrameDocumentInteractiveLifecycleAction,
) -> FrameDocumentLifecycleTaskEffect {
if !self.child_document_lifecycle_action_is_current(action.into()) {
return FrameDocumentLifecycleTaskEffect::NotApplied;
}
let dispatch_scope = super::super::OwnerDispatchScope::Child(action.child_handle());
let execution_owner = crate::native_bridge::WindowExecutionContextOwner::Frame(
action.owner().local_window_id,
);
let Some((_, context)) =
self.window_execution_context(scope, execution_owner, dispatch_scope)
else {
return if self.queue_child_document_interactive_lifecycle_action(action) {
FrameDocumentLifecycleTaskEffect::ConsumedWithoutEvent
} else {
FrameDocumentLifecycleTaskEffect::NotApplied
};
};
let context = v8::Global::new(scope, context);
let context = v8::Local::new(scope, &context);
let child_scope = &mut v8::ContextScope::new(scope, context);
self.run_child_document_lifecycle_action(child_scope, action.into())
}
pub(crate) fn queue_child_document_domcontentloaded_if_ready(
&mut self,
child_handle: DomHandle,
@@ -937,12 +937,11 @@ impl JsContextHost {
}
}
PreparedImportMapSource::ExternalUnsupported => {
tracing::debug!(
child_handle = ?child_handle,
document_handle = ?document_handle,
script_handle = ?node_id,
"child parser external import map is unsupported"
);
if !self.queue_script_preparation_error(scope, node_id) {
return ScriptDisposition::AdmissionFailed {
script_handle: node_id,
};
}
}
}
ScriptDisposition::Continue
@@ -1143,7 +1142,7 @@ impl JsContextHost {
document_handle,
parser,
) {
self.queue_child_document_interactive_lifecycle_action(action);
self.finish_child_document_parser_stop(scope, action);
}
}
@@ -749,9 +749,10 @@ impl JsContextHost {
};
};
let initial_classic_ready_work = install.initial_classic_ready_work;
let parser_stop_queued = install
.parser_stop_action
.is_some_and(|action| self.queue_child_document_interactive_lifecycle_action(action));
let parser_stop_queued = install.parser_stop_action.is_some_and(|action| {
self.finish_child_document_parser_stop(scope, action)
!= crate::frame_owner_model::FrameDocumentLifecycleTaskEffect::NotApplied
});
self.promote_pending_service_worker_child_client(handle);
self.register_or_update_service_worker_child_client(handle);
self.complete_pending_service_worker_child_client_navigation(handle);
@@ -781,6 +781,16 @@ impl JsContextHost {
.promise_rejection()
}
pub(crate) fn page_main_document_lifecycle_sender(
&self,
) -> crate::page_task_queue::RendererPageMainDocumentLifecycleSender {
self.page_task_capabilities
.get()
.expect("a live Page Window must install its lifecycle task capability")
.dom_manipulation()
.main_document_lifecycle()
}
pub(crate) fn page_file_reading_sender(
&self,
) -> crate::page_task_queue::RendererPageFileReadingSender {
+8 -4
View File
@@ -17,6 +17,7 @@ mod history_traversal;
mod image_load_event;
mod indexed_db_task;
mod internal_loading;
mod main_document_lifecycle;
mod main_document_post_parse;
mod main_document_runtime;
mod main_document_task_owner;
@@ -218,12 +219,10 @@ pub(crate) use self::dedicated_worker_client_event::{
RendererPageDedicatedWorkerClientEventProducer, RendererPageDedicatedWorkerClientEventSender,
RendererPageDedicatedWorkerClientEventTask,
};
#[cfg(test)]
pub(crate) use self::dom_manipulation::RendererPageDomManipulationOwner;
pub(crate) use self::dom_manipulation::{
PageDomManipulationTurnAction, PageDomManipulationTurnOutcome,
RendererPageDomManipulationRoute, RendererPageDomManipulationSender,
RendererPageDomManipulationTask,
RendererPageDomManipulationOwner, RendererPageDomManipulationRoute,
RendererPageDomManipulationSender, RendererPageDomManipulationTask,
};
pub(crate) use self::dynamic_import_owner_action::{
PageDynamicImportOwnerActionDocumentEffect, PageDynamicImportOwnerActionTurnAction,
@@ -278,6 +277,11 @@ pub(crate) use self::internal_loading::{
RendererPageInternalLoadingOwner, RendererPageInternalLoadingSender,
RendererPageInternalLoadingTask,
};
pub(crate) use self::main_document_lifecycle::{
PageMainDocumentLifecycleTurnAction, PageMainDocumentLifecycleTurnOutcome,
RendererPageMainDocumentLifecycleCompletion, RendererPageMainDocumentLifecycleOwner,
RendererPageMainDocumentLifecycleSender, RendererPageMainDocumentLifecycleTask,
};
pub(crate) use self::main_document_post_parse::{
MainDocumentCompletionRecheckEffect, MainDocumentPostParseCallbackExecution,
MainDocumentPostParseCallbackSettlement, MainDocumentPostParseExecution,
@@ -390,7 +390,7 @@ pub(crate) type PageChildClassicScriptSourceLoadTurnOutcome =
PageOwnerTurnOutcome<PageChildClassicScriptSourceLoadTurnAction>;
impl RendererPageChildFrameTask {
fn new(owner: RendererPageChildFrameTaskOwner) -> Self {
pub(super) fn new(owner: RendererPageChildFrameTaskOwner) -> Self {
debug_assert!(!matches!(
owner.target(),
RendererPageChildFrameTaskTarget::ParserModuleRootStart(_)
@@ -566,10 +566,14 @@ impl RendererPageChildFrameTaskSender {
))
}
pub(crate) fn send_document_lifecycle(
pub(crate) fn send_interactive_lifecycle(
&self,
target: RendererPageChildDocumentLifecycleTarget,
) -> Result<(), RendererPageChildFrameTaskRouteClosed> {
debug_assert!(matches!(
target.action(),
crate::frame_owner_model::FrameDocumentLifecycleAction::Interactive(_)
));
self.send(RendererPageChildFrameTaskTarget::DocumentLifecycle(target))
}
@@ -78,6 +78,8 @@ pub(crate) enum RendererPageDomManipulationOwner {
ScriptPreparationError(RendererPageScriptPreparationErrorOwner),
PromiseRejection(RendererPagePromiseRejectionOwner),
ImageLoadEvent(RendererPageImageLoadEventOwner),
MainDocumentLifecycle(super::RendererPageMainDocumentLifecycleOwner),
ChildDocumentLifecycle(super::RendererPageChildFrameTaskOwner),
PopupLoadEvent(RendererPagePopupLoadEventOwner),
PopupClose(RendererPagePopupCloseOwner),
ConnectedStyleEvent(RendererPageStylesheetTaskOwner),
@@ -97,6 +99,8 @@ pub(crate) enum RendererPageDomManipulationTask {
ScriptPreparationError(RendererPageScriptPreparationErrorTask),
PromiseRejection(RendererPagePromiseRejectionTask),
ImageLoadEvent(RendererPageImageLoadEventTask),
MainDocumentLifecycle(super::RendererPageMainDocumentLifecycleTask),
ChildDocumentLifecycle(super::RendererPageChildFrameTask),
PopupLoadEvent(RendererPagePopupLoadEventTask),
PopupClose(RendererPagePopupCloseTask),
ConnectedStyleEvent(RendererPageConnectedStyleEventTask),
@@ -130,6 +134,12 @@ impl RendererPageDomManipulationTask {
Self::ImageLoadEvent(task) => {
RendererPageDomManipulationOwner::ImageLoadEvent(task.owner())
}
Self::MainDocumentLifecycle(task) => {
RendererPageDomManipulationOwner::MainDocumentLifecycle(task.owner)
}
Self::ChildDocumentLifecycle(task) => {
RendererPageDomManipulationOwner::ChildDocumentLifecycle(task.owner())
}
Self::PopupLoadEvent(task) => {
RendererPageDomManipulationOwner::PopupLoadEvent(task.owner())
}
@@ -169,6 +179,8 @@ pub(crate) enum PageDomManipulationTurnAction {
ScriptPreparationError(super::PageScriptPreparationErrorTurnAction),
PromiseRejection(super::PagePromiseRejectionTurnAction),
ImageLoadEvent(super::PageImageLoadEventTurnAction),
MainDocumentLifecycle(super::PageMainDocumentLifecycleTurnAction),
ChildDocumentLifecycle(super::PageChildDocumentLifecycleTurnAction),
PopupLoadEvent(super::PagePopupLoadEventTurnAction),
PopupClose(super::PagePopupCloseTurnAction),
ConnectedStyleEvent(PageConnectedStyleEventTurnAction),
@@ -234,6 +246,30 @@ impl RendererPageDomManipulationSender {
RendererPageImageLoadEventSender::new(self.route.clone(), self.root_document)
}
pub(crate) fn main_document_lifecycle(&self) -> super::RendererPageMainDocumentLifecycleSender {
super::RendererPageMainDocumentLifecycleSender::new(self.route.clone(), self.root_document)
}
pub(crate) fn send_child_document_lifecycle(
&self,
target: super::RendererPageChildDocumentLifecycleTarget,
) -> Result<(), super::child_frame_task::RendererPageChildFrameTaskRouteClosed> {
debug_assert!(!matches!(
target.action(),
crate::frame_owner_model::FrameDocumentLifecycleAction::Interactive(_)
));
self.route
.send(RendererPageDomManipulationTask::ChildDocumentLifecycle(
super::RendererPageChildFrameTask::new(
super::RendererPageChildFrameTaskOwner::new(
self.root_document,
super::RendererPageChildFrameTaskTarget::DocumentLifecycle(target),
),
),
))
.map_err(|_| super::child_frame_task::RendererPageChildFrameTaskRouteClosed)
}
pub(crate) fn popup_load_event(&self) -> RendererPagePopupLoadEventSender {
RendererPagePopupLoadEventSender::new(self.route.clone(), self.root_document)
}
@@ -292,6 +328,21 @@ pub(crate) struct RendererPageDomManipulationSource {
}
impl RendererPageDomManipulationSource {
pub(crate) fn has_main_document_lifecycle_task(
&mut self,
root_document: RendererDocumentToken,
document: crate::frame_owner_model::FrameDocumentTaskOwner,
) -> bool {
self.source.has_matching_task(|ready| {
matches!(
ready.value(),
RendererPageDomManipulationTask::MainDocumentLifecycle(task)
if task.owner.root_document == root_document
&& task.owner.body.owner() == document
)
})
}
/// Remove cancelled task closures before they become scheduler-visible.
///
/// Element toggle coalescing cancels and reposts at the tail, matching the
@@ -0,0 +1,82 @@
use tokio::sync::oneshot;
use crate::{
frame_owner_model::FrameDocumentTaskOwner,
runtime::{PageOwnerTurnOutcome, RendererDocumentToken},
script_vm::{MainDocumentLifecycleBody, MainDocumentLifecycleTargetEffect},
};
use super::dom_manipulation::{RendererPageDomManipulationRoute, RendererPageDomManipulationTask};
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) struct RendererPageMainDocumentLifecycleOwner {
pub(crate) root_document: RendererDocumentToken,
pub(crate) body: MainDocumentLifecycleBody,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) enum RendererPageMainDocumentLifecycleCompletion {
Executed,
/// An earlier DOM task added a load delay after this task was admitted.
/// Return the exact load boundary to its driver without completing it.
LoadBlocked {
owner: FrameDocumentTaskOwner,
},
}
/// An admitted HTML lifecycle task, after parser/defer and load prerequisites.
/// Its receipt releases the driver's completion token only after execution.
#[derive(Debug)]
pub(crate) struct RendererPageMainDocumentLifecycleTask {
pub(crate) owner: RendererPageMainDocumentLifecycleOwner,
pub(crate) completion: Option<oneshot::Sender<RendererPageMainDocumentLifecycleCompletion>>,
}
#[derive(Clone, Debug)]
pub(crate) struct RendererPageMainDocumentLifecycleSender {
route: RendererPageDomManipulationRoute,
root_document: RendererDocumentToken,
}
impl RendererPageMainDocumentLifecycleSender {
pub(super) fn new(
route: RendererPageDomManipulationRoute,
root_document: RendererDocumentToken,
) -> Self {
Self {
route,
root_document,
}
}
pub(crate) fn send(
&self,
body: MainDocumentLifecycleBody,
completion: Option<oneshot::Sender<RendererPageMainDocumentLifecycleCompletion>>,
) -> anyhow::Result<()> {
anyhow::ensure!(
!matches!(body, MainDocumentLifecycleBody::Interactive(_)),
"interactive readiness belongs to parser completion"
);
self.route
.send(RendererPageDomManipulationTask::MainDocumentLifecycle(
RendererPageMainDocumentLifecycleTask {
owner: RendererPageMainDocumentLifecycleOwner {
root_document: self.root_document,
body,
},
completion,
},
))
.map_err(|_| anyhow::anyhow!("main document lifecycle DOM task route closed"))
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) struct PageMainDocumentLifecycleTurnAction {
pub(crate) owner: RendererPageMainDocumentLifecycleOwner,
pub(crate) target: MainDocumentLifecycleTargetEffect,
}
pub(crate) type PageMainDocumentLifecycleTurnOutcome =
PageOwnerTurnOutcome<PageMainDocumentLifecycleTurnAction>;
@@ -627,6 +627,15 @@ pub(crate) enum RendererPageSchedulerTask {
}
impl RendererPageOwnedTaskSources {
pub(crate) fn has_queued_main_document_lifecycle_dom_task(
&mut self,
root_document: crate::runtime::RendererDocumentToken,
document: crate::frame_owner_model::FrameDocumentTaskOwner,
) -> bool {
self.dom_manipulation
.has_main_document_lifecycle_task(root_document, document)
}
pub(crate) fn new(
runtime_wake: PageRuntimeWakeSignal,
owner_wake: RendererOwnerWakeSender,
@@ -1958,6 +1967,43 @@ impl RendererPageOwnedTaskSourcesTestHarness {
})
}
/// Compare the two child semantic source heads without changing either FIFO.
pub(crate) fn next_child_semantic_task_target(
&self,
) -> Option<super::RendererPageChildFrameTaskTarget> {
self.sources
.borrow_mut()
.ready_descriptors()
.into_iter()
.filter(|descriptor| {
matches!(
descriptor,
RendererPageReadyDescriptor::ChildFrameTask { .. }
| RendererPageReadyDescriptor::DomManipulation {
owner: super::RendererPageDomManipulationOwner::ChildDocumentLifecycle(
_
),
..
}
)
})
.min_by_key(|descriptor| {
(
descriptor.runnable_since(),
descriptor.enqueue_order().unwrap_or(0),
descriptor.source_kind(),
)
})
.map(|descriptor| match descriptor {
RendererPageReadyDescriptor::ChildFrameTask { owner, .. }
| RendererPageReadyDescriptor::DomManipulation {
owner: super::RendererPageDomManipulationOwner::ChildDocumentLifecycle(owner),
..
} => owner.target(),
_ => unreachable!("child semantic selector must retain its exact target"),
})
}
/// Dequeue one production scheduler task selected by a narrow semantic
/// fixture. The selector sees the same descriptor as the owner scheduler;
/// this helper adds no lane-specific consumer or alternate queue model.
@@ -141,12 +141,13 @@ pub(super) struct PendingDocumentLifecycleTurn {
/// can immediately take another owner turn without ordinary Page work.
///
/// This is stable residence state, not a wake hint. It keeps a runnable
/// parser-finish chain (notably `interactive` -> DOMContentLoaded) from
/// being displaced by an older resource terminal that became ready while
/// the parser task was still executing.
/// parser preparation and lifecycle task admission contiguous. Once an
/// event is admitted to the DOM source, the resident waits for its receipt
/// and ordinary task arbitration owns event delivery.
pub(super) owner_turn_is_runnable: bool,
pub(super) driver: PostParseLifecycleDriver,
pub(super) completed_task: Option<PostParsePageOwnedTask>,
pub(super) awaiting_dom_task: Option<PendingDocumentLifecycleDomTask>,
pub(super) completion_action: Option<PostParseLifecycleCompletionAction>,
/// Whether this exact resident owns a sealed main-parser defer/module
/// continuation.
@@ -158,3 +159,10 @@ pub(super) struct PendingDocumentLifecycleTurn {
pub(super) has_sealed_main_parser_script_queue: bool,
pub(super) started: Instant,
}
pub(super) struct PendingDocumentLifecycleDomTask {
pub(super) task: PostParsePageOwnedTask,
pub(super) completion: tokio::sync::oneshot::Receiver<
crate::page_task_queue::RendererPageMainDocumentLifecycleCompletion,
>,
}
@@ -763,11 +763,30 @@ pub(super) fn select_page_scheduler_turn(
!lifecycle_is_deferred && entry.document_lifecycle_owner_turn_is_runnable();
let has_ready_main_parser_script_continuation =
!lifecycle_is_deferred && entry.has_ready_main_parser_script_continuation();
let document_lifecycle = DocumentLifecycleClassReadiness::from_resident_state(
let mut document_lifecycle = DocumentLifecycleClassReadiness::from_resident_state(
has_pending_document_lifecycle_turn,
document_lifecycle_owner_turn_is_runnable,
has_ready_main_parser_script_continuation,
);
let has_lifecycle_dom_task = entry
.page_vm()
.vm()
.current_main_document_task_owner()
.is_some_and(|owner| {
task_sources.has_queued_main_document_lifecycle_dom_task(
entry.page_vm().document_lifecycle.identity().document,
owner,
)
});
if has_lifecycle_dom_task
&& !matches!(
document_lifecycle,
DocumentLifecycleClassReadiness::RunnableContinuation
| DocumentLifecycleClassReadiness::ReadyMainParserScriptContinuation
)
{
document_lifecycle = DocumentLifecycleClassReadiness::QueuedDomTask;
}
let gate_policy = lifecycle_gate
.as_mut()
.map(|gate| gate.turn_policy(entry, !snapshot.eligible.is_empty()))
@@ -794,8 +813,26 @@ pub(super) fn select_page_scheduler_turn(
),
},
Some(PageTurnClass::Ordinary) => {
// Preserve parser-finish priority across task sources while the
// lifecycle event occupies its real DOM FIFO position. Earlier DOM
// tasks run first; no descriptor can jump to the lifecycle payload.
let dom_head_is_eligible = snapshot.eligible.iter().any(|descriptor| {
matches!(
descriptor,
crate::page_task_queue::RendererPageReadyDescriptor::DomManipulation { .. }
)
});
// A child lifecycle head can be awaiting its exact realm. Allow
// that prerequisite source to run before restoring DOM priority.
let eligible = if has_lifecycle_dom_task && dom_head_is_eligible {
snapshot.eligible.into_iter().filter(|descriptor| {
matches!(descriptor, crate::page_task_queue::RendererPageReadyDescriptor::DomManipulation { .. })
}).collect()
} else {
snapshot.eligible
};
let selected = scheduler
.select_ready_descriptor(snapshot.eligible)
.select_ready_descriptor(eligible)
.expect("selected ordinary Page-turn class must retain an eligible descriptor");
let task = task_sources.take_task(selected);
RendererPageScheduledTurn::Ordinary(Box::new(task))
@@ -815,6 +852,12 @@ pub(in crate::runtime) async fn advance_page_owner_one_turn_via_local_task(
) -> (LivePageEntry, Result<()>) {
run_entry_on_bound_owner_local_store_local_task(local_executor, entry, move |entry| {
Box::pin(async move {
let main_lifecycle_owner = match &task {
RendererPageSchedulerTask::DomManipulation(
crate::page_task_queue::RendererPageDomManipulationTask::MainDocumentLifecycle(task),
) => Some(task.owner),
_ => None,
};
let replacement_lifecycle_snapshot = entry
.page_vm()
.document_replacement_lifecycle_action_snapshot();
@@ -847,6 +890,21 @@ pub(in crate::runtime) async fn advance_page_owner_one_turn_via_local_task(
.await
};
if application.is_ok()
&& main_lifecycle_owner.is_some_and(|owner| {
owner.root_document == entry.page_vm().document_lifecycle.identity().document
&& entry.page_vm().vm().current_main_document_task_owner() == Some(owner.body.owner())
})
{
let (_, pending) = entry.page_vm_and_document_lifecycle_turn_mut();
if let Some(pending) = pending {
// Resume the exact driver to consume its receipt. An
// executed event publishes the reached stage before other
// sources run; a blocked load returns to its prerequisites.
pending.owner_turn_is_runnable = true;
}
}
match (application, reconciliation) {
(Ok(()), Ok(_)) => Ok(()),
(Err(action_error), Ok(_)) => Err(action_error),
@@ -81,6 +81,8 @@ pub(super) enum PageTurnClass {
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(super) enum DocumentLifecycleClassReadiness {
Absent,
/// The DOM source owns the admitted event. Only its FIFO head may run.
QueuedDomTask,
Available,
RunnableContinuation,
ReadyMainParserScriptContinuation,
@@ -110,7 +112,7 @@ impl DocumentLifecycleClassReadiness {
}
const fn is_available(self) -> bool {
!matches!(self, Self::Absent)
!matches!(self, Self::Absent | Self::QueuedDomTask)
}
}
@@ -249,6 +249,7 @@ impl PageVm {
owner_turn_is_runnable: true,
driver,
completed_task: None,
awaiting_dom_task: None,
completion_action: None,
has_sealed_main_parser_script_queue,
started,
@@ -372,6 +373,34 @@ impl PageVm {
return Ok(settled);
}
let pending = pending_document_lifecycle_turn
.as_mut()
.expect("post-parse lifecycle state should remain installed");
if let Some(waiting) = pending.awaiting_dom_task.as_mut() {
match waiting.completion.try_recv() {
Ok(crate::page_task_queue::RendererPageMainDocumentLifecycleCompletion::Executed) => {
pending.completed_task = Some(pending.awaiting_dom_task.take().unwrap().task);
}
Ok(crate::page_task_queue::RendererPageMainDocumentLifecycleCompletion::LoadBlocked { owner }) => {
// The attempt did not execute the load boundary. Its token
// must never tell the driver that this stage was reached.
pending.awaiting_dom_task = None;
self.page_task_queue.enqueue_front_post_parse_work_preserving_order(vec![
PostParsePageOwnedWork::main_document_window_load(owner),
]);
}
Err(tokio::sync::oneshot::error::TryRecvError::Empty) => {
return Ok(DocumentLifecycleTurnOutcome::blocked(
DocumentLifecycleTurnAction::None,
document,
));
}
Err(tokio::sync::oneshot::error::TryRecvError::Closed) => {
anyhow::bail!("current lifecycle DOM task disappeared before completion");
}
}
}
// Feed the previous exact task result into the driver before other
// page work is admitted. `AwaitProgress` parks immediately; a producer
// wake returns to owner arbitration instead of waiting in this method.
@@ -412,17 +441,6 @@ impl PageVm {
)
.await
}
PostParseLifecycleAdvance::TimerQueuedByClassicDeferBeforeDomContentLoaded => {
self.run_classic_defer_timer_before_domcontentloaded(&request_client)
.await?;
self.outcome_after_exact_post_parse_action(
pending_document_lifecycle_turn,
document,
stage,
DocumentLifecycleTurnAction::Progressed,
false,
)
}
PostParseLifecycleAdvance::NeedsContinuation => self
.outcome_after_exact_post_parse_action(
pending_document_lifecycle_turn,
@@ -570,6 +588,7 @@ impl PageVm {
owner_turn_is_runnable: true,
driver,
completed_task: None,
awaiting_dom_task: None,
completion_action: None,
has_sealed_main_parser_script_queue: false,
started: Instant::now(),
@@ -598,6 +617,7 @@ impl PageVm {
owner_turn_is_runnable: _,
driver,
completed_task,
awaiting_dom_task: _,
completion_action,
has_sealed_main_parser_script_queue: _,
started: _,
@@ -620,6 +640,7 @@ impl PageVm {
owner_turn_is_runnable: true,
driver,
completed_task,
awaiting_dom_task: None,
completion_action,
has_sealed_main_parser_script_queue,
started: Instant::now(),
@@ -647,6 +668,7 @@ impl PageVm {
owner_turn_is_runnable: true,
driver,
completed_task: None,
awaiting_dom_task: None,
completion_action: None,
has_sealed_main_parser_script_queue: self
.vm()
@@ -3,11 +3,9 @@
//! The exact lifecycle resident remains the sole ordinary DCL/load authority.
//! This component only reconciles an already-claimed body: typed checkpoint
//! continuations, milestone journal visibility, body settlement, and lifecycle
//! priming stay in one auditable order. Parser completion may call this same
//! component after claiming the exact DCL direct successor, but the parser
//! continuation's own task-end checkpoint has already completed first. This
//! preserves the observable task boundary without opening a scheduler round in
//! which an ordinary Page task could overtake DCL.
//! priming stay in one auditable order. DCL and load reach this coordinator
//! through the shared DOM task source after parser completion has admitted
//! them; interactive readiness remains part of parser completion.
use anyhow::Result;
use std::time::Instant;
@@ -42,17 +40,6 @@ pub(super) async fn execute_main_document_lifecycle_on_owner_local_task(
execute_main_document_lifecycle_body_on_owner_local_task(page_vm, body).await
}
pub(super) async fn execute_parser_exact_domcontentloaded_on_owner_local_task(
page_vm: &mut PageVm,
owner: crate::frame_owner_model::FrameDocumentTaskOwner,
) -> Result<MainDocumentLifecycleTaskRun> {
execute_main_document_lifecycle_body_on_owner_local_task(
page_vm,
MainDocumentLifecycleBody::DomContentLoaded { owner },
)
.await
}
async fn execute_main_document_lifecycle_body_on_owner_local_task(
page_vm: &mut PageVm,
body: MainDocumentLifecycleBody,
+76 -13
View File
@@ -105,6 +105,7 @@ mod page_image_load_event;
mod page_indexed_db_task;
mod page_internal_loading;
mod page_internal_loading_task_completion;
mod page_main_document_lifecycle;
mod page_main_document_post_parse;
mod page_main_document_runtime;
mod page_main_native_module_task;
@@ -2119,9 +2120,23 @@ impl PageVm {
) -> bool {
use crate::{
frame_owner_model::ChildFrameSemanticTurnKind,
page_task_queue::RendererPageChildFrameTaskTarget,
page_task_queue::{
RendererPageChildFrameTaskTarget, RendererPageDomManipulationOwner,
RendererPageReadyDescriptor,
},
};
if expected == ChildFrameSemanticTurnKind::DocumentLifecycle {
return self.page_task_executor_sources_for_test().has_scheduler_task_for_executor_test(|descriptor| {
matches!(descriptor,
RendererPageReadyDescriptor::DomManipulation { owner: RendererPageDomManipulationOwner::ChildDocumentLifecycle(_), .. }
) || matches!(descriptor,
RendererPageReadyDescriptor::ChildFrameTask { owner, .. }
if matches!(owner.target(), RendererPageChildFrameTaskTarget::DocumentLifecycle(_))
)
});
}
let Some(target) = self
.page_task_executor_sources_for_test()
.next_child_frame_task_target()
@@ -2961,7 +2976,11 @@ impl PageVm {
{
return Some(ChildFrameSemanticTurnKind::NavigationCommit);
}
if self
if matches!(
self.page_task_executor_sources_for_test()
.next_child_semantic_task_target(),
Some(crate::page_task_queue::RendererPageChildFrameTaskTarget::DocumentLifecycle(_))
) && self
.run_exact_selected_page_task_for_test(
PageSelectedTaskTestSelector::ChildDocumentLifecycle,
&loader,
@@ -3170,11 +3189,61 @@ impl PageVm {
match advance {
PostParseLifecycleAdvance::PageOwnedTask(mut task) => {
let request_client = self.request_client.clone();
self.execute_ordinary_post_parse_page_owned_task_on_named_owner_lane(
&request_client,
task.take_work_for_execution(),
)
.await?;
let work = task.take_work_for_execution();
if let PostParsePageOwnedWork::Lifecycle(lifecycle) = &work
&& matches!(
**lifecycle,
PostParseLifecycleWork::DispatchDomContentLoaded { .. }
| PostParseLifecycleWork::DispatchWindowLoad { .. }
)
{
let body = crate::script_vm::MainDocumentLifecycleBody::from_post_parse_work(
lifecycle,
)
.unwrap();
let (sender, mut receiver) = tokio::sync::oneshot::channel();
self.vm()
.queue_main_document_lifecycle_dom_task(body, Some(sender))?;
if matches!(
body,
crate::script_vm::MainDocumentLifecycleBody::DomContentLoaded { .. }
) {
self.run_ready_classic_defer_timers_before_domcontentloaded(
&request_client,
)
.await?;
}
loop {
match receiver.try_recv() {
Ok(crate::page_task_queue::RendererPageMainDocumentLifecycleCompletion::Executed) => break,
Ok(crate::page_task_queue::RendererPageMainDocumentLifecycleCompletion::LoadBlocked { owner }) => {
self.page_task_queue.enqueue_front_post_parse_work_preserving_order(vec![PostParsePageOwnedWork::main_document_window_load(owner)]);
return Ok(PostParseLifecycleLoopAdvance::Continue(Box::new(None)));
}
Err(tokio::sync::oneshot::error::TryRecvError::Closed) => {
anyhow::bail!(
"lifecycle DOM task disappeared before its fixture completion"
);
}
Err(tokio::sync::oneshot::error::TryRecvError::Empty) => {
anyhow::ensure!(
self.run_exact_selected_page_task_for_test(
PageSelectedTaskTestSelector::AnyDomManipulation,
&request_client,
)
.await?,
"queued lifecycle task must remain runnable"
);
}
}
}
} else {
self.execute_ordinary_post_parse_page_owned_task_on_named_owner_lane(
&request_client,
work,
)
.await?;
}
if self.vm().has_pending_location_navigation() {
return Ok(PostParseLifecycleLoopAdvance::Complete(
PostParseLifecycleCompletionAction::TriggeredNavigation,
@@ -3184,12 +3253,6 @@ impl PageVm {
*task,
))))
}
PostParseLifecycleAdvance::TimerQueuedByClassicDeferBeforeDomContentLoaded => {
let request_client = self.request_client.clone();
self.run_classic_defer_timer_before_domcontentloaded(&request_client)
.await?;
Ok(PostParseLifecycleLoopAdvance::Continue(Box::new(None)))
}
PostParseLifecycleAdvance::NeedsContinuation => {
Ok(PostParseLifecycleLoopAdvance::Continue(Box::new(None)))
}
@@ -22,14 +22,22 @@ impl PageVm {
matches!(
descriptor,
RendererPageReadyDescriptor::ChildFrameTask { owner, .. }
if matches!(
| RendererPageReadyDescriptor::DomManipulation {
owner: crate::page_task_queue::RendererPageDomManipulationOwner::ChildDocumentLifecycle(owner), ..
} if matches!(
owner.target(),
RendererPageChildFrameTaskTarget::DocumentLifecycle(_)
)
)
})?;
let RendererPageSchedulerTask::ChildFrameTask(task) = task else {
unreachable!("DocumentLifecycle descriptor must dequeue a child-frame task")
let task = match task {
RendererPageSchedulerTask::ChildFrameTask(task)
| RendererPageSchedulerTask::DomManipulation(
crate::page_task_queue::RendererPageDomManipulationTask::ChildDocumentLifecycle(
task,
),
) => task,
_ => unreachable!("DocumentLifecycle descriptor must dequeue its admitted task"),
};
Some(self.apply_selected_page_child_document_lifecycle_turn(task))
}
@@ -1,15 +1,8 @@
//! Task-end boundary for an exact child `Document` lifecycle action.
//! Task-end boundary for an exact child Document lifecycle action.
//!
//! Interactive, `DOMContentLoaded`, and complete transitions are ordinary
//! selected child-frame tasks. Their bodies synchronously dispatch the
//! corresponding document event when a wrapper still exists, but deliberately
//! leave Promise reactions pending. This module maps that execution-produced
//! fact to the sole Page task-completion authority.
//!
//! This does not govern the parser-owned direct-successor boundary where the
//! final deferred script completes parsing and synchronously dispatches DCL in
//! the same task. That separately typed parser completion path must remain
//! contiguous and must not be turned into another scheduler task here.
//! DCL and complete use the shared DOM source. Interactive readiness belongs
//! to parser stop; an unmaterialized realm may need a child owner continuation.
//! The selected dispatcher completes the event's checkpoint and follow-up.
use crate::page_task_queue::{
PageChildDocumentLifecycleTargetEffect, PageChildDocumentLifecycleTurnAction,
@@ -5,11 +5,20 @@ use crate::page_task_queue::{
use super::PageVm;
impl PageVm {
pub(in crate::runtime) fn apply_selected_page_dom_manipulation_turn(
pub(in crate::runtime) async fn apply_selected_page_dom_manipulation_turn(
&mut self,
task: RendererPageDomManipulationTask,
) -> anyhow::Result<PageDomManipulationTurnOutcome> {
match task {
RendererPageDomManipulationTask::ChildDocumentLifecycle(task) => Ok(self
.apply_selected_page_child_document_lifecycle_turn(task)
.map_action(PageDomManipulationTurnAction::ChildDocumentLifecycle)),
RendererPageDomManipulationTask::MainDocumentLifecycle(task) => self
.apply_selected_page_main_document_lifecycle_turn(task)
.await
.map(|outcome| {
outcome.map_action(PageDomManipulationTurnAction::MainDocumentLifecycle)
}),
RendererPageDomManipulationTask::BroadcastChannel(task) => self
.apply_selected_page_broadcast_channel_delivery_turn(task)
.map(|outcome| outcome.map_action(PageDomManipulationTurnAction::BroadcastChannel)),
@@ -18,6 +18,12 @@ impl PageVm {
loader: &crate::network::ResourceRequestClient,
) -> Result<()> {
let completion = match action {
PageDomManipulationTurnAction::ChildDocumentLifecycle(action) => {
action.into_page_task_completion()
}
PageDomManipulationTurnAction::MainDocumentLifecycle(action) => {
action.into_page_task_completion()
}
PageDomManipulationTurnAction::BroadcastChannel(action) => {
action.into_page_task_completion()
}
@@ -21,6 +21,7 @@ pub(crate) enum PageDomManipulationTestFamily {
FileEntryFileCallback,
ScriptPreparationError,
PromiseRejection,
MainDocumentLifecycle,
ImageLoadEvent,
PopupLoadEvent,
ConnectedStyleEvent,
@@ -34,6 +35,9 @@ impl PageDomManipulationTestFamily {
matches!(
(self, owner),
(
Self::MainDocumentLifecycle,
RendererPageDomManipulationOwner::MainDocumentLifecycle(_)
) | (
Self::BroadcastChannel,
RendererPageDomManipulationOwner::BroadcastChannel(_)
) | (
@@ -0,0 +1,125 @@
use crate::{
page_task_queue::{
PageMainDocumentLifecycleTurnAction, PageMainDocumentLifecycleTurnOutcome,
RendererPageMainDocumentLifecycleCompletion, RendererPageMainDocumentLifecycleTask,
},
runtime::PendingDocumentLifecycleTurn,
script_vm::{
MainDocumentLifecycleBody, MainDocumentLifecycleTargetEffect,
MainDocumentLifecycleTargetRejection, PostParsePageOwnedTask,
},
};
use super::{IntoPageTaskCompletion, PageTaskCompletion, PageVm};
/// Event bodies finish their own checkpoints through the lifecycle coordinator.
/// A blocked or stale task does not enter page code.
impl IntoPageTaskCompletion for PageMainDocumentLifecycleTurnAction {
fn into_page_task_completion(self) -> PageTaskCompletion {
PageTaskCompletion::NoCompletion
}
}
impl PageVm {
pub(super) async fn run_ready_classic_defer_timers_before_domcontentloaded(
&mut self,
loader: &crate::network::ResourceRequestClient,
) -> anyhow::Result<()> {
// Preserve the existing cross-source timer preference, after DCL has
// acquired its FIFO position in the DOM task source.
while self
.vm()
.document_runtime
.has_ready_timeout_queued_by_classic_defer_script()
{
self.run_classic_defer_timer_before_domcontentloaded(loader)
.await?;
}
Ok(())
}
pub(super) fn queue_post_parse_lifecycle_dom_task(
&self,
pending: &mut PendingDocumentLifecycleTurn,
body: MainDocumentLifecycleBody,
task: PostParsePageOwnedTask,
) -> anyhow::Result<()> {
anyhow::ensure!(
pending.awaiting_dom_task.is_none() && pending.completed_task.is_none(),
"a lifecycle resident may await only one exact task"
);
let (sender, receiver) = tokio::sync::oneshot::channel();
self.vm()
.queue_main_document_lifecycle_dom_task(body, Some(sender))?;
pending.awaiting_dom_task = Some(
super::super::document_lifecycle_turn::PendingDocumentLifecycleDomTask {
task,
completion: receiver,
},
);
Ok(())
}
pub(super) async fn apply_selected_page_main_document_lifecycle_turn(
&mut self,
task: RendererPageMainDocumentLifecycleTask,
) -> anyhow::Result<PageMainDocumentLifecycleTurnOutcome> {
let RendererPageMainDocumentLifecycleTask { owner, completion } = task;
if owner.root_document == self.document_lifecycle.identity().document
&& let MainDocumentLifecycleBody::WindowLoad {
owner: document_owner,
} = owner.body
&& self
.vm_mut()
.main_document_window_load_task_is_ready(document_owner)
== Some(false)
{
// The DOM queue may have run a producer since admission. Do not
// open the milestone journal or consume its boundary token while
// the exact Document has a new load delay.
let completion =
completion.expect("queued Window load must retain its lifecycle driver");
let _ = completion.send(RendererPageMainDocumentLifecycleCompletion::LoadBlocked {
owner: document_owner,
});
return Ok(PageMainDocumentLifecycleTurnOutcome::new(
PageMainDocumentLifecycleTurnAction {
owner,
target: MainDocumentLifecycleTargetEffect::NotApplied {
reason: MainDocumentLifecycleTargetRejection::TransitionRejected,
current_owner: self.vm().current_main_document_task_owner(),
},
},
));
}
let target = if owner.root_document == self.document_lifecycle.identity().document {
let run = super::main_document_lifecycle_completion::execute_main_document_lifecycle_on_owner_local_task(
self, owner.body,
).await?;
let target = run.completion.target();
if let crate::script_vm::MainDocumentLifecycleFollowup::ScheduleInternalLoading {
task,
ready_at,
} = run.completion.into_followup()
{
self.vm()
.schedule_page_internal_loading_task(task, ready_at)?;
}
// The common selected Page owner reconciles document.open() after
// the body and its checkpoints, just as for other DOM tasks.
target
} else {
MainDocumentLifecycleTargetEffect::NotApplied {
reason: MainDocumentLifecycleTargetRejection::TransitionRejected,
current_owner: self.vm().current_main_document_task_owner(),
}
};
if let Some(completion) = completion {
// Replacement may have retired the receiver during this task.
let _ = completion.send(RendererPageMainDocumentLifecycleCompletion::Executed);
}
Ok(PageMainDocumentLifecycleTurnOutcome::new(
PageMainDocumentLifecycleTurnAction { owner, target },
))
}
}
@@ -52,6 +52,7 @@ pub(crate) enum PageSelectedTaskTestSelector {
ChildNavigationCommit,
ChildModulepreloadEventAction,
DomManipulation(PageDomManipulationTestFamily),
AnyDomManipulation,
DedicatedWorkerClientEvent,
DynamicImportOwnerAction,
FileReading,
@@ -89,6 +90,9 @@ impl PageSelectedTaskTestSelector {
Self::ChildDocumentLifecycle => matches!(
descriptor,
RendererPageReadyDescriptor::ChildFrameTask { owner, .. }
| RendererPageReadyDescriptor::DomManipulation {
owner: crate::page_task_queue::RendererPageDomManipulationOwner::ChildDocumentLifecycle(owner), ..
}
if matches!(
owner.target(),
crate::page_task_queue::RendererPageChildFrameTaskTarget::DocumentLifecycle(_)
@@ -153,6 +157,10 @@ impl PageSelectedTaskTestSelector {
descriptor,
RendererPageReadyDescriptor::ChildModulepreloadEventAction { .. }
),
Self::AnyDomManipulation => matches!(
descriptor,
RendererPageReadyDescriptor::DomManipulation { .. }
),
Self::DomManipulation(family) => matches!(
descriptor,
RendererPageReadyDescriptor::DomManipulation { owner, .. }
@@ -308,7 +316,13 @@ impl PageSelectedTaskTestSelector {
fn matches_task(self, task: &RendererPageSchedulerTask) -> bool {
match (self, task) {
(Self::ChildDocumentLifecycle, RendererPageSchedulerTask::ChildFrameTask(task)) => {
(Self::ChildDocumentLifecycle, RendererPageSchedulerTask::ChildFrameTask(task))
| (
Self::ChildDocumentLifecycle,
RendererPageSchedulerTask::DomManipulation(
RendererPageDomManipulationTask::ChildDocumentLifecycle(task),
),
) => {
matches!(
task.owner().target(),
crate::page_task_queue::RendererPageChildFrameTaskTarget::DocumentLifecycle(_)
@@ -369,6 +383,7 @@ impl PageSelectedTaskTestSelector {
Self::ChildModulepreloadEventAction,
RendererPageSchedulerTask::ChildModulepreloadEventAction(_),
) => true,
(Self::AnyDomManipulation, RendererPageSchedulerTask::DomManipulation(_)) => true,
(Self::DomManipulation(family), RendererPageSchedulerTask::DomManipulation(task)) => {
family.matches_owner(task.owner())
}
@@ -74,6 +74,27 @@ impl PageVm {
descriptor: RendererPageReadyDescriptor,
) -> bool {
match descriptor {
RendererPageReadyDescriptor::DomManipulation {
owner:
crate::page_task_queue::RendererPageDomManipulationOwner::ChildDocumentLifecycle(
owner,
),
..
} => {
if owner.root_document() != self.document_lifecycle.identity().document {
return true;
}
let crate::page_task_queue::RendererPageChildFrameTaskTarget::DocumentLifecycle(
target,
) = owner.target()
else {
unreachable!("child lifecycle DOM carrier must retain its exact target");
};
// A current task retains its DOM FIFO position while its
// separately queued realm prerequisite runs. It is not stale
// merely because that realm has not materialized yet.
!self.vm().child_document_lifecycle_waits_for_realm(target)
}
RendererPageReadyDescriptor::ActionWindow { .. }
| RendererPageReadyDescriptor::DomManipulation { .. }
| RendererPageReadyDescriptor::UserInteraction { .. }
@@ -4,8 +4,8 @@
//! owns the bounded handoff from that work to parser completion: it consumes a
//! one-shot drained-queue permit, asks the lifecycle authority to claim the
//! exact DOMContentLoaded successor, closes the selected parser task's
//! checkpoint boundary, and only then hands that already-claimed successor to
//! the lifecycle coordinator. The coordinator applies it to the surviving
//! checkpoint boundary, and admits the claimed successor to the shared DOM
//! task source. Its selected-task coordinator applies it to the surviving
//! exact Document or stale-rejects it after replacement.
//! It is not a scheduler lane and stores no durable lifecycle state.
@@ -16,9 +16,7 @@ use crate::frame_owner_model::FrameDocumentTaskOwner;
use crate::network::ResourceRequestClient;
use crate::page_task_queue::{PostParseLifecycleWork, PostParsePageOwnedWork};
use crate::runtime::{PendingDocumentLifecycleTurn, RendererDocumentLifecycleIdentity};
use crate::script_vm::{
MainDocumentLifecycleBodyKind, ParserFinishDomContentLoadedTask, PostParsePageOwnedTask,
};
use crate::script_vm::{MainDocumentLifecycleBody, PostParsePageOwnedTask};
use super::super::document_lifecycle_turn::{
DocumentLifecycleTurnAction, DocumentLifecycleTurnOutcome,
@@ -164,31 +162,20 @@ impl ParserCompletion {
};
}
};
// The parser continuation and DOMContentLoaded are distinct HTML task
// boundaries even though this Chromium-compatible direct successor
// deliberately does not reopen ordinary scheduler arbitration. Drain
// terminal reactions first; the lifecycle coordinator then owns DCL's
// separate task-end checkpoint.
// Finish parser reactions before admitting the DCL global task. An
// earlier DOM task keeps its FIFO position; later timer callbacks may
// enqueue DOM work only behind this already admitted lifecycle task.
Self::finish_task(page_vm, task_effect)?;
page_vm.vm().queue_main_document_lifecycle_dom_task(
MainDocumentLifecycleBody::DomContentLoaded {
owner: successor_owner,
},
None,
)?;
let request_client = page_vm.request_client.clone();
while page_vm
.vm()
.document_runtime
.has_ready_timeout_queued_by_classic_defer_script()
{
page_vm
.run_classic_defer_timer_before_domcontentloaded(&request_client)
.await?;
}
let run = super::main_document_lifecycle_completion::execute_parser_exact_domcontentloaded_on_owner_local_task(
page_vm,
successor_owner,
)
.await?;
anyhow::ensure!(
run.completion.kind() == MainDocumentLifecycleBodyKind::DomContentLoaded,
"parse-time DOMContentLoaded successor lost its typed lifecycle execution"
);
page_vm
.run_ready_classic_defer_timers_before_domcontentloaded(&request_client)
.await?;
Ok(())
}
@@ -228,32 +215,6 @@ impl PageVm {
ParserCompletion::finish_parse_time(self, completion).await
}
async fn execute_domcontentloaded_after_main_parser_finish_on_named_owner_lane(
&mut self,
mut task: ParserFinishDomContentLoadedTask,
) -> Result<PostParsePageOwnedTask> {
let owner = task.owner();
let work = task.take_work_for_execution();
let PostParsePageOwnedWork::Lifecycle(work) = work else {
anyhow::bail!("parser-finish successor is not lifecycle work");
};
let successor_owner = match ParserCompletion::exact_domcontentloaded_owner(*work, owner) {
Ok(owner) => owner,
Err(message) => anyhow::bail!(message),
};
let run = super::main_document_lifecycle_completion::execute_parser_exact_domcontentloaded_on_owner_local_task(
self,
successor_owner,
)
.await?;
anyhow::ensure!(
run.completion.kind() == MainDocumentLifecycleBodyKind::DomContentLoaded,
"DOMContentLoaded direct successor lost its typed lifecycle execution"
);
Ok(task.into_completed_task())
}
pub(super) async fn execute_and_complete_selected_post_parse_page_owned_task(
&mut self,
loader: &ResourceRequestClient,
@@ -262,12 +223,38 @@ impl PageVm {
stage: PageVmInitStage,
mut task: Box<PostParsePageOwnedTask>,
) -> Result<DocumentLifecycleTurnOutcome> {
let work = task.take_work_for_execution();
if let PostParsePageOwnedWork::Lifecycle(lifecycle) = &work
&& matches!(
**lifecycle,
PostParseLifecycleWork::DispatchDomContentLoaded { .. }
| PostParseLifecycleWork::DispatchWindowLoad { .. }
)
{
let body = MainDocumentLifecycleBody::from_post_parse_work(lifecycle)
.expect("selected DCL/load work must have a typed lifecycle body");
self.queue_post_parse_lifecycle_dom_task(
pending_document_lifecycle_turn
.as_mut()
.expect("lifecycle resident must remain installed"),
body,
*task,
)?;
if matches!(body, MainDocumentLifecycleBody::DomContentLoaded { .. }) {
self.run_ready_classic_defer_timers_before_domcontentloaded(loader)
.await?;
}
return self.outcome_after_exact_post_parse_action(
pending_document_lifecycle_turn,
document,
stage,
DocumentLifecycleTurnAction::Progressed,
true,
);
}
let replacement_lifecycle_snapshot = self.document_replacement_lifecycle_action_snapshot();
let execution = self
.execute_post_parse_page_owned_task_on_named_owner_lane(
loader,
task.take_work_for_execution(),
)
.execute_post_parse_page_owned_task_on_named_owner_lane(loader, work)
.await;
// Generic post-parse callbacks must finish their old-realm task before
// a synchronous `document.open()` replacement is admitted. MainParser
@@ -416,50 +403,32 @@ impl PageVm {
};
}
};
if let Some(dcl_task) = claimed_dcl {
if let Some(mut dcl_task) = claimed_dcl {
ParserCompletion::finish_task_with_replacement_admission(self, task_effect)?;
while self
.vm()
.document_runtime
.has_ready_timeout_queued_by_classic_defer_script()
{
self.run_classic_defer_timer_before_domcontentloaded(loader)
.await?;
}
let replacement_lifecycle_snapshot =
self.document_replacement_lifecycle_action_snapshot();
let execution = self
.execute_domcontentloaded_after_main_parser_finish_on_named_owner_lane(
dcl_task,
)
.await;
let admission = self
.take_document_replacement_lifecycle_admission_after_action(
replacement_lifecycle_snapshot,
);
let completed_dcl_task = match (execution, admission) {
(Ok(task), Ok(_)) => task,
(Err(execution_error), Ok(_)) => return Err(execution_error),
(Ok(_), Err(admission_error)) => return Err(admission_error),
(Err(execution_error), Err(admission_error)) => {
return Err(anyhow::anyhow!(
"parser-finish DOMContentLoaded action failed ({execution_error:#}) and its Document replacement admission also failed ({admission_error:#})"
));
}
let owner = dcl_task.owner();
let PostParsePageOwnedWork::Lifecycle(work) =
dcl_task.take_work_for_execution()
else {
anyhow::bail!("parser-finish successor is not lifecycle work");
};
pending_document_lifecycle_turn
.as_mut()
.expect("post-parse lifecycle state should remain installed")
.completed_task = Some(completed_dcl_task);
if let Some(outcome) = self
.transition_lifecycle_for_pending_top_level_navigation(
pending_document_lifecycle_turn,
document,
stage,
)
{
return Ok(outcome);
}
let owner = ParserCompletion::exact_domcontentloaded_owner(*work, owner)
.map_err(anyhow::Error::msg)?;
self.queue_post_parse_lifecycle_dom_task(
pending_document_lifecycle_turn
.as_mut()
.expect("lifecycle resident must remain installed"),
MainDocumentLifecycleBody::DomContentLoaded { owner },
dcl_task.into_pending_task(),
)?;
self.run_ready_classic_defer_timers_before_domcontentloaded(loader)
.await?;
return self.outcome_after_exact_post_parse_action(
pending_document_lifecycle_turn,
document,
stage,
DocumentLifecycleTurnAction::Progressed,
true,
);
} else {
ParserCompletion::finish_task_with_replacement_admission(self, task_effect)?;
}
@@ -23,7 +23,7 @@ impl PageVm {
self.apply_selected_page_action_window_turn(deadline)
}
RendererPageSchedulerTask::DomManipulation(task) => {
let outcome = self.apply_selected_page_dom_manipulation_turn(task)?;
let outcome = self.apply_selected_page_dom_manipulation_turn(task).await?;
self.finish_selected_page_dom_manipulation_task(outcome.action, loader)
.await?;
Ok(())
@@ -206,6 +206,26 @@ async fn advance_unblocked_exact_lifecycle_to_stage(
},
..
} => assert_eq!(*runnable_document, document),
DocumentLifecycleTurnOutcome {
readiness: DocumentLifecycleTurnReadiness::Blocked { document: waiting },
..
} if pending
.as_ref()
.is_some_and(|pending| pending.awaiting_dom_task.is_some()) =>
{
assert_eq!(*waiting, document);
let loader = page_vm.request_client.clone();
assert!(
page_vm
.run_exact_selected_page_task_for_test(
super::super::PageSelectedTaskTestSelector::DomManipulation(
super::super::PageDomManipulationTestFamily::MainDocumentLifecycle
),
&loader
)
.await?
);
}
_ => panic!(
"unblocked exact lifecycle should remain runnable before {stage:?}: {outcome:?}"
),
@@ -267,6 +287,47 @@ async fn prepare_parse_time_exact_domcontentloaded(
Ok(owner)
}
impl PageVm {
async fn advance_lifecycle_and_selected_dom_task_for_test(
&mut self,
pending: &mut Option<PendingDocumentLifecycleTurn>,
document: RendererDocumentLifecycleIdentity,
) -> anyhow::Result<DocumentLifecycleTurnOutcome> {
for _ in 0..64 {
let outcome = self
.advance_post_parse_lifecycle_one_owner_turn(pending, document)
.await?;
if !matches!(
outcome.readiness,
DocumentLifecycleTurnReadiness::Blocked { .. }
) || pending
.as_ref()
.is_none_or(|pending| pending.awaiting_dom_task.is_none())
{
return Ok(outcome);
}
let snapshot = self.document_replacement_lifecycle_action_snapshot();
let loader = self.request_client.clone();
assert!(
self.run_exact_selected_page_task_for_test(
super::super::PageSelectedTaskTestSelector::DomManipulation(
super::super::PageDomManipulationTestFamily::MainDocumentLifecycle,
),
&loader,
)
.await?
);
if let Some(outcome) = self
.reconcile_document_replacement_lifecycle_after_owner_action(snapshot, pending)
.await?
{
return Ok(outcome);
}
}
anyhow::bail!("lifecycle fixture exceeded its bounded DOM task turns")
}
}
async fn exact_lifecycle_turn_reaches_handler_navigation(
event_target: &str,
event_name: &str,
@@ -303,7 +364,7 @@ async fn exact_lifecycle_turn_reaches_handler_navigation(
for _ in 0..128 {
match page_vm
.advance_post_parse_lifecycle_one_owner_turn(&mut pending, document)
.advance_lifecycle_and_selected_dom_task_for_test(&mut pending, document)
.await?
{
DocumentLifecycleTurnOutcome {
@@ -369,7 +430,7 @@ async fn exact_lifecycle_turn_publishes_document_open_replacement(
for _ in 0..96 {
match page_vm
.advance_post_parse_lifecycle_one_owner_turn(
.advance_lifecycle_and_selected_dom_task_for_test(
&mut pending_document_lifecycle_turn,
document,
)
@@ -401,7 +462,17 @@ async fn exact_lifecycle_turn_publishes_document_open_replacement(
},
..
} => {
assert_eq!(next_document, document);
if next_document != document {
assert_ne!(next_document, initial_document);
assert_eq!(page_vm.document_lifecycle.identity(), next_document);
assert_eq!(
pending_document_lifecycle_turn
.as_ref()
.map(|pending| pending.document),
Some(next_document)
);
return Ok(());
}
document = next_document;
}
DocumentLifecycleTurnOutcome {
@@ -576,7 +647,7 @@ fn load_stage_non_replacing_javascript_navigation_resumes_exact_lifecycle() {
for _ in 0..128 {
let outcome = page_vm
.advance_post_parse_lifecycle_one_owner_turn(&mut pending, document)
.advance_lifecycle_and_selected_dom_task_for_test(&mut pending, document)
.await?;
if !matches!(
outcome.action,
@@ -622,7 +693,7 @@ fn load_stage_non_replacing_javascript_navigation_resumes_exact_lifecycle() {
for _ in 0..128 {
let resumed = page_vm
.advance_post_parse_lifecycle_one_owner_turn(
.advance_lifecycle_and_selected_dom_task_for_test(
&mut pending,
document,
)
@@ -1729,6 +1800,15 @@ Promise.resolve().then(() => __parseTimeExactDclOrder.push("module-terminal:micr
outcome,
ParseTimeMainParserBoundaryOutcome::CurrentDocumentRetained
);
assert_eq!(
page_vm.vm_mut().eval_without_microtask_checkpoint_for_test(
"__parseTimeExactDclOrder.join('|')"
)?,
"module-terminal:microtask",
"parser completion must only enqueue DCL"
);
let loader = page_vm.request_client.clone();
assert!(page_vm.run_exact_selected_page_task_for_test(super::super::PageSelectedTaskTestSelector::DomManipulation(super::super::PageDomManipulationTestFamily::MainDocumentLifecycle), &loader).await?);
assert_eq!(
page_vm.vm_mut().eval_without_microtask_checkpoint_for_test(
"__parseTimeExactDclOrder.join('|')"
@@ -1789,6 +1869,8 @@ Promise.resolve().then(() => {
ParseTimeMainParserBoundaryOutcome::DocumentReplaced,
"the parser task-end checkpoint must report replacement before attempting the old exact DCL"
);
let loader = page_vm.request_client.clone();
assert!(page_vm.run_exact_selected_page_task_for_test(super::super::PageSelectedTaskTestSelector::DomManipulation(super::super::PageDomManipulationTestFamily::MainDocumentLifecycle), &loader).await?);
assert_ne!(
page_vm.vm().current_main_document_task_owner(),
Some(owner)
@@ -1918,13 +2000,16 @@ document.addEventListener("DOMContentLoaded", () => {
.await?;
assert_eq!(
outcome,
ParseTimeMainParserBoundaryOutcome::DocumentReplaced,
"phase one must stop the retired Document after an exact-DCL reaction replaces it"
ParseTimeMainParserBoundaryOutcome::CurrentDocumentRetained,
"the parser returns before the queued DCL listener can replace its Document"
);
assert_ne!(
page_vm.vm().current_main_document_task_owner(),
Some(owner)
let loader = page_vm.request_client.clone();
assert!(
page_vm
.run_exact_selected_page_task_for_test(super::super::PageSelectedTaskTestSelector::DomManipulation(super::super::PageDomManipulationTestFamily::MainDocumentLifecycle), &loader)
.await?
);
assert_ne!(page_vm.vm().current_main_document_task_owner(), Some(owner));
assert_eq!(
page_vm.vm_mut().eval(
"String(document.getElementById('parse-time-replacement') !== null)"
@@ -2377,7 +2462,7 @@ fn domcontentloaded_turn_transitions_same_exact_document_to_load_residence() {
let dcl_outcome = loop {
let outcome = page_vm
.advance_post_parse_lifecycle_one_owner_turn(
.advance_lifecycle_and_selected_dom_task_for_test(
&mut pending_document_lifecycle_turn,
document,
)
@@ -2437,7 +2522,7 @@ fn domcontentloaded_turn_transitions_same_exact_document_to_load_residence() {
loop {
match page_vm
.advance_post_parse_lifecycle_one_owner_turn(
.advance_lifecycle_and_selected_dom_task_for_test(
&mut pending_document_lifecycle_turn,
document,
)
@@ -3605,7 +3690,7 @@ fn top_level_http_location_navigation_reserves_service_worker_client_until_commi
} => {
loop {
match page_vm
.advance_post_parse_lifecycle_one_owner_turn(
.advance_lifecycle_and_selected_dom_task_for_test(
&mut pending_document_lifecycle_turn,
lifecycle_document,
)
@@ -2053,12 +2053,20 @@ async fn run_child_interactive_domcontentloaded_then_host_load_for_wait(
page_vm: &mut PageVm,
label: &str,
) -> ChildFrameSemanticTurnKind {
run_expected_child_frame_task_source_after_realm_prerequisite_for_wait(
page_vm,
ChildFrameSemanticTurnKind::DocumentLifecycle,
&format!("{label} interactive transition"),
)
.await;
if page_vm.has_ready_child_frame_semantic_turn_for_test(
ChildFrameSemanticTurnKind::RealmMaterialization,
) {
run_expected_child_realm_materialization_for_wait(page_vm, label).await;
}
if matches!(page_vm.page_task_executor_sources_for_test().next_child_frame_task_target(), Some(crate::page_task_queue::RendererPageChildFrameTaskTarget::DocumentLifecycle(target)) if matches!(target.action(), crate::frame_owner_model::FrameDocumentLifecycleAction::Interactive(_)))
{
run_expected_child_frame_task_source_after_realm_prerequisite_for_wait(
page_vm,
ChildFrameSemanticTurnKind::DocumentLifecycle,
&format!("{label} queued interactive transition"),
)
.await;
}
run_child_domcontentloaded_then_host_load_for_wait(page_vm, label).await
}
@@ -3439,14 +3447,7 @@ globalThis.__childClassicWaitValue = 91;
"external:true|current:external-classic|inline:91",
"second DocumentScriptReady should run parser continuation without iframe load"
);
followup_sources.push(
run_expected_child_frame_task_source_after_realm_prerequisite_for_wait(
&mut page_vm,
ChildFrameSemanticTurnKind::DocumentLifecycle,
"child classic parser EOF interactive transition",
)
.await,
);
assert_eq!(page_vm.vm_mut().eval("document.querySelector('iframe').contentDocument.readyState")?, "interactive", "parser EOF must apply interactive synchronously");
followup_sources.push(
run_expected_child_frame_task_source_after_realm_prerequisite_for_wait(
&mut page_vm,
@@ -3535,10 +3536,9 @@ globalThis.__childClassicWaitValue = 91;
ChildFrameSemanticTurnKind::DocumentScriptReady,
ChildFrameSemanticTurnKind::DocumentLifecycle,
ChildFrameSemanticTurnKind::DocumentLifecycle,
ChildFrameSemanticTurnKind::DocumentLifecycle,
ChildFrameSemanticTurnKind::HostLoad
],
"child classic completion should progress through script, parser continuation, interactive, DOMContentLoaded, complete, and HostLoad turns"
"child classic completion should progress through script, parser continuation, DOMContentLoaded, complete, and HostLoad turns"
);
assert_eq!(
final_events,
@@ -3679,14 +3679,7 @@ async fn page_vm_child_parser_blocking_classic_waits_for_preceding_stylesheet()
"script:stylesheet-ready:loading",
"stylesheet source must be installed before the parser-blocking script executes while the document is still loading"
);
followup_sources.push(
run_expected_child_frame_task_source_after_realm_prerequisite_for_wait(
&mut page_vm,
ChildFrameSemanticTurnKind::DocumentLifecycle,
"stylesheet-gated child interactive transition",
)
.await,
);
assert_eq!(page_vm.vm_mut().eval("document.querySelector('iframe').contentDocument.readyState")?, "interactive", "parser EOF must apply interactive synchronously");
followup_sources.push(
run_expected_child_frame_task_source_after_realm_prerequisite_for_wait(
&mut page_vm,
@@ -3750,7 +3743,6 @@ async fn page_vm_child_parser_blocking_classic_waits_for_preceding_stylesheet()
ChildFrameSemanticTurnKind::DocumentScriptReady,
ChildFrameSemanticTurnKind::DocumentLifecycle,
ChildFrameSemanticTurnKind::DocumentLifecycle,
ChildFrameSemanticTurnKind::DocumentLifecycle,
ChildFrameSemanticTurnKind::HostLoad,
]
);
@@ -4516,9 +4508,8 @@ globalThis.__childClassicDeferWaitValue = 73;
ChildFrameSemanticTurnKind::RealmMaterialization,
ChildFrameSemanticTurnKind::DocumentScriptReady,
ChildFrameSemanticTurnKind::DocumentScriptReady,
ChildFrameSemanticTurnKind::DocumentLifecycle
],
"child defer classic bootstrap should end with the document-owned interactive turn"
"child defer classic bootstrap should apply interactive at parser EOF"
);
assert_eq!(
followup_sources,
@@ -5100,7 +5091,6 @@ globalThis.__childClassicAsyncWaitValue = 41;
ChildFrameSemanticTurnKind::DocumentScriptReady,
ChildFrameSemanticTurnKind::DocumentScriptReady,
ChildFrameSemanticTurnKind::DocumentLifecycle,
ChildFrameSemanticTurnKind::DocumentLifecycle
],
"child async bootstrap should dispatch interactive and DCL before the later async completion"
);
@@ -5347,7 +5337,6 @@ parent.__childClassicDeferOrderEvents.push("current:" + document.currentScript.i
ChildFrameSemanticTurnKind::RealmMaterialization,
ChildFrameSemanticTurnKind::DocumentScriptReady,
ChildFrameSemanticTurnKind::DocumentScriptReady,
ChildFrameSemanticTurnKind::DocumentLifecycle
],
"child defer ordering bootstrap should reach interactive before either source completion"
);
@@ -5782,8 +5771,8 @@ async fn page_vm_child_parser_defer_preserves_cross_kind_document_order() {
"parser EOF must not execute any mixed parser-deferred script"
);
assert!(
bootstrap_sources.contains(&ChildFrameSemanticTurnKind::DocumentLifecycle),
"mixed parser-deferred document should reach interactive"
page_vm.vm_mut().eval("document.querySelector('iframe').contentDocument.readyState")? == "interactive",
"mixed parser-deferred document should become interactive at parser EOF"
);
assert_eq!(
bootstrap_sources
@@ -6493,7 +6482,6 @@ globalThis.__childParserModuleWaitValue = 188;
ChildFrameSemanticTurnKind::DocumentScriptReady,
ChildFrameSemanticTurnKind::ParserModuleRootStart,
ChildFrameSemanticTurnKind::DocumentScriptReady,
ChildFrameSemanticTurnKind::DocumentLifecycle
],
"the typed root fetch-start must preserve parser discovery FIFO, then the parser should run the following inline script and reach interactive"
);
@@ -8865,8 +8853,8 @@ async fn page_vm_child_modulepreload_terminal_event_does_not_delay_complete() {
.iter()
.filter(|source| **source == ChildFrameSemanticTurnKind::DocumentLifecycle)
.count()
>= 3,
"interactive, DOMContentLoaded, and complete must advance while the terminal link event remains queued: {lifecycle_turns:?}"
== 2,
"DOMContentLoaded and complete must each take a queued turn while the terminal link event remains queued: {lifecycle_turns:?}"
);
assert!(
lifecycle_turns.contains(&ChildFrameSemanticTurnKind::HostLoad),
@@ -9082,8 +9070,8 @@ async fn page_vm_child_modulepreload_fetch_does_not_delay_iframe_load() {
.iter()
.filter(|source| **source == ChildFrameSemanticTurnKind::DocumentLifecycle)
.count()
>= 3,
"interactive, DOMContentLoaded and complete should run while modulepreload fetch is pending: {startup_sources:?}"
== 2,
"DOMContentLoaded and complete should each take a queued turn while modulepreload fetch is pending: {startup_sources:?}"
);
assert!(startup_sources.contains(&ChildFrameSemanticTurnKind::HostLoad));
assert_eq!(
@@ -9565,7 +9553,7 @@ globalThis.__childDocumentLoadWaitValue = 42;
first_followup_source,
events_after_first_followup,
lifecycle_ready_after_first_followup,
interactive_source,
interactive_state,
host_load_source,
final_events,
) = local_executor
@@ -9644,12 +9632,7 @@ globalThis.__childDocumentLoadWaitValue = 42;
ChildFrameSemanticTurnKind::DocumentLifecycle,
);
let interactive_source = run_expected_child_frame_task_source_after_realm_prerequisite_for_wait(
&mut page_vm,
ChildFrameSemanticTurnKind::DocumentLifecycle,
"child document parser EOF interactive transition",
)
.await;
let interactive_state = page_vm.vm_mut().eval("document.querySelector('iframe').contentDocument.readyState")?;
let host_load_source = run_child_domcontentloaded_then_host_load_for_wait(
&mut page_vm,
"child document iframe load",
@@ -9672,7 +9655,7 @@ globalThis.__childDocumentLoadWaitValue = 42;
first_followup_source,
events_after_first_followup,
lifecycle_ready_after_first_followup,
interactive_source,
interactive_state,
host_load_source,
final_events,
))
@@ -9710,8 +9693,8 @@ globalThis.__childDocumentLoadWaitValue = 42;
"document-script ready should make the later lifecycle turn runnable"
);
assert_eq!(
interactive_source,
ChildFrameSemanticTurnKind::DocumentLifecycle,
interactive_state,
"interactive",
"parser EOF should become interactive before HostLoad"
);
assert_eq!(
@@ -9885,8 +9868,6 @@ parent.__multiChildDocumentEvents.push("child-b-script:" + (globalThis === self)
page_vm.run_next_child_frame_task_source_for_semantic_test().await,
page_vm.run_next_child_frame_task_source_for_semantic_test().await,
page_vm.run_next_child_frame_task_source_for_semantic_test().await,
page_vm.run_next_child_frame_task_source_for_semantic_test().await,
page_vm.run_next_child_frame_task_source_for_semantic_test().await,
];
let first_host_load_source = page_vm.run_next_child_frame_task_source_for_semantic_test().await;
let events_after_first_host_load = page_vm
@@ -9966,8 +9947,8 @@ parent.__multiChildDocumentEvents.push("child-b-script:" + (globalThis === self)
);
assert_eq!(
lifecycle_sources,
vec![Some(ChildFrameSemanticTurnKind::DocumentLifecycle); 6],
"interactive, DOMContentLoaded and complete must each consume one lifecycle turn per child"
vec![Some(ChildFrameSemanticTurnKind::DocumentLifecycle); 4],
"DOMContentLoaded and complete must each consume one lifecycle turn per child"
);
assert_eq!(
first_host_load_source,
@@ -10034,7 +10015,7 @@ parent.__childReadyHostLoadEvents.push("child-script:" + (globalThis === self));
script_ready_source,
events_after_script_ready,
lifecycle_ready_after_script,
interactive_source,
interactive_state,
host_load_source,
events_after_host_load,
) = local_executor
@@ -10092,7 +10073,7 @@ parent.__childReadyHostLoadEvents.push("child-script:" + (globalThis === self));
ChildFrameSemanticTurnKind::DocumentLifecycle,
);
let interactive_source = page_vm.run_next_child_frame_task_source_for_semantic_test().await;
let interactive_state = page_vm.vm_mut().eval("document.querySelector('iframe').contentDocument.readyState")?;
let host_load_source = Some(
run_child_domcontentloaded_then_host_load_for_wait(
&mut page_vm,
@@ -10111,7 +10092,7 @@ parent.__childReadyHostLoadEvents.push("child-script:" + (globalThis === self));
script_ready_source,
events_after_script_ready,
lifecycle_ready_after_script,
interactive_source,
interactive_state,
host_load_source,
events_after_host_load,
))
@@ -10146,8 +10127,8 @@ parent.__childReadyHostLoadEvents.push("child-script:" + (globalThis === self));
"DocumentScriptReady should make the later lifecycle turn runnable"
);
assert_eq!(
interactive_source,
Some(ChildFrameSemanticTurnKind::DocumentLifecycle),
interactive_state,
"interactive",
"parser EOF should dispatch interactive before HostLoad"
);
assert_eq!(
@@ -10771,7 +10752,7 @@ async fn page_vm_realm_materialization_created_ready_work_enters_document_script
.run_next_child_frame_task_source_for_semantic_test()
.await,
Some(ChildFrameSemanticTurnKind::DocumentLifecycle),
"outer parser EOF should become interactive before nested document work"
"outer DOMContentLoaded must precede nested parser work admitted after it"
);
assert_eq!(
page_vm
@@ -10792,7 +10773,7 @@ async fn page_vm_realm_materialization_created_ready_work_enters_document_script
.run_next_child_frame_task_source_for_semantic_test()
.await,
Some(ChildFrameSemanticTurnKind::DocumentLifecycle),
"outer DOMContentLoaded should remain a later FIFO turn after nested work already admitted by realm materialization"
"nested DOMContentLoaded must remain a later FIFO turn after its parser work"
);
Ok::<_, anyhow::Error>(())
})
@@ -11344,12 +11325,12 @@ onload = () => {
host_load_pending_after_completion,
script_ready_source,
events_after_script_ready,
interactive_source,
interactive_state,
host_load_source,
events_after_host_load,
nested_script_ready_source,
events_after_nested_script_ready,
nested_interactive_source,
nested_interactive_state,
nested_host_load_source,
events_after_nested_host_load,
) = local_executor
@@ -11393,7 +11374,7 @@ onload = () => {
let events_after_script_ready = page_vm
.vm_mut()
.eval("__hostLoadNestedEvents.join('|')")?;
let interactive_source = page_vm.run_next_child_frame_task_source_for_semantic_test().await;
let interactive_state = page_vm.vm_mut().eval("document.querySelector('iframe').contentDocument.readyState")?;
let host_load_source = Some(
run_child_domcontentloaded_then_host_load_for_wait(
&mut page_vm,
@@ -11419,8 +11400,7 @@ onload = () => {
let events_after_nested_script_ready = page_vm
.vm_mut()
.eval("__hostLoadNestedEvents.join('|')")?;
let nested_interactive_source =
page_vm.run_next_child_frame_task_source_for_semantic_test().await;
let nested_interactive_state = page_vm.vm_mut().eval("document.querySelector('iframe').contentDocument.querySelector('iframe').contentDocument.readyState")?;
let nested_host_load_source = Some(
run_child_domcontentloaded_then_host_load_for_wait(
&mut page_vm,
@@ -11438,12 +11418,12 @@ onload = () => {
host_load_pending_after_completion,
script_ready_source,
events_after_script_ready,
interactive_source,
interactive_state,
host_load_source,
events_after_host_load,
nested_script_ready_source,
events_after_nested_script_ready,
nested_interactive_source,
nested_interactive_state,
nested_host_load_source,
events_after_nested_host_load,
))
@@ -11471,8 +11451,8 @@ onload = () => {
);
assert_eq!(events_after_script_ready, "child-script");
assert_eq!(
interactive_source,
Some(ChildFrameSemanticTurnKind::DocumentLifecycle),
interactive_state,
"interactive",
"child parser EOF should become interactive before window load"
);
assert_eq!(
@@ -11494,8 +11474,8 @@ onload = () => {
"nested script should run on the later DocumentScriptReady turn"
);
assert_eq!(
nested_interactive_source,
Some(ChildFrameSemanticTurnKind::DocumentLifecycle),
nested_interactive_state,
"interactive",
"nested parser EOF should become interactive before nested HostLoad"
);
assert_eq!(
@@ -11586,7 +11566,7 @@ addEventListener("load", () => parent.__childLoadNavigationEvents.push("load-lis
completion_source,
script_ready_source,
events_after_script_ready,
interactive_source,
interactive_state,
host_load_source,
events_after_host_load,
pending_after_host_load,
@@ -11638,7 +11618,7 @@ addEventListener("load", () => parent.__childLoadNavigationEvents.push("load-lis
.vm_mut()
.eval("__childLoadNavigationEvents.join('|')")?;
let interactive_source = page_vm.run_next_child_frame_task_source_for_semantic_test().await;
let interactive_state = page_vm.vm_mut().eval("document.querySelector('iframe').contentDocument.readyState")?;
let host_load_source = Some(
run_child_domcontentloaded_then_host_load_for_wait(
&mut page_vm,
@@ -11688,7 +11668,7 @@ addEventListener("load", () => parent.__childLoadNavigationEvents.push("load-lis
completion_source,
script_ready_source,
events_after_script_ready,
interactive_source,
interactive_state,
host_load_source,
events_after_host_load,
pending_after_host_load,
@@ -11723,8 +11703,8 @@ addEventListener("load", () => parent.__childLoadNavigationEvents.push("load-lis
"DocumentScriptReady should not dispatch child window load inline"
);
assert_eq!(
interactive_source,
Some(ChildFrameSemanticTurnKind::DocumentLifecycle),
interactive_state,
"interactive",
"child document should become interactive before its window load"
);
assert_eq!(
@@ -213,7 +213,7 @@ JSON.stringify({
PageDomManipulationTestFamily::ViewTransitionUpdate,
)
.expect("view-transition update callback should queue one platform task");
let body = page_vm.apply_selected_page_dom_manipulation_turn(task)?;
let body = page_vm.apply_selected_page_dom_manipulation_turn(task).await?;
let PageDomManipulationTurnAction::ViewTransitionUpdate(action) = body.action else {
unreachable!("the selected view-transition task must retain its typed action")
};
@@ -1246,7 +1246,7 @@ impl<'loader, 'state> ParserDriver<'loader, 'state> {
start_line,
start_column,
import_map,
);
)?;
Ok(ScriptHandoffOutcome::NoNavigation)
}
ParserScriptHandoff::NoExecution {
@@ -753,6 +753,16 @@ fn streaming_stylesheet_and_json_ld_reach_tail_and_post_parse_boundary() {
DocumentLifecycleTurnReadiness::Runnable { .. },
..
} => {}
DocumentLifecycleTurnOutcome {
readiness: DocumentLifecycleTurnReadiness::Blocked { .. },
..
} if pending.as_ref().is_some_and(|pending| pending.awaiting_dom_task.is_some()) => {
let loader = page_vm.request_client.clone();
assert!(page_vm.run_exact_selected_page_task_for_test(
crate::runtime::page_vm::PageSelectedTaskTestSelector::AnyDomManipulation,
&loader,
).await?, "admitted DCL must advance through the real DOM source head");
}
outcome => panic!(
"streaming fixture should reach DCL without parking: {outcome:?}"
),
+23 -2
View File
@@ -5338,9 +5338,23 @@ impl ScriptVm {
) -> bool {
use crate::{
frame_owner_model::ChildFrameSemanticTurnKind,
page_task_queue::RendererPageChildFrameTaskTarget,
page_task_queue::{
RendererPageChildFrameTaskTarget, RendererPageDomManipulationOwner,
RendererPageReadyDescriptor,
},
};
if expected == ChildFrameSemanticTurnKind::DocumentLifecycle {
return self._page_task_residence_for_executor_test.as_ref().expect("semantic fixture must retain its sources").task_sources().has_scheduler_task_for_executor_test(|descriptor| {
matches!(descriptor,
RendererPageReadyDescriptor::DomManipulation { owner: RendererPageDomManipulationOwner::ChildDocumentLifecycle(_), .. }
) || matches!(descriptor,
RendererPageReadyDescriptor::ChildFrameTask { owner, .. }
if matches!(owner.target(), RendererPageChildFrameTaskTarget::DocumentLifecycle(_))
)
});
}
let Some(target) = self
._page_task_residence_for_executor_test
.as_ref()
@@ -5393,7 +5407,14 @@ impl ScriptVm {
{
return Some(ChildFrameSemanticTurnKind::NavigationCommit);
}
if self
if matches!(
self._page_task_residence_for_executor_test
.as_ref()
.expect("child fixture must retain its sources")
.task_sources()
.next_child_semantic_task_target(),
Some(crate::page_task_queue::RendererPageChildFrameTaskTarget::DocumentLifecycle(_))
) && self
.run_child_document_lifecycle_body_for_test()
.expect("typed child lifecycle executor turn should succeed")
.is_some()
@@ -27,15 +27,29 @@ impl<'vm> ChildDocumentLifecycleOwner<'vm> {
pub(super) fn notify_parser_stop_action(
&mut self,
action: FrameDocumentInteractiveLifecycleAction,
) -> bool {
self.vm
._context_host
.borrow_mut()
.queue_child_document_interactive_lifecycle_action(action)
) -> Result<FrameDocumentLifecycleTaskEffect> {
let context_host = self.vm._context_host.clone();
self.vm.with_default_context_scope(move |scope, _| {
Ok(context_host
.borrow_mut()
.finish_child_document_parser_stop(scope, action))
})
}
}
impl ScriptVm {
pub(crate) fn child_document_lifecycle_waits_for_realm(
&self,
target: RendererPageChildDocumentLifecycleTarget,
) -> bool {
let host = self._context_host.borrow();
host.child_document_lifecycle_action_is_current(target.action())
&& host.has_child_frame_realm_materialization_request(
target.child_handle(),
target.document_owner(),
)
}
pub(crate) fn current_child_document_lifecycle_target(
&self,
expected: RendererPageChildDocumentLifecycleTarget,
@@ -102,9 +116,9 @@ impl ScriptVm {
let Some(task) = source.take_scheduler_task_for_executor_test(|descriptor| {
matches!(
descriptor,
crate::page_task_queue::RendererPageReadyDescriptor::ChildFrameTask {
owner,
..
crate::page_task_queue::RendererPageReadyDescriptor::ChildFrameTask { owner, .. }
| crate::page_task_queue::RendererPageReadyDescriptor::DomManipulation {
owner: crate::page_task_queue::RendererPageDomManipulationOwner::ChildDocumentLifecycle(owner), ..
} if matches!(
owner.target(),
RendererPageChildFrameTaskTarget::DocumentLifecycle(_)
@@ -113,8 +127,14 @@ impl ScriptVm {
}) else {
return Ok(None);
};
let crate::page_task_queue::RendererPageSchedulerTask::ChildFrameTask(task) = task else {
unreachable!("child-frame descriptor must dequeue its own family source")
let task = match task {
crate::page_task_queue::RendererPageSchedulerTask::ChildFrameTask(task)
| crate::page_task_queue::RendererPageSchedulerTask::DomManipulation(
crate::page_task_queue::RendererPageDomManipulationTask::ChildDocumentLifecycle(
task,
),
) => task,
_ => unreachable!("child lifecycle selector must dequeue its admitted task"),
};
let RendererPageChildFrameTaskTarget::DocumentLifecycle(target) = task.owner().target()
else {
@@ -60,7 +60,7 @@ impl ScriptVm {
self.apply_child_document_owner_transition(transition);
}
if let Some(action) = parser_stop_action {
ChildDocumentLifecycleOwner::new(self).notify_parser_stop_action(action);
ChildDocumentLifecycleOwner::new(self).notify_parser_stop_action(action)?;
}
let mut ready_owner = ChildDocumentScriptSchedulerOwner::new(self);
for work in ready_work {
@@ -5317,7 +5317,7 @@ mod tests {
vm: &mut ScriptVm,
label: &str,
) {
for transition in ["interactive", "DOMContentLoaded", "complete"] {
for transition in ["DOMContentLoaded", "complete"] {
assert!(
vm.run_child_frame_task_source_once_for_test(
ChildFrameSemanticTurnKind::DocumentLifecycle,
@@ -5355,7 +5355,6 @@ mod tests {
label: &str,
) {
for expected in [
ChildFrameSemanticTurnKind::DocumentLifecycle,
ChildFrameSemanticTurnKind::DocumentLifecycle,
ChildFrameSemanticTurnKind::DocumentLifecycle,
ChildFrameSemanticTurnKind::HostLoad,
@@ -110,8 +110,8 @@ impl ScriptVm {
/// The main-document lifecycle coordinator owns the ordinary task-end
/// checkpoint. Keeping this primitive body-only prevents DCL and
/// `readystatechange` helpers from silently becoming a second completion
/// authority. Parser-finish DCL temporarily uses the explicit compatibility
/// wrapper below until P5-A3 moves that direct successor as one unit.
/// authority. Parser completion admits DCL to the DOM task source after
/// finishing its own task-end checkpoint.
pub(super) fn dispatch_document_lifecycle_event_body(
&mut self,
event_type: &str,
@@ -908,6 +908,32 @@ impl ScriptVm {
Some(ParserFinishDomContentLoadedWork::new(owner, *work))
}
pub(crate) fn main_document_window_load_task_is_ready(
&mut self,
owner: FrameDocumentTaskOwner,
) -> Option<bool> {
let has_runtime_delay = self.has_post_domcontentloaded_load_delaying_runtime_work();
self._context_host
.borrow()
.current_main_document_complete_transition_is_ready(owner)
.map(|ready| ready && !has_runtime_delay)
}
pub(crate) fn queue_main_document_lifecycle_dom_task(
&self,
body: super::MainDocumentLifecycleBody,
completion: Option<
tokio::sync::oneshot::Sender<
crate::page_task_queue::RendererPageMainDocumentLifecycleCompletion,
>,
>,
) -> anyhow::Result<()> {
self._context_host
.borrow()
.page_main_document_lifecycle_sender()
.send(body, completion)
}
/// Consume the completed parser task and claim only its exact, already
/// queued DOMContentLoaded successor.
///
@@ -1840,21 +1866,6 @@ impl ScriptVm {
reached_boundary,
invalidation_policy,
} = PostParseProcessingAction::from_document_processing_action(action);
if work.is_domcontentloaded_task()
&& self
.document_runtime
.has_ready_timeout_queued_by_classic_defer_script()
{
// The lifecycle plan pre-seeds this boundary marker so owner work
// stays ordered. It has now reached the front after defer-like and
// parser-owned pre-DCL work, which is the point at which HTML queues
// the actual DOMContentLoaded global task. Give only ready timers
// scheduled by classic defer scripts their intervening task turn.
page_task_queue.enqueue_front_post_parse_work_preserving_order(vec![work]);
return PostParseDriverStep::Ready(Box::new(
ReadyPostParseAction::TimerQueuedByClassicDeferBeforeDomContentLoaded,
));
}
if !(work.is_domcontentloaded_task() || work.is_window_load_task()) {
return PostParseDriverStep::Ready(Box::new(ReadyPostParseAction::Processing(
Box::new(PostParseProcessingAction {
@@ -1977,11 +1988,6 @@ impl ScriptVm {
ReadyPostParseAction::Processing(action) => {
return Ok(PostParseProcessingStep::Action(action));
}
ReadyPostParseAction::TimerQueuedByClassicDeferBeforeDomContentLoaded => {
return Ok(
PostParseProcessingStep::TimerQueuedByClassicDeferBeforeDomContentLoaded,
);
}
},
PostParseDriverStep::NeedsContinuation => {
return Ok(PostParseProcessingStep::NeedsContinuation);
@@ -2020,11 +2026,6 @@ impl ScriptVm {
execution.into_page_owned_task(),
)));
}
PostParseProcessingStep::TimerQueuedByClassicDeferBeforeDomContentLoaded => {
return Ok(
PostParseLifecycleAdvance::TimerQueuedByClassicDeferBeforeDomContentLoaded,
);
}
PostParseProcessingStep::NeedsContinuation => {
return Ok(PostParseLifecycleAdvance::NeedsContinuation);
}
@@ -28,7 +28,6 @@ pub(crate) enum PostParseDrainResult {
pub(crate) enum PostParseLifecycleAdvance {
PageOwnedTask(Box<PostParsePageOwnedTask>),
TimerQueuedByClassicDeferBeforeDomContentLoaded,
NeedsContinuation,
AwaitProgress,
Complete(PostParseLifecycleCompletionAction),
@@ -193,20 +192,20 @@ pub(crate) struct PostParsePageOwnedTask {
pub(super) completion: PostParseTaskCompletion,
}
/// The exact DOMContentLoaded action claimed as the direct successor of a
/// The exact DOMContentLoaded action claimed for DOM task admission after a
/// drained main-parser queue.
///
/// This value is deliberately short-lived: the existing post-parse queue and
/// lifecycle driver remain the only durable authority. The wrapper merely
/// prevents an already-claimed DCL action from falling back to a generic
/// page-owned task before the parser continuation commits it.
/// page-owned task before the parser continuation admits it to the DOM source.
pub(crate) struct ParserFinishDomContentLoadedTask {
owner: FrameDocumentTaskOwner,
task: PostParsePageOwnedTask,
}
/// The exact parse-time DOMContentLoaded work claimed as the direct successor
/// of a drained main-parser queue.
/// The exact parse-time DOMContentLoaded work claimed for DOM task admission
/// after a drained main-parser queue.
///
/// Unlike [`ParserFinishDomContentLoadedTask`], phase one has no installed
/// post-parse driver task token to complete. The lifecycle authority still
@@ -244,7 +243,7 @@ impl ParserFinishDomContentLoadedTask {
self.task.take_work_for_execution()
}
pub(crate) fn into_completed_task(self) -> PostParsePageOwnedTask {
pub(crate) fn into_pending_task(self) -> PostParsePageOwnedTask {
self.task
}
}
@@ -323,7 +322,6 @@ impl PostParseProcessingAction {
pub(crate) enum ReadyPostParseAction {
Processing(Box<PostParseProcessingAction>),
TimerQueuedByClassicDeferBeforeDomContentLoaded,
}
pub(crate) enum PostParseRuntimeDriverStep {
@@ -340,7 +338,6 @@ pub(crate) enum PostParseDriverStep {
pub(crate) enum PostParseProcessingStep {
Action(Box<PostParseProcessingAction>),
TimerQueuedByClassicDeferBeforeDomContentLoaded,
NeedsContinuation,
AwaitProgress,
Idle,
@@ -6068,7 +6068,7 @@ impl ScriptVm {
}
};
self.apply_pending_child_document_owner_retirements();
let (application, body_activity) = application;
let (application, mut body_activity) = application;
let Some(application) = application else {
return Ok(CurrentChildDocumentLoadApplication::Applied { body_activity });
};
@@ -6076,9 +6076,13 @@ impl ScriptVm {
if let Some(transition) = owner_transition {
self.apply_child_document_owner_transition(transition);
}
if let Some(action) = parser_stop_action {
super::child_document_lifecycle::ChildDocumentLifecycleOwner::new(self)
.notify_parser_stop_action(action);
if let Some(action) = parser_stop_action
&& super::child_document_lifecycle::ChildDocumentLifecycleOwner::new(self)
.notify_parser_stop_action(action)?
== crate::frame_owner_model::FrameDocumentLifecycleTaskEffect::EventDispatched
{
body_activity =
crate::native_bridge::ChildDocumentLoadBodyActivity::PageCodeOrEventDispatch;
}
if let Some(work) = work {
super::child_document_script_scheduler::ChildDocumentScriptSchedulerOwner::new(self)
@@ -11654,7 +11654,7 @@ async fn iframe_javascript_url_string_completion_replaces_child_document() {
"javascript URL should execute on DocumentScriptReady",
)
.await;
for transition in ["interactive", "DOMContentLoaded", "complete"] {
for transition in ["DOMContentLoaded", "complete"] {
expect_child_frame_task_source_after_realm_prerequisite(
&mut vm,
ChildFrameSemanticTurnKind::DocumentLifecycle,
@@ -13644,7 +13644,7 @@ async fn child_window_load_replacement_stops_old_delivery_before_owner_output()
"old Window load listener must retain a current callback relevant realm"
);
}
for context in ["old interactive", "old DOMContentLoaded", "old complete"] {
for context in ["old DOMContentLoaded", "old complete"] {
expect_child_frame_task_source_after_realm_prerequisite(
&mut vm,
ChildFrameSemanticTurnKind::DocumentLifecycle,
@@ -13696,11 +13696,7 @@ async fn child_window_load_replacement_stops_old_delivery_before_owner_output()
"replacement child load handlers should install",
)
.await;
for context in [
"replacement interactive",
"replacement DOMContentLoaded",
"replacement complete",
] {
for context in ["replacement DOMContentLoaded", "replacement complete"] {
expect_child_frame_task_source_after_realm_prerequisite(
&mut vm,
ChildFrameSemanticTurnKind::DocumentLifecycle,
@@ -13855,7 +13851,7 @@ async fn child_pageshow_replacement_stops_old_frame_finish_and_protocol_output()
"old Window lifecycle listeners must retain current callback relevant realms"
);
}
for context in ["old interactive", "old DOMContentLoaded", "old complete"] {
for context in ["old DOMContentLoaded", "old complete"] {
expect_child_frame_task_source_after_realm_prerequisite(
&mut vm,
ChildFrameSemanticTurnKind::DocumentLifecycle,
@@ -13906,11 +13902,7 @@ async fn child_pageshow_replacement_stops_old_frame_finish_and_protocol_output()
"replacement pageshow handlers should install",
)
.await;
for context in [
"replacement interactive",
"replacement DOMContentLoaded",
"replacement complete",
] {
for context in ["replacement DOMContentLoaded", "replacement complete"] {
expect_child_frame_task_source_after_realm_prerequisite(
&mut vm,
ChildFrameSemanticTurnKind::DocumentLifecycle,
@@ -14088,13 +14080,12 @@ async fn child_static_media_delays_complete_and_iframe_load_until_loadeddata() {
"child media parser script should run before lifecycle",
)
.await;
expect_page_child_frame_task_source_after_realm_prerequisite(
&mut vm,
&loader,
ChildFrameSemanticTurnKind::DocumentLifecycle,
"child media document should enter interactive and accept its media token",
)
.await;
assert_eq!(
vm.eval("document.querySelector('iframe').contentDocument.readyState")
.expect("child readiness"),
"interactive",
"parser EOF must apply interactive synchronously"
);
expect_page_child_frame_task_source_after_realm_prerequisite(
&mut vm,
&loader,
@@ -14212,13 +14203,12 @@ async fn child_media_network_failure_releases_lifecycle_before_later_host_load()
"child media parser script should install listeners",
)
.await;
expect_page_child_frame_task_source_after_realm_prerequisite(
&mut vm,
&loader,
ChildFrameSemanticTurnKind::DocumentLifecycle,
"child failed media document should become interactive",
)
.await;
assert_eq!(
vm.eval("document.querySelector('iframe').contentDocument.readyState")
.expect("child readiness"),
"interactive",
"parser EOF must apply interactive synchronously"
);
let request = request_rx.await.expect("child media request should arrive");
assert!(
request
@@ -14339,13 +14329,12 @@ async fn child_image_network_failure_releases_lifecycle_before_later_host_load()
"child image parser script should install listeners",
)
.await;
expect_page_child_frame_task_source_after_realm_prerequisite(
&mut vm,
&loader,
ChildFrameSemanticTurnKind::DocumentLifecycle,
"child image document should become interactive",
)
.await;
assert_eq!(
vm.eval("document.querySelector('iframe').contentDocument.readyState")
.expect("child readiness"),
"interactive",
"parser EOF must apply interactive synchronously"
);
let request = request_rx.await.expect("child image request should arrive");
assert!(
request
@@ -14518,13 +14507,12 @@ async fn child_dynamic_media_accepted_during_dcl_delays_later_load_turns() {
"dynamic media parser script should install its DCL producer",
)
.await;
expect_page_child_frame_task_source_after_realm_prerequisite(
&mut vm,
&loader,
ChildFrameSemanticTurnKind::DocumentLifecycle,
"dynamic media document should enter interactive",
)
.await;
assert_eq!(
vm.eval("document.querySelector('iframe').contentDocument.readyState")
.expect("child readiness"),
"interactive",
"parser EOF must apply interactive synchronously"
);
expect_page_child_frame_task_source_after_realm_prerequisite(
&mut vm,
&loader,
@@ -14617,13 +14605,12 @@ async fn child_static_text_track_starts_at_interactive_without_own_load_token()
"child track parser script should install its listener",
)
.await;
expect_page_child_frame_task_source_after_realm_prerequisite(
&mut vm,
&loader,
ChildFrameSemanticTurnKind::DocumentLifecycle,
"interactive should start the static child track",
)
.await;
assert_eq!(
vm.eval("document.querySelector('iframe').contentDocument.readyState")
.expect("child readiness"),
"interactive",
"parser EOF must apply interactive synchronously"
);
assert!(
vm.run_one_dom_manipulation_task_executor_turn(
PageDomManipulationTestFamily::TextTrackDefaultMode,
@@ -14746,12 +14733,12 @@ async fn child_document_replacement_retires_media_sequence_and_delay() {
"first media document parser script should run",
)
.await;
expect_child_frame_task_source_after_realm_prerequisite(
&mut vm,
ChildFrameSemanticTurnKind::DocumentLifecycle,
"first media document should enter interactive",
)
.await;
assert_eq!(
vm.eval("document.querySelector('iframe').contentDocument.readyState")
.expect("child readiness"),
"interactive",
"parser EOF must apply interactive synchronously"
);
expect_child_frame_task_source_after_realm_prerequisite(
&mut vm,
ChildFrameSemanticTurnKind::DocumentLifecycle,
@@ -14796,7 +14783,6 @@ async fn child_document_replacement_retires_media_sequence_and_delay() {
)
.await;
for (source, transition) in [
(ChildFrameSemanticTurnKind::DocumentLifecycle, "interactive"),
(
ChildFrameSemanticTurnKind::DocumentLifecycle,
"DOMContentLoaded",
@@ -14868,13 +14854,12 @@ async fn moving_pending_child_media_restarts_under_the_new_document_owner() {
"moving media parser script should run",
)
.await;
expect_page_child_frame_task_source_after_realm_prerequisite(
&mut vm,
&loader,
ChildFrameSemanticTurnKind::DocumentLifecycle,
"moving media document should enter interactive",
)
.await;
assert_eq!(
vm.eval("document.querySelector('iframe').contentDocument.readyState")
.expect("child readiness"),
"interactive",
"parser EOF must apply interactive synchronously"
);
expect_page_child_frame_task_source_after_realm_prerequisite(
&mut vm,
&loader,
@@ -15014,12 +14999,12 @@ async fn child_image_event_delay_blocks_complete_and_host_load_until_terminal()
"child image parser script should run before lifecycle",
)
.await;
expect_child_frame_task_source_after_realm_prerequisite(
&mut vm,
ChildFrameSemanticTurnKind::DocumentLifecycle,
"child image document should enter interactive",
)
.await;
assert_eq!(
vm.eval("document.querySelector('iframe').contentDocument.readyState")
.expect("child readiness"),
"interactive",
"parser EOF must apply interactive synchronously"
);
expect_child_frame_task_source_after_realm_prerequisite(
&mut vm,
ChildFrameSemanticTurnKind::DocumentLifecycle,
@@ -15117,12 +15102,12 @@ async fn child_document_replacement_cancels_stale_image_event_and_delay() {
"first image document parser script should run",
)
.await;
expect_child_frame_task_source_after_realm_prerequisite(
&mut vm,
ChildFrameSemanticTurnKind::DocumentLifecycle,
"first image document should enter interactive",
)
.await;
assert_eq!(
vm.eval("document.querySelector('iframe').contentDocument.readyState")
.expect("child readiness"),
"interactive",
"parser EOF must apply interactive synchronously"
);
expect_child_frame_task_source_after_realm_prerequisite(
&mut vm,
ChildFrameSemanticTurnKind::DocumentLifecycle,
@@ -15166,8 +15151,19 @@ async fn child_document_replacement_cancels_stale_image_event_and_delay() {
"replacement parser script should run",
)
.await;
assert!(
vm.apply_next_image_load_event_body_for_test()
.expect("stale image DOM task"),
"the earlier image task must retire at the shared DOM FIFO head"
);
assert_eq!(
vm.eval("__childImageReplacementEvents.join('|')")
.expect("retired image trace"),
"first-dcl",
"the stale image task must not dispatch into the replacement"
);
for (source, transition) in [
(ChildFrameSemanticTurnKind::DocumentLifecycle, "interactive"),
(
ChildFrameSemanticTurnKind::DocumentLifecycle,
"DOMContentLoaded",
@@ -15233,12 +15229,12 @@ async fn moving_pending_child_image_rebinds_event_without_consuming_new_request(
"moving image parser script should run",
)
.await;
expect_child_frame_task_source_after_realm_prerequisite(
&mut vm,
ChildFrameSemanticTurnKind::DocumentLifecycle,
"moving image document should enter interactive",
)
.await;
assert_eq!(
vm.eval("document.querySelector('iframe').contentDocument.readyState")
.expect("child readiness"),
"interactive",
"parser EOF must apply interactive synchronously"
);
expect_child_frame_task_source_after_realm_prerequisite(
&mut vm,
ChildFrameSemanticTurnKind::DocumentLifecycle,
@@ -15266,6 +15262,17 @@ async fn moving_pending_child_image_rebinds_event_without_consuming_new_request(
"#,
)
.expect("pending child image should move to the parent document");
assert!(
vm.apply_next_image_load_event_body_for_test()
.expect("old image DOM task"),
"the old image task must retire before the later complete task at the shared FIFO head"
);
assert_eq!(
vm.eval("__movedChildImageEvents.join('|')")
.expect("old image trace"),
"child-dcl",
"retiring the old image task must leave the rebound request pending"
);
expect_child_frame_task_source_after_realm_prerequisite(
&mut vm,
ChildFrameSemanticTurnKind::DocumentLifecycle,
@@ -16035,12 +16042,12 @@ async fn child_document_close_without_defer_queues_replacement_domcontentloaded(
"child parser script should install the original DCL handler",
)
.await;
expect_child_frame_task_source_after_realm_prerequisite(
&mut vm,
ChildFrameSemanticTurnKind::DocumentLifecycle,
"original child document should become interactive",
)
.await;
assert_eq!(
vm.eval("document.querySelector('iframe').contentDocument.readyState")
.expect("child readiness"),
"interactive",
"parser EOF must apply interactive synchronously"
);
expect_child_frame_task_source_after_realm_prerequisite(
&mut vm,
ChildFrameSemanticTurnKind::DocumentLifecycle,
@@ -16150,10 +16157,16 @@ async fn child_script_document_open_after_location_navigation_is_noop() {
.await,
"the blob URL assigned by the child script must commit on a later navigation turn"
);
expect_one_child_frame_task_source(
&mut vm,
ChildFrameSemanticTurnKind::RealmMaterialization,
"blob replacement must materialize its exact realm",
)
.await;
expect_one_child_frame_task_source(
&mut vm,
ChildFrameSemanticTurnKind::DocumentLifecycle,
"the queued srcdoc lifecycle task must stale-discard before blob lifecycle work",
"the older srcdoc DCL must stale-discard at the shared DOM head",
)
.await;
for transition in ["interactive", "DOMContentLoaded", "complete"] {
+34 -31
View File
@@ -4752,11 +4752,16 @@ async fn run_child_document_lifecycle_and_host_load_for_test(vm: &mut ScriptVm,
// Only consecutive materialization tasks at the stable family head
// belong here. Never jump over an earlier DocumentScriptReady task.
}
assert!(
vm.run_child_frame_task_source_once_for_test(ChildFrameSemanticTurnKind::DocumentLifecycle)
if matches!(vm._page_task_residence_for_executor_test.as_ref().expect("child fixture sources").task_sources().next_child_frame_task_target(), Some(crate::page_task_queue::RendererPageChildFrameTaskTarget::DocumentLifecycle(target)) if matches!(target.action(), crate::frame_owner_model::FrameDocumentLifecycleAction::Interactive(_)))
{
assert!(
vm.run_child_frame_task_source_once_for_test(
ChildFrameSemanticTurnKind::DocumentLifecycle
)
.await,
"{message}: DocumentLifecycle should make the installed document interactive"
);
"{message}: a child without a parser realm must apply its queued interactive transition"
);
}
assert!(
vm.run_child_frame_task_source_once_for_test(ChildFrameSemanticTurnKind::DocumentLifecycle)
.await,
@@ -7147,7 +7152,7 @@ async fn child_srcdoc_inline_classic_script_runs_as_frame_script_job() {
"child:true|current:inline-classic",
"DocumentScriptReady should execute the inline classic script without firing iframe load"
);
for transition in ["interactive", "DOMContentLoaded", "complete"] {
for transition in ["DOMContentLoaded", "complete"] {
assert!(
vm.run_child_frame_task_source_once_for_test(
ChildFrameSemanticTurnKind::DocumentLifecycle
@@ -7264,7 +7269,7 @@ async fn child_inline_classic_script_moved_from_original_document_is_skipped() {
"after:after-stale-inline",
"parser continuation should run the next inline classic script without firing iframe load"
);
for transition in ["interactive", "DOMContentLoaded", "complete"] {
for transition in ["DOMContentLoaded", "complete"] {
run_realm_prerequisite_then_expected_child_frame_semantic_turn_for_test(
&mut vm,
ChildFrameSemanticTurnKind::DocumentLifecycle,
@@ -7593,7 +7598,7 @@ async fn child_external_classic_script_load_executes_as_frame_script_job() {
"external:true|external-current:external-classic|external-write:true|script-load|inline-current:after-external-classic|inline:73",
"ignored document.open() must leave the parser owner alive for the following inline script"
);
for transition in ["interactive", "DOMContentLoaded", "complete"] {
for transition in ["DOMContentLoaded", "complete"] {
run_page_realm_prerequisite_then_expected_child_frame_semantic_turn(
&mut vm,
&loader,
@@ -8935,10 +8940,11 @@ async fn child_inline_parser_module_executes_from_registered_pending_script() {
"parser should continue past the deferred inline module after graph start",
)
.await;
assert!(
vm.run_child_frame_task_source_once_for_test(ChildFrameSemanticTurnKind::DocumentLifecycle)
.await,
"parser EOF should make the child document interactive before defer execution"
assert_eq!(
vm.eval("document.querySelector('iframe').contentDocument.readyState")
.expect("child readiness"),
"interactive",
"parser EOF must apply interactive synchronously"
);
assert!(
vm.run_child_frame_task_source_once_for_test(
@@ -9048,13 +9054,12 @@ async fn child_external_parser_module_executes_from_document_ready_lane() {
"parser should continue past module-defer script from DocumentScriptReady after fetch start",
)
.await;
run_page_realm_prerequisite_then_expected_child_frame_semantic_turn(
&mut vm,
&loader,
ChildFrameSemanticTurnKind::DocumentLifecycle,
"child parser module should enter interactive before module-defer execution",
)
.await;
assert_eq!(
vm.eval("document.querySelector('iframe').contentDocument.readyState")
.expect("child readiness"),
"interactive",
"parser EOF must apply interactive synchronously"
);
assert!(
!vm.run_one_child_frame_task_executor_turn(ChildFrameSemanticTurnKind::HostLoad, &loader)
.await
@@ -9194,7 +9199,7 @@ async fn child_dynamic_import_root_fetch_uses_child_import_map_and_initiator_url
.expect("child dynamic import owner ready flag should evaluate"),
"true"
);
for transition in ["interactive", "DOMContentLoaded", "complete"] {
for transition in ["DOMContentLoaded", "complete"] {
run_realm_prerequisite_then_expected_child_frame_semantic_turn_for_test(
&mut vm,
ChildFrameSemanticTurnKind::DocumentLifecycle,
@@ -9451,14 +9456,11 @@ async fn child_external_classic_source_error_dispatches_before_later_inline() {
"script-error|after-inline",
"parser continuation should execute the following inline script without firing iframe load"
);
assert!(
vm.run_one_child_frame_task_executor_turn(
ChildFrameSemanticTurnKind::DocumentLifecycle,
&loader,
)
.await
.expect("child interactive task should use the selected-task dispatcher"),
"parser EOF should dispatch interactive before HostLoad"
assert_eq!(
vm.eval("document.querySelector('iframe').contentDocument.readyState")
.expect("child readiness"),
"interactive",
"parser EOF must apply interactive synchronously"
);
assert!(
vm.run_one_child_frame_task_executor_turn(
@@ -9596,10 +9598,11 @@ async fn child_inline_classic_throw_reports_to_child_window_and_continues() {
"child-error:Uncaught Error: child-boom:child-boom:true|after-inline:true",
"parser continuation should run the following inline script without firing iframe load"
);
assert!(
vm.run_child_frame_task_source_once_for_test(ChildFrameSemanticTurnKind::DocumentLifecycle)
.await,
"parser EOF should dispatch interactive before HostLoad"
assert_eq!(
vm.eval("document.querySelector('iframe').contentDocument.readyState")
.expect("child readiness"),
"interactive",
"parser EOF must apply interactive synchronously"
);
assert!(
vm.run_child_frame_task_source_once_for_test(ChildFrameSemanticTurnKind::DocumentLifecycle)
@@ -128,7 +128,8 @@ addEventListener("error", () => {
base_url: document_url.clone(),
initiator_url: document_url,
},
);
)
.expect("parser import-map handoff should be admitted");
assert_eq!(
vm.eval_without_microtask_checkpoint_for_test("__importMapFailureOrder.join('|')")
@@ -182,7 +183,8 @@ fn parser_import_map_registration_obeys_inline_script_csp() {
base_url: document_url.clone(),
initiator_url: document_url,
},
);
)
.expect("parser import-map handoff should be admitted");
}
let base_url = Url::parse("https://script-terminal.test/import-map.html").unwrap();