fix(scripts): execute dynamic child modules in insertion order

This commit is contained in:
ldm0
2026-09-27 06:43:24 +08:00
parent 1690353a88
commit 9a43bda5ae
24 changed files with 547 additions and 86 deletions
@@ -2174,7 +2174,7 @@ fn start_connected_child_document_script(
source_kind,
} => {
match unsafe { &mut *host_ptr }
.queue_child_dynamic_external_classic_script_for_current_document(
.queue_child_dynamic_document_script_for_current_document(
scope,
owner_document_handle,
node,
@@ -10,7 +10,8 @@ use crate::{
document_task_lane::DocumentRealmTask,
dom::NodeId,
frame_owner_model::{
DocumentLoadDelayTokenId, FrameDocumentOwner, FrameDocumentTaskOwner, FrameRealmId,
ChildDocumentModuleScriptLoadDelay, FrameDocumentOwner, FrameDocumentTaskOwner,
FrameRealmId,
},
module_runtime::{ModuleEntryId, ModuleGraphHandle, ModuleLoadError, ModuleMapKey},
parser_module_pending::ParserPendingModuleScriptState,
@@ -118,7 +119,7 @@ pub(crate) struct FrameDocumentModuleGraphReadyPayload {
script_handle: DomHandle,
request_key: ModuleMapKey,
tree_id: module_tree::ModuleTreeId,
load_delay_token: DocumentLoadDelayTokenId,
load_delay_token: ChildDocumentModuleScriptLoadDelay,
}
pub(crate) type FrameDocumentModuleGraphReadyTarget =
@@ -200,7 +201,7 @@ impl FrameDocumentModuleGraphReadyTarget {
script_handle: DomHandle,
request_key: ModuleMapKey,
tree_id: module_tree::ModuleTreeId,
load_delay_token: DocumentLoadDelayTokenId,
load_delay_token: impl Into<ChildDocumentModuleScriptLoadDelay>,
) -> Self {
assert_eq!(pending_script_id.owner(), owner.document_owner());
Self::new(
@@ -211,7 +212,7 @@ impl FrameDocumentModuleGraphReadyTarget {
script_handle,
request_key,
tree_id,
load_delay_token,
load_delay_token: load_delay_token.into(),
},
)
}
@@ -235,7 +236,7 @@ impl FrameDocumentModuleGraphReadyTarget {
self.payload().tree_id
}
pub(crate) fn load_delay_token(&self) -> DocumentLoadDelayTokenId {
pub(crate) fn load_delay_token(&self) -> ChildDocumentModuleScriptLoadDelay {
self.payload().load_delay_token
}
}
@@ -269,7 +270,7 @@ pub(crate) struct FrameDocumentModuleGraphFailedPayload {
script_handle: DomHandle,
request_key: ModuleMapKey,
tree_id: Option<module_tree::ModuleTreeId>,
load_delay_token: DocumentLoadDelayTokenId,
load_delay_token: ChildDocumentModuleScriptLoadDelay,
}
pub(crate) type FrameDocumentModuleGraphFailedTarget =
@@ -283,7 +284,7 @@ impl FrameDocumentModuleGraphFailedTarget {
script_handle: DomHandle,
request_key: ModuleMapKey,
tree_id: Option<module_tree::ModuleTreeId>,
load_delay_token: DocumentLoadDelayTokenId,
load_delay_token: impl Into<ChildDocumentModuleScriptLoadDelay>,
) -> Self {
assert_eq!(pending_script_id.owner(), owner.document_owner());
Self::new(
@@ -294,7 +295,7 @@ impl FrameDocumentModuleGraphFailedTarget {
script_handle,
request_key,
tree_id,
load_delay_token,
load_delay_token: load_delay_token.into(),
},
)
}
@@ -318,7 +319,7 @@ impl FrameDocumentModuleGraphFailedTarget {
self.payload().tree_id
}
pub(crate) fn load_delay_token(&self) -> DocumentLoadDelayTokenId {
pub(crate) fn load_delay_token(&self) -> ChildDocumentModuleScriptLoadDelay {
self.payload().load_delay_token
}
}
@@ -362,7 +363,7 @@ impl ModuleScriptGraphFailedWork<FrameDocumentModuleGraphFailedTarget> {
script_handle: DomHandle,
request_key: ModuleMapKey,
tree_id: Option<module_tree::ModuleTreeId>,
load_delay_token: DocumentLoadDelayTokenId,
load_delay_token: impl Into<ChildDocumentModuleScriptLoadDelay>,
error: ModuleLoadError,
) -> Self {
Self::with_target(
@@ -404,7 +405,7 @@ impl ModuleScriptGraphFailedWork<FrameDocumentModuleGraphFailedTarget> {
self.target.tree_id()
}
pub(crate) fn load_delay_token(&self) -> DocumentLoadDelayTokenId {
pub(crate) fn load_delay_token(&self) -> ChildDocumentModuleScriptLoadDelay {
self.target.load_delay_token()
}
}
@@ -447,7 +448,7 @@ impl ModuleScriptGraphReadyWork<FrameDocumentModuleGraphReadyTarget> {
script_handle: DomHandle,
request_key: ModuleMapKey,
tree_id: module_tree::ModuleTreeId,
load_delay_token: DocumentLoadDelayTokenId,
load_delay_token: impl Into<ChildDocumentModuleScriptLoadDelay>,
graph: ModuleGraphHandle,
) -> Self {
Self::with_target(
@@ -489,7 +490,7 @@ impl ModuleScriptGraphReadyWork<FrameDocumentModuleGraphReadyTarget> {
self.target.tree_id()
}
pub(crate) fn load_delay_token(&self) -> DocumentLoadDelayTokenId {
pub(crate) fn load_delay_token(&self) -> ChildDocumentModuleScriptLoadDelay {
self.target.load_delay_token()
}
}
+11 -10
View File
@@ -66,16 +66,17 @@ pub(crate) use frame_task_sources::{
PendingChildExternalClassicDocumentScript, PendingChildJavascriptUrlDocumentScript,
};
pub(crate) use lifecycle_tasks::{
ChildDocumentAsyncClassicScriptLoadDelay, DocumentLinkEventOwner,
FrameDocumentCompleteLifecycleAction, FrameDocumentDomContentLoadedLifecycleAction,
FrameDocumentImageLoadEventBinding, FrameDocumentInteractiveLifecycleAction,
FrameDocumentLifecycleAction, FrameDocumentLifecycleTaskEffect,
FrameDocumentMediaLoadDelayBinding, FrameDocumentUnloadLifecycleAction,
MainDocumentCompleteLifecycleAction, MainDocumentDomContentLoadedLifecycleAction,
MainDocumentImageLoadDelayBinding, MainDocumentInteractiveLifecycleAction,
MainDocumentMediaLoadDelayBinding, MainDocumentScriptLoadDelayKind,
MainDocumentScriptLoadDelayLease, MainDocumentScriptLoadDelayRelease,
MainDocumentStyleLoadEventBinding, StylesheetSubresourceLoadDelayBinding,
ChildDocumentAsyncClassicScriptLoadDelay, ChildDocumentModuleScriptLoadDelay,
DocumentLinkEventOwner, FrameDocumentCompleteLifecycleAction,
FrameDocumentDomContentLoadedLifecycleAction, FrameDocumentImageLoadEventBinding,
FrameDocumentInteractiveLifecycleAction, FrameDocumentLifecycleAction,
FrameDocumentLifecycleTaskEffect, FrameDocumentMediaLoadDelayBinding,
FrameDocumentUnloadLifecycleAction, MainDocumentCompleteLifecycleAction,
MainDocumentDomContentLoadedLifecycleAction, MainDocumentImageLoadDelayBinding,
MainDocumentInteractiveLifecycleAction, MainDocumentMediaLoadDelayBinding,
MainDocumentScriptLoadDelayKind, MainDocumentScriptLoadDelayLease,
MainDocumentScriptLoadDelayRelease, MainDocumentStyleLoadEventBinding,
StylesheetSubresourceLoadDelayBinding,
};
pub(crate) use load_delivery_tasks::{
FrameDocumentLoadDeliveryAction, FrameDocumentLoadDeliveryAdmission,
@@ -80,6 +80,7 @@ pub(crate) enum FrameDocumentRealmBoundScriptWork {
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum FrameDocumentScriptWorkAdmission {
QueuedBehindRealm,
QueuedBehindScript,
Runnable,
}
@@ -23,6 +23,28 @@ impl ChildDocumentAsyncClassicScriptLoadDelay {
}
}
/// A module inserted after `load` still runs without reopening the load gate.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum ChildDocumentModuleScriptLoadDelay {
Pending(DocumentLoadDelayTokenId),
AlreadyUnblocked,
}
impl From<DocumentLoadDelayTokenId> for ChildDocumentModuleScriptLoadDelay {
fn from(token: DocumentLoadDelayTokenId) -> Self {
Self::Pending(token)
}
}
impl ChildDocumentModuleScriptLoadDelay {
pub(crate) const fn token(self) -> Option<DocumentLoadDelayTokenId> {
match self {
Self::Pending(token) => Some(token),
Self::AlreadyUnblocked => None,
}
}
}
/// Result of consuming one exact main-Document script load-delay lease.
///
/// This reports a durable load-gate state transition. It does not itself wake
@@ -10,13 +10,12 @@ use crate::module_runtime::{
};
use crate::planning::{PreparedScript, ScriptFetchMetadata};
use super::lifecycle_tasks::DocumentLinkEventOwner;
use super::lifecycle_tasks::{ChildDocumentModuleScriptLoadDelay, DocumentLinkEventOwner};
use super::module_graph::{
FrameDocumentDynamicImportTerminalPreparedAction, FrameDocumentModuleTerminalQueueFollowup,
};
use super::records::{
DocumentLoadDelayTokenId, FrameDocumentOwner, FrameDocumentTaskOwner, FrameRealmId,
FrameRequestId, FrameRequestKind,
FrameDocumentOwner, FrameDocumentTaskOwner, FrameRealmId, FrameRequestId, FrameRequestKind,
};
/// Exact PageVm-local execution target captured when a child module fetch is
@@ -176,7 +175,7 @@ pub(crate) struct FrameDocumentParserRootModuleClient {
base_url: url::Url,
fetch_metadata: ScriptFetchMetadata,
source_is_external: bool,
load_delay_token: DocumentLoadDelayTokenId,
load_delay_token: ChildDocumentModuleScriptLoadDelay,
}
impl FrameDocumentParserRootModuleClient {
@@ -187,7 +186,7 @@ impl FrameDocumentParserRootModuleClient {
base_url: url::Url,
fetch_metadata: ScriptFetchMetadata,
source_is_external: bool,
load_delay_token: DocumentLoadDelayTokenId,
load_delay_token: impl Into<ChildDocumentModuleScriptLoadDelay>,
) -> Self {
Self {
pending_script_key,
@@ -196,7 +195,7 @@ impl FrameDocumentParserRootModuleClient {
base_url,
fetch_metadata,
source_is_external,
load_delay_token,
load_delay_token: load_delay_token.into(),
}
}
@@ -227,7 +226,7 @@ impl FrameDocumentParserRootModuleClient {
self.source_is_external
}
pub(crate) fn load_delay_token(&self) -> DocumentLoadDelayTokenId {
pub(crate) fn load_delay_token(&self) -> ChildDocumentModuleScriptLoadDelay {
self.load_delay_token
}
}
@@ -274,7 +273,7 @@ impl FrameDocumentParserModuleRootStartTask {
pending_script_id: ParserPendingScriptId<FrameDocumentOwner>,
script_handle: DomHandle,
script: PreparedScript,
load_delay_token: DocumentLoadDelayTokenId,
load_delay_token: impl Into<ChildDocumentModuleScriptLoadDelay>,
) -> Self {
assert_eq!(script.kind, crate::types::ScriptKind::Module);
assert_eq!(pending_script_id.owner(), owner.document_owner());
@@ -70,7 +70,7 @@ impl ChildDocumentModulatorStore {
parent_key: ModuleMapKey,
requests: Vec<ModuleRequestRecord>,
effective_fetch_metadata: ModuleFetchMetadata,
load_delay_token: crate::frame_owner_model::DocumentLoadDelayTokenId,
load_delay_token: crate::frame_owner_model::ChildDocumentModuleScriptLoadDelay,
) -> module_tree::ModuleTreeId {
assert_eq!(pending_script_id.owner(), owner.document_owner());
let document_owner = owner.document_owner();
@@ -160,8 +160,8 @@ fn parser_root_script(handle: usize, url: &Url) -> PreparedScript {
fn parser_root_load_delay_token(
handle: usize,
) -> crate::frame_owner_model::DocumentLoadDelayTokenId {
crate::frame_owner_model::DocumentLoadDelayTokenId(handle as u64 + 1)
) -> crate::frame_owner_model::ChildDocumentModuleScriptLoadDelay {
crate::frame_owner_model::DocumentLoadDelayTokenId(handle as u64 + 1).into()
}
fn parser_root_client(handle: usize, url: &Url) -> FrameDocumentParserRootModuleClient {
@@ -352,7 +352,7 @@ pub(crate) fn module_script_graph_failed_work_from_root_client(
script: crate::planning::PreparedScript,
script_handle: DomHandle,
request_key: ModuleMapKey,
load_delay_token: crate::frame_owner_model::DocumentLoadDelayTokenId,
load_delay_token: crate::frame_owner_model::ChildDocumentModuleScriptLoadDelay,
error: ModuleLoadError,
) -> DocumentModuleGraphFailedWork {
DocumentModuleGraphFailedWork::new(
+24 -15
View File
@@ -2,15 +2,16 @@ use std::collections::{BTreeMap, HashMap, VecDeque};
use super::ids::FrameOwnerIdAllocator;
use super::lifecycle_tasks::{
ChildDocumentAsyncClassicScriptLoadDelay, DocumentLinkEventOwner,
FrameDocumentCompleteLifecycleAction, FrameDocumentDomContentLoadedLifecycleAction,
FrameDocumentImageLoadEventBinding, FrameDocumentInteractiveLifecycleAction,
FrameDocumentLifecycleAction, FrameDocumentMediaLoadDelayBinding,
MainDocumentCompleteLifecycleAction, MainDocumentDomContentLoadedLifecycleAction,
MainDocumentImageLoadDelayBinding, MainDocumentInteractiveLifecycleAction,
MainDocumentMediaLoadDelayBinding, MainDocumentScriptLoadDelayKind,
MainDocumentScriptLoadDelayLease, MainDocumentScriptLoadDelayRelease,
MainDocumentStyleLoadEventBinding, StylesheetSubresourceLoadDelayBinding,
ChildDocumentAsyncClassicScriptLoadDelay, ChildDocumentModuleScriptLoadDelay,
DocumentLinkEventOwner, FrameDocumentCompleteLifecycleAction,
FrameDocumentDomContentLoadedLifecycleAction, FrameDocumentImageLoadEventBinding,
FrameDocumentInteractiveLifecycleAction, FrameDocumentLifecycleAction,
FrameDocumentMediaLoadDelayBinding, MainDocumentCompleteLifecycleAction,
MainDocumentDomContentLoadedLifecycleAction, MainDocumentImageLoadDelayBinding,
MainDocumentInteractiveLifecycleAction, MainDocumentMediaLoadDelayBinding,
MainDocumentScriptLoadDelayKind, MainDocumentScriptLoadDelayLease,
MainDocumentScriptLoadDelayRelease, MainDocumentStyleLoadEventBinding,
StylesheetSubresourceLoadDelayBinding,
};
use super::load_event_gate::DocumentLoadGateRelease;
use super::module_clients::{
@@ -2968,12 +2969,20 @@ impl FrameOwnerStore {
&mut self,
child_handle: DomHandle,
owner: FrameDocumentTaskOwner,
) -> Option<DocumentLoadDelayTokenId> {
self.acquire_current_child_document_load_delay(
child_handle,
owner,
DocumentLoadDelayReason::AsyncModuleScript,
)
) -> Option<ChildDocumentModuleScriptLoadDelay> {
if !self.child_document_task_owner_is_current(child_handle, owner) {
return None;
}
if let Some(token) =
self.acquire_document_load_delay(owner, DocumentLoadDelayReason::AsyncModuleScript)
{
return Some(ChildDocumentModuleScriptLoadDelay::Pending(token));
}
self.documents
.get(&owner.document_id)
.filter(|document| document.local_window_id == owner.local_window_id)
.is_some_and(|document| document.lifecycle_progress.is_complete())
.then_some(ChildDocumentModuleScriptLoadDelay::AlreadyUnblocked)
}
pub(crate) fn accept_current_child_image_load_event(
@@ -4141,11 +4141,28 @@ fn child_document_script_delays_own_dcl_and_complete_readiness() {
.is_none(),
"the async-module token must delay complete without blocking DCL"
);
assert!(store.release_async_module_script_load_delay(owner, async_module));
assert!(store.release_async_module_script_load_delay(
owner,
async_module.token().expect("pending module delay")
));
let complete = store
.prepare_current_child_document_complete_transition(child_handle, owner)
.expect("the final async-module terminal should unblock complete");
assert!(store.apply_current_child_document_complete_transition(complete));
assert_eq!(
store.acquire_current_child_async_module_script_load_delay(child_handle, owner),
Some(ChildDocumentModuleScriptLoadDelay::AlreadyUnblocked),
"dynamic modules remain admissible after load without reopening its gate",
);
assert_eq!(
store.current_child_document_has_load_delay_tokens(child_handle, owner),
Some(false)
);
assert!(
store
.prepare_current_child_document_complete_transition(child_handle, owner)
.is_none()
);
let replacement_transition = store
.replace_child_document(
@@ -4191,7 +4208,10 @@ fn child_document_script_delays_own_dcl_and_complete_readiness() {
"replacement must retire parser-deferred delays owned by the old document"
);
assert!(
!store.release_async_module_script_load_delay(replacement_owner, retired_async_module),
!store.release_async_module_script_load_delay(
replacement_owner,
retired_async_module.token().expect("pending module delay")
),
"replacement must retire async-module delays owned by the old document"
);
let final_owner = final_transition
@@ -4213,7 +4233,12 @@ fn child_document_script_delays_own_dcl_and_complete_readiness() {
"bulk cancellation must consume the parser-deferred token exactly once"
);
assert!(
!store.release_async_module_script_load_delay(final_owner, replacement_async_module),
!store.release_async_module_script_load_delay(
final_owner,
replacement_async_module
.token()
.expect("pending module delay")
),
"bulk cancellation must consume the async-module token exactly once"
);
assert_eq!(
@@ -36,7 +36,7 @@ impl NativeDocumentModulator {
parent_key: ModuleMapKey,
requests: Vec<ModuleRequestRecord>,
effective_fetch_metadata: ModuleFetchMetadata,
load_delay_token: crate::frame_owner_model::DocumentLoadDelayTokenId,
load_delay_token: crate::frame_owner_model::ChildDocumentModuleScriptLoadDelay,
) -> module_tree::ModuleTreeId {
let request_count = requests.len();
let parser_tree_job = NativeModuleGraphJob::parser_owned_compiled_entry(
@@ -6,7 +6,9 @@ use moli_module_script_tree as module_tree;
use crate::document_module_graph::{ModuleEntryId, ModuleMapKey};
use crate::document_runtime::DomHandle;
use crate::document_script_scheduler::{ParserPendingScriptId, ParserPendingScriptKey};
use crate::frame_owner_model::{DocumentLoadDelayTokenId, FrameDocumentTaskOwner, FrameRealmId};
use crate::frame_owner_model::{
ChildDocumentModuleScriptLoadDelay, FrameDocumentTaskOwner, FrameRealmId,
};
use crate::planning::PreparedScript;
use super::NativeModuleGraphJob;
@@ -22,7 +24,7 @@ pub(crate) struct NativeParserModuleTreeRoot {
entry_id: ModuleEntryId,
request_count: usize,
dependency_count: usize,
load_delay_token: DocumentLoadDelayTokenId,
load_delay_token: ChildDocumentModuleScriptLoadDelay,
}
impl NativeParserModuleTreeRoot {
@@ -36,7 +38,7 @@ impl NativeParserModuleTreeRoot {
tree_id: module_tree::ModuleTreeId,
entry_id: ModuleEntryId,
request_count: usize,
load_delay_token: DocumentLoadDelayTokenId,
load_delay_token: ChildDocumentModuleScriptLoadDelay,
) -> Self {
Self {
owner,
@@ -95,7 +97,7 @@ impl NativeParserModuleTreeRoot {
self.dependency_count
}
pub(crate) fn load_delay_token(&self) -> DocumentLoadDelayTokenId {
pub(crate) fn load_delay_token(&self) -> ChildDocumentModuleScriptLoadDelay {
self.load_delay_token
}
@@ -1025,7 +1025,7 @@ impl JsContextHost {
if script.kind != ScriptKind::Module {
return ScriptDisposition::Continue;
}
if !self.queue_child_parser_module_root_for_current_document(
if !self.queue_child_module_script_for_current_document(
child_handle,
script_handle,
blocking_stylesheet_signatures,
@@ -7,7 +7,7 @@ use crate::{
impl JsContextHost {
#[allow(clippy::too_many_arguments)]
pub(crate) fn queue_child_dynamic_external_classic_script_for_current_document(
pub(crate) fn queue_child_dynamic_document_script_for_current_document(
&mut self,
scope: &mut v8::PinScope<'_, '_>,
owner_document_handle: DomHandle,
@@ -18,7 +18,9 @@ impl JsContextHost {
mode: ScriptMode,
source_kind: ScriptSourceKind,
) -> std::result::Result<bool, String> {
if kind != ScriptKind::Classic || source_kind != ScriptSourceKind::External {
if kind != ScriptKind::Module
&& (kind != ScriptKind::Classic || source_kind != ScriptSourceKind::External)
{
return Ok(false);
}
let Some(child_handle) =
@@ -26,9 +28,17 @@ impl JsContextHost {
else {
return Ok(false);
};
// Frame-document scheduling owns ordering and exact Document identities.
// This load payload is intentionally unbound to the main scheduler.
let script = build_runtime_prepared_script(
if !self.child_browsing_context_is_live(child_handle)
|| self.child_browsing_context_document_handle(child_handle)
!= Some(owner_document_handle)
|| self.dom_host().owner_document_handle(script_handle) != Some(owner_document_handle)
{
return Ok(false);
}
let Some(owner) = self.current_child_document_task_owner(child_handle) else {
return Ok(false);
};
let mut script = build_runtime_prepared_script(
preparation,
script_handle,
0,
@@ -38,14 +48,34 @@ impl JsContextHost {
kind,
mode,
)?;
Ok(
// Classic and module scripts with async=false share insertion order.
// Node allocation order does not determine execution order.
script.position = self
.child_runtime_script_order
.register(owner, script_handle, mode);
let accepted = if kind == ScriptKind::Module {
self.queue_child_module_script_for_current_document(
child_handle,
script_handle,
Default::default(),
script,
)
} else {
self.queue_child_external_classic_document_script_for_current_document(
child_handle,
owner_document_handle,
script_handle,
script,
),
)
)
};
if accepted {
let _ = self
.dom_host_mut()
.set_script_already_started(script_handle, true);
} else {
self.finish_child_runtime_script(owner, script_handle);
}
Ok(accepted)
}
pub(crate) fn execute_child_dynamic_inline_classic_script_on_current_stack(
@@ -129,7 +129,9 @@ impl JsContextHost {
self.cancel_child_classic_document_script_work(handle);
}
pub(in crate::native_bridge::context_host) fn queue_child_parser_module_root_for_current_document(
/// Parser and runtime insertions share the module graph pipeline. Only
/// ModuleDefer reserves parser order and delays DOMContentLoaded.
pub(in crate::native_bridge::context_host) fn queue_child_module_script_for_current_document(
&mut self,
handle: DomHandle,
script_handle: DomHandle,
@@ -168,6 +170,7 @@ impl JsContextHost {
let load_delay_token = if parser_ordered {
self.frame_owner_store
.acquire_current_child_parser_deferred_script_load_delay(handle, owner)
.map(Into::into)
} else {
self.frame_owner_store
.acquire_current_child_async_module_script_load_delay(handle, owner)
@@ -324,9 +327,12 @@ impl JsContextHost {
pub(crate) fn release_child_module_script_load_delay(
&mut self,
owner: FrameDocumentTaskOwner,
load_delay_token: crate::frame_owner_model::DocumentLoadDelayTokenId,
load_delay: crate::frame_owner_model::ChildDocumentModuleScriptLoadDelay,
parser_ordered: bool,
) -> bool {
let Some(load_delay_token) = load_delay.token() else {
return false;
};
if parser_ordered {
self.frame_owner_store
.release_parser_deferred_script_load_delay(owner, load_delay_token)
@@ -0,0 +1,228 @@
use std::collections::{HashMap, VecDeque};
use crate::{
document_runtime::DomHandle,
frame_owner_model::{FrameDocumentScriptReadyTaskWork, FrameDocumentTaskOwner, FrameRealmId},
types::ScriptMode,
};
struct RuntimeScriptEntry {
node: DomHandle,
position: usize,
dispatched: bool,
ready: Option<(FrameRealmId, FrameDocumentScriptReadyTaskWork)>,
}
/// The HTML in-order list retains scripts until earlier elements have run.
/// Eligible work then enters the stable Page source, which owns task FIFO.
#[derive(Default)]
pub(super) struct ChildRuntimeScriptOrder {
next_position: usize,
documents: HashMap<FrameDocumentTaskOwner, VecDeque<RuntimeScriptEntry>>,
}
impl ChildRuntimeScriptOrder {
pub(super) fn register(
&mut self,
owner: FrameDocumentTaskOwner,
node: DomHandle,
mode: ScriptMode,
) -> usize {
if let Some(entry) = self
.documents
.get(&owner)
.and_then(|entries| entries.iter().find(|entry| entry.node == node))
{
return entry.position;
}
let position = self.next_position;
self.next_position = position
.checked_add(1)
.expect("child runtime script position overflow");
if matches!(mode, ScriptMode::InOrder | ScriptMode::ModuleInOrder) {
self.documents
.entry(owner)
.or_default()
.push_back(RuntimeScriptEntry {
node,
position,
dispatched: false,
ready: None,
});
}
position
}
pub(super) fn waiting_for_predecessor(
&self,
owner: FrameDocumentTaskOwner,
node: DomHandle,
) -> bool {
self.documents.get(&owner).is_some_and(|entries| {
entries
.iter()
.position(|entry| entry.node == node)
.is_some_and(|index| index != 0)
})
}
pub(super) fn admit_or_retain(
&mut self,
realm_id: FrameRealmId,
work: FrameDocumentScriptReadyTaskWork,
) -> Option<FrameDocumentScriptReadyTaskWork> {
let route = work.route();
let Some(entries) = self.documents.get_mut(&route.task_owner()) else {
return Some(work);
};
let Some(index) = entries
.iter()
.position(|entry| entry.node == route.script_handle())
else {
return Some(work);
};
let entry = &mut entries[index];
// Evaluation reactions may arrive after initial execution was admitted.
if index == 0 || entry.dispatched {
entry.dispatched = true;
return Some(work);
}
debug_assert!(
entry.ready.is_none(),
"an ordered script has one initial ready result"
);
entry.ready = Some((realm_id, work));
None
}
pub(super) fn finish(
&mut self,
owner: FrameDocumentTaskOwner,
node: DomHandle,
) -> Option<(FrameRealmId, FrameDocumentScriptReadyTaskWork)> {
let entries = self.documents.get_mut(&owner)?;
let index = entries.iter().position(|entry| entry.node == node)?;
entries.remove(index);
let next = entries.front_mut().and_then(|entry| {
let ready = entry.ready.take()?;
entry.dispatched = true;
Some(ready)
});
if entries.is_empty() {
self.documents.remove(&owner);
}
next
}
pub(super) fn remove_document(
&mut self,
owner: FrameDocumentTaskOwner,
) -> Vec<FrameDocumentScriptReadyTaskWork> {
self.documents
.remove(&owner)
.into_iter()
.flatten()
.filter_map(|entry| entry.ready.map(|(_, work)| work))
.collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::frame_owner_model::{
DocumentId, FrameDocumentRealmBoundScriptWork, FrameSchedulerLaneId, LocalWindowId,
PendingChildDynamicDocumentScript,
};
fn owner(document: u64) -> FrameDocumentTaskOwner {
FrameDocumentTaskOwner::new(
FrameSchedulerLaneId(1),
LocalWindowId(2),
DocumentId(document),
)
}
fn work(owner: FrameDocumentTaskOwner, node: DomHandle) -> FrameDocumentScriptReadyTaskWork {
FrameDocumentRealmBoundScriptWork::DynamicClassic(PendingChildDynamicDocumentScript {
child_handle: DomHandle::new(1),
owner,
realm_id: Some(FrameRealmId(2)),
script_handle: node,
source: String::new(),
script_nonce: None,
script_integrity: None,
})
.into()
}
#[test]
fn runtime_order_uses_admission_not_node_order_and_does_not_hold_async() {
let mut order = ChildRuntimeScriptOrder::default();
let owner = owner(3);
let (first, second, third, independent) = (
DomHandle::new(40),
DomHandle::new(20),
DomHandle::new(10),
DomHandle::new(5),
);
assert!(
order.register(owner, first, ScriptMode::InOrder)
< order.register(owner, second, ScriptMode::ModuleInOrder)
);
order.register(owner, third, ScriptMode::InOrder);
order.register(owner, independent, ScriptMode::Async);
let realm = FrameRealmId(2);
assert!(order.admit_or_retain(realm, work(owner, third)).is_none());
assert!(order.admit_or_retain(realm, work(owner, second)).is_none());
assert!(
order
.admit_or_retain(realm, work(owner, independent))
.is_some()
);
assert!(order.admit_or_retain(realm, work(owner, first)).is_some());
assert_eq!(
order
.finish(owner, first)
.unwrap()
.1
.route()
.script_handle(),
second
);
// A TLA reaction for the admitted module is independent of later elements.
assert!(order.admit_or_retain(realm, work(owner, second)).is_some());
assert_eq!(
order
.finish(owner, second)
.unwrap()
.1
.route()
.script_handle(),
third
);
assert!(order.finish(owner, third).is_none());
assert!(order.documents.is_empty());
}
#[test]
fn runtime_order_rollback_and_document_retirement_leave_other_owners_runnable() {
let mut order = ChildRuntimeScriptOrder::default();
let (first, rejected, last) = (DomHandle::new(3), DomHandle::new(4), DomHandle::new(5));
let (old, current) = (owner(10), owner(11));
for node in [first, rejected, last] {
order.register(old, node, ScriptMode::ModuleInOrder);
}
let realm = FrameRealmId(2);
assert!(order.admit_or_retain(realm, work(old, last)).is_none());
assert!(order.finish(old, rejected).is_none());
order.register(current, first, ScriptMode::ModuleInOrder);
assert!(order.admit_or_retain(realm, work(current, first)).is_some());
let retired = order.remove_document(old);
assert_eq!(retired.len(), 1);
assert_eq!(retired[0].route().script_handle(), last);
assert!(order.finish(old, first).is_none());
assert!(order.finish(current, first).is_none());
assert!(order.documents.is_empty());
}
}
@@ -349,6 +349,8 @@ impl JsContextHost {
super::text_track_default_modes::TextTrackDefaultModeState::default(),
child_document_script_ready_tasks:
super::document_script_ready_inputs::ChildDocumentScriptReadyTaskLedger::default(),
child_runtime_script_order:
super::child_runtime_script_order::ChildRuntimeScriptOrder::default(),
pending_child_external_classic_document_scripts: HashMap::new(),
pending_child_modulepreload_work_awaiting_realm: VecDeque::new(),
active_child_browsing_context_host_loads: Vec::new(),
@@ -186,6 +186,12 @@ impl JsContextHost {
return false;
}
let Some(work) = self
.child_runtime_script_order
.admit_or_retain(realm_id, work)
else {
return true;
};
let task_id = self.child_document_script_ready_tasks.allocate_task_id();
let target = RendererPageChildDocumentScriptReadyTarget::new(
route.child_handle(),
@@ -202,9 +208,9 @@ impl JsContextHost {
return true;
}
if let Some(work) = self.child_document_script_ready_tasks.remove_exact(target) {
self.settle_child_document_script_ready_task_without_execution(work);
}
// A closed Page route cannot accept successors. Retire the whole
// Document queue without recursively promoting its ordered scripts.
self.retire_child_document_script_ready_tasks_for_owner(owner);
tracing::debug!(
?target,
"retired child DocumentScriptReady payload after stable route closure"
@@ -226,10 +232,10 @@ impl JsContextHost {
self.queue_child_document_script_ready_task_for_realm(work, realm_id)
}
/// Move every currently-runnable scheduler action into the single stable
/// child-frame source. Current owners without a materialized realm retain
/// their work in the scheduler store; stale owners are consumed rather
/// than blocking later Documents.
/// Admit scheduler actions through the runtime in-order list into the
/// single stable child-frame source. Current owners without a materialized
/// realm retain their work in the scheduler store; stale owners are consumed
/// rather than blocking later Documents.
pub(crate) fn admit_runnable_child_document_script_tasks(&mut self) -> usize {
let mut admitted = 0;
loop {
@@ -309,6 +315,17 @@ impl JsContextHost {
self.child_document_script_ready_tasks.remove_exact(target)
}
pub(crate) fn finish_child_runtime_script(
&mut self,
owner: FrameDocumentTaskOwner,
script_handle: crate::document_runtime::DomHandle,
) {
if let Some((realm_id, work)) = self.child_runtime_script_order.finish(owner, script_handle)
{
let _ = self.queue_child_document_script_ready_task_for_realm(work, realm_id);
}
}
pub(crate) fn discard_pending_child_document_script_ready_task(
&mut self,
task_id: RendererPageChildDocumentScriptReadyTaskId,
@@ -324,7 +341,10 @@ impl JsContextHost {
&mut self,
owner: FrameDocumentTaskOwner,
) -> usize {
let retired = self.child_document_script_ready_tasks.remove_owner(owner);
// Also drain work waiting for an earlier runtime script. This path is
// used for both Document retirement and failed realm materialization.
let mut retired = self.child_runtime_script_order.remove_document(owner);
retired.extend(self.child_document_script_ready_tasks.remove_owner(owner));
let retired_count = retired.len();
for work in retired {
self.settle_child_document_script_ready_task_without_execution(work);
@@ -418,8 +438,16 @@ impl JsContextHost {
..
} => FrameDocumentScriptWorkAdmission::QueuedBehindRealm,
};
let work = work.bind_to_realm(realm_id);
self.queue_child_document_script_ready_task_for_realm(work.into(), realm_id)
let work = FrameDocumentScriptReadyTaskWork::from(work.bind_to_realm(realm_id));
let admission = if self
.child_runtime_script_order
.waiting_for_predecessor(owner, work.route().script_handle())
{
FrameDocumentScriptWorkAdmission::QueuedBehindScript
} else {
admission
};
self.queue_child_document_script_ready_task_for_realm(work, realm_id)
.then_some(admission)
}
@@ -445,6 +473,8 @@ impl JsContextHost {
&mut self,
work: FrameDocumentScriptReadyTaskWork,
) {
let route = work.route();
self.finish_child_runtime_script(route.task_owner(), route.script_handle());
let FrameDocumentScriptReadyTaskWork::DocumentScriptExecution(work) = work else {
// Scheduler-owned parser/module state is retired together with its
// exact Document. It must not be reinserted into another queue.
@@ -80,6 +80,7 @@ mod child_dynamic_scripts;
mod child_events;
mod child_frame_navigation;
mod child_frame_runtime;
mod child_runtime_script_order;
pub(crate) use child_frame_runtime::install_child_window_proxy_access_check_handlers;
mod child_frame_snapshots;
mod child_frames;
@@ -969,6 +970,7 @@ pub(crate) struct JsContextHost {
text_track_default_modes: text_track_default_modes::TextTrackDefaultModeState,
child_document_script_ready_tasks:
document_script_ready_inputs::ChildDocumentScriptReadyTaskLedger,
child_runtime_script_order: child_runtime_script_order::ChildRuntimeScriptOrder,
pending_child_external_classic_document_scripts:
HashMap<u64, child_frames::PendingChildExternalClassicDocumentScriptLoad>,
pending_child_modulepreload_work_awaiting_realm:
@@ -1,5 +1,100 @@
use super::*;
async fn drain_child_runtime_module_tasks(page_vm: &mut PageVm) -> anyhow::Result<()> {
let loader = page_vm.request_client.clone();
for _ in 0..32 {
if page_vm
.run_next_child_frame_task_source_for_semantic_test()
.await
.is_some()
{
continue;
}
if page_vm
.run_exact_selected_page_task_for_test(
PageSelectedTaskTestSelector::ChildModuleScriptTerminal,
&loader,
)
.await?
|| page_vm
.run_exact_selected_page_task_for_test(
PageSelectedTaskTestSelector::ModuleReaction,
&loader,
)
.await?
{
continue;
}
return Ok(());
}
anyhow::bail!("child runtime module tasks did not settle in 32 turns")
}
#[tokio::test(flavor = "current_thread")]
async fn child_runtime_inline_modules_preserve_insertion_order_after_load_and_release_tla() {
run_page_vm_async_test(async move {
let loader = crate::network::ResourceRequestClient::new(&FetchConfig::default())?;
let (mut page_vm, _resource_source, _owner_wake_rx) =
page_vm_with_bound_task_sources_and_owner_wake(
&loader,
Url::parse("https://example.com/runtime-module")?,
);
page_vm.vm_mut().eval(
r#"
globalThis.events = [];
const frame = document.createElement('iframe');
frame.id = 'runtime-module';
frame.srcdoc = '<!doctype html><body>child';
frame.onload = () => events.push('frame-load');
document.body.append(frame);
"queued"
"#,
)?;
drain_child_runtime_module_tasks(&mut page_vm).await?;
assert_eq!(page_vm.vm_mut().eval("events.join('|')")?, "frame-load");
page_vm.vm_mut().eval(
r#"
(() => {
const d = document.getElementById('runtime-module').contentDocument;
const second = d.createElement('script'), first = d.createElement('script');
first.type = second.type = 'module';
first.async = second.async = false;
first.textContent = `parent.events.push('first-start');
await new Promise(resolve => parent.releaseModule = resolve);
parent.events.push('first-end');`;
second.textContent = `parent.events.push('second');`;
first.onload = () => events.push('first-load');
second.onload = () => events.push('second-load');
d.body.append(first);
d.body.append(second);
return 'queued';
})()
"#,
)?;
drain_child_runtime_module_tasks(&mut page_vm).await?;
assert_eq!(
page_vm.vm_mut().eval("events.join('|')")?,
"frame-load|first-start|second",
"a pending TLA must not hold the next ordered module or repeat document load"
);
page_vm.vm_mut().eval("releaseModule(); 'resolved'")?;
drain_child_runtime_module_tasks(&mut page_vm).await?;
assert_eq!(
page_vm.vm_mut().eval("events.join('|')")?,
"frame-load|first-start|second|first-end"
);
assert_eq!(
page_vm
.vm_mut()
.eval("document.getElementById('runtime-module').contentDocument.readyState")?,
"complete"
);
Ok::<_, anyhow::Error>(())
})
.await
.expect("child runtime inline modules should execute after load");
}
pub(super) async fn queue_child_module_document_script_ready(
page_vm: &mut PageVm,
base_url: &str,
@@ -570,7 +570,7 @@ impl ScriptVm {
parent_key: ModuleMapKey,
requests: Vec<ModuleRequestRecord>,
effective_fetch_metadata: ModuleFetchMetadata,
load_delay_token: crate::frame_owner_model::DocumentLoadDelayTokenId,
load_delay_token: crate::frame_owner_model::ChildDocumentModuleScriptLoadDelay,
) -> ModuleTreeId {
self.child_document_modulator_store
.record_compiled_parser_root(
@@ -285,7 +285,7 @@ impl ChildModuleScriptExecutionOwner<'_> {
script_handle: DomHandle,
mode: ScriptMode,
pending_script_key: crate::document_script_scheduler::ParserPendingScriptKey,
load_delay_token: crate::frame_owner_model::DocumentLoadDelayTokenId,
load_delay_token: crate::frame_owner_model::ChildDocumentModuleScriptLoadDelay,
) -> std::result::Result<(), DocumentScriptExecutionOutcome> {
let current = {
let host = self.vm._context_host.borrow();
@@ -398,7 +398,7 @@ impl ChildModuleScriptExecutionOwner<'_> {
realm_id: FrameRealmId,
mode: ScriptMode,
pending_script_key: crate::document_script_scheduler::ParserPendingScriptKey,
load_delay_token: crate::frame_owner_model::DocumentLoadDelayTokenId,
load_delay_token: crate::frame_owner_model::ChildDocumentModuleScriptLoadDelay,
) -> std::result::Result<(), DocumentScriptExecutionOutcome> {
let current = self.check_current_frame_parser_module_route(owner, realm_id);
if current.is_err() && mode == ScriptMode::ModuleDefer {
@@ -436,7 +436,7 @@ impl ChildModuleScriptExecutionOwner<'_> {
&mut self,
owner: FrameDocumentTaskOwner,
mode: ScriptMode,
load_delay_token: crate::frame_owner_model::DocumentLoadDelayTokenId,
load_delay_token: crate::frame_owner_model::ChildDocumentModuleScriptLoadDelay,
) -> bool {
let released = self
.vm
@@ -46,7 +46,8 @@ impl<'vm> ChildReadyDocumentScriptOwner<'vm> {
work: FrameDocumentScriptReadyTaskWork,
) -> ChildReadyDocumentScriptOwnerOutput<'_> {
Box::pin(async move {
match work {
let route = work.route();
let outcome = match work {
FrameDocumentScriptReadyTaskWork::Scheduler(work) => {
self.run_ready_work(work).await
}
@@ -63,7 +64,14 @@ impl<'vm> ChildReadyDocumentScriptOwner<'vm> {
.run_ready_work(work)
.await
}
}
};
// Execution of a module begins in this task; a top-level await
// continuation must not hold later in-order script elements.
self.vm
._context_host
.borrow_mut()
.finish_child_runtime_script(route.task_owner(), route.script_handle());
outcome
})
}