diff --git a/moli-renderer-v8/src/document_runtime/mutation_commands.rs b/moli-renderer-v8/src/document_runtime/mutation_commands.rs index 970cea940d..9d1f37e6c1 100644 --- a/moli-renderer-v8/src/document_runtime/mutation_commands.rs +++ b/moli-renderer-v8/src/document_runtime/mutation_commands.rs @@ -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, diff --git a/moli-renderer-v8/src/document_script_scheduler/module_ready.rs b/moli-renderer-v8/src/document_script_scheduler/module_ready.rs index fd1850618d..b06327a83d 100644 --- a/moli-renderer-v8/src/document_script_scheduler/module_ready.rs +++ b/moli-renderer-v8/src/document_script_scheduler/module_ready.rs @@ -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, ) -> 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, - 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, - load_delay_token: DocumentLoadDelayTokenId, + load_delay_token: impl Into, ) -> 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 { script_handle: DomHandle, request_key: ModuleMapKey, tree_id: Option, - load_delay_token: DocumentLoadDelayTokenId, + load_delay_token: impl Into, error: ModuleLoadError, ) -> Self { Self::with_target( @@ -404,7 +405,7 @@ impl ModuleScriptGraphFailedWork { 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 { script_handle: DomHandle, request_key: ModuleMapKey, tree_id: module_tree::ModuleTreeId, - load_delay_token: DocumentLoadDelayTokenId, + load_delay_token: impl Into, graph: ModuleGraphHandle, ) -> Self { Self::with_target( @@ -489,7 +490,7 @@ impl ModuleScriptGraphReadyWork { 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() } } diff --git a/moli-renderer-v8/src/frame_owner_model.rs b/moli-renderer-v8/src/frame_owner_model.rs index 8418fe3108..86a95b1716 100644 --- a/moli-renderer-v8/src/frame_owner_model.rs +++ b/moli-renderer-v8/src/frame_owner_model.rs @@ -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, diff --git a/moli-renderer-v8/src/frame_owner_model/frame_task_sources/document_script_ready.rs b/moli-renderer-v8/src/frame_owner_model/frame_task_sources/document_script_ready.rs index cf27efe381..3b0bef5ee7 100644 --- a/moli-renderer-v8/src/frame_owner_model/frame_task_sources/document_script_ready.rs +++ b/moli-renderer-v8/src/frame_owner_model/frame_task_sources/document_script_ready.rs @@ -80,6 +80,7 @@ pub(crate) enum FrameDocumentRealmBoundScriptWork { #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub(crate) enum FrameDocumentScriptWorkAdmission { QueuedBehindRealm, + QueuedBehindScript, Runnable, } diff --git a/moli-renderer-v8/src/frame_owner_model/lifecycle_tasks.rs b/moli-renderer-v8/src/frame_owner_model/lifecycle_tasks.rs index 6a624156a0..f31d688214 100644 --- a/moli-renderer-v8/src/frame_owner_model/lifecycle_tasks.rs +++ b/moli-renderer-v8/src/frame_owner_model/lifecycle_tasks.rs @@ -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 for ChildDocumentModuleScriptLoadDelay { + fn from(token: DocumentLoadDelayTokenId) -> Self { + Self::Pending(token) + } +} + +impl ChildDocumentModuleScriptLoadDelay { + pub(crate) const fn token(self) -> Option { + 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 diff --git a/moli-renderer-v8/src/frame_owner_model/module_clients.rs b/moli-renderer-v8/src/frame_owner_model/module_clients.rs index 5dd609ecf1..7d01f022bd 100644 --- a/moli-renderer-v8/src/frame_owner_model/module_clients.rs +++ b/moli-renderer-v8/src/frame_owner_model/module_clients.rs @@ -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, ) -> 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, script_handle: DomHandle, script: PreparedScript, - load_delay_token: DocumentLoadDelayTokenId, + load_delay_token: impl Into, ) -> Self { assert_eq!(script.kind, crate::types::ScriptKind::Module); assert_eq!(pending_script_id.owner(), owner.document_owner()); diff --git a/moli-renderer-v8/src/frame_owner_model/module_graph/parser_roots.rs b/moli-renderer-v8/src/frame_owner_model/module_graph/parser_roots.rs index 9c42bf08ac..7acbdbc3bc 100644 --- a/moli-renderer-v8/src/frame_owner_model/module_graph/parser_roots.rs +++ b/moli-renderer-v8/src/frame_owner_model/module_graph/parser_roots.rs @@ -70,7 +70,7 @@ impl ChildDocumentModulatorStore { parent_key: ModuleMapKey, requests: Vec, 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(); diff --git a/moli-renderer-v8/src/frame_owner_model/module_graph/tests.rs b/moli-renderer-v8/src/frame_owner_model/module_graph/tests.rs index 1872f90c26..5dd2af69a8 100644 --- a/moli-renderer-v8/src/frame_owner_model/module_graph/tests.rs +++ b/moli-renderer-v8/src/frame_owner_model/module_graph/tests.rs @@ -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 { diff --git a/moli-renderer-v8/src/frame_owner_model/module_graph/tree_jobs.rs b/moli-renderer-v8/src/frame_owner_model/module_graph/tree_jobs.rs index 740cb3f196..14b007e536 100644 --- a/moli-renderer-v8/src/frame_owner_model/module_graph/tree_jobs.rs +++ b/moli-renderer-v8/src/frame_owner_model/module_graph/tree_jobs.rs @@ -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( diff --git a/moli-renderer-v8/src/frame_owner_model/store.rs b/moli-renderer-v8/src/frame_owner_model/store.rs index 844d477a7b..5fac398d32 100644 --- a/moli-renderer-v8/src/frame_owner_model/store.rs +++ b/moli-renderer-v8/src/frame_owner_model/store.rs @@ -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 { - self.acquire_current_child_document_load_delay( - child_handle, - owner, - DocumentLoadDelayReason::AsyncModuleScript, - ) + ) -> Option { + 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( diff --git a/moli-renderer-v8/src/frame_owner_model/store_tests.rs b/moli-renderer-v8/src/frame_owner_model/store_tests.rs index b3bab000d7..4d158a2b3a 100644 --- a/moli-renderer-v8/src/frame_owner_model/store_tests.rs +++ b/moli-renderer-v8/src/frame_owner_model/store_tests.rs @@ -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!( diff --git a/moli-renderer-v8/src/module_runtime/frame_document_parser_tree.rs b/moli-renderer-v8/src/module_runtime/frame_document_parser_tree.rs index e6b1e31ea5..04dacdfc89 100644 --- a/moli-renderer-v8/src/module_runtime/frame_document_parser_tree.rs +++ b/moli-renderer-v8/src/module_runtime/frame_document_parser_tree.rs @@ -36,7 +36,7 @@ impl NativeDocumentModulator { parent_key: ModuleMapKey, requests: Vec, 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( diff --git a/moli-renderer-v8/src/module_runtime/parser_tree_registry.rs b/moli-renderer-v8/src/module_runtime/parser_tree_registry.rs index 2e72ae9c8f..cb46cb1269 100644 --- a/moli-renderer-v8/src/module_runtime/parser_tree_registry.rs +++ b/moli-renderer-v8/src/module_runtime/parser_tree_registry.rs @@ -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 } diff --git a/moli-renderer-v8/src/native_bridge/context_host/child_documents/live_parser.rs b/moli-renderer-v8/src/native_bridge/context_host/child_documents/live_parser.rs index 0bd470f17e..c6f2ba8ce4 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/child_documents/live_parser.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/child_documents/live_parser.rs @@ -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, diff --git a/moli-renderer-v8/src/native_bridge/context_host/child_dynamic_scripts.rs b/moli-renderer-v8/src/native_bridge/context_host/child_dynamic_scripts.rs index 321f298b2a..f04e1afe58 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/child_dynamic_scripts.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/child_dynamic_scripts.rs @@ -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 { - 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( diff --git a/moli-renderer-v8/src/native_bridge/context_host/child_frames/module_scripts.rs b/moli-renderer-v8/src/native_bridge/context_host/child_frames/module_scripts.rs index cdcb952661..637d2f8fea 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/child_frames/module_scripts.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/child_frames/module_scripts.rs @@ -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) diff --git a/moli-renderer-v8/src/native_bridge/context_host/child_runtime_script_order.rs b/moli-renderer-v8/src/native_bridge/context_host/child_runtime_script_order.rs new file mode 100644 index 0000000000..41726dfaab --- /dev/null +++ b/moli-renderer-v8/src/native_bridge/context_host/child_runtime_script_order.rs @@ -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>, +} + +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 { + 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 { + 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()); + } +} diff --git a/moli-renderer-v8/src/native_bridge/context_host/core.rs b/moli-renderer-v8/src/native_bridge/context_host/core.rs index baed850e4a..5336e09c1b 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/core.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/core.rs @@ -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(), diff --git a/moli-renderer-v8/src/native_bridge/context_host/document_script_ready_inputs.rs b/moli-renderer-v8/src/native_bridge/context_host/document_script_ready_inputs.rs index eecdd1c3bd..3a9024f3cc 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/document_script_ready_inputs.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/document_script_ready_inputs.rs @@ -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. diff --git a/moli-renderer-v8/src/native_bridge/context_host/mod.rs b/moli-renderer-v8/src/native_bridge/context_host/mod.rs index 36fab43c93..166e8d7810 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/mod.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/mod.rs @@ -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, pending_child_modulepreload_work_awaiting_realm: diff --git a/moli-renderer-v8/src/runtime/page_vm/tests/child_module_document_script_ready.rs b/moli-renderer-v8/src/runtime/page_vm/tests/child_module_document_script_ready.rs index 62db957e62..eb19214f08 100644 --- a/moli-renderer-v8/src/runtime/page_vm/tests/child_module_document_script_ready.rs +++ b/moli-renderer-v8/src/runtime/page_vm/tests/child_module_document_script_ready.rs @@ -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 = '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, diff --git a/moli-renderer-v8/src/script_vm/child_document_modulator.rs b/moli-renderer-v8/src/script_vm/child_document_modulator.rs index 66fc25d22d..720ded20bf 100644 --- a/moli-renderer-v8/src/script_vm/child_document_modulator.rs +++ b/moli-renderer-v8/src/script_vm/child_document_modulator.rs @@ -570,7 +570,7 @@ impl ScriptVm { parent_key: ModuleMapKey, requests: Vec, 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( diff --git a/moli-renderer-v8/src/script_vm/native_module/child_parser_module/execution.rs b/moli-renderer-v8/src/script_vm/native_module/child_parser_module/execution.rs index 826a508632..e7e04d87b3 100644 --- a/moli-renderer-v8/src/script_vm/native_module/child_parser_module/execution.rs +++ b/moli-renderer-v8/src/script_vm/native_module/child_parser_module/execution.rs @@ -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 diff --git a/moli-renderer-v8/src/script_vm/native_module/child_ready_document_script.rs b/moli-renderer-v8/src/script_vm/native_module/child_ready_document_script.rs index 59757f2f29..b2e194cac8 100644 --- a/moli-renderer-v8/src/script_vm/native_module/child_ready_document_script.rs +++ b/moli-renderer-v8/src/script_vm/native_module/child_ready_document_script.rs @@ -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 }) }