fix(devtools): unify page inspector command ingress

This commit is contained in:
ldm0
2026-08-17 00:40:39 +08:00
committed by Donough Liu
parent 73aa8520fd
commit d0f09dbe6d
21 changed files with 2195 additions and 762 deletions
+12 -10
View File
@@ -137,10 +137,12 @@ impl Page {
inspector_session_id: Option<String>,
object_id: &str,
) -> anyhow::Result<PendingPageCommand> {
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<PendingPageCommand> {
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<PendingPageCommand> {
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,
},
),
)
}
+10 -10
View File
@@ -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<CompletedPageCommand>),
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)
+82 -36
View File
@@ -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<RuntimeContextRestoreEvent>) {
@@ -473,10 +474,36 @@ impl Page {
inspector_session_id: Option<String>,
raw_json: String,
) -> Result<PendingPageCommand> {
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<String>,
raw_json: String,
) -> Result<PendingPageCommand> {
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<String>,
route: RendererInspectorCommandRoute,
raw_json: String,
) -> Result<PendingPageCommand> {
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<String>,
raw_json: String,
deferred_response: RendererRuntimeInspectorResponseSender,
) -> Result<PendingPageCommand> {
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<String>,
route: RendererInspectorCommandRoute,
raw_json: String,
deferred_response: RendererRuntimeInspectorResponseSender,
) -> Result<PendingPageCommand> {
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<String>,
inspector_route: RendererInspectorCommandRoute,
owner_context_resolution_action: Option<String>,
raw_json: String,
deferred_response: RendererRuntimeInspectorResponseSender,
) -> Result<PendingRuntimeInspectorCommandDispatch> {
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<PendingPageCommand> {
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<PendingPageCommand> {
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<PendingPageCommand> {
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<bool> {
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<PendingPageCommand> {
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<i64>,
) -> 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<Option<RendererOutputFence>> {
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();
+8 -7
View File
@@ -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,
+39 -35
View File
@@ -372,10 +372,12 @@ impl Page {
inspector_session_id: Option<String>,
object_id: &str,
) -> Result<PendingPageCommand> {
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<String>,
object_id: &str,
) -> Result<PendingPageCommand> {
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<moli_page_types::DomScrollIntoViewRect>,
) -> Result<PendingPageCommand> {
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<String>,
object_id: &str,
) -> Result<PendingPageCommand> {
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<String>,
object_id: &str,
) -> Result<PendingPageCommand> {
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<String>,
object_id: String,
) -> Result<PendingPageCommand> {
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<super::SelectedFile>,
append: bool,
) -> Result<PendingPageCommand> {
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<PendingPageCommand> {
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<PendingPageCommand> {
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<i64>,
object_group: Option<&str>,
) -> Result<PendingPageCommand> {
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<String>,
object_id: String,
) -> Result<PendingPageCommand> {
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(
+5 -4
View File
@@ -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,
};
+16 -10
View File
@@ -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",
] {
@@ -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,<body>IO interrupt</body>",
)
.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()
+23 -7
View File
@@ -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;
}
+3 -1
View File
@@ -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,
+3 -1
View File
@@ -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,
+87 -27
View File
@@ -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<PageCommandFirstDispatchIdentity> {
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 {
@@ -166,12 +166,15 @@ impl RendererPageHandle {
pub fn enqueue_routable_runtime_inspector_command(
&self,
inspector_session_id: Option<String>,
route: RendererInspectorCommandRoute,
owner_context_resolution_action: Option<String>,
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,
@@ -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<RendererPageLocalEntry>,
page_command_first_dispatch:
PageCommandFirstDispatchResidence<DevToolsSessionKey, RenderRuntimePendingTurn>,
PageCommandFirstDispatchResidence<PageCommandFirstDispatchLane, RenderRuntimePendingTurn>,
owner_maintenance: RendererPageOwnerMaintenanceResidence,
task_sources: RendererPageOwnedTaskSources,
lifecycle_gate: Option<LifecycleGate>,
@@ -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<RenderRuntimePendingTurn> {
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<RenderRuntimePendingTurn> {
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<RenderRuntimePendingTurn> {
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<RenderRuntimePendingTurn> {
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(
@@ -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<Key, Command> {
/// 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<Key, VecDeque<Command>>,
}
@@ -37,9 +38,9 @@ impl<Key, Command> Default for PageCommandFirstDispatchResidence<Key, Command> {
}
impl<Key: Ord, Command> PageCommandFirstDispatchResidence<Key, Command> {
/// 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<Command> {
match self.lanes.entry(key) {
std::collections::btree_map::Entry::Vacant(entry) => {
@@ -53,7 +54,7 @@ impl<Key: Ord, Command> PageCommandFirstDispatchResidence<Key, Command> {
}
}
/// 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<Command> {
debug_assert!(
@@ -79,32 +80,68 @@ impl<Key: Ord, Command> PageCommandFirstDispatchResidence<Key, Command> {
#[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"
);
}
}
+232 -255
View File
@@ -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<RendererPageReply> {
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<std::time::Instant>,
@@ -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 { .. }
)
}
+73 -13
View File
@@ -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<Vec<RendererRuntimeInspectorMessage>> {
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<Vec<RendererRuntimeInspectorMessage>> {
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<Vec<RendererRuntimeInspectorMessage>> {
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(
+684 -173
View File
@@ -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<String>, 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<String>,
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<RendererInspectorSessionRestoreSnapshot>,
isolated_worlds: Vec<crate::protocol_types::RuntimeIsolatedWorldDefinition>,
stored_runtime_bindings: Vec<crate::protocol_types::RuntimeBindingRegistration>,
session_runtime_bindings: Vec<crate::protocol_types::RuntimeBindingRegistration>,
},
DetachRuntimeInspectorSession {
pause_guard: RendererRuntimeInspectorSessionDetachGuard,
},
AddRuntimeBinding {
name: String,
execution_context_name: Option<String>,
execution_context_id: Option<i64>,
},
DomDebuggerGetEventListeners {
object_id: String,
depth: i32,
pierce: bool,
},
ComputedStylePropertiesForObjectId {
object_id: String,
},
ScrollObjectNodeIntoViewIfNeeded {
object_id: String,
rect: Option<moli_page_types::DomScrollIntoViewRect>,
},
ClientRectForObjectId {
object_id: String,
},
DocumentGeometryForObjectId {
object_id: String,
},
NodeHasGeometryForObjectId {
object_id: String,
},
FocusDocumentNodeForObjectId {
object_id: String,
},
SetFileInputFilesForObjectId {
object_id: String,
files: Vec<crate::dom::native::SelectedFile>,
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<i64>,
object_group: Option<String>,
},
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<String>,
raw_json: String,
},
DispatchRuntimeProtocolMessageWithDeferredResponse {
inspector_session_id: Option<String>,
raw_json: String,
deferred_response: RendererRuntimeInspectorResponseSender,
},
DispatchQueuedRuntimeInspectorCommand {
command_id: u64,
inspector_session_id: Option<String>,
},
DispatchRuntimeProtocolMessageWithContextResolution {
inspector_session_id: Option<String>,
action: String,
raw_json: String,
},
DispatchRuntimeProtocolMessageWithContextResolutionAndDeferredResponse {
inspector_session_id: Option<String>,
action: String,
raw_json: String,
deferred_response: RendererRuntimeInspectorResponseSender,
},
DomDebuggerGetEventListeners {
inspector_session_id: Option<String>,
object_id: String,
depth: i32,
pierce: bool,
},
DomDebuggerConfigureEventListenerBreakpoint {
inspector_session_id: Option<String>,
breakpoint: RendererDomDebuggerEventListenerBreakpoint,
@@ -3662,26 +3891,6 @@ pub enum RendererPageCommand {
breakpoint_type: String,
enabled: bool,
},
RuntimeEnableEvents {
inspector_session_id: Option<String>,
},
ApplyRuntimeProtocolState {
inspector_session_id: Option<String>,
session_restore_snapshots: Vec<RendererInspectorSessionRestoreSnapshot>,
isolated_worlds: Vec<crate::protocol_types::RuntimeIsolatedWorldDefinition>,
stored_runtime_bindings: Vec<crate::protocol_types::RuntimeBindingRegistration>,
session_runtime_bindings: Vec<crate::protocol_types::RuntimeBindingRegistration>,
},
DetachRuntimeInspectorSession {
inspector_session_id: Option<String>,
pause_guard: RendererRuntimeInspectorSessionDetachGuard,
},
AddRuntimeBinding {
inspector_session_id: Option<String>,
name: String,
execution_context_name: Option<String>,
execution_context_id: Option<i64>,
},
CreateIsolatedWorld {
name: String,
grant_universal_access: bool,
@@ -3741,35 +3950,18 @@ pub enum RendererPageCommand {
ComputedStylePropertiesForBackendNodeId {
backend_node_id: u32,
},
ComputedStylePropertiesForObjectId {
inspector_session_id: Option<String>,
object_id: String,
},
SetInlineStyleSheetTextForStyleSheetId {
inspector_session_id: Option<String>,
style_sheet_id: String,
text: String,
},
ScrollObjectNodeIntoViewIfNeeded {
inspector_session_id: Option<String>,
object_id: String,
rect: Option<moli_page_types::DomScrollIntoViewRect>,
},
ScrollBackendNodeIntoViewIfNeeded {
backend_node_id: u32,
rect: Option<moli_page_types::DomScrollIntoViewRect>,
},
ClientRectForObjectId {
inspector_session_id: Option<String>,
object_id: String,
},
ClientRectForBackendNodeId {
backend_node_id: u32,
},
DocumentGeometryForObjectId {
inspector_session_id: Option<String>,
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<String>,
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<String>,
object_id: String,
},
TriggerAutofill(RendererAutofillTriggerRequest),
ResetNavigationHistory,
SetFileInputFilesForBackendNodeId {
@@ -3812,19 +3996,6 @@ pub enum RendererPageCommand {
files: Vec<crate::dom::native::SelectedFile>,
append: bool,
},
SetFileInputFilesForObjectId {
inspector_session_id: Option<String>,
object_id: String,
files: Vec<crate::dom::native::SelectedFile>,
append: bool,
},
DocumentNodeSnapshotForObjectId {
inspector_session_id: Option<String>,
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<String>,
object_id: String,
},
AccessibilityNodeAndAncestorPayloadsForObjectId {
inspector_session_id: Option<String>,
object_id: String,
},
AccessibilityPartialTreePayloadsForObjectId {
inspector_session_id: Option<String>,
object_id: String,
fetch_relatives: bool,
},
StyleSheetPayloadForStyleSheetId {
inspector_session_id: Option<String>,
style_sheet_id: String,
@@ -3990,11 +4148,6 @@ pub enum RendererPageCommand {
OuterHtmlForDocument {
include_shadow_dom: bool,
},
OuterHtmlForObjectId {
inspector_session_id: Option<String>,
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<String>,
backend_node_id: u32,
execution_context_id: Option<i64>,
object_group: Option<String>,
},
ResolveBlobObject {
inspector_session_id: Option<String>,
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<DevToolsSessionKey> {
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<String>,
route: RendererInspectorCommandRoute,
command: RendererInspectorPageCommand,
) -> Self {
Self::Inspector(RendererInspectorCommandEnvelope::new(
inspector_session_id,
route,
command,
))
}
pub fn dispatch_runtime_protocol_message(
inspector_session_id: Option<String>,
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<String>,
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<String>,
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<String>,
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<String>) -> Self {
Self::inspector_command(
inspector_session_id,
RendererInspectorCommandRoute::MainThread,
RendererInspectorPageCommand::RuntimeEnableEvents,
)
}
pub fn apply_runtime_protocol_state(
inspector_session_id: Option<String>,
session_restore_snapshots: Vec<RendererInspectorSessionRestoreSnapshot>,
isolated_worlds: Vec<crate::protocol_types::RuntimeIsolatedWorldDefinition>,
stored_runtime_bindings: Vec<crate::protocol_types::RuntimeBindingRegistration>,
session_runtime_bindings: Vec<crate::protocol_types::RuntimeBindingRegistration>,
) -> 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<String>,
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<String>,
name: String,
execution_context_name: Option<String>,
execution_context_id: Option<i64>,
) -> 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<String>,
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<String>,
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<String>,
object_id: String,
rect: Option<moli_page_types::DomScrollIntoViewRect>,
) -> 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<String>,
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<String>,
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<String>,
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<String>,
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<String>,
object_id: String,
files: Vec<crate::dom::native::SelectedFile>,
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<String>,
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<String>,
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<String>,
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<String>,
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<String>,
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<String>,
backend_node_id: u32,
execution_context_id: Option<i64>,
object_group: Option<String>,
) -> 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<String>, 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,
+71 -65
View File
@@ -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<serde_json::Value>, 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<Vec<serde_json::Value>> {
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<i64>,
) -> 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(()),
@@ -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<HashMap<usize, Weak<RendererInspectorPauseLoopLocal>>> =
RefCell::new(HashMap::new());
}
static NEXT_INTERRUPT_ROUTE_ID: AtomicUsize = AtomicUsize::new(1);
struct RendererInspectorClient {
isolate: UnsafeCell<v8::UnsafeRawIsolatePtr>,
context_registry: DocumentInspectorContextRegistry,
@@ -35,13 +42,29 @@ struct RendererInspectorPauseSession {
pub(super) struct RendererInspectorPauseSessionRegistration {
pause_loop: Weak<RendererInspectorPauseLoopLocal>,
context_group_id: i32,
agent_token: RendererDevToolsAgentToken,
session_key: DevToolsSessionKey,
session: Weak<v8::inspector::V8InspectorSession>,
}
struct RendererInspectorPauseLoopLocal {
bridge: RendererInspectorPauseBridge,
interrupt_route_id: Option<usize>,
sessions: RefCell<HashMap<(i32, DevToolsSessionKey), RendererInspectorPauseSession>>,
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<Self> {
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<RendererInspectorPauseSession>,
) {
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,
+476 -43
View File
@@ -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<String>,
pub(crate) agent_token: RendererDevToolsAgentToken,
pub(crate) metadata: RendererInspectorCommandMetadata,
pub(crate) owner_context_resolution_action: Option<String>,
pub(crate) raw_json: String,
pub(crate) response: RendererRuntimeInspectorResponseSender,
claim_tx: Option<tokio::sync::oneshot::Sender<RendererRuntimeInspectorCommandClaim>>,
}
#[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<tokio::sync::oneshot::Receiver<RendererRuntimeInspectorCommandClaim>>,
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<RendererInspectorPauseBridgeState>,
command_ready: Condvar,
interrupt_route: Option<RendererInspectorInterruptRoute>,
}
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<RendererInspectorFirstDispatchLane, u64>,
commands: VecDeque<RendererInspectorPauseCommand>,
pending_prefaces: VecDeque<RendererInspectorPausePreface>,
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<RendererInspectorInterruptRoute>) -> 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<usize> {
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<String>,
route: RendererInspectorCommandRoute,
owner_context_resolution_action: Option<String>,
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<RendererInspectorPauseCommand> {
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,