diff --git a/moli-core/src/page/accessibility_support/mod.rs b/moli-core/src/page/accessibility_support/mod.rs index 83d9f0607e..9a0f6badd4 100644 --- a/moli-core/src/page/accessibility_support/mod.rs +++ b/moli-core/src/page/accessibility_support/mod.rs @@ -137,10 +137,12 @@ impl Page { inspector_session_id: Option, object_id: &str, ) -> anyhow::Result { - self.start_page_command(RendererPageCommand::AccessibilityTreePayloadsForObjectId { - inspector_session_id, - object_id: object_id.to_owned(), - }) + self.start_page_command( + RendererPageCommand::accessibility_tree_payloads_for_object_id( + inspector_session_id, + object_id.to_owned(), + ), + ) } pub fn start_accessibility_node_and_ancestor_payloads_for_object_id( @@ -149,10 +151,10 @@ impl Page { object_id: &str, ) -> anyhow::Result { self.start_page_command( - RendererPageCommand::AccessibilityNodeAndAncestorPayloadsForObjectId { + RendererPageCommand::accessibility_node_and_ancestor_payloads_for_object_id( inspector_session_id, - object_id: object_id.to_owned(), - }, + object_id.to_owned(), + ), ) } @@ -163,11 +165,11 @@ impl Page { fetch_relatives: bool, ) -> anyhow::Result { self.start_page_command( - RendererPageCommand::AccessibilityPartialTreePayloadsForObjectId { + RendererPageCommand::accessibility_partial_tree_payloads_for_object_id( inspector_session_id, - object_id: object_id.to_owned(), + object_id.to_owned(), fetch_relatives, - }, + ), ) } diff --git a/moli-core/src/page/command_dispatch.rs b/moli-core/src/page/command_dispatch.rs index 98d0977d2d..5baeb00d19 100644 --- a/moli-core/src/page/command_dispatch.rs +++ b/moli-core/src/page/command_dispatch.rs @@ -18,12 +18,12 @@ pub struct PendingRuntimeInspectorCommandDispatch { /// Identifies which execution owner consumed one routable Inspector command. /// -/// `Pause` commands complete inside V8's nested message loop while the Page -/// actor is blocked. Callers must not synchronously re-enter that Page before -/// exposing the Inspector response, because only the client can resume it. +/// `Inspector` claims complete either in V8's interrupt callback or its nested +/// message loop while the Page actor is blocked. Callers must not synchronously +/// re-enter that Page before exposing the Inspector response. pub enum CompletedRuntimeInspectorCommandDispatch { Owner(Box), - Pause, + Inspector, Canceled, } @@ -349,8 +349,8 @@ impl PendingRuntimeInspectorCommandDispatch { RendererRuntimeInspectorCommandClaim::Owner => Err(anyhow::anyhow!( "runtime inspector command was claimed by owner without an owner task" )), - RendererRuntimeInspectorCommandClaim::Pause => { - Ok(CompletedRuntimeInspectorCommandDispatch::Pause) + RendererRuntimeInspectorCommandClaim::Inspector => { + Ok(CompletedRuntimeInspectorCommandDispatch::Inspector) } RendererRuntimeInspectorCommandClaim::Canceled => { Ok(CompletedRuntimeInspectorCommandDispatch::Canceled) @@ -369,8 +369,8 @@ impl PendingRuntimeInspectorCommandDispatch { .await .map(Box::new) .map(CompletedRuntimeInspectorCommandDispatch::Owner), - RendererRuntimeInspectorCommandClaim::Pause => { - Ok(CompletedRuntimeInspectorCommandDispatch::Pause) + RendererRuntimeInspectorCommandClaim::Inspector => { + Ok(CompletedRuntimeInspectorCommandDispatch::Inspector) } RendererRuntimeInspectorCommandClaim::Canceled => { Ok(CompletedRuntimeInspectorCommandDispatch::Canceled) @@ -386,8 +386,8 @@ impl PendingRuntimeInspectorCommandDispatch { completion, ))) } - RendererRuntimeInspectorCommandClaim::Pause => { - Ok(CompletedRuntimeInspectorCommandDispatch::Pause) + RendererRuntimeInspectorCommandClaim::Inspector => { + Ok(CompletedRuntimeInspectorCommandDispatch::Inspector) } RendererRuntimeInspectorCommandClaim::Canceled => { Ok(CompletedRuntimeInspectorCommandDispatch::Canceled) diff --git a/moli-core/src/page/js_evaluation.rs b/moli-core/src/page/js_evaluation.rs index a5543eec62..3f978c5df1 100644 --- a/moli-core/src/page/js_evaluation.rs +++ b/moli-core/src/page/js_evaluation.rs @@ -16,8 +16,9 @@ use crate::RendererOutputFence; use crate::renderer::{ RendererDomDebuggerDomBreakpointResolution, RendererDomDebuggerEventListenerBreakpoint, RendererDomDebuggerEventListenersResolution, RendererDomDebuggerXhrBreakpoint, - RendererPageCommand, RendererPageReply, RendererPerformanceMetricSnapshot, - RendererRuntimeHeapUsage, RendererRuntimeInspectorResponseSender, + RendererInspectorCommandRoute, RendererPageCommand, RendererPageReply, + RendererPerformanceMetricSnapshot, RendererRuntimeHeapUsage, + RendererRuntimeInspectorResponseSender, }; fn dedupe_runtime_context_created_events(events: &mut Vec) { @@ -473,10 +474,36 @@ impl Page { inspector_session_id: Option, raw_json: String, ) -> Result { - self.start_page_command(RendererPageCommand::DispatchRuntimeProtocolMessage { + self.start_runtime_protocol_message_for_inspector_session_with_route( inspector_session_id, + RendererInspectorCommandRoute::MainThread, raw_json, - }) + ) + } + + pub fn start_runtime_interrupt_protocol_message_for_inspector_session( + &self, + inspector_session_id: Option, + raw_json: String, + ) -> Result { + self.start_runtime_protocol_message_for_inspector_session_with_route( + inspector_session_id, + RendererInspectorCommandRoute::Io, + raw_json, + ) + } + + fn start_runtime_protocol_message_for_inspector_session_with_route( + &self, + inspector_session_id: Option, + route: RendererInspectorCommandRoute, + raw_json: String, + ) -> Result { + self.start_page_command(RendererPageCommand::dispatch_runtime_protocol_message( + inspector_session_id, + route, + raw_json, + )) } pub fn start_runtime_protocol_message_with_deferred_response( @@ -496,38 +523,54 @@ impl Page { inspector_session_id: Option, raw_json: String, deferred_response: RendererRuntimeInspectorResponseSender, + ) -> Result { + self.start_runtime_protocol_message_for_inspector_session_with_deferred_response_and_route( + inspector_session_id, + RendererInspectorCommandRoute::MainThread, + raw_json, + deferred_response, + ) + } + + fn start_runtime_protocol_message_for_inspector_session_with_deferred_response_and_route( + &self, + inspector_session_id: Option, + route: RendererInspectorCommandRoute, + raw_json: String, + deferred_response: RendererRuntimeInspectorResponseSender, ) -> Result { self.start_page_command( - RendererPageCommand::DispatchRuntimeProtocolMessageWithDeferredResponse { + RendererPageCommand::dispatch_runtime_protocol_message_with_deferred_response( inspector_session_id, + route, raw_json, deferred_response, - }, + ), ) } pub fn start_routable_runtime_protocol_message_for_inspector_session( &self, inspector_session_id: Option, + inspector_route: RendererInspectorCommandRoute, owner_context_resolution_action: Option, raw_json: String, deferred_response: RendererRuntimeInspectorResponseSender, ) -> Result { - let owner_fallback_inspector_session_id = inspector_session_id.clone(); let route = self.handle.enqueue_routable_runtime_inspector_command( inspector_session_id, + inspector_route, owner_context_resolution_action, raw_json, deferred_response, ); let owner_pending = if route.requires_owner_fallback() { let command_id = route.command_id(); - match self.start_page_command( - RendererPageCommand::DispatchQueuedRuntimeInspectorCommand { - command_id, - inspector_session_id: owner_fallback_inspector_session_id, - }, - ) { + let command = RendererPageCommand::dispatch_queued_runtime_inspector_command( + route.metadata().clone(), + command_id, + ); + match self.start_page_command(command) { Ok(pending) => Some(pending), Err(error) => { self.handle.cancel_queued_runtime_inspector_command( @@ -567,11 +610,12 @@ impl Page { raw_json: String, ) -> Result { self.start_page_command( - RendererPageCommand::DispatchRuntimeProtocolMessageWithContextResolution { + RendererPageCommand::dispatch_runtime_protocol_message_with_context_resolution( inspector_session_id, + RendererInspectorCommandRoute::MainThread, action, raw_json, - }, + ), ) } @@ -597,12 +641,13 @@ impl Page { deferred_response: RendererRuntimeInspectorResponseSender, ) -> Result { self.start_page_command( - RendererPageCommand::DispatchRuntimeProtocolMessageWithContextResolutionAndDeferredResponse { + RendererPageCommand::dispatch_runtime_protocol_message_with_context_resolution_and_deferred_response( inspector_session_id, + RendererInspectorCommandRoute::MainThread, action, raw_json, deferred_response, - }, + ), ) } @@ -697,9 +742,9 @@ impl Page { &self, inspector_session_id: Option<&str>, ) -> Result { - self.start_page_command(RendererPageCommand::RuntimeEnableEvents { - inspector_session_id: inspector_session_id.map(str::to_owned), - }) + self.start_page_command(RendererPageCommand::runtime_enable_events( + inspector_session_id.map(str::to_owned), + )) } pub fn finish_runtime_enable_events( @@ -747,10 +792,10 @@ impl Page { ) -> Result { let pause_guard = self.handle.arm_runtime_inspector_session_detach(); let reply = self - .dispatch_page_command_async(RendererPageCommand::DetachRuntimeInspectorSession { - inspector_session_id: inspector_session_id.map(str::to_owned), + .dispatch_page_command_async(RendererPageCommand::detach_runtime_inspector_session( + inspector_session_id.map(str::to_owned), pause_guard, - }) + )) .await?; expect_page_reply!( reply, @@ -821,12 +866,12 @@ impl Page { depth: i32, pierce: bool, ) -> Result { - self.start_page_command(RendererPageCommand::DomDebuggerGetEventListeners { + self.start_page_command(RendererPageCommand::dom_debugger_get_event_listeners( inspector_session_id, object_id, depth, pierce, - }) + )) } pub fn finish_dom_debugger_get_event_listeners( @@ -937,12 +982,12 @@ impl Page { execution_context_name: Option<&str>, execution_context_id: Option, ) -> Result<()> { - let command = RendererPageCommand::AddRuntimeBinding { + let command = RendererPageCommand::add_runtime_binding( inspector_session_id, - name: name.to_owned(), - execution_context_name: execution_context_name.map(str::to_owned), + name.to_owned(), + execution_context_name.map(str::to_owned), execution_context_id, - }; + ); self.dispatch_unit_page_command_async(command, "add runtime binding") .await } @@ -1047,13 +1092,14 @@ impl Page { stored_runtime_bindings: &[RuntimeBindingRegistration], session_runtime_bindings: &[RuntimeBindingRegistration], ) -> Result> { - let pending = self.start_page_command(RendererPageCommand::ApplyRuntimeProtocolState { - inspector_session_id: inspector_session_id.clone(), - session_restore_snapshots: session_restore_snapshots.to_vec(), - isolated_worlds: isolated_worlds.to_vec(), - stored_runtime_bindings: stored_runtime_bindings.to_vec(), - session_runtime_bindings: session_runtime_bindings.to_vec(), - })?; + let pending = + self.start_page_command(RendererPageCommand::apply_runtime_protocol_state( + inspector_session_id.clone(), + session_restore_snapshots.to_vec(), + isolated_worlds.to_vec(), + stored_runtime_bindings.to_vec(), + session_runtime_bindings.to_vec(), + ))?; let completion = pending.wait().await?; let output = self.finish_page_command_turn(completion); let (completion, predecessor) = output.into_completion_and_predecessor(); diff --git a/moli-core/src/page/mod.rs b/moli-core/src/page/mod.rs index 2fd5628c61..e8b1342519 100644 --- a/moli-core/src/page/mod.rs +++ b/moli-core/src/page/mod.rs @@ -99,13 +99,14 @@ pub use moli_renderer_v8::{ RendererDomFocusOutcome, RendererDomMutationEvent, RendererDomMutationEventBatch, RendererDomSnapshotCaptureOptions, RendererDomSnapshotCapturePayload, RendererDragData, RendererDragDataItem, RendererDraggedDirectory, RendererDraggedFile, RendererFrameToken, - RendererInputDispatchOutcome, RendererJavaScriptDialogCompletion, RendererJavaScriptDialogId, - RendererJavaScriptDialogResult, RendererJavaScriptDialogSource, RendererLayoutMetrics, - RendererLifecycleEpoch, RendererLifecycleEventStamp, RendererLifecycleStartReason, - RendererLifecycleTerminationStamp, RendererMainDocumentCommit, - RendererPageCommandPostResponseContinuation, RendererPageCreationArtifacts, - RendererPageCreationDiagnostics, RendererPageDiagnosticsSnapshot, RendererPageDumpFormat, - RendererPageDumpOptions, RendererPageDumpStripOptions, RendererPendingDownloadActivation, + RendererInputDispatchOutcome, RendererInspectorCommandRoute, + RendererJavaScriptDialogCompletion, RendererJavaScriptDialogId, RendererJavaScriptDialogResult, + RendererJavaScriptDialogSource, RendererLayoutMetrics, RendererLifecycleEpoch, + RendererLifecycleEventStamp, RendererLifecycleStartReason, RendererLifecycleTerminationStamp, + RendererMainDocumentCommit, RendererPageCommandPostResponseContinuation, + RendererPageCreationArtifacts, RendererPageCreationDiagnostics, + RendererPageDiagnosticsSnapshot, RendererPageDumpFormat, RendererPageDumpOptions, + RendererPageDumpStripOptions, RendererPendingDownloadActivation, RendererPendingDownloadResponse, RendererPendingFileChooserActivation, RendererPendingJavaScriptDialog, RendererPendingPopupActivation, RendererPendingSameDocumentNavigation, RendererPendingTopLevelHistoryTraversal, diff --git a/moli-core/src/page/renderer_command_support.rs b/moli-core/src/page/renderer_command_support.rs index 84c73cc9a5..8d05038543 100644 --- a/moli-core/src/page/renderer_command_support.rs +++ b/moli-core/src/page/renderer_command_support.rs @@ -372,10 +372,12 @@ impl Page { inspector_session_id: Option, object_id: &str, ) -> Result { - self.start_page_command(RendererPageCommand::ComputedStylePropertiesForObjectId { - inspector_session_id, - object_id: object_id.to_owned(), - }) + self.start_page_command( + RendererPageCommand::computed_style_properties_for_object_id( + inspector_session_id, + object_id.to_owned(), + ), + ) } pub fn finish_computed_style_properties( @@ -399,10 +401,10 @@ impl Page { inspector_session_id: Option, object_id: &str, ) -> Result { - self.start_page_command(RendererPageCommand::NodeHasGeometryForObjectId { + self.start_page_command(RendererPageCommand::node_has_geometry_for_object_id( inspector_session_id, - object_id: object_id.to_owned(), - }) + object_id.to_owned(), + )) } pub fn start_scroll_node_into_view_if_needed_for_object_id_in_inspector_session( @@ -411,11 +413,11 @@ impl Page { object_id: &str, rect: Option, ) -> Result { - self.start_page_command(RendererPageCommand::ScrollObjectNodeIntoViewIfNeeded { + self.start_page_command(RendererPageCommand::scroll_object_node_into_view_if_needed( inspector_session_id, - object_id: object_id.to_owned(), + object_id.to_owned(), rect, - }) + )) } pub fn start_scroll_backend_node_into_view_if_needed( @@ -482,10 +484,10 @@ impl Page { inspector_session_id: Option, object_id: &str, ) -> Result { - self.start_page_command(RendererPageCommand::ClientRectForObjectId { + self.start_page_command(RendererPageCommand::client_rect_for_object_id( inspector_session_id, - object_id: object_id.to_owned(), - }) + object_id.to_owned(), + )) } pub fn finish_client_rect_for_object_id( @@ -546,10 +548,10 @@ impl Page { inspector_session_id: Option, object_id: &str, ) -> Result { - self.start_page_command(RendererPageCommand::DocumentGeometryForObjectId { + self.start_page_command(RendererPageCommand::document_geometry_for_object_id( inspector_session_id, - object_id: object_id.to_owned(), - }) + object_id.to_owned(), + )) } pub fn finish_document_geometry_for_object_id( @@ -714,10 +716,10 @@ impl Page { inspector_session_id: Option, object_id: String, ) -> Result { - self.start_page_command(RendererPageCommand::FocusDocumentNodeForObjectId { + self.start_page_command(RendererPageCommand::focus_document_node_for_object_id( inspector_session_id, object_id, - }) + )) } pub fn start_autofill_trigger( @@ -773,12 +775,12 @@ impl Page { files: Vec, append: bool, ) -> Result { - self.start_page_command(RendererPageCommand::SetFileInputFilesForObjectId { + self.start_page_command(RendererPageCommand::set_file_input_files_for_object_id( inspector_session_id, - object_id: object_id.to_owned(), + object_id.to_owned(), files, append, - }) + )) } pub fn finish_set_file_input_files_for_object_id( @@ -802,13 +804,13 @@ impl Page { depth: i32, pierce: bool, ) -> Result { - self.start_page_command(RendererPageCommand::DocumentNodeSnapshotForObjectId { + self.start_page_command(RendererPageCommand::document_node_snapshot_for_object_id( inspector_session_id, include_whitespace, - object_id: object_id.to_owned(), + object_id.to_owned(), depth, pierce, - }) + )) } pub fn finish_document_node_snapshot_for_object_id( @@ -1420,11 +1422,11 @@ impl Page { object_id: &str, include_shadow_dom: bool, ) -> Result { - self.start_page_command(RendererPageCommand::OuterHtmlForObjectId { + self.start_page_command(RendererPageCommand::outer_html_for_object_id( inspector_session_id, - object_id: object_id.to_owned(), + object_id.to_owned(), include_shadow_dom, - }) + )) } pub fn finish_outer_html_for_object_id( @@ -1570,12 +1572,14 @@ impl Page { execution_context_id: Option, object_group: Option<&str>, ) -> Result { - self.start_page_command(RendererPageCommand::ResolveRuntimeObjectForBackendNodeId { - inspector_session_id, - backend_node_id, - execution_context_id, - object_group: object_group.map(str::to_owned), - }) + self.start_page_command( + RendererPageCommand::resolve_runtime_object_for_backend_node_id( + inspector_session_id, + backend_node_id, + execution_context_id, + object_group.map(str::to_owned), + ), + ) } pub fn finish_resolve_runtime_object_for_backend_node_id( @@ -1593,10 +1597,10 @@ impl Page { inspector_session_id: Option, object_id: String, ) -> Result { - self.start_page_command(RendererPageCommand::ResolveBlobObject { + self.start_page_command(RendererPageCommand::resolve_blob_object( inspector_session_id, object_id, - }) + )) } pub fn finish_resolve_blob_object( diff --git a/moli-core/src/renderer.rs b/moli-core/src/renderer.rs index 5a1eaa4dca..ba7685fbe4 100644 --- a/moli-core/src/renderer.rs +++ b/moli-core/src/renderer.rs @@ -17,10 +17,11 @@ pub(crate) use moli_renderer_v8::{ RendererDomDebuggerDomBreakpointResolution, RendererDomDebuggerEventListenerBreakpoint, RendererDomDebuggerEventListenersResolution, RendererDomDebuggerXhrBreakpoint, RendererDomFocusOutcome, RendererDomSnapshotCaptureOptions, RendererDomSnapshotCapturePayload, - RendererLayoutMetrics, RendererOwnerCommand, RendererOwnerHandle, RendererOwnerReply, - RendererPageCommand, RendererPageCommandPending, RendererPageCookieFacadeSnapshotReply, - RendererPageDumpOptions, RendererPageHandle, RendererPageReply, RendererPageState, - RendererPendingDownloadActivation, RendererPerformanceMetricSnapshot, RendererRuntimeHeapUsage, + RendererInspectorCommandRoute, RendererLayoutMetrics, RendererOwnerCommand, + RendererOwnerHandle, RendererOwnerReply, RendererPageCommand, RendererPageCommandPending, + RendererPageCookieFacadeSnapshotReply, RendererPageDumpOptions, RendererPageHandle, + RendererPageReply, RendererPageState, RendererPendingDownloadActivation, + RendererPerformanceMetricSnapshot, RendererRuntimeHeapUsage, RendererRuntimeInspectorResponseSender, RendererRuntimeRemoteObjectResolution, ScriptRunOutcome, }; diff --git a/moli-protocol-cdp/src/wire.rs b/moli-protocol-cdp/src/wire.rs index 808b512c75..036945b0e9 100644 --- a/moli-protocol-cdp/src/wire.rs +++ b/moli-protocol-cdp/src/wire.rs @@ -461,17 +461,21 @@ impl PageWireAction { #[derive(Clone, Copy, Debug, Eq, PartialEq)] enum DebuggerWireAction { Interruptible, - ExecutionControl, + IoExecutionControl, + MainThreadExecutionControl, Other, } impl DebuggerWireAction { fn is_interruptible(self) -> bool { - matches!(self, Self::Interruptible | Self::ExecutionControl) + matches!(self, Self::Interruptible | Self::IoExecutionControl) } fn executes_page_javascript(self) -> bool { - self == Self::ExecutionControl + matches!( + self, + Self::IoExecutionControl | Self::MainThreadExecutionControl + ) } } @@ -516,8 +520,10 @@ impl DebuggerWireAction { | "setBreakpoint" | "setBreakpointByUrl" | "setBreakpointsActive" => Self::Interruptible, - "continueToLocation" | "restartFrame" | "resume" | "stepInto" | "stepOut" - | "stepOver" => Self::ExecutionControl, + "resume" => Self::IoExecutionControl, + "continueToLocation" | "restartFrame" | "stepInto" | "stepOut" | "stepOver" => { + Self::MainThreadExecutionControl + } _ => Self::Other, } } @@ -673,12 +679,7 @@ mod tests { "Debugger.getStackTrace", "Debugger.pause", "Debugger.removeBreakpoint", - "Debugger.continueToLocation", - "Debugger.restartFrame", "Debugger.resume", - "Debugger.stepInto", - "Debugger.stepOut", - "Debugger.stepOver", "Debugger.setBreakpoint", "Debugger.setBreakpointByUrl", "Debugger.setBreakpointsActive", @@ -695,7 +696,12 @@ mod tests { } for method in [ + "Debugger.continueToLocation", "Debugger.enable", + "Debugger.restartFrame", + "Debugger.stepInto", + "Debugger.stepOut", + "Debugger.stepOver", "Performance.enable", "Runtime.getIsolateId", ] { diff --git a/moli-protocol-server/src/protocol_server/tests/websocket.rs b/moli-protocol-server/src/protocol_server/tests/websocket.rs index 8ebf244a47..a9df91f130 100644 --- a/moli-protocol-server/src/protocol_server/tests/websocket.rs +++ b/moli-protocol-server/src/protocol_server/tests/websocket.rs @@ -1452,6 +1452,166 @@ async fn websocket_cdp_debugger_pause_interrupts_in_flight_runtime_evaluate() { abort_test_cdp_server(protocol_server).await; } +#[tokio::test] +async fn websocket_cdp_io_terminate_interrupts_busy_main_thread_and_skips_main_follower() { + let (cdp_addr, protocol_server) = spawn_test_protocol_server().await; + let (mut socket, _) = connect_async(format!( + "ws://{cdp_addr}/devtools/browser/{DEFAULT_BROWSER_ID}" + )) + .await + .expect("connect to cdp websocket"); + let session_id = cdp_create_default_session_and_navigate( + &mut socket, + "data:text/html,IO interrupt", + ) + .await; + + let enabled = send_cdp_command( + &mut socket, + 6, + "Debugger.enable", + Some(&session_id), + json!({}), + ) + .await; + assert!( + enabled + .iter() + .any(|message| message["id"] == json!(6_u64) && message.get("error").is_none()), + "Debugger.enable should succeed: {enabled:#?}" + ); + + socket + .send(WsMessage::Text( + json!({ + "id": 7_u64, + "method": "Runtime.evaluate", + "sessionId": session_id, + "params": { + "expression": "debugger; for (;;) {}", + "returnByValue": true, + } + }) + .to_string() + .into(), + )) + .await + .expect("send the non-yielding MainThread Runtime.evaluate"); + let mut observed = recv_until_match(&mut socket, |message| { + message["sessionId"].as_str() == Some(session_id.as_str()) + && message["method"] == json!("Debugger.paused") + }) + .await; + assert!( + observed.iter().all(|message| message["id"] != json!(7_u64)), + "the busy Runtime.evaluate must still be in flight at its debugger barrier: {observed:#?}" + ); + + observed.extend( + send_cdp_command( + &mut socket, + 8, + "Debugger.resume", + Some(&session_id), + json!({}), + ) + .await, + ); + if observed.iter().all(|message| { + message["sessionId"].as_str() != Some(session_id.as_str()) + || message["method"] != json!("Debugger.resumed") + }) { + observed.extend( + recv_until_match(&mut socket, |message| { + message["sessionId"].as_str() == Some(session_id.as_str()) + && message["method"] == json!("Debugger.resumed") + }) + .await, + ); + } + assert!( + observed.iter().all(|message| message["id"] != json!(7_u64)), + "the resumed Runtime.evaluate must enter its non-yielding loop: {observed:#?}" + ); + + // This MainThread follower is deliberately queued before the IO command. + // An interrupt callback must skip it, dispatch terminateExecution, and + // leave the follower for ordinary owner dispatch after V8 unwinds. + socket + .send(WsMessage::Text( + json!({ + "id": 9_u64, + "method": "Runtime.evaluate", + "sessionId": session_id, + "params": { + "expression": "6 * 7", + "returnByValue": true, + } + }) + .to_string() + .into(), + )) + .await + .expect("queue the MainThread follower"); + let terminated = tokio::time::timeout( + Duration::from_secs(10), + send_cdp_command( + &mut socket, + 10, + "Runtime.terminateExecution", + Some(&session_id), + json!({}), + ), + ) + .await + .expect("IO terminateExecution must interrupt non-yielding MainThread JavaScript"); + observed.extend(terminated); + + let mut saw_busy_response = observed.iter().any(|message| message["id"] == json!(7_u64)); + let mut saw_follower_response = observed.iter().any(|message| message["id"] == json!(9_u64)); + if !saw_busy_response || !saw_follower_response { + observed.extend( + recv_until_match(&mut socket, |message| { + saw_busy_response |= message["id"] == json!(7_u64); + saw_follower_response |= message["id"] == json!(9_u64); + saw_busy_response && saw_follower_response + }) + .await, + ); + } + + let terminate_response = observed + .iter() + .find(|message| message["id"] == json!(10_u64)) + .expect("terminateExecution response"); + assert_eq!( + terminate_response["result"], + json!({}), + "terminateExecution must complete through the IO V8 interrupt: {observed:#?}" + ); + let busy_response = observed + .iter() + .find(|message| message["id"] == json!(7_u64)) + .expect("terminated Runtime.evaluate response"); + assert!( + busy_response.get("error").is_some() + || busy_response["result"]["exceptionDetails"].is_object(), + "the non-yielding evaluation must report termination: {busy_response:#?}" + ); + let follower_response = observed + .iter() + .find(|message| message["id"] == json!(9_u64)) + .expect("MainThread follower response"); + assert_eq!( + follower_response["result"]["result"]["value"], + json!(42), + "the skipped MainThread follower must run normally after termination: {observed:#?}" + ); + + let _ = socket.close(None).await; + abort_test_cdp_server(protocol_server).await; +} + #[tokio::test] async fn websocket_cdp_raw_client_runtime_evaluate_immediately_after_page_navigate_succeeds() { // Regression test for the raw-CDP race: a raw client can pipeline @@ -4989,7 +5149,7 @@ async fn websocket_cdp_debugger_step_out_responds_before_resumed_and_caller_paus } #[tokio::test] -async fn websocket_cdp_debugger_pause_allows_auxiliary_runtime_commands() { +async fn websocket_cdp_debugger_pause_allows_auxiliary_main_thread_commands() { let (cdp_addr, protocol_server) = spawn_test_protocol_server().await; let (mut socket, _) = connect_async(format!( "ws://{cdp_addr}/devtools/browser/{DEFAULT_BROWSER_ID}" @@ -5074,10 +5234,9 @@ async fn websocket_cdp_debugger_pause_allows_auxiliary_runtime_commands() { !observed.iter().any(|message| message["id"] == json!(6_u64)), "Runtime.evaluate must remain pending while the renderer owner is paused: {observed:#?}" ); - // V8's nested inspector loop owns this command while the Page actor is - // blocked by the first evaluate. Its response and pause-bridge events are - // owner-independent; trying to capture a Page-owner output snapshot before - // publishing the response deadlocks because the client cannot resume. + // Chromium's normal debugger loop pumps its main-thread DevTools receiver. + // This command must therefore reach the auxiliary V8 session even though + // the ordinary Page owner turn that entered the pause has not returned. let auxiliary_evaluate = tokio::time::timeout( Duration::from_secs(5), send_cdp_command( @@ -5089,14 +5248,14 @@ async fn websocket_cdp_debugger_pause_allows_auxiliary_runtime_commands() { ), ) .await - .expect("auxiliary Runtime.evaluate must complete while the Page owner is paused"); + .expect("auxiliary Main Runtime.evaluate must complete in the debugger loop"); assert!( auxiliary_evaluate.iter().any(|message| { message["id"] == json!(7_u64) && message["sessionId"] == json!(auxiliary_session_id) && message["result"]["result"]["value"] == json!(42) }), - "pause-loop Runtime.evaluate should complete before resume: {auxiliary_evaluate:#?}" + "pause-loop Main Runtime.evaluate should complete before resume: {auxiliary_evaluate:#?}" ); let auxiliary_object = tokio::time::timeout( @@ -5110,7 +5269,7 @@ async fn websocket_cdp_debugger_pause_allows_auxiliary_runtime_commands() { ), ) .await - .expect("object-valued Runtime.evaluate must complete while the Page owner is paused"); + .expect("object-valued Main Runtime.evaluate must complete in the debugger loop"); assert!( auxiliary_object.iter().any(|message| { message["id"] == json!(51_u64) @@ -5120,8 +5279,7 @@ async fn websocket_cdp_debugger_pause_allows_auxiliary_runtime_commands() { .as_str() .is_some_and(|object_id| !object_id.is_empty()) }), - "pause-loop object response must not enter Page-owner node normalization: \ - {auxiliary_object:#?}" + "pause-loop object response must remain owner-independent: {auxiliary_object:#?}" ); socket @@ -5131,9 +5289,9 @@ async fn websocket_cdp_debugger_pause_allows_auxiliary_runtime_commands() { "method": "Runtime.evaluate", "sessionId": auxiliary_session_id, "params": { - "expression": "Promise.resolve(43)", - "awaitPromise": true, - "returnByValue": true + "expression": "Promise.resolve(43)", + "awaitPromise": true, + "returnByValue": true } }) .to_string() diff --git a/moli-protocol/src/conn/runtime_eval.rs b/moli-protocol/src/conn/runtime_eval.rs index f6cd7e6394..ac191d8090 100644 --- a/moli-protocol/src/conn/runtime_eval.rs +++ b/moli-protocol/src/conn/runtime_eval.rs @@ -15,7 +15,8 @@ use moli_core::{ DocumentNodeObjectSnapshot, DocumentNodeRuntimeObjectResolution, MAX_INSPECTOR_PROTOCOL_VALUE_DEPTH, RendererAgentAttachmentId, RendererCommandTurnOutput, RendererDomBidiNodeBindingResolution, RendererDomBidiNodeSharedIdResolution, - RendererRuntimeCommandOutput, RendererRuntimeInspectorMessage, RendererRuntimeRealmInfo, + RendererInspectorCommandRoute, RendererRuntimeCommandOutput, + RendererRuntimeInspectorMessage, RendererRuntimeRealmInfo, }, }; @@ -4231,7 +4232,7 @@ impl CdpConnection { Some(RendererRuntimeInspectorMessage::protocol(message)) } - fn runtime_inspector_pause_active_for_session_owner( + pub(crate) fn runtime_inspector_pause_active_for_session_owner( &mut self, session_id: Option<&str>, ) -> bool { @@ -4275,9 +4276,18 @@ impl CdpConnection { } else { self.runtime_session_owner_page_mut(session_id)? }; - let pending = page - .start_runtime_protocol_message_for_inspector_session(inspector_session_id, raw_json) - .map_err(|error| format!("runtime inspector dispatch failed: {error}"))?; + let pending = if allow_suspended_document_access { + page.start_runtime_interrupt_protocol_message_for_inspector_session( + inspector_session_id, + raw_json, + ) + } else { + page.start_runtime_protocol_message_for_inspector_session( + inspector_session_id, + raw_json, + ) + } + .map_err(|error| format!("runtime inspector dispatch failed: {error}"))?; Ok(PendingRuntimeProtocolMessageDispatch { session_id: session_id.map(str::to_owned), route, @@ -4340,6 +4350,11 @@ impl CdpConnection { }; let pending = match page.start_routable_runtime_protocol_message_for_inspector_session( inspector_session_id, + if allow_suspended_document_access { + RendererInspectorCommandRoute::Io + } else { + RendererInspectorCommandRoute::MainThread + }, None, raw_json, response_sender, @@ -4415,6 +4430,7 @@ impl CdpConnection { }; let pending = match page.start_routable_runtime_protocol_message_for_inspector_session( inspector_session_id, + RendererInspectorCommandRoute::MainThread, Some(action.to_owned()), raw_json, response_sender, @@ -4444,7 +4460,7 @@ impl CdpConnection { moli_core::page::CompletedRuntimeInspectorCommandDispatch::Owner(completion) => { *completion } - moli_core::page::CompletedRuntimeInspectorCommandDispatch::Pause + moli_core::page::CompletedRuntimeInspectorCommandDispatch::Inspector | moli_core::page::CompletedRuntimeInspectorCommandDispatch::Canceled => { return Ok(None); } @@ -4604,7 +4620,7 @@ impl CdpConnection { moli_core::page::CompletedRuntimeInspectorCommandDispatch::Owner(completion) => { *completion } - moli_core::page::CompletedRuntimeInspectorCommandDispatch::Pause + moli_core::page::CompletedRuntimeInspectorCommandDispatch::Inspector | moli_core::page::CompletedRuntimeInspectorCommandDispatch::Canceled => { continue; } diff --git a/moli-renderer-v8/src/lib.rs b/moli-renderer-v8/src/lib.rs index 5fc19b2e78..79d2314110 100644 --- a/moli-renderer-v8/src/lib.rs +++ b/moli-renderer-v8/src/lib.rs @@ -247,7 +247,9 @@ pub use runtime::{ RendererDomSearchRegistration, RendererDomSearchResultNode, RendererDomSearchResultsResolution, RendererDomSnapshotCaptureOptions, RendererDomSnapshotCapturePayload, RendererDragData, RendererDragDataItem, RendererDraggedDirectory, RendererDraggedFile, RendererFrameToken, - RendererGeometryQuad, RendererInputDispatchOutcome, RendererInspectorProtocolConfiguration, + RendererGeometryQuad, RendererInputDispatchOutcome, RendererInspectorCommandEnvelope, + RendererInspectorCommandMetadata, RendererInspectorCommandRoute, + RendererInspectorFirstDispatchLifecycle, RendererInspectorProtocolConfiguration, RendererInspectorProtocolConfigurationCommand, RendererInspectorSessionRestoreSnapshot, RendererJavaScriptDialogCompletion, RendererJavaScriptDialogId, RendererJavaScriptDialogResult, RendererJavaScriptDialogSource, RendererLayoutMetrics, RendererLifecycleDecider, diff --git a/moli-renderer-v8/src/runtime/mod.rs b/moli-renderer-v8/src/runtime/mod.rs index 1fd78c9f73..ded7813f0a 100644 --- a/moli-renderer-v8/src/runtime/mod.rs +++ b/moli-renderer-v8/src/runtime/mod.rs @@ -329,7 +329,9 @@ pub use self::page_surface::{ RendererDomSearchRegistration, RendererDomSearchResultNode, RendererDomSearchResultsResolution, RendererDomSnapshotCaptureOptions, RendererDomSnapshotCapturePayload, RendererDragData, RendererDragDataItem, RendererDraggedDirectory, RendererDraggedFile, RendererGeometryQuad, - RendererInputDispatchOutcome, RendererInspectorProtocolConfiguration, + RendererInputDispatchOutcome, RendererInspectorCommandEnvelope, + RendererInspectorCommandMetadata, RendererInspectorCommandRoute, + RendererInspectorFirstDispatchLifecycle, RendererInspectorProtocolConfiguration, RendererInspectorProtocolConfigurationCommand, RendererInspectorSessionRestoreSnapshot, RendererJavaScriptDialogId, RendererJavaScriptDialogSource, RendererLayoutMetrics, RendererMainDocumentCommit, RendererMoliDomMemoryDiagnostics, RendererMoliMemoryDiagnostics, diff --git a/moli-renderer-v8/src/runtime/owner.rs b/moli-renderer-v8/src/runtime/owner.rs index ee1851c7c5..f2380d92f6 100644 --- a/moli-renderer-v8/src/runtime/owner.rs +++ b/moli-renderer-v8/src/runtime/owner.rs @@ -86,7 +86,7 @@ use crate::service_worker_runtime::{ use crate::shared_worker_runtime::{ SharedWorkerRuntimeOwnerWake, shared_worker_owner_wake_channel, }; -use moli_page_types::{DevToolsSessionKey, LayoutPolicy}; +use moli_page_types::LayoutPolicy; use std::collections::VecDeque; use tokio::sync::{mpsc, oneshot}; @@ -374,7 +374,13 @@ pub(super) struct RenderRuntimePendingTurn { #[derive(Clone, Debug, Eq, PartialEq)] struct PageCommandFirstDispatchIdentity { token: RendererPageToken, + lane: PageCommandFirstDispatchLane, +} + +#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)] +pub(super) struct PageCommandFirstDispatchLane { inspector_session: DevToolsSessionKey, + route: RendererInspectorCommandRoute, } impl RenderRuntimePendingTurn { @@ -669,12 +675,21 @@ impl RenderRuntimeTurn { &self, ) -> Option { match self { - Self::RunLivePageCommand { token, command, .. } => command - .first_dispatch_inspector_session() - .map(|inspector_session| PageCommandFirstDispatchIdentity { - token: *token, - inspector_session, - }), + Self::RunLivePageCommand { token, command, .. } => { + command.inspector_metadata().map(|metadata| { + debug_assert_eq!( + metadata.first_dispatch_lifecycle(), + RendererInspectorFirstDispatchLifecycle::OrderedUntilFirstDispatch, + ); + PageCommandFirstDispatchIdentity { + token: *token, + lane: PageCommandFirstDispatchLane { + inspector_session: metadata.session().clone(), + route: metadata.route(), + }, + } + }) + } _ => None, } } @@ -800,7 +815,7 @@ fn live_page_command_should_follow_pending_navigation(command: &RendererPageComm } fn live_page_command_requires_materialized_child_realms(command: &RendererPageCommand) -> bool { - matches!(command, RendererPageCommand::RuntimeEnableEvents { .. }) + matches!(command, RendererPageCommand::Inspector(envelope) if envelope.requires_materialized_child_realms()) } const fn page_creation_navigation_reply_policy( @@ -2442,12 +2457,11 @@ impl RendererOwnerHandle { } } - /// The stable Page slot owns each Inspector session's command arrival - /// order. Only the command that owns its session's first-dispatch lane - /// enters the owner-wide runnable queue. Other sessions and non-Inspector - /// Page commands remain independent. The pause bridge is still the - /// interrupt path used by commands such as `Debugger.resume` once V8 is - /// paused. + /// The stable Page slot owns each Inspector `(session, route)` arrival + /// order. Chromium intentionally separates main-thread and IO ingress, + /// so each route has its own FIFO lane. Only the command that owns that + /// lane enters the owner-wide runnable queue; other routes, sessions and + /// non-Inspector Page commands remain independent. fn admit_page_command_first_dispatch( turn: RenderRuntimePendingTurn, pending_turns: &mut RenderRuntimePendingTurnQueue, @@ -2458,7 +2472,7 @@ impl RendererOwnerHandle { }; if let Some(admitted) = admit_page_command_first_dispatch_on_bound_owner_local_store( identity.token, - identity.inspector_session, + identity.lane, turn, ) { pending_turns.push_back(admitted); @@ -2471,7 +2485,7 @@ impl RendererOwnerHandle { ) { if let Some(next) = complete_page_command_first_dispatch_on_bound_owner_local_store( identity.token, - &identity.inspector_session, + &identity.lane, ) { pending_turns.push_back(next); } @@ -7055,37 +7069,46 @@ mod tests { } #[test] - fn first_dispatch_identity_uses_the_exact_renderer_inspector_session() { + fn first_dispatch_identity_uses_the_exact_inspector_session_and_route() { let token = RendererPageToken::new_for_testing(PageId::new_for_testing(8)); let attached = RenderRuntimeTurn::RunLivePageCommand { token, - command: RendererPageCommand::DispatchQueuedRuntimeInspectorCommand { - command_id: 1, - inspector_session_id: Some("SID-aux".to_owned()), - }, + command: RendererPageCommand::dispatch_queued_runtime_inspector_command( + RendererInspectorCommandMetadata::new( + Some("SID-aux".to_owned()), + RendererInspectorCommandRoute::MainThread, + ), + 1, + ), capture_policy: RendererPageStateCapturePolicy::ProtocolTurn, }; assert_eq!( attached.live_page_command_first_dispatch_identity(), Some(PageCommandFirstDispatchIdentity { token, - inspector_session: DevToolsSessionKey::Attached("SID-aux".to_owned()), + lane: PageCommandFirstDispatchLane { + inspector_session: DevToolsSessionKey::Attached("SID-aux".to_owned()), + route: RendererInspectorCommandRoute::MainThread, + }, }) ); let primary = RenderRuntimeTurn::RunLivePageCommand { token, - command: RendererPageCommand::DispatchQueuedRuntimeInspectorCommand { - command_id: 2, - inspector_session_id: None, - }, + command: RendererPageCommand::dispatch_queued_runtime_inspector_command( + RendererInspectorCommandMetadata::new(None, RendererInspectorCommandRoute::Io), + 2, + ), capture_policy: RendererPageStateCapturePolicy::ProtocolTurn, }; assert_eq!( primary.live_page_command_first_dispatch_identity(), Some(PageCommandFirstDispatchIdentity { token, - inspector_session: DevToolsSessionKey::Primary, + lane: PageCommandFirstDispatchLane { + inspector_session: DevToolsSessionKey::Primary, + route: RendererInspectorCommandRoute::Io, + }, }) ); @@ -7101,6 +7124,43 @@ mod tests { ); } + #[test] + fn special_inspector_commands_join_raw_commands_at_the_structural_boundary() { + let token = RendererPageToken::new_for_testing(PageId::new_for_testing(9)); + let turn_for = |command| RenderRuntimeTurn::RunLivePageCommand { + token, + command, + capture_policy: RendererPageStateCapturePolicy::ProtocolTurn, + }; + let enable = turn_for(RendererPageCommand::runtime_enable_events(Some( + "SID-shared-lane".to_owned(), + ))); + let raw_main = turn_for(RendererPageCommand::dispatch_runtime_protocol_message( + Some("SID-shared-lane".to_owned()), + RendererInspectorCommandRoute::MainThread, + r#"{"id":1,"method":"Runtime.disable"}"#.to_owned(), + )); + let raw_io = turn_for(RendererPageCommand::dispatch_runtime_protocol_message( + Some("SID-shared-lane".to_owned()), + RendererInspectorCommandRoute::Io, + r#"{"id":2,"method":"Runtime.terminateExecution"}"#.to_owned(), + )); + + let enable_identity = enable + .live_page_command_first_dispatch_identity() + .expect("Runtime.enable must enter the Inspector boundary"); + let main_identity = raw_main + .live_page_command_first_dispatch_identity() + .expect("raw main-thread dispatch must enter the Inspector boundary"); + let io_identity = raw_io + .live_page_command_first_dispatch_identity() + .expect("raw IO dispatch must enter the Inspector boundary"); + + assert_eq!(enable_identity, main_identity); + assert_ne!(enable_identity, io_identity); + assert_eq!(io_identity.lane.route, RendererInspectorCommandRoute::Io); + } + #[test] fn replacement_view_waiter_ignores_generic_activity_and_wrong_document_settlement() { let condition = RenderRuntimeParkCondition::ReplacementDocumentViewSettlement { diff --git a/moli-renderer-v8/src/runtime/owner_local.rs b/moli-renderer-v8/src/runtime/owner_local.rs index 3ce62b39ee..724a92215a 100644 --- a/moli-renderer-v8/src/runtime/owner_local.rs +++ b/moli-renderer-v8/src/runtime/owner_local.rs @@ -166,12 +166,15 @@ impl RendererPageHandle { pub fn enqueue_routable_runtime_inspector_command( &self, inspector_session_id: Option, + route: RendererInspectorCommandRoute, owner_context_resolution_action: Option, raw_json: String, response: RendererRuntimeInspectorResponseSender, ) -> crate::script_vm::inspector_pause::RendererRuntimeInspectorCommandRoute { self.inspector_pause_bridge.enqueue_command( + self.devtools_agent_token, inspector_session_id, + route, owner_context_resolution_action, raw_json, response, diff --git a/moli-renderer-v8/src/runtime/owner_local_store.rs b/moli-renderer-v8/src/runtime/owner_local_store.rs index 1347685b36..42ecd262d6 100644 --- a/moli-renderer-v8/src/runtime/owner_local_store.rs +++ b/moli-renderer-v8/src/runtime/owner_local_store.rs @@ -17,7 +17,9 @@ use super::navigation::{ PageCreationNavigationFailurePublisher, PageCreationResolution, PageCreationRetirement, PageNavigationOwnerFailure, page_creation_navigation_failure_scope, }; -use super::owner::{RenderRuntimePendingTurn, RendererCreateStreamingRawPageRequest}; +use super::owner::{ + PageCommandFirstDispatchLane, RenderRuntimePendingTurn, RendererCreateStreamingRawPageRequest, +}; use super::owner_deadline_index::OwnerDeadlineIndex; use super::owner_local::RendererAttachedPage; use super::owner_maintenance::{ @@ -45,7 +47,6 @@ use crate::script_vm::{ RendererPageScriptEnvironment, }; use crate::{RendererNavigationReplyPolicy, RendererTopLevelNavigationDispatch}; -use moli_page_types::DevToolsSessionKey; use tokio::sync::oneshot; #[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] @@ -654,7 +655,7 @@ struct RendererOwnerLocalPageSlot { owner_slot: RendererPageSlotHandle, turn_scheduler: PageTurnScheduler, page_command_first_dispatch: - PageCommandFirstDispatchResidence, + PageCommandFirstDispatchResidence, owner_maintenance: RendererPageOwnerMaintenanceResidence, task_sources: RendererPageOwnedTaskSources, lifecycle_gate: Option, @@ -1559,24 +1560,24 @@ pub(super) fn take_entry_for_command_on_bound_owner_local_store( pub(super) fn admit_page_command_first_dispatch_on_bound_owner_local_store( token: RendererPageToken, - inspector_session: DevToolsSessionKey, + lane: PageCommandFirstDispatchLane, turn: RenderRuntimePendingTurn, ) -> Option { with_bound_render_runtime_owner_local_store_session(|session| { session .store - .admit_page_command_first_dispatch(token, inspector_session, turn) + .admit_page_command_first_dispatch(token, lane, turn) }) } pub(super) fn complete_page_command_first_dispatch_on_bound_owner_local_store( token: RendererPageToken, - inspector_session: &DevToolsSessionKey, + lane: &PageCommandFirstDispatchLane, ) -> Option { with_bound_render_runtime_owner_local_store_session(|session| { session .store - .complete_page_command_first_dispatch(token, inspector_session) + .complete_page_command_first_dispatch(token, lane) }) } @@ -2907,7 +2908,7 @@ impl RendererOwnerLocalStore { fn admit_page_command_first_dispatch( &mut self, token: RendererPageToken, - inspector_session: DevToolsSessionKey, + lane: PageCommandFirstDispatchLane, turn: RenderRuntimePendingTurn, ) -> Option { let Some(page_slot) = self @@ -2919,20 +2920,18 @@ impl RendererOwnerLocalStore { // remains responsible for its protocol reply. return Some(turn); }; - page_slot - .page_command_first_dispatch - .admit(inspector_session, turn) + page_slot.page_command_first_dispatch.admit(lane, turn) } fn complete_page_command_first_dispatch( &mut self, token: RendererPageToken, - inspector_session: &DevToolsSessionKey, + lane: &PageCommandFirstDispatchLane, ) -> Option { self.page_hosts .get_mut(&token.local_host_id) .and_then(|host| host.pages.get_mut(&token.page_id)) - .and_then(|slot| slot.page_command_first_dispatch.complete(inspector_session)) + .and_then(|slot| slot.page_command_first_dispatch.complete(lane)) } fn checkout_entry_for_owner_turn( diff --git a/moli-renderer-v8/src/runtime/page_command_residence.rs b/moli-renderer-v8/src/runtime/page_command_residence.rs index 38b2e82562..3804afaf25 100644 --- a/moli-renderer-v8/src/runtime/page_command_residence.rs +++ b/moli-renderer-v8/src/runtime/page_command_residence.rs @@ -2,16 +2,17 @@ //! //! A Page entry is temporarily checked out while an owner turn runs, so //! Inspector-session ordering cannot live in `RendererPageLocalEntry`. This -//! residence stays in the stable Page slot and admits each session's commands -//! in arrival order until they reach their first V8 dispatch. Independent -//! sessions on the same Page have independent lanes. +//! residence stays in the stable Page slot and admits each `(session, route)` +//! stream in arrival order until commands reach their first V8 dispatch. +//! Independent sessions and Chromium-style main-thread/IO routes on the same +//! Page have independent lanes. use std::collections::{BTreeMap, VecDeque}; pub(super) struct PageCommandFirstDispatchResidence { /// Presence of a lane is the stable marker for its active command, which /// has already moved to the renderer owner. The deque retains only later - /// same-session commands. + /// same-lane commands. lanes: BTreeMap>, } @@ -37,9 +38,9 @@ impl Default for PageCommandFirstDispatchResidence { } impl PageCommandFirstDispatchResidence { - /// Returns the command when it owns its session's active dispatch slot. A + /// Returns the command when it owns its lane's active dispatch slot. A /// `None` result means the Page residence retained it behind a predecessor - /// from the same session. + /// from the same lane. pub(super) fn admit(&mut self, key: Key, command: Command) -> Option { match self.lanes.entry(key) { std::collections::btree_map::Entry::Vacant(entry) => { @@ -53,7 +54,7 @@ impl PageCommandFirstDispatchResidence { } } - /// Releases one session's active command and returns its next FIFO waiter, + /// Releases one lane's active command and returns its next FIFO waiter, /// which inherits the lane without an intermediate idle state. pub(super) fn complete(&mut self, key: &Key) -> Option { debug_assert!( @@ -79,32 +80,68 @@ impl PageCommandFirstDispatchResidence { #[cfg(test)] mod tests { use super::*; + use crate::runtime::RendererInspectorCommandRoute; + + type Lane = (&'static str, RendererInspectorCommandRoute); #[test] - fn each_inspector_session_owns_an_independent_fifo_dispatch_lane() { + fn each_inspector_session_and_route_owns_an_independent_fifo_dispatch_lane() { let mut first_page = PageCommandFirstDispatchResidence::default(); let mut other_page = PageCommandFirstDispatchResidence::default(); + let session_a_main: Lane = ("session-a", RendererInspectorCommandRoute::MainThread); + let session_a_io: Lane = ("session-a", RendererInspectorCommandRoute::Io); + let session_b_main: Lane = ("session-b", RendererInspectorCommandRoute::MainThread); - assert_eq!(first_page.admit("session-a", "a-first"), Some("a-first")); - assert_eq!(first_page.admit("session-a", "a-second"), None); assert_eq!( - first_page.admit("session-b", "b-first"), + first_page.admit(session_a_main, "a-main-first"), + Some("a-main-first") + ); + assert_eq!(first_page.admit(session_a_main, "a-main-second"), None); + assert_eq!( + first_page.admit(session_a_io, "a-io-first"), + Some("a-io-first"), + "an IO command must not wait for main-thread work in the same session" + ); + assert_eq!( + first_page.admit(session_b_main, "b-first"), Some("b-first"), "a parked command must not block a different Inspector session" ); - assert_eq!(first_page.admit("session-b", "b-second"), None); + assert_eq!(first_page.admit(session_b_main, "b-second"), None); - assert_eq!(other_page.admit("session-a", "other"), Some("other")); - assert_eq!(other_page.complete(&"session-a"), None); + assert_eq!(other_page.admit(session_a_main, "other"), Some("other")); + assert_eq!(other_page.complete(&session_a_main), None); - assert_eq!(first_page.complete(&"session-b"), Some("b-second")); - assert_eq!(first_page.complete(&"session-b"), None); - assert_eq!(first_page.complete(&"session-a"), Some("a-second")); - assert_eq!(first_page.complete(&"session-a"), None); + assert_eq!(first_page.complete(&session_b_main), Some("b-second")); + assert_eq!(first_page.complete(&session_b_main), None); + assert_eq!(first_page.complete(&session_a_io), None); + assert_eq!(first_page.complete(&session_a_main), Some("a-main-second")); + assert_eq!(first_page.complete(&session_a_main), None); assert_eq!( - first_page.admit("session-a", "a-after-idle"), + first_page.admit(session_a_main, "a-after-idle"), Some("a-after-idle") ); } + + #[test] + fn retiring_a_page_drains_waiters_and_resets_every_lane() { + let mut residence = PageCommandFirstDispatchResidence::default(); + let main: Lane = ("session-a", RendererInspectorCommandRoute::MainThread); + let io: Lane = ("session-a", RendererInspectorCommandRoute::Io); + + assert_eq!(residence.admit(main, "main-active"), Some("main-active")); + assert_eq!(residence.admit(main, "main-waiting"), None); + assert_eq!(residence.admit(io, "io-active"), Some("io-active")); + assert_eq!(residence.admit(io, "io-waiting"), None); + + let mut drained = residence.drain_waiting(); + drained.sort_unstable(); + assert_eq!(drained, ["io-waiting", "main-waiting"]); + assert_eq!( + residence.admit(main, "main-after-retirement"), + Some("main-after-retirement"), + "retirement must not leave a stale active-lane marker" + ); + } } diff --git a/moli-renderer-v8/src/runtime/page_commands.rs b/moli-renderer-v8/src/runtime/page_commands.rs index cb7b8b8dea..8967ca597c 100644 --- a/moli-renderer-v8/src/runtime/page_commands.rs +++ b/moli-renderer-v8/src/runtime/page_commands.rs @@ -1,3 +1,4 @@ +use super::page_surface::RendererInspectorPageCommand; use super::*; impl PageVm { @@ -8,6 +9,9 @@ impl PageVm { let throttling_started = renderer_page_command_uses_cpu_throttling(&command).then(std::time::Instant::now); let result = match command { + RendererPageCommand::Inspector(command) => { + self.dispatch_renderer_inspector_command(command) + } RendererPageCommand::EvaluateExpression { expression, await_promise, @@ -126,12 +130,6 @@ impl PageVm { RendererPageCommand::FocusDocumentBackendNode { backend_node_id } => self .focus_document_backend_node(backend_node_id) .map(RendererPageReply::DomFocusOutcome), - RendererPageCommand::FocusDocumentNodeForObjectId { - inspector_session_id, - object_id, - } => self - .focus_document_node_for_object_id(inspector_session_id.as_deref(), &object_id) - .map(RendererPageReply::DomFocusOutcome), RendererPageCommand::TriggerAutofill(request) => self .trigger_autofill(request) .map(RendererPageReply::AutofillTriggerOutcome), @@ -146,19 +144,6 @@ impl PageVm { } => Ok(RendererPageReply::OptionalBool( self.set_file_input_files_for_backend_node_id(backend_node_id, files, append)?, )), - RendererPageCommand::SetFileInputFilesForObjectId { - inspector_session_id, - object_id, - files, - append, - } => Ok(RendererPageReply::OptionalBool( - self.set_file_input_files_for_object_id( - inspector_session_id.as_deref(), - &object_id, - files, - append, - )?, - )), RendererPageCommand::InsertTextIntoActiveControl(text) => self .insert_text_into_active_control(&text) .map(RendererPageReply::Bool), @@ -181,99 +166,6 @@ impl PageVm { should_insert_text, ) .map(RendererPageReply::InputDispatchOutcome), - RendererPageCommand::DispatchRuntimeProtocolMessage { - inspector_session_id, - raw_json, - } => self - .dispatch_runtime_protocol_message_for_inspector_session( - inspector_session_id.as_deref(), - &raw_json, - ) - .map(RendererRuntimeCommandOutput::from_messages) - .map(RendererPageReply::RuntimeInspectorProtocolMessages), - RendererPageCommand::DispatchRuntimeProtocolMessageWithDeferredResponse { - inspector_session_id, - raw_json, - deferred_response, - } => self - .dispatch_runtime_protocol_message_for_inspector_session_with_deferred_response( - inspector_session_id.as_deref(), - &raw_json, - deferred_response, - ) - .map(RendererRuntimeCommandOutput::from_messages) - .map(RendererPageReply::RuntimeInspectorProtocolMessages), - RendererPageCommand::DispatchQueuedRuntimeInspectorCommand { command_id, .. } => self - .dispatch_queued_runtime_inspector_command(command_id) - .map(RendererRuntimeCommandOutput::from_messages) - .map(RendererPageReply::RuntimeInspectorProtocolMessages), - RendererPageCommand::DispatchRuntimeProtocolMessageWithContextResolution { - inspector_session_id, - action, - raw_json, - } => self - .dispatch_runtime_protocol_message_for_inspector_session_with_context_resolution( - inspector_session_id.as_deref(), - &action, - &raw_json, - ) - .map(RendererRuntimeCommandOutput::from_messages) - .map(RendererPageReply::RuntimeInspectorProtocolMessages), - RendererPageCommand::DispatchRuntimeProtocolMessageWithContextResolutionAndDeferredResponse { - inspector_session_id, - action, - raw_json, - deferred_response, - } => self - .dispatch_runtime_protocol_message_for_inspector_session_with_context_resolution_and_deferred_response( - inspector_session_id.as_deref(), - &action, - &raw_json, - deferred_response, - ) - .map(RendererRuntimeCommandOutput::from_messages) - .map(RendererPageReply::RuntimeInspectorProtocolMessages), - RendererPageCommand::RuntimeEnableEvents { - inspector_session_id, - } => self - .runtime_enable_events(inspector_session_id.as_deref()) - .map(RendererPageReply::RuntimeInspectorProtocolMessages), - RendererPageCommand::ApplyRuntimeProtocolState { - inspector_session_id, - session_restore_snapshots, - isolated_worlds, - stored_runtime_bindings, - session_runtime_bindings, - } => { - self.apply_runtime_protocol_state( - inspector_session_id.as_deref(), - &session_restore_snapshots, - &isolated_worlds, - &stored_runtime_bindings, - &session_runtime_bindings, - )?; - Ok(RendererPageReply::Unit) - } - RendererPageCommand::DetachRuntimeInspectorSession { - inspector_session_id, - pause_guard: _pause_guard, - } => Ok(RendererPageReply::Bool( - self.detach_runtime_inspector_session(inspector_session_id.as_deref()), - )), - RendererPageCommand::AddRuntimeBinding { - inspector_session_id, - name, - execution_context_name, - execution_context_id, - } => { - self.add_runtime_binding( - inspector_session_id.as_deref(), - &name, - execution_context_name.as_deref(), - execution_context_id, - )?; - Ok(RendererPageReply::Unit) - } RendererPageCommand::CreateIsolatedWorld { name, grant_universal_access, @@ -429,19 +321,6 @@ impl PageVm { self.vm_mut().performance_metric_snapshot()?, )), ), - RendererPageCommand::DomDebuggerGetEventListeners { - inspector_session_id, - object_id, - depth, - pierce, - } => Ok(RendererPageReply::DomDebuggerEventListeners( - self.vm_mut().dom_debugger_event_listeners( - inspector_session_id.as_deref(), - &object_id, - depth, - pierce, - )?, - )), RendererPageCommand::DomDebuggerConfigureEventListenerBreakpoint { inspector_session_id, breakpoint, @@ -528,15 +407,6 @@ impl PageVm { self.computed_style_properties_for_backend_node_id(backend_node_id)?, )) } - RendererPageCommand::ComputedStylePropertiesForObjectId { - inspector_session_id, - object_id, - } => Ok(RendererPageReply::ComputedStyleProperties( - self.computed_style_properties_for_object_id( - inspector_session_id.as_deref(), - &object_id, - )?, - )), RendererPageCommand::SetInlineStyleSheetTextForStyleSheetId { inspector_session_id, style_sheet_id, @@ -548,43 +418,17 @@ impl PageVm { &text, ) .map(RendererPageReply::Bool), - RendererPageCommand::ScrollObjectNodeIntoViewIfNeeded { - inspector_session_id, - object_id, - rect, - } => self - .scroll_node_into_view_if_needed_for_object_id( - inspector_session_id.as_deref(), - &object_id, - rect, - ) - .map(RendererPageReply::ScrollIntoViewResult), RendererPageCommand::ScrollBackendNodeIntoViewIfNeeded { backend_node_id, rect, } => self .scroll_backend_node_into_view_if_needed(backend_node_id, rect) .map(RendererPageReply::ScrollIntoViewResult), - RendererPageCommand::ClientRectForObjectId { - inspector_session_id, - object_id, - } => Ok(RendererPageReply::OptionalDocumentNodeClientRect( - self.client_rect_for_object_id(inspector_session_id.as_deref(), &object_id)?, - )), RendererPageCommand::ClientRectForBackendNodeId { backend_node_id } => Ok( RendererPageReply::OptionalDocumentNodeClientRect( self.client_rect_for_backend_node_id(backend_node_id)?, ), ), - RendererPageCommand::DocumentGeometryForObjectId { - inspector_session_id, - object_id, - } => Ok(RendererPageReply::OptionalDocumentNodeGeometry( - self.document_geometry_for_object_id( - inspector_session_id.as_deref(), - &object_id, - )?, - )), RendererPageCommand::DocumentGeometryForBackendNodeId { backend_node_id } => Ok( RendererPageReply::OptionalDocumentNodeGeometry( self.document_geometry_for_backend_node_id(backend_node_id)?, @@ -605,12 +449,6 @@ impl PageVm { ignore_pointer_events_none, )?, )), - RendererPageCommand::NodeHasGeometryForObjectId { - inspector_session_id, - object_id, - } => Ok(RendererPageReply::OptionalBool( - self.node_has_geometry_for_object_id(inspector_session_id.as_deref(), &object_id)?, - )), RendererPageCommand::NodeHasGeometryForBackendNodeId { backend_node_id } => { Ok(RendererPageReply::OptionalBool( self.node_has_geometry_for_backend_node_id(backend_node_id)?, @@ -619,26 +457,6 @@ impl PageVm { RendererPageCommand::RemoveDocumentBackendNodeId { backend_node_id } => self .remove_document_backend_node_id(backend_node_id) .map(RendererPageReply::Bool), - RendererPageCommand::DocumentNodeSnapshotForObjectId { - inspector_session_id, - include_whitespace, - object_id, - depth, - pierce, - } => { - self.configure_document_dom_agent_session( - inspector_session_id.as_deref(), - include_whitespace, - ); - Ok(RendererPageReply::OptionalDocumentNodeObjectSnapshot( - Box::new(self.document_node_snapshot_for_object_id( - inspector_session_id.as_deref(), - &object_id, - depth, - pierce, - )?), - )) - } RendererPageCommand::DocumentNodeSnapshotForBackendNodeId { backend_node_id, depth, @@ -963,35 +781,6 @@ impl PageVm { self.accessibility_node_payload_for_child_frame(&frame_id), )) } - RendererPageCommand::AccessibilityTreePayloadsForObjectId { - inspector_session_id, - object_id, - } => Ok(RendererPageReply::OptionalAccessibilityPayloadsForObjectId( - self.accessibility_tree_payloads_for_object_id( - inspector_session_id.as_deref(), - &object_id, - )?, - )), - RendererPageCommand::AccessibilityNodeAndAncestorPayloadsForObjectId { - inspector_session_id, - object_id, - } => Ok(RendererPageReply::OptionalAccessibilityPayloadsForObjectId( - self.accessibility_node_and_ancestor_payloads_for_object_id( - inspector_session_id.as_deref(), - &object_id, - )?, - )), - RendererPageCommand::AccessibilityPartialTreePayloadsForObjectId { - inspector_session_id, - object_id, - fetch_relatives, - } => Ok(RendererPageReply::OptionalAccessibilityPayloadsForObjectId( - self.accessibility_partial_tree_payloads_for_object_id( - inspector_session_id.as_deref(), - &object_id, - fetch_relatives, - )?, - )), RendererPageCommand::StyleSheetPayloadForStyleSheetId { inspector_session_id, style_sheet_id, @@ -1017,17 +806,6 @@ impl PageVm { self.outer_html_for_document(include_shadow_dom), )) } - RendererPageCommand::OuterHtmlForObjectId { - inspector_session_id, - object_id, - include_shadow_dom, - } => Ok(RendererPageReply::OptionalString( - self.outer_html_for_object_id( - inspector_session_id.as_deref(), - &object_id, - include_shadow_dom, - )?, - )), RendererPageCommand::OuterHtmlForBackendNodeId { backend_node_id, include_shadow_dom, @@ -1046,28 +824,6 @@ impl PageVm { RendererPageCommand::CaptureScreenshot(request) => self .capture_screenshot(request) .map(RendererPageReply::CaptureScreenshot), - RendererPageCommand::ResolveRuntimeObjectForBackendNodeId { - inspector_session_id, - backend_node_id, - execution_context_id, - object_group, - } => Ok(RendererPageReply::RuntimeRemoteObjectResolution( - self.resolve_runtime_object_for_backend_node_id( - inspector_session_id.as_deref(), - backend_node_id, - execution_context_id, - object_group.as_deref(), - )?, - )), - RendererPageCommand::ResolveBlobObject { - inspector_session_id, - object_id, - } => Ok(RendererPageReply::BlobUuid( - self.vm_mut().blob_uuid_for_runtime_object_id( - inspector_session_id.as_deref(), - &object_id, - )?, - )), RendererPageCommand::BlobBytesForUuid { uuid } => Ok( RendererPageReply::OptionalBlobBytes(self.vm().blob_bytes_for_uuid(&uuid)), ), @@ -1360,6 +1116,231 @@ impl PageVm { result } + fn dispatch_renderer_inspector_command( + &mut self, + envelope: RendererInspectorCommandEnvelope, + ) -> Result { + let (metadata, command) = envelope.into_parts(); + let inspector_session_id = metadata.session().wire_session_id(); + match command { + RendererInspectorPageCommand::DispatchRuntimeProtocolMessage { raw_json } => self + .dispatch_runtime_protocol_message_for_inspector_session( + inspector_session_id, + &raw_json, + ) + .map(RendererRuntimeCommandOutput::from_messages) + .map(RendererPageReply::RuntimeInspectorProtocolMessages), + RendererInspectorPageCommand::DispatchRuntimeProtocolMessageWithDeferredResponse { + raw_json, + deferred_response, + } => self + .dispatch_runtime_protocol_message_for_inspector_session_with_deferred_response( + inspector_session_id, + &raw_json, + deferred_response, + ) + .map(RendererRuntimeCommandOutput::from_messages) + .map(RendererPageReply::RuntimeInspectorProtocolMessages), + RendererInspectorPageCommand::DispatchQueuedRuntimeInspectorCommand { + command_id, + } => self + .dispatch_queued_runtime_inspector_command(command_id) + .map(RendererRuntimeCommandOutput::from_messages) + .map(RendererPageReply::RuntimeInspectorProtocolMessages), + RendererInspectorPageCommand::DispatchRuntimeProtocolMessageWithContextResolution { + action, + raw_json, + } => self + .dispatch_runtime_protocol_message_for_inspector_session_with_context_resolution( + inspector_session_id, + &action, + &raw_json, + ) + .map(RendererRuntimeCommandOutput::from_messages) + .map(RendererPageReply::RuntimeInspectorProtocolMessages), + RendererInspectorPageCommand::DispatchRuntimeProtocolMessageWithContextResolutionAndDeferredResponse { + action, + raw_json, + deferred_response, + } => self + .dispatch_runtime_protocol_message_for_inspector_session_with_context_resolution_and_deferred_response( + inspector_session_id, + &action, + &raw_json, + deferred_response, + ) + .map(RendererRuntimeCommandOutput::from_messages) + .map(RendererPageReply::RuntimeInspectorProtocolMessages), + RendererInspectorPageCommand::RuntimeEnableEvents => self + .runtime_enable_events(inspector_session_id) + .map(RendererPageReply::RuntimeInspectorProtocolMessages), + RendererInspectorPageCommand::ApplyRuntimeProtocolState { + session_restore_snapshots, + isolated_worlds, + stored_runtime_bindings, + session_runtime_bindings, + } => { + self.apply_runtime_protocol_state( + inspector_session_id, + &session_restore_snapshots, + &isolated_worlds, + &stored_runtime_bindings, + &session_runtime_bindings, + )?; + Ok(RendererPageReply::Unit) + } + RendererInspectorPageCommand::DetachRuntimeInspectorSession { + pause_guard: _pause_guard, + } => Ok(RendererPageReply::Bool( + self.detach_runtime_inspector_session(inspector_session_id), + )), + RendererInspectorPageCommand::AddRuntimeBinding { + name, + execution_context_name, + execution_context_id, + } => { + self.add_runtime_binding( + inspector_session_id, + &name, + execution_context_name.as_deref(), + execution_context_id, + )?; + Ok(RendererPageReply::Unit) + } + RendererInspectorPageCommand::DomDebuggerGetEventListeners { + object_id, + depth, + pierce, + } => Ok(RendererPageReply::DomDebuggerEventListeners( + self.vm_mut().dom_debugger_event_listeners( + inspector_session_id, + &object_id, + depth, + pierce, + )?, + )), + RendererInspectorPageCommand::ComputedStylePropertiesForObjectId { object_id } => { + Ok(RendererPageReply::ComputedStyleProperties( + self.computed_style_properties_for_object_id( + inspector_session_id, + &object_id, + )?, + )) + } + RendererInspectorPageCommand::ScrollObjectNodeIntoViewIfNeeded { object_id, rect } => { + self.scroll_node_into_view_if_needed_for_object_id( + inspector_session_id, + &object_id, + rect, + ) + .map(RendererPageReply::ScrollIntoViewResult) + } + RendererInspectorPageCommand::ClientRectForObjectId { object_id } => { + Ok(RendererPageReply::OptionalDocumentNodeClientRect( + self.client_rect_for_object_id(inspector_session_id, &object_id)?, + )) + } + RendererInspectorPageCommand::DocumentGeometryForObjectId { object_id } => { + Ok(RendererPageReply::OptionalDocumentNodeGeometry( + self.document_geometry_for_object_id(inspector_session_id, &object_id)?, + )) + } + RendererInspectorPageCommand::NodeHasGeometryForObjectId { object_id } => { + Ok(RendererPageReply::OptionalBool( + self.node_has_geometry_for_object_id(inspector_session_id, &object_id)?, + )) + } + RendererInspectorPageCommand::FocusDocumentNodeForObjectId { object_id } => self + .focus_document_node_for_object_id(inspector_session_id, &object_id) + .map(RendererPageReply::DomFocusOutcome), + RendererInspectorPageCommand::SetFileInputFilesForObjectId { + object_id, + files, + append, + } => Ok(RendererPageReply::OptionalBool( + self.set_file_input_files_for_object_id( + inspector_session_id, + &object_id, + files, + append, + )?, + )), + RendererInspectorPageCommand::DocumentNodeSnapshotForObjectId { + include_whitespace, + object_id, + depth, + pierce, + } => { + self.configure_document_dom_agent_session( + inspector_session_id, + include_whitespace, + ); + Ok(RendererPageReply::OptionalDocumentNodeObjectSnapshot( + Box::new(self.document_node_snapshot_for_object_id( + inspector_session_id, + &object_id, + depth, + pierce, + )?), + )) + } + RendererInspectorPageCommand::AccessibilityTreePayloadsForObjectId { object_id } => { + Ok(RendererPageReply::OptionalAccessibilityPayloadsForObjectId( + self.accessibility_tree_payloads_for_object_id( + inspector_session_id, + &object_id, + )?, + )) + } + RendererInspectorPageCommand::AccessibilityNodeAndAncestorPayloadsForObjectId { + object_id, + } => Ok(RendererPageReply::OptionalAccessibilityPayloadsForObjectId( + self.accessibility_node_and_ancestor_payloads_for_object_id( + inspector_session_id, + &object_id, + )?, + )), + RendererInspectorPageCommand::AccessibilityPartialTreePayloadsForObjectId { + object_id, + fetch_relatives, + } => Ok(RendererPageReply::OptionalAccessibilityPayloadsForObjectId( + self.accessibility_partial_tree_payloads_for_object_id( + inspector_session_id, + &object_id, + fetch_relatives, + )?, + )), + RendererInspectorPageCommand::OuterHtmlForObjectId { + object_id, + include_shadow_dom, + } => Ok(RendererPageReply::OptionalString( + self.outer_html_for_object_id( + inspector_session_id, + &object_id, + include_shadow_dom, + )?, + )), + RendererInspectorPageCommand::ResolveRuntimeObjectForBackendNodeId { + backend_node_id, + execution_context_id, + object_group, + } => Ok(RendererPageReply::RuntimeRemoteObjectResolution( + self.resolve_runtime_object_for_backend_node_id( + inspector_session_id, + backend_node_id, + execution_context_id, + object_group.as_deref(), + )?, + )), + RendererInspectorPageCommand::ResolveBlobObject { object_id } => { + Ok(RendererPageReply::BlobUuid( + self.vm_mut() + .blob_uuid_for_runtime_object_id(inspector_session_id, &object_id)?, + )) + } + } + } + async fn apply_cpu_throttling_delay_after_page_command( &self, started: Option, @@ -1387,6 +1368,9 @@ impl PageVm { } fn renderer_page_command_uses_cpu_throttling(command: &RendererPageCommand) -> bool { + if let RendererPageCommand::Inspector(envelope) = command { + return envelope.uses_cpu_throttling(); + } matches!( command, RendererPageCommand::EvaluateExpression { .. } @@ -1398,12 +1382,6 @@ fn renderer_page_command_uses_cpu_throttling(command: &RendererPageCommand) -> b | RendererPageCommand::DispatchDragEventAtPoint { .. } | RendererPageCommand::InsertTextIntoActiveControl(_) | RendererPageCommand::DispatchKeyEvent { .. } - | RendererPageCommand::DispatchRuntimeProtocolMessage { .. } - | RendererPageCommand::DispatchRuntimeProtocolMessageWithDeferredResponse { .. } - | RendererPageCommand::DispatchQueuedRuntimeInspectorCommand { .. } - | RendererPageCommand::DispatchRuntimeProtocolMessageWithContextResolution { .. } - | RendererPageCommand::DispatchRuntimeProtocolMessageWithContextResolutionAndDeferredResponse { .. } - | RendererPageCommand::DomDebuggerGetEventListeners { .. } | RendererPageCommand::DomDebuggerConfigureEventListenerBreakpoint { .. } | RendererPageCommand::DomDebuggerConfigureXhrBreakpoint { .. } | RendererPageCommand::DomDebuggerConfigureDomBreakpoint { .. } @@ -1414,7 +1392,6 @@ fn renderer_page_command_uses_cpu_throttling(command: &RendererPageCommand) -> b | RendererPageCommand::MutateDocumentBackendNodeAttribute { .. } | RendererPageCommand::EditDocumentNode { .. } | RendererPageCommand::FocusDocumentBackendNode { .. } - | RendererPageCommand::FocusDocumentNodeForObjectId { .. } | RendererPageCommand::SetDocumentContent { .. } ) } diff --git a/moli-renderer-v8/src/runtime/page_dom.rs b/moli-renderer-v8/src/runtime/page_dom.rs index 5f302d980a..5476067504 100644 --- a/moli-renderer-v8/src/runtime/page_dom.rs +++ b/moli-renderer-v8/src/runtime/page_dom.rs @@ -751,20 +751,23 @@ impl PageVm { let Some(command) = pause_bridge.claim_command_for_owner(command_id) else { return Ok(Vec::new()); }; + let mut first_dispatch = pause_bridge.first_dispatch_guard(&command); + let inspector_session = command.metadata.session().clone(); + let inspector_session_id = inspector_session.wire_session_id(); match command.owner_context_resolution_action.as_deref() { - Some(action) => self - .dispatch_runtime_protocol_message_for_inspector_session_with_context_resolution_and_deferred_response( - command.inspector_session_id.as_deref(), - action, - &command.raw_json, - command.response, - ), - None => self - .dispatch_runtime_protocol_message_for_inspector_session_with_deferred_response( - command.inspector_session_id.as_deref(), - &command.raw_json, - command.response, - ), + Some(action) => self.dispatch_queued_runtime_protocol_message_with_context_resolution( + inspector_session_id, + action, + &command.raw_json, + command.response, + &mut first_dispatch, + ), + None => self.dispatch_runtime_protocol_message_with_command_output_at_first_dispatch( + inspector_session_id, + &command.raw_json, + command.response, + Some(&mut first_dispatch), + ), } } @@ -827,11 +830,65 @@ impl PageVm { ) } + fn dispatch_queued_runtime_protocol_message_with_context_resolution( + &mut self, + inspector_session_id: Option<&str>, + action: &str, + raw_json: &str, + deferred_response: RendererRuntimeInspectorResponseSender, + first_dispatch: &mut crate::script_vm::inspector_pause::RendererInspectorFirstDispatchGuard, + ) -> Result> { + if action == "evaluate" { + // The child-default fast path dispatches while probing, so expose + // its first-dispatch edge before entering that helper. + first_dispatch.release(); + if let Some(messages) = self.try_dispatch_child_default_runtime_evaluate(raw_json)? { + let call_id = deferred_response.call_id(); + if let Some(message) = messages.into_iter().next() + && let Err(message) = + deferred_response.send(message.into_v8_inspector_message()) + { + tracing::debug!( + call_id, + message = ?message, + "dropping child-frame runtime response because deferred receiver was closed" + ); + } + return Ok(Vec::new()); + } + } + let prepared_json = + self.prepare_runtime_protocol_message_with_context_resolution(action, raw_json)?; + self.dispatch_runtime_protocol_message_with_command_output_at_first_dispatch( + inspector_session_id, + &prepared_json, + deferred_response, + Some(first_dispatch), + ) + } + fn dispatch_runtime_protocol_message_with_command_output( + &mut self, + inspector_session_id: Option<&str>, + raw_json: &str, + deferred_response: RendererRuntimeInspectorResponseSender, + ) -> Result> { + self.dispatch_runtime_protocol_message_with_command_output_at_first_dispatch( + inspector_session_id, + raw_json, + deferred_response, + None, + ) + } + + fn dispatch_runtime_protocol_message_with_command_output_at_first_dispatch( &mut self, inspector_session_id: Option<&str>, raw_json: &str, mut deferred_response: RendererRuntimeInspectorResponseSender, + mut first_dispatch: Option< + &mut crate::script_vm::inspector_pause::RendererInspectorFirstDispatchGuard, + >, ) -> Result> { ensure!( self.pending_runtime_command_output.is_none(), @@ -868,6 +925,9 @@ impl PageVm { lifecycle_target: PageVmRuntimeCommandLifecycleTarget::AwaitingExplicitDocumentReplacement, }); + if let Some(first_dispatch) = first_dispatch.as_mut() { + first_dispatch.release(); + } let dispatch_result = self .vm_mut() .dispatch_inspector_protocol_message_for_session_with_deferred_response_and_command_output( diff --git a/moli-renderer-v8/src/runtime/page_surface.rs b/moli-renderer-v8/src/runtime/page_surface.rs index 11f3e8fbc9..97280a3e93 100644 --- a/moli-renderer-v8/src/runtime/page_surface.rs +++ b/moli-renderer-v8/src/runtime/page_surface.rs @@ -3528,8 +3528,267 @@ pub enum RendererDomDebuggerDomBreakpointResolution { UnknownType(String), } +/// Chromium routes Page DevTools work through separate main-thread and IO +/// session ingress paths. Ordering is guaranteed within one session and one +/// route, while an IO command may overtake main-thread work that has not +/// reached its first dispatch yet. +#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] +pub enum RendererInspectorCommandRoute { + MainThread, + Io, +} + +/// The command owns its `(Page, session, route)` lane until its first access to +/// the frontend V8 Inspector session. Protocol response completion is not part +/// of this lifetime: V8 may complete a response asynchronously, and holding the +/// lane for that response could prevent a later resume command from running. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum RendererInspectorFirstDispatchLifecycle { + OrderedUntilFirstDispatch, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct RendererInspectorCommandMetadata { + session: DevToolsSessionKey, + route: RendererInspectorCommandRoute, + first_dispatch: RendererInspectorFirstDispatchLifecycle, +} + +impl RendererInspectorCommandMetadata { + pub fn new(inspector_session_id: Option, route: RendererInspectorCommandRoute) -> Self { + Self { + session: DevToolsSessionKey::from_wire_session_id( + inspector_session_id + .as_deref() + .filter(|session_id| !session_id.is_empty()), + ), + route, + first_dispatch: RendererInspectorFirstDispatchLifecycle::OrderedUntilFirstDispatch, + } + } + + pub fn session(&self) -> &DevToolsSessionKey { + &self.session + } + + pub fn route(&self) -> RendererInspectorCommandRoute { + self.route + } + + pub fn first_dispatch_lifecycle(&self) -> RendererInspectorFirstDispatchLifecycle { + self.first_dispatch + } +} + +/// Strongly typed DevToolsSession ingress. The payload deliberately does not +/// carry another session id: every operation that accesses a frontend +/// `V8InspectorSession` must obtain its identity and dispatch policy from this +/// envelope. +pub struct RendererInspectorCommandEnvelope { + metadata: RendererInspectorCommandMetadata, + command: RendererInspectorPageCommand, +} + +impl RendererInspectorCommandEnvelope { + fn new( + inspector_session_id: Option, + route: RendererInspectorCommandRoute, + command: RendererInspectorPageCommand, + ) -> Self { + Self::with_metadata( + RendererInspectorCommandMetadata::new(inspector_session_id, route), + command, + ) + } + + fn with_metadata( + metadata: RendererInspectorCommandMetadata, + command: RendererInspectorPageCommand, + ) -> Self { + Self { metadata, command } + } + + pub fn metadata(&self) -> &RendererInspectorCommandMetadata { + &self.metadata + } + + pub(crate) fn into_parts( + self, + ) -> ( + RendererInspectorCommandMetadata, + RendererInspectorPageCommand, + ) { + (self.metadata, self.command) + } + + pub(crate) fn requires_materialized_child_realms(&self) -> bool { + matches!( + &self.command, + RendererInspectorPageCommand::RuntimeEnableEvents + ) + } + + pub(crate) fn cdp_nav_timing_label(&self) -> Option<&'static str> { + match &self.command { + RendererInspectorPageCommand::DispatchQueuedRuntimeInspectorCommand { .. } => { + Some("DispatchQueuedRuntimeInspectorCommand") + } + RendererInspectorPageCommand::RuntimeEnableEvents => Some("RuntimeEnableEvents"), + RendererInspectorPageCommand::ApplyRuntimeProtocolState { .. } => { + Some("ApplyRuntimeProtocolState") + } + RendererInspectorPageCommand::DetachRuntimeInspectorSession { .. } => { + Some("DetachRuntimeInspectorSession") + } + RendererInspectorPageCommand::DocumentNodeSnapshotForObjectId { .. } => { + Some("DocumentNodeSnapshotForObjectId") + } + RendererInspectorPageCommand::AccessibilityTreePayloadsForObjectId { .. } => { + Some("AccessibilityTreePayloadsForObjectId") + } + RendererInspectorPageCommand::AccessibilityNodeAndAncestorPayloadsForObjectId { + .. + } => Some("AccessibilityNodeAndAncestorPayloadsForObjectId"), + RendererInspectorPageCommand::AccessibilityPartialTreePayloadsForObjectId { + .. + } => Some("AccessibilityPartialTreePayloadsForObjectId"), + RendererInspectorPageCommand::OuterHtmlForObjectId { .. } => { + Some("OuterHtmlForObjectId") + } + RendererInspectorPageCommand::ScrollObjectNodeIntoViewIfNeeded { .. } => { + Some("ScrollObjectNodeIntoViewIfNeeded") + } + RendererInspectorPageCommand::ClientRectForObjectId { .. } => { + Some("ClientRectForObjectId") + } + RendererInspectorPageCommand::DocumentGeometryForObjectId { .. } => { + Some("DocumentGeometryForObjectId") + } + RendererInspectorPageCommand::NodeHasGeometryForObjectId { .. } => { + Some("NodeHasGeometryForObjectId") + } + RendererInspectorPageCommand::SetFileInputFilesForObjectId { .. } => { + Some("SetFileInputFilesForObjectId") + } + RendererInspectorPageCommand::ResolveRuntimeObjectForBackendNodeId { .. } => { + Some("ResolveRuntimeObjectForBackendNodeId") + } + RendererInspectorPageCommand::ResolveBlobObject { .. } => Some("ResolveBlobObject"), + _ => None, + } + } + + pub(crate) fn uses_cpu_throttling(&self) -> bool { + matches!( + &self.command, + RendererInspectorPageCommand::DispatchRuntimeProtocolMessage { .. } + | RendererInspectorPageCommand::DispatchRuntimeProtocolMessageWithDeferredResponse { .. } + | RendererInspectorPageCommand::DispatchQueuedRuntimeInspectorCommand { .. } + | RendererInspectorPageCommand::DispatchRuntimeProtocolMessageWithContextResolution { .. } + | RendererInspectorPageCommand::DispatchRuntimeProtocolMessageWithContextResolutionAndDeferredResponse { .. } + | RendererInspectorPageCommand::DomDebuggerGetEventListeners { .. } + | RendererInspectorPageCommand::FocusDocumentNodeForObjectId { .. } + ) + } +} + +pub(crate) enum RendererInspectorPageCommand { + DispatchRuntimeProtocolMessage { + raw_json: String, + }, + DispatchRuntimeProtocolMessageWithDeferredResponse { + raw_json: String, + deferred_response: RendererRuntimeInspectorResponseSender, + }, + DispatchQueuedRuntimeInspectorCommand { + command_id: u64, + }, + DispatchRuntimeProtocolMessageWithContextResolution { + action: String, + raw_json: String, + }, + DispatchRuntimeProtocolMessageWithContextResolutionAndDeferredResponse { + action: String, + raw_json: String, + deferred_response: RendererRuntimeInspectorResponseSender, + }, + RuntimeEnableEvents, + ApplyRuntimeProtocolState { + session_restore_snapshots: Vec, + isolated_worlds: Vec, + stored_runtime_bindings: Vec, + session_runtime_bindings: Vec, + }, + DetachRuntimeInspectorSession { + pause_guard: RendererRuntimeInspectorSessionDetachGuard, + }, + AddRuntimeBinding { + name: String, + execution_context_name: Option, + execution_context_id: Option, + }, + DomDebuggerGetEventListeners { + object_id: String, + depth: i32, + pierce: bool, + }, + ComputedStylePropertiesForObjectId { + object_id: String, + }, + ScrollObjectNodeIntoViewIfNeeded { + object_id: String, + rect: Option, + }, + ClientRectForObjectId { + object_id: String, + }, + DocumentGeometryForObjectId { + object_id: String, + }, + NodeHasGeometryForObjectId { + object_id: String, + }, + FocusDocumentNodeForObjectId { + object_id: String, + }, + SetFileInputFilesForObjectId { + object_id: String, + files: Vec, + append: bool, + }, + DocumentNodeSnapshotForObjectId { + include_whitespace: bool, + object_id: String, + depth: i32, + pierce: bool, + }, + AccessibilityTreePayloadsForObjectId { + object_id: String, + }, + AccessibilityNodeAndAncestorPayloadsForObjectId { + object_id: String, + }, + AccessibilityPartialTreePayloadsForObjectId { + object_id: String, + fetch_relatives: bool, + }, + OuterHtmlForObjectId { + object_id: String, + include_shadow_dom: bool, + }, + ResolveRuntimeObjectForBackendNodeId { + backend_node_id: u32, + execution_context_id: Option, + object_group: Option, + }, + ResolveBlobObject { + object_id: String, + }, +} + #[non_exhaustive] pub enum RendererPageCommand { + Inspector(RendererInspectorCommandEnvelope), EvaluateExpression { expression: String, await_promise: bool, @@ -3616,36 +3875,6 @@ pub enum RendererPageCommand { auto_repeat: bool, should_insert_text: bool, }, - DispatchRuntimeProtocolMessage { - inspector_session_id: Option, - raw_json: String, - }, - DispatchRuntimeProtocolMessageWithDeferredResponse { - inspector_session_id: Option, - raw_json: String, - deferred_response: RendererRuntimeInspectorResponseSender, - }, - DispatchQueuedRuntimeInspectorCommand { - command_id: u64, - inspector_session_id: Option, - }, - DispatchRuntimeProtocolMessageWithContextResolution { - inspector_session_id: Option, - action: String, - raw_json: String, - }, - DispatchRuntimeProtocolMessageWithContextResolutionAndDeferredResponse { - inspector_session_id: Option, - action: String, - raw_json: String, - deferred_response: RendererRuntimeInspectorResponseSender, - }, - DomDebuggerGetEventListeners { - inspector_session_id: Option, - object_id: String, - depth: i32, - pierce: bool, - }, DomDebuggerConfigureEventListenerBreakpoint { inspector_session_id: Option, breakpoint: RendererDomDebuggerEventListenerBreakpoint, @@ -3662,26 +3891,6 @@ pub enum RendererPageCommand { breakpoint_type: String, enabled: bool, }, - RuntimeEnableEvents { - inspector_session_id: Option, - }, - ApplyRuntimeProtocolState { - inspector_session_id: Option, - session_restore_snapshots: Vec, - isolated_worlds: Vec, - stored_runtime_bindings: Vec, - session_runtime_bindings: Vec, - }, - DetachRuntimeInspectorSession { - inspector_session_id: Option, - pause_guard: RendererRuntimeInspectorSessionDetachGuard, - }, - AddRuntimeBinding { - inspector_session_id: Option, - name: String, - execution_context_name: Option, - execution_context_id: Option, - }, CreateIsolatedWorld { name: String, grant_universal_access: bool, @@ -3741,35 +3950,18 @@ pub enum RendererPageCommand { ComputedStylePropertiesForBackendNodeId { backend_node_id: u32, }, - ComputedStylePropertiesForObjectId { - inspector_session_id: Option, - object_id: String, - }, SetInlineStyleSheetTextForStyleSheetId { inspector_session_id: Option, style_sheet_id: String, text: String, }, - ScrollObjectNodeIntoViewIfNeeded { - inspector_session_id: Option, - object_id: String, - rect: Option, - }, ScrollBackendNodeIntoViewIfNeeded { backend_node_id: u32, rect: Option, }, - ClientRectForObjectId { - inspector_session_id: Option, - object_id: String, - }, ClientRectForBackendNodeId { backend_node_id: u32, }, - DocumentGeometryForObjectId { - inspector_session_id: Option, - object_id: String, - }, DocumentGeometryForBackendNodeId { backend_node_id: u32, }, @@ -3780,10 +3972,6 @@ pub enum RendererPageCommand { include_user_agent_shadow_dom: bool, ignore_pointer_events_none: bool, }, - NodeHasGeometryForObjectId { - inspector_session_id: Option, - object_id: String, - }, NodeHasGeometryForBackendNodeId { backend_node_id: u32, }, @@ -3801,10 +3989,6 @@ pub enum RendererPageCommand { FocusDocumentBackendNode { backend_node_id: u32, }, - FocusDocumentNodeForObjectId { - inspector_session_id: Option, - object_id: String, - }, TriggerAutofill(RendererAutofillTriggerRequest), ResetNavigationHistory, SetFileInputFilesForBackendNodeId { @@ -3812,19 +3996,6 @@ pub enum RendererPageCommand { files: Vec, append: bool, }, - SetFileInputFilesForObjectId { - inspector_session_id: Option, - object_id: String, - files: Vec, - append: bool, - }, - DocumentNodeSnapshotForObjectId { - inspector_session_id: Option, - include_whitespace: bool, - object_id: String, - depth: i32, - pierce: bool, - }, DocumentNodeSnapshotForBackendNodeId { backend_node_id: u32, depth: i32, @@ -3964,19 +4135,6 @@ pub enum RendererPageCommand { AccessibilityNodePayloadForChildFrame { frame_id: String, }, - AccessibilityTreePayloadsForObjectId { - inspector_session_id: Option, - object_id: String, - }, - AccessibilityNodeAndAncestorPayloadsForObjectId { - inspector_session_id: Option, - object_id: String, - }, - AccessibilityPartialTreePayloadsForObjectId { - inspector_session_id: Option, - object_id: String, - fetch_relatives: bool, - }, StyleSheetPayloadForStyleSheetId { inspector_session_id: Option, style_sheet_id: String, @@ -3990,11 +4148,6 @@ pub enum RendererPageCommand { OuterHtmlForDocument { include_shadow_dom: bool, }, - OuterHtmlForObjectId { - inspector_session_id: Option, - object_id: String, - include_shadow_dom: bool, - }, OuterHtmlForBackendNodeId { backend_node_id: u32, include_shadow_dom: bool, @@ -4005,16 +4158,6 @@ pub enum RendererPageCommand { SerializeHtml, LayoutMetrics, CaptureScreenshot(RendererCaptureScreenshotRequest), - ResolveRuntimeObjectForBackendNodeId { - inspector_session_id: Option, - backend_node_id: u32, - execution_context_id: Option, - object_group: Option, - }, - ResolveBlobObject { - inspector_session_id: Option, - object_id: String, - }, BlobBytesForUuid { uuid: String, }, @@ -4169,39 +4312,353 @@ pub enum RendererRuntimeRemoteObjectResolution { } impl RendererPageCommand { - /// Returns the V8 Inspector session whose message order owns this command's - /// first-dispatch boundary. Direct Inspector commands and the owner fallback - /// for pause-routable commands share the same session lane, preventing - /// same-session overtaking without coupling independent sessions. - pub(super) fn first_dispatch_inspector_session(&self) -> Option { - let inspector_session_id = match self { - Self::DispatchRuntimeProtocolMessage { - inspector_session_id, - .. - } - | Self::DispatchRuntimeProtocolMessageWithDeferredResponse { - inspector_session_id, - .. - } - | Self::DispatchQueuedRuntimeInspectorCommand { - inspector_session_id, - .. - } - | Self::DispatchRuntimeProtocolMessageWithContextResolution { - inspector_session_id, - .. - } - | Self::DispatchRuntimeProtocolMessageWithContextResolutionAndDeferredResponse { - inspector_session_id, - .. - } => inspector_session_id.as_deref(), - _ => return None, - }; - Some(DevToolsSessionKey::from_wire_session_id( + fn inspector_command( + inspector_session_id: Option, + route: RendererInspectorCommandRoute, + command: RendererInspectorPageCommand, + ) -> Self { + Self::Inspector(RendererInspectorCommandEnvelope::new( inspector_session_id, + route, + command, )) } + pub fn dispatch_runtime_protocol_message( + inspector_session_id: Option, + route: RendererInspectorCommandRoute, + raw_json: String, + ) -> Self { + Self::inspector_command( + inspector_session_id, + route, + RendererInspectorPageCommand::DispatchRuntimeProtocolMessage { raw_json }, + ) + } + + pub fn dispatch_runtime_protocol_message_with_deferred_response( + inspector_session_id: Option, + route: RendererInspectorCommandRoute, + raw_json: String, + deferred_response: RendererRuntimeInspectorResponseSender, + ) -> Self { + Self::inspector_command( + inspector_session_id, + route, + RendererInspectorPageCommand::DispatchRuntimeProtocolMessageWithDeferredResponse { + raw_json, + deferred_response, + }, + ) + } + + pub fn dispatch_queued_runtime_inspector_command( + metadata: RendererInspectorCommandMetadata, + command_id: u64, + ) -> Self { + Self::Inspector(RendererInspectorCommandEnvelope::with_metadata( + metadata, + RendererInspectorPageCommand::DispatchQueuedRuntimeInspectorCommand { command_id }, + )) + } + + pub fn dispatch_runtime_protocol_message_with_context_resolution( + inspector_session_id: Option, + route: RendererInspectorCommandRoute, + action: String, + raw_json: String, + ) -> Self { + Self::inspector_command( + inspector_session_id, + route, + RendererInspectorPageCommand::DispatchRuntimeProtocolMessageWithContextResolution { + action, + raw_json, + }, + ) + } + + pub fn dispatch_runtime_protocol_message_with_context_resolution_and_deferred_response( + inspector_session_id: Option, + route: RendererInspectorCommandRoute, + action: String, + raw_json: String, + deferred_response: RendererRuntimeInspectorResponseSender, + ) -> Self { + Self::inspector_command( + inspector_session_id, + route, + RendererInspectorPageCommand::DispatchRuntimeProtocolMessageWithContextResolutionAndDeferredResponse { + action, + raw_json, + deferred_response, + }, + ) + } + + pub fn runtime_enable_events(inspector_session_id: Option) -> Self { + Self::inspector_command( + inspector_session_id, + RendererInspectorCommandRoute::MainThread, + RendererInspectorPageCommand::RuntimeEnableEvents, + ) + } + + pub fn apply_runtime_protocol_state( + inspector_session_id: Option, + session_restore_snapshots: Vec, + isolated_worlds: Vec, + stored_runtime_bindings: Vec, + session_runtime_bindings: Vec, + ) -> Self { + Self::inspector_command( + inspector_session_id, + RendererInspectorCommandRoute::MainThread, + RendererInspectorPageCommand::ApplyRuntimeProtocolState { + session_restore_snapshots, + isolated_worlds, + stored_runtime_bindings, + session_runtime_bindings, + }, + ) + } + + pub fn detach_runtime_inspector_session( + inspector_session_id: Option, + pause_guard: RendererRuntimeInspectorSessionDetachGuard, + ) -> Self { + Self::inspector_command( + inspector_session_id, + RendererInspectorCommandRoute::Io, + RendererInspectorPageCommand::DetachRuntimeInspectorSession { pause_guard }, + ) + } + + pub fn add_runtime_binding( + inspector_session_id: Option, + name: String, + execution_context_name: Option, + execution_context_id: Option, + ) -> Self { + Self::inspector_command( + inspector_session_id, + RendererInspectorCommandRoute::MainThread, + RendererInspectorPageCommand::AddRuntimeBinding { + name, + execution_context_name, + execution_context_id, + }, + ) + } + + pub fn dom_debugger_get_event_listeners( + inspector_session_id: Option, + object_id: String, + depth: i32, + pierce: bool, + ) -> Self { + Self::inspector_command( + inspector_session_id, + RendererInspectorCommandRoute::MainThread, + RendererInspectorPageCommand::DomDebuggerGetEventListeners { + object_id, + depth, + pierce, + }, + ) + } + + pub fn computed_style_properties_for_object_id( + inspector_session_id: Option, + object_id: String, + ) -> Self { + Self::inspector_command( + inspector_session_id, + RendererInspectorCommandRoute::MainThread, + RendererInspectorPageCommand::ComputedStylePropertiesForObjectId { object_id }, + ) + } + + pub fn scroll_object_node_into_view_if_needed( + inspector_session_id: Option, + object_id: String, + rect: Option, + ) -> Self { + Self::inspector_command( + inspector_session_id, + RendererInspectorCommandRoute::MainThread, + RendererInspectorPageCommand::ScrollObjectNodeIntoViewIfNeeded { object_id, rect }, + ) + } + + pub fn client_rect_for_object_id( + inspector_session_id: Option, + object_id: String, + ) -> Self { + Self::inspector_command( + inspector_session_id, + RendererInspectorCommandRoute::MainThread, + RendererInspectorPageCommand::ClientRectForObjectId { object_id }, + ) + } + + pub fn document_geometry_for_object_id( + inspector_session_id: Option, + object_id: String, + ) -> Self { + Self::inspector_command( + inspector_session_id, + RendererInspectorCommandRoute::MainThread, + RendererInspectorPageCommand::DocumentGeometryForObjectId { object_id }, + ) + } + + pub fn node_has_geometry_for_object_id( + inspector_session_id: Option, + object_id: String, + ) -> Self { + Self::inspector_command( + inspector_session_id, + RendererInspectorCommandRoute::MainThread, + RendererInspectorPageCommand::NodeHasGeometryForObjectId { object_id }, + ) + } + + pub fn focus_document_node_for_object_id( + inspector_session_id: Option, + object_id: String, + ) -> Self { + Self::inspector_command( + inspector_session_id, + RendererInspectorCommandRoute::MainThread, + RendererInspectorPageCommand::FocusDocumentNodeForObjectId { object_id }, + ) + } + + pub fn set_file_input_files_for_object_id( + inspector_session_id: Option, + object_id: String, + files: Vec, + append: bool, + ) -> Self { + Self::inspector_command( + inspector_session_id, + RendererInspectorCommandRoute::MainThread, + RendererInspectorPageCommand::SetFileInputFilesForObjectId { + object_id, + files, + append, + }, + ) + } + + pub fn document_node_snapshot_for_object_id( + inspector_session_id: Option, + include_whitespace: bool, + object_id: String, + depth: i32, + pierce: bool, + ) -> Self { + Self::inspector_command( + inspector_session_id, + RendererInspectorCommandRoute::MainThread, + RendererInspectorPageCommand::DocumentNodeSnapshotForObjectId { + include_whitespace, + object_id, + depth, + pierce, + }, + ) + } + + pub fn accessibility_tree_payloads_for_object_id( + inspector_session_id: Option, + object_id: String, + ) -> Self { + Self::inspector_command( + inspector_session_id, + RendererInspectorCommandRoute::MainThread, + RendererInspectorPageCommand::AccessibilityTreePayloadsForObjectId { object_id }, + ) + } + + pub fn accessibility_node_and_ancestor_payloads_for_object_id( + inspector_session_id: Option, + object_id: String, + ) -> Self { + Self::inspector_command( + inspector_session_id, + RendererInspectorCommandRoute::MainThread, + RendererInspectorPageCommand::AccessibilityNodeAndAncestorPayloadsForObjectId { + object_id, + }, + ) + } + + pub fn accessibility_partial_tree_payloads_for_object_id( + inspector_session_id: Option, + object_id: String, + fetch_relatives: bool, + ) -> Self { + Self::inspector_command( + inspector_session_id, + RendererInspectorCommandRoute::MainThread, + RendererInspectorPageCommand::AccessibilityPartialTreePayloadsForObjectId { + object_id, + fetch_relatives, + }, + ) + } + + pub fn outer_html_for_object_id( + inspector_session_id: Option, + object_id: String, + include_shadow_dom: bool, + ) -> Self { + Self::inspector_command( + inspector_session_id, + RendererInspectorCommandRoute::MainThread, + RendererInspectorPageCommand::OuterHtmlForObjectId { + object_id, + include_shadow_dom, + }, + ) + } + + pub fn resolve_runtime_object_for_backend_node_id( + inspector_session_id: Option, + backend_node_id: u32, + execution_context_id: Option, + object_group: Option, + ) -> Self { + Self::inspector_command( + inspector_session_id, + RendererInspectorCommandRoute::MainThread, + RendererInspectorPageCommand::ResolveRuntimeObjectForBackendNodeId { + backend_node_id, + execution_context_id, + object_group, + }, + ) + } + + pub fn resolve_blob_object(inspector_session_id: Option, object_id: String) -> Self { + Self::inspector_command( + inspector_session_id, + RendererInspectorCommandRoute::MainThread, + RendererInspectorPageCommand::ResolveBlobObject { object_id }, + ) + } + + /// Metadata is structurally present for every command that can access a + /// frontend V8 Inspector session; no command-variant allowlist is involved. + pub fn inspector_metadata(&self) -> Option<&RendererInspectorCommandMetadata> { + match self { + Self::Inspector(envelope) => Some(envelope.metadata()), + _ => None, + } + } + pub(crate) fn interruptible_by_javascript_dialog(&self) -> bool { #[cfg(test)] if matches!(self, Self::TakeDocumentLifecycleEvents) { @@ -4220,13 +4677,8 @@ impl RendererPageCommand { pub(crate) fn cdp_nav_timing_label(&self) -> Option<&'static str> { match self { - Self::RuntimeEnableEvents { .. } => Some("RuntimeEnableEvents"), - Self::DispatchQueuedRuntimeInspectorCommand { .. } => { - Some("DispatchQueuedRuntimeInspectorCommand") - } + Self::Inspector(envelope) => envelope.cdp_nav_timing_label(), Self::DocumentStorageKeySnapshot => Some("DocumentStorageKeySnapshot"), - Self::ApplyRuntimeProtocolState { .. } => Some("ApplyRuntimeProtocolState"), - Self::DetachRuntimeInspectorSession { .. } => Some("DetachRuntimeInspectorSession"), Self::CreateIsolatedWorldRuntimeActivity { .. } => { Some("CreateIsolatedWorldRuntimeActivity") } @@ -4242,7 +4694,6 @@ impl RendererPageCommand { Self::ChildFrameDocumentRootNodeReference { .. } => { Some("ChildFrameDocumentRootNodeReference") } - Self::DocumentNodeSnapshotForObjectId { .. } => Some("DocumentNodeSnapshotForObjectId"), Self::DocumentNodeSnapshotForBackendNodeId { .. } => { Some("DocumentNodeSnapshotForBackendNodeId") } @@ -4316,36 +4767,20 @@ impl RendererPageCommand { Self::AccessibilityNodePayloadForChildFrame { .. } => { Some("AccessibilityNodePayloadForChildFrame") } - Self::AccessibilityTreePayloadsForObjectId { .. } => { - Some("AccessibilityTreePayloadsForObjectId") - } - Self::AccessibilityNodeAndAncestorPayloadsForObjectId { .. } => { - Some("AccessibilityNodeAndAncestorPayloadsForObjectId") - } - Self::AccessibilityPartialTreePayloadsForObjectId { .. } => { - Some("AccessibilityPartialTreePayloadsForObjectId") - } Self::OuterHtmlForDocument { .. } => Some("OuterHtmlForDocument"), - Self::OuterHtmlForObjectId { .. } => Some("OuterHtmlForObjectId"), Self::OuterHtmlForBackendNodeId { .. } => Some("OuterHtmlForBackendNodeId"), Self::RenderPageDump { .. } => Some("RenderPageDump"), Self::SerializeHtml => Some("SerializeHtml"), Self::LayoutMetrics => Some("LayoutMetrics"), Self::CaptureScreenshot(_) => Some("CaptureScreenshot"), - Self::ScrollObjectNodeIntoViewIfNeeded { .. } => { - Some("ScrollObjectNodeIntoViewIfNeeded") - } Self::ScrollBackendNodeIntoViewIfNeeded { .. } => { Some("ScrollBackendNodeIntoViewIfNeeded") } - Self::ClientRectForObjectId { .. } => Some("ClientRectForObjectId"), Self::ClientRectForBackendNodeId { .. } => Some("ClientRectForBackendNodeId"), - Self::DocumentGeometryForObjectId { .. } => Some("DocumentGeometryForObjectId"), Self::DocumentGeometryForBackendNodeId { .. } => { Some("DocumentGeometryForBackendNodeId") } Self::DocumentHitTest { .. } => Some("DocumentHitTest"), - Self::NodeHasGeometryForObjectId { .. } => Some("NodeHasGeometryForObjectId"), Self::NodeHasGeometryForBackendNodeId { .. } => Some("NodeHasGeometryForBackendNodeId"), Self::RemoveDocumentBackendNodeId { .. } => Some("RemoveDocumentBackendNodeId"), Self::EditDocumentNode { .. } => Some("EditDocumentNode"), @@ -4356,11 +4791,6 @@ impl RendererPageCommand { Self::SetFileInputFilesForBackendNodeId { .. } => { Some("SetFileInputFilesForBackendNodeId") } - Self::SetFileInputFilesForObjectId { .. } => Some("SetFileInputFilesForObjectId"), - Self::ResolveRuntimeObjectForBackendNodeId { .. } => { - Some("ResolveRuntimeObjectForBackendNodeId") - } - Self::ResolveBlobObject { .. } => Some("ResolveBlobObject"), Self::BlobBytesForUuid { .. } => Some("BlobBytesForUuid"), Self::DocumentFrontendNodeIdsForBackendNodeIds { .. } => { Some("DocumentFrontendNodeIdsForBackendNodeIds") @@ -4377,6 +4807,87 @@ impl RendererPageCommand { } } +#[cfg(test)] +mod renderer_inspector_command_envelope_tests { + use super::*; + + fn assert_metadata( + command: &RendererPageCommand, + session: DevToolsSessionKey, + route: RendererInspectorCommandRoute, + ) { + let metadata = command + .inspector_metadata() + .expect("every frontend V8 Inspector operation must carry command metadata"); + assert_eq!(metadata.session(), &session); + assert_eq!(metadata.route(), route); + assert_eq!( + metadata.first_dispatch_lifecycle(), + RendererInspectorFirstDispatchLifecycle::OrderedUntilFirstDispatch + ); + } + + #[test] + fn raw_special_and_runtime_object_commands_share_one_inspector_boundary() { + let attached = Some("SID-envelope".to_owned()); + let main_thread_commands = [ + RendererPageCommand::runtime_enable_events(attached.clone()), + RendererPageCommand::apply_runtime_protocol_state( + attached.clone(), + Vec::new(), + Vec::new(), + Vec::new(), + Vec::new(), + ), + RendererPageCommand::add_runtime_binding( + attached.clone(), + "binding".to_owned(), + None, + None, + ), + RendererPageCommand::computed_style_properties_for_object_id( + attached, + "runtime-object".to_owned(), + ), + ]; + for command in &main_thread_commands { + assert_metadata( + command, + DevToolsSessionKey::Attached("SID-envelope".to_owned()), + RendererInspectorCommandRoute::MainThread, + ); + } + + let io_command = RendererPageCommand::dispatch_runtime_protocol_message( + None, + RendererInspectorCommandRoute::Io, + r#"{"id":1,"method":"Runtime.terminateExecution"}"#.to_owned(), + ); + assert_metadata( + &io_command, + DevToolsSessionKey::Primary, + RendererInspectorCommandRoute::Io, + ); + + assert!( + RendererPageCommand::PageDiagnosticsSnapshot + .inspector_metadata() + .is_none(), + "ordinary Page commands must remain outside Inspector lanes" + ); + } + + #[test] + fn empty_wire_session_id_normalizes_to_the_primary_session() { + let command = RendererPageCommand::runtime_enable_events(Some(String::new())); + assert_metadata( + &command, + DevToolsSessionKey::Primary, + RendererInspectorCommandRoute::MainThread, + ); + } +} + #[derive(Debug, Clone, PartialEq)] pub struct RendererRuntimeRemoteObject { protocol_value: Value, diff --git a/moli-renderer-v8/src/runtime/tests.rs b/moli-renderer-v8/src/runtime/tests.rs index 21c8664284..3e50a27db9 100644 --- a/moli-renderer-v8/src/runtime/tests.rs +++ b/moli-renderer-v8/src/runtime/tests.rs @@ -1,13 +1,14 @@ use super::{ ExternalRawDocumentBodyStream, JsLocalExecutor, JsRuntime, JsRuntimeOwner, PageVmInitStage, PreparedRendererDocument, RendererCaptureScreenshotReply, RendererDragData, - RendererDraggedDirectory, RendererDraggedFile, RendererInspectorProtocolConfiguration, - RendererInspectorSessionRestoreSnapshot, RendererOutputItem, RendererOutputPublication, - RendererOutputResidenceIdentity, RendererOutputTransportMessage, - RendererOutputTransportReceiver, RendererOutputTransportSender, RendererOwnerAction, - RendererPageCommand, RendererPageHandle, RendererPageReply, RendererPageTestingHandle, - RendererPendingPopupActivation, RendererPreparedDocumentCommitConfiguration, - RendererProtocolObservation, RendererRuntimeCommandOutput, RendererRuntimeInspectorMessage, + RendererDraggedDirectory, RendererDraggedFile, RendererInspectorCommandRoute, + RendererInspectorProtocolConfiguration, RendererInspectorSessionRestoreSnapshot, + RendererOutputItem, RendererOutputPublication, RendererOutputResidenceIdentity, + RendererOutputTransportMessage, RendererOutputTransportReceiver, RendererOutputTransportSender, + RendererOwnerAction, RendererPageCommand, RendererPageHandle, RendererPageReply, + RendererPageTestingHandle, RendererPendingPopupActivation, + RendererPreparedDocumentCommitConfiguration, RendererProtocolObservation, + RendererRuntimeCommandOutput, RendererRuntimeInspectorMessage, RendererRuntimeInspectorResponseSender, }; use crate::local_executor::{is_on_script_execution_lane_for, scope_on_scaffold_js_local_executor}; @@ -3753,9 +3754,10 @@ async fn replacement_navigation_releases_old_inspector_deferred_response_callbac let (response_tx, mut response_rx) = oneshot::channel(); let (dispatch, _) = page .run_async_command( - RendererPageCommand::DispatchRuntimeProtocolMessageWithDeferredResponse { + RendererPageCommand::dispatch_runtime_protocol_message_with_deferred_response( inspector_session_id, - raw_json: serde_json::json!({ + RendererInspectorCommandRoute::MainThread, + serde_json::json!({ "id": call_id, "method": "Runtime.evaluate", "params": { @@ -3766,11 +3768,11 @@ async fn replacement_navigation_releases_old_inspector_deferred_response_callbac }, }) .to_string(), - deferred_response: RendererRuntimeInspectorResponseSender::new( + RendererRuntimeInspectorResponseSender::new( call_id, response_tx, ), - }, + ), ) .await .expect("never-settling Runtime.evaluate should register a deferred callback"); @@ -3816,9 +3818,10 @@ async fn replacement_navigation_releases_old_inspector_deferred_response_callbac let (replacement_response_tx, mut replacement_response_rx) = oneshot::channel(); let replacement_completion = page .enqueue_async_command( - RendererPageCommand::DispatchRuntimeProtocolMessageWithDeferredResponse { - inspector_session_id: Some("session-a".to_owned()), - raw_json: serde_json::json!({ + RendererPageCommand::dispatch_runtime_protocol_message_with_deferred_response( + Some("session-a".to_owned()), + RendererInspectorCommandRoute::MainThread, + serde_json::json!({ "id": reused_call_id, "method": "Runtime.evaluate", "params": { @@ -3828,11 +3831,11 @@ async fn replacement_navigation_releases_old_inspector_deferred_response_callbac }, }) .to_string(), - deferred_response: RendererRuntimeInspectorResponseSender::new( + RendererRuntimeInspectorResponseSender::new( reused_call_id, replacement_response_tx, ), - }, + ), ) .expect("replacement PageVM command should enqueue") .wait() @@ -3880,9 +3883,10 @@ async fn runtime_binding_replay_cannot_consume_same_id_frontend_deferred_respons let (response_tx, mut response_rx) = oneshot::channel(); let (dispatch, _) = page .run_async_command( - RendererPageCommand::DispatchRuntimeProtocolMessageWithDeferredResponse { - inspector_session_id: None, - raw_json: serde_json::json!({ + RendererPageCommand::dispatch_runtime_protocol_message_with_deferred_response( + None, + RendererInspectorCommandRoute::MainThread, + serde_json::json!({ "id": colliding_call_id, "method": "Runtime.evaluate", "params": { @@ -3892,11 +3896,11 @@ async fn runtime_binding_replay_cannot_consume_same_id_frontend_deferred_respons }, }) .to_string(), - deferred_response: RendererRuntimeInspectorResponseSender::new( + RendererRuntimeInspectorResponseSender::new( colliding_call_id, response_tx, ), - }, + ), ) .await .expect("frontend awaitPromise should remain deferred"); @@ -9185,9 +9189,9 @@ async fn runtime_enable_waits_for_queued_child_realm_before_reporting_contexts() }) .expect("child realm setup command should enqueue"); let enable = page - .enqueue_async_command(RendererPageCommand::RuntimeEnableEvents { - inspector_session_id: Some("SID-runtime-enable-child-barrier".to_owned()), - }) + .enqueue_async_command(RendererPageCommand::runtime_enable_events(Some( + "SID-runtime-enable-child-barrier".to_owned(), + ))) .expect("Runtime.enable command should enqueue behind child realm setup"); let setup = setup @@ -9646,13 +9650,13 @@ async fn apply_runtime_protocol_state_keeps_session_binding_replay_scoped() { }; let (reply, _) = page - .run_async_command(RendererPageCommand::ApplyRuntimeProtocolState { - inspector_session_id: Some("SID-primary".to_owned()), - session_restore_snapshots: Vec::new(), - isolated_worlds: Vec::new(), - stored_runtime_bindings: vec![stored_only_binding], - session_runtime_bindings: Vec::new(), - }) + .run_async_command(RendererPageCommand::apply_runtime_protocol_state( + Some("SID-primary".to_owned()), + Vec::new(), + Vec::new(), + vec![stored_only_binding], + Vec::new(), + )) .await .expect("runtime protocol state should apply"); assert!( @@ -16177,15 +16181,16 @@ async fn command_turn_output_scope_is_removed_after_command_error() { let (invalid_response_tx, _invalid_response_rx) = oneshot::channel(); let invalid = page .enqueue_async_command( - RendererPageCommand::DispatchRuntimeProtocolMessageWithContextResolutionAndDeferredResponse { - inspector_session_id: None, - action: "evaluate".to_owned(), - raw_json: "{".to_owned(), - deferred_response: RendererRuntimeInspectorResponseSender::new( + RendererPageCommand::dispatch_runtime_protocol_message_with_context_resolution_and_deferred_response( + None, + RendererInspectorCommandRoute::MainThread, + "evaluate".to_owned(), + "{".to_owned(), + RendererRuntimeInspectorResponseSender::new( 710_220, invalid_response_tx, ), - }, + ), ) .expect("invalid Runtime.evaluate should enqueue") .wait() @@ -16199,9 +16204,10 @@ async fn command_turn_output_scope_is_removed_after_command_error() { let (response_tx, _response_rx) = oneshot::channel(); let completion = page .enqueue_async_command( - RendererPageCommand::DispatchRuntimeProtocolMessageWithDeferredResponse { - inspector_session_id: None, - raw_json: serde_json::json!({ + RendererPageCommand::dispatch_runtime_protocol_message_with_deferred_response( + None, + RendererInspectorCommandRoute::MainThread, + serde_json::json!({ "id": call_id, "method": "Runtime.evaluate", "params": { @@ -16210,11 +16216,8 @@ async fn command_turn_output_scope_is_removed_after_command_error() { }, }) .to_string(), - deferred_response: RendererRuntimeInspectorResponseSender::new( - call_id, - response_tx, - ), - }, + RendererRuntimeInspectorResponseSender::new(call_id, response_tx), + ), ) .expect("the command after an error should enqueue") .wait() @@ -16260,9 +16263,10 @@ async fn runtime_document_close_completion_parks_lifecycle_until_capability_rele let (response_tx, _response_rx) = oneshot::channel(); let completion = page .enqueue_async_command( - RendererPageCommand::DispatchRuntimeProtocolMessageWithDeferredResponse { - inspector_session_id: None, - raw_json: serde_json::json!({ + RendererPageCommand::dispatch_runtime_protocol_message_with_deferred_response( + None, + RendererInspectorCommandRoute::MainThread, + serde_json::json!({ "id": call_id, "method": "Runtime.evaluate", "params": { @@ -16271,11 +16275,11 @@ async fn runtime_document_close_completion_parks_lifecycle_until_capability_rele }, }) .to_string(), - deferred_response: RendererRuntimeInspectorResponseSender::new( + RendererRuntimeInspectorResponseSender::new( call_id, response_tx, ), - }, + ), ) .expect("Runtime.evaluate should enqueue") .wait() @@ -17196,10 +17200,11 @@ async fn dispatch_runtime_protocol_with_output_for_test( ) -> anyhow::Result<(Vec, RendererRuntimeCommandOutput)> { let raw_json = serde_json::to_string(&request)?; let output = page - .enqueue_async_command(RendererPageCommand::DispatchRuntimeProtocolMessage { - inspector_session_id: None, + .enqueue_async_command(RendererPageCommand::dispatch_runtime_protocol_message( + None, + RendererInspectorCommandRoute::MainThread, raw_json, - }) + )) .expect("runtime protocol command should enqueue") .wait() .await?; @@ -17224,11 +17229,12 @@ async fn dispatch_runtime_protocol_with_context_resolution_for_test( let raw_json = serde_json::to_string(&request)?; let (reply, _) = page .run_async_command( - RendererPageCommand::DispatchRuntimeProtocolMessageWithContextResolution { - inspector_session_id: None, - action: action.to_owned(), + RendererPageCommand::dispatch_runtime_protocol_message_with_context_resolution( + None, + RendererInspectorCommandRoute::MainThread, + action.to_owned(), raw_json, - }, + ), ) .await?; match reply { @@ -17263,9 +17269,9 @@ async fn runtime_enable_events_for_inspector_session_for_test( inspector_session_id: Option<&str>, ) -> anyhow::Result> { let (reply, _) = page - .run_async_command(RendererPageCommand::RuntimeEnableEvents { - inspector_session_id: inspector_session_id.map(str::to_owned), - }) + .run_async_command(RendererPageCommand::runtime_enable_events( + inspector_session_id.map(str::to_owned), + )) .await?; match reply { RendererPageReply::RuntimeInspectorProtocolMessages(output) => Ok(output @@ -17407,12 +17413,12 @@ async fn add_runtime_binding_for_test( execution_context_id: Option, ) -> anyhow::Result<()> { let (reply, _) = page - .run_async_command(RendererPageCommand::AddRuntimeBinding { - inspector_session_id: None, - name: name.to_owned(), - execution_context_name: execution_context_name.map(str::to_owned), + .run_async_command(RendererPageCommand::add_runtime_binding( + None, + name.to_owned(), + execution_context_name.map(str::to_owned), execution_context_id, - }) + )) .await?; match reply { RendererPageReply::Unit => Ok(()), diff --git a/moli-renderer-v8/src/script_vm/inspector/v8_backend.rs b/moli-renderer-v8/src/script_vm/inspector/v8_backend.rs index d4c0564600..cd6465b844 100644 --- a/moli-renderer-v8/src/script_vm/inspector/v8_backend.rs +++ b/moli-renderer-v8/src/script_vm/inspector/v8_backend.rs @@ -14,9 +14,16 @@ use std::{ cell::{Cell, RefCell, UnsafeCell}, collections::HashMap, rc::{Rc, Weak}, - sync::atomic::{AtomicI64, Ordering}, + sync::atomic::{AtomicI64, AtomicUsize, Ordering}, }; +thread_local! { + static INTERRUPT_LOOPS: RefCell>> = + RefCell::new(HashMap::new()); +} + +static NEXT_INTERRUPT_ROUTE_ID: AtomicUsize = AtomicUsize::new(1); + struct RendererInspectorClient { isolate: UnsafeCell, context_registry: DocumentInspectorContextRegistry, @@ -35,13 +42,29 @@ struct RendererInspectorPauseSession { pub(super) struct RendererInspectorPauseSessionRegistration { pause_loop: Weak, context_group_id: i32, + agent_token: RendererDevToolsAgentToken, session_key: DevToolsSessionKey, session: Weak, } struct RendererInspectorPauseLoopLocal { bridge: RendererInspectorPauseBridge, + interrupt_route_id: Option, sessions: RefCell>, + interrupt_sessions: RefCell< + HashMap<(RendererDevToolsAgentToken, DevToolsSessionKey), RendererInspectorPauseSession>, + >, +} + +impl Drop for RendererInspectorPauseLoopLocal { + fn drop(&mut self) { + let Some(interrupt_route_id) = self.interrupt_route_id else { + return; + }; + let _ = INTERRUPT_LOOPS.try_with(|loops| { + loops.borrow_mut().remove(&interrupt_route_id); + }); + } } impl Drop for RendererInspectorPauseSessionRegistration { @@ -57,15 +80,38 @@ impl Drop for RendererInspectorPauseSessionRegistration { { sessions.remove(&key); } + let interrupt_key = (self.agent_token, self.session_key.clone()); + let mut interrupt_sessions = pause_loop.interrupt_sessions.borrow_mut(); + if interrupt_sessions + .get(&interrupt_key) + .is_some_and(|entry| Weak::ptr_eq(&entry.session, &self.session)) + { + interrupt_sessions.remove(&interrupt_key); + } } } impl RendererInspectorPauseLoopLocal { - fn new(bridge: RendererInspectorPauseBridge) -> Self { - Self { + fn new(bridge: RendererInspectorPauseBridge) -> Rc { + let interrupt_route_id = bridge.interrupt_route_id(); + let pause_loop = Rc::new(Self { bridge, + interrupt_route_id, sessions: RefCell::new(HashMap::new()), + interrupt_sessions: RefCell::new(HashMap::new()), + }); + if let Some(interrupt_route_id) = interrupt_route_id { + let previous = INTERRUPT_LOOPS.with(|loops| { + loops + .borrow_mut() + .insert(interrupt_route_id, Rc::downgrade(&pause_loop)) + }); + assert!( + previous.is_none(), + "renderer Inspector interrupt route IDs must be unique" + ); } + pause_loop } fn register_session( @@ -77,18 +123,26 @@ impl RendererInspectorPauseLoopLocal { outbound: InspectorOutbound, ) -> RendererInspectorPauseSessionRegistration { let weak_session = Rc::downgrade(session); - self.sessions.borrow_mut().insert( - (context_group_id.get(), session_key.clone()), - RendererInspectorPauseSession { - session: weak_session.clone(), - outbound, - agent_token, - session_key: session_key.clone(), - }, - ); + let route = RendererInspectorPauseSession { + session: weak_session.clone(), + outbound, + agent_token, + session_key: session_key.clone(), + }; + self.sessions + .borrow_mut() + .insert((context_group_id.get(), session_key.clone()), route.clone()); + // Context groups may overlap briefly during document replacement. + // Active-JS interrupts carry the stable renderer-agent identity rather + // than a V8 context-group id, so keep an explicit latest-registration + // index instead of depending on HashMap iteration order. + self.interrupt_sessions + .borrow_mut() + .insert((agent_token, session_key.clone()), route); RendererInspectorPauseSessionRegistration { pause_loop: Rc::downgrade(self), context_group_id: context_group_id.get(), + agent_token, session_key, session: weak_session, } @@ -105,17 +159,35 @@ impl RendererInspectorPauseLoopLocal { } fn dispatch_command(&self, context_group_id: i32, command: RendererInspectorPauseCommand) { - let session_key = DevToolsSessionKey::from_wire_session_id( - command - .inspector_session_id - .as_deref() - .filter(|session_id| !session_id.is_empty()), - ); + let session_key = command.metadata.session().clone(); let session = self .sessions .borrow() .get(&(context_group_id, session_key)) + .filter(|session| session.agent_token == command.agent_token) .cloned(); + self.dispatch_command_to_session(command, session); + } + + fn dispatch_next_io_command_from_interrupt(&self) { + let Some(command) = self.bridge.claim_io_command_for_interrupt() else { + return; + }; + let session_key = command.metadata.session(); + let session = self + .interrupt_sessions + .borrow() + .get(&(command.agent_token, session_key.clone())) + .cloned(); + self.dispatch_command_to_session(command, session); + } + + fn dispatch_command_to_session( + &self, + command: RendererInspectorPauseCommand, + session: Option, + ) { + let mut first_dispatch = self.bridge.first_dispatch_guard(&command); let Some(session) = session else { send_pause_dispatch_error(command.response, "Inspector session is not available"); return; @@ -128,6 +200,7 @@ impl RendererInspectorPauseLoopLocal { session .outbound .register_response_callback(command.response); + first_dispatch.release(); v8_session.dispatch_protocol_message(v8::inspector::StringView::from( command.raw_json.as_bytes(), )); @@ -143,6 +216,38 @@ impl RendererInspectorPauseLoopLocal { } } +fn allocate_interrupt_route_id() -> usize { + NEXT_INTERRUPT_ROUTE_ID + .fetch_update(Ordering::Relaxed, Ordering::Relaxed, |current| { + current.checked_add(1) + }) + .expect("renderer Inspector interrupt route ID exhausted") +} + +unsafe extern "C" fn dispatch_inspector_interrupt( + isolate: v8::UnsafeRawIsolatePtr, + data: *mut std::ffi::c_void, +) { + let interrupt_route_id = data as usize; + let pause_loop = INTERRUPT_LOOPS + .try_with(|loops| { + loops + .borrow() + .get(&interrupt_route_id) + .and_then(Weak::upgrade) + }) + .ok() + .flatten(); + let Some(pause_loop) = pause_loop else { + return; + }; + let mut isolate_ptr = isolate; + let isolate = unsafe { v8::Isolate::ref_from_raw_isolate_ptr_mut(&mut isolate_ptr) }; + with_scoped_inspector_microtasks(isolate, || { + pause_loop.dispatch_next_io_command_from_interrupt(); + }); +} + fn send_pause_dispatch_error(response: RendererRuntimeInspectorResponseSender, message: &str) { let call_id = response.call_id(); let _ = response.send(json!({ @@ -301,8 +406,12 @@ impl RendererInspectorIsolateBackend { let isolate_ptr = unsafe { isolate.as_raw_isolate_ptr() }; let context_registry = DocumentInspectorContextRegistry::default(); let unique_id_state = Rc::new(RendererInspectorClientUniqueIdState::new()); - let pause_bridge = RendererInspectorPauseBridge::default(); - let pause_loop = Rc::new(RendererInspectorPauseLoopLocal::new(pause_bridge.clone())); + let pause_bridge = RendererInspectorPauseBridge::new_interruptible( + isolate.thread_safe_handle(), + dispatch_inspector_interrupt, + allocate_interrupt_route_id(), + ); + let pause_loop = RendererInspectorPauseLoopLocal::new(pause_bridge.clone()); let inspector_client = v8::inspector::V8InspectorClient::new(Box::new(RendererInspectorClient::new( isolate_ptr, diff --git a/moli-renderer-v8/src/script_vm/inspector_pause.rs b/moli-renderer-v8/src/script_vm/inspector_pause.rs index c61324c274..06ba9c39ee 100644 --- a/moli-renderer-v8/src/script_vm/inspector_pause.rs +++ b/moli-renderer-v8/src/script_vm/inspector_pause.rs @@ -1,5 +1,6 @@ use std::{ - collections::{HashSet, VecDeque}, + collections::{HashMap, HashSet, VecDeque}, + ffi::c_void, sync::Arc, }; @@ -8,30 +9,50 @@ use parking_lot::{Condvar, Mutex}; use serde_json::{Value, json}; use crate::runtime::{ - PageId, PendingRendererOutputRecord, RendererOutputResidenceIdentity, - RendererProtocolObservation, RendererRuntimeCommandCausalIdentity, - RendererRuntimeInspectorMessage, RendererRuntimeInspectorMessageBatch, - RendererRuntimeInspectorResponseSender, RendererTurnOutputJournal, + PageId, PendingRendererOutputRecord, RendererInspectorCommandMetadata, + RendererInspectorCommandRoute, RendererOutputResidenceIdentity, RendererProtocolObservation, + RendererRuntimeCommandCausalIdentity, RendererRuntimeInspectorMessage, + RendererRuntimeInspectorMessageBatch, RendererRuntimeInspectorResponseSender, + RendererTurnOutputJournal, }; pub(crate) struct RendererInspectorPauseCommand { command_id: u64, - pub(crate) inspector_session_id: Option, + pub(crate) agent_token: RendererDevToolsAgentToken, + pub(crate) metadata: RendererInspectorCommandMetadata, pub(crate) owner_context_resolution_action: Option, pub(crate) raw_json: String, pub(crate) response: RendererRuntimeInspectorResponseSender, claim_tx: Option>, } +#[derive(Clone, Debug, Eq, Hash, PartialEq)] +struct RendererInspectorFirstDispatchLane { + agent_token: RendererDevToolsAgentToken, + session: DevToolsSessionKey, + route: RendererInspectorCommandRoute, +} + +impl RendererInspectorPauseCommand { + fn first_dispatch_lane(&self) -> RendererInspectorFirstDispatchLane { + RendererInspectorFirstDispatchLane { + agent_token: self.agent_token, + session: self.metadata.session().clone(), + route: self.metadata.route(), + } + } +} + #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum RendererRuntimeInspectorCommandClaim { Owner, - Pause, + Inspector, Canceled, } pub struct RendererRuntimeInspectorCommandRoute { command_id: u64, + metadata: RendererInspectorCommandMetadata, requires_owner_fallback: bool, claim_rx: Option>, bridge: RendererInspectorPauseBridge, @@ -42,6 +63,10 @@ impl RendererRuntimeInspectorCommandRoute { self.command_id } + pub fn metadata(&self) -> &RendererInspectorCommandMetadata { + &self.metadata + } + pub fn requires_owner_fallback(&self) -> bool { self.requires_owner_fallback } @@ -74,6 +99,16 @@ pub(crate) struct RendererInspectorPauseBridge { struct RendererInspectorPauseBridgeShared { state: Mutex, command_ready: Condvar, + interrupt_route: Option, +} + +type RendererInspectorInterruptCallback = + unsafe extern "C" fn(v8::UnsafeRawIsolatePtr, *mut c_void); + +struct RendererInspectorInterruptRoute { + isolate: v8::IsolateHandle, + callback: RendererInspectorInterruptCallback, + callback_data: usize, } #[derive(Clone)] @@ -95,6 +130,7 @@ struct RendererInspectorPauseBridgeState { quit_requested: bool, session_detach_arms: usize, target_closed: bool, + active_first_dispatch_lanes: HashMap, commands: VecDeque, pending_prefaces: VecDeque, paused_sessions_awaiting_resumed: HashSet<(RendererDevToolsAgentToken, DevToolsSessionKey)>, @@ -203,6 +239,28 @@ pub(super) struct RendererInspectorPausePrefaceGuard { id: u64, } +pub(crate) struct RendererInspectorFirstDispatchGuard { + bridge: RendererInspectorPauseBridge, + active: Option<(RendererInspectorFirstDispatchLane, u64)>, +} + +impl Drop for RendererInspectorFirstDispatchGuard { + fn drop(&mut self) { + self.release(); + } +} + +impl RendererInspectorFirstDispatchGuard { + /// Releases the lane at the call boundary into V8 Inspector. The command + /// response and any JavaScript it starts may remain pending after this + /// point; neither is part of the first-dispatch lifetime. + pub(crate) fn release(&mut self) { + if let Some((lane, command_id)) = self.active.take() { + self.bridge.finish_first_dispatch(lane, command_id); + } + } +} + impl Drop for RendererInspectorPausePrefaceGuard { fn drop(&mut self) { self.bridge.cancel_pause_preface(self.id); @@ -217,6 +275,28 @@ pub(super) struct RendererInspectorPauseOutboundRoute { impl Default for RendererInspectorPauseBridge { fn default() -> Self { + Self::new(None) + } +} + +impl RendererInspectorPauseBridge { + pub(super) fn new_interruptible( + isolate: v8::IsolateHandle, + callback: RendererInspectorInterruptCallback, + callback_data: usize, + ) -> Self { + assert_ne!( + callback_data, 0, + "Inspector interrupt route ID must be non-zero" + ); + Self::new(Some(RendererInspectorInterruptRoute { + isolate, + callback, + callback_data, + })) + } + + fn new(interrupt_route: Option) -> Self { Self { shared: Arc::new(RendererInspectorPauseBridgeShared { state: Mutex::new(RendererInspectorPauseBridgeState { @@ -226,6 +306,7 @@ impl Default for RendererInspectorPauseBridge { quit_requested: false, session_detach_arms: 0, target_closed: false, + active_first_dispatch_lanes: HashMap::new(), commands: VecDeque::new(), pending_prefaces: VecDeque::new(), paused_sessions_awaiting_resumed: HashSet::new(), @@ -234,6 +315,7 @@ impl Default for RendererInspectorPauseBridge { route: None, }), command_ready: Condvar::new(), + interrupt_route, }), } } @@ -248,6 +330,10 @@ impl std::fmt::Debug for RendererInspectorPauseBridge { .field("quit_requested", &state.quit_requested) .field("session_detach_arms", &state.session_detach_arms) .field("target_closed", &state.target_closed) + .field( + "active_first_dispatch_lanes", + &state.active_first_dispatch_lanes, + ) .field("pending_commands", &state.commands.len()) .field("pending_prefaces", &state.pending_prefaces.len()) .field( @@ -267,6 +353,13 @@ impl std::fmt::Debug for RendererInspectorPauseBridge { } impl RendererInspectorPauseBridge { + pub(super) fn interrupt_route_id(&self) -> Option { + self.shared + .interrupt_route + .as_ref() + .map(|route| route.callback_data) + } + pub(super) fn outbound_route( &self, agent_token: RendererDevToolsAgentToken, @@ -304,7 +397,11 @@ impl RendererInspectorPauseBridge { } let call_id = command.response.call_id(); let causal_identity = RendererRuntimeCommandCausalIdentity::new( - command.inspector_session_id.clone(), + command + .metadata + .session() + .wire_session_id() + .map(str::to_owned), call_id, ); let mut state = self.shared.state.lock(); @@ -439,7 +536,9 @@ impl RendererInspectorPauseBridge { pub(crate) fn enqueue_command( &self, + agent_token: RendererDevToolsAgentToken, inspector_session_id: Option, + route: RendererInspectorCommandRoute, owner_context_resolution_action: Option, raw_json: String, response: RendererRuntimeInspectorResponseSender, @@ -451,9 +550,11 @@ impl RendererInspectorPauseBridge { .next_command_id .checked_add(1) .expect("runtime inspector pause command ID overflow"); + let metadata = RendererInspectorCommandMetadata::new(inspector_session_id, route); let command = RendererInspectorPauseCommand { command_id, - inspector_session_id, + agent_token, + metadata: metadata.clone(), owner_context_resolution_action, raw_json, response, @@ -464,6 +565,7 @@ impl RendererInspectorPauseBridge { fail_pause_command(command, "Inspector target closed while paused"); return RendererRuntimeInspectorCommandRoute { command_id, + metadata, requires_owner_fallback: false, claim_rx: Some(claim_rx), bridge: self.clone(), @@ -474,18 +576,32 @@ impl RendererInspectorPauseBridge { if pause_active { self.shared.command_ready.notify_one(); } + drop(state); + if route == RendererInspectorCommandRoute::Io { + self.request_io_interrupt(); + } RendererRuntimeInspectorCommandRoute { command_id, - // Queue the owner task even while V8 is paused. The nested pause loop - // normally claims the command first, making that task a no-op. If a - // preceding Debugger.resume exits the loop first, the owner task is - // already waiting and can dispatch the still-unclaimed command. + metadata, + // Every route keeps an owner fallback. IO can win through a V8 + // interrupt, while both Chromium transport routes can run in the + // nested debugger loop. If V8 unwinds first, the already-queued + // owner turn can dispatch the still-unclaimed command. requires_owner_fallback: true, claim_rx: Some(claim_rx), bridge: self.clone(), } } + fn request_io_interrupt(&self) { + let Some(route) = self.shared.interrupt_route.as_ref() else { + return; + }; + let _ = route + .isolate + .request_interrupt(route.callback, route.callback_data as *mut c_void); + } + pub(crate) fn claim_command_for_owner( &self, command_id: u64, @@ -498,10 +614,27 @@ impl RendererInspectorPauseBridge { .commands .iter() .position(|command| command.command_id == command_id)?; + let lane = state.commands[position].first_dispatch_lane(); + if state.active_first_dispatch_lanes.contains_key(&lane) + || state + .commands + .iter() + .take(position) + .any(|command| command.first_dispatch_lane() == lane) + { + return None; + } let command = state .commands .remove(position) .expect("queued runtime inspector command position should remain valid"); + assert!( + state + .active_first_dispatch_lanes + .insert(lane, command.command_id) + .is_none(), + "a first-dispatch lane must have at most one active command" + ); Some(claim_pause_command( command, RendererRuntimeInspectorCommandClaim::Owner, @@ -540,16 +673,98 @@ impl RendererInspectorPauseBridge { if state.target_closed || state.quit_requested || state.session_detach_arms != 0 { return None; } - if let Some(command) = state.commands.pop_front() { + if let Some(position) = state.commands.iter().position(|command| { + !state + .active_first_dispatch_lanes + .contains_key(&command.first_dispatch_lane()) + }) { + let command = state + .commands + .remove(position) + .expect("queued Inspector command position must remain valid"); + let lane = command.first_dispatch_lane(); + assert!( + state + .active_first_dispatch_lanes + .insert(lane, command.command_id) + .is_none(), + "a first-dispatch lane must have at most one active command" + ); return Some(claim_pause_command( command, - RendererRuntimeInspectorCommandClaim::Pause, + RendererRuntimeInspectorCommandClaim::Inspector, )); } self.shared.command_ready.wait(&mut state); } } + pub(super) fn claim_io_command_for_interrupt(&self) -> Option { + let mut state = self.shared.state.lock(); + if state.target_closed { + return None; + } + let position = state.commands.iter().position(|command| { + command.metadata.route() == RendererInspectorCommandRoute::Io + && !state + .active_first_dispatch_lanes + .contains_key(&command.first_dispatch_lane()) + })?; + let command = state + .commands + .remove(position) + .expect("queued IO Inspector command position must remain valid"); + let lane = command.first_dispatch_lane(); + assert!( + state + .active_first_dispatch_lanes + .insert(lane, command.command_id) + .is_none(), + "a first-dispatch lane must have at most one active command" + ); + Some(claim_pause_command( + command, + RendererRuntimeInspectorCommandClaim::Inspector, + )) + } + + pub(crate) fn first_dispatch_guard( + &self, + command: &RendererInspectorPauseCommand, + ) -> RendererInspectorFirstDispatchGuard { + let lane = command.first_dispatch_lane(); + let state = self.shared.state.lock(); + assert_eq!( + state.active_first_dispatch_lanes.get(&lane), + Some(&command.command_id), + "a claimed Inspector command must own its first-dispatch lane" + ); + drop(state); + RendererInspectorFirstDispatchGuard { + bridge: self.clone(), + active: Some((lane, command.command_id)), + } + } + + fn finish_first_dispatch(&self, lane: RendererInspectorFirstDispatchLane, command_id: u64) { + let has_waiting_io = { + let mut state = self.shared.state.lock(); + assert_eq!( + state.active_first_dispatch_lanes.remove(&lane), + Some(command_id), + "only the active Inspector command may release its first-dispatch lane" + ); + self.shared.command_ready.notify_all(); + state + .commands + .iter() + .any(|command| command.metadata.route() == RendererInspectorCommandRoute::Io) + }; + if has_waiting_io { + self.request_io_interrupt(); + } + } + pub(crate) fn request_quit(&self) { let mut state = self.shared.state.lock(); if state.phase != RendererInspectorPausePhase::Running { @@ -564,7 +779,7 @@ impl RendererInspectorPauseBridge { state.quit_requested = false; // Commands that lost the nested-loop race to Debugger.resume stay // queued. Every live route has an owner fallback queued before it is - // exposed to the caller, so normal owner dispatch claims them after V8 + // exposed to the caller, so owner dispatch claims them after V8 // unwinds instead of reporting a synthetic cancellation. } @@ -627,7 +842,7 @@ impl RendererInspectorPauseBridge { ) { let route = { let state = self.shared.state.lock(); - if state.target_closed || state.phase == RendererInspectorPausePhase::Running { + if state.target_closed { return; } state.route.clone() @@ -644,7 +859,7 @@ impl RendererInspectorPauseBridge { ) .resolve() .unwrap_or_else(|_| { - panic!("Inspector pause state update must have resolved source identity") + panic!("Inspector IO state update must have resolved source identity") }), ); } @@ -899,17 +1114,26 @@ mod tests { let (response, _response_rx) = response_sender(41); let command_route = bridge.enqueue_command( + RendererDevToolsAgentToken::allocate(), None, + RendererInspectorCommandRoute::MainThread, None, r#"{"id":41,"method":"Debugger.stepOut","params":{}}"#.to_owned(), response, ); + assert_eq!( + command_route.metadata().route(), + RendererInspectorCommandRoute::MainThread + ); let command = bridge .wait_for_command() .expect("the nested pause loop should claim stepOut"); + let first_dispatch = bridge.first_dispatch_guard(&command); + assert_eq!(command.metadata, command_route.metadata().clone()); let dispatch = bridge.begin_command_dispatch(&command); outbound.mark_command_response(41, true); drop(dispatch); + drop(first_dispatch); bridge.leave_pause(); let resumed = expect_immediate_command_output( outbound.route_notification(&json!({"method": "Debugger.resumed", "params": {}})), @@ -953,7 +1177,9 @@ mod tests { let (response, _response_rx) = response_sender(43); let command_route = bridge.enqueue_command( + RendererDevToolsAgentToken::allocate(), None, + RendererInspectorCommandRoute::MainThread, None, r#"{"id":43,"method":"Debugger.stepOut","params":{}}"#.to_owned(), response, @@ -961,9 +1187,11 @@ mod tests { let command = bridge .wait_for_command() .expect("the nested pause loop should claim stepOut"); + let first_dispatch = bridge.first_dispatch_guard(&command); let dispatch = bridge.begin_command_dispatch(&command); outbound.mark_command_response(43, true); drop(dispatch); + drop(first_dispatch); bridge.leave_pause(); assert!( expect_immediate_command_output( @@ -1000,7 +1228,9 @@ mod tests { let (response, _response_rx) = response_sender(42); let command_route = bridge.enqueue_command( + RendererDevToolsAgentToken::allocate(), None, + RendererInspectorCommandRoute::MainThread, None, r#"{"id":42,"method":"Debugger.stepOut","params":{}}"#.to_owned(), response, @@ -1008,9 +1238,11 @@ mod tests { let command = bridge .wait_for_command() .expect("the nested pause loop should claim stepOut"); + let first_dispatch = bridge.first_dispatch_guard(&command); let dispatch = bridge.begin_command_dispatch(&command); outbound.mark_command_response(42, false); drop(dispatch); + drop(first_dispatch); assert!( bridge @@ -1076,55 +1308,252 @@ mod tests { } #[tokio::test] - async fn command_queued_while_running_can_be_claimed_by_a_racing_pause() { + async fn pause_loop_accepts_main_while_io_can_overtake_on_its_own_lane() { let bridge = RendererInspectorPauseBridge::default(); configure_page(&bridge, PageId::new_for_testing(1)); - let (response, response_rx) = response_sender(7); - let route = bridge.enqueue_command( + let agent_token = RendererDevToolsAgentToken::allocate(); + let (main_response, main_response_rx) = response_sender(7); + let main_route = bridge.enqueue_command( + agent_token, None, + RendererInspectorCommandRoute::MainThread, Some("evaluate".to_owned()), r#"{"id":7,"method":"Runtime.evaluate","params":{"expression":"42"}}"#.to_owned(), - response, + main_response, + ); + let (io_response, io_response_rx) = response_sender(8); + let io_route = bridge.enqueue_command( + agent_token, + None, + RendererInspectorCommandRoute::Io, + None, + r#"{"id":8,"method":"Runtime.terminateExecution"}"#.to_owned(), + io_response, + ); + + assert_eq!( + main_route.metadata().session(), + &DevToolsSessionKey::Primary + ); + assert_eq!( + main_route.metadata().route(), + RendererInspectorCommandRoute::MainThread ); assert!( - route.requires_owner_fallback(), + main_route.requires_owner_fallback(), "the command should initially schedule an owner fallback" ); - let command_id = route.command_id(); assert!(expect_immediate_preface(route_paused(&bridge)).is_empty()); assert!(bridge.enter_pause()); - let command = bridge - .wait_for_command() - .expect("the pause loop should claim the pre-pause command"); + let io_command = bridge + .claim_io_command_for_interrupt() + .expect("the IO callback may overtake an earlier independent Main command"); + let io_first_dispatch = bridge.first_dispatch_guard(&io_command); + assert_eq!(io_command.metadata, io_route.metadata().clone()); assert_eq!( - command.owner_context_resolution_action.as_deref(), - Some("evaluate"), - "the owner-only context resolution metadata must stay attached to the command" + io_route.wait_for_claim().await, + Ok(RendererRuntimeInspectorCommandClaim::Inspector) ); + io_command + .response + .send(json!({"id": 8, "result": {}})) + .expect("IO response receiver should remain open"); + assert_eq!(io_response_rx.await.expect("IO response").call_id, 8); + drop(io_first_dispatch); + // A normal debugger pause pumps Chromium's main-thread DevTools + // receiver too. Main does not interrupt running JavaScript, but it is + // eligible once V8 has entered the nested message loop. + let main_command = bridge + .wait_for_command() + .expect("the nested pause loop should claim Main work"); + let main_first_dispatch = bridge.first_dispatch_guard(&main_command); + assert_eq!(main_command.metadata, main_route.metadata().clone()); assert_eq!( - route.wait_for_claim().await, - Ok(RendererRuntimeInspectorCommandClaim::Pause) + main_command.owner_context_resolution_action.as_deref(), + Some("evaluate"), + "owner context-resolution metadata must stay attached" ); - assert!( - bridge.claim_command_for_owner(command_id).is_none(), - "the delayed owner fallback must not dispatch a command already claimed by pause" + assert_eq!( + main_route.wait_for_claim().await, + Ok(RendererRuntimeInspectorCommandClaim::Inspector) ); - command + main_command .response .send(json!({"id": 7, "result": {"result": {"value": 42}}})) - .expect("test response receiver should remain open"); - assert_eq!( - response_rx.await.expect("test response").call_id, - 7, - "the claimed command must retain its original response sender" - ); + .expect("Main response receiver should remain open"); + assert_eq!(main_response_rx.await.expect("Main response").call_id, 7); + drop(main_first_dispatch); + + bridge.request_quit(); + assert!(bridge.wait_for_command().is_none()); bridge.leave_pause(); } + #[test] + fn active_owner_io_dispatch_cannot_be_overtaken_by_its_io_successor() { + let bridge = RendererInspectorPauseBridge::default(); + let agent_token = RendererDevToolsAgentToken::allocate(); + let (first_response, _first_response_rx) = response_sender(11); + let first_route = bridge.enqueue_command( + agent_token, + None, + RendererInspectorCommandRoute::Io, + None, + r#"{"id":11,"method":"Debugger.pause"}"#.to_owned(), + first_response, + ); + let (second_response, _second_response_rx) = response_sender(12); + let second_route = bridge.enqueue_command( + agent_token, + None, + RendererInspectorCommandRoute::Io, + None, + r#"{"id":12,"method":"Runtime.terminateExecution"}"#.to_owned(), + second_response, + ); + + let first = bridge + .claim_command_for_owner(first_route.command_id()) + .expect("the first IO command should be owner-claimable while V8 is idle"); + let first_dispatch = bridge.first_dispatch_guard(&first); + assert!( + bridge.claim_io_command_for_interrupt().is_none(), + "an interrupt callback must not overtake an owner-claimed IO predecessor" + ); + + drop(first_dispatch); + let second = bridge + .claim_io_command_for_interrupt() + .expect("the IO successor should become claimable after first dispatch"); + assert_eq!(second.command_id, second_route.command_id()); + let second_dispatch = bridge.first_dispatch_guard(&second); + drop(second_dispatch); + } + + #[test] + fn first_dispatch_fifo_is_scoped_to_the_exact_agent_session_and_route() { + let bridge = RendererInspectorPauseBridge::default(); + let agent_token = RendererDevToolsAgentToken::allocate(); + let (first_response, _first_response_rx) = response_sender(21); + let first_route = bridge.enqueue_command( + agent_token, + None, + RendererInspectorCommandRoute::Io, + None, + r#"{"id":21,"method":"Debugger.pause"}"#.to_owned(), + first_response, + ); + let (second_response, _second_response_rx) = response_sender(22); + let second_route = bridge.enqueue_command( + agent_token, + None, + RendererInspectorCommandRoute::Io, + None, + r#"{"id":22,"method":"Runtime.terminateExecution"}"#.to_owned(), + second_response, + ); + let (other_response, _other_response_rx) = response_sender(23); + let other_route = bridge.enqueue_command( + agent_token, + Some("SID-other".to_owned()), + RendererInspectorCommandRoute::Io, + None, + r#"{"id":23,"method":"Debugger.pause"}"#.to_owned(), + other_response, + ); + + let first = bridge + .claim_io_command_for_interrupt() + .expect("the first primary-session IO command should be claimable"); + assert_eq!(first.command_id, first_route.command_id()); + let first_dispatch = bridge.first_dispatch_guard(&first); + + let other = bridge + .claim_io_command_for_interrupt() + .expect("a different session lane must remain independent"); + assert_eq!(other.command_id, other_route.command_id()); + let other_dispatch = bridge.first_dispatch_guard(&other); + assert!( + bridge.claim_io_command_for_interrupt().is_none(), + "the primary-session successor must not overtake its active predecessor" + ); + drop(other_dispatch); + assert!( + bridge.claim_io_command_for_interrupt().is_none(), + "releasing another session must not release the primary lane" + ); + + drop(first_dispatch); + let second = bridge + .claim_io_command_for_interrupt() + .expect("the exact-lane successor should become claimable once"); + assert_eq!(second.command_id, second_route.command_id()); + let second_dispatch = bridge.first_dispatch_guard(&second); + drop(second_dispatch); + assert!(bridge.claim_io_command_for_interrupt().is_none()); + } + #[tokio::test] - async fn unclaimed_pause_command_is_handed_to_owner_after_resume() { + async fn page_teardown_cancels_waiters_while_an_io_dispatch_is_in_flight() { + let bridge = RendererInspectorPauseBridge::default(); + let page_id = PageId::new_for_testing(1); + configure_page(&bridge, page_id); + let agent_token = RendererDevToolsAgentToken::allocate(); + let (active_response, _active_response_rx) = response_sender(31); + let active_route = bridge.enqueue_command( + agent_token, + None, + RendererInspectorCommandRoute::Io, + None, + r#"{"id":31,"method":"Debugger.pause"}"#.to_owned(), + active_response, + ); + let (waiting_response, waiting_response_rx) = response_sender(32); + let waiting_route = bridge.enqueue_command( + agent_token, + None, + RendererInspectorCommandRoute::Io, + None, + r#"{"id":32,"method":"Runtime.terminateExecution"}"#.to_owned(), + waiting_response, + ); + + let active = bridge + .claim_io_command_for_interrupt() + .expect("the first IO command should enter dispatch"); + assert_eq!(active.command_id, active_route.command_id()); + let active_dispatch = bridge.first_dispatch_guard(&active); + bridge.detach_page(page_id); + + let canceled = waiting_response_rx + .await + .expect("teardown must settle the queued IO response"); + let response = canceled + .output + .protocol_response(32) + .expect("teardown cancellation response"); + assert_eq!( + response["error"]["message"], + json!("Inspector page closed while paused") + ); + assert_eq!(waiting_route.command_id(), 2); + + drop(active_dispatch); + assert!( + bridge + .shared + .state + .lock() + .active_first_dispatch_lanes + .is_empty(), + "an in-flight callback must release its lane safely after teardown" + ); + } + + #[tokio::test] + async fn main_thread_command_is_handed_to_owner_after_resume() { let bridge = RendererInspectorPauseBridge::default(); configure_page(&bridge, PageId::new_for_testing(1)); assert!( @@ -1138,7 +1567,9 @@ mod tests { let (response, response_rx) = response_sender(9); let route = bridge.enqueue_command( + RendererDevToolsAgentToken::allocate(), None, + RendererInspectorCommandRoute::MainThread, Some("evaluate".to_owned()), r#"{"id":9,"method":"Runtime.evaluate","params":{"expression":"42"}}"#.to_owned(), response, @@ -1220,7 +1651,9 @@ mod tests { configure_page(&bridge, PageId::new_for_testing(1)); let (response, response_rx) = response_sender(8); let route = bridge.enqueue_command( + RendererDevToolsAgentToken::allocate(), None, + RendererInspectorCommandRoute::MainThread, None, r#"{"id":8,"method":"Runtime.getIsolateId"}"#.to_owned(), response,