diff --git a/moli-core/src/browser/mod.rs b/moli-core/src/browser/mod.rs index 7bdbe01d53..52c18b2523 100644 --- a/moli-core/src/browser/mod.rs +++ b/moli-core/src/browser/mod.rs @@ -92,6 +92,31 @@ define_browser_identity!( "Identity of one browser-owned request decision, independent of protocol request IDs." ); +static NEXT_BROWSER_SEQUENCE: AtomicU64 = AtomicU64::new(1); + +/// Process-monotonic sequence of completed Browser semantic occurrences. +/// +/// Unlike the object identities above, this value defines publication order. +/// It is allocated only after the Browser mutation represented by an +/// occurrence has completed successfully. +#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] +pub struct BrowserSequence(NonZeroU64); + +impl BrowserSequence { + /// Allocates the next process-wide Browser occurrence sequence. + pub fn allocate() -> Self { + Self(allocate_nonzero_u64( + &NEXT_BROWSER_SEQUENCE, + "Browser sequence", + )) + } + + /// Returns the ordered value for diagnostics and projection fences. + pub const fn get(self) -> u64 { + self.0.get() + } +} + #[cfg(any(test, feature = "test-support"))] impl DocumentId { /// Constructs a deterministic identity for cross-crate tests. @@ -123,6 +148,7 @@ mod tests { assert_ne!(DocumentId::allocate(), DocumentId::allocate()); assert_ne!(NavigationId::allocate(), NavigationId::allocate()); assert_ne!(BrowserRequestId::allocate(), BrowserRequestId::allocate()); + assert!(BrowserSequence::allocate() < BrowserSequence::allocate()); } #[test] @@ -145,5 +171,9 @@ mod tests { size_of::>(), size_of::() ); + assert_eq!( + size_of::>(), + size_of::() + ); } } diff --git a/moli-core/src/page/command_dispatch.rs b/moli-core/src/page/command_dispatch.rs index 4692c19541..6f8991affb 100644 --- a/moli-core/src/page/command_dispatch.rs +++ b/moli-core/src/page/command_dispatch.rs @@ -1,8 +1,8 @@ use super::{ - Page, RendererAgentAttachmentId, RendererCommandTurnOutput, RendererPageCommand, - RendererPageCommandPending, RendererPageReply, RendererRuntimeInspectorIoCommandClaim, - RendererRuntimeInspectorIoCommandRoute, RendererRuntimeInspectorMainCommandCompletion, - RendererRuntimeInspectorMainCommandRoute, + Page, RendererAgentAttachmentId, RendererCommandTurnOutput, RendererInspectorCommandRoute, + RendererPageCommand, RendererPageCommandPending, RendererPageReply, + RendererRuntimeInspectorIoCommandClaim, RendererRuntimeInspectorIoCommandRoute, + RendererRuntimeInspectorMainCommandCompletion, RendererRuntimeInspectorMainCommandRoute, }; use crate::RendererOutputFence; use anyhow::{Result, bail}; @@ -43,6 +43,26 @@ impl PendingRuntimeInspectorCommandDispatch { ), } } + + pub fn renderer_route(&self) -> RendererInspectorCommandRoute { + match &self.kind { + PendingRuntimeInspectorCommandDispatchKind::MainIngress(_) => { + RendererInspectorCommandRoute::MainThread + } + PendingRuntimeInspectorCommandDispatchKind::Io(_) => RendererInspectorCommandRoute::Io, + } + } + + pub fn renderer_agent_attachment_id(&self) -> Option { + match &self.kind { + PendingRuntimeInspectorCommandDispatchKind::MainIngress(route) => { + route.ticket().attachment() + } + PendingRuntimeInspectorCommandDispatchKind::Io(pending) => { + pending.renderer_agent_attachment_id() + } + } + } } /// One non-V8 renderer agent command admitted through the Page IO @@ -486,6 +506,10 @@ impl PendingDevToolsIoCommandDispatch { Self { route } } + pub fn renderer_agent_attachment_id(&self) -> Option { + self.route.ticket().attachment() + } + pub async fn wait(self) -> Result { match self .route diff --git a/moli-protocol-cdp/src/lib.rs b/moli-protocol-cdp/src/lib.rs index 5a91f73633..8ce3d06825 100644 --- a/moli-protocol-cdp/src/lib.rs +++ b/moli-protocol-cdp/src/lib.rs @@ -21,5 +21,5 @@ pub use target::{CdpTargetKindWire, CdpTargetType, cdp_target_type_wire_value}; pub use time::{DEFAULT_LOADER_ID, monotonic_timestamp_seconds}; pub use wire::{ CdpInspectorTaskMode, CdpRendererCommandPolicy, CdpRendererCommandReplacement, - CdpRendererCommandReplayDispatch, CdpRendererDispatchLane, CdpRequest, ParsedCdpCommand, + CdpRendererCommandReplayDispatch, CdpRequest, ParsedCdpCommand, }; diff --git a/moli-protocol-cdp/src/wire.rs b/moli-protocol-cdp/src/wire.rs index 7490e0d2b4..6df245fa5e 100644 --- a/moli-protocol-cdp/src/wire.rs +++ b/moli-protocol-cdp/src/wire.rs @@ -155,21 +155,6 @@ impl CdpRequest { } } -/// Renderer lane selected after a command falls through its DevTools handler. -/// -/// Chromium physically separates renderer main-thread and IO -/// DevTools routes. Commands completed by a service or Browser handler have no -/// renderer lane at all. -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub enum CdpRendererDispatchLane { - /// The command must wait for a cross-document navigation to install its - /// replacement renderer attachment. - Main, - /// The command may address the suspended renderer in order to inspect, - /// interrupt, or release it while navigation is in flight. - Io, -} - /// V8 execution capability of a command delivered through a renderer /// DevTools IO receiver. /// @@ -217,14 +202,13 @@ pub enum CdpRendererCommandReplayDispatch { ResolveRuntimeContext, } -/// Immutable renderer scheduling policy derived once from a validated CDP -/// method at command ingress. +/// Immutable renderer behavior derived once from a validated CDP method at +/// command ingress. /// -/// Downstream dispatch and renderer-call registration copy this value instead -/// of rebuilding policy from serialized Inspector JSON. +/// This contains V8 task and replacement behavior only. Renderer routing is a +/// DevTools AgentHost decision made after the domain handler runs. #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub struct CdpRendererCommandPolicy { - renderer_lane: Option, inspector_task_mode: CdpInspectorTaskMode, renderer_replacement: CdpRendererCommandReplacement, renderer_replay_dispatch: CdpRendererCommandReplayDispatch, @@ -232,10 +216,6 @@ pub struct CdpRendererCommandPolicy { } impl CdpRendererCommandPolicy { - pub const fn renderer_lane(self) -> Option { - self.renderer_lane - } - pub const fn inspector_task_mode(self) -> CdpInspectorTaskMode { self.inspector_task_mode } @@ -380,10 +360,6 @@ impl ParsedCdpCommand { self.renderer_policy } - pub fn renderer_lane(&self) -> Option { - self.renderer_policy.renderer_lane() - } - pub fn inspector_task_mode(&self) -> CdpInspectorTaskMode { self.renderer_policy.inspector_task_mode() } @@ -399,17 +375,7 @@ impl ParsedCdpCommand { #[derive(Clone, Copy, Debug, Eq, PartialEq)] enum CdpMethodDomain { - Accessibility, - Console, - Css, Debugger, - Dom, - DomSnapshot, - Emulation, - HeapProfiler, - Page, - Performance, - Profiler, Runtime, Other, } @@ -417,17 +383,7 @@ enum CdpMethodDomain { impl CdpMethodDomain { fn parse(domain: &str) -> Self { match domain { - "Accessibility" => Self::Accessibility, - "Console" => Self::Console, - "CSS" => Self::Css, "Debugger" => Self::Debugger, - "DOM" => Self::Dom, - "DOMSnapshot" => Self::DomSnapshot, - "Emulation" => Self::Emulation, - "HeapProfiler" => Self::HeapProfiler, - "Page" => Self::Page, - "Performance" => Self::Performance, - "Profiler" => Self::Profiler, "Runtime" => Self::Runtime, _ => Self::Other, } @@ -466,96 +422,23 @@ impl RuntimeWireAction { } } -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -enum PageWireAction { - MainThread, - Crash, - Other, -} - -impl PageWireAction { - fn parse(action: &str) -> Self { - match action { - "captureScreenshot" - | "captureSnapshot" - | "createIsolatedWorld" - | "getFrameTree" - | "getResourceTree" - | "searchInResource" - | "getLayoutMetrics" - | "printToPDF" => Self::MainThread, - "crash" => Self::Crash, - _ => Self::Other, - } - } -} - #[derive(Clone, Copy, Debug, Eq, PartialEq)] enum DebuggerWireAction { - Interruptible, - IoExecutionControl, - MainThreadExecutionControl, + ExecutionControl, Other, } impl DebuggerWireAction { - fn is_interruptible(self) -> bool { - matches!(self, Self::Interruptible | Self::IoExecutionControl) - } - fn executes_page_javascript(self) -> bool { - matches!( - self, - Self::IoExecutionControl | Self::MainThreadExecutionControl - ) - } -} - -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -enum PerformanceWireAction { - GetMetrics, - Other, -} - -impl PerformanceWireAction { - fn parse(action: &str) -> Self { - match action { - "getMetrics" => Self::GetMetrics, - _ => Self::Other, - } - } -} - -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -enum EmulationWireAction { - SetScriptExecutionDisabled, - Other, -} - -impl EmulationWireAction { - fn parse(action: &str) -> Self { - match action { - "setScriptExecutionDisabled" => Self::SetScriptExecutionDisabled, - _ => Self::Other, - } + matches!(self, Self::ExecutionControl) } } impl DebuggerWireAction { fn parse(action: &str) -> Self { match action { - "getPossibleBreakpoints" - | "getScriptSource" - | "getStackTrace" - | "pause" - | "removeBreakpoint" - | "setBreakpoint" - | "setBreakpointByUrl" - | "setBreakpointsActive" => Self::Interruptible, - "resume" => Self::IoExecutionControl, - "continueToLocation" | "restartFrame" | "stepInto" | "stepOut" | "stepOver" => { - Self::MainThreadExecutionControl - } + "resume" | "continueToLocation" | "restartFrame" | "stepInto" | "stepOut" + | "stepOver" => Self::ExecutionControl, _ => Self::Other, } } @@ -565,7 +448,6 @@ impl CdpRendererCommandPolicy { fn for_method(method: &str) -> Self { let Some((domain, action)) = method.split_once('.') else { return Self { - renderer_lane: None, inspector_task_mode: CdpInspectorTaskMode::for_method(method), renderer_replacement: CdpRendererCommandReplacement::Replay, renderer_replay_dispatch: CdpRendererCommandReplayDispatch::Direct, @@ -577,53 +459,7 @@ impl CdpRendererCommandPolicy { (domain == CdpMethodDomain::Runtime).then(|| RuntimeWireAction::parse(action)); let debugger_action = (domain == CdpMethodDomain::Debugger).then(|| DebuggerWireAction::parse(action)); - let renderer_lane = match domain { - CdpMethodDomain::Runtime => { - if runtime_action == Some(RuntimeWireAction::TerminateExecution) { - Some(CdpRendererDispatchLane::Io) - } else { - Some(CdpRendererDispatchLane::Main) - } - } - CdpMethodDomain::Debugger => { - if debugger_action.is_some_and(DebuggerWireAction::is_interruptible) { - Some(CdpRendererDispatchLane::Io) - } else { - Some(CdpRendererDispatchLane::Main) - } - } - CdpMethodDomain::Performance => { - if PerformanceWireAction::parse(action) == PerformanceWireAction::GetMetrics { - Some(CdpRendererDispatchLane::Io) - } else { - Some(CdpRendererDispatchLane::Main) - } - } - CdpMethodDomain::Emulation => { - if EmulationWireAction::parse(action) - == EmulationWireAction::SetScriptExecutionDisabled - { - Some(CdpRendererDispatchLane::Io) - } else { - None - } - } - CdpMethodDomain::Page => match PageWireAction::parse(action) { - PageWireAction::MainThread => Some(CdpRendererDispatchLane::Main), - PageWireAction::Crash => Some(CdpRendererDispatchLane::Io), - PageWireAction::Other => None, - }, - CdpMethodDomain::Accessibility - | CdpMethodDomain::Console - | CdpMethodDomain::Css - | CdpMethodDomain::Dom - | CdpMethodDomain::DomSnapshot - | CdpMethodDomain::HeapProfiler - | CdpMethodDomain::Profiler => Some(CdpRendererDispatchLane::Main), - CdpMethodDomain::Other => None, - }; Self { - renderer_lane, inspector_task_mode: CdpInspectorTaskMode::for_method(method), renderer_replacement: if runtime_action.is_some_and(|action| { matches!( @@ -669,76 +505,6 @@ mod tests { assert_eq!(command.session_id(), Some("s1")); assert_eq!(command.command_output_session_id(), Some("s1")); assert!(command.runtime_command_executes_page_javascript()); - assert_eq!(command.renderer_lane(), Some(CdpRendererDispatchLane::Main)); - } - - #[test] - fn parsed_cdp_command_keeps_navigation_control_commands_unblocked() { - let command = parse( - r#"{"id":9,"method":"Fetch.continueResponse","params":{"requestId":"r"},"sessionId":"s1"}"#, - ); - - assert_eq!(command.method(), "Fetch.continueResponse"); - assert_eq!(command.session_id(), Some("s1")); - assert_eq!(command.renderer_lane(), None); - } - - #[test] - fn renderer_inspector_control_commands_wait_for_navigation_resume() { - for method in [ - "Runtime.getIsolateId", - "Console.clearMessages", - "Profiler.enable", - "HeapProfiler.enable", - ] { - let command = parse(format!(r#"{{"id":11,"method":"{method}"}}"#)); - assert!( - command.renderer_lane() == Some(CdpRendererDispatchLane::Main), - "{method} must not bind to the suspended attachment" - ); - } - } - - #[test] - fn renderer_navigation_suspension_matches_chromium_io_route_exceptions() { - for method in [ - "Debugger.getPossibleBreakpoints", - "Debugger.getScriptSource", - "Debugger.getStackTrace", - "Debugger.pause", - "Debugger.removeBreakpoint", - "Debugger.resume", - "Debugger.setBreakpoint", - "Debugger.setBreakpointByUrl", - "Debugger.setBreakpointsActive", - "Emulation.setScriptExecutionDisabled", - "Page.crash", - "Performance.getMetrics", - "Runtime.terminateExecution", - ] { - let command = parse(format!(r#"{{"id":12,"method":"{method}"}}"#)); - assert!( - command.renderer_lane() == Some(CdpRendererDispatchLane::Io), - "{method} must remain dispatchable while the renderer attachment is suspended" - ); - } - - for method in [ - "Debugger.continueToLocation", - "Debugger.enable", - "Debugger.restartFrame", - "Debugger.stepInto", - "Debugger.stepOut", - "Debugger.stepOver", - "Performance.enable", - "Runtime.getIsolateId", - ] { - let command = parse(format!(r#"{{"id":13,"method":"{method}"}}"#)); - assert!( - command.renderer_lane() == Some(CdpRendererDispatchLane::Main), - "{method} must bind to the replacement renderer attachment" - ); - } } #[test] @@ -836,15 +602,6 @@ mod tests { ); } - #[test] - fn page_resource_search_waits_for_document_navigation() { - let command = parse( - r#"{"id":10,"method":"Page.searchInResource","params":{"frameId":"F","url":"https://example.test/","query":"needle"},"sessionId":"s1"}"#, - ); - - assert_eq!(command.renderer_lane(), Some(CdpRendererDispatchLane::Main)); - } - #[test] fn parsed_command_rejects_invalid_json_instead_of_storing_parse_failure() { let error = ParsedCdpCommand::parse_str("{") @@ -908,7 +665,6 @@ mod tests { assert_eq!(command.request().id(), 14); assert_eq!(command.session_id(), Some("s2")); - assert_eq!(command.renderer_lane(), Some(CdpRendererDispatchLane::Io)); assert!(!command.runtime_command_executes_page_javascript()); } diff --git a/moli-protocol-server/src/cdp_scheduler.rs b/moli-protocol-server/src/cdp_scheduler.rs index 4d263eb8ed..b7a15a2107 100644 --- a/moli-protocol-server/src/cdp_scheduler.rs +++ b/moli-protocol-server/src/cdp_scheduler.rs @@ -11,16 +11,16 @@ use moli_core::{ runtime::NavigationRuntimeConfig, }; use moli_protocol::{ - BackgroundNavigationCompletion, BackgroundProtocolEvent, CdpCommandTaskStep, CdpConnection, - CdpInitialStoragePartition, CdpRendererDispatchLane, CdpSchedulerEvent, - CdpTargetHostLifecycleObserver, CommandDispatchContext, CompletedCdpCommandDispatch, + AgentHostDispatchResult, BackgroundNavigationCompletion, BackgroundProtocolEvent, + CdpConnection, CdpInitialStoragePartition, CdpSchedulerEvent, CdpTargetHostLifecycleObserver, + CommandDispatchContext, CompletedCdpCommandDispatch, CompletedDeferredMainDocumentLoadCompletion, DeferredMainDocumentLoadCompletionOutputAction, DeferredMainDocumentLoadCompletionOutputInterest, DeferredMainDocumentLoadObservationId, DeferredMainDocumentLoadPredecessorCandidate, DevToolsPageResidenceIdentity, PageScreencastCaptureCompletion, PageScreencastCaptureStart, PageScreencastRegistration, PageScreencastSubscriptionStatus, ParsedCdpCommand, PendingCdpCommandDispatch, PendingDeferredMainDocumentLoadCompletion, PendingPageScreencastCapture, - ProtocolSchedulerWorkKind, RuntimeCommandOutputBarriers, + ProtocolSchedulerWorkKind, RendererCommandResponseOrder, conn::{RuntimeInspectorResponseReady, RuntimeInspectorResponseReadySender}, devtools_runtime::{ DevToolsCommand, DevToolsCommandResult, DevToolsError, DevToolsNavigationWait, @@ -39,7 +39,7 @@ mod adapter_scheduler; mod command_dispatch; mod frontend_control; mod protocol_residence; -mod runtime_command_barrier; +mod renderer_command_response_order; mod runtime_dispatch; pub(crate) use actor::spawn_cdp_scheduler_actor; @@ -51,7 +51,7 @@ pub(crate) use frontend_control::{CdpCookieSnapshot, CdpOwnerActorLifecycle}; use protocol_residence::{ ClientTurnPredecessor, ProtocolSchedulerResidence, ProtocolSchedulerStep, SchedulerQueues, }; -use runtime_command_barrier::CommandOutputReleasePermit; +use renderer_command_response_order::CommandOutputReleasePermit; pub(crate) use runtime_dispatch::{ DevToolsRuntimeCommandProgress, PendingDevToolsRuntimeDeferredReplyExecution, }; @@ -113,7 +113,7 @@ pub(crate) enum CommandStartAction { pub(crate) struct CdpScheduler { conn: CdpConnection, pending_navigation_background_events: VecDeque, - runtime_command_output_barriers: RuntimeCommandOutputBarriers, + renderer_command_response_order: RendererCommandResponseOrder, queues: SchedulerQueues, page_screencasts: HashMap, PageScreencastSchedule>, page_screencast_interval_ms: u32, @@ -704,7 +704,7 @@ impl CdpScheduler { Self { conn, pending_navigation_background_events: VecDeque::new(), - runtime_command_output_barriers: RuntimeCommandOutputBarriers::default(), + renderer_command_response_order: RendererCommandResponseOrder::default(), queues: SchedulerQueues::default(), page_screencasts: HashMap::new(), page_screencast_interval_ms, @@ -844,11 +844,11 @@ impl CdpScheduler { .conn .start_parsed_command_dispatch_with_context(command, &mut command_context); // Dispatch registers a session-local renderer call id before the - // renderer task can be observed by this actor. The response barrier + // renderer task can be observed by this actor. The response-order permit // must use that exact id rather than infer one from the frontend CDP // request id. - let runtime_output_barrier = if command.runtime_command_executes_page_javascript() { - self.runtime_command_output_barriers.admit( + let renderer_response_permit = if command.runtime_command_executes_page_javascript() { + self.renderer_command_response_order.admit( &self.conn, command.request().id(), command.command_output_session_id(), @@ -857,14 +857,20 @@ impl CdpScheduler { None }; let output_release_permit = - CommandOutputReleasePermit::new(response_flush_permit, runtime_output_barrier); + CommandOutputReleasePermit::new(response_flush_permit, renderer_response_permit); let step = match dispatch_step { - CdpCommandTaskStep::Pending(mut pending) => { + AgentHostDispatchResult::PendingService(mut pending) => { let scheduler_events = pending.take_scheduler_events(); self.apply_scheduler_events(scheduler_events); CommandTaskStep::Pending(pending) } - CdpCommandTaskStep::Complete(result) => { + AgentHostDispatchResult::FallThrough(dispatch) => { + let mut pending = dispatch.into_pending(); + let scheduler_events = pending.take_scheduler_events(); + self.apply_scheduler_events(scheduler_events); + CommandTaskStep::Pending(pending) + } + AgentHostDispatchResult::Complete(result) => { let ( events, post_renderer_output_events, @@ -905,12 +911,18 @@ impl CdpScheduler { .complete_pending_command_dispatch_with_context(completed, command_context) .await { - CdpCommandTaskStep::Pending(mut pending) => { + AgentHostDispatchResult::PendingService(mut pending) => { let scheduler_events = pending.take_scheduler_events(); self.apply_scheduler_events(scheduler_events); CommandTaskStep::Pending(pending) } - CdpCommandTaskStep::Complete(result) => { + AgentHostDispatchResult::FallThrough(dispatch) => { + let mut pending = dispatch.into_pending(); + let scheduler_events = pending.take_scheduler_events(); + self.apply_scheduler_events(scheduler_events); + CommandTaskStep::Pending(pending) + } + AgentHostDispatchResult::Complete(result) => { let ( events, post_renderer_output_events, @@ -2032,10 +2044,7 @@ impl CdpScheduler { } pub(crate) fn command_waits_for_navigation_flush(&self, command: &ParsedCdpCommand) -> bool { - command.renderer_lane() == Some(CdpRendererDispatchLane::Main) - && self - .conn - .renderer_document_navigation_is_suspended_for_session_owner(command.session_id()) + self.conn.command_waits_for_document_projection(command) } pub(crate) fn route_background_event_around_inflight_navigation( @@ -2327,7 +2336,7 @@ impl CdpScheduler { .conn .ingest_renderer_output_turn_async( publication, - &mut self.runtime_command_output_barriers, + &mut self.renderer_command_response_order, ) .await; let ( @@ -2471,20 +2480,20 @@ impl CdpScheduler { let Some(permit) = output_release_permit else { return ProtocolOutputSequence::empty(); }; - let Some(runtime_barrier) = permit.finish_response() else { + let Some(renderer_response) = permit.finish_response() else { return ProtocolOutputSequence::empty(); }; let completion = self .conn - .release_runtime_command_output_barrier_turn_async( - &mut self.runtime_command_output_barriers, - runtime_barrier, + .release_renderer_command_response_permit_turn_async( + &mut self.renderer_command_response_order, + renderer_response, ) .await; if moli_trace::cdp_runtime_trace_enabled() { tracing::info!( target: "moli_cdp_runtime", - stage = "runtime_command_output_barrier_terminal", + stage = "renderer_command_response_terminal", terminal = ?completion.terminal(), ); } diff --git a/moli-protocol-server/src/cdp_scheduler/command_dispatch.rs b/moli-protocol-server/src/cdp_scheduler/command_dispatch.rs index 85cf3a5e5a..de47b6a747 100644 --- a/moli-protocol-server/src/cdp_scheduler/command_dispatch.rs +++ b/moli-protocol-server/src/cdp_scheduler/command_dispatch.rs @@ -136,7 +136,7 @@ impl CommandTurnOutput { /// /// The frozen sequence has already been split by its domain-declared /// `BeforeResponse`/`AfterResponse` order. Only the former reaches this - /// method; the latter remains in the command barrier until the response + /// method; the latter remains held by response ordering until the response /// flush consumes its single-use permit. pub(crate) fn prepend_protocol_output(&mut self, output: ProtocolOutputSequence) { if output.is_empty() { diff --git a/moli-protocol-server/src/cdp_scheduler/frontend_control/target_control.rs b/moli-protocol-server/src/cdp_scheduler/frontend_control/target_control.rs index 22fe428172..f4021d8afd 100644 --- a/moli-protocol-server/src/cdp_scheduler/frontend_control/target_control.rs +++ b/moli-protocol-server/src/cdp_scheduler/frontend_control/target_control.rs @@ -234,7 +234,7 @@ impl CdpFrontendTargetControl { .await; assert!( output.is_empty(), - "a frontend-control command without a Runtime barrier cannot release held output" + "a frontend-control command without a renderer response permit cannot release held output" ); if method == "Target.attachToBrowserTarget" && let Some(session_id) = response diff --git a/moli-protocol-server/src/cdp_scheduler/renderer_command_response_order.rs b/moli-protocol-server/src/cdp_scheduler/renderer_command_response_order.rs new file mode 100644 index 0000000000..845684395e --- /dev/null +++ b/moli-protocol-server/src/cdp_scheduler/renderer_command_response_order.rs @@ -0,0 +1,32 @@ +use moli_protocol::{CommandResponseFlushPermit, RendererCommandResponsePermit}; + +/// The single-use authority that finishes one command's protocol-output +/// boundary. +/// +/// The frontend flush permit and optional renderer-response permit are one +/// value so a command cannot publish its response while forgetting to release +/// the concrete Page output held on its behalf. Commands that do not execute +/// Page JavaScript still carry the frontend permit and simply have no +/// renderer response-order permit. +#[must_use = "a command output release permit must be completed exactly once"] +pub(crate) struct CommandOutputReleasePermit { + response_flush: CommandResponseFlushPermit, + renderer_response_permit: Option, +} + +impl CommandOutputReleasePermit { + pub(super) fn new( + response_flush: CommandResponseFlushPermit, + renderer_response_permit: Option, + ) -> Self { + Self { + response_flush, + renderer_response_permit, + } + } + + pub(super) fn finish_response(self) -> Option { + self.response_flush.finish(); + self.renderer_response_permit + } +} diff --git a/moli-protocol-server/src/cdp_scheduler/runtime_command_barrier.rs b/moli-protocol-server/src/cdp_scheduler/runtime_command_barrier.rs deleted file mode 100644 index 2cb57685b7..0000000000 --- a/moli-protocol-server/src/cdp_scheduler/runtime_command_barrier.rs +++ /dev/null @@ -1,32 +0,0 @@ -use moli_protocol::{CommandResponseFlushPermit, RuntimeCommandOutputBarrierPermit}; - -/// The single-use authority that finishes one command's protocol-output -/// boundary. -/// -/// The response permit and optional Runtime-output barrier are one value so a -/// command cannot publish its response while forgetting to release the -/// concrete Page output held on its behalf. Commands that do not execute -/// Page JavaScript still carry the response permit and simply have no Runtime -/// barrier. -#[must_use = "a command output release permit must be completed exactly once"] -pub(crate) struct CommandOutputReleasePermit { - response_flush: CommandResponseFlushPermit, - runtime_output_barrier: Option, -} - -impl CommandOutputReleasePermit { - pub(super) fn new( - response_flush: CommandResponseFlushPermit, - runtime_output_barrier: Option, - ) -> Self { - Self { - response_flush, - runtime_output_barrier, - } - } - - pub(super) fn finish_response(self) -> Option { - self.response_flush.finish(); - self.runtime_output_barrier - } -} diff --git a/moli-protocol/src/conn.rs b/moli-protocol/src/conn.rs index bee09b1b50..81e87df276 100644 --- a/moli-protocol/src/conn.rs +++ b/moli-protocol/src/conn.rs @@ -109,7 +109,11 @@ pub(crate) use cookie_owner::{ }; pub use devtools_command::DevToolsCommandDispatchOutcome; pub(crate) use devtools_command::DevToolsCommandExecutionOutput; -pub use dispatch::{CdpCommandTaskStep, CompletedCdpCommandDispatch, PendingCdpCommandDispatch}; +pub use dispatch::{ + AgentHostDispatchResult, CdpCommandTaskStep, CompletedCdpCommandDispatch, + PendingCdpCommandDispatch, RendererDispatch, RendererDispatchBinding, RendererDispatchLane, + RendererPageDispatchBinding, +}; pub(crate) use fetch_support::PendingStreamingDocumentResponseNavigation; pub(crate) use fetch_support::{ ClaimedFetchNavigation, ClaimedFetchResponseNavigation, ClaimedSubresourceContinueRequest, @@ -131,7 +135,7 @@ pub use fetch_support::{ }; pub use moli_protocol_cdp::{ CdpRendererCommandPolicy, CdpRendererCommandReplacement, CdpRendererCommandReplayDispatch, - CdpRendererDispatchLane, CdpRequest, ParsedCdpCommand, + CdpRequest, ParsedCdpCommand, }; use target::DEFAULT_BROWSER_CONTEXT_ID; pub(crate) use target::{ @@ -526,9 +530,8 @@ pub(crate) use popup_navigation_work::{ PopupTargetNavigationKind, PopupTargetNavigationOwnerAction, }; pub(crate) use runtime_eval::{ - ClaimedPendingInspectorAwait, ClaimedPendingInspectorAwaitOwner, RuntimeBindingCallEvent, - RuntimeEnableReplayEvent, renderer_command_turn_frontend_protocol_response, - runtime_remote_object_ids_in_map, + ClaimedPendingInspectorAwait, RuntimeBindingCallEvent, RuntimeEnableReplayEvent, + renderer_command_turn_frontend_protocol_response, runtime_remote_object_ids_in_map, }; pub use runtime_eval::{ CompletedMoliDiagnosticsDispatch, CompletedRuntimeBindingPageCommandDispatch, @@ -561,20 +564,20 @@ pub use state::{ DownloadNavigation, EmulatedDeviceMetrics, EmulatedGeolocationOverride, EmulatedGeolocationOverrideState, EmulatedMediaOverrides, IsolatedWorldDefinition, LoadedNavigation, NavigationDispatchState, NavigationLoadOutcome, NavigationRequestLoadPolicy, - PageNavigationHistoryEntry, PageTargetHost, RuntimeBindingDefinition, TargetInfo, URL_BASE, + PageAgentHost, PageNavigationHistoryEntry, RuntimeBindingDefinition, TargetInfo, URL_BASE, }; pub(crate) use state::{ BrowserContextPageStorageHandles, BrowserContextStoragePartitionHandles, CommittedRendererDocumentBinding, CompletedDownloadBodyArtifact, ContextNetworkPolicy, DedicatedWorkerMainScriptOutcome, DedicatedWorkerMainScriptSnapshot, DedicatedWorkerTargetState, DevToolsBrowserIdentityOverride, DevToolsConsoleOutputSessionState, - DevToolsLogViolationThreshold, DocumentId, DuplicatePendingRendererCommand, - EmulatedNetworkConditions, EmulatedViewportSurface, EmulationPolicyChange, - InitialDocumentCreator, InspectorCommandDispatch, NETWORK_ERROR_PAGE_URL, NavigationId, - NavigationResultProjection, NavigationSourceDocumentSecurityContext, - NetworkErrorPageNavigation, PageScreencastConfig, PageScreencastFormat, - PendingBidiChannelListener, PendingInspectorAwait, PendingRendererCommandKey, - PerformanceTimeDomain, PreparedRendererCallDispatch, ProfilerAction, ProfilerInspectorCommand, + DevToolsLogViolationThreshold, DocumentId, DocumentProjectionFence, + DocumentProjectionOutputRelease, DuplicatePendingRendererCommand, EmulatedNetworkConditions, + EmulatedViewportSurface, EmulationPolicyChange, InitialDocumentCreator, + InspectorCommandDispatch, NETWORK_ERROR_PAGE_URL, NavigationId, NavigationResultProjection, + NavigationSourceDocumentSecurityContext, NetworkErrorPageNavigation, PageScreencastConfig, + PageScreencastFormat, PendingBidiChannelListener, PendingInspectorAwait, PerformanceTimeDomain, + PreparedRendererCallDispatch, ProfilerAction, ProfilerInspectorCommand, RendererCommandCorrelation, RendererCommandDescriptor, RendererCommandReplay, RendererDocumentLifecycleObservation, RendererDocumentLifecycleObserver, RendererMainDocumentCommitSeed, RendererPageResidenceIdentity, @@ -600,13 +603,14 @@ pub(crate) use state::{ TargetRuntimeSessionState, }; pub(crate) use state::{HistoryTraversalDestination, ResolvedHistoryTraversal}; -pub(crate) use target::{ - PreparedTargetAttach, PreparedTargetHostClosure, PreparedTargetHostDelta, SessionDisposalPlan, - SessionDisposalTarget, TargetAttachSessionCommit, TargetClosureCleanupPlan, TargetEventPlan, - TargetSessionDetachCleanupPlan, -}; use target::{ - TargetClosurePlan, TargetControlPlane, TargetHostDelta, target_destroyed_automation_events, + DevToolsAgentHostRegistry, TargetClosurePlan, TargetHostDelta, + target_destroyed_automation_events, +}; +pub(crate) use target::{ + DevToolsSessionHandlerSet, PreparedTargetAttach, PreparedTargetHostClosure, + PreparedTargetHostDelta, SessionDisposalPlan, SessionDisposalTarget, TargetAttachSessionCommit, + TargetClosureCleanupPlan, TargetEventPlan, TargetSessionDetachCleanupPlan, }; pub(crate) use top_level_navigation_work::TopLevelLocationNavigationOwnerAction; @@ -829,44 +833,6 @@ impl DeferredMainDocumentLoadPredecessorCandidate { } } -#[derive(Clone, Debug, PartialEq, Eq)] -pub(crate) struct RuntimeAwaitJob { - command_id: u64, - owner: CommandOwnerScope, - object_group: Option, - action: &'static str, -} - -impl RuntimeAwaitJob { - pub(crate) fn new( - command_id: u64, - owner: &CommandOwnerScope, - object_group: Option<&str>, - action: &'static str, - ) -> Self { - Self { - command_id, - owner: owner.clone(), - object_group: object_group.map(str::to_owned), - action, - } - } - - pub(crate) fn trace_fields(&self) -> serde_json::Value { - json!({ - "commandId": self.command_id, - "sessionId": self.owner.session_id(), - "ownerRoute": self.owner.explicit_route().map(|route| format!("{route:?}")), - "objectGroup": self.object_group, - "action": self.action, - }) - } - - pub(crate) fn session_id(&self) -> Option { - self.owner.session_id().map(str::to_owned) - } -} - #[derive(Clone)] pub struct CdpInitialStoragePartition { handles: BrowserContextStoragePartitionHandles, @@ -1194,7 +1160,7 @@ pub struct CdpConnection { // additional attached sessions. A randomized HashMap iteration order made that // choice vary between otherwise identical processes. auto_attach_owner_sessions: IndexMap, AutoAttachOwnerPolicy>, - target_control: TargetControlPlane, + agent_hosts: DevToolsAgentHostRegistry, default_target_lifecycle: DefaultTargetLifecycle, service_worker_auto_attach_related_owners: Vec, service_worker_pause_on_start_owner_sessions: HashSet>, @@ -1209,10 +1175,6 @@ pub struct CdpConnection { next_page_domain_subscription_generation: u64, next_internal_runtime_command_id: u64, network_request_id_allocator: ConnectionNetworkRequestIdAllocator, - pending_runtime_await_jobs: HashMap, - claimed_pending_inspector_await_owners: - HashMap, - // Browser profile, permissions, download and global IO state. pub window_bounds: BrowserWindowBounds, download_policy: moli_core::browser::DownloadPolicy, @@ -1246,7 +1208,7 @@ pub struct CdpConnection { scheduler_state: CdpConnectionSchedulerState, // Standalone navigation state for commands that have no concrete Page - // owner. Every page-owned engine lives in its stable PageTargetHost. + // owner. Every page-owned engine lives in its stable PageAgentHost. standalone_navigation_engine: StandaloneNavigationEngineSlot, } @@ -1276,7 +1238,7 @@ impl Drop for CdpConnection { root.terminate_renderer_producers_for_owner_shutdown(); } - // Dropping contexts releases every PageTargetHost and its Page state. + // Dropping contexts releases every PageAgentHost and its Page state. drop(contexts); // RenderRuntimeOwner joins happen when the last JsRuntime-backed @@ -1326,7 +1288,7 @@ impl CdpConnection { if let Some((browser_context_id, target_id)) = active_owner { return self .ensure_page_navigation_engine_for_target(&browser_context_id, &target_id) - .expect("active PageTargetHost navigation engine disappeared"); + .expect("active PageAgentHost navigation engine disappeared"); } self.standalone_navigation_engine.ensure_mut() } @@ -1504,7 +1466,7 @@ impl CdpConnection { target_info_change_events_enabled: false, target_discovery_filter: None, auto_attach_owner_sessions: IndexMap::new(), - target_control: TargetControlPlane::default(), + agent_hosts: DevToolsAgentHostRegistry::default(), default_target_lifecycle: DefaultTargetLifecycle::default(), service_worker_auto_attach_related_owners: Vec::new(), service_worker_pause_on_start_owner_sessions: HashSet::new(), @@ -1524,8 +1486,6 @@ impl CdpConnection { next_page_domain_subscription_generation: 0, next_internal_runtime_command_id: 902_000_000, network_request_id_allocator: ConnectionNetworkRequestIdAllocator::default(), - pending_runtime_await_jobs: HashMap::new(), - claimed_pending_inspector_await_owners: HashMap::new(), base_browser_identity, global_extra_headers: Default::default(), global_browser_identity_override: None, @@ -1780,12 +1740,12 @@ impl CdpConnection { self.document_navigation_state_for_owner(&owner_scope) } - pub fn renderer_document_navigation_is_suspended_for_session_owner( + pub fn document_projection_is_pending_for_session_owner( &self, session_id: Option<&str>, ) -> bool { self.runtime_session_owner_slot(session_id) - .is_ok_and(TargetRuntimeSlot::renderer_document_navigation_is_suspended) + .is_ok_and(TargetRuntimeSlot::document_projection_is_pending) } pub(crate) fn accepts_pending_document_navigation_for_owner( @@ -1993,10 +1953,16 @@ impl CdpConnection { return (None, Vec::new()); }; context.install_document_lifecycle_for_test(&target_id, lifecycle); + let browser_sequence = context + .renderer_document_lifecycle_binding_for_target(&target_id) + .filter(|binding| binding.document_id == document) + .map(|binding| binding.browser_sequence) + .unwrap_or_else(moli_core::browser::BrowserSequence::allocate); self.project_committed_document_lifecycle_for_owner( owner, CommittedDocumentLifecycle { document, + browser_sequence, artifacts, }, navigation, @@ -2528,7 +2494,6 @@ impl CdpConnection { "commandId": command_id, "sessionId": session_id, "fields": fields, - "pendingRuntimeAwaitJobCount": self.pending_runtime_await_jobs.len(), })); } @@ -3093,7 +3058,7 @@ impl CdpConnection { "connection": { "hasActiveBrowserContext": self.browser_context.is_some(), "inactiveBrowserContextCount": self.inactive_browser_contexts.len(), - "browserSessionIdCount": self.target_control.browser_session_count(), + "browserSessionIdCount": self.agent_hosts.browser_session_count(), "globalIoStreamCount": self.global_io_streams.len(), "tracing": self.tracing_state.diagnostics(), "permissionOverrideCount": self.permission_override_count(), @@ -3324,7 +3289,7 @@ impl CdpConnection { if let Some(page_target_id) = self.primary_page_target_id_for_tab_target_id(target_id) { let page_target_info = self.devtools_page_or_worker_target_info(page_target_id)?; return self - .target_control + .agent_hosts .tab_target_info_for_page_target_info(page_target_info); } self.devtools_page_or_worker_target_info(target_id) @@ -3344,7 +3309,7 @@ impl CdpConnection { let mut target_infos = Vec::new(); if let Some(page_target_info) = self.default_target_lifecycle.placeholder_page_info() { if let Some(tab_target_info) = self - .target_control + .agent_hosts .tab_target_info_for_page_target_info(page_target_info.clone()) { target_infos.push(tab_target_info); @@ -3358,7 +3323,7 @@ impl CdpConnection { browser_context.target_popup_id(target_id.as_str()); } if let Some(tab_target_info) = self - .target_control + .agent_hosts .tab_target_info_for_page_target_info(page_or_worker_target_info.clone()) { target_infos.push(tab_target_info); @@ -3437,7 +3402,7 @@ impl CdpConnection { } else { self.gen_tab_target_id() }; - self.target_control + self.agent_hosts .register_tab(tab_target_id.clone(), page_target_id.to_owned()); for target_id in [&tab_target_id, page_target_id] { if let Some(target_info) = self.target_info_for_host_delta(target_id) { @@ -3451,7 +3416,7 @@ impl CdpConnection { #[doc(hidden)] pub fn tab_target_id_for_page_target_id(&self, page_target_id: &str) -> Option<&str> { - self.target_control + self.agent_hosts .tab_target_id_for_page_target_id(page_target_id) } @@ -3459,12 +3424,12 @@ impl CdpConnection { &self, tab_target_id: &str, ) -> Option<&str> { - self.target_control + self.agent_hosts .primary_page_target_id_for_tab_target_id(tab_target_id) } pub(crate) fn primary_session_id_for_tab_target_id(&self, tab_target_id: &str) -> Option<&str> { - self.target_control + self.agent_hosts .primary_session_id_for_tab_target_id(tab_target_id) } @@ -3474,7 +3439,7 @@ impl CdpConnection { session_id: String, is_attached_session: bool, ) -> bool { - self.target_control.assign_session_to_tab_target( + self.agent_hosts.assign_session_to_tab_target( tab_target_id, session_id, is_attached_session, @@ -3482,7 +3447,7 @@ impl CdpConnection { } pub(crate) fn remove_tab_session(&mut self, session_id: &str) -> Option { - self.target_control.remove_tab_session(session_id) + self.agent_hosts.remove_tab_session(session_id) } pub(crate) fn remove_tab_for_page_target( @@ -3490,7 +3455,7 @@ impl CdpConnection { page_target_id: &str, ) -> Option { let closure_plan = self - .target_control + .agent_hosts .remove_tab_by_page_target_id(page_target_id)?; for target_id in closure_plan.destroyed_target_ids() { self.notify_target_host_lifecycle(CdpTargetHostLifecycleDelta::Destroyed { @@ -3535,7 +3500,7 @@ impl CdpConnection { } pub(crate) fn tab_target_id_for_session_id(&self, session_id: &str) -> Option<&str> { - self.target_control.tab_target_id_for_session_id(session_id) + self.agent_hosts.tab_target_id_for_session_id(session_id) } pub(crate) fn browser_context_id_for_tab_target_id( @@ -3559,7 +3524,7 @@ impl CdpConnection { if page_target_info.kind != DevToolsTargetKind::Page { return None; } - self.target_control + self.agent_hosts .tab_target_info_for_page_target_info(page_target_info.clone()) } @@ -3576,7 +3541,7 @@ impl CdpConnection { let root_filter = owner_session_id .is_none() .then(|| filter.to_devtools_entries()); - self.target_control + self.agent_hosts .set_discover_targets(owner_session_id, filter); if let Some(root_filter) = root_filter { self.target_discovery_enabled = true; @@ -3601,7 +3566,7 @@ impl CdpConnection { } pub(crate) fn clear_target_discovery_for_owner(&mut self, owner_session_id: Option<&str>) { - self.target_control.clear_discover_targets(owner_session_id); + self.agent_hosts.clear_discover_targets(owner_session_id); if owner_session_id.is_none() { self.target_discovery_enabled = false; self.target_info_change_events_enabled = false; @@ -3633,8 +3598,7 @@ impl CdpConnection { &self, owner_session_id: Option<&str>, ) -> Option> { - self.target_control - .discover_filter_entries(owner_session_id) + self.agent_hosts.discover_filter_entries(owner_session_id) } pub(crate) fn initial_target_created_events_for_discovery_owner( @@ -3642,23 +3606,23 @@ impl CdpConnection { owner_session_id: Option<&str>, target_infos: Vec, ) -> Vec { - self.target_control + self.agent_hosts .initial_target_created_events_for_owner(owner_session_id, target_infos) } pub(crate) fn has_any_target_discovery(&self) -> bool { - self.target_control.has_any_discovery() + self.agent_hosts.has_any_discovery() } pub(crate) fn has_any_target_info_observer(&self) -> bool { - self.target_control.has_any_target_info_observer() + self.agent_hosts.has_any_target_info_observer() } fn exact_target_created_events_for_all_discovery_owners( &mut self, target_info: DevToolsTargetInfo, ) -> Vec { - self.target_control + self.agent_hosts .target_created_events_for_all_discovery_owners(target_info) } @@ -3667,7 +3631,7 @@ impl CdpConnection { } fn target_created_event_plan_for_target_delta(&mut self, target_id: &str) -> TargetEventPlan { - let deltas = self.target_control.target_created_deltas(target_id); + let deltas = self.agent_hosts.target_created_deltas(target_id); self.target_host_delta_events(deltas) } @@ -3747,7 +3711,7 @@ impl CdpConnection { return TargetEventPlan::default(); }; TargetEventPlan::from_background_events( - self.target_control + self.agent_hosts .target_info_changed_events_for_all_discovery_owners(target_info), ) } @@ -3772,7 +3736,7 @@ impl CdpConnection { pub(crate) fn prepare_target_host_closure(&self, target_id: &str) -> PreparedTargetHostClosure { let mut detached_info_deltas = Vec::new(); let mut destroyed_deltas = Vec::new(); - for delta in self.target_control.target_destroyed_deltas(target_id) { + for delta in self.agent_hosts.target_destroyed_deltas(target_id) { let target_id = delta.target_id().to_owned(); let Some(target_info) = self.target_info_for_host_delta(&target_id) else { continue; @@ -3846,7 +3810,7 @@ impl CdpConnection { &self, target_info: DevToolsTargetInfo, ) -> Vec { - self.target_control + self.agent_hosts .target_info_changed_events_for_all_observer_owners(target_info) } @@ -3854,7 +3818,7 @@ impl CdpConnection { &mut self, target_info: DevToolsTargetInfo, ) -> Vec { - self.target_control + self.agent_hosts .target_destroyed_events_for_all_discovery_owners(target_info) } @@ -3864,7 +3828,7 @@ impl CdpConnection { status: &str, error_code: i32, ) -> Vec { - self.target_control + self.agent_hosts .target_crashed_events_for_all_discovery_owners(target_id, status, error_code) } @@ -3881,13 +3845,13 @@ impl CdpConnection { &self, target_info: DevToolsTargetInfo, ) -> Vec { - self.target_control + self.agent_hosts .project_page_tab_target_infos_for_destruction(target_info) } #[cfg(test)] pub(crate) fn tab_target_count(&self) -> usize { - self.target_control.len() + self.agent_hosts.len() } #[cfg(test)] @@ -3897,7 +3861,7 @@ impl CdpConnection { } fn has_registered_target_id(&self, target_id: &str) -> bool { - self.target_control.contains_tab_or_page_relation(target_id) + self.agent_hosts.contains_tab_or_page_relation(target_id) || self .browser_contexts() .any(|context| context.devtools_target_info(target_id).is_some()) @@ -3943,7 +3907,7 @@ impl CdpConnection { } } if !self - .target_control + .agent_hosts .contains_tab_or_page_relation(&default_target_id) { self.register_top_level_page_target(&default_target_id); diff --git a/moli-protocol/src/conn/browser_context/network_owner.rs b/moli-protocol/src/conn/browser_context/network_owner.rs index 172d3d4392..2b3e59e4d5 100644 --- a/moli-protocol/src/conn/browser_context/network_owner.rs +++ b/moli-protocol/src/conn/browser_context/network_owner.rs @@ -12,7 +12,7 @@ use moli_core::page::PendingPageCommand; use moli_page_types::DevToolsSessionKey; struct TargetNetworkListenerOwnerMut<'a> { - target: &'a mut crate::conn::PageTargetHost, + target: &'a mut crate::conn::PageAgentHost, session_key: DevToolsSessionKey, } diff --git a/moli-protocol/src/conn/browser_context/page_owner.rs b/moli-protocol/src/conn/browser_context/page_owner.rs index e4baf61c9a..543739ce89 100644 --- a/moli-protocol/src/conn/browser_context/page_owner.rs +++ b/moli-protocol/src/conn/browser_context/page_owner.rs @@ -985,7 +985,7 @@ fn runtime_observable_console_payloads( #[cfg(test)] mod tests { use super::*; - use crate::conn::PageTargetHost; + use crate::conn::PageAgentHost; #[test] fn csp_policy_preserves_peer_contributions_across_update_disable_and_detach() { @@ -1072,7 +1072,7 @@ mod tests { } } - fn background_session_state_mut(state: &mut PageTargetHost) -> TargetSessionStateMut<'_> { + fn background_session_state_mut(state: &mut PageAgentHost) -> TargetSessionStateMut<'_> { TargetSessionStateMut { devtools_session_state: &mut state.devtools_sessions [moli_page_types::DevToolsSessionKey::Primary], diff --git a/moli-protocol/src/conn/browser_context/runtime_owner.rs b/moli-protocol/src/conn/browser_context/runtime_owner.rs index f204875738..15691eba83 100644 --- a/moli-protocol/src/conn/browser_context/runtime_owner.rs +++ b/moli-protocol/src/conn/browser_context/runtime_owner.rs @@ -282,8 +282,9 @@ impl CdpConnection { Some(0), session_id.as_deref(), ) { - crate::conn::CdpCommandTaskStep::Complete(outcome) => outcome, - crate::conn::CdpCommandTaskStep::Pending(_) => { + crate::conn::AgentHostDispatchResult::Complete(outcome) => outcome, + crate::conn::AgentHostDispatchResult::PendingService(_) + | crate::conn::AgentHostDispatchResult::FallThrough(_) => { unreachable!("a completed Runtime listener plan cannot become pending") } } @@ -397,7 +398,7 @@ impl CdpConnection { session_id: &str, route: &CdpSessionRoute, ) -> Option<()> { - if self.target_control.attached_session_route(session_id) == Some(route) { + if self.agent_hosts.attached_session_route(session_id) == Some(route) { return Some(()); } let target_id = match route { @@ -406,7 +407,7 @@ impl CdpConnection { | CdpSessionRoute::ServiceWorkerTarget { target_id, .. } => target_id.clone(), _ => return None, }; - self.target_control.commit_attached_session( + self.agent_hosts.commit_attached_session( session_id.to_owned(), None, &target_id, diff --git a/moli-protocol/src/conn/browser_context/session_owner.rs b/moli-protocol/src/conn/browser_context/session_owner.rs index d7973dc54d..ec5c790bdb 100644 --- a/moli-protocol/src/conn/browser_context/session_owner.rs +++ b/moli-protocol/src/conn/browser_context/session_owner.rs @@ -1,4 +1,4 @@ -use crate::conn::{CdpConnection, CdpSessionRoute, CommandOwnerScope, PageTargetHost}; +use crate::conn::{CdpConnection, CdpSessionRoute, CommandOwnerScope, PageAgentHost}; use moli_page_types::DevToolsSessionKey; pub(super) struct TargetSessionOwner { @@ -33,7 +33,7 @@ impl CdpConnection { pub(crate) fn mutate_target_page_state_for_session( &mut self, session_id: Option<&str>, - f: impl FnOnce(&mut PageTargetHost), + f: impl FnOnce(&mut PageAgentHost), ) -> bool { self.target_session_owner_mut(session_id) .map(|mut owner| owner.mutate_page_state(|state, _| f(state))) diff --git a/moli-protocol/src/conn/browser_context/target_session_owner.rs b/moli-protocol/src/conn/browser_context/target_session_owner.rs index dbf0c47497..5fc606cee9 100644 --- a/moli-protocol/src/conn/browser_context/target_session_owner.rs +++ b/moli-protocol/src/conn/browser_context/target_session_owner.rs @@ -4,9 +4,10 @@ use crate::conn::DocumentStartScript; #[cfg(test)] use crate::conn::state::BrowserContextResourceStorageHandles; use crate::conn::state::{ - BrowserContextPageStorageHandles, DevToolsSessionState, PageNavigationHistoryEntry, - PageTargetHost, RendererMainDocumentCommitSeed, TargetFetchConfig, TargetOwnerState, - TargetPageResidenceIdentity, TargetRuntimeSessionState, TargetRuntimeSlot, WindowSurfaceState, + BrowserContextPageStorageHandles, DevToolsSessionState, PageAgentHost, + PageNavigationHistoryEntry, RendererMainDocumentCommitSeed, TargetFetchConfig, + TargetOwnerState, TargetPageResidenceIdentity, TargetRuntimeSessionState, TargetRuntimeSlot, + WindowSurfaceState, }; use crate::conn::{ BackgroundProtocolEvent, CommandOwnerScope, ConnectionNetworkRequestIdAllocator, @@ -506,7 +507,7 @@ impl<'a> TargetSessionOwnerRef<'a> { .target_navigation_history_entry_url(&self.target_id, entry_id) } - fn target(&self) -> &'a crate::conn::PageTargetHost { + fn target(&self) -> &'a crate::conn::PageAgentHost { self.browser_context .page_target(&self.target_id) .expect("resolved Page target owner must remain live") @@ -660,19 +661,19 @@ impl TargetSessionStateMut<'_> { } impl<'a> TargetSessionOwnerMut<'a> { - fn target(&self) -> &crate::conn::PageTargetHost { + fn target(&self) -> &crate::conn::PageAgentHost { self.browser_context .page_target(&self.target_id) .expect("resolved Page target owner must remain live") } - fn target_mut(&mut self) -> &mut crate::conn::PageTargetHost { + fn target_mut(&mut self) -> &mut crate::conn::PageAgentHost { self.browser_context .page_target_mut(&self.target_id) .expect("resolved Page target owner must remain live") } - fn into_target_mut(self) -> &'a mut crate::conn::PageTargetHost { + fn into_target_mut(self) -> &'a mut crate::conn::PageAgentHost { self.browser_context .page_target_mut(&self.target_id) .expect("resolved Page target owner must remain live") @@ -702,7 +703,7 @@ impl<'a> TargetSessionOwnerMut<'a> { pub(super) fn mutate_page_state( &mut self, - f: impl FnOnce(&mut PageTargetHost, &DevToolsSessionKey) -> T, + f: impl FnOnce(&mut PageAgentHost, &DevToolsSessionKey) -> T, ) -> T { let session_key = self.session_key.clone(); f(self.target_mut(), &session_key) @@ -1522,6 +1523,17 @@ impl CdpConnection { ) } + pub(crate) fn page_agent_host_main_frame_slot_for_owner( + &self, + owner: &CommandOwnerScope, + ) -> Option { + Some( + self.target_session_owner_ref_for_owner(owner)? + .target() + .main_frame_slot_id(), + ) + } + pub(crate) fn resolved_page_owner_identity_for_owner( &self, owner: &CommandOwnerScope, diff --git a/moli-protocol/src/conn/command_view.rs b/moli-protocol/src/conn/command_view.rs index 8bcdf0a94e..38d6e10958 100644 --- a/moli-protocol/src/conn/command_view.rs +++ b/moli-protocol/src/conn/command_view.rs @@ -24,8 +24,9 @@ pub struct Cmd<'a> { pub session_id: Option<&'a str>, /// Raw JSON of the entire message, useful for inspector passthrough. pub json: &'a str, - /// Renderer scheduling facts derived from the same validated method as - /// every other field in this view. + /// Renderer execution policy derived from the same validated method as + /// every other field in this view. Dispatch ownership and Main/IO routing + /// are decided later by the domain handler. /// /// This is private so callers cannot pair one command with another /// command's policy. Domain dispatchers consume it through diff --git a/moli-protocol/src/conn/dispatch.rs b/moli-protocol/src/conn/dispatch.rs index 9d564c21c8..f6fd6b263e 100644 --- a/moli-protocol/src/conn/dispatch.rs +++ b/moli-protocol/src/conn/dispatch.rs @@ -9,17 +9,106 @@ use super::*; pub struct PendingCdpCommandDispatch { inner: PendingCdpCommandDispatchKind, scheduler_events: Vec, + owner_scope: CommandOwnerScope, } pub struct CompletedCdpCommandDispatch { inner: CompletedCdpCommandDispatchKind, + owner_scope: CommandOwnerScope, } +/// Renderer receiver selected only after a DevTools domain handler falls +/// through its browser/content phase. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum RendererDispatchLane { + Main, + Io, +} + +/// Exact Page AgentHost binding that admitted one renderer command. +/// +/// `MainFrameSlotId` remains stable across cross-document navigation while +/// `DocumentId` and `RendererAgentAttachmentId` identify the admitted binding +/// generation. A completion cannot be rebound to whichever Document is current +/// later. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct RendererPageDispatchBinding { + main_frame_slot: moli_core::browser::MainFrameSlotId, + document: moli_core::browser::DocumentId, + attachment: moli_core::page::RendererAgentAttachmentId, +} + +impl RendererPageDispatchBinding { + pub const fn main_frame_slot(self) -> moli_core::browser::MainFrameSlotId { + self.main_frame_slot + } + + pub const fn document(self) -> moli_core::browser::DocumentId { + self.document + } + + pub const fn attachment(self) -> moli_core::page::RendererAgentAttachmentId { + self.attachment + } +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum RendererDispatchBinding { + Page(RendererPageDispatchBinding), + Worker { target_id: String }, +} + +/// One concrete renderer fallthrough plus the DevTools-owned pending reply. +pub struct RendererDispatch { + lane: RendererDispatchLane, + binding: RendererDispatchBinding, + pending: Box, +} + +impl RendererDispatch { + pub const fn lane(&self) -> RendererDispatchLane { + self.lane + } + + pub const fn binding(&self) -> &RendererDispatchBinding { + &self.binding + } + + pub fn into_pending(self) -> Box { + self.pending + } +} + +/// Result of the DevToolsSession/AgentHost browser-content dispatch phase. +/// +/// Completion ownership and renderer routing deliberately remain separate: +/// only `FallThrough` has a renderer binding and Main/IO lane. +pub enum AgentHostDispatchResult { + Complete(CdpRendererOwnerTurnOutcome), + PendingService(Box), + FallThrough(RendererDispatch), +} + +/// Compatibility task step used by direct in-process/test callers. Production +/// frontend dispatch consumes `AgentHostDispatchResult` so it cannot erase the +/// service/fallthrough distinction at ingress. pub enum CdpCommandTaskStep { Pending(Box), Complete(CdpRendererOwnerTurnOutcome), } +impl From for CdpCommandTaskStep { + fn from(result: AgentHostDispatchResult) -> Self { + match result { + AgentHostDispatchResult::Complete(outcome) => Self::Complete(outcome), + AgentHostDispatchResult::PendingService(pending) => Self::Pending(pending), + AgentHostDispatchResult::FallThrough(dispatch) => { + Self::Pending(dispatch.into_pending()) + } + } + } +} + impl CdpCommandTaskStep { #[cfg(test)] pub fn into_parts(self) -> (Vec, Vec) { @@ -103,6 +192,31 @@ impl PendingCdpCommandDispatchKind { Self::Tracing(_) => "Tracing", } } + + fn renderer_dispatch_lane(&self) -> Option { + match self { + Self::Runtime(pending) => pending.renderer_dispatch_lane(), + Self::Accessibility(pending) => pending.renderer_dispatch_lane(), + Self::Css(pending) => pending.renderer_dispatch_lane(), + Self::Dom(pending) => pending.renderer_dispatch_lane(), + Self::DomDebugger(pending) => pending.renderer_dispatch_lane(), + Self::DomSnapshot(pending) => pending.renderer_dispatch_lane(), + Self::Page(pending) => pending.renderer_dispatch_lane(), + Self::Performance(pending) => pending.renderer_dispatch_lane(), + Self::Emulation(pending) => pending.renderer_dispatch_lane(), + Self::Autofill(_) + | Self::Input(_) + | Self::DomStorage(_) + | Self::Storage(_) + | Self::Network(_) + | Self::Fetch(_) + | Self::Io(_) + | Self::Security(_) + | Self::Browser(_) + | Self::Target(_) + | Self::Tracing(_) => None, + } + } } impl CompletedCdpCommandDispatchKind { @@ -133,10 +247,15 @@ impl CompletedCdpCommandDispatchKind { } impl PendingCdpCommandDispatch { - fn new(inner: PendingCdpCommandDispatchKind, scheduler_events: Vec) -> Self { + fn new( + inner: PendingCdpCommandDispatchKind, + scheduler_events: Vec, + owner_scope: CommandOwnerScope, + ) -> Self { Self { inner, scheduler_events, + owner_scope, } } @@ -235,11 +354,17 @@ impl PendingCdpCommandDispatch { self, conn: &mut CdpConnection, ) -> CompletedCdpCommandDispatch { - match self.inner { + let Self { + inner, + owner_scope, + scheduler_events: _, + } = self; + match inner { PendingCdpCommandDispatchKind::Runtime(pending) => CompletedCdpCommandDispatch { inner: CompletedCdpCommandDispatchKind::Runtime(Box::new( pending.complete_scheduler_deferred_inspector_reply(conn), )), + owner_scope, }, _ => { unreachable!( @@ -256,7 +381,12 @@ impl PendingCdpCommandDispatch { } pub async fn wait(self) -> CompletedCdpCommandDispatch { - let kind = self.inner.name(); + let Self { + inner, + scheduler_events: _, + owner_scope, + } = self; + let kind = inner.name(); let trace_started = moli_trace::cdp_runtime_trace_enabled().then(std::time::Instant::now); if trace_started.is_some() { tracing::info!( @@ -265,7 +395,7 @@ impl PendingCdpCommandDispatch { pending_kind = kind, ); } - let inner = match self.inner { + let inner = match inner { PendingCdpCommandDispatchKind::Runtime(pending) => { CompletedCdpCommandDispatchKind::Runtime(Box::new(Box::pin(pending.wait()).await)) } @@ -337,7 +467,7 @@ impl PendingCdpCommandDispatch { elapsed_us = %started.elapsed().as_micros(), ); } - CompletedCdpCommandDispatch { inner } + CompletedCdpCommandDispatch { inner, owner_scope } } } @@ -371,11 +501,52 @@ fn append_command_output_plan( } impl CdpConnection { + /// Whether a command must wait for the Page AgentHost to publish its next + /// Document binding before its browser/content handler may run. + /// + /// This is deliberately not a renderer-lane classifier. A lane exists + /// only after a handler returns `FallThrough`; this admission check merely + /// prevents a known Main-thread fallthrough from binding to the outgoing + /// Document while a replacement is being projected. + pub fn command_waits_for_document_projection(&self, command: &ParsedCdpCommand) -> bool { + if !self.document_projection_is_pending_for_session_owner(command.session_id()) { + return false; + } + let Some(cmd) = Cmd::from_parsed(command) else { + return false; + }; + let Some((domain, _)) = cmd.method.split_once('.') else { + return false; + }; + match domain { + "Runtime" => crate::domains::runtime::command_waits_for_document_projection(&cmd), + "Debugger" => crate::domains::debugger::command_waits_for_document_projection(&cmd), + "Console" => crate::domains::console::command_waits_for_document_projection(&cmd), + "Profiler" => crate::domains::profiler::command_waits_for_document_projection(&cmd), + "HeapProfiler" => { + crate::domains::heap_profiler::command_waits_for_document_projection(&cmd) + } + "Accessibility" => { + crate::domains::accessibility::command_waits_for_document_projection(&cmd) + } + "CSS" => crate::domains::css::command_waits_for_document_projection(&cmd), + "DOM" => crate::domains::dom::command_waits_for_document_projection(&cmd), + "DOMDebugger" => { + crate::domains::dom_debugger::command_waits_for_document_projection(&cmd) + } + "DOMSnapshot" => { + crate::domains::dom_snapshot::command_waits_for_document_projection(&cmd) + } + "Page" => crate::domains::page::command_waits_for_document_projection(&cmd), + _ => false, + } + } + fn complete_with_protocol_events( &mut self, command_context: &mut CommandDispatchContext, protocol_events: Vec, - ) -> CdpCommandTaskStep { + ) -> AgentHostDispatchResult { // `CommandDispatchContext::protocol_events` contains concrete output // explicitly classified as preceding this command's response. Keep // that ordering at the one common completion boundary instead of @@ -389,7 +560,7 @@ impl CdpConnection { self.record_tracing_protocol_events(&protocol_events); self.record_tracing_protocol_events(&post_renderer_output_events); self.record_tracing_protocol_events(&post_response_events); - CdpCommandTaskStep::Complete( + AgentHostDispatchResult::Complete( CdpTurnOutcome::new_with_protocol_and_post_response_events( protocol_events, post_response_events, @@ -406,7 +577,7 @@ impl CdpConnection { plan: CommandOutputPlan, command_id: Option, session_id: Option<&str>, - ) -> CdpCommandTaskStep { + ) -> AgentHostDispatchResult { let mut protocol_events = Vec::new(); append_command_output_plan( &mut protocol_events, @@ -418,12 +589,53 @@ impl CdpConnection { self.complete_with_protocol_events(command_context, protocol_events) } - fn pending_step(&mut self, inner: PendingCdpCommandDispatchKind) -> CdpCommandTaskStep { + fn pending_step( + &mut self, + owner_scope: CommandOwnerScope, + inner: PendingCdpCommandDispatchKind, + ) -> AgentHostDispatchResult { + let renderer_lane = inner.renderer_dispatch_lane(); let scheduler_events = self.take_scheduler_events(); - CdpCommandTaskStep::Pending(Box::new(PendingCdpCommandDispatch::new( + let pending = Box::new(PendingCdpCommandDispatch::new( inner, scheduler_events, - ))) + owner_scope.clone(), + )); + let Some(lane) = renderer_lane else { + return AgentHostDispatchResult::PendingService(pending); + }; + let binding = self + .renderer_dispatch_binding_for_owner(&owner_scope) + .expect("a renderer fallthrough must retain its exact AgentHost binding"); + AgentHostDispatchResult::FallThrough(RendererDispatch { + lane, + binding, + pending, + }) + } + + fn renderer_dispatch_binding_for_owner( + &self, + owner: &CommandOwnerScope, + ) -> Option { + match owner.resolve_route(self)? { + CdpSessionRoute::PageTarget { .. } => { + let attachment = self.current_renderer_agent_attachment_for_owner(owner)?; + Some(RendererDispatchBinding::Page(RendererPageDispatchBinding { + main_frame_slot: self.page_agent_host_main_frame_slot_for_owner(owner)?, + document: attachment.document(), + attachment: attachment.id(), + })) + } + CdpSessionRoute::SharedWorkerTarget { target_id, .. } + | CdpSessionRoute::DedicatedWorkerTarget { target_id, .. } + | CdpSessionRoute::ServiceWorkerTarget { target_id, .. } => { + Some(RendererDispatchBinding::Worker { target_id }) + } + CdpSessionRoute::Browser + | CdpSessionRoute::BrowserContext { .. } + | CdpSessionRoute::TabTarget { .. } => None, + } } #[cfg(test)] @@ -448,15 +660,17 @@ impl CdpConnection { Ok(command) => command, Err(error) => { let mut command_context = CommandDispatchContext::default(); - return self.complete_with_output_plan( - &mut command_context, - CommandOutputPlan::error_without_session( - error.response_code(), - error.response_message(), - ), - error.command_id(), - None, - ); + return self + .complete_with_output_plan( + &mut command_context, + CommandOutputPlan::error_without_session( + error.response_code(), + error.response_message(), + ), + error.command_id(), + None, + ) + .into(); } }; self.start_parsed_command_dispatch(&command) @@ -469,13 +683,14 @@ impl CdpConnection { ) -> CdpCommandTaskStep { let mut command_context = CommandDispatchContext::default(); self.start_parsed_command_dispatch_with_context(command, &mut command_context) + .into() } pub fn start_parsed_command_dispatch_with_context( &mut self, command: &ParsedCdpCommand, command_context: &mut CommandDispatchContext, - ) -> CdpCommandTaskStep { + ) -> AgentHostDispatchResult { let req = command.request(); let Some(dot) = req.method().find('.') else { return self.complete_with_output_plan( @@ -520,12 +735,16 @@ impl CdpConnection { .with_terminal_response_delivery_override( command_context.terminal_response_delivery_override(), ); + let command_owner = CommandOwnerScope::capture(self, cmd.session_id); self.record_tracing_command(cmd.method, cmd.session_id); let step = match domain { "Browser" => Some( match crate::domains::browser::try_start_browser_command_dispatch(self, &cmd) { crate::domains::browser::BrowserCommandTaskStep::Pending(pending) => { - self.pending_step(PendingCdpCommandDispatchKind::Browser(pending)) + self.pending_step( + command_owner.clone(), + PendingCdpCommandDispatchKind::Browser(pending), + ) } crate::domains::browser::BrowserCommandTaskStep::Complete(plan) => { self.complete_with_output_plan(command_context, plan, cmd.id, cmd.session_id) @@ -535,7 +754,10 @@ impl CdpConnection { "Runtime" => crate::domains::runtime::try_start_runtime_command_dispatch(self, &cmd) .map(|step| match step { crate::domains::runtime::RuntimeCommandTaskStep::Pending(pending) => { - self.pending_step(PendingCdpCommandDispatchKind::Runtime(pending)) + self.pending_step( + command_owner.clone(), + PendingCdpCommandDispatchKind::Runtime(pending), + ) } crate::domains::runtime::RuntimeCommandTaskStep::Complete(plan) => { self.complete_with_output_plan(command_context, plan, cmd.id, cmd.session_id) @@ -548,7 +770,10 @@ impl CdpConnection { ) .map(|step| match step { crate::domains::runtime::RuntimeCommandTaskStep::Pending(pending) => { - self.pending_step(PendingCdpCommandDispatchKind::Runtime(pending)) + self.pending_step( + command_owner.clone(), + PendingCdpCommandDispatchKind::Runtime(pending), + ) } crate::domains::runtime::RuntimeCommandTaskStep::Complete(plan) => { self.complete_with_output_plan(command_context, plan, cmd.id, cmd.session_id) @@ -558,7 +783,10 @@ impl CdpConnection { "Profiler" => crate::domains::profiler::try_start_profiler_command_dispatch(self, &cmd) .map(|step| match step { crate::domains::runtime::RuntimeCommandTaskStep::Pending(pending) => { - self.pending_step(PendingCdpCommandDispatchKind::Runtime(pending)) + self.pending_step( + command_owner.clone(), + PendingCdpCommandDispatchKind::Runtime(pending), + ) } crate::domains::runtime::RuntimeCommandTaskStep::Complete(plan) => { self.complete_with_output_plan(command_context, plan, cmd.id, cmd.session_id) @@ -571,7 +799,10 @@ impl CdpConnection { ) .map(|step| match step { crate::domains::runtime::RuntimeCommandTaskStep::Pending(pending) => { - self.pending_step(PendingCdpCommandDispatchKind::Runtime(pending)) + self.pending_step( + command_owner.clone(), + PendingCdpCommandDispatchKind::Runtime(pending), + ) } crate::domains::runtime::RuntimeCommandTaskStep::Complete(plan) => { self.complete_with_output_plan( @@ -589,7 +820,10 @@ impl CdpConnection { match step { crate::domains::accessibility::AccessibilityCommandDispatchStep::Pending( pending, - ) => self.pending_step(PendingCdpCommandDispatchKind::Accessibility(pending)), + ) => self.pending_step( + command_owner.clone(), + PendingCdpCommandDispatchKind::Accessibility(pending), + ), crate::domains::accessibility::AccessibilityCommandDispatchStep::Complete( plan, ) => self.complete_with_output_plan(command_context, plan, cmd.id, cmd.session_id), @@ -599,7 +833,10 @@ impl CdpConnection { "Input" => Some( match crate::domains::input::try_start_input_command_dispatch(self, &cmd) { crate::domains::input::InputCommandDispatchStep::Pending(pending) => { - self.pending_step(PendingCdpCommandDispatchKind::Input(pending)) + self.pending_step( + command_owner.clone(), + PendingCdpCommandDispatchKind::Input(pending), + ) } crate::domains::input::InputCommandDispatchStep::Complete(plan) => { self.complete_with_output_plan(command_context, plan, cmd.id, cmd.session_id) @@ -609,7 +846,10 @@ impl CdpConnection { "CSS" => crate::domains::css::try_start_css_command_dispatch(self, &cmd).map(|step| { match step { crate::domains::css::CssCommandDispatchStep::Pending(pending) => { - self.pending_step(PendingCdpCommandDispatchKind::Css(pending)) + self.pending_step( + command_owner.clone(), + PendingCdpCommandDispatchKind::Css(pending), + ) } crate::domains::css::CssCommandDispatchStep::Complete(plan) => { self.complete_with_output_plan(command_context, plan, cmd.id, cmd.session_id) @@ -619,7 +859,10 @@ impl CdpConnection { "DOM" => crate::domains::dom::try_start_dom_command_dispatch(self, &cmd).map(|step| { match step { crate::domains::dom::DomCommandDispatchStep::Pending(pending) => { - self.pending_step(PendingCdpCommandDispatchKind::Dom(*pending)) + self.pending_step( + command_owner.clone(), + PendingCdpCommandDispatchKind::Dom(*pending), + ) } crate::domains::dom::DomCommandDispatchStep::Complete(plan) => { self.complete_with_output_plan(command_context, plan, cmd.id, cmd.session_id) @@ -631,7 +874,10 @@ impl CdpConnection { self, &cmd, ) { crate::domains::dom_storage::DomStorageCommandTaskStep::Pending(pending) => { - self.pending_step(PendingCdpCommandDispatchKind::DomStorage(pending)) + self.pending_step( + command_owner.clone(), + PendingCdpCommandDispatchKind::DomStorage(pending), + ) } crate::domains::dom_storage::DomStorageCommandTaskStep::Complete(plan) => { self.complete_with_output_plan(command_context, plan, cmd.id, cmd.session_id) @@ -645,7 +891,10 @@ impl CdpConnection { ) .map(|step| match step { crate::domains::runtime::RuntimeCommandTaskStep::Pending(pending) => { - self.pending_step(PendingCdpCommandDispatchKind::Runtime(pending)) + self.pending_step( + command_owner.clone(), + PendingCdpCommandDispatchKind::Runtime(pending), + ) } crate::domains::runtime::RuntimeCommandTaskStep::Complete(plan) => { self.complete_with_output_plan(command_context, plan, cmd.id, cmd.session_id) @@ -655,7 +904,7 @@ impl CdpConnection { } "Emulation" | "Network" if let Some(step) = crate::domains::runtime::try_start_worker_emulation_command_dispatch(self, &cmd) => { Some(match step { - crate::domains::runtime::RuntimeCommandTaskStep::Pending(pending) => self.pending_step(PendingCdpCommandDispatchKind::Runtime(pending)), + crate::domains::runtime::RuntimeCommandTaskStep::Pending(pending) => self.pending_step(command_owner.clone(), PendingCdpCommandDispatchKind::Runtime(pending)), crate::domains::runtime::RuntimeCommandTaskStep::Complete(plan) => self.complete_with_output_plan(command_context, plan, cmd.id, cmd.session_id), }) } @@ -663,13 +912,19 @@ impl CdpConnection { match crate::domains::network::start_network_domain_command_dispatch(self, &cmd) { crate::domains::network::NetworkDomainCommandTaskStep::Network( crate::domains::network::NetworkCommandTaskStep::Pending(pending), - ) => self.pending_step(PendingCdpCommandDispatchKind::Network(pending)), + ) => self.pending_step( + command_owner.clone(), + PendingCdpCommandDispatchKind::Network(pending), + ), crate::domains::network::NetworkDomainCommandTaskStep::Network( crate::domains::network::NetworkCommandTaskStep::Complete(plan), ) => self.complete_with_output_plan(command_context, plan, cmd.id, cmd.session_id), crate::domains::network::NetworkDomainCommandTaskStep::Storage( crate::domains::storage::StorageCommandTaskStep::Pending(pending), - ) => self.pending_step(PendingCdpCommandDispatchKind::Storage(pending)), + ) => self.pending_step( + command_owner.clone(), + PendingCdpCommandDispatchKind::Storage(pending), + ), crate::domains::network::NetworkDomainCommandTaskStep::Storage( crate::domains::storage::StorageCommandTaskStep::Complete(plan), ) => self.complete_with_output_plan(command_context, plan, cmd.id, cmd.session_id), @@ -682,7 +937,10 @@ impl CdpConnection { crate::domains::target::try_start_target_command_dispatch(self, &cmd).map(|step| { match step { crate::domains::target::TargetCommandTaskStep::Pending(pending) => { - self.pending_step(PendingCdpCommandDispatchKind::Target(pending)) + self.pending_step( + command_owner.clone(), + PendingCdpCommandDispatchKind::Target(pending), + ) } crate::domains::target::TargetCommandTaskStep::Complete(plan) => { self.complete_with_output_plan(command_context, plan, cmd.id, cmd.session_id) @@ -693,7 +951,10 @@ impl CdpConnection { "Tracing" => Some( match crate::domains::tracing::try_start_tracing_command_dispatch(self, &cmd) { crate::domains::tracing::TracingCommandTaskStep::Pending(pending) => { - self.pending_step(PendingCdpCommandDispatchKind::Tracing(*pending)) + self.pending_step( + command_owner.clone(), + PendingCdpCommandDispatchKind::Tracing(*pending), + ) } crate::domains::tracing::TracingCommandTaskStep::Complete(plan) => self .complete_with_output_plan( @@ -708,7 +969,10 @@ impl CdpConnection { crate::domains::fetch::try_start_fetch_command_dispatch(self, &cmd).map(|step| { match step { crate::domains::fetch::FetchCommandTaskStep::Pending(pending) => { - self.pending_step(PendingCdpCommandDispatchKind::Fetch(pending)) + self.pending_step( + command_owner.clone(), + PendingCdpCommandDispatchKind::Fetch(pending), + ) } crate::domains::fetch::FetchCommandTaskStep::Complete(plan) => { self.complete_with_output_plan(command_context, plan, cmd.id, cmd.session_id) @@ -720,7 +984,10 @@ impl CdpConnection { crate::domains::page::try_start_page_command_dispatch(self, &cmd).map(|step| { match step { crate::domains::page::PageCommandTaskStep::Pending(pending) => { - self.pending_step(PendingCdpCommandDispatchKind::Page(pending)) + self.pending_step( + command_owner.clone(), + PendingCdpCommandDispatchKind::Page(pending), + ) } crate::domains::page::PageCommandTaskStep::Complete(plan) => { self.complete_with_output_plan(command_context, plan, cmd.id, cmd.session_id) @@ -739,7 +1006,10 @@ impl CdpConnection { "Storage" => Some( match crate::domains::storage::try_start_storage_command_dispatch(self, &cmd) { crate::domains::storage::StorageCommandTaskStep::Pending(pending) => { - self.pending_step(PendingCdpCommandDispatchKind::Storage(pending)) + self.pending_step( + command_owner.clone(), + PendingCdpCommandDispatchKind::Storage(pending), + ) } crate::domains::storage::StorageCommandTaskStep::Complete(plan) => { self.complete_with_output_plan(command_context, plan, cmd.id, cmd.session_id) @@ -753,7 +1023,10 @@ impl CdpConnection { ) { crate::domains::dom_snapshot::DomSnapshotCommandDispatchStep::Pending( pending, - ) => self.pending_step(PendingCdpCommandDispatchKind::DomSnapshot(pending)), + ) => self.pending_step( + command_owner.clone(), + PendingCdpCommandDispatchKind::DomSnapshot(pending), + ), crate::domains::dom_snapshot::DomSnapshotCommandDispatchStep::Complete( plan, ) => self.complete_with_output_plan( @@ -768,7 +1041,10 @@ impl CdpConnection { "Security" => Some( match crate::domains::security::try_start_security_command_dispatch(self, &cmd) { crate::domains::security::SecurityCommandTaskStep::Pending(pending) => { - self.pending_step(PendingCdpCommandDispatchKind::Security(pending)) + self.pending_step( + command_owner.clone(), + PendingCdpCommandDispatchKind::Security(pending), + ) } crate::domains::security::SecurityCommandTaskStep::Complete(plan) => { self.complete_with_output_plan(command_context, plan, cmd.id, cmd.session_id) @@ -782,7 +1058,10 @@ impl CdpConnection { "IO" => Some( match crate::domains::io::try_start_io_command_dispatch(self, &cmd) { crate::domains::io::IoCommandTaskStep::Pending(pending) => { - self.pending_step(PendingCdpCommandDispatchKind::Io(pending)) + self.pending_step( + command_owner.clone(), + PendingCdpCommandDispatchKind::Io(pending), + ) } crate::domains::io::IoCommandTaskStep::Complete(plan) => { self.complete_with_output_plan(command_context, plan, cmd.id, cmd.session_id) @@ -792,7 +1071,10 @@ impl CdpConnection { "Autofill" => Some( match crate::domains::autofill::try_start_autofill_command_dispatch(self, &cmd) { crate::domains::autofill::AutofillCommandTaskStep::Pending(pending) => { - self.pending_step(PendingCdpCommandDispatchKind::Autofill(pending)) + self.pending_step( + command_owner.clone(), + PendingCdpCommandDispatchKind::Autofill(pending), + ) } crate::domains::autofill::AutofillCommandTaskStep::Complete(plan) => { self.complete_with_output_plan( @@ -826,7 +1108,10 @@ impl CdpConnection { &cmd, ) { crate::domains::performance::PerformanceCommandTaskStep::Pending(pending) => { - self.pending_step(PendingCdpCommandDispatchKind::Performance(pending)) + self.pending_step( + command_owner.clone(), + PendingCdpCommandDispatchKind::Performance(pending), + ) } crate::domains::performance::PerformanceCommandTaskStep::Complete(plan) => { self.complete_with_output_plan(command_context, plan, cmd.id, cmd.session_id) @@ -838,7 +1123,10 @@ impl CdpConnection { self, &cmd, ) { crate::domains::dom_debugger::DomDebuggerCommandTaskStep::Pending(pending) => { - self.pending_step(PendingCdpCommandDispatchKind::DomDebugger(pending)) + self.pending_step( + command_owner.clone(), + PendingCdpCommandDispatchKind::DomDebugger(pending), + ) } crate::domains::dom_debugger::DomDebuggerCommandTaskStep::Complete(plan) => { self.complete_with_output_plan(command_context, plan, cmd.id, cmd.session_id) @@ -850,7 +1138,10 @@ impl CdpConnection { ) .map(|step| match step { crate::domains::emulation::EmulationCommandTaskStep::Pending(pending) => { - self.pending_step(PendingCdpCommandDispatchKind::Emulation(pending)) + self.pending_step( + command_owner.clone(), + PendingCdpCommandDispatchKind::Emulation(pending), + ) } crate::domains::emulation::EmulationCommandTaskStep::Complete(plan) => { self.complete_with_output_plan(command_context, plan, cmd.id, cmd.session_id) @@ -884,15 +1175,17 @@ impl CdpConnection { let mut command_context = CommandDispatchContext::default(); self.complete_pending_command_dispatch_with_context(completed, &mut command_context) .await + .into() } pub async fn complete_pending_command_dispatch_with_context( &mut self, completed: CompletedCdpCommandDispatch, command_context: &mut CommandDispatchContext, - ) -> CdpCommandTaskStep { + ) -> AgentHostDispatchResult { + let CompletedCdpCommandDispatch { inner, owner_scope } = completed; let mut out = Vec::new(); - match completed.inner { + match inner { CompletedCdpCommandDispatchKind::Runtime(completed) => { let completed = *completed; let command_id = completed.command_id(); @@ -906,7 +1199,10 @@ impl CdpConnection { .await { crate::domains::runtime::RuntimeCommandTaskStep::Pending(pending) => { - return self.pending_step(PendingCdpCommandDispatchKind::Runtime(pending)); + return self.pending_step( + owner_scope.clone(), + PendingCdpCommandDispatchKind::Runtime(pending), + ); } crate::domains::runtime::RuntimeCommandTaskStep::Complete(plan) => { append_command_output_plan( @@ -937,8 +1233,10 @@ impl CdpConnection { crate::domains::accessibility::AccessibilityCommandDispatchStep::Pending( pending, ) => { - return self - .pending_step(PendingCdpCommandDispatchKind::Accessibility(pending)); + return self.pending_step( + owner_scope.clone(), + PendingCdpCommandDispatchKind::Accessibility(pending), + ); } crate::domains::accessibility::AccessibilityCommandDispatchStep::Complete( plan, @@ -980,7 +1278,10 @@ impl CdpConnection { let session_id = completed.session_id().map(str::to_owned); match crate::domains::css::complete_pending_css_command(self, completed) { crate::domains::css::CssCommandDispatchStep::Pending(pending) => { - return self.pending_step(PendingCdpCommandDispatchKind::Css(pending)); + return self.pending_step( + owner_scope.clone(), + PendingCdpCommandDispatchKind::Css(pending), + ); } crate::domains::css::CssCommandDispatchStep::Complete(plan) => { append_command_output_plan( @@ -1009,7 +1310,10 @@ impl CdpConnection { ); match step { crate::domains::dom::DomCommandTaskStep::Pending(pending) => { - return self.pending_step(PendingCdpCommandDispatchKind::Dom(*pending)); + return self.pending_step( + owner_scope.clone(), + PendingCdpCommandDispatchKind::Dom(*pending), + ); } crate::domains::dom::DomCommandTaskStep::Complete => {} } @@ -1029,8 +1333,10 @@ impl CdpConnection { self, *completed, ) { crate::domains::dom_storage::DomStorageCommandTaskStep::Pending(pending) => { - return self - .pending_step(PendingCdpCommandDispatchKind::DomStorage(pending)); + return self.pending_step( + owner_scope.clone(), + PendingCdpCommandDispatchKind::DomStorage(pending), + ); } crate::domains::dom_storage::DomStorageCommandTaskStep::Complete(plan) => { append_command_output_plan( @@ -1052,8 +1358,10 @@ impl CdpConnection { crate::domains::dom_snapshot::DomSnapshotCommandDispatchStep::Pending( pending, ) => { - return self - .pending_step(PendingCdpCommandDispatchKind::DomSnapshot(pending)); + return self.pending_step( + owner_scope.clone(), + PendingCdpCommandDispatchKind::DomSnapshot(pending), + ); } crate::domains::dom_snapshot::DomSnapshotCommandDispatchStep::Complete( plan, @@ -1079,7 +1387,10 @@ impl CdpConnection { .await { crate::domains::page::PageCommandTaskStep::Pending(pending) => { - return self.pending_step(PendingCdpCommandDispatchKind::Page(pending)); + return self.pending_step( + owner_scope.clone(), + PendingCdpCommandDispatchKind::Page(pending), + ); } crate::domains::page::PageCommandTaskStep::Complete(plan) => { append_command_output_plan( @@ -1138,7 +1449,10 @@ impl CdpConnection { let session_id = completed.session_id().map(str::to_owned); match crate::domains::network::complete_pending_network_command(self, completed) { crate::domains::network::NetworkCommandTaskStep::Pending(pending) => { - return self.pending_step(PendingCdpCommandDispatchKind::Network(pending)); + return self.pending_step( + owner_scope.clone(), + PendingCdpCommandDispatchKind::Network(pending), + ); } crate::domains::network::NetworkCommandTaskStep::Complete(plan) => { append_command_output_plan( @@ -1216,7 +1530,10 @@ impl CdpConnection { .await { crate::domains::target::TargetCommandTaskStep::Pending(pending) => { - return self.pending_step(PendingCdpCommandDispatchKind::Target(pending)); + return self.pending_step( + owner_scope.clone(), + PendingCdpCommandDispatchKind::Target(pending), + ); } crate::domains::target::TargetCommandTaskStep::Complete(plan) => { append_command_output_plan( @@ -1242,7 +1559,7 @@ impl CdpConnection { command_context.take_renderer_fenced_protocol_events(); let post_response_events = command_context.take_post_response_events(); let scheduler_events = self.take_scheduler_events(); - CdpCommandTaskStep::Complete( + AgentHostDispatchResult::Complete( CdpTurnOutcome::new_with_protocol_and_post_response_events( out, post_response_events, @@ -1334,19 +1651,21 @@ impl CdpConnection { if let Some(response_delivery) = response_delivery_override { command_context.set_terminal_response_delivery_override(response_delivery); } - let mut step = match command.as_ref() { - Ok(command) => { - self.start_parsed_command_dispatch_with_context(command, &mut command_context) - } - Err(error) => self.complete_with_output_plan( - &mut command_context, - CommandOutputPlan::error_without_session( - error.response_code(), - error.response_message(), - ), - error.command_id(), - None, - ), + let mut step: CdpCommandTaskStep = match command.as_ref() { + Ok(command) => self + .start_parsed_command_dispatch_with_context(command, &mut command_context) + .into(), + Err(error) => self + .complete_with_output_plan( + &mut command_context, + CommandOutputPlan::error_without_session( + error.response_code(), + error.response_message(), + ), + error.command_id(), + None, + ) + .into(), }; loop { match step { @@ -1379,7 +1698,8 @@ impl CdpConnection { completed, &mut command_context, )) - .await; + .await + .into(); } } } diff --git a/moli-protocol/src/conn/dispatch_tests.rs b/moli-protocol/src/conn/dispatch_tests.rs index b32ce4fdb5..2e36736cc9 100644 --- a/moli-protocol/src/conn/dispatch_tests.rs +++ b/moli-protocol/src/conn/dispatch_tests.rs @@ -9104,3 +9104,207 @@ fn command_dispatch_completes_additional_sync_domain_unknown_methods_without_leg ); } } + +async fn loaded_agent_host_dispatch_connection_for_test() -> CdpConnection { + let mut conn = CdpConnection::new(); + let mut browser_context = BrowserContext::new("BID-agent-host-dispatch".to_owned()); + browser_context.set_active_target_id("TID-agent-host-dispatch".to_owned()); + conn.install_browser_context_fixture_for_test(browser_context); + let page = conn + .load_page_via_runtime_async("data:text/html,

agent host dispatch

") + .await + .expect("agent host dispatch page should load"); + conn.browser_context + .as_mut() + .expect("browser context") + .replace_active_page_for_test(Some(page)); + conn +} + +fn parsed_agent_host_command(id: u64, method: &str, params: Value) -> ParsedCdpCommand { + ParsedCdpCommand::parse_str( + serde_json::to_string(&json!({ + "id": id, + "method": method, + "params": params, + })) + .expect("agent host command should serialize"), + ) + .expect("agent host command should parse") +} + +#[tokio::test] +async fn agent_host_dispatch_exposes_only_actual_renderer_fallthrough_binding() { + let mut conn = loaded_agent_host_dispatch_connection_for_test().await; + let owner = CommandOwnerScope::capture(&conn, None); + let expected_document = conn + .current_document_id_for_owner(&owner) + .expect("loaded page should have a current Document"); + let expected_main_frame_slot = conn + .page_agent_host_main_frame_slot_for_owner(&owner) + .expect("loaded Page AgentHost should retain its main-frame slot"); + let expected_attachment = conn + .current_renderer_agent_attachment_id_for_owner(&owner) + .expect("loaded page should have a renderer attachment"); + + let evaluate = + parsed_agent_host_command(20_001, "Runtime.evaluate", json!({"expression": "1 + 1"})); + let mut command_context = CommandDispatchContext::default(); + let dispatch = conn.start_parsed_command_dispatch_with_context(&evaluate, &mut command_context); + let AgentHostDispatchResult::FallThrough(dispatch) = dispatch else { + panic!("Runtime.evaluate should fall through to the renderer"); + }; + assert_eq!(dispatch.lane(), RendererDispatchLane::Main); + assert_eq!( + dispatch.binding(), + &RendererDispatchBinding::Page(RendererPageDispatchBinding { + main_frame_slot: expected_main_frame_slot, + document: expected_document, + attachment: expected_attachment, + }) + ); + let completed = dispatch.into_pending().wait().await; + assert!(matches!( + conn.complete_pending_command_dispatch_with_context(completed, &mut command_context) + .await, + AgentHostDispatchResult::Complete(_) + )); + + let terminate = parsed_agent_host_command(20_002, "Runtime.terminateExecution", json!({})); + let mut command_context = CommandDispatchContext::default(); + let dispatch = + conn.start_parsed_command_dispatch_with_context(&terminate, &mut command_context); + let AgentHostDispatchResult::FallThrough(dispatch) = dispatch else { + panic!("Runtime.terminateExecution should fall through to renderer IO"); + }; + assert_eq!(dispatch.lane(), RendererDispatchLane::Io); + assert_eq!( + dispatch.binding(), + &RendererDispatchBinding::Page(RendererPageDispatchBinding { + main_frame_slot: expected_main_frame_slot, + document: expected_document, + attachment: expected_attachment, + }) + ); + let completed = dispatch.into_pending().wait().await; + assert!(matches!( + conn.complete_pending_command_dispatch_with_context(completed, &mut command_context) + .await, + AgentHostDispatchResult::Complete(_) + )); + + let screenshot = parsed_agent_host_command(20_003, "Page.captureScreenshot", json!({})); + let mut command_context = CommandDispatchContext::default(); + let dispatch = + conn.start_parsed_command_dispatch_with_context(&screenshot, &mut command_context); + match dispatch { + AgentHostDispatchResult::PendingService(pending) => { + let completed = pending.wait().await; + assert!(matches!( + conn.complete_pending_command_dispatch_with_context( + completed, + &mut command_context, + ) + .await, + AgentHostDispatchResult::Complete(_) + )); + } + AgentHostDispatchResult::Complete(_) => {} + AgentHostDispatchResult::FallThrough(_) => { + panic!("native Page.captureScreenshot must not become renderer fallthrough") + } + } + + let permission = parsed_agent_host_command( + 20_004, + "Browser.setPermission", + json!({ + "permission": {"name": "geolocation"}, + "setting": "denied" + }), + ); + let mut command_context = CommandDispatchContext::default(); + let dispatch = + conn.start_parsed_command_dispatch_with_context(&permission, &mut command_context); + let AgentHostDispatchResult::PendingService(pending) = dispatch else { + panic!("live Browser.setPermission should be a service continuation"); + }; + let completed = pending.wait().await; + assert!(matches!( + conn.complete_pending_command_dispatch_with_context(completed, &mut command_context) + .await, + AgentHostDispatchResult::Complete(_) + )); +} + +#[tokio::test] +async fn document_projection_gate_uses_handler_disposition_not_wire_method_lane() { + let mut conn = loaded_agent_host_dispatch_connection_for_test().await; + let owner = CommandOwnerScope::capture(&conn, None); + let navigation = conn + .start_document_navigation_for_owner(&owner, "LOADER-agent-host-gate".to_owned()) + .expect("cross-Document navigation should start"); + + for (id, method, params) in [ + (20_101, "Runtime.evaluate", json!({"expression": "1"})), + (20_102, "Debugger.enable", json!({})), + (20_103, "Console.enable", json!({})), + (20_104, "Profiler.enable", json!({})), + (20_105, "HeapProfiler.enable", json!({})), + (20_106, "Accessibility.getFullAXTree", json!({})), + (20_107, "CSS.enable", json!({})), + (20_108, "DOM.getDocument", json!({})), + ( + 20_109, + "DOMDebugger.getEventListeners", + json!({"objectId": "1"}), + ), + (20_110, "DOMSnapshot.captureSnapshot", json!({})), + (20_111, "Page.getFrameTree", json!({})), + (20_112, "Page.getLayoutMetrics", json!({})), + ] { + let command = parsed_agent_host_command(id, method, params); + assert!( + conn.command_waits_for_document_projection(&command), + "{method} must bind only after the replacement Document is projected" + ); + } + + for (id, method, params) in [ + (20_201, "Runtime.terminateExecution", json!({})), + ( + 20_202, + "Runtime.addBinding", + json!({"name": "duringNavigation"}), + ), + (20_203, "Debugger.pause", json!({})), + (20_204, "Performance.getMetrics", json!({})), + ( + 20_205, + "Emulation.setScriptExecutionDisabled", + json!({"value": true}), + ), + (20_206, "Page.captureScreenshot", json!({})), + (20_207, "Page.printToPDF", json!({})), + (20_208, "Page.searchInResource", json!({})), + ( + 20_209, + "Input.dispatchKeyEvent", + json!({"type": "keyDown", "key": "a"}), + ), + (20_210, "Browser.setPermission", json!({})), + (20_211, "HeapProfiler.moliDiagnostics", json!({})), + (20_212, "Accessibility.enable", json!({})), + (20_213, "DOM.enable", json!({})), + (20_214, "DOMSnapshot.enable", json!({})), + ] { + let command = parsed_agent_host_command(id, method, params); + assert!( + !conn.command_waits_for_document_projection(&command), + "{method} must remain service/native/IO-dispatchable during projection" + ); + } + + let _ = conn.finish_navigation_without_document_projection_for_owner(&owner, &navigation); + conn.clear_pending_document_navigation_for_owner_if_matches(&owner, &navigation); +} diff --git a/moli-protocol/src/conn/inspector_route.rs b/moli-protocol/src/conn/inspector_route.rs index fda585ca0e..6b77130171 100644 --- a/moli-protocol/src/conn/inspector_route.rs +++ b/moli-protocol/src/conn/inspector_route.rs @@ -2,7 +2,10 @@ use moli_core::page::{ DevToolsSessionKey, RendererAgentAttachmentId, RendererRuntimeInspectorMessageBatch, }; -use super::state::{FinishedRendererDocumentNavigation, RendererAgentAttachment}; +use super::state::{ + CommittedRendererDocumentBinding, DocumentProjectionFence, DocumentProjectionOutputRelease, + RendererAgentAttachment, +}; use super::{CdpConnection, CommandOwnerScope, NavigationId}; impl CdpConnection { @@ -21,10 +24,17 @@ impl CdpConnection { &self, owner: &CommandOwnerScope, ) -> Option { + self.current_renderer_agent_attachment_for_owner(owner) + .map(RendererAgentAttachment::id) + } + + pub(crate) fn current_renderer_agent_attachment_for_owner( + &self, + owner: &CommandOwnerScope, + ) -> Option { self.runtime_session_owner_slot_for_owner(owner) .ok() .and_then(|slot| slot.current_renderer_attachment()) - .map(RendererAgentAttachment::id) } pub(crate) fn route_current_renderer_inspector_output_for_owner( @@ -116,11 +126,11 @@ impl CdpConnection { } } - pub(crate) fn finish_renderer_document_navigation_for_owner( + pub(crate) fn finish_navigation_without_document_projection_for_owner( &mut self, owner: &CommandOwnerScope, token: &NavigationId, - ) -> Option { + ) -> Option { if self .validate_navigation_target_owner_for_scope(owner, token) .is_err() @@ -130,7 +140,7 @@ impl CdpConnection { match self .runtime_session_owner_slot_mut_for_owner(owner) .and_then(|slot| { - slot.finish_renderer_document_navigation(token) + slot.finish_navigation_without_document_projection(token) .map_err(|error| error.to_string()) }) { Ok(finish) => Some(finish), @@ -146,6 +156,40 @@ impl CdpConnection { } } + pub(crate) fn publish_document_projection_fence_for_owner( + &mut self, + owner: &CommandOwnerScope, + projected: &CommittedRendererDocumentBinding, + fence: DocumentProjectionFence, + ) -> DocumentProjectionOutputRelease { + let current_attachment = self + .current_renderer_agent_attachment_for_owner(owner) + .expect("a rebound Document projection must retain its renderer attachment"); + assert_eq!( + ( + projected.document_id, + projected.browser_sequence, + current_attachment.id(), + current_attachment.document(), + current_attachment.browser_sequence(), + ), + ( + fence.document(), + fence.browser_sequence(), + fence.renderer_attachment(), + fence.document(), + fence.browser_sequence(), + ), + "a Document projection fence requires its exact frame and renderer binding" + ); + self.runtime_session_owner_slot_mut_for_owner(owner) + .and_then(|slot| { + slot.publish_document_projection_fence(fence) + .map_err(|error| error.to_string()) + }) + .expect("an exact Document projection fence must publish once") + } + fn filter_renderer_inspector_batches_for_target_owner( &self, owner: &CommandOwnerScope, @@ -389,11 +433,11 @@ mod tests { assert!(!conn.clear_pending_document_navigation_for_owner_if_matches(&owner, &first)); assert!(conn.accepts_pending_document_navigation_for_owner(&owner, &second)); assert!( - conn.finish_renderer_document_navigation_for_owner(&owner, &first) + conn.finish_navigation_without_document_projection_for_owner(&owner, &first) .is_none() ); assert!( - conn.finish_renderer_document_navigation_for_owner(&owner, &second) + conn.finish_navigation_without_document_projection_for_owner(&owner, &second) .is_some() ); } diff --git a/moli-protocol/src/conn/protocol_output.rs b/moli-protocol/src/conn/protocol_output.rs index 9db93fa8dd..ad8a68835e 100644 --- a/moli-protocol/src/conn/protocol_output.rs +++ b/moli-protocol/src/conn/protocol_output.rs @@ -1,7 +1,7 @@ use super::{CdpConnection, CdpTurnOutcome}; use crate::domains::activity::{ - ReadyProtocolSchedulerWork, RuntimeCommandOutputBarrierCompletion, - RuntimeCommandOutputBarrierPermit, RuntimeCommandOutputBarriers, + ReadyProtocolSchedulerWork, RendererCommandResponseCompletion, RendererCommandResponseOrder, + RendererCommandResponsePermit, }; use moli_core::RendererOutputTransportMessage; @@ -254,13 +254,13 @@ impl CdpConnection { pub async fn ingest_renderer_output_turn_async( &mut self, publication: RendererOutputTransportMessage, - barriers: &mut RuntimeCommandOutputBarriers, + order: &mut RendererCommandResponseOrder, ) -> CdpTurnOutcome { let mut command_context = super::CommandDispatchContext::default(); let protocol_events = crate::domains::activity::ingest_renderer_output_transport_async( self, publication, - barriers, + order, &mut command_context, ) .await; @@ -290,14 +290,14 @@ impl CdpConnection { ) } - pub async fn release_runtime_command_output_barrier_turn_async( + pub async fn release_renderer_command_response_permit_turn_async( &mut self, - barriers: &mut RuntimeCommandOutputBarriers, - permit: RuntimeCommandOutputBarrierPermit, - ) -> RuntimeCommandOutputBarrierCompletion { + order: &mut RendererCommandResponseOrder, + permit: RendererCommandResponsePermit, + ) -> RendererCommandResponseCompletion { let mut command_context = super::CommandDispatchContext::default(); - let terminal = barriers.release(self, permit, &mut command_context).await; - RuntimeCommandOutputBarrierCompletion::new( + let terminal = order.release(self, permit, &mut command_context).await; + RendererCommandResponseCompletion::new( terminal, CdpTurnOutcome::new_with_protocol_and_post_response_events( command_context.take_protocol_events(), @@ -307,14 +307,14 @@ impl CdpConnection { ) } - pub async fn cancel_runtime_command_output_barrier_turn_async( + pub async fn cancel_renderer_command_response_permit_turn_async( &mut self, - barriers: &mut RuntimeCommandOutputBarriers, - permit: RuntimeCommandOutputBarrierPermit, - ) -> RuntimeCommandOutputBarrierCompletion { + order: &mut RendererCommandResponseOrder, + permit: RendererCommandResponsePermit, + ) -> RendererCommandResponseCompletion { let mut command_context = super::CommandDispatchContext::default(); - let terminal = barriers.cancel(self, permit, &mut command_context).await; - RuntimeCommandOutputBarrierCompletion::new( + let terminal = order.cancel(self, permit, &mut command_context).await; + RendererCommandResponseCompletion::new( terminal, CdpTurnOutcome::new_with_protocol_and_post_response_events( command_context.take_protocol_events(), diff --git a/moli-protocol/src/conn/runtime_eval.rs b/moli-protocol/src/conn/runtime_eval.rs index b5b4f3be4f..7395af90bd 100644 --- a/moli-protocol/src/conn/runtime_eval.rs +++ b/moli-protocol/src/conn/runtime_eval.rs @@ -1,4 +1,4 @@ -use std::{collections::hash_map::Entry, future::Future, pin::Pin}; +use std::{future::Future, pin::Pin}; use moli_page_types::{FrontendCommandId, RendererCallId, RendererInspectorResponseDelivery}; use moli_protocol_cdp::CdpRendererCommandReplayDispatch; @@ -242,41 +242,6 @@ pub(crate) struct OwnerRuntimeResponse { pub(crate) struct ClaimedPendingInspectorAwait { command_id: u64, owner: CommandOwnerScope, - entry: PendingInspectorAwait, -} - -#[derive(Debug)] -pub(crate) struct ClaimedPendingInspectorAwaitOwner { - command_id: u64, - owner: CommandOwnerScope, - bidi_channel_object_group: Option, - renderer_correlation: Option, -} - -impl ClaimedPendingInspectorAwaitOwner { - fn from_claimed(claimed: &ClaimedPendingInspectorAwait) -> Self { - Self { - command_id: claimed.command_id, - owner: claimed.owner.clone(), - bidi_channel_object_group: claimed - .entry - .bidi_channel_listener() - .map(|listener| listener.channel_object_group().to_owned()), - renderer_correlation: claimed.entry.renderer_correlation(), - } - } - - fn session_id(&self) -> Option<&str> { - self.owner.session_id() - } - - fn matches_session_owner(&self, session_id: Option<&str>) -> bool { - self.session_id() == session_id - } - - fn matches_owner(&self, owner: &CommandOwnerScope) -> bool { - &self.owner == owner - } } impl OwnerRuntimeResponse { @@ -583,6 +548,10 @@ fn collect_moli_diagnostics_pending_snapshots( } impl PendingRuntimeProtocolMessageDispatch { + pub(crate) fn renderer_route(&self) -> moli_core::page::RendererInspectorCommandRoute { + self.pending.renderer_route() + } + pub async fn wait(self) -> Result { let completion = self .pending @@ -798,6 +767,33 @@ fn push_pending_inspector_await_error_background_event( )); } +fn push_drained_pending_inspector_await_error( + direct_events: &mut Vec, + claimed_events: &mut Vec, + cdp_id: u64, + owner: &CommandOwnerScope, + entry: &PendingInspectorAwait, + reason: &'static str, +) { + if entry.scheduler_deferred_reply_claimed() { + let mut response = + RuntimeInspectorResponseReady::for_owner(cdp_id, owner, Err(reason.to_owned())); + if let Some(correlation) = entry.renderer_correlation() { + response.bind_renderer_call_id(correlation.renderer_call_id()); + } + claimed_events.push(BackgroundProtocolEvent::runtime_inspector_response_ready( + response, + )); + return; + } + push_pending_inspector_await_error_background_event( + direct_events, + cdp_id, + entry.session_id(), + reason, + ); +} + fn push_terminated_renderer_call_error_background_events( out: &mut Vec, terminated: Vec, @@ -1392,35 +1388,22 @@ impl CdpConnection { ) } - pub(crate) fn register_runtime_await_job_for_owner( + pub(crate) fn trace_runtime_await_started( &mut self, cdp_request_id: u64, owner: &CommandOwnerScope, object_group: Option<&str>, action: &'static str, ) { - let session_id = owner.session_id(); - let job = RuntimeAwaitJob::new(cdp_request_id, owner, object_group, action); - let trace_fields = job.trace_fields(); - let key = PendingRendererCommandKey::new(session_id, cdp_request_id); - match self.pending_runtime_await_jobs.entry(key) { - Entry::Vacant(entry) => { - entry.insert(job); - } - Entry::Occupied(_) => { - tracing::error!( - cdp_request_id, - session_id, - "runtime await trace job already exists for frontend command" - ); - return; - } - } self.record_runtime_await_trace( - "runtime_await_job_start", + "runtime_await_started", Some(cdp_request_id), - session_id, - trace_fields, + owner.session_id(), + json!({ + "ownerRoute": owner.explicit_route().map(|route| format!("{route:?}")), + "objectGroup": object_group, + "action": action, + }), ); } @@ -1433,7 +1416,7 @@ impl CdpConnection { "runtime_await_pending_registered", Some(cdp_request_id), session_id, - self.runtime_await_job_trace_fields(cdp_request_id, session_id), + json!({}), ); } @@ -1451,66 +1434,37 @@ impl CdpConnection { json!({ "messages": messages, "matchingResponseSeen": saw_current_response, - "job": cdp_request_id - .map(|id| self.runtime_await_job_trace_fields(id, session_id)), }), ); } - pub(crate) fn complete_runtime_await_job( + pub(crate) fn trace_runtime_await_completed( &mut self, cdp_request_id: u64, session_id: Option<&str>, ) { - let key = PendingRendererCommandKey::new(session_id, cdp_request_id); - let Some(job) = self.pending_runtime_await_jobs.remove(&key) else { - return; - }; - let session_id = job.session_id(); - let fields = job.trace_fields(); self.record_runtime_await_trace( "runtime_await_completed", Some(cdp_request_id), - session_id.as_deref(), - fields, + session_id, + json!({}), ); } - pub(crate) fn cancel_runtime_await_job( + pub(crate) fn trace_runtime_await_cancelled( &mut self, cdp_request_id: u64, session_id: Option<&str>, reason: &'static str, ) { - let key = PendingRendererCommandKey::new(session_id, cdp_request_id); - let Some(job) = self.pending_runtime_await_jobs.remove(&key) else { - return; - }; - let session_id = job.session_id(); - let mut fields = job.trace_fields(); - if let Some(object) = fields.as_object_mut() { - object.insert("reason".to_owned(), json!(reason)); - } self.record_runtime_await_trace( "runtime_await_cancelled", Some(cdp_request_id), - session_id.as_deref(), - fields, + session_id, + json!({ "reason": reason }), ); } - fn runtime_await_job_trace_fields( - &self, - cdp_request_id: u64, - session_id: Option<&str>, - ) -> Value { - let key = PendingRendererCommandKey::new(session_id, cdp_request_id); - self.pending_runtime_await_jobs - .get(&key) - .map(RuntimeAwaitJob::trace_fields) - .unwrap_or_else(|| json!({})) - } - pub(crate) fn runtime_await_owner_route_for_session( &self, session_id: Option<&str>, @@ -1623,8 +1577,12 @@ impl CdpConnection { cdp_request_id: u64, session_id: Option<&str>, ) { - self.cancel_runtime_await_job(cdp_request_id, session_id, "forgotten"); - let _ = self.remove_pending_inspector_await_for_cancellation(cdp_request_id, session_id); + if self + .remove_pending_inspector_await_for_cancellation(cdp_request_id, session_id) + .is_some() + { + self.trace_runtime_await_cancelled(cdp_request_id, session_id, "forgotten"); + } } pub(crate) fn forget_pending_inspector_await_for_owner( @@ -1632,9 +1590,12 @@ impl CdpConnection { cdp_request_id: u64, owner: &CommandOwnerScope, ) { - self.cancel_runtime_await_job(cdp_request_id, owner.session_id(), "forgotten"); - let _ = - self.remove_pending_inspector_await_for_cancellation_for_owner(cdp_request_id, owner); + if self + .remove_pending_inspector_await_for_cancellation_for_owner(cdp_request_id, owner) + .is_some() + { + self.trace_runtime_await_cancelled(cdp_request_id, owner.session_id(), "forgotten"); + } } pub(crate) fn claim_pending_inspector_await_for_scheduler_deferred_reply( @@ -1642,100 +1603,11 @@ impl CdpConnection { cdp_request_id: u64, owner: &CommandOwnerScope, ) -> Option { - let claimed = self - .remove_pending_inspector_await_for_owner(cdp_request_id, owner) - .map(|entry| ClaimedPendingInspectorAwait { + self.claim_pending_inspector_await_for_owner(cdp_request_id, owner) + .then(|| ClaimedPendingInspectorAwait { command_id: cdp_request_id, owner: owner.clone(), - entry, - })?; - let key = PendingRendererCommandKey::new(owner.session_id(), cdp_request_id); - match self.claimed_pending_inspector_await_owners.entry(key) { - Entry::Vacant(entry) => { - entry.insert(ClaimedPendingInspectorAwaitOwner::from_claimed(&claimed)); - } - Entry::Occupied(_) => { - panic!("claimed pending Inspector await owner must be unique per session"); - } - } - Some(claimed) - } - - fn remove_claimed_pending_inspector_await_owner( - &mut self, - cdp_request_id: u64, - session_id: Option<&str>, - ) -> Option { - let key = PendingRendererCommandKey::new(session_id, cdp_request_id); - self.claimed_pending_inspector_await_owners.remove(&key) - } - - fn drain_claimed_pending_inspector_await_owners_for_session( - &mut self, - session_id: Option<&str>, - ) -> Vec { - let to_remove = self - .claimed_pending_inspector_await_owners - .iter() - .filter_map(|(key, owner)| { - owner - .matches_session_owner(session_id) - .then_some(key.clone()) }) - .collect::>(); - to_remove - .into_iter() - .filter_map(|key| self.claimed_pending_inspector_await_owners.remove(&key)) - .collect() - } - - fn drain_claimed_pending_inspector_await_owners_for_owner( - &mut self, - owner: &CommandOwnerScope, - ) -> Vec { - let to_remove = self - .claimed_pending_inspector_await_owners - .iter() - .filter_map(|(key, claimed)| claimed.matches_owner(owner).then_some(key.clone())) - .collect::>(); - to_remove - .into_iter() - .filter_map(|key| self.claimed_pending_inspector_await_owners.remove(&key)) - .collect() - } - - fn push_claimed_pending_inspector_await_owner_errors( - &mut self, - background_events: &mut Vec, - owners: Vec, - reason: &'static str, - ) { - for owner in owners { - self.cancel_runtime_await_job(owner.command_id, owner.session_id(), reason); - if let Some(correlation) = owner.renderer_correlation { - let _ = self.take_renderer_call_for_frontend_if_matches_for_owner( - &owner.owner, - correlation.frontend_command_id().get(), - correlation.renderer_call_id(), - correlation.dispatched_attachment_id(), - ); - } - if let Some(object_group) = owner.bidi_channel_object_group.as_deref() { - self.unregister_runtime_remote_object_group_for_owner(&owner.owner, object_group); - continue; - } - let mut response = RuntimeInspectorResponseReady::for_owner( - owner.command_id, - &owner.owner, - Err(reason.to_owned()), - ); - if let Some(correlation) = owner.renderer_correlation { - response.bind_renderer_call_id(correlation.renderer_call_id()); - } - background_events.push(BackgroundProtocolEvent::runtime_inspector_response_ready( - response, - )); - } } #[cfg(test)] @@ -1743,9 +1615,18 @@ impl CdpConnection { &self, session_id: Option<&str>, ) -> bool { - self.claimed_pending_inspector_await_owners - .values() - .any(|owner| owner.matches_session_owner(session_id)) + if let Some(owner_session_id) = session_id + && let Some(target) = self.shared_worker_target_for_session(session_id) + { + return target.has_claimed_pending_inspector_awaits_for_session(owner_session_id); + } + if let Some(owner_session_id) = session_id + && let Some(target) = self.service_worker_target_for_session(session_id) + { + return target.has_claimed_pending_inspector_awaits_for_session(owner_session_id); + } + self.target_devtools_session_state_for_session(session_id) + .is_some_and(DevToolsSessionState::has_claimed_pending_inspector_awaits) } #[cfg(test)] @@ -1756,15 +1637,15 @@ impl CdpConnection { if let Some(owner_session_id) = session_id && let Some(target) = self.shared_worker_target_for_session(session_id) { - return target.has_pending_inspector_awaits_for_session(owner_session_id); + return target.has_unclaimed_pending_inspector_awaits_for_session(owner_session_id); } if let Some(owner_session_id) = session_id && let Some(target) = self.service_worker_target_for_session(session_id) { - return target.has_pending_inspector_awaits_for_session(owner_session_id); + return target.has_unclaimed_pending_inspector_awaits_for_session(owner_session_id); } self.target_devtools_session_state_for_session(session_id) - .is_some_and(DevToolsSessionState::has_pending_inspector_awaits) + .is_some_and(DevToolsSessionState::has_unclaimed_pending_inspector_awaits) } pub(crate) fn complete_claimed_pending_inspector_await_for_scheduler_deferred_reply( @@ -1775,14 +1656,12 @@ impl CdpConnection { let Some(claimed) = claimed else { return; }; - let ClaimedPendingInspectorAwait { - command_id, - owner, - entry, - } = claimed; - let session_id = owner.session_id().map(str::to_owned); - self.remove_claimed_pending_inspector_await_owner(command_id, session_id.as_deref()); - self.complete_runtime_await_job(command_id, session_id.as_deref()); + let ClaimedPendingInspectorAwait { command_id, owner } = claimed; + let Some(entry) = self.take_claimed_pending_inspector_await_for_owner(command_id, &owner) + else { + return; + }; + self.trace_runtime_await_completed(command_id, owner.session_id()); self.apply_completed_pending_inspector_await_entry(&owner, entry, protocol_events); } @@ -1794,14 +1673,12 @@ impl CdpConnection { let Some(claimed) = claimed else { return; }; - let ClaimedPendingInspectorAwait { - command_id, - owner, - entry, - } = claimed; - let session_id = owner.session_id().map(str::to_owned); - self.remove_claimed_pending_inspector_await_owner(command_id, session_id.as_deref()); - self.cancel_runtime_await_job(command_id, session_id.as_deref(), reason); + let ClaimedPendingInspectorAwait { command_id, owner } = claimed; + let Some(entry) = self.take_claimed_pending_inspector_await_for_owner(command_id, &owner) + else { + return; + }; + self.trace_runtime_await_cancelled(command_id, owner.session_id(), reason); if let Some(correlation) = entry.renderer_correlation() { let _ = self.take_renderer_call_for_frontend_if_matches_for_owner( &owner, @@ -1896,6 +1773,46 @@ impl CdpConnection { .flatten() } + fn claim_pending_inspector_await_for_owner( + &mut self, + cdp_request_id: u64, + owner: &CommandOwnerScope, + ) -> bool { + if let Some(owner_session_id) = owner.session_id() { + if let Some(target) = self.shared_worker_target_for_session_mut(owner.session_id()) { + return target.claim_pending_inspector_await(owner_session_id, cdp_request_id); + } + if let Some(target) = self.service_worker_target_for_session_mut(owner.session_id()) { + return target.claim_pending_inspector_await(owner_session_id, cdp_request_id); + } + } + self.with_target_devtools_session_state_for_owner_mut(owner, |state| { + state.claim_pending_inspector_await(cdp_request_id) + }) + .unwrap_or(false) + } + + fn take_claimed_pending_inspector_await_for_owner( + &mut self, + cdp_request_id: u64, + owner: &CommandOwnerScope, + ) -> Option { + if let Some(owner_session_id) = owner.session_id() { + if let Some(target) = self.shared_worker_target_for_session_mut(owner.session_id()) { + return target + .take_claimed_pending_inspector_await(owner_session_id, cdp_request_id); + } + if let Some(target) = self.service_worker_target_for_session_mut(owner.session_id()) { + return target + .take_claimed_pending_inspector_await(owner_session_id, cdp_request_id); + } + } + self.with_target_devtools_session_state_for_owner_mut(owner, |state| { + state.take_claimed_pending_inspector_await(cdp_request_id) + }) + .flatten() + } + fn remove_pending_inspector_await_for_cancellation( &mut self, cdp_request_id: u64, @@ -1950,9 +1867,6 @@ impl CdpConnection { } pub fn has_pending_inspector_awaits(&self) -> bool { - if !self.claimed_pending_inspector_await_owners.is_empty() { - return true; - } self.browser_contexts().any(|browser_context| { browser_context .page_targets @@ -1974,13 +1888,6 @@ impl CdpConnection { } pub fn has_pending_inspector_awaits_for_session_owner(&self, session_id: Option<&str>) -> bool { - if self - .claimed_pending_inspector_await_owners - .values() - .any(|owner| owner.matches_session_owner(session_id)) - { - return true; - } if let Some(owner_session_id) = session_id && let Some(target) = self.shared_worker_target_for_session(session_id) { @@ -1997,6 +1904,7 @@ impl CdpConnection { pub(crate) fn fail_pending_inspector_awaits_from_shared_worker_target_session_background_events_into( out: &mut Vec, + claimed_events: &mut Vec, target: &mut SharedWorkerTargetState, owner_session_id: &str, reason: &'static str, @@ -2010,10 +1918,12 @@ impl CdpConnection { ); continue; } - push_pending_inspector_await_error_background_event( + push_drained_pending_inspector_await_error( out, + claimed_events, cdp_id, - entry.session_id(), + &CommandOwnerScope::for_session(owner_session_id), + &entry, reason, ); } @@ -2029,6 +1939,7 @@ impl CdpConnection { pub(crate) fn fail_pending_inspector_awaits_from_service_worker_target_state_background_events_into( out: &mut Vec, + claimed_events: &mut Vec, target: &mut ServiceWorkerTargetState, reason: &'static str, ) { @@ -2042,10 +1953,17 @@ impl CdpConnection { } continue; } - push_pending_inspector_await_error_background_event( + let owner = CommandOwnerScope::for_session( + entry + .session_id() + .expect("service-worker await must belong to an attached session"), + ); + push_drained_pending_inspector_await_error( out, + claimed_events, cdp_id, - entry.session_id(), + &owner, + &entry, reason, ); } @@ -2066,12 +1984,9 @@ impl CdpConnection { session_id: Option<&str>, reason: &'static str, ) { - let claimed = self.drain_claimed_pending_inspector_await_owners_for_session(session_id); - self.push_claimed_pending_inspector_await_owner_errors( - claimed_background_events, - claimed, - reason, - ); + let owner = session_id + .map(CommandOwnerScope::for_session) + .unwrap_or_else(|| CommandOwnerScope::capture(self, None)); if let Some(owner_session_id) = session_id && self.shared_worker_target_for_session(session_id).is_some() { @@ -2081,7 +1996,7 @@ impl CdpConnection { .unwrap_or_default(); let mut listener_groups_to_unregister = Vec::new(); for (cdp_id, entry) in drained { - self.cancel_runtime_await_job(cdp_id, entry.session_id(), reason); + self.trace_runtime_await_cancelled(cdp_id, entry.session_id(), reason); if let Some(listener) = entry.bidi_channel_listener() { listener_groups_to_unregister.push(( entry.session_id().map(str::to_owned), @@ -2089,10 +2004,12 @@ impl CdpConnection { )); continue; } - push_pending_inspector_await_error_background_event( + push_drained_pending_inspector_await_error( out, + claimed_background_events, cdp_id, - entry.session_id(), + &owner, + &entry, reason, ); } @@ -2125,7 +2042,7 @@ impl CdpConnection { .unwrap_or_default(); let mut listener_groups_to_unregister = Vec::new(); for (cdp_id, entry) in drained { - self.cancel_runtime_await_job(cdp_id, entry.session_id(), reason); + self.trace_runtime_await_cancelled(cdp_id, entry.session_id(), reason); if let Some(listener) = entry.bidi_channel_listener() { listener_groups_to_unregister.push(( entry.session_id().map(str::to_owned), @@ -2133,10 +2050,12 @@ impl CdpConnection { )); continue; } - push_pending_inspector_await_error_background_event( + push_drained_pending_inspector_await_error( out, + claimed_background_events, cdp_id, - entry.session_id(), + &owner, + &entry, reason, ); } @@ -2166,7 +2085,7 @@ impl CdpConnection { }) .unwrap_or_default(); for (cdp_id, entry) in drained { - self.cancel_runtime_await_job(cdp_id, entry.session_id(), reason); + self.trace_runtime_await_cancelled(cdp_id, entry.session_id(), reason); if let Some(listener) = entry.bidi_channel_listener() { self.unregister_runtime_remote_object_group_for_session_owner( entry.session_id(), @@ -2174,10 +2093,12 @@ impl CdpConnection { ); continue; } - push_pending_inspector_await_error_background_event( + push_drained_pending_inspector_await_error( out, + claimed_background_events, cdp_id, - entry.session_id(), + &owner, + &entry, reason, ); } @@ -2206,19 +2127,13 @@ impl CdpConnection { return; } - let claimed = self.drain_claimed_pending_inspector_await_owners_for_owner(owner); - self.push_claimed_pending_inspector_await_owner_errors( - claimed_background_events, - claimed, - reason, - ); let drained = self .with_target_devtools_session_state_for_owner_mut(owner, |state| { state.drain_pending_inspector_awaits() }) .unwrap_or_default(); for (cdp_id, entry) in drained { - self.cancel_runtime_await_job(cdp_id, entry.session_id(), reason); + self.trace_runtime_await_cancelled(cdp_id, entry.session_id(), reason); if let Some(listener) = entry.bidi_channel_listener() { self.unregister_runtime_remote_object_group_for_owner( owner, @@ -2226,10 +2141,12 @@ impl CdpConnection { ); continue; } - push_pending_inspector_await_error_background_event( + push_drained_pending_inspector_await_error( out, + claimed_background_events, cdp_id, - entry.session_id(), + owner, + &entry, reason, ); } @@ -3740,8 +3657,12 @@ impl CdpConnection { ); } } - self.complete_runtime_await_job(frontend_command_id, session_id.as_deref()); - let _ = self.remove_pending_inspector_await_for_owner(frontend_command_id, owner); + if self + .remove_pending_inspector_await_for_owner(frontend_command_id, owner) + .is_some() + { + self.trace_runtime_await_completed(frontend_command_id, session_id.as_deref()); + } true }); } @@ -3842,7 +3763,7 @@ impl CdpConnection { background_events: &mut Vec, ) -> bool { let command_id = response.command_id; - self.complete_runtime_await_job(command_id, response.session_id()); + self.trace_runtime_await_completed(command_id, response.session_id()); self.trace_owner_runtime_response_route(&response); let current_seen = Some(command_id) == current_cmd_id; match self.route_bidi_channel_listener_owner_runtime_response(&response) { @@ -5597,8 +5518,12 @@ impl CdpConnection { }; debug_assert_eq!(resolved, correlation); let frontend_command_id = resolved.frontend_command_id().get(); - self.complete_runtime_await_job(frontend_command_id, frontend_session_id); - let _ = self.remove_pending_inspector_await(frontend_command_id, frontend_session_id); + if self + .remove_pending_inspector_await(frontend_command_id, frontend_session_id) + .is_some() + { + self.trace_runtime_await_completed(frontend_command_id, frontend_session_id); + } let mut response = json!({ "id": frontend_command_id, "error": { @@ -8071,10 +7996,6 @@ mod tests { let descriptor = if id == 71 { devtools_session_renderer_command_descriptor_for_test(id) } else { - assert_eq!( - frontend.renderer_lane(), - Some(moli_protocol_cdp::CdpRendererDispatchLane::Io) - ); RendererCommandDescriptor::set_script_execution_disabled( payload.clone(), frontend.renderer_policy(), @@ -9180,20 +9101,19 @@ mod tests { conn.register_pending_inspector_await(1, Some("SID-active")); conn.register_pending_inspector_await(1, Some("SID-bg")); - conn.register_runtime_await_job_for_owner( + conn.trace_runtime_await_started( 1, &CommandOwnerScope::for_session("SID-active"), None, "evaluate", ); - conn.register_runtime_await_job_for_owner( + conn.trace_runtime_await_started( 1, &CommandOwnerScope::for_session("SID-bg"), None, "evaluate", ); - assert_eq!(conn.pending_runtime_await_jobs.len(), 2); let claimed = conn .claim_pending_inspector_await_for_scheduler_deferred_reply( 1, @@ -9210,11 +9130,6 @@ mod tests { ); assert!(!conn.has_pending_inspector_awaits_for_session_owner(Some("SID-active"))); assert!(conn.has_pending_inspector_awaits_for_session_owner(Some("SID-bg"))); - assert_eq!(conn.pending_runtime_await_jobs.len(), 1); - assert_eq!( - conn.runtime_await_job_trace_fields(1, Some("SID-bg"))["sessionId"], - json!("SID-bg") - ); let mut response_events = Vec::new(); let mut background_events = Vec::new(); @@ -9232,7 +9147,6 @@ mod tests { assert!(response_events.is_empty()); assert!(background_events.is_empty()); assert!(conn.has_pending_inspector_awaits_for_session_owner(Some("SID-bg"))); - assert_eq!(conn.pending_runtime_await_jobs.len(), 1); let background_seen = conn.route_inspector_messages_into( vec![json!({ @@ -9253,7 +9167,53 @@ mod tests { assert_eq!(message["id"], json!(1)); assert_eq!(message["sessionId"], json!("SID-bg")); assert!(!conn.has_pending_inspector_awaits()); - assert!(conn.pending_runtime_await_jobs.is_empty()); + } + + #[test] + fn session_detach_settles_claimed_await_before_late_scheduler_completion() { + let mut conn = CdpConnection::default(); + let mut browser_context = BrowserContext::new("BID-claimed-detach".to_owned()); + browser_context.set_active_target_id("TID-claimed-detach".to_owned()); + browser_context.attach_active_session("SID-claimed-detach".to_owned()); + conn.install_browser_context_fixture_for_test(browser_context); + let owner = CommandOwnerScope::for_session("SID-claimed-detach"); + conn.try_register_pending_inspector_await_with_object_group_for_owner( + 3, + &owner, + Some("claimed-group"), + ) + .unwrap(); + let claimed = conn + .claim_pending_inspector_await_for_scheduler_deferred_reply(3, &owner) + .expect("registered await should be claimable"); + + let mut direct_events = Vec::new(); + let mut claimed_events = Vec::new(); + conn.fail_pending_inspector_awaits_for_session_owner_background_events_into( + &mut direct_events, + &mut claimed_events, + Some("SID-claimed-detach"), + "Target detached", + ); + + assert!(direct_events.is_empty()); + assert_eq!(claimed_events.len(), 1); + assert!(!conn.has_pending_inspector_awaits_for_session_owner(Some("SID-claimed-detach"))); + conn.complete_claimed_pending_inspector_await_for_scheduler_deferred_reply( + Some(claimed), + &[BackgroundProtocolEvent::command_success( + Some(3), + Some("SID-claimed-detach"), + json!({ "result": { "objectId": "late-object" } }), + )], + ); + assert!( + !conn.runtime_remote_object_id_known_for_session_owner( + Some("SID-claimed-detach"), + "late-object", + ), + "a late scheduler completion must not mutate a detached session" + ); } #[test] @@ -9472,7 +9432,7 @@ mod tests { Some("runtime-group"), ) .unwrap(); - conn.register_runtime_await_job_for_owner( + conn.trace_runtime_await_started( 77, &CommandOwnerScope::for_session("SID-bg"), Some("runtime-group"), diff --git a/moli-protocol/src/conn/state/browser_context.rs b/moli-protocol/src/conn/state/browser_context.rs index ad08bd3be7..2af69d3d31 100644 --- a/moli-protocol/src/conn/state/browser_context.rs +++ b/moli-protocol/src/conn/state/browser_context.rs @@ -33,8 +33,8 @@ use super::{ EmulatedDeviceMetrics, EmulatedGeolocationOverrideState, EmulatedNetworkConditions, }, javascript_dialog::TargetPreparedJavaScriptDialog, + page_agent_host::{PageAgentHost, PageAgentHostRegistry}, page_slot::DocumentStartScript, - page_target_host::{PageTargetHost, PageTargetRegistry}, service_worker_target::ServiceWorkerTargetState, shared_worker_target::SharedWorkerTargetState, web_contents::WebContents, @@ -68,7 +68,7 @@ pub(crate) use storage_partition::{OriginStorageUsage, SiteDataClearOptions}; /// This migration wrapper is removed at Commits 24b/30. pub struct BrowserContext { pub id: String, - pub(crate) page_targets: PageTargetRegistry, + pub(crate) page_targets: PageAgentHostRegistry, /// Test-only cookie overrides, inherited by the first page fixture. #[cfg(test)] pub(crate) default_document_cookie_manager_surface: BrowserContextCookieManagerSurface, @@ -286,13 +286,13 @@ impl BrowserContext { self.physical.id } - pub(crate) fn active_page_target(&self) -> &PageTargetHost { + pub(crate) fn active_page_target(&self) -> &PageAgentHost { self.page_targets .active(self.physical.selected_web_contents_id()) .expect("BrowserContext has no active page target") } - pub(crate) fn active_page_target_mut(&mut self) -> &mut PageTargetHost { + pub(crate) fn active_page_target_mut(&mut self) -> &mut PageAgentHost { self.page_targets .active_mut(self.physical.selected_web_contents_id()) .expect("BrowserContext has no active page target") @@ -425,7 +425,7 @@ impl BrowserContext { Self { id, - page_targets: PageTargetRegistry::default(), + page_targets: PageAgentHostRegistry::default(), #[cfg(test)] default_document_cookie_manager_surface: BrowserContextCookieManagerSurface::default(), target_popup_ids: HashMap::new(), @@ -820,7 +820,7 @@ impl BrowserContext { }) } - fn target_owner_diagnostics(&self, target: &PageTargetHost) -> Value { + fn target_owner_diagnostics(&self, target: &PageAgentHost) -> Value { let navigation = self .web_contents_for_target(target.target_id()) .expect("live WebContents") @@ -829,7 +829,7 @@ impl BrowserContext { let creator = document.creator().map(|creator| json!({ "targetId": self.page_targets.iter() .find(|target| target.web_contents_id() == creator.web_contents_id()) - .map(PageTargetHost::target_id), + .map(PageAgentHost::target_id), "securityOrigin": creator.security_origin(), "secureContextType": creator.secure_context_type(), })); @@ -1314,7 +1314,7 @@ impl BrowserContext { pub(crate) fn active_target_id(&self) -> Option<&str> { self.page_targets .get_for_web_contents(self.physical.selected_web_contents_id()?) - .map(PageTargetHost::target_id) + .map(PageAgentHost::target_id) } pub(crate) fn active_target_id_owned(&self) -> Option { @@ -1336,7 +1336,7 @@ impl BrowserContext { pub(crate) fn reported_active_user_agent_override(&self) -> Option<&str> { self.page_targets .active(self.physical.selected_web_contents_id()) - .and_then(PageTargetHost::reported_user_agent_override) + .and_then(PageAgentHost::reported_user_agent_override) .or_else(|| { self.default_browser_identity_override() .map(moli_browser_profile::BrowserIdentityProfile::user_agent) diff --git a/moli-protocol/src/conn/state/browser_context/collection.rs b/moli-protocol/src/conn/state/browser_context/collection.rs index c45efff2e4..ab2161c798 100644 --- a/moli-protocol/src/conn/state/browser_context/collection.rs +++ b/moli-protocol/src/conn/state/browser_context/collection.rs @@ -1,6 +1,6 @@ use super::BrowserContext; use crate::conn::state::{ - PageTargetHost, TargetIdentityState, + PageAgentHost, TargetIdentityState, page_slot::TargetPageSlot, web_contents::{ EmulationPolicy, EmulationPolicyChange, WebContents, WindowSurface, WindowSurfaceState, @@ -81,7 +81,7 @@ impl BrowserContext { return false; } let id = contents.id(); - let mut projection = PageTargetHost::new( + let mut projection = PageAgentHost::new( target_id, primary_session_id, identity, @@ -118,7 +118,7 @@ impl BrowserContext { let Some(id) = self .page_targets .get(target_id) - .map(PageTargetHost::web_contents_id) + .map(PageAgentHost::web_contents_id) else { return false; }; diff --git a/moli-protocol/src/conn/state/browser_context/collection/tests.rs b/moli-protocol/src/conn/state/browser_context/collection/tests.rs index 60489b5a44..aacc8ffe28 100644 --- a/moli-protocol/src/conn/state/browser_context/collection/tests.rs +++ b/moli-protocol/src/conn/state/browser_context/collection/tests.rs @@ -198,7 +198,7 @@ async fn close_retires_projection_waiters_and_channel_before_the_owned_page_tear assert!(matches!( projection .runtime_slot - .finish_renderer_document_navigation(&NavigationId::allocate()), + .finish_navigation_without_document_projection(&NavigationId::allocate()), Err(DevToolsRendererChannelError::Closed) )); assert_eq!( diff --git a/moli-protocol/src/conn/state/browser_context/page_slot.rs b/moli-protocol/src/conn/state/browser_context/page_slot.rs index 70d08aab64..2dcf838f48 100644 --- a/moli-protocol/src/conn/state/browser_context/page_slot.rs +++ b/moli-protocol/src/conn/state/browser_context/page_slot.rs @@ -3,8 +3,8 @@ use super::BrowserContext; use moli_core::page::RendererLifecycleEventStamp; use moli_core::{ browser::{ - DocumentId, DocumentLifecycle, DocumentLifetime, DocumentLifetimeObserver, NavigationId, - RendererPageResidenceIdentity, + BrowserSequence, DocumentId, DocumentLifecycle, DocumentLifetime, DocumentLifetimeObserver, + NavigationId, RendererPageResidenceIdentity, }, page::{ Page, RendererDocumentLifecycleEvent, RendererDocumentLifecycleEventKind, @@ -60,6 +60,7 @@ pub(crate) struct CommittedRendererDocumentBinding { pub(crate) frame_id: String, pub(crate) loader_id: String, pub(crate) document_id: DocumentId, + pub(crate) browser_sequence: BrowserSequence, pub(crate) document_open_replacement_epoch: Option, } @@ -980,7 +981,7 @@ impl BrowserContext { .get_mut(target_id) .expect("registered Target projection") .runtime_slot - .start_renderer_document_navigation(token); + .begin_document_projection(token); token } @@ -1278,10 +1279,16 @@ impl BrowserContext { return Vec::new(); }; self.install_document_lifecycle_for_test(target_id, lifecycle); + let browser_sequence = self + .renderer_document_lifecycle_binding_for_target(target_id) + .filter(|binding| binding.document_id == document) + .map(|binding| binding.browser_sequence) + .unwrap_or_else(BrowserSequence::allocate); self.project_committed_document_lifecycle_for_target( target_id, crate::conn::CommittedDocumentLifecycle { document, + browser_sequence, artifacts, }, navigation, @@ -1300,6 +1307,7 @@ impl BrowserContext { ) -> Vec { let crate::conn::CommittedDocumentLifecycle { document, + browser_sequence, artifacts, } = lifecycle; if self.target_document_id(target_id) != Some(document) { @@ -1337,6 +1345,7 @@ impl BrowserContext { frame_id, loader_id, document_id, + browser_sequence, document_open_replacement_epoch: None, }; if self diff --git a/moli-protocol/src/conn/state/browser_context/page_slot/document_host_tests.rs b/moli-protocol/src/conn/state/browser_context/page_slot/document_host_tests.rs index ce7c291947..566a39cdd2 100644 --- a/moli-protocol/src/conn/state/browser_context/page_slot/document_host_tests.rs +++ b/moli-protocol/src/conn/state/browser_context/page_slot/document_host_tests.rs @@ -250,11 +250,13 @@ async fn creation_projection_cannot_rewind_native_progress_or_retarget_a_replace let native = owner.renderer_document_lifecycle_authoritative_snapshot_for_target(TARGET); // Projection is delayed until after the Browser has accepted more progress. // Replaying/rebinding the creation occurrence may only affect visibility. + let browser_sequence = moli_core::browser::BrowserSequence::allocate(); for _ in 0..2 { let projected = owner.project_committed_document_lifecycle_for_target( TARGET, CommittedDocumentLifecycle { document, + browser_sequence, artifacts: artifacts.clone(), }, Some(navigation), @@ -669,8 +671,10 @@ async fn document_replacement_dismisses_dialog_without_protocol_session_cleanup( async fn browser_drop_dismisses_dialog_even_when_session_snapshot_survives() { let browser = Browser::new(BrowserConfig::default()).unwrap(); let (mut owner, completion) = page_with_installed_dialog_for_test(&browser).await; - let snapshot = owner.active_page_target().devtools_sessions + let dialog_projection_snapshot = owner.active_page_target().devtools_sessions [moli_page_types::DevToolsSessionKey::Primary] + .page_session_state + .javascript_dialog_state .clone(); let id = owner.selected_web_contents_id().unwrap(); drop(owner.page_targets.remove(TARGET).unwrap()); @@ -681,10 +685,10 @@ async fn browser_drop_dismisses_dialog_even_when_session_snapshot_survives() { assert!( !completion.finish(true, "late reply".into()), - "Browser drop must dismiss the dialog even if a cloned session projection survives" + "Browser drop must dismiss the dialog even if a dialog projection snapshot survives" ); assert!(!completion.wait().accepted); - drop(snapshot); + drop(dialog_projection_snapshot); } #[tokio::test] diff --git a/moli-protocol/src/conn/state/browser_context/page_state/fetch_state.rs b/moli-protocol/src/conn/state/browser_context/page_state/fetch_state.rs index fa3e648788..695c176f0a 100644 --- a/moli-protocol/src/conn/state/browser_context/page_state/fetch_state.rs +++ b/moli-protocol/src/conn/state/browser_context/page_state/fetch_state.rs @@ -1,6 +1,6 @@ #[cfg(test)] use crate::conn::fetch_support::OpenBodyStreamError; -use crate::conn::{BrowserContext, ConnectionNetworkRequestIdAllocator, PageTargetHost}; +use crate::conn::{BrowserContext, ConnectionNetworkRequestIdAllocator, PageAgentHost}; #[cfg(test)] use crate::conn::{ DocumentBodySource, NavigationDispatchState, NavigationId, PausedDocumentTransfer, @@ -12,29 +12,29 @@ fn document_navigation_loader_id(sequence: u64) -> String { } impl BrowserContext { - pub(crate) fn page_target(&self, target_id: &str) -> Option<&PageTargetHost> { + pub(crate) fn page_target(&self, target_id: &str) -> Option<&PageAgentHost> { self.page_targets.get(target_id) } - pub(crate) fn page_target_mut(&mut self, target_id: &str) -> Option<&mut PageTargetHost> { + pub(crate) fn page_target_mut(&mut self, target_id: &str) -> Option<&mut PageAgentHost> { self.page_targets.get_mut(target_id) } - pub fn background_target(&self, target_id: &str) -> Option<&PageTargetHost> { + pub fn background_target(&self, target_id: &str) -> Option<&PageAgentHost> { (!self.is_active_target(target_id)) .then(|| self.page_target(target_id)) .flatten() } #[cfg(test)] - pub(crate) fn background_target_mut(&mut self, target_id: &str) -> Option<&mut PageTargetHost> { + pub(crate) fn background_target_mut(&mut self, target_id: &str) -> Option<&mut PageAgentHost> { if self.is_active_target(target_id) { return None; } self.page_target_mut(target_id) } - pub(crate) fn background_targets(&self) -> impl DoubleEndedIterator { + pub(crate) fn background_targets(&self) -> impl DoubleEndedIterator { self.page_targets .background(self.physical.selected_web_contents_id()) } @@ -327,7 +327,7 @@ impl BrowserContext { } } -impl PageTargetHost { +impl PageAgentHost { pub(crate) fn prepare_document_navigation_request_ids( &mut self, network_request_id_allocator: &mut ConnectionNetworkRequestIdAllocator, diff --git a/moli-protocol/src/conn/state/browser_context/page_state/loaded.rs b/moli-protocol/src/conn/state/browser_context/page_state/loaded.rs index 33dea66c10..f7e8ab527e 100644 --- a/moli-protocol/src/conn/state/browser_context/page_state/loaded.rs +++ b/moli-protocol/src/conn/state/browser_context/page_state/loaded.rs @@ -3,13 +3,13 @@ use crate::conn::state::TargetPageAbsenceReason; use crate::conn::state::web_contents::{ DocumentNavigationDestination, PreparedDocumentNavigation, RetiringDocument, }; -use crate::conn::state::{DevToolsRendererChannelError, DocumentId}; -use crate::conn::{BrowserContext, PageTargetHost, TargetRuntimeSlot}; +use crate::conn::state::{DevToolsRendererChannelError, DocumentId, DocumentProjectionFence}; +use crate::conn::{BrowserContext, PageAgentHost, TargetRuntimeSlot}; use moli_core::page::{Page, RendererPageCommandPostResponseContinuation}; pub(crate) struct LoadedNavigationPageCommit { pub(crate) lifecycle: crate::conn::state::web_contents::CommittedDocumentLifecycle, - pub(crate) inspection_projection: Result<(), DevToolsRendererChannelError>, + pub(crate) inspection_projection: Result, pub(crate) replaced_page_owner: Option, pub(crate) previous_document_retirement: RetiringDocument, pub(crate) committed_document_post_response_continuation: @@ -80,10 +80,11 @@ impl BrowserContext { .page_targets .get_mut(target_id) .expect("resolved projection"); - if let Err(error) = target - .runtime_slot - .project_initial_document_inspection(commit.inspection_endpoint) - { + if let Err(error) = target.runtime_slot.project_initial_document_inspection( + commit.key.document(), + commit.lifecycle.browser_sequence, + commit.inspection_endpoint, + ) { tracing::warn!(%error, "initial document inspection projection failed"); } target @@ -370,8 +371,13 @@ impl BrowserContext { .expect("resolved target projection"); let inspection_projection = target .runtime_slot - .project_committed_document_inspection(commit.navigation, commit.inspection_endpoint) - .map(|previous| { + .project_committed_document_inspection( + commit.navigation, + commit.document, + commit.lifecycle.browser_sequence, + commit.inspection_endpoint, + ) + .map(|(previous, fence)| { if let Some(previous) = previous { let new_attachment = target .runtime_slot @@ -387,6 +393,7 @@ impl BrowserContext { .runtime_slot .install_pending_renderer_call_replacements(replacements); } + fence }); self.reset_document_projection_for_target( target_id, @@ -431,7 +438,7 @@ impl BrowserContext { } } -impl PageTargetHost { +impl PageAgentHost { pub(crate) fn target_url(&self) -> &str { self.target_identity.url() } diff --git a/moli-protocol/src/conn/state/browser_context/page_state/page_targets.rs b/moli-protocol/src/conn/state/browser_context/page_state/page_targets.rs index fa7d5fada4..afd3b57ae7 100644 --- a/moli-protocol/src/conn/state/browser_context/page_state/page_targets.rs +++ b/moli-protocol/src/conn/state/browser_context/page_state/page_targets.rs @@ -3,7 +3,7 @@ use crate::conn::state::{SessionStorageNamespace, TargetPageAbsenceReason}; use crate::conn::state::{page_slot::TargetPageSlot, web_contents::WebContents}; use crate::conn::{ - BrowserContext, DedicatedWorkerTargetState, InitialDocumentCreator, PageTargetHost, + BrowserContext, DedicatedWorkerTargetState, InitialDocumentCreator, PageAgentHost, ServiceWorkerTargetState, SharedWorkerTargetState, TargetIdentityState, }; use crate::devtools_runtime::{ @@ -16,7 +16,7 @@ impl BrowserContext { &mut self, target_id: &str, ) -> Option<( - PageTargetHost, + PageAgentHost, crate::conn::state::web_contents::ClosingWebContents, )> { let id = self.page_targets.get(target_id)?.web_contents_id(); @@ -159,7 +159,7 @@ impl BrowserContext { .id(); self.page_targets .get_for_web_contents(id) - .map(PageTargetHost::target_id) + .map(PageAgentHost::target_id) } pub(crate) fn has_attached_child_frame_id(&self, frame_id: &str) -> bool { @@ -171,7 +171,7 @@ impl BrowserContext { pub(crate) fn remember_target_window_name(&mut self, target_name: &str, target_id: &str) { let Some(id) = self .page_target(target_id) - .map(PageTargetHost::web_contents_id) + .map(PageAgentHost::web_contents_id) else { return; }; @@ -291,7 +291,7 @@ impl BrowserContext { self.page_targets .iter() .find(|target| target.devtools_sessions.attached(session_id).is_some()) - .map(PageTargetHost::target_id) + .map(PageAgentHost::target_id) } pub(crate) fn attached_session_ids_for_target(&self, target_id: &str) -> Vec { @@ -1470,7 +1470,7 @@ mod tests { } #[test] - fn page_target_host_keeps_protocol_and_owner_state_together() { + fn page_agent_host_keeps_protocol_and_owner_state_together() { let mut context = BrowserContext::new("BC-1".to_owned()); context.stage_background_target( "TID-bg".to_owned(), diff --git a/moli-protocol/src/conn/state/browser_context/page_state/surfaces.rs b/moli-protocol/src/conn/state/browser_context/page_state/surfaces.rs index 4c85a2b9ef..f71f16cd15 100644 --- a/moli-protocol/src/conn/state/browser_context/page_state/surfaces.rs +++ b/moli-protocol/src/conn/state/browser_context/page_state/surfaces.rs @@ -1,7 +1,7 @@ #[cfg(test)] use crate::conn::cookie_manager_surface::BrowserContextCookieManagerSurfaceSnapshot; use crate::conn::state::PageSurface; -use crate::conn::{BrowserContext, DocumentStartScript, EmulatedViewportSurface, PageTargetHost}; +use crate::conn::{BrowserContext, DocumentStartScript, EmulatedViewportSurface, PageAgentHost}; #[cfg(test)] use moli_cookie_jar::{BrowserCookieFacadeContextOverrides, BrowserCookieFacadeOverrides}; @@ -289,7 +289,7 @@ impl BrowserContext { // Context default resolution stays in this residence until Commit 7; // source generation itself only reads the embedded Browser object. - fn page_surface_for_state(&self, state: &PageTargetHost, foreground: bool) -> PageSurface { + fn page_surface_for_state(&self, state: &PageAgentHost, foreground: bool) -> PageSurface { let mut surface = self .web_contents_for_target(state.target_id()) .expect("live WebContents") diff --git a/moli-protocol/src/conn/state/browser_context/runtime_slot.rs b/moli-protocol/src/conn/state/browser_context/runtime_slot.rs index 5f494c856d..ebb50d664c 100644 --- a/moli-protocol/src/conn/state/browser_context/runtime_slot.rs +++ b/moli-protocol/src/conn/state/browser_context/runtime_slot.rs @@ -1,4 +1,5 @@ use super::BrowserContext; +use moli_core::browser::BrowserSequence; use moli_core::page::{ Page, RendererAgentAttachmentId, RendererDocumentLifecycleIdentity, RendererRuntimeInspectorMessageBatch, ScriptNetworkOutputItem, ScriptObservableOutputItem, @@ -24,7 +25,8 @@ use crate::{ }; use crate::conn::state::devtools_renderer_channel::{ - DevToolsRendererChannel, RendererAgentBinding, RendererAgentDetachReason, + DevToolsRendererChannel, DocumentProjectionFence, RendererAgentBinding, + RendererAgentDetachReason, RendererOutputHoldRelease, }; use crate::conn::state::page_slot::{TargetPageAbsenceReason, TargetPageSlot}; use crate::conn::state::{ @@ -33,7 +35,7 @@ use crate::conn::state::{ TargetJavaScriptDialogScope, TargetJavaScriptDialogScopeObserver, }; -pub(crate) struct FinishedRendererDocumentNavigation { +pub(crate) struct DocumentProjectionOutputRelease { pub(crate) released_output: Vec, pub(crate) renderer_call_replacements: Option, } @@ -162,28 +164,39 @@ impl TargetRuntimeSlot { self.javascript_dialog_scope.retire(); } - pub(super) fn start_renderer_document_navigation(&mut self, navigation: NavigationId) { + pub(super) fn begin_document_projection(&mut self, navigation: NavigationId) { self.devtools_renderer_channel.reopen_after_target_crash(); self.devtools_renderer_channel - .navigation_started(navigation) + .begin_document_projection(navigation) .expect("an open target runtime slot must accept a new document navigation"); } pub(in crate::conn::state) fn project_committed_document_inspection( &mut self, navigation: NavigationId, + document: DocumentId, + browser_sequence: BrowserSequence, endpoint: moli_renderer_v8::RendererInspectionEndpoint, - ) -> Result, DevToolsRendererChannelError> { - self.devtools_renderer_channel - .document_committed(navigation, endpoint) + ) -> Result< + (Option, DocumentProjectionFence), + DevToolsRendererChannelError, + > { + self.devtools_renderer_channel.document_committed( + navigation, + document, + browser_sequence, + endpoint, + ) } pub(in crate::conn::state) fn project_initial_document_inspection( &mut self, + document: DocumentId, + browser_sequence: BrowserSequence, endpoint: moli_renderer_v8::RendererInspectionEndpoint, ) -> Result<(), DevToolsRendererChannelError> { self.devtools_renderer_channel - .attach_current(endpoint) + .attach_current(document, browser_sequence, endpoint) .map(|_| ()) } @@ -196,19 +209,38 @@ impl TargetRuntimeSlot { .route_current_output(attachment_id, batches) } - pub(crate) fn finish_renderer_document_navigation( + pub(crate) fn finish_navigation_without_document_projection( &mut self, token: &NavigationId, - ) -> Result { - let resume = self.devtools_renderer_channel.navigation_finished(token)?; + ) -> Result { + let resume = self + .devtools_renderer_channel + .finish_navigation_without_document_projection(token)?; + Ok(self.release_renderer_document_output(resume)) + } + + pub(crate) fn publish_document_projection_fence( + &mut self, + fence: DocumentProjectionFence, + ) -> Result { + let resume = self + .devtools_renderer_channel + .publish_document_projection(fence)?; + Ok(self.release_renderer_document_output(resume)) + } + + fn release_renderer_document_output( + &mut self, + resume: Option, + ) -> DocumentProjectionOutputRelease { let renderer_call_replacements = resume .is_some() .then(|| std::mem::take(&mut self.pending_renderer_call_replacements)) .filter(|replacements| !replacements.is_empty()); - Ok(FinishedRendererDocumentNavigation { + DocumentProjectionOutputRelease { released_output: self.devtools_renderer_channel.take_released_output(), renderer_call_replacements, - }) + } } pub(crate) fn install_pending_renderer_call_replacements( @@ -218,8 +250,9 @@ impl TargetRuntimeSlot { self.pending_renderer_call_replacements = replacements; } - pub(crate) fn renderer_document_navigation_is_suspended(&self) -> bool { - self.devtools_renderer_channel.output_is_suspended() + pub(crate) fn document_projection_is_pending(&self) -> bool { + self.devtools_renderer_channel + .document_projection_is_pending() } pub(crate) fn current_renderer_attachment(&self) -> Option { @@ -235,8 +268,18 @@ impl TargetRuntimeSlot { &mut self, endpoint: moli_renderer_v8::RendererInspectionEndpoint, ) { + let document = self + .devtools_renderer_channel + .current() + .expect("reattachment requires a current renderer binding") + .document(); + let browser_sequence = self + .devtools_renderer_channel + .current() + .expect("reattachment requires a current renderer binding") + .browser_sequence(); self.devtools_renderer_channel - .attach_current(endpoint) + .attach_current(document, browser_sequence, endpoint) .unwrap(); } @@ -344,19 +387,21 @@ impl TargetRuntimeSlot { } #[cfg(test)] - fn ensure_renderer_attachment_for_replacement(&mut self, page: Option<&mut Page>) { - let Some(page) = page else { - return; - }; + fn ensure_renderer_attachment_for_replacement( + &mut self, + document: DocumentId, + agent_token: moli_core::page::RendererDevToolsAgentToken, + endpoint: moli_renderer_v8::RendererInspectionEndpoint, + ) { if !self .devtools_renderer_channel .current() .is_some_and(|attachment| { - attachment.agent_token() == page.renderer_devtools_agent_token() + attachment.agent_token() == agent_token && attachment.document() == document }) { self.devtools_renderer_channel - .attach_current(page.renderer_inspection_endpoint()) + .attach_current(document, BrowserSequence::allocate(), endpoint) .expect("a loaded page cannot be installed into a closed renderer channel"); } } @@ -883,20 +928,31 @@ impl BrowserContext { pub(super) fn replace_loaded_page_for_target( &mut self, target_id: &str, - mut page: Option, + page: Option, ) -> Option { + let inspection = page.as_ref().map(|page| { + ( + page.renderer_devtools_agent_token(), + page.renderer_inspection_endpoint(), + ) + }); let previous_document = self.target_document_id(target_id).zip( self.loaded_page_for_target(target_id) .map(RendererPageResidenceIdentity::from_page), ); let retiring = self.begin_document_projection_replacement_for_target(target_id, previous_document); - self.page_targets - .get_mut(target_id) - .expect("resolved target projection") - .runtime_slot - .ensure_renderer_attachment_for_replacement(page.as_mut()); let previous = self.replace_target_document(target_id, page); + if let Some((agent_token, endpoint)) = inspection { + let document = self + .target_document_id(target_id) + .expect("a replacement Page must install a Document"); + self.page_targets + .get_mut(target_id) + .expect("resolved target projection") + .runtime_slot + .ensure_renderer_attachment_for_replacement(document, agent_token, endpoint); + } self.finish_document_projection_replacement_for_target(target_id, retiring); previous } @@ -1021,8 +1077,8 @@ impl BrowserContext { "rendererChannelClosed": self.page_targets.get(target_id).expect("resolved target projection must remain live").runtime_slot.devtools_renderer_channel.is_closed(), "rendererChannelHasCurrentAttachment": self.page_targets.get(target_id).expect("resolved target projection must remain live").runtime_slot.devtools_renderer_channel.current().is_some(), - "rendererChannelInflightNavigationCount": - self.page_targets.get(target_id).expect("resolved target projection must remain live").runtime_slot.devtools_renderer_channel.inflight_navigation_count(), + "pendingDocumentProjectionCount": + self.page_targets.get(target_id).expect("resolved target projection must remain live").runtime_slot.devtools_renderer_channel.pending_navigation_count(), "hasNetworkEventListeners": self.page_targets.get(target_id).expect("resolved target projection must remain live").runtime_slot.has_network_event_listeners(), "nextFetchRequestId": self.page_targets.get(target_id).expect("resolved target projection must remain live").runtime_slot.request_counters.next_fetch_request_id, "nextSubresourceFetchRequestId": self.page_targets.get(target_id).expect("resolved target projection must remain live").runtime_slot.request_counters.next_subresource_fetch_request_id, @@ -1269,6 +1325,7 @@ mod tests { frame_id: "FRAME-old".to_owned(), loader_id: "LOADER-old".to_owned(), document_id: DocumentId::from_raw_for_test(1), + browser_sequence: BrowserSequence::allocate(), document_open_replacement_epoch: None, }, network_agent: retiring_agent, diff --git a/moli-protocol/src/conn/state/browser_context/session.rs b/moli-protocol/src/conn/state/browser_context/session.rs index b97bd4efdd..a4be0a8551 100644 --- a/moli-protocol/src/conn/state/browser_context/session.rs +++ b/moli-protocol/src/conn/state/browser_context/session.rs @@ -1,7 +1,7 @@ use super::BrowserContext; use crate::conn::state::devtools_session::DevToolsNetworkSessionState; use crate::conn::state::javascript_dialog::TargetJavaScriptDialogState; -use crate::conn::state::page_target_host::PageTargetHost; +use crate::conn::state::page_agent_host::PageAgentHost; use crate::conn::state::web_contents::NetworkRequestPolicy; use crate::domains::audits_output_state::TargetAuditsSessionState; use moli_core::page::V8InspectorSessionState; @@ -59,7 +59,7 @@ impl TargetPerformanceSessionState { } } -impl PageTargetHost { +impl PageAgentHost { pub(crate) fn reported_user_agent_override(&self) -> Option<&str> { self.devtools_sessions .reported_user_agent_override() diff --git a/moli-protocol/src/conn/state/devtools_renderer_channel.rs b/moli-protocol/src/conn/state/devtools_renderer_channel.rs index e2a6ecffac..4a37f2a7e3 100644 --- a/moli-protocol/src/conn/state/devtools_renderer_channel.rs +++ b/moli-protocol/src/conn/state/devtools_renderer_channel.rs @@ -1,6 +1,7 @@ use std::collections::HashSet; use std::fmt; +use moli_core::browser::BrowserSequence; use moli_core::page::{ PendingDevToolsIoCommandDispatch, PendingPageCommand, PendingRuntimeInspectorCommandDispatch, RendererAgentAttachmentId, RendererDevToolsAgentToken, RendererInspectorCommandRoute, @@ -11,19 +12,27 @@ use moli_renderer_v8::{ RendererRuntimeInspectorMainCommandRoute, RendererRuntimeInspectorResponseSender, }; -use super::NavigationId; +use super::{DocumentId, NavigationId}; #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub(crate) struct RendererAgentAttachment { id: RendererAgentAttachmentId, agent_token: RendererDevToolsAgentToken, + document: DocumentId, + browser_sequence: BrowserSequence, } impl RendererAgentAttachment { - fn new(agent_token: RendererDevToolsAgentToken) -> Self { + fn new( + document: DocumentId, + browser_sequence: BrowserSequence, + agent_token: RendererDevToolsAgentToken, + ) -> Self { Self { id: RendererAgentAttachmentId::allocate(), agent_token, + document, + browser_sequence, } } @@ -34,6 +43,52 @@ impl RendererAgentAttachment { pub(crate) fn agent_token(self) -> RendererDevToolsAgentToken { self.agent_token } + + pub(crate) fn document(self) -> DocumentId { + self.document + } + + pub(crate) fn browser_sequence(self) -> BrowserSequence { + self.browser_sequence + } +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +struct DocumentProjectionFenceKey { + document: DocumentId, + browser_sequence: BrowserSequence, + renderer_attachment: RendererAgentAttachmentId, +} + +/// Move-only authority to expose output from one rebound renderer Document. +#[derive(Debug, Eq, PartialEq)] +#[must_use = "a Document projection fence must be published before its channel is retired"] +pub(crate) struct DocumentProjectionFence { + key: DocumentProjectionFenceKey, +} + +impl DocumentProjectionFence { + fn new(attachment: RendererAgentAttachment) -> Self { + Self { + key: DocumentProjectionFenceKey { + document: attachment.document(), + browser_sequence: attachment.browser_sequence(), + renderer_attachment: attachment.id(), + }, + } + } + + pub(crate) fn document(&self) -> DocumentId { + self.key.document + } + + pub(crate) fn browser_sequence(&self) -> BrowserSequence { + self.key.browser_sequence + } + + pub(crate) fn renderer_attachment(&self) -> RendererAgentAttachmentId { + self.key.renderer_attachment + } } /// The DevTools binding owns inspection ingress, never the Browser Page. @@ -270,25 +325,32 @@ enum DevToolsRendererChannelLifecycle { pub(crate) struct DevToolsRendererChannel { lifecycle: DevToolsRendererChannelLifecycle, current: Option, - inflight_cross_document_navigations: HashSet, - suspended_attachment: Option, + pending_document_navigations: HashSet, + pending_document_projection: Option, + held_attachment: Option, buffered_output: Vec, } +#[derive(Debug)] +struct PendingDocumentProjection { + navigation: NavigationId, + fence: DocumentProjectionFenceKey, +} + #[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub(crate) struct RendererChannelResume { - suspended_attachment: Option, +pub(crate) struct RendererOutputHoldRelease { + held_attachment: Option, current_attachment: Option, } -impl RendererChannelResume { +impl RendererOutputHoldRelease { #[cfg(test)] pub(crate) fn replacement( self, ) -> Option<(RendererAgentAttachmentId, RendererAgentAttachmentId)> { - let suspended = self.suspended_attachment?; + let held = self.held_attachment?; let current = self.current_attachment?; - (suspended.id() != current.id()).then_some((suspended.id(), current.id())) + (held.id() != current.id()).then_some((held.id(), current.id())) } } @@ -301,13 +363,19 @@ struct BufferedRendererInspectorBatch { impl DevToolsRendererChannel { pub(crate) fn attach_current( &mut self, + document: DocumentId, + browser_sequence: BrowserSequence, endpoint: RendererInspectionEndpoint, ) -> Result, DevToolsRendererChannelError> { self.ensure_open()?; Ok(self .current .replace(RendererAgentBinding { - attachment: RendererAgentAttachment::new(endpoint.agent_token()), + attachment: RendererAgentAttachment::new( + document, + browser_sequence, + endpoint.agent_token(), + ), endpoint, }) .map(|previous| previous.attachment())) @@ -321,17 +389,17 @@ impl DevToolsRendererChannel { self.current.as_ref() } - pub(crate) fn navigation_started( + pub(crate) fn begin_document_projection( &mut self, navigation: NavigationId, ) -> Result<(), DevToolsRendererChannelError> { self.ensure_open()?; - let was_suspended = self.output_is_suspended(); - if !self.inflight_cross_document_navigations.insert(navigation) { + let was_pending = self.document_projection_is_pending(); + if !self.pending_document_navigations.insert(navigation) { return Err(DevToolsRendererChannelError::DuplicateNavigation); } - if !was_suspended { - self.suspended_attachment = self.current(); + if !was_pending { + self.held_attachment = self.current(); } Ok(()) } @@ -340,12 +408,32 @@ impl DevToolsRendererChannel { pub(crate) fn document_committed( &mut self, navigation: NavigationId, + document: DocumentId, + browser_sequence: BrowserSequence, endpoint: RendererInspectionEndpoint, - ) -> Result, DevToolsRendererChannelError> { - let previous = self.attach_current(endpoint)?; - self.inflight_cross_document_navigations + ) -> Result< + (Option, DocumentProjectionFence), + DevToolsRendererChannelError, + > { + self.ensure_open()?; + if self.pending_document_projection.is_some() { + return Err(DevToolsRendererChannelError::ProjectionPending); + } + if !self.pending_document_navigations.contains(&navigation) { + return Err(DevToolsRendererChannelError::UnknownNavigation); + } + let previous = self.attach_current(document, browser_sequence, endpoint)?; + self.pending_document_navigations .retain(|pending| *pending == navigation); - Ok(previous) + let fence = DocumentProjectionFence::new( + self.current() + .expect("a successful renderer rebind must install its attachment"), + ); + self.pending_document_projection = Some(PendingDocumentProjection { + navigation, + fence: fence.key, + }); + Ok((previous, fence)) } pub(crate) fn route_current_output( @@ -363,37 +451,78 @@ impl DevToolsRendererChannel { self.route_validated_output(attachment_id, batches) } - pub(crate) fn navigation_finished( + pub(crate) fn finish_navigation_without_document_projection( &mut self, navigation: &NavigationId, - ) -> Result, DevToolsRendererChannelError> { + ) -> Result, DevToolsRendererChannelError> { self.ensure_open()?; - if !self.inflight_cross_document_navigations.remove(navigation) - || self.output_is_suspended() + if self + .pending_document_projection + .as_ref() + .is_some_and(|pending| &pending.navigation == navigation) + { + return Err(DevToolsRendererChannelError::ProjectionPending); + } + if !self.pending_document_navigations.remove(navigation) + || self.document_projection_is_pending() { return Ok(None); } - Ok(Some(RendererChannelResume { - suspended_attachment: self.suspended_attachment.take(), + Ok(Some(RendererOutputHoldRelease { + held_attachment: self.held_attachment.take(), current_attachment: self.current(), })) } - pub(crate) fn output_is_suspended(&self) -> bool { - !self.inflight_cross_document_navigations.is_empty() + pub(crate) fn publish_document_projection( + &mut self, + fence: DocumentProjectionFence, + ) -> Result, DevToolsRendererChannelError> { + self.ensure_open()?; + let Some(pending) = self.pending_document_projection.as_ref() else { + return Err(DevToolsRendererChannelError::StaleProjectionFence); + }; + if pending.fence != fence.key + || self.current().is_none_or(|current| { + current.document() != fence.document() + || current.browser_sequence() != fence.browser_sequence() + || current.id() != fence.renderer_attachment() + }) + { + return Err(DevToolsRendererChannelError::StaleProjectionFence); + } + let pending = self + .pending_document_projection + .take() + .expect("validated pending Document projection"); + assert!( + self.pending_document_navigations + .remove(&pending.navigation), + "a pending Document projection must retain its navigation hold" + ); + if self.document_projection_is_pending() { + return Ok(None); + } + Ok(Some(RendererOutputHoldRelease { + held_attachment: self.held_attachment.take(), + current_attachment: self.current(), + })) + } + + pub(crate) fn document_projection_is_pending(&self) -> bool { + !self.pending_document_navigations.is_empty() } pub(crate) fn has_navigation(&self, navigation: &NavigationId) -> bool { - self.inflight_cross_document_navigations - .contains(navigation) + self.pending_document_navigations.contains(navigation) } - pub(crate) fn inflight_navigation_count(&self) -> usize { - self.inflight_cross_document_navigations.len() + pub(crate) fn pending_navigation_count(&self) -> usize { + self.pending_document_navigations.len() } pub(crate) fn take_released_output(&mut self) -> Vec { - if self.output_is_suspended() { + if self.document_projection_is_pending() { return Vec::new(); } let Some(current) = self.current() else { @@ -410,6 +539,10 @@ impl DevToolsRendererChannel { _reason: RendererAgentDetachReason, ) -> Result, DevToolsRendererChannelError> { self.ensure_open()?; + self.pending_document_navigations.clear(); + self.pending_document_projection = None; + self.held_attachment = None; + self.buffered_output.clear(); Ok(self.current.take().map(|current| current.attachment())) } @@ -421,8 +554,9 @@ impl DevToolsRendererChannel { return None; } self.lifecycle = DevToolsRendererChannelLifecycle::Closed(reason); - self.inflight_cross_document_navigations.clear(); - self.suspended_attachment = None; + self.pending_document_navigations.clear(); + self.pending_document_projection = None; + self.held_attachment = None; self.buffered_output.clear(); self.current.take().map(|current| current.attachment()) } @@ -463,10 +597,11 @@ impl DevToolsRendererChannel { { return Err(DevToolsRendererChannelError::MismatchedAgent); } - if self.output_is_suspended() { - let releases_current_prefix = batches - .iter() - .any(RendererRuntimeInspectorMessageBatch::has_renderer_protocol_response); + if self.document_projection_is_pending() { + let releases_current_prefix = self.pending_document_projection.is_none() + && batches + .iter() + .any(RendererRuntimeInspectorMessageBatch::has_renderer_protocol_response); self.buffer_output(attachment_id, batches); if releases_current_prefix { // Main ingress remains suspended, but Chromium's existing @@ -521,6 +656,9 @@ impl DevToolsRendererChannel { pub(crate) enum DevToolsRendererChannelError { Closed, DuplicateNavigation, + UnknownNavigation, + ProjectionPending, + StaleProjectionFence, StaleAttachment, MismatchedAgent, } @@ -530,6 +668,9 @@ impl fmt::Display for DevToolsRendererChannelError { formatter.write_str(match self { Self::Closed => "renderer channel is closed", Self::DuplicateNavigation => "renderer channel navigation is already in flight", + Self::UnknownNavigation => "renderer channel navigation is not in flight", + Self::ProjectionPending => "renderer Document projection is still pending", + Self::StaleProjectionFence => "renderer Document projection fence is stale", Self::StaleAttachment => "renderer Inspector output belongs to a stale attachment", Self::MismatchedAgent => { "renderer Inspector output agent does not match its attachment" @@ -602,17 +743,24 @@ mod tests { .unwrap(); let mut channel = DevToolsRendererChannel::default(); channel - .attach_current(outgoing.renderer_inspection_endpoint()) + .attach_current( + DocumentId::allocate(), + BrowserSequence::allocate(), + outgoing.renderer_inspection_endpoint(), + ) .unwrap(); let original = channel.current().unwrap(); let navigation = NavigationId::allocate(); - channel.navigation_started(navigation).unwrap(); - assert_eq!( - channel - .document_committed(navigation, committed_page.renderer_inspection_endpoint()) - .unwrap(), - Some(original) - ); + channel.begin_document_projection(navigation).unwrap(); + let (previous, _fence) = channel + .document_committed( + navigation, + DocumentId::allocate(), + BrowserSequence::allocate(), + committed_page.renderer_inspection_endpoint(), + ) + .unwrap(); + assert_eq!(previous, Some(original)); let attachment = channel.current().unwrap(); let registration = moli_core::page::RuntimeBindingRegistration { devtools_session: None, @@ -688,14 +836,22 @@ mod tests { let mut channel = DevToolsRendererChannel::default(); assert_eq!( - channel.attach_current(page.renderer_inspection_endpoint()), + channel.attach_current( + DocumentId::allocate(), + BrowserSequence::allocate(), + page.renderer_inspection_endpoint() + ), Ok(None) ); let first = channel.current().expect("first attachment"); assert_eq!(first.agent_token(), agent); let replaced = channel - .attach_current(page.renderer_inspection_endpoint()) + .attach_current( + DocumentId::allocate(), + BrowserSequence::allocate(), + page.renderer_inspection_endpoint(), + ) .expect("reattach") .expect("replaced attachment"); let second = channel.current().expect("second attachment"); @@ -710,54 +866,73 @@ mod tests { let request = NavigationId::allocate(); let mut channel = DevToolsRendererChannel::default(); channel - .attach_current(page.renderer_inspection_endpoint()) + .attach_current( + DocumentId::allocate(), + BrowserSequence::allocate(), + page.renderer_inspection_endpoint(), + ) .expect("initial attach"); let current = channel.current(); channel - .navigation_started(request) + .begin_document_projection(request) .expect("navigation start"); - assert!(channel.output_is_suspended()); + assert!(channel.document_projection_is_pending()); assert!( channel - .navigation_finished(&request) + .finish_navigation_without_document_projection(&request) .expect("navigation finish") .is_some(), "a failed load finishes without committing its candidate" ); assert_eq!(channel.current(), current); - assert!(!channel.output_is_suspended()); + assert!(!channel.document_projection_is_pending()); } #[tokio::test(flavor = "multi_thread")] - async fn browser_commit_retires_superseded_output_suspensions() { + async fn browser_commit_retires_superseded_projection_holds() { let (_browser, page) = inspection_page().await; let (_next_browser, next_page) = inspection_page().await; let first = NavigationId::allocate(); let committed = NavigationId::allocate(); let mut channel = DevToolsRendererChannel::default(); channel - .attach_current(page.renderer_inspection_endpoint()) + .attach_current( + DocumentId::allocate(), + BrowserSequence::allocate(), + page.renderer_inspection_endpoint(), + ) .unwrap(); let original = channel.current().unwrap(); - channel.navigation_started(first).unwrap(); - channel.navigation_started(committed).unwrap(); + channel.begin_document_projection(first).unwrap(); + channel.begin_document_projection(committed).unwrap(); // Browser has already selected the winning Document. This is projection, // not a second validation/commit state machine. - assert_eq!( - channel - .document_committed(committed, next_page.renderer_inspection_endpoint()) - .unwrap(), - Some(original) - ); + let (previous, fence) = channel + .document_committed( + committed, + DocumentId::allocate(), + BrowserSequence::allocate(), + next_page.renderer_inspection_endpoint(), + ) + .unwrap(); + assert_eq!(previous, Some(original)); assert_eq!( channel.current().unwrap().agent_token(), next_page.renderer_devtools_agent_token() ); - assert_eq!(channel.inflight_navigation_count(), 1); - assert!(channel.navigation_finished(&committed).unwrap().is_some()); - assert_eq!(channel.navigation_finished(&first), Ok(None)); + assert_eq!(channel.pending_navigation_count(), 1); + assert!( + channel + .publish_document_projection(fence) + .unwrap() + .is_some() + ); + assert_eq!( + channel.finish_navigation_without_document_projection(&first), + Ok(None) + ); } #[tokio::test(flavor = "multi_thread")] @@ -765,7 +940,11 @@ mod tests { let (_browser, page) = inspection_page().await; let mut channel = DevToolsRendererChannel::default(); channel - .attach_current(page.renderer_inspection_endpoint()) + .attach_current( + DocumentId::allocate(), + BrowserSequence::allocate(), + page.renderer_inspection_endpoint(), + ) .unwrap(); drop(page); let binding = channel.current_binding().unwrap(); @@ -793,7 +972,11 @@ mod tests { let (_browser, mut page) = inspection_page().await; let mut channel = DevToolsRendererChannel::default(); channel - .attach_current(page.renderer_inspection_endpoint()) + .attach_current( + DocumentId::allocate(), + BrowserSequence::allocate(), + page.renderer_inspection_endpoint(), + ) .unwrap(); channel .detach_current(RendererAgentDetachReason::ExplicitDetach) @@ -809,41 +992,56 @@ mod tests { } #[test] - fn output_remains_suspended_until_all_overlapping_navigations_finish() { + fn output_remains_held_until_all_overlapping_navigations_finish() { let request_a = NavigationId::allocate(); let request_b = NavigationId::allocate(); let mut channel = DevToolsRendererChannel::default(); - channel.navigation_started(request_a).expect("navigation A"); - channel.navigation_started(request_b).expect("navigation B"); - assert_eq!(channel.inflight_navigation_count(), 2); - assert!(channel.output_is_suspended()); + channel + .begin_document_projection(request_a) + .expect("navigation A"); + channel + .begin_document_projection(request_b) + .expect("navigation B"); + assert_eq!(channel.pending_navigation_count(), 2); + assert!(channel.document_projection_is_pending()); - assert_eq!(channel.navigation_finished(&request_b), Ok(None)); - assert!(channel.output_is_suspended()); + assert_eq!( + channel.finish_navigation_without_document_projection(&request_b), + Ok(None) + ); + assert!(channel.document_projection_is_pending()); assert!( channel - .navigation_finished(&request_a) + .finish_navigation_without_document_projection(&request_a) .expect("final overlapping navigation") .is_some() ); - assert!(!channel.output_is_suspended()); + assert!(!channel.document_projection_is_pending()); } #[tokio::test(flavor = "multi_thread")] - async fn output_suspension_rejects_duplicate_and_ignores_unknown_completion() { + async fn projection_hold_rejects_duplicate_and_ignores_unknown_completion() { let request = NavigationId::allocate(); let unknown = NavigationId::allocate(); let mut channel = DevToolsRendererChannel::default(); - channel.navigation_started(request).unwrap(); + channel.begin_document_projection(request).unwrap(); assert_eq!( - channel.navigation_started(request), + channel.begin_document_projection(request), Err(DevToolsRendererChannelError::DuplicateNavigation) ); - assert_eq!(channel.navigation_finished(&unknown), Ok(None)); - assert!(channel.output_is_suspended()); - assert!(channel.navigation_finished(&request).unwrap().is_some()); - assert!(!channel.output_is_suspended()); + assert_eq!( + channel.finish_navigation_without_document_projection(&unknown), + Ok(None) + ); + assert!(channel.document_projection_is_pending()); + assert!( + channel + .finish_navigation_without_document_projection(&request) + .unwrap() + .is_some() + ); + assert!(!channel.document_projection_is_pending()); } #[tokio::test(flavor = "multi_thread")] @@ -853,10 +1051,14 @@ mod tests { let agent = page.renderer_devtools_agent_token(); let mut channel = DevToolsRendererChannel::default(); channel - .attach_current(page.renderer_inspection_endpoint()) + .attach_current( + DocumentId::allocate(), + BrowserSequence::allocate(), + page.renderer_inspection_endpoint(), + ) .expect("initial attach"); channel - .navigation_started(request) + .begin_document_projection(request) .expect("navigation start"); let detached = channel @@ -864,17 +1066,26 @@ mod tests { .expect("current attachment"); assert_eq!(detached.agent_token(), agent); assert!(channel.is_closed()); - assert_eq!(channel.inflight_navigation_count(), 0); + assert_eq!(channel.pending_navigation_count(), 0); assert_eq!( - channel.attach_current(page.renderer_inspection_endpoint()), + channel.attach_current( + DocumentId::allocate(), + BrowserSequence::allocate(), + page.renderer_inspection_endpoint() + ), Err(DevToolsRendererChannelError::Closed) ); assert_eq!( - channel.navigation_started(NavigationId::allocate()), + channel.begin_document_projection(NavigationId::allocate()), Err(DevToolsRendererChannelError::Closed) ); assert_eq!( - channel.document_committed(request, page.renderer_inspection_endpoint()), + channel.document_committed( + request, + DocumentId::allocate(), + BrowserSequence::allocate(), + page.renderer_inspection_endpoint(), + ), Err(DevToolsRendererChannelError::Closed) ); assert_eq!(channel.close(RendererAgentDetachReason::TargetClosed), None); @@ -887,7 +1098,11 @@ mod tests { let agent = page.renderer_devtools_agent_token(); let mut channel = DevToolsRendererChannel::default(); channel - .attach_current(page.renderer_inspection_endpoint()) + .attach_current( + DocumentId::allocate(), + BrowserSequence::allocate(), + page.renderer_inspection_endpoint(), + ) .expect("initial attach"); let detached = channel @@ -898,7 +1113,11 @@ mod tests { assert!(channel.reopen_after_target_crash()); assert!(!channel.is_closed()); assert!(!channel.reopen_after_target_crash()); - assert!(channel.navigation_started(NavigationId::allocate()).is_ok()); + assert!( + channel + .begin_document_projection(NavigationId::allocate()) + .is_ok() + ); } #[tokio::test(flavor = "multi_thread")] @@ -910,11 +1129,15 @@ mod tests { let request = NavigationId::allocate(); let mut channel = DevToolsRendererChannel::default(); channel - .attach_current(page.renderer_inspection_endpoint()) + .attach_current( + DocumentId::allocate(), + BrowserSequence::allocate(), + page.renderer_inspection_endpoint(), + ) .expect("old attach"); let old_attachment = channel.current().expect("old attachment"); channel - .navigation_started(request) + .begin_document_projection(request) .expect("navigation start"); assert!( @@ -923,10 +1146,20 @@ mod tests { .expect("route old output") .is_empty() ); - channel - .document_committed(request, candidate_page.renderer_inspection_endpoint()) + let committed_document = DocumentId::allocate(); + let committed_sequence = BrowserSequence::allocate(); + let (_, fence) = channel + .document_committed( + request, + committed_document, + committed_sequence, + candidate_page.renderer_inspection_endpoint(), + ) .unwrap(); let current = channel.current().unwrap(); + assert_eq!(fence.document(), committed_document); + assert_eq!(fence.browser_sequence(), committed_sequence); + assert_eq!(fence.renderer_attachment(), current.id()); assert!( channel .route_current_output(current.id(), vec![batch(new_agent, "new")]) @@ -934,9 +1167,33 @@ mod tests { .is_empty(), "committing Browser state does not release new-generation output before projection finishes" ); + assert_eq!( + channel.finish_navigation_without_document_projection(&request), + Err(DevToolsRendererChannelError::ProjectionPending), + "a generic navigation terminal must not bypass the committed projection fence" + ); + assert!( + channel + .route_current_output(current.id(), vec![response_batch(new_agent, 18)]) + .expect("route new-generation response") + .is_empty(), + "even an IO response from the rebound attachment must wait for frame projection" + ); + let stale_fence = DocumentProjectionFence { + key: DocumentProjectionFenceKey { + document: fence.document(), + browser_sequence: BrowserSequence::allocate(), + renderer_attachment: fence.renderer_attachment(), + }, + }; + assert_eq!( + channel.publish_document_projection(stale_fence), + Err(DevToolsRendererChannelError::StaleProjectionFence), + "a mismatched Browser occurrence must not consume the exact projection fence" + ); let resume = channel - .navigation_finished(&request) - .expect("navigation finish") + .publish_document_projection(fence) + .expect("publish projection fence") .expect("channel resume"); assert_eq!( resume.replacement(), @@ -944,8 +1201,9 @@ mod tests { ); let released = channel.take_released_output(); - assert_eq!(released.len(), 1); + assert_eq!(released.len(), 2); assert_eq!(batch_marker(&released[0]), Some("new")); + assert!(released[1].has_renderer_protocol_response()); } #[tokio::test(flavor = "multi_thread")] @@ -955,11 +1213,15 @@ mod tests { let request = NavigationId::allocate(); let mut channel = DevToolsRendererChannel::default(); channel - .attach_current(page.renderer_inspection_endpoint()) + .attach_current( + DocumentId::allocate(), + BrowserSequence::allocate(), + page.renderer_inspection_endpoint(), + ) .expect("current attach"); let attachment = channel.current().expect("current attachment"); channel - .navigation_started(request) + .begin_document_projection(request) .expect("navigation start"); assert!( channel @@ -969,7 +1231,7 @@ mod tests { ); let resume = channel - .navigation_finished(&request) + .finish_navigation_without_document_projection(&request) .expect("navigation finish") .expect("channel resume"); assert_eq!(resume.replacement(), None); @@ -985,11 +1247,15 @@ mod tests { let request = NavigationId::allocate(); let mut channel = DevToolsRendererChannel::default(); channel - .attach_current(page.renderer_inspection_endpoint()) + .attach_current( + DocumentId::allocate(), + BrowserSequence::allocate(), + page.renderer_inspection_endpoint(), + ) .expect("current attach"); let attachment = channel.current().expect("current attachment"); channel - .navigation_started(request) + .begin_document_projection(request) .expect("navigation start"); assert!( @@ -1005,7 +1271,7 @@ mod tests { assert_eq!(released.len(), 2); assert_eq!(batch_marker(&released[0]), Some("before-response")); assert!(released[1].has_renderer_protocol_response()); - assert!(channel.output_is_suspended()); + assert!(channel.document_projection_is_pending()); assert!(channel.take_released_output().is_empty()); } @@ -1016,11 +1282,19 @@ mod tests { let other_agent = RendererDevToolsAgentToken::allocate(); let mut channel = DevToolsRendererChannel::default(); channel - .attach_current(page.renderer_inspection_endpoint()) + .attach_current( + DocumentId::allocate(), + BrowserSequence::allocate(), + page.renderer_inspection_endpoint(), + ) .expect("first attach"); let stale = channel.current().expect("first attachment"); channel - .attach_current(page.renderer_inspection_endpoint()) + .attach_current( + DocumentId::allocate(), + BrowserSequence::allocate(), + page.renderer_inspection_endpoint(), + ) .expect("reattach"); let current = channel.current().expect("current attachment"); diff --git a/moli-protocol/src/conn/state/devtools_session.rs b/moli-protocol/src/conn/state/devtools_session.rs index 5cc6949cff..7aa9c8a403 100644 --- a/moli-protocol/src/conn/state/devtools_session.rs +++ b/moli-protocol/src/conn/state/devtools_session.rs @@ -30,7 +30,7 @@ use moli_page_types::{ /// /// Browser-side policy and renderer-inspector state live together here so /// attachment, replay, and disposal all address the same session object. -#[derive(Clone, Debug, Default, PartialEq)] +#[derive(Debug, Default, PartialEq)] pub(crate) struct DevToolsSessionState { pub(crate) dom_session_state: DevToolsDomSessionState, pub(crate) dom_debugger_event_listener_breakpoints: @@ -977,6 +977,30 @@ impl DevToolsSessionState { self.pending_inspector_awaits.remove(cdp_request_id) } + pub(crate) fn claim_pending_inspector_await(&mut self, cdp_request_id: u64) -> bool { + self.pending_inspector_awaits + .claim_scheduler_deferred_reply(cdp_request_id) + } + + pub(crate) fn take_claimed_pending_inspector_await( + &mut self, + cdp_request_id: u64, + ) -> Option { + self.pending_inspector_awaits + .take_claimed_scheduler_deferred_reply(cdp_request_id) + } + + #[cfg(test)] + pub(crate) fn has_claimed_pending_inspector_awaits(&self) -> bool { + self.pending_inspector_awaits + .has_claimed_scheduler_deferred_reply() + } + + #[cfg(test)] + pub(crate) fn has_unclaimed_pending_inspector_awaits(&self) -> bool { + self.pending_inspector_awaits.has_unclaimed_await() + } + pub(crate) fn has_pending_inspector_awaits(&self) -> bool { !self.pending_inspector_awaits.is_empty() } @@ -1638,7 +1662,7 @@ mod tests { sessions .set_timezone_override(&key, Some("Europe/Paris".to_owned())) .unwrap(); - let snapshot = sessions.primary().clone(); + let snapshot = sessions.primary().emulation_session_state.clone(); drop(sessions); let replacement = moli_core::ProcessEnvironmentOwner::default(); diff --git a/moli-protocol/src/conn/state/mod.rs b/moli-protocol/src/conn/state/mod.rs index 01583bd850..9c2f11d819 100644 --- a/moli-protocol/src/conn/state/mod.rs +++ b/moli-protocol/src/conn/state/mod.rs @@ -13,7 +13,7 @@ pub(in crate::conn) use browser_context::javascript_dialog; mod navigation_outcome; mod page_resource; pub(in crate::conn) use browser_context::page_slot; -mod page_target_host; +mod page_agent_host; mod pending_renderer_command; mod profiler; pub(in crate::conn) use browser_context::runtime_slot; @@ -51,7 +51,8 @@ pub(crate) use identity::{ pub(crate) use moli_core::browser::{DocumentId, NavigationId, RendererPageResidenceIdentity}; pub(crate) use devtools_renderer_channel::{ - DevToolsRendererChannelError, RendererAgentAttachment, RendererAgentBinding, + DevToolsRendererChannelError, DocumentProjectionFence, RendererAgentAttachment, + RendererAgentBinding, }; pub(crate) use dedicated_worker_target::{ @@ -78,7 +79,7 @@ pub(crate) use page_slot::{ }; pub use page_slot::{DocumentStartScript, IsolatedWorldDefinition, RuntimeBindingDefinition}; -pub(crate) use runtime_slot::{FinishedRendererDocumentNavigation, TargetRuntimeSlot}; +pub(crate) use runtime_slot::{DocumentProjectionOutputRelease, TargetRuntimeSlot}; pub use fetch::TargetFetchConfig; pub(crate) use fetch::{TargetFetchOwner, TargetFetchSubresourceInterceptionSnapshot}; @@ -91,9 +92,9 @@ pub(crate) use javascript_dialog::{ TargetPreparedJavaScriptDialog, TargetPreparedJavaScriptDialogRoute, }; pub(crate) use pending_renderer_command::{ - DuplicatePendingRendererCommand, PendingRendererCommandKey, PreparedRendererCallDispatch, - PreparedRendererCallTermination, RegisterRendererCallError, RendererCommandCorrelation, - RendererCommandDescriptor, RendererCommandReplay, + DuplicatePendingRendererCommand, PreparedRendererCallDispatch, PreparedRendererCallTermination, + RegisterRendererCallError, RendererCommandCorrelation, RendererCommandDescriptor, + RendererCommandReplay, }; pub(crate) use profiler::{ProfilerAction, ProfilerInspectorCommand}; pub(crate) use service_worker_lifetime::{ @@ -153,7 +154,7 @@ pub use emulation::{ EmulatedMediaOverrides, }; pub(crate) use emulation::{EmulatedNetworkConditions, EmulatedViewportSurface}; -pub use page_target_host::PageTargetHost; +pub use page_agent_host::PageAgentHost; pub(crate) use target_state::{ PendingBidiChannelListener, PendingInspectorAwait, TargetOwnerState, }; diff --git a/moli-protocol/src/conn/state/page_target_host.rs b/moli-protocol/src/conn/state/page_agent_host.rs similarity index 93% rename from moli-protocol/src/conn/state/page_target_host.rs rename to moli-protocol/src/conn/state/page_agent_host.rs index c08f360d7d..2aca09670a 100644 --- a/moli-protocol/src/conn/state/page_target_host.rs +++ b/moli-protocol/src/conn/state/page_agent_host.rs @@ -15,7 +15,7 @@ use crate::conn::cookie_manager_surface::BrowserContextCookieManagerSurface; /// DevTools projection of a stable Browser page and its main-frame slot. /// Browser ownership and foreground selection belong to the physical Context. #[derive(Debug)] -pub struct PageTargetHost { +pub struct PageAgentHost { target_id: String, web_contents_id: WebContentsId, main_frame_slot_id: MainFrameSlotId, @@ -36,7 +36,7 @@ pub struct PageTargetHost { pub(crate) owner_state: TargetOwnerState, } -impl PageTargetHost { +impl PageAgentHost { pub(crate) fn new( target_id: String, primary_session_id: Option, @@ -155,11 +155,11 @@ impl PageTargetHost { /// DevTools page projections. The Browser collection owns selection and close. #[derive(Debug, Default)] -pub(crate) struct PageTargetRegistry { - hosts: IndexMap, +pub(crate) struct PageAgentHostRegistry { + hosts: IndexMap, } -impl PageTargetRegistry { +impl PageAgentHostRegistry { #[cfg(test)] pub(crate) fn is_empty(&self) -> bool { self.hosts.is_empty() @@ -169,14 +169,14 @@ impl PageTargetRegistry { self.hosts.len() } - pub(crate) fn active(&self, selected: Option) -> Option<&PageTargetHost> { + pub(crate) fn active(&self, selected: Option) -> Option<&PageAgentHost> { self.get_for_web_contents(selected?) } pub(crate) fn active_mut( &mut self, selected: Option, - ) -> Option<&mut PageTargetHost> { + ) -> Option<&mut PageAgentHost> { let id = selected?; self.iter_mut().find(|host| host.web_contents_id() == id) } @@ -184,19 +184,19 @@ impl PageTargetRegistry { pub(in crate::conn) fn get_for_web_contents( &self, id: WebContentsId, - ) -> Option<&PageTargetHost> { + ) -> Option<&PageAgentHost> { self.iter().find(|host| host.web_contents_id() == id) } - pub(crate) fn get(&self, target_id: &str) -> Option<&PageTargetHost> { + pub(crate) fn get(&self, target_id: &str) -> Option<&PageAgentHost> { self.hosts.get(target_id) } - pub(crate) fn get_mut(&mut self, target_id: &str) -> Option<&mut PageTargetHost> { + pub(crate) fn get_mut(&mut self, target_id: &str) -> Option<&mut PageAgentHost> { self.hosts.get_mut(target_id) } - pub(crate) fn insert(&mut self, host: PageTargetHost) -> bool { + pub(crate) fn insert(&mut self, host: PageAgentHost) -> bool { let target_id = host.target_id().to_owned(); if self.hosts.contains_key(&target_id) { return false; @@ -205,7 +205,7 @@ impl PageTargetRegistry { true } - pub(crate) fn remove(&mut self, target_id: &str) -> Option { + pub(crate) fn remove(&mut self, target_id: &str) -> Option { self.hosts.shift_remove(target_id) } @@ -223,18 +223,18 @@ impl PageTargetRegistry { true } - pub(crate) fn iter(&self) -> impl DoubleEndedIterator { + pub(crate) fn iter(&self) -> impl DoubleEndedIterator { self.hosts.values() } - pub(crate) fn iter_mut(&mut self) -> impl DoubleEndedIterator { + pub(crate) fn iter_mut(&mut self) -> impl DoubleEndedIterator { self.hosts.values_mut() } pub(crate) fn background( &self, active_id: Option, - ) -> impl DoubleEndedIterator { + ) -> impl DoubleEndedIterator { self.iter() .filter(move |host| Some(host.web_contents_id()) != active_id) } diff --git a/moli-protocol/src/conn/state/pending_renderer_command.rs b/moli-protocol/src/conn/state/pending_renderer_command.rs index 1cf38c9768..36f54672f4 100644 --- a/moli-protocol/src/conn/state/pending_renderer_command.rs +++ b/moli-protocol/src/conn/state/pending_renderer_command.rs @@ -9,8 +9,7 @@ use moli_core::{ RendererRuntimeInspectorResponseSender, }; use moli_page_types::{ - DevToolsSessionKey, FrontendCommandId, RendererAgentAttachmentId, RendererCallId, - RendererInspectorResponseDelivery, + FrontendCommandId, RendererAgentAttachmentId, RendererCallId, RendererInspectorResponseDelivery, }; use moli_protocol_cdp::{ CdpRendererCommandPolicy, CdpRendererCommandReplacement, CdpRendererCommandReplayDispatch, @@ -134,21 +133,6 @@ impl fmt::Display for DuplicatePendingRendererCommand { impl std::error::Error for DuplicatePendingRendererCommand {} -#[derive(Clone, Debug, Eq, Hash, PartialEq)] -pub(crate) struct PendingRendererCommandKey { - session_key: DevToolsSessionKey, - frontend_command_id: FrontendCommandId, -} - -impl PendingRendererCommandKey { - pub(crate) fn new(session_id: Option<&str>, frontend_command_id: u64) -> Self { - Self { - session_key: DevToolsSessionKey::from_wire_session_id(session_id), - frontend_command_id: FrontendCommandId::new(frontend_command_id), - } - } -} - #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub(crate) struct RendererCallIdExhausted; diff --git a/moli-protocol/src/conn/state/service_worker_target.rs b/moli-protocol/src/conn/state/service_worker_target.rs index 468d77a5c4..9e7478a53b 100644 --- a/moli-protocol/src/conn/state/service_worker_target.rs +++ b/moli-protocol/src/conn/state/service_worker_target.rs @@ -1105,8 +1105,43 @@ impl ServiceWorkerTargetState { cdp_request_id: u64, ) -> Option { self.session_state_mut(owner_session_id)? - .pending_inspector_awaits - .remove(cdp_request_id) + .remove_pending_inspector_await(cdp_request_id) + } + + #[cfg(test)] + pub(crate) fn has_claimed_pending_inspector_awaits_for_session( + &self, + session_id: &str, + ) -> bool { + self.session_state(session_id) + .is_some_and(DevToolsSessionState::has_claimed_pending_inspector_awaits) + } + + #[cfg(test)] + pub(crate) fn has_unclaimed_pending_inspector_awaits_for_session( + &self, + session_id: &str, + ) -> bool { + self.session_state(session_id) + .is_some_and(DevToolsSessionState::has_unclaimed_pending_inspector_awaits) + } + + pub(crate) fn claim_pending_inspector_await( + &mut self, + owner_session_id: &str, + cdp_request_id: u64, + ) -> bool { + self.session_state_mut(owner_session_id) + .is_some_and(|state| state.claim_pending_inspector_await(cdp_request_id)) + } + + pub(crate) fn take_claimed_pending_inspector_await( + &mut self, + owner_session_id: &str, + cdp_request_id: u64, + ) -> Option { + self.session_state_mut(owner_session_id)? + .take_claimed_pending_inspector_await(cdp_request_id) } fn current_run_identity( diff --git a/moli-protocol/src/conn/state/shared_worker_target.rs b/moli-protocol/src/conn/state/shared_worker_target.rs index c96d2a80d8..2de3a2a9de 100644 --- a/moli-protocol/src/conn/state/shared_worker_target.rs +++ b/moli-protocol/src/conn/state/shared_worker_target.rs @@ -700,8 +700,25 @@ impl SharedWorkerTargetState { cdp_request_id: u64, ) -> Option { self.session_state_mut(owner_session_id)? - .pending_inspector_awaits - .remove(cdp_request_id) + .remove_pending_inspector_await(cdp_request_id) + } + + pub(crate) fn claim_pending_inspector_await( + &mut self, + owner_session_id: &str, + cdp_request_id: u64, + ) -> bool { + self.session_state_mut(owner_session_id) + .is_some_and(|state| state.claim_pending_inspector_await(cdp_request_id)) + } + + pub(crate) fn take_claimed_pending_inspector_await( + &mut self, + owner_session_id: &str, + cdp_request_id: u64, + ) -> Option { + self.session_state_mut(owner_session_id)? + .take_claimed_pending_inspector_await(cdp_request_id) } pub(crate) fn has_pending_inspector_awaits(&self) -> bool { @@ -715,6 +732,24 @@ impl SharedWorkerTargetState { .is_some_and(|state| !state.pending_inspector_awaits.is_empty()) } + #[cfg(test)] + pub(crate) fn has_claimed_pending_inspector_awaits_for_session( + &self, + session_id: &str, + ) -> bool { + self.session_state(session_id) + .is_some_and(DevToolsSessionState::has_claimed_pending_inspector_awaits) + } + + #[cfg(test)] + pub(crate) fn has_unclaimed_pending_inspector_awaits_for_session( + &self, + session_id: &str, + ) -> bool { + self.session_state(session_id) + .is_some_and(DevToolsSessionState::has_unclaimed_pending_inspector_awaits) + } + pub(crate) fn pending_inspector_await_count_all_sessions(&self) -> usize { self.sessions .values() diff --git a/moli-protocol/src/conn/state/target_state.rs b/moli-protocol/src/conn/state/target_state.rs index 7e348e1143..ddf3c84cdb 100644 --- a/moli-protocol/src/conn/state/target_state.rs +++ b/moli-protocol/src/conn/state/target_state.rs @@ -74,12 +74,13 @@ impl PendingBidiChannelListener { } } -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, PartialEq, Eq)] pub(crate) struct PendingInspectorAwait { session_id: Option, object_group: Option, bidi_channel_listener: Option, renderer_correlation: Option, + scheduler_deferred_reply_claimed: bool, } impl PendingInspectorAwait { @@ -100,9 +101,13 @@ impl PendingInspectorAwait { pub(crate) fn renderer_correlation(&self) -> Option { self.renderer_correlation } + + pub(crate) const fn scheduler_deferred_reply_claimed(&self) -> bool { + self.scheduler_deferred_reply_claimed + } } -#[derive(Debug, Clone, Default, PartialEq, Eq)] +#[derive(Debug, Default, PartialEq, Eq)] pub(crate) struct TargetPendingInspectorAwaitRegistry { entries: PendingRendererCommandRegistry, } @@ -127,6 +132,7 @@ impl TargetPendingInspectorAwaitRegistry { object_group: object_group.map(str::to_owned), bidi_channel_listener: None, renderer_correlation: None, + scheduler_deferred_reply_claimed: false, }, ) } @@ -147,6 +153,7 @@ impl TargetPendingInspectorAwaitRegistry { object_group: object_group.map(str::to_owned), bidi_channel_listener: Some(listener), renderer_correlation, + scheduler_deferred_reply_claimed: false, }, ) } @@ -265,8 +272,60 @@ impl TargetPendingInspectorAwaitRegistry { self.entries.len() } + /// Claims an await for a scheduler-owned deferred reply without moving it + /// out of its DevTools session. The session registry remains the sole + /// lifetime authority, so detach can settle the command before a late + /// scheduler continuation resumes. + pub(crate) fn claim_scheduler_deferred_reply(&mut self, cdp_request_id: u64) -> bool { + let Some(entry) = self.entries.get_mut(FrontendCommandId::new(cdp_request_id)) else { + return false; + }; + if entry.scheduler_deferred_reply_claimed { + return false; + } + entry.scheduler_deferred_reply_claimed = true; + true + } + + pub(crate) fn take_claimed_scheduler_deferred_reply( + &mut self, + cdp_request_id: u64, + ) -> Option { + let frontend_command_id = FrontendCommandId::new(cdp_request_id); + if !self + .entries + .get_mut(frontend_command_id)? + .scheduler_deferred_reply_claimed + { + return None; + } + self.entries.remove(frontend_command_id) + } + pub(crate) fn remove(&mut self, cdp_request_id: u64) -> Option { - self.entries.remove(FrontendCommandId::new(cdp_request_id)) + let frontend_command_id = FrontendCommandId::new(cdp_request_id); + if self + .entries + .get_mut(frontend_command_id)? + .scheduler_deferred_reply_claimed + { + return None; + } + self.entries.remove(frontend_command_id) + } + + #[cfg(test)] + pub(crate) fn has_claimed_scheduler_deferred_reply(&self) -> bool { + self.entries + .iter() + .any(|(_, entry)| entry.scheduler_deferred_reply_claimed) + } + + #[cfg(test)] + pub(crate) fn has_unclaimed_await(&self) -> bool { + self.entries + .iter() + .any(|(_, entry)| !entry.scheduler_deferred_reply_claimed) } pub(crate) fn drain_all(&mut self) -> Vec<(u64, PendingInspectorAwait)> { diff --git a/moli-protocol/src/conn/state/tests.rs b/moli-protocol/src/conn/state/tests.rs index b4a1cc66bd..f7d86ffd33 100644 --- a/moli-protocol/src/conn/state/tests.rs +++ b/moli-protocol/src/conn/state/tests.rs @@ -47,7 +47,7 @@ fn test_navigation_dispatch_state(fetch_request_id: &str) -> NavigationDispatchS } #[test] -fn page_target_host_owns_session_state_directly() { +fn page_agent_host_owns_session_state_directly() { let mut context = BrowserContext::new("CTX-session-state".into()); context.set_active_target_id("TID-state-test"); let target = context.active_page_target_mut(); diff --git a/moli-protocol/src/conn/state/web_contents/initial_document.rs b/moli-protocol/src/conn/state/web_contents/initial_document.rs index 254bab9c28..cd0396c5a6 100644 --- a/moli-protocol/src/conn/state/web_contents/initial_document.rs +++ b/moli-protocol/src/conn/state/web_contents/initial_document.rs @@ -1,7 +1,7 @@ use moli_core::{ browser::{ - DocumentId, DocumentLifecycle, MainFrameSlotId, RendererPageResidenceIdentity, - WebContentsId, + BrowserSequence, DocumentId, DocumentLifecycle, MainFrameSlotId, + RendererPageResidenceIdentity, WebContentsId, }, runtime::{BuiltDocumentPage, PendingPreparedDocumentPage, PreparedDocumentPagePolicy}, }; @@ -308,12 +308,14 @@ impl WebContents { { self.navigation.mark_initial_empty_document_exited(); } + let browser_sequence = BrowserSequence::allocate(); completion.finish(Ok(())); Ok(CommittedInitialDocument { key, inspection_endpoint, lifecycle: CommittedDocumentLifecycle { document: key.document, + browser_sequence, artifacts: page_creation_artifacts, }, diagnostics: page_creation_diagnostics, diff --git a/moli-protocol/src/conn/state/web_contents/navigation_commit.rs b/moli-protocol/src/conn/state/web_contents/navigation_commit.rs index 74563f19e0..10fe60a4d3 100644 --- a/moli-protocol/src/conn/state/web_contents/navigation_commit.rs +++ b/moli-protocol/src/conn/state/web_contents/navigation_commit.rs @@ -1,6 +1,6 @@ use moli_core::{ browser::{ - DocumentId, DocumentLifecycle, MainFrameSlotId, NavigationId, + BrowserSequence, DocumentId, DocumentLifecycle, MainFrameSlotId, NavigationId, RendererPageResidenceIdentity, WebContentsId, }, page::{Page, RendererPageCommandPostResponseContinuation, RendererPageCreationArtifacts}, @@ -177,6 +177,7 @@ pub(crate) struct RetiringDocument(Option); /// initialize or rewind native state from a frontend projection. pub(crate) struct CommittedDocumentLifecycle { pub(crate) document: DocumentId, + pub(crate) browser_sequence: BrowserSequence, pub(crate) artifacts: RendererPageCreationArtifacts, } @@ -328,6 +329,7 @@ impl WebContents { self.navigation .commit_pending_document_navigation_if_matches(&navigation) ); + let browser_sequence = BrowserSequence::allocate(); Ok(CommittedDocumentNavigation { web_contents: self.id(), frame_slot: self.main_frame.id(), @@ -338,6 +340,7 @@ impl WebContents { inspection_endpoint, lifecycle: CommittedDocumentLifecycle { document: document_id, + browser_sequence, artifacts: prepared.creation_artifacts, }, info: prepared.info, diff --git a/moli-protocol/src/conn/target/control.rs b/moli-protocol/src/conn/target/agent_host_registry.rs similarity index 91% rename from moli-protocol/src/conn/target/control.rs rename to moli-protocol/src/conn/target/agent_host_registry.rs index 18a2ac44bc..3ccfae37bc 100644 --- a/moli-protocol/src/conn/target/control.rs +++ b/moli-protocol/src/conn/target/agent_host_registry.rs @@ -7,19 +7,20 @@ use crate::devtools_runtime::{ }; use super::{ - CommittedAttachSession, DetachedTargetSession, PreparedAttachSession, TargetClosureCleanupPlan, - TargetClosurePlan, TargetEventPlan, TargetHandlerStore, TargetHostDelta, TargetSessionRegistry, + CommittedAttachSession, DetachedTargetSession, DevToolsSessionHandlerSet, + PreparedAttachSession, TargetClosureCleanupPlan, TargetClosurePlan, TargetEventPlan, + TargetHandlerStore, TargetHostDelta, TargetSessionRegistry, }; use crate::conn::{BackgroundProtocolEvent, CdpSessionRoute, CdpTargetFilter}; -#[derive(Clone, Debug, Default, PartialEq, Eq)] -pub(crate) struct TargetControlPlane { +#[derive(Debug, Default, PartialEq, Eq)] +pub(crate) struct DevToolsAgentHostRegistry { sessions: TargetSessionRegistry, graph: TargetGraph, handlers: TargetHandlerStore, } -impl TargetControlPlane { +impl DevToolsAgentHostRegistry { pub(crate) fn register_tab(&mut self, tab_target_id: String, primary_page_target_id: String) { self.graph .register_tab(tab_target_id, primary_page_target_id); @@ -468,6 +469,13 @@ impl TargetControlPlane { self.sessions.attached_session_route(session_id) } + pub(crate) fn attached_session_handler_set( + &self, + session_id: &str, + ) -> Option { + self.sessions.attached_session_handler_set(session_id) + } + pub(crate) fn browser_session_count(&self) -> usize { self.sessions.browser_session_count() } @@ -567,7 +575,7 @@ fn target_detached_event( #[cfg(test)] mod tests { - use crate::conn::{CdpSessionRoute, CdpTargetFilter, TargetControlPlane}; + use crate::conn::{CdpSessionRoute, CdpTargetFilter, DevToolsAgentHostRegistry}; use crate::devtools_runtime::{DevToolsTargetId, DevToolsTargetInfo, DevToolsTargetKind}; fn target_info(target_id: &str, kind: DevToolsTargetKind) -> DevToolsTargetInfo { @@ -587,18 +595,18 @@ mod tests { #[test] fn target_crashed_events_only_reach_discovery_owners_that_reported_the_host() { - let mut control = TargetControlPlane::default(); - control.set_discover_targets(None, CdpTargetFilter::default_target_discovery()); - control.set_discover_targets( + let mut registry = DevToolsAgentHostRegistry::default(); + registry.set_discover_targets(None, CdpTargetFilter::default_target_discovery()); + registry.set_discover_targets( Some("SID-reported"), CdpTargetFilter::default_target_discovery(), ); - control.set_discover_targets( + registry.set_discover_targets( Some("SID-unreported"), CdpTargetFilter::default_target_discovery(), ); assert_eq!( - control + registry .initial_target_created_events_for_owner( None, vec![target_info("TID-page", DevToolsTargetKind::Page)], @@ -607,7 +615,7 @@ mod tests { 1 ); assert_eq!( - control + registry .initial_target_created_events_for_owner( Some("SID-reported"), vec![target_info("TID-page", DevToolsTargetKind::Page)], @@ -617,7 +625,7 @@ mod tests { ); let events = - control.target_crashed_events_for_all_discovery_owners("TID-page", "crashed", 5); + registry.target_crashed_events_for_all_discovery_owners("TID-page", "crashed", 5); assert_eq!(events.len(), 2); let messages = events .into_iter() @@ -633,7 +641,7 @@ mod tests { assert_eq!(messages[1]["sessionId"], "SID-reported"); assert!( - control + registry .target_crashed_events_for_all_discovery_owners("TID-never-reported", "crashed", 5,) .is_empty() ); @@ -641,15 +649,15 @@ mod tests { #[test] fn commit_auto_attached_session_event_records_committed_session_plan() { - let mut control = TargetControlPlane::default(); + let mut registry = DevToolsAgentHostRegistry::default(); let route = CdpSessionRoute::PageTarget { browser_context_id: "BID-1".to_owned(), target_id: "TID-page".to_owned(), session_key: moli_page_types::DevToolsSessionKey::Primary, }; - control.ensure_owner(Some("SID-tab")); - let plan = control.commit_attached_session_event( + registry.ensure_owner(Some("SID-tab")); + let plan = registry.commit_attached_session_event( "SID-page".to_owned(), Some("SID-tab"), "TID-page", @@ -660,11 +668,11 @@ mod tests { ); assert_eq!( - control.attached_sessions_for_target("TID-page"), + registry.attached_sessions_for_target("TID-page"), vec!["SID-page".to_owned()] ); assert_eq!( - control.auto_attached_sessions_for_owner(Some("SID-tab")), + registry.auto_attached_sessions_for_owner(Some("SID-tab")), vec!["SID-page".to_owned()] ); @@ -683,13 +691,13 @@ mod tests { #[test] fn detach_attached_session_event_plan_records_detached_session() { - let mut control = TargetControlPlane::default(); + let mut registry = DevToolsAgentHostRegistry::default(); let route = CdpSessionRoute::PageTarget { browser_context_id: "BID-1".to_owned(), target_id: "TID-page".to_owned(), session_key: moli_page_types::DevToolsSessionKey::Primary, }; - control.commit_attached_session_event( + registry.commit_attached_session_event( "SID-page".to_owned(), Some("SID-owner"), "TID-page", @@ -699,7 +707,7 @@ mod tests { target_info("TID-page", DevToolsTargetKind::Page), ); - let plan = control + let plan = registry .detach_attached_session_event_plan( "SID-page", Some("Target closed"), @@ -707,7 +715,7 @@ mod tests { ) .expect("attached session should detach"); - assert!(control.attached_sessions_for_target("TID-page").is_empty()); + assert!(registry.attached_sessions_for_target("TID-page").is_empty()); let detached_sessions = plan.detached_sessions(); assert_eq!(detached_sessions.len(), 1); let detached = &detached_sessions[0]; @@ -727,9 +735,9 @@ mod tests { #[test] fn target_closure_attached_sessions_event_plan_detaches_all_target_sessions() { - let mut control = TargetControlPlane::default(); + let mut registry = DevToolsAgentHostRegistry::default(); for session_id in ["SID-a", "SID-b"] { - control.commit_attached_session_event( + registry.commit_attached_session_event( session_id.to_owned(), None, "TID-page", @@ -744,13 +752,13 @@ mod tests { ); } - let plan = control.detach_target_closure_attached_sessions_event_plan( + let plan = registry.detach_target_closure_attached_sessions_event_plan( "TID-page", Some("Target closed"), None, ); - assert!(control.attached_sessions_for_target("TID-page").is_empty()); + assert!(registry.attached_sessions_for_target("TID-page").is_empty()); assert_eq!( plan.detached_sessions() .iter() @@ -763,9 +771,9 @@ mod tests { #[test] fn target_closure_cleanup_event_plan_detaches_declared_sessions() { - let mut control = TargetControlPlane::default(); + let mut registry = DevToolsAgentHostRegistry::default(); for session_id in ["SID-primary", "SID-attached"] { - control.commit_attached_session_event( + registry.commit_attached_session_event( session_id.to_owned(), None, "TID-page", @@ -780,7 +788,7 @@ mod tests { ); } - let plan = control.detach_target_closure_cleanup_event_plan( + let plan = registry.detach_target_closure_cleanup_event_plan( crate::conn::TargetClosureCleanupPlan::new( "TID-page", Some("Render process gone."), @@ -789,7 +797,7 @@ mod tests { None, ); - assert!(control.attached_sessions_for_target("TID-page").is_empty()); + assert!(registry.attached_sessions_for_target("TID-page").is_empty()); assert_eq!( plan.detached_sessions() .iter() @@ -802,9 +810,9 @@ mod tests { #[test] fn rollback_attached_session_without_event_clears_session_indexes() { - let mut control = TargetControlPlane::default(); - control.ensure_owner(Some("SID-tab")); - control.commit_attached_session_event( + let mut registry = DevToolsAgentHostRegistry::default(); + registry.ensure_owner(Some("SID-tab")); + registry.commit_attached_session_event( "SID-page".to_owned(), Some("SID-tab"), "TID-page", @@ -818,21 +826,21 @@ mod tests { target_info("TID-page", DevToolsTargetKind::Page), ); - let rollback_plan = control.rollback_attached_session_without_event("SID-page"); + let rollback_plan = registry.rollback_attached_session_without_event("SID-page"); assert_eq!( rollback_plan.rolled_back_session_ids(), &["SID-page".to_owned()] ); assert!(rollback_plan.into_background_events().is_empty()); - assert!(control.attached_sessions_for_target("TID-page").is_empty()); + assert!(registry.attached_sessions_for_target("TID-page").is_empty()); assert!( - control + registry .auto_attached_sessions_for_owner(Some("SID-tab")) .is_empty() ); assert!( - control + registry .rollback_attached_session_without_event("SID-page") .rolled_back_session_ids() .is_empty() diff --git a/moli-protocol/src/conn/target/auto_attach_owner.rs b/moli-protocol/src/conn/target/auto_attach_owner.rs index 5bbb6c2350..d8f70f5513 100644 --- a/moli-protocol/src/conn/target/auto_attach_owner.rs +++ b/moli-protocol/src/conn/target/auto_attach_owner.rs @@ -45,7 +45,7 @@ impl CdpConnection { self.clear_service_worker_auto_attach_related_owner(session_id); let key = session_id.map(str::to_owned); if enabled { - self.target_control.ensure_owner(session_id); + self.agent_hosts.ensure_owner(session_id); self.auto_attach_owner_sessions.insert( key, AutoAttachOwnerPolicy { diff --git a/moli-protocol/src/conn/target/mod.rs b/moli-protocol/src/conn/target/mod.rs index 5e834bd0ea..e0fbc92d68 100644 --- a/moli-protocol/src/conn/target/mod.rs +++ b/moli-protocol/src/conn/target/mod.rs @@ -1,6 +1,6 @@ mod activation; +mod agent_host_registry; mod auto_attach_owner; -mod control; mod default_target; mod graph; mod observer; @@ -13,13 +13,14 @@ mod worker_auto_attach; mod worker_session; pub(crate) use activation::TargetActivationTransition; -pub(crate) use control::TargetControlPlane; +pub(crate) use agent_host_registry::DevToolsAgentHostRegistry; pub(crate) use default_target::{DEFAULT_BROWSER_CONTEXT_ID, DefaultTargetLifecycle}; pub(crate) use graph::{TargetClosurePlan, TargetHostDelta}; pub(crate) use observer::{TargetHandlerStore, target_destroyed_automation_events}; pub(crate) use route::{CdpSessionRoute, TargetHandlerAccessMode}; pub(crate) use session::{ - CommittedAttachSession, DetachedTargetSession, PreparedAttachSession, TargetSessionRegistry, + CommittedAttachSession, DetachedTargetSession, DevToolsSessionHandlerSet, + PreparedAttachSession, TargetSessionRegistry, }; pub(crate) use transaction::{ PreparedTargetAttach, PreparedTargetHostClosure, PreparedTargetHostDelta, SessionDisposalPlan, diff --git a/moli-protocol/src/conn/target/route.rs b/moli-protocol/src/conn/target/route.rs index 765804b93f..fdfdfd7233 100644 --- a/moli-protocol/src/conn/target/route.rs +++ b/moli-protocol/src/conn/target/route.rs @@ -177,7 +177,7 @@ impl CdpConnection { .as_deref() == Some(target_id) || self - .target_control + .agent_hosts .auto_attached_target_ids_for_owner(Some(session_id)) .iter() .any(|attached_target_id| attached_target_id == target_id) @@ -185,9 +185,7 @@ impl CdpConnection { pub(crate) fn session_route(&self, session_id: Option<&str>) -> Option { let session_id = session_id?; - self.target_control - .attached_session_route(session_id) - .cloned() + self.agent_hosts.attached_session_route(session_id).cloned() } pub(crate) fn target_session_route_for_target_id( @@ -313,7 +311,7 @@ mod tests { target_id: "TID-a".to_owned(), session_key: DevToolsSessionKey::Primary, }; - connection.target_control.commit_attached_session( + connection.agent_hosts.commit_attached_session( "SID-a".to_owned(), None, "TID-a", diff --git a/moli-protocol/src/conn/target/session.rs b/moli-protocol/src/conn/target/session.rs index 4cdff74391..31ab5f2f3d 100644 --- a/moli-protocol/src/conn/target/session.rs +++ b/moli-protocol/src/conn/target/session.rs @@ -2,12 +2,36 @@ use std::collections::{HashMap, HashSet}; use crate::conn::CdpSessionRoute; +/// Browser-side domain handlers installed for one attached DevTools session. +/// The attachment registry freezes this class at commit and disposal carries +/// it forward instead of reconstructing ownership from teardown state. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) enum DevToolsSessionHandlerSet { + Connection, + Page, + Worker, +} + +impl DevToolsSessionHandlerSet { + pub(crate) fn for_route(route: &CdpSessionRoute) -> Option { + match route { + CdpSessionRoute::Browser | CdpSessionRoute::TabTarget { .. } => Some(Self::Connection), + CdpSessionRoute::PageTarget { .. } => Some(Self::Page), + CdpSessionRoute::SharedWorkerTarget { .. } + | CdpSessionRoute::DedicatedWorkerTarget { .. } + | CdpSessionRoute::ServiceWorkerTarget { .. } => Some(Self::Worker), + CdpSessionRoute::BrowserContext { .. } => None, + } + } +} + #[derive(Clone, Debug, PartialEq, Eq)] pub(crate) struct PreparedAttachSession { session_id: String, owner_session_id: Option, target_id: String, route: CdpSessionRoute, + handler_set: DevToolsSessionHandlerSet, auto_attached: bool, waiting_for_debugger: bool, } @@ -21,11 +45,14 @@ impl PreparedAttachSession { auto_attached: bool, waiting_for_debugger: bool, ) -> Self { + let handler_set = DevToolsSessionHandlerSet::for_route(&route) + .expect("attached DevTools session must have a disposable route"); Self { session_id, owner_session_id: owner_session_id.map(str::to_owned), target_id: target_id.to_owned(), route, + handler_set, auto_attached, waiting_for_debugger, } @@ -38,6 +65,7 @@ pub(crate) struct CommittedAttachSession { owner_session_id: Option, target_id: String, route: CdpSessionRoute, + handler_set: DevToolsSessionHandlerSet, auto_attached: bool, waiting_for_debugger: bool, } @@ -154,11 +182,12 @@ struct AttachedTargetSession { owner_session_id: Option, target_id: String, route: CdpSessionRoute, + handler_set: DevToolsSessionHandlerSet, auto_attached: bool, waiting_for_debugger: bool, } -#[derive(Clone, Debug, Default, PartialEq, Eq)] +#[derive(Debug, Default, PartialEq, Eq)] pub(crate) struct TargetSessionRegistry { attached_sessions: HashMap, attached_sessions_by_target: HashMap>, @@ -177,6 +206,7 @@ impl TargetSessionRegistry { let owner_session_id = prepared.owner_session_id; let target_id = prepared.target_id; let route = prepared.route; + let handler_set = prepared.handler_set; let auto_attached = prepared.auto_attached; let waiting_for_debugger = prepared.waiting_for_debugger; @@ -187,6 +217,7 @@ impl TargetSessionRegistry { owner_session_id: owner_session_id.clone(), target_id: target_id.clone(), route: route.clone(), + handler_set, auto_attached, waiting_for_debugger, }, @@ -213,6 +244,7 @@ impl TargetSessionRegistry { owner_session_id, target_id, route, + handler_set, auto_attached, waiting_for_debugger, } @@ -343,6 +375,15 @@ impl TargetSessionRegistry { .map(|session| &session.route) } + pub(crate) fn attached_session_handler_set( + &self, + session_id: &str, + ) -> Option { + self.attached_sessions + .get(session_id) + .map(|session| session.handler_set) + } + pub(crate) fn browser_session_count(&self) -> usize { self.attached_sessions .values() @@ -562,8 +603,8 @@ fn test_route_for_target(session_id: &str, target_id: &str) -> CdpSessionRoute { #[cfg(test)] mod tests { use super::{ - PreparedAttachSession, PreparedAutoAttachSession, TargetSessionRegistry, - test_route_for_target, + DevToolsSessionHandlerSet, PreparedAttachSession, PreparedAutoAttachSession, + TargetSessionRegistry, test_route_for_target, }; use crate::conn::CdpSessionRoute; @@ -731,11 +772,13 @@ mod tests { false, false, )); + let expected_handlers = DevToolsSessionHandlerSet::Connection; assert_eq!(committed.session_id, "SID-tab"); assert_eq!(committed.owner_session_id.as_deref(), Some("SID-browser")); assert_eq!(committed.target_id, "TAB-TID-page"); assert_eq!(committed.route, route); + assert_eq!(committed.handler_set, expected_handlers); assert!(!committed.auto_attached); assert!(!committed.waiting_for_debugger); assert_eq!( @@ -743,6 +786,10 @@ mod tests { vec!["SID-tab".to_owned()] ); assert_eq!(registry.attached_session_route("SID-tab"), Some(&route)); + assert_eq!( + registry.attached_session_handler_set("SID-tab"), + Some(expected_handlers) + ); assert!( registry .auto_attached_sessions_for_owner(Some("SID-browser")) diff --git a/moli-protocol/src/conn/target/session_binding.rs b/moli-protocol/src/conn/target/session_binding.rs index f714eff84d..660c35f7d3 100644 --- a/moli-protocol/src/conn/target/session_binding.rs +++ b/moli-protocol/src/conn/target/session_binding.rs @@ -10,8 +10,17 @@ use super::{ }; impl CdpConnection { + /// Freezes the exact AgentHost route and installed handler set before + /// asynchronous cleanup. The registry entry stays live until the caller + /// commits this plan's binding removal. + pub(crate) fn session_disposal_plan(&self, session_id: &str) -> Option { + let route = self.agent_hosts.attached_session_route(session_id)?; + let handler_set = self.agent_hosts.attached_session_handler_set(session_id)?; + SessionDisposalPlan::for_attached_session(session_id, route, handler_set) + } + /// Commits target-side session declarations built by low-level test - /// fixtures into the same control-plane registry used by production + /// fixtures into the same AgentHost registry used by production /// attachment transactions. /// /// Older tests construct `BrowserContext` and worker targets directly, @@ -91,13 +100,13 @@ impl CdpConnection { for (session_id, target_id, route) in declared { if self - .target_control + .agent_hosts .attached_session_route(&session_id) .is_some() { continue; } - self.target_control + self.agent_hosts .commit_attached_session(session_id, None, &target_id, route, false, false); } } @@ -199,12 +208,12 @@ impl CdpConnection { let Some(session_id) = session_id else { return false; }; - self.target_control.attached_session_route(session_id) == Some(&CdpSessionRoute::Browser) + self.agent_hosts.attached_session_route(session_id) == Some(&CdpSessionRoute::Browser) } #[cfg(test)] pub(crate) fn register_browser_session(&mut self, session_id: String) { - self.target_control.commit_attached_session( + self.agent_hosts.commit_attached_session( session_id, None, "browser", @@ -236,19 +245,19 @@ impl CdpConnection { "InvalidSessionId" ); let owner_session_id = self - .target_control + .agent_hosts .attached_session_owner_session_id(plan.session_id()) .map(str::to_owned); let session_id = plan.session_id().to_owned(); let event_plan = self - .target_control + .agent_hosts .detach_attached_session_event_plan( plan.session_id(), None, owner_session_id.as_deref(), ) .ok_or_else(|| anyhow::anyhow!("InvalidSessionId"))?; - self.remove_detached_session_control_owner(&session_id); + self.remove_detached_session_handler_owner(&session_id); Ok(event_plan) } @@ -257,7 +266,7 @@ impl CdpConnection { self.cancel_tracing_for_session_owner(None); self.clear_auto_attach_owner(None); self.clear_target_discovery_for_owner(None); - self.target_control.remove_owner(None); + self.agent_hosts.remove_owner(None); } pub(crate) fn release_primary_target_session_binding_without_event( @@ -292,7 +301,7 @@ impl CdpConnection { let target_id = route.target_id().unwrap_or_else(|| { panic!("test auto-attached session route must identify a target: {route:?}") }); - self.target_control.commit_auto_attached_session_for_target( + self.agent_hosts.commit_auto_attached_session_for_target( session_id, owner_session_id, target_id, @@ -320,7 +329,7 @@ impl CdpConnection { route: CdpSessionRoute, ) { let target_id = route.target_id().unwrap_or("browser").to_owned(); - self.target_control.commit_attached_session( + self.agent_hosts.commit_attached_session( session_id.to_owned(), None, &target_id, @@ -358,10 +367,9 @@ impl CdpConnection { let (session_id, owner_session_id, route, auto_attached, waiting_for_debugger) = session.into_parts(); if auto_attached { - self.target_control - .ensure_owner(owner_session_id.as_deref()); + self.agent_hosts.ensure_owner(owner_session_id.as_deref()); } - plan.extend(self.target_control.commit_attached_session_event( + plan.extend(self.agent_hosts.commit_attached_session_event( session_id, owner_session_id.as_deref(), &target_id, @@ -690,7 +698,7 @@ impl CdpConnection { target_id: &str, target_info: DevToolsTargetInfo, ) -> TargetEventPlan { - self.target_control.commit_attached_session_event( + self.agent_hosts.commit_attached_session_event( session_id, owner_session_id, target_id, @@ -706,10 +714,10 @@ impl CdpConnection { session_id: &str, ) -> TargetEventPlan { let plan = self - .target_control + .agent_hosts .rollback_attached_session_without_event(session_id); for session_id in plan.rolled_back_session_ids() { - self.remove_detached_session_control_owner(session_id); + self.remove_detached_session_handler_owner(session_id); } plan } @@ -740,7 +748,7 @@ impl CdpConnection { ) -> TargetEventPlan { let attached_state_delta_plan = emit_attached_state_delta .then(|| self.exact_target_info_changed_event_plan_for_target_delta(target_id)); - let mut plan = self.target_control.detach_known_session_event_plan( + let mut plan = self.agent_hosts.detach_known_session_event_plan( target_id, session_id, reason, @@ -750,7 +758,7 @@ impl CdpConnection { .detached_sessions() .iter() .any(|session| session.target_id() == target_id && session.was_waiting_for_debugger()); - self.remove_detached_session_control_owner(session_id); + self.remove_detached_session_handler_owner(session_id); if let Some(attached_state_delta_plan) = attached_state_delta_plan { plan.extend(attached_state_delta_plan); } @@ -773,18 +781,10 @@ impl CdpConnection { .map(str::to_owned) .collect::>(); for session_id in session_ids { - let Some(route) = self.session_route(Some(&session_id)) else { + let Some(disposal_plan) = self.session_disposal_plan(&session_id) else { tracing::warn!( session_id, - "closed target session no longer has an authoritative route" - ); - continue; - }; - let Some(disposal_plan) = SessionDisposalPlan::for_session_route(&session_id, &route) - else { - tracing::warn!( - session_id, - "closed target session does not support target disposal" + "closed target session no longer has an authoritative disposal binding" ); continue; }; @@ -800,10 +800,10 @@ impl CdpConnection { parent_session_id: Option<&str>, ) -> TargetEventPlan { let plan = self - .target_control + .agent_hosts .detach_target_closure_cleanup_event_plan(cleanup_plan, parent_session_id); for session in plan.detached_sessions() { - self.remove_detached_session_control_owner(session.session_id()); + self.remove_detached_session_handler_owner(session.session_id()); } plan } @@ -907,7 +907,7 @@ impl CdpConnection { %error, "failed to clean prepared target binding during attach rollback" ); - // Keep both the domain binding and its control-plane route as + // Keep both the domain binding and its AgentHost route as // retry authority. Dropping only the latter would make any // renderer-owned state unreachable. return TargetEventPlan::default(); @@ -919,10 +919,10 @@ impl CdpConnection { &self, session_id: &str, ) -> TargetAutoAttachedSessionDetachPlan { - let route = self.session_route(Some(session_id)).unwrap_or_else(|| { - panic!("committed auto-attached session {session_id} must retain its route") + let disposal_plan = self.session_disposal_plan(session_id).unwrap_or_else(|| { + panic!("committed auto-attached session {session_id} must retain its disposal binding") }); - TargetAutoAttachedSessionDetachPlan::from_session_route(session_id, route) + TargetAutoAttachedSessionDetachPlan::from_session_disposal_plan(disposal_plan) } pub(crate) fn rollback_auto_attached_session_detach_plan_without_event( @@ -1034,7 +1034,7 @@ impl CdpConnection { let parent_session_id = cleanup_plan .parent_session_id() .or_else(|| { - self.target_control + self.agent_hosts .attached_session_owner_session_id(&session_id) }) .map(str::to_owned); @@ -1056,16 +1056,16 @@ impl CdpConnection { } } - fn remove_detached_session_control_owner(&mut self, session_id: &str) { - self.target_control.remove_owner(Some(session_id)); + fn remove_detached_session_handler_owner(&mut self, session_id: &str) { + self.agent_hosts.remove_owner(Some(session_id)); } pub(crate) fn attached_sessions_for_target(&self, target_id: &str) -> Vec { - self.target_control.attached_sessions_for_target(target_id) + self.agent_hosts.attached_sessions_for_target(target_id) } pub(crate) fn target_has_waiting_for_debugger_session(&self, target_id: &str) -> bool { - self.target_control + self.agent_hosts .target_has_waiting_for_debugger_session(target_id) } @@ -1074,7 +1074,7 @@ impl CdpConnection { session_id: Option<&str>, ) -> bool { session_id.is_some_and(|session_id| { - self.target_control + self.agent_hosts .release_waiting_for_debugger_session(session_id) }) } @@ -1083,7 +1083,7 @@ impl CdpConnection { &self, owner_session_id: Option<&str>, ) -> Vec { - self.target_control + self.agent_hosts .auto_attached_sessions_for_owner(owner_session_id) } @@ -1091,12 +1091,12 @@ impl CdpConnection { &self, owner_session_id: Option<&str>, ) -> Vec { - self.target_control + self.agent_hosts .attached_session_cascade_for_owner(owner_session_id) } pub(crate) fn attached_session_cascade_for_root_frontend(&self) -> Vec { - self.target_control + self.agent_hosts .attached_session_cascade_for_root_frontend() } @@ -1104,7 +1104,7 @@ impl CdpConnection { &self, owner_session_id: Option<&str>, ) -> Vec { - self.target_control + self.agent_hosts .auto_attached_session_cascade_for_owner(owner_session_id) } } diff --git a/moli-protocol/src/conn/target/transaction.rs b/moli-protocol/src/conn/target/transaction.rs index 8af7348cf0..4b08d0b2b0 100644 --- a/moli-protocol/src/conn/target/transaction.rs +++ b/moli-protocol/src/conn/target/transaction.rs @@ -2,7 +2,9 @@ use crate::conn::{BackgroundProtocolEvent, CdpSessionRoute}; use crate::devtools_runtime::DevToolsTargetInfo; use moli_page_types::DevToolsSessionKey; -use super::{CommittedAttachSession, DetachedTargetSession, TargetHostDelta}; +use super::{ + CommittedAttachSession, DetachedTargetSession, DevToolsSessionHandlerSet, TargetHostDelta, +}; #[derive(Clone, Debug, Default, PartialEq)] pub(crate) struct TargetEventPlan { @@ -305,19 +307,35 @@ impl PreparedTargetAttach { pub(crate) struct SessionDisposalPlan { session_id: String, target: SessionDisposalTarget, + handler_set: DevToolsSessionHandlerSet, } impl SessionDisposalPlan { - /// Freezes the exact binding that must remain alive while every domain - /// handler disables its session-owned state. - /// - /// Callers must execute the domain cleanup phase before committing this - /// plan's binding removal. Keeping both phases tied to one value prevents - /// detach paths from resolving the session again after asynchronous work. - pub(crate) fn for_session_route(session_id: &str, route: &CdpSessionRoute) -> Option { + /// Attach rollback has no committed registry entry, so it derives the + /// otherwise immutable handler set from the prepared route. + fn for_uncommitted_session_route(session_id: &str, route: &CdpSessionRoute) -> Option { + let target = SessionDisposalTarget::from_route(route)?; Some(Self { session_id: session_id.to_owned(), - target: SessionDisposalTarget::from_route(route)?, + handler_set: DevToolsSessionHandlerSet::for_route(route)?, + target, + }) + } + + pub(crate) fn for_attached_session( + session_id: &str, + route: &CdpSessionRoute, + handler_set: DevToolsSessionHandlerSet, + ) -> Option { + let target = SessionDisposalTarget::from_route(route)?; + debug_assert_eq!( + Some(handler_set), + DevToolsSessionHandlerSet::for_route(route) + ); + Some(Self { + session_id: session_id.to_owned(), + target, + handler_set, }) } @@ -333,6 +351,10 @@ impl SessionDisposalPlan { &self.target } + pub(crate) fn handler_set(&self) -> DevToolsSessionHandlerSet { + self.handler_set + } + pub(crate) fn target_id(&self) -> Option<&str> { self.target.target_id() } @@ -451,7 +473,8 @@ pub(crate) struct TargetAttachRollbackPlan { impl TargetAttachRollbackPlan { pub(crate) fn from_prepared_attach_session(prepared: &TargetAttachSessionCommit) -> Self { let session_id = prepared.session_id().to_owned(); - let cleanup_plan = SessionDisposalPlan::for_session_route(&session_id, prepared.route()); + let cleanup_plan = + SessionDisposalPlan::for_uncommitted_session_route(&session_id, prepared.route()); Self { session_id, cleanup_plan, @@ -474,20 +497,27 @@ pub(crate) enum TargetAutoAttachedSessionDetachPlan { } impl TargetAutoAttachedSessionDetachPlan { + #[cfg(test)] pub(crate) fn from_session_route( session_id: impl Into, route: CdpSessionRoute, ) -> Self { let session_id = session_id.into(); - if matches!(route, CdpSessionRoute::Browser) { - return Self::Rollback { session_id }; - } - match SessionDisposalPlan::for_session_route(&session_id, &route) { - Some(cleanup_plan) => Self::Detach { cleanup_plan }, + match SessionDisposalPlan::for_uncommitted_session_route(&session_id, &route) { + Some(cleanup_plan) => Self::from_session_disposal_plan(cleanup_plan), None => Self::Rollback { session_id }, } } + pub(crate) fn from_session_disposal_plan(cleanup_plan: SessionDisposalPlan) -> Self { + if matches!(cleanup_plan.target(), SessionDisposalTarget::Browser) { + return Self::Rollback { + session_id: cleanup_plan.session_id().to_owned(), + }; + } + Self::Detach { cleanup_plan } + } + pub(crate) fn session_id(&self) -> &str { match self { Self::Rollback { session_id } => session_id, @@ -606,15 +636,17 @@ mod tests { #[test] fn target_binding_cleanup_plan_maps_route_to_cleanup_action() { - let browser = - SessionDisposalPlan::for_session_route("SID-browser", &CdpSessionRoute::Browser) - .expect("Browser sessions require domain cleanup"); + let browser = SessionDisposalPlan::for_uncommitted_session_route( + "SID-browser", + &CdpSessionRoute::Browser, + ) + .expect("Browser sessions require domain cleanup"); assert_eq!(browser.target(), &SessionDisposalTarget::Browser); assert_eq!(browser.browser_context_id(), None); assert_eq!(browser.target_id(), None); assert_eq!( - SessionDisposalPlan::for_session_route( + SessionDisposalPlan::for_uncommitted_session_route( "SID-active", &CdpSessionRoute::PageTarget { browser_context_id: "BID-1".to_owned(), @@ -632,7 +664,7 @@ mod tests { ); assert_eq!( - SessionDisposalPlan::for_session_route( + SessionDisposalPlan::for_uncommitted_session_route( "SID-bg", &CdpSessionRoute::PageTarget { browser_context_id: "BID-1".to_owned(), @@ -650,7 +682,7 @@ mod tests { ); assert_eq!( - SessionDisposalPlan::for_session_route( + SessionDisposalPlan::for_uncommitted_session_route( "SID-tab", &CdpSessionRoute::TabTarget { browser_context_id: "BID-1".to_owned(), diff --git a/moli-protocol/src/conn/tests/mod.rs b/moli-protocol/src/conn/tests/mod.rs index 1fbed3197c..80662e8992 100644 --- a/moli-protocol/src/conn/tests/mod.rs +++ b/moli-protocol/src/conn/tests/mod.rs @@ -790,7 +790,7 @@ async fn memory_diagnostics_reports_page_vm_document_isolate_model() { assert_eq!( diagnostics["isolateScope"]["loadedDocumentRendererOwnerCount"], json!(2), - "each loaded PageTargetHost owns an independently schedulable renderer owner" + "each loaded PageAgentHost owns an independently schedulable renderer owner" ); assert_eq!( diagnostics["isolateScope"]["estimatedDocumentIsolateCount"], @@ -978,7 +978,7 @@ async fn memory_diagnostics_excludes_empty_page_hosts_from_document_isolates() { let empty_page_engine = conn .browser_context_by_id("BID-empty-page") .and_then(|context| context.page_navigation_engine("TID-empty-page")) - .expect("empty PageTargetHost must own its navigation engine"); + .expect("empty PageAgentHost must own its navigation engine"); assert!( !empty_page_engine.shares_renderer_owner_with(conn.standalone_navigation_engine.ensure()), "test setup must keep a distinct renderer owner without a loaded document" @@ -1177,10 +1177,10 @@ fn memory_diagnostics_counts_isolated_page_engines_by_renderer_owner() { .browser_context .as_ref() .and_then(|context| context.page_navigation_engine("TID-shared-diagnostics-bg")) - .expect("background PageTargetHost must own an engine"); + .expect("background PageAgentHost must own an engine"); assert!( !background.shares_renderer_owner_with(active), - "same-context PageTargetHost engines must remain independently schedulable" + "same-context PageAgentHost engines must remain independently schedulable" ); let diagnostics = conn.moli_memory_diagnostics(); @@ -1188,7 +1188,7 @@ fn memory_diagnostics_counts_isolated_page_engines_by_renderer_owner() { assert_eq!( diagnostics["connection"]["pageNavigationEngineCount"], json!(2), - "each PageTargetHost must expose one resident NavigationEngine" + "each PageAgentHost must expose one resident NavigationEngine" ); assert_eq!( diagnostics["isolateScope"]["pageNavigationEngineRendererOwnerCount"], @@ -1243,7 +1243,7 @@ fn page_navigation_engines_remain_target_local_across_selection() { .browser_identity() .user_agent(), "Moli/Page-First", - "selection must not replace or move a PageTargetHost engine" + "selection must not replace or move a PageAgentHost engine" ); assert_eq!( conn.browser_context diff --git a/moli-protocol/src/domains/accessibility.rs b/moli-protocol/src/domains/accessibility.rs index 453c2820e5..3fc922c841 100644 --- a/moli-protocol/src/domains/accessibility.rs +++ b/moli-protocol/src/domains/accessibility.rs @@ -1,4 +1,4 @@ -use crate::conn::{CdpConnection, Cmd, CommandOwnerScope}; +use crate::conn::{CdpConnection, Cmd, CommandOwnerScope, RendererDispatchLane}; use crate::domains::actions::AccessibilityAction; use crate::domains::command_output::CommandOutputPlan; use moli_core::page::{ @@ -29,6 +29,11 @@ pub(crate) enum AccessibilityCommandDispatchStep { Complete(CommandOutputPlan), } +pub(crate) fn command_waits_for_document_projection(cmd: &Cmd<'_>) -> bool { + cmd.parse_action::() + .is_some_and(AccessibilityAction::queries_tree) +} + enum PendingAccessibilityCommandKind { TopFrameFullTree { max_depth: Option, @@ -74,6 +79,12 @@ struct PendingAccessibilityCommandStartError { } impl PendingAccessibilityCommandDispatch { + pub(crate) fn renderer_dispatch_lane(&self) -> Option { + self.pending + .renderer_agent_attachment_id() + .map(|_| RendererDispatchLane::Main) + } + fn from_command( conn: &CdpConnection, cmd: &Cmd<'_>, diff --git a/moli-protocol/src/domains/activity/main_document.rs b/moli-protocol/src/domains/activity/main_document.rs index 85a5561647..cdbe0c5eb9 100644 --- a/moli-protocol/src/domains/activity/main_document.rs +++ b/moli-protocol/src/domains/activity/main_document.rs @@ -1389,6 +1389,7 @@ mod tests { frame_id: "FRAME-1".to_owned(), loader_id: "LID-1".to_owned(), document_id: crate::conn::DocumentId::from_raw_for_test(1), + browser_sequence: moli_core::browser::BrowserSequence::allocate(), document_open_replacement_epoch: None, }; let renderer_lifecycle_events = vec![ diff --git a/moli-protocol/src/domains/activity/mod.rs b/moli-protocol/src/domains/activity/mod.rs index f1bad07f1d..268784fd92 100644 --- a/moli-protocol/src/domains/activity/mod.rs +++ b/moli-protocol/src/domains/activity/mod.rs @@ -5,7 +5,7 @@ mod output_payloads; mod output_slot; mod output_work; mod publication_route; -mod runtime_command_barrier; +mod renderer_command_response_order; mod scheduler_work; mod subresource; @@ -26,9 +26,9 @@ pub(crate) use output_work::{ ProtocolOutputWork, RootFrameStoppedLoadingSettlement, RootFrameStoppedLoadingSettlementError, }; pub(crate) use publication_route::{RendererPublicationOwner, renderer_publication_owners}; -pub use runtime_command_barrier::{ - RuntimeCommandOutputBarrierCompletion, RuntimeCommandOutputBarrierPermit, - RuntimeCommandOutputBarrierTerminal, RuntimeCommandOutputBarriers, +pub use renderer_command_response_order::{ + RendererCommandResponseCompletion, RendererCommandResponseOrder, RendererCommandResponsePermit, + RendererCommandResponseTerminal, }; pub(crate) use scheduler_work::ReadyProtocolSchedulerWork; pub use scheduler_work::{ diff --git a/moli-protocol/src/domains/activity/output_ingress/prepared_outputs.rs b/moli-protocol/src/domains/activity/output_ingress/prepared_outputs.rs index 064656a2ce..cbc969e9ab 100644 --- a/moli-protocol/src/domains/activity/output_ingress/prepared_outputs.rs +++ b/moli-protocol/src/domains/activity/output_ingress/prepared_outputs.rs @@ -14,8 +14,8 @@ use crate::domains::page::{PagePreparedOutputSlot, SLOT_TOP_LEVEL_LOCATION_NAVIG /// /// The batch owns typed payload exactly once and preserves the producer's /// explicit FIFO order. It cannot select renderer work or inspect current renderer state. Consumers -/// must either project it, hold its remaining after-response slots behind an -/// exact command barrier, or consume only its owner actions during stale +/// must either project it, hold its remaining after-response slots for an +/// exact command response permit, or consume only its owner actions during stale /// cleanup. #[derive(Debug)] #[must_use = "prepared protocol outputs must be projected, held, or cleaned up exactly once"] diff --git a/moli-protocol/src/domains/activity/output_ingress/renderer.rs b/moli-protocol/src/domains/activity/output_ingress/renderer.rs index dc1259e9f5..81898b339f 100644 --- a/moli-protocol/src/domains/activity/output_ingress/renderer.rs +++ b/moli-protocol/src/domains/activity/output_ingress/renderer.rs @@ -5,7 +5,7 @@ use std::collections::VecDeque; use super::super::publication_route::RendererPublicationOwner; use super::super::publication_route::RendererPublicationProjection; use super::super::publication_route::RendererPublicationRoute; -use super::super::runtime_command_barrier::RuntimeCommandOutputBarriers; +use super::super::renderer_command_response_order::RendererCommandResponseOrder; use super::prepared_outputs::PreparedProtocolOutputs; use crate::conn::{ BackgroundProtocolEvent, CdpConnection, CommandDispatchContext, CommandOwnerScope, @@ -52,7 +52,7 @@ fn renderer_owner_action_owner( pub(crate) async fn ingest_renderer_output_transport_async( conn: &mut CdpConnection, publication: RendererOutputTransportMessage, - barriers: &mut RuntimeCommandOutputBarriers, + order: &mut RendererCommandResponseOrder, command_context: &mut CommandDispatchContext, ) -> Vec { match publication { @@ -83,7 +83,7 @@ pub(crate) async fn ingest_renderer_output_transport_async( while let Some(output) = ready.pop_front() { let (output, owner) = output.into_parts(); let cursor = output.cursor(); - ingest_renderer_output_publication(conn, output, owner, barriers, command_context) + ingest_renderer_output_publication(conn, output, owner, order, command_context) .await; match conn.complete_renderer_output_projection(cursor) { super::RendererOutputIngressAdmission::Ready(next) => ready.extend(next), @@ -102,7 +102,7 @@ async fn ingest_renderer_output_publication( conn: &mut CdpConnection, publication: RendererOutputPublication, owner: RendererPublicationOwner, - barriers: &mut RuntimeCommandOutputBarriers, + order: &mut RendererCommandResponseOrder, command_context: &mut CommandDispatchContext, ) { let cursor = publication.cursor(); @@ -150,7 +150,7 @@ async fn ingest_renderer_output_publication( records, cursor, projection, - barriers, + order, command_context, ) .await; @@ -166,7 +166,7 @@ async fn ingest_renderer_output_publication( records, cursor, projection, - barriers, + order, command_context, ) .await; @@ -180,7 +180,7 @@ async fn project_renderer_output_records_for_owner( records: Vec, cursor: moli_core::RendererOutputCursor, projection: RendererPublicationProjection, - barriers: &mut RuntimeCommandOutputBarriers, + order: &mut RendererCommandResponseOrder, command_context: &mut CommandDispatchContext, ) { for record in records { @@ -243,7 +243,7 @@ async fn project_renderer_output_records_for_owner( action, ) .await; - barriers + order .route_publication_outputs( conn, &action_owner, @@ -289,7 +289,7 @@ async fn project_renderer_output_records_for_owner( &observation, ) }; - barriers + order .route_publication_outputs( conn, owner, @@ -410,7 +410,7 @@ mod tests { }) .collect(); let cursor = RendererOutputCursor::new_for_test(stream, 1); - let mut barriers = RuntimeCommandOutputBarriers::default(); + let mut order = RendererCommandResponseOrder::default(); let mut commands = CommandDispatchContext::default(); if let Some(projection) = projection { project_renderer_output_records_for_owner( @@ -419,7 +419,7 @@ mod tests { records, cursor, projection, - &mut barriers, + &mut order, &mut commands, ) .await; @@ -439,7 +439,7 @@ mod tests { &mut conn, RendererOutputPublication::new_for_test(cursor, records), missing_route, - &mut barriers, + &mut order, &mut commands, ) .await; diff --git a/moli-protocol/src/domains/activity/runtime_command_barrier.rs b/moli-protocol/src/domains/activity/renderer_command_response_order.rs similarity index 73% rename from moli-protocol/src/domains/activity/runtime_command_barrier.rs rename to moli-protocol/src/domains/activity/renderer_command_response_order.rs index 4c383ca76b..836d36b0e2 100644 --- a/moli-protocol/src/domains/activity/runtime_command_barrier.rs +++ b/moli-protocol/src/domains/activity/renderer_command_response_order.rs @@ -8,40 +8,40 @@ use crate::conn::{ TargetPageResidenceIdentity, }; -/// The single-use authority for one Runtime command's causal output barrier. +/// The single-use authority for one renderer command's response-order boundary. /// /// The permit is deliberately move-only. Releasing or canceling it removes /// exactly one command predecessor from held output; cloning it would make the -/// barrier terminal ambiguous. +/// response-order terminal ambiguous. #[derive(Debug)] -#[must_use = "a Runtime command output barrier must be released or canceled exactly once"] -pub struct RuntimeCommandOutputBarrierPermit { - id: RuntimeCommandOutputBarrierId, +#[must_use = "a renderer command response permit must be released or canceled exactly once"] +pub struct RendererCommandResponsePermit { + id: RendererCommandResponsePermitId, command_id: u64, } #[derive(Clone, Copy, Debug, Default, Eq, Ord, PartialEq, PartialOrd)] -struct RuntimeCommandOutputBarrierId(u64); +struct RendererCommandResponsePermitId(u64); -impl RuntimeCommandOutputBarrierId { +impl RendererCommandResponsePermitId { fn checked_next(self) -> Self { Self( self.0 .checked_add(1) - .expect("Runtime command output barrier id overflow"), + .expect("renderer command response ordering id overflow"), ) } } #[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] -struct RuntimeCommandOutputHoldId(u64); +struct RendererCommandOutputHoldId(u64); -impl RuntimeCommandOutputHoldId { +impl RendererCommandOutputHoldId { fn checked_next(self) -> Self { Self( self.0 .checked_add(1) - .expect("Runtime command held-output id overflow"), + .expect("renderer command held-output id overflow"), ) } } @@ -56,13 +56,13 @@ enum RuntimeCommandCausalOwner { } #[derive(Clone, Debug, Eq, PartialEq)] -struct RuntimeCommandOutputRoute { +struct RendererCommandOutputRoute { delivery_scope: CommandOwnerScope, causal_owner: RuntimeCommandCausalOwner, } #[derive(Debug)] -struct ActiveRuntimeCommandOutputBarrier { +struct ActiveRendererCommandResponsePermit { command_id: u64, command_scope: CommandOwnerScope, causal_owner: RuntimeCommandCausalOwner, @@ -76,21 +76,21 @@ enum HeldOutputReleaseMode { } #[derive(Debug)] -struct HeldRuntimeCommandOutput { - id: RuntimeCommandOutputHoldId, +struct HeldRendererCommandOutput { + id: RendererCommandOutputHoldId, renderer_output_cursor: Option, - predecessors: BTreeSet, + predecessors: BTreeSet, release_mode: HeldOutputReleaseMode, outputs: PreparedProtocolOutputs, } #[derive(Debug)] -struct HeldRuntimeCommandOutputRoute { - route: RuntimeCommandOutputRoute, - outputs: VecDeque, +struct HeldRendererCommandOutputRoute { + route: RendererCommandOutputRoute, + outputs: VecDeque, } -/// Typed terminal for one command barrier. +/// Typed terminal for one command permit. /// /// `Released` means the exact command owner still existed when its response /// entered the protocol output sequence. `Superseded` means the command's @@ -99,28 +99,25 @@ struct HeldRuntimeCommandOutputRoute { /// canceled/superseded output performs only browser-owner cleanup and does not /// project protocol-only observations from the stale command. #[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub enum RuntimeCommandOutputBarrierTerminal { +pub enum RendererCommandResponseTerminal { Released, Canceled, Superseded, } -/// One consumed barrier terminal plus the protocol/scheduler output produced +/// One consumed permit terminal plus the protocol/scheduler output produced /// while releasing its held concrete work. -pub struct RuntimeCommandOutputBarrierCompletion { - terminal: RuntimeCommandOutputBarrierTerminal, +pub struct RendererCommandResponseCompletion { + terminal: RendererCommandResponseTerminal, outcome: CdpTurnOutcome, } -impl RuntimeCommandOutputBarrierCompletion { - pub(crate) fn new( - terminal: RuntimeCommandOutputBarrierTerminal, - outcome: CdpTurnOutcome, - ) -> Self { +impl RendererCommandResponseCompletion { + pub(crate) fn new(terminal: RendererCommandResponseTerminal, outcome: CdpTurnOutcome) -> Self { Self { terminal, outcome } } - pub fn terminal(&self) -> RuntimeCommandOutputBarrierTerminal { + pub fn terminal(&self) -> RendererCommandResponseTerminal { self.terminal } @@ -129,19 +126,19 @@ impl RuntimeCommandOutputBarrierCompletion { } } -/// Stable owner-local residence for concrete output held behind Runtime -/// command responses. +/// Stable owner-local residence for concrete output held until its renderer +/// command response is ready for frontend publication. /// /// A held batch is frozen once by the renderer/protocol owner and is never /// reconstructed from current state. Batches stay FIFO within their exact -/// delivery route. Each batch names the exact Runtime command that produced +/// delivery route. Each batch names the exact renderer command that produced /// it; another pending command on the same Page is not a predecessor. #[derive(Debug, Default)] -pub struct RuntimeCommandOutputBarriers { - next_barrier_id: RuntimeCommandOutputBarrierId, - next_hold_id: RuntimeCommandOutputHoldId, - active: BTreeMap, - held_routes: Vec, +pub struct RendererCommandResponseOrder { + next_permit_id: RendererCommandResponsePermitId, + next_hold_id: RendererCommandOutputHoldId, + active: BTreeMap, + held_routes: Vec, } impl RuntimeCommandCausalOwner { @@ -168,7 +165,7 @@ impl RuntimeCommandCausalOwner { } } -impl RuntimeCommandOutputRoute { +impl RendererCommandOutputRoute { fn capture_for_scope(conn: &CdpConnection, delivery_scope: CommandOwnerScope) -> Option { Some(Self { causal_owner: RuntimeCommandCausalOwner::capture_for_scope(conn, &delivery_scope)?, @@ -177,13 +174,13 @@ impl RuntimeCommandOutputRoute { } } -impl RuntimeCommandOutputBarriers { +impl RendererCommandResponseOrder { pub fn admit( &mut self, conn: &CdpConnection, command_id: u64, session_id: Option<&str>, - ) -> Option { + ) -> Option { let command_scope = CommandOwnerScope::capture(conn, session_id); let causal_owner = RuntimeCommandCausalOwner::capture(conn, session_id)?; // V8 Inspector commands use a session-local renderer call id. It is @@ -193,16 +190,16 @@ impl RuntimeCommandOutputBarriers { let renderer_cause = conn.renderer_runtime_command_cause_for_frontend(session_id, command_id)?; assert!( - !self.active.values().any(|barrier| { - barrier.causal_owner == causal_owner && barrier.renderer_cause == renderer_cause + !self.active.values().any(|permit| { + permit.causal_owner == causal_owner && permit.renderer_cause == renderer_cause }), - "an exact renderer Runtime command cause must have only one active response barrier" + "an exact renderer command cause must have only one active response permit" ); - let id = self.next_barrier_id; - self.next_barrier_id = self.next_barrier_id.checked_next(); + let id = self.next_permit_id; + self.next_permit_id = self.next_permit_id.checked_next(); let replaced = self.active.insert( id, - ActiveRuntimeCommandOutputBarrier { + ActiveRendererCommandResponsePermit { command_id, command_scope, causal_owner, @@ -211,9 +208,9 @@ impl RuntimeCommandOutputBarriers { ); assert!( replaced.is_none(), - "Runtime command barrier ids must not be reused while active" + "renderer command response permit ids must not be reused while active" ); - Some(RuntimeCommandOutputBarrierPermit { id, command_id }) + Some(RendererCommandResponsePermit { id, command_id }) } pub(super) async fn route_publication_outputs( @@ -226,7 +223,7 @@ impl RuntimeCommandOutputBarriers { command_context: &mut CommandDispatchContext, ) { let Some(route) = - RuntimeCommandOutputRoute::capture_for_scope(conn, delivery_scope.clone()) + RendererCommandOutputRoute::capture_for_scope(conn, delivery_scope.clone()) else { outputs .project_async(conn, delivery_scope, command_context) @@ -245,16 +242,16 @@ impl RuntimeCommandOutputBarriers { let matching: Vec<_> = self .active .iter() - .filter_map(|(id, barrier)| { - (barrier.causal_owner == route.causal_owner - && &barrier.renderer_cause == renderer_cause) + .filter_map(|(id, permit)| { + (permit.causal_owner == route.causal_owner + && &permit.renderer_cause == renderer_cause) .then_some(*id) }) .collect(); - let [barrier_id] = matching.as_slice() else { + let [permit_id] = matching.as_slice() else { assert!( matching.is_empty(), - "one renderer Runtime command cause cannot match multiple active barriers" + "one renderer command cause cannot match multiple active response permits" ); outputs .project_async(conn, delivery_scope, command_context) @@ -272,10 +269,10 @@ impl RuntimeCommandOutputBarriers { } else { outputs }; - self.hold_for_exact_barrier( + self.hold_for_exact_response( conn, route, - *barrier_id, + *permit_id, renderer_output_cursor, causal_outputs, command_context, @@ -283,11 +280,11 @@ impl RuntimeCommandOutputBarriers { .await; } - async fn hold_for_exact_barrier( + async fn hold_for_exact_response( &mut self, conn: &mut CdpConnection, - route: RuntimeCommandOutputRoute, - barrier_id: RuntimeCommandOutputBarrierId, + route: RendererCommandOutputRoute, + permit_id: RendererCommandResponsePermitId, renderer_output_cursor: Option, outputs: PreparedProtocolOutputs, command_context: &mut CommandDispatchContext, @@ -304,10 +301,10 @@ impl RuntimeCommandOutputBarriers { }; let id = self.next_hold_id; self.next_hold_id = self.next_hold_id.checked_next(); - let held = HeldRuntimeCommandOutput { + let held = HeldRendererCommandOutput { id, renderer_output_cursor, - predecessors: BTreeSet::from([barrier_id]), + predecessors: BTreeSet::from([permit_id]), release_mode: HeldOutputReleaseMode::All, outputs, }; @@ -318,7 +315,7 @@ impl RuntimeCommandOutputBarriers { { existing.outputs.push_back(held); } else { - self.held_routes.push(HeldRuntimeCommandOutputRoute { + self.held_routes.push(HeldRendererCommandOutputRoute { route, outputs: VecDeque::from([held]), }); @@ -328,16 +325,16 @@ impl RuntimeCommandOutputBarriers { pub(crate) async fn release( &mut self, conn: &mut CdpConnection, - permit: RuntimeCommandOutputBarrierPermit, + permit: RendererCommandResponsePermit, command_context: &mut CommandDispatchContext, - ) -> RuntimeCommandOutputBarrierTerminal { - let (barrier_id, barrier) = self.consume_permit(permit); - let terminal = if self.command_owner_is_current(conn, &barrier) { - RuntimeCommandOutputBarrierTerminal::Released + ) -> RendererCommandResponseTerminal { + let (permit_id, permit) = self.consume_permit(permit); + let terminal = if self.command_owner_is_current(conn, &permit) { + RendererCommandResponseTerminal::Released } else { - RuntimeCommandOutputBarrierTerminal::Superseded + RendererCommandResponseTerminal::Superseded }; - self.finish_predecessor(conn, barrier_id, barrier, terminal, command_context) + self.finish_predecessor(conn, permit_id, permit, terminal, command_context) .await; terminal } @@ -345,58 +342,60 @@ impl RuntimeCommandOutputBarriers { pub(crate) async fn cancel( &mut self, conn: &mut CdpConnection, - permit: RuntimeCommandOutputBarrierPermit, + permit: RendererCommandResponsePermit, command_context: &mut CommandDispatchContext, - ) -> RuntimeCommandOutputBarrierTerminal { - let (barrier_id, barrier) = self.consume_permit(permit); - let terminal = RuntimeCommandOutputBarrierTerminal::Canceled; - self.finish_predecessor(conn, barrier_id, barrier, terminal, command_context) + ) -> RendererCommandResponseTerminal { + let (permit_id, permit) = self.consume_permit(permit); + let terminal = RendererCommandResponseTerminal::Canceled; + self.finish_predecessor(conn, permit_id, permit, terminal, command_context) .await; terminal } fn consume_permit( &mut self, - permit: RuntimeCommandOutputBarrierPermit, + permit: RendererCommandResponsePermit, ) -> ( - RuntimeCommandOutputBarrierId, - ActiveRuntimeCommandOutputBarrier, + RendererCommandResponsePermitId, + ActiveRendererCommandResponsePermit, ) { - let barrier = self + let permit_id = permit.id; + let command_id = permit.command_id; + let active = self .active - .remove(&permit.id) - .expect("Runtime command output barrier permit must name one active barrier"); + .remove(&permit_id) + .expect("renderer command response ordering permit must name one active permit"); assert_eq!( - barrier.command_id, permit.command_id, - "Runtime command output barrier permit must retain its exact command identity" + active.command_id, command_id, + "renderer command response ordering permit must retain its exact command identity" ); - (permit.id, barrier) + (permit_id, active) } fn command_owner_is_current( &self, conn: &CdpConnection, - barrier: &ActiveRuntimeCommandOutputBarrier, + permit: &ActiveRendererCommandResponsePermit, ) -> bool { - RuntimeCommandCausalOwner::capture_for_scope(conn, &barrier.command_scope).as_ref() - == Some(&barrier.causal_owner) + RuntimeCommandCausalOwner::capture_for_scope(conn, &permit.command_scope).as_ref() + == Some(&permit.causal_owner) } async fn finish_predecessor( &mut self, conn: &mut CdpConnection, - barrier_id: RuntimeCommandOutputBarrierId, - barrier: ActiveRuntimeCommandOutputBarrier, - terminal: RuntimeCommandOutputBarrierTerminal, + permit_id: RendererCommandResponsePermitId, + permit: ActiveRendererCommandResponsePermit, + terminal: RendererCommandResponseTerminal, command_context: &mut CommandDispatchContext, ) { for route in &mut self.held_routes { - if route.route.causal_owner != barrier.causal_owner { + if route.route.causal_owner != permit.causal_owner { continue; } for held in &mut route.outputs { - let was_predecessor = held.predecessors.remove(&barrier_id); - if was_predecessor && terminal != RuntimeCommandOutputBarrierTerminal::Released { + let was_predecessor = held.predecessors.remove(&permit_id); + if was_predecessor && terminal != RendererCommandResponseTerminal::Released { held.release_mode = HeldOutputReleaseMode::OwnerActionsOnly; } } @@ -427,7 +426,7 @@ impl RuntimeCommandOutputBarriers { if moli_trace::cdp_runtime_trace_enabled() { tracing::info!( target: "moli_cdp_runtime", - stage = "runtime_command_barrier_output_release", + stage = "renderer_command_response_output_release", hold_id = held.id.0, renderer_output_stream_epoch = ?held.renderer_output_cursor.map(|cursor| cursor.stream().epoch().get()), renderer_output_sequence = ?held.renderer_output_cursor.map(RendererOutputCursor::sequence), @@ -494,20 +493,20 @@ mod tests { }; use super::{ - RuntimeCommandOutputBarrierPermit, RuntimeCommandOutputBarrierTerminal, - RuntimeCommandOutputBarriers, + RendererCommandResponseOrder, RendererCommandResponsePermit, + RendererCommandResponseTerminal, }; - const SESSION_ID: &str = "SID-runtime-command-barrier"; + const SESSION_ID: &str = "SID-runtime-command-permit"; async fn connection_with_loaded_page() -> CdpConnection { let mut conn = CdpConnection::new(); let page = conn - .load_page_via_runtime_async("data:text/html,runtime-command-barrier") + .load_page_via_runtime_async("data:text/html,runtime-command-permit") .await - .expect("barrier test page should load"); - let mut browser_context = BrowserContext::new("BID-runtime-command-barrier".to_owned()); - browser_context.set_active_target_id("TID-runtime-command-barrier"); + .expect("permit test page should load"); + let mut browser_context = BrowserContext::new("BID-runtime-command-permit".to_owned()); + browser_context.set_active_target_id("TID-runtime-command-permit"); browser_context.attach_active_session(SESSION_ID); browser_context.set_target_url(page.final_url().as_str().to_owned()); let _ = browser_context.replace_active_page_for_test(Some(page)); @@ -536,9 +535,9 @@ mod tests { fn admit_registered_command( conn: &mut CdpConnection, - barriers: &mut RuntimeCommandOutputBarriers, + order: &mut RendererCommandResponseOrder, frontend_command_id: u64, - ) -> RuntimeCommandOutputBarrierPermit { + ) -> RendererCommandResponsePermit { let descriptor = RendererCommandDescriptor::from_synthesized_payload( serde_json::json!({ "id": frontend_command_id, @@ -555,14 +554,14 @@ mod tests { descriptor, ) .expect("test Runtime command should register its renderer call"); - barriers + order .admit(conn, frontend_command_id, Some(SESSION_ID)) - .expect("registered Runtime command should admit an exact output barrier") + .expect("registered Runtime command should admit an exact output permit") } fn renderer_cause_for_permit( conn: &CdpConnection, - permit: &RuntimeCommandOutputBarrierPermit, + permit: &RendererCommandResponsePermit, ) -> RendererRuntimeCommandCausalIdentity { conn.renderer_runtime_command_cause_for_frontend(Some(SESSION_ID), permit.command_id) .expect("active test command should retain its exact renderer call identity") @@ -570,8 +569,8 @@ mod tests { async fn route_same_document_navigation( conn: &mut CdpConnection, - barriers: &mut RuntimeCommandOutputBarriers, - permit: Option<&super::RuntimeCommandOutputBarrierPermit>, + order: &mut RendererCommandResponseOrder, + permit: Option<&super::RendererCommandResponsePermit>, fragment: &str, command_context: &mut CommandDispatchContext, ) { @@ -585,7 +584,7 @@ mod tests { RendererDocumentSourcedSameDocumentNavigation::new( source_document, RendererPendingSameDocumentNavigation { - url: format!("data:text/html,runtime-command-barrier#{fragment}"), + url: format!("data:text/html,runtime-command-permit#{fragment}"), navigation_type: "fragment".to_owned(), }, ), @@ -593,7 +592,7 @@ mod tests { ) .await; let renderer_cause = permit.map(|permit| renderer_cause_for_permit(conn, permit)); - barriers + order .route_publication_outputs( conn, &command_owner, @@ -607,8 +606,8 @@ mod tests { async fn route_top_level_navigation_for_command( conn: &mut CdpConnection, - barriers: &mut RuntimeCommandOutputBarriers, - permit: &super::RuntimeCommandOutputBarrierPermit, + order: &mut RendererCommandResponseOrder, + permit: &super::RendererCommandResponsePermit, marker: &str, command_context: &mut CommandDispatchContext, ) { @@ -628,7 +627,7 @@ mod tests { ), ); - barriers + order .route_publication_outputs(conn, &command_owner, None, None, outputs, command_context) .await; } @@ -660,9 +659,9 @@ mod tests { let [event]: [crate::conn::CdpSchedulerEvent; 1] = conn .take_scheduler_events() .try_into() - .expect("barrier release should publish one concrete navigation action"); + .expect("permit release should publish one concrete navigation action"); let crate::conn::CdpSchedulerEvent::ProtocolWorkPublished { work } = event else { - panic!("barrier release must publish concrete protocol work"); + panic!("permit release must publish concrete protocol work"); }; assert!(work.is_top_level_location_navigation_owner_action()); let (events, nested_scheduler_events) = conn @@ -685,44 +684,42 @@ mod tests { #[tokio::test(flavor = "multi_thread")] async fn same_document_navigation_projects_before_exact_runtime_command_response() { let mut conn = connection_with_loaded_page().await; - let mut barriers = RuntimeCommandOutputBarriers::default(); - let permit = admit_registered_command(&mut conn, &mut barriers, 1); + let mut order = RendererCommandResponseOrder::default(); + let permit = admit_registered_command(&mut conn, &mut order, 1); let mut command_context = CommandDispatchContext::default(); route_same_document_navigation( &mut conn, - &mut barriers, + &mut order, Some(&permit), "held", &mut command_context, ) .await; - assert_eq!(barriers.active_count(), 1); - assert_eq!(barriers.held_output_count(), 0); + assert_eq!(order.active_count(), 1); + assert_eq!(order.held_output_count(), 0); assert!( contains_same_document_navigation(&protocol_messages(&mut command_context)), "the completed history mutation must be reported before the exact command response" ); - let terminal = barriers - .release(&mut conn, permit, &mut command_context) - .await; + let terminal = order.release(&mut conn, permit, &mut command_context).await; - assert_eq!(terminal, RuntimeCommandOutputBarrierTerminal::Released); - assert_eq!(barriers.active_count(), 0); - assert_eq!(barriers.held_output_count(), 0); + assert_eq!(terminal, RendererCommandResponseTerminal::Released); + assert_eq!(order.active_count(), 0); + assert_eq!(order.held_output_count(), 0); assert!( !contains_same_document_navigation(&protocol_messages(&mut command_context)), - "releasing the barrier must not deliver the same navigation twice" + "releasing the permit must not deliver the same navigation twice" ); } #[tokio::test(flavor = "multi_thread")] async fn debugger_transition_messages_wait_for_the_exact_command_response() { let mut conn = connection_with_loaded_page().await; - let mut barriers = RuntimeCommandOutputBarriers::default(); - let permit = admit_registered_command(&mut conn, &mut barriers, 2); + let mut order = RendererCommandResponseOrder::default(); + let permit = admit_registered_command(&mut conn, &mut order, 2); let cause = renderer_cause_for_permit(&conn, &permit); let attachment = conn .runtime_session_owner_slot(Some(SESSION_ID)) @@ -768,7 +765,7 @@ mod tests { let mut command_context = CommandDispatchContext::default(); let command_owner = CommandOwnerScope::for_session(SESSION_ID); - barriers + order .route_publication_outputs( &mut conn, &command_owner, @@ -779,17 +776,15 @@ mod tests { ) .await; - assert_eq!(barriers.held_output_count(), 1); + assert_eq!(order.held_output_count(), 1); assert!( protocol_messages(&mut command_context).is_empty(), "Debugger transition output must not overtake the matching response" ); assert_eq!( - barriers - .release(&mut conn, permit, &mut command_context) - .await, - RuntimeCommandOutputBarrierTerminal::Released + order.release(&mut conn, permit, &mut command_context).await, + RendererCommandResponseTerminal::Released ); let messages = protocol_messages(&mut command_context); assert_eq!( @@ -804,45 +799,41 @@ mod tests { #[tokio::test(flavor = "multi_thread")] async fn output_is_owned_by_one_exact_command_not_every_command_on_the_page() { let mut conn = connection_with_loaded_page().await; - let mut barriers = RuntimeCommandOutputBarriers::default(); - let first = admit_registered_command(&mut conn, &mut barriers, 11); - let second = admit_registered_command(&mut conn, &mut barriers, 12); + let mut order = RendererCommandResponseOrder::default(); + let first = admit_registered_command(&mut conn, &mut order, 11); + let second = admit_registered_command(&mut conn, &mut order, 12); let mut command_context = CommandDispatchContext::default(); route_top_level_navigation_for_command( &mut conn, - &mut barriers, + &mut order, &first, "exact-predecessor", &mut command_context, ) .await; - assert_eq!(barriers.held_output_count(), 1); + assert_eq!(order.held_output_count(), 1); assert_eq!( - barriers - .release(&mut conn, second, &mut command_context) - .await, - RuntimeCommandOutputBarrierTerminal::Released + order.release(&mut conn, second, &mut command_context).await, + RendererCommandResponseTerminal::Released ); - assert_eq!(barriers.active_count(), 1); - assert_eq!(barriers.held_output_count(), 1); + assert_eq!(order.active_count(), 1); + assert_eq!(order.held_output_count(), 1); assert!( !contains_top_level_navigation(&protocol_messages(&mut command_context)), "an unrelated command response must not release another command's output" ); assert_eq!( - barriers - .release(&mut conn, first, &mut command_context) - .await, - RuntimeCommandOutputBarrierTerminal::Released + order.release(&mut conn, first, &mut command_context).await, + RendererCommandResponseTerminal::Released ); - assert_eq!(barriers.active_count(), 0); - assert_eq!(barriers.held_output_count(), 0); + assert_eq!(order.active_count(), 0); + assert_eq!(order.held_output_count(), 0); assert!( !contains_top_level_navigation(&protocol_messages(&mut command_context)), - "barrier release must publish, not execute, its concrete owner action" + "permit release must publish, not execute, its concrete owner action" ); complete_published_top_level_navigation(&mut conn, &mut command_context).await; assert!( @@ -854,45 +845,41 @@ mod tests { #[tokio::test(flavor = "multi_thread")] async fn canceling_an_unrelated_command_does_not_downgrade_held_output() { let mut conn = connection_with_loaded_page().await; - let mut barriers = RuntimeCommandOutputBarriers::default(); - let first = admit_registered_command(&mut conn, &mut barriers, 15); - let second = admit_registered_command(&mut conn, &mut barriers, 16); + let mut order = RendererCommandResponseOrder::default(); + let first = admit_registered_command(&mut conn, &mut order, 15); + let second = admit_registered_command(&mut conn, &mut order, 16); let mut command_context = CommandDispatchContext::default(); route_top_level_navigation_for_command( &mut conn, - &mut barriers, + &mut order, &first, "exact-predecessor-survives-unrelated-cancel", &mut command_context, ) .await; - assert_eq!(barriers.held_output_count(), 1); + assert_eq!(order.held_output_count(), 1); assert_eq!( - barriers - .cancel(&mut conn, second, &mut command_context) - .await, - RuntimeCommandOutputBarrierTerminal::Canceled + order.cancel(&mut conn, second, &mut command_context).await, + RendererCommandResponseTerminal::Canceled ); - assert_eq!(barriers.active_count(), 1); - assert_eq!(barriers.held_output_count(), 1); + assert_eq!(order.active_count(), 1); + assert_eq!(order.held_output_count(), 1); assert!( !contains_top_level_navigation(&protocol_messages(&mut command_context)), "canceling an unrelated command must neither release nor downgrade another command's output" ); assert_eq!( - barriers - .release(&mut conn, first, &mut command_context) - .await, - RuntimeCommandOutputBarrierTerminal::Released + order.release(&mut conn, first, &mut command_context).await, + RendererCommandResponseTerminal::Released ); - assert_eq!(barriers.active_count(), 0); - assert_eq!(barriers.held_output_count(), 0); + assert_eq!(order.active_count(), 0); + assert_eq!(order.held_output_count(), 0); assert!( !contains_top_level_navigation(&protocol_messages(&mut command_context)), - "barrier release must publish, not execute, the retained owner action" + "permit release must publish, not execute, the retained owner action" ); complete_published_top_level_navigation(&mut conn, &mut command_context).await; assert!( @@ -904,37 +891,35 @@ mod tests { #[tokio::test(flavor = "multi_thread")] async fn unattributed_same_page_output_is_not_held_by_a_pending_command() { let mut conn = connection_with_loaded_page().await; - let mut barriers = RuntimeCommandOutputBarriers::default(); - let permit = admit_registered_command(&mut conn, &mut barriers, 13); + let mut order = RendererCommandResponseOrder::default(); + let permit = admit_registered_command(&mut conn, &mut order, 13); let mut command_context = CommandDispatchContext::default(); route_same_document_navigation( &mut conn, - &mut barriers, + &mut order, None, "independent-page-task", &mut command_context, ) .await; - assert_eq!(barriers.held_output_count(), 0); + assert_eq!(order.held_output_count(), 0); assert!( contains_same_document_navigation(&protocol_messages(&mut command_context)), "output without the exact command identity must remain visible while that command awaits" ); assert_eq!( - barriers - .release(&mut conn, permit, &mut command_context) - .await, - RuntimeCommandOutputBarrierTerminal::Released + order.release(&mut conn, permit, &mut command_context).await, + RendererCommandResponseTerminal::Released ); } #[tokio::test(flavor = "multi_thread")] async fn concrete_runtime_navigation_cause_survives_an_earlier_untagged_wake() { let mut conn = connection_with_loaded_page().await; - let mut barriers = RuntimeCommandOutputBarriers::default(); - let permit = admit_registered_command(&mut conn, &mut barriers, 14); + let mut order = RendererCommandResponseOrder::default(); + let permit = admit_registered_command(&mut conn, &mut order, 14); let cause = renderer_cause_for_permit(&conn, &permit); let owner = conn .target_page_residence_identity_for_session(Some(SESSION_ID)) @@ -952,7 +937,7 @@ mod tests { let mut command_context = CommandDispatchContext::default(); let command_owner = CommandOwnerScope::for_session(SESSION_ID); - barriers + order .route_publication_outputs( &mut conn, &command_owner, @@ -963,21 +948,19 @@ mod tests { ) .await; - assert_eq!(barriers.held_output_count(), 1); + assert_eq!(order.held_output_count(), 1); assert!( !contains_top_level_navigation(&protocol_messages(&mut command_context)), "an older untagged wake must not release a concrete command-caused navigation" ); assert_eq!( - barriers - .release(&mut conn, permit, &mut command_context) - .await, - RuntimeCommandOutputBarrierTerminal::Released + order.release(&mut conn, permit, &mut command_context).await, + RendererCommandResponseTerminal::Released ); assert!( !contains_top_level_navigation(&protocol_messages(&mut command_context)), - "barrier release must publish, not execute, the concrete navigation action" + "permit release must publish, not execute, the concrete navigation action" ); complete_published_top_level_navigation(&mut conn, &mut command_context).await; assert!( @@ -987,10 +970,10 @@ mod tests { } #[tokio::test(flavor = "multi_thread")] - async fn replacement_supersedes_old_barrier_without_holding_new_page_output() { + async fn replacement_supersedes_old_response_permit_without_holding_new_page_output() { let mut conn = connection_with_loaded_page().await; - let mut barriers = RuntimeCommandOutputBarriers::default(); - let permit = admit_registered_command(&mut conn, &mut barriers, 21); + let mut order = RendererCommandResponseOrder::default(); + let permit = admit_registered_command(&mut conn, &mut order, 21); conn.replace_document_fixture_for_owner_test(&crate::conn::CommandOwnerScope::capture( &conn, Some(SESSION_ID), @@ -999,7 +982,7 @@ mod tests { route_same_document_navigation( &mut conn, - &mut barriers, + &mut order, Some(&permit), "replacement", &mut command_context, @@ -1007,9 +990,9 @@ mod tests { .await; assert_eq!( - barriers.held_output_count(), + order.held_output_count(), 0, - "output from the replacement Page must not be held by an old Page barrier" + "output from the replacement Page must not be held by an old Page permit" ); assert!( contains_same_document_navigation(&protocol_messages(&mut command_context)), @@ -1017,42 +1000,38 @@ mod tests { recaptured through the old command" ); assert_eq!( - barriers - .release(&mut conn, permit, &mut command_context) - .await, - RuntimeCommandOutputBarrierTerminal::Superseded + order.release(&mut conn, permit, &mut command_context).await, + RendererCommandResponseTerminal::Superseded ); - assert_eq!(barriers.active_count(), 0); + assert_eq!(order.active_count(), 0); } #[tokio::test(flavor = "multi_thread")] async fn replacement_after_capture_retires_held_old_page_output() { let mut conn = connection_with_loaded_page().await; - let mut barriers = RuntimeCommandOutputBarriers::default(); - let permit = admit_registered_command(&mut conn, &mut barriers, 30); + let mut order = RendererCommandResponseOrder::default(); + let permit = admit_registered_command(&mut conn, &mut order, 30); let mut command_context = CommandDispatchContext::default(); route_top_level_navigation_for_command( &mut conn, - &mut barriers, + &mut order, &permit, "retired-source-page", &mut command_context, ) .await; - assert_eq!(barriers.held_output_count(), 1); + assert_eq!(order.held_output_count(), 1); conn.replace_document_fixture_for_owner_test(&crate::conn::CommandOwnerScope::capture( &conn, Some(SESSION_ID), )); assert_eq!( - barriers - .release(&mut conn, permit, &mut command_context) - .await, - RuntimeCommandOutputBarrierTerminal::Superseded + order.release(&mut conn, permit, &mut command_context).await, + RendererCommandResponseTerminal::Superseded ); - assert_eq!(barriers.held_output_count(), 0); + assert_eq!(order.held_output_count(), 0); complete_published_top_level_navigation(&mut conn, &mut command_context).await; assert!( !contains_top_level_navigation(&protocol_messages(&mut command_context)), @@ -1061,29 +1040,27 @@ mod tests { } #[tokio::test(flavor = "multi_thread")] - async fn explicit_cancel_consumes_the_barrier_once() { + async fn explicit_cancel_consumes_the_response_permit_once() { let mut conn = connection_with_loaded_page().await; - let mut barriers = RuntimeCommandOutputBarriers::default(); - let permit = admit_registered_command(&mut conn, &mut barriers, 31); + let mut order = RendererCommandResponseOrder::default(); + let permit = admit_registered_command(&mut conn, &mut order, 31); let mut command_context = CommandDispatchContext::default(); route_top_level_navigation_for_command( &mut conn, - &mut barriers, + &mut order, &permit, "canceled-protocol-command", &mut command_context, ) .await; - assert_eq!(barriers.held_output_count(), 1); + assert_eq!(order.held_output_count(), 1); assert_eq!( - barriers - .cancel(&mut conn, permit, &mut command_context) - .await, - RuntimeCommandOutputBarrierTerminal::Canceled + order.cancel(&mut conn, permit, &mut command_context).await, + RendererCommandResponseTerminal::Canceled ); - assert_eq!(barriers.active_count(), 0); - assert_eq!(barriers.held_output_count(), 0); + assert_eq!(order.active_count(), 0); + assert_eq!(order.held_output_count(), 0); assert!( !contains_top_level_navigation(&protocol_messages(&mut command_context)), "cancel must publish, not execute, the already-produced owner action" diff --git a/moli-protocol/src/domains/console.rs b/moli-protocol/src/domains/console.rs index 3b43e605b3..6ecf3b403b 100644 --- a/moli-protocol/src/domains/console.rs +++ b/moli-protocol/src/domains/console.rs @@ -4,6 +4,10 @@ use crate::domains::actions::ConsoleAction; use crate::domains::command_output::CommandOutputPlan; use crate::domains::runtime::{RuntimeCommandTaskStep, start_console_inspector_command_dispatch}; +pub(crate) fn command_waits_for_document_projection(cmd: &Cmd<'_>) -> bool { + cmd.parse_action::().is_some() +} + pub(crate) fn try_start_console_command_dispatch( conn: &mut CdpConnection, cmd: &Cmd<'_>, diff --git a/moli-protocol/src/domains/css.rs b/moli-protocol/src/domains/css.rs index 36426775b1..5bba44b559 100644 --- a/moli-protocol/src/domains/css.rs +++ b/moli-protocol/src/domains/css.rs @@ -1,4 +1,4 @@ -use crate::conn::{CdpConnection, Cmd, CommandOwnerScope}; +use crate::conn::{CdpConnection, Cmd, CommandOwnerScope, RendererDispatchLane}; use crate::domains::actions::CssAction; use crate::domains::command_output::CommandOutputPlan; use chromiumoxide_cdp::cdp::browser_protocol::css::{ @@ -44,6 +44,10 @@ pub(crate) enum CssCommandDispatchStep { Complete(CommandOutputPlan), } +pub(crate) fn command_waits_for_document_projection(cmd: &Cmd<'_>) -> bool { + cmd.parse_action::().is_some() +} + enum PendingCssCommandKind { Enable { frame_id: String, @@ -78,6 +82,12 @@ struct PendingCssCommandStartError { } impl PendingCssCommandDispatch { + pub(crate) fn renderer_dispatch_lane(&self) -> Option { + self.pending + .renderer_agent_attachment_id() + .map(|_| RendererDispatchLane::Main) + } + fn from_command( conn: &CdpConnection, cmd: &Cmd<'_>, diff --git a/moli-protocol/src/domains/debugger.rs b/moli-protocol/src/domains/debugger.rs index 4b936a1e82..ffd952aae7 100644 --- a/moli-protocol/src/domains/debugger.rs +++ b/moli-protocol/src/domains/debugger.rs @@ -3,6 +3,10 @@ use serde_json::{Map, Value}; use crate::conn::{CdpConnection, Cmd}; use crate::domains::runtime::{RuntimeCommandTaskStep, start_debugger_inspector_command_dispatch}; +pub(crate) fn command_waits_for_document_projection(cmd: &Cmd<'_>) -> bool { + crate::domains::runtime::debugger_command_waits_for_document_projection(cmd) +} + pub(crate) fn try_start_debugger_command_dispatch( conn: &mut CdpConnection, cmd: &Cmd<'_>, @@ -160,7 +164,7 @@ mod tests { .expect("start cross-Document navigation"); assert!( ctx.conn - .renderer_document_navigation_is_suspended_for_session_owner(None) + .document_projection_is_pending_for_session_owner(None) ); let source = command( @@ -182,7 +186,10 @@ mod tests { let _ = ctx .conn - .finish_renderer_document_navigation_for_owner(&navigation_owner, &navigation); + .finish_navigation_without_document_projection_for_owner( + &navigation_owner, + &navigation, + ); ctx.conn .clear_pending_document_navigation_for_owner_if_matches(&navigation_owner, &navigation); } diff --git a/moli-protocol/src/domains/dom.rs b/moli-protocol/src/domains/dom.rs index b02e1b0728..5c5fa82b3c 100644 --- a/moli-protocol/src/domains/dom.rs +++ b/moli-protocol/src/domains/dom.rs @@ -44,6 +44,11 @@ pub(crate) enum DomCommandDispatchStep { Complete(CommandOutputPlan), } +pub(crate) fn command_waits_for_document_projection(cmd: &Cmd<'_>) -> bool { + cmd.parse_action::() + .is_some_and(|action| action != DomAction::Enable) +} + pub(crate) fn try_start_dom_command_dispatch( conn: &mut CdpConnection, cmd: &Cmd<'_>, diff --git a/moli-protocol/src/domains/dom/resolve.rs b/moli-protocol/src/domains/dom/resolve.rs index c55476d22e..f03fb4946d 100644 --- a/moli-protocol/src/domains/dom/resolve.rs +++ b/moli-protocol/src/domains/dom/resolve.rs @@ -380,6 +380,12 @@ pub(super) enum PendingSetChildNodesAfter { } impl PendingDomCommandDispatch { + pub(crate) fn renderer_dispatch_lane(&self) -> Option { + self.pending + .renderer_agent_attachment_id() + .map(|_| crate::conn::RendererDispatchLane::Main) + } + pub(crate) async fn wait(self) -> CompletedDomCommandDispatch { let completed = Box::pin(self.pending.wait()) .await diff --git a/moli-protocol/src/domains/dom_debugger.rs b/moli-protocol/src/domains/dom_debugger.rs index 6e38de5da7..e8bf123df2 100644 --- a/moli-protocol/src/domains/dom_debugger.rs +++ b/moli-protocol/src/domains/dom_debugger.rs @@ -6,7 +6,7 @@ use moli_core::page::{ use serde::Deserialize; use serde_json::{Map, Value, json}; -use crate::conn::{CdpConnection, Cmd, CommandOwnerScope}; +use crate::conn::{CdpConnection, Cmd, CommandOwnerScope, RendererDispatchLane}; use crate::domains::actions::DomDebuggerAction; use crate::domains::command_output::CommandOutputPlan; @@ -43,6 +43,10 @@ pub(crate) enum DomDebuggerCommandTaskStep { Complete(CommandOutputPlan), } +pub(crate) fn command_waits_for_document_projection(cmd: &Cmd<'_>) -> bool { + cmd.parse_action::().is_some() +} + #[derive(Deserialize)] #[serde(rename_all = "camelCase")] struct GetEventListenersParams { @@ -78,6 +82,12 @@ fn default_depth() -> i32 { } impl PendingDomDebuggerCommandDispatch { + pub(crate) fn renderer_dispatch_lane(&self) -> Option { + self.pending + .renderer_agent_attachment_id() + .map(|_| RendererDispatchLane::Main) + } + pub(crate) async fn wait(self) -> CompletedDomDebuggerCommandDispatch { CompletedDomDebuggerCommandDispatch { command_id: self.command_id, diff --git a/moli-protocol/src/domains/dom_snapshot.rs b/moli-protocol/src/domains/dom_snapshot.rs index e5f7ab3ac8..d733f37313 100644 --- a/moli-protocol/src/domains/dom_snapshot.rs +++ b/moli-protocol/src/domains/dom_snapshot.rs @@ -9,7 +9,7 @@ use moli_core::page::{ }; use serde::Deserialize; -use crate::conn::{CdpConnection, Cmd, CommandOwnerScope}; +use crate::conn::{CdpConnection, Cmd, CommandOwnerScope, RendererDispatchLane}; use crate::domains::actions::DomSnapshotAction; use crate::domains::command_output::CommandOutputPlan; @@ -30,7 +30,17 @@ pub(crate) enum DomSnapshotCommandDispatchStep { Complete(CommandOutputPlan), } +pub(crate) fn command_waits_for_document_projection(cmd: &Cmd<'_>) -> bool { + cmd.parse_action::() == Some(DomSnapshotAction::CaptureSnapshot) +} + impl PendingDomSnapshotCommandDispatch { + pub(crate) fn renderer_dispatch_lane(&self) -> Option { + self.pending + .renderer_agent_attachment_id() + .map(|_| RendererDispatchLane::Main) + } + pub async fn wait(self) -> CompletedDomSnapshotCommandDispatch { CompletedDomSnapshotCommandDispatch { command_id: self.command_id, diff --git a/moli-protocol/src/domains/emulation.rs b/moli-protocol/src/domains/emulation.rs index 2035c5917f..e87eb1a26b 100644 --- a/moli-protocol/src/domains/emulation.rs +++ b/moli-protocol/src/domains/emulation.rs @@ -122,6 +122,16 @@ impl PendingEmulationPageOperation { } impl PendingEmulationCommandDispatch { + pub(crate) const fn renderer_dispatch_lane(&self) -> Option { + match self.pending { + PendingEmulationRendererDispatch::IoAdapterReply(_) + | PendingEmulationRendererDispatch::IoSessionOutput { .. } => { + Some(crate::conn::RendererDispatchLane::Io) + } + PendingEmulationRendererDispatch::Pages(_) => None, + } + } + pub(crate) async fn wait(self) -> CompletedEmulationCommandDispatch { let completed = match self.pending { PendingEmulationRendererDispatch::Pages(pending_pages) => { diff --git a/moli-protocol/src/domains/heap_profiler.rs b/moli-protocol/src/domains/heap_profiler.rs index 361feda456..54b08802fe 100644 --- a/moli-protocol/src/domains/heap_profiler.rs +++ b/moli-protocol/src/domains/heap_profiler.rs @@ -6,6 +6,16 @@ use crate::domains::runtime::{ start_moli_diagnostics_command_dispatch, }; +pub(crate) fn command_waits_for_document_projection(cmd: &Cmd<'_>) -> bool { + cmd.parse_action::() + .is_some_and(|action| { + !matches!( + action, + HeapProfilerAction::MoliDiagnostics | HeapProfilerAction::MoliResetIdleEngine + ) + }) +} + pub(crate) fn try_start_heap_profiler_command_dispatch( conn: &mut CdpConnection, cmd: &Cmd<'_>, diff --git a/moli-protocol/src/domains/page.rs b/moli-protocol/src/domains/page.rs index 5dd927133c..50e8ae332e 100644 --- a/moli-protocol/src/domains/page.rs +++ b/moli-protocol/src/domains/page.rs @@ -360,7 +360,44 @@ pub(crate) enum PageCommandTaskStep { Complete(CommandOutputPlan), } +pub(crate) fn command_waits_for_document_projection(cmd: &Cmd<'_>) -> bool { + matches!( + cmd.parse_action::(), + Some(PageAction::GetFrameTree | PageAction::GetResourceTree | PageAction::GetLayoutMetrics) + ) +} + impl PendingPageCommandDispatch { + pub(crate) fn renderer_dispatch_lane(&self) -> Option { + match self.kind.as_ref() { + PendingPageCommandKind::GetFrameTree { .. } + | PendingPageCommandKind::GetLayoutMetrics { .. } => { + Some(crate::conn::RendererDispatchLane::Main) + } + PendingPageCommandKind::BringToFront { .. } + | PendingPageCommandKind::AppendDefaultDocumentStartScript { .. } + | PendingPageCommandKind::RemoveDocumentStartScript { .. } + | PendingPageCommandKind::AddScriptToEvaluateOnNewDocument(_) + | PendingPageCommandKind::SearchInResource(_) + | PendingPageCommandKind::GetAppManifest(_) + | PendingPageCommandKind::ResetNavigationHistory { .. } + | PendingPageCommandKind::SetDocumentContent { .. } + | PendingPageCommandKind::SetBypassContentSecurityPolicy { .. } + | PendingPageCommandKind::SameDocumentNavigate(_) + | PendingPageCommandKind::CaptureSnapshot { .. } + | PendingPageCommandKind::CaptureScreenshot { .. } + | PendingPageCommandKind::PrintToPdf { .. } + | PendingPageCommandKind::Navigate(_) + | PendingPageCommandKind::TraverseSameDocumentHistory(_) + | PendingPageCommandKind::ChildFrameNavigate(_) + | PendingPageCommandKind::ContinueNavigationWithoutRequestPause(_) + | PendingPageCommandKind::StopLoading + | PendingPageCommandKind::Crash + | PendingPageCommandKind::Close + | PendingPageCommandKind::CreateIsolatedWorld(_) => None, + } + } + pub async fn wait(self) -> CompletedPageCommandDispatch { let kind = match *self.kind { PendingPageCommandKind::BringToFront { diff --git a/moli-protocol/src/domains/page/navigation.rs b/moli-protocol/src/domains/page/navigation.rs index 1f693ffbfa..29d89532db 100644 --- a/moli-protocol/src/domains/page/navigation.rs +++ b/moli-protocol/src/domains/page/navigation.rs @@ -3205,6 +3205,7 @@ async fn complete_materialized_navigation_into_buffer_inner_async( ) { let navigation_owner = state.owner.clone(); let navigation_session_id = navigation_owner.session_id().map(str::to_owned); + let mut document_projection_release = None; match navigation { network::MaterializedNavigationLoadOutcome::ResponseCommitReady(navigation) => { match conn.start_response_document_materialization_for_owner( @@ -3219,7 +3220,7 @@ async fn complete_materialized_navigation_into_buffer_inner_async( network::materialize_loaded_navigation_progress( conn, &state, navigation, ); - commit_loaded_navigation_async( + document_projection_release = commit_loaded_navigation_async( conn, out, &token, @@ -3254,8 +3255,10 @@ async fn complete_materialized_navigation_into_buffer_inner_async( let primary_protocol_session_id = conn .runtime_session_owner_primary_session_id_for_owner(&navigation_owner) .or_else(|| navigation_session_id.clone()); - let (routed_renderer_output, renderer_call_replacements) = conn - .finish_renderer_document_navigation_for_owner(&navigation_owner, &token) + let (routed_renderer_output, renderer_call_replacements) = document_projection_release + .or_else(|| { + conn.finish_navigation_without_document_projection_for_owner(&navigation_owner, &token) + }) .map(|finish| (finish.released_output, finish.renderer_call_replacements)) .unwrap_or_default(); if !routed_renderer_output.is_empty() { diff --git a/moli-protocol/src/domains/page/navigation_commit.rs b/moli-protocol/src/domains/page/navigation_commit.rs index 6a9df3a8e6..74d4bcd4b4 100644 --- a/moli-protocol/src/domains/page/navigation_commit.rs +++ b/moli-protocol/src/domains/page/navigation_commit.rs @@ -19,7 +19,7 @@ pub(super) async fn commit_loaded_navigation_async( prepared: crate::conn::PreparedDocumentNavigation, navigation: MaterializedLoadedDocumentProgress, command_context: &mut CommandDispatchContext, -) { +) -> Option { let MaterializedLoadedDocumentProgress { pending_download, final_url, @@ -38,7 +38,7 @@ pub(super) async fn commit_loaded_navigation_async( navigation_activity = navigation_activity.with_network_error_page_result(error_page.error_text().to_owned()); } - let Some(lifecycle) = commit_and_project_loaded_navigation_async( + let (lifecycle, projection_fence) = commit_and_project_loaded_navigation_async( conn, out, navigation_activity.state(), @@ -46,10 +46,7 @@ pub(super) async fn commit_loaded_navigation_async( initial_runtime_realms, command_context, ) - .await - else { - return; - }; + .await?; if !is_network_error_page { let _ = conn.commit_main_document_resource_for_owner( &navigation_activity.state().owner, @@ -69,6 +66,7 @@ pub(super) async fn commit_loaded_navigation_async( &navigation_activity.state().owner, crate::conn::CommittedDocumentLifecycle { document: lifecycle.document, + browser_sequence: lifecycle.browser_sequence, artifacts, }, Some(*token), @@ -95,21 +93,39 @@ pub(super) async fn commit_loaded_navigation_async( navigation_activity.defer_initial_renderer_document_lifecycle_events_until_load_boundary( deferred_initial_renderer_document_lifecycle_events, ); + let projection_owner = navigation_activity.state().owner.clone(); + let projected_document = renderer_document_binding.clone(); // Keep the loaded commit tail boxed: the target/Patchright CDP test thread // has historically hit stack limits when this future is inlined. - Box::pin(async move { - navigation_activity - .emit_loaded_navigation_commit_async( - conn, - out, - pending_download, - renderer_document_binding, - initial_renderer_document_lifecycle_events, - renderer_output_predecessor, - ) - .await; - }) + Box::pin(navigation_activity.emit_loaded_navigation_commit_async( + &mut *conn, + out, + pending_download, + renderer_document_binding, + initial_renderer_document_lifecycle_events, + renderer_output_predecessor, + )) .await; + + match projection_fence { + Some(fence) => { + let projected_document = projected_document.as_ref().unwrap_or_else(|| { + panic!( + "Browser sequence {} rebound renderer attachment {} without a committed frame projection", + fence.browser_sequence().get(), + fence.renderer_attachment().get(), + ) + }); + Some(conn.publish_document_projection_fence_for_owner( + &projection_owner, + projected_document, + fence, + )) + } + None => { + conn.finish_navigation_without_document_projection_for_owner(&projection_owner, token) + } + } } fn split_renderer_page_creation_lifecycle_at_load_boundary( @@ -196,7 +212,10 @@ async fn commit_and_project_loaded_navigation_async( prepared: crate::conn::PreparedDocumentNavigation, initial_runtime_realms: Vec, command_context: &mut CommandDispatchContext, -) -> Option { +) -> Option<( + crate::conn::CommittedDocumentLifecycle, + Option, +)> { let commit = match conn.commit_loaded_navigation(prepared) { Ok(commit) => commit, Err(error) => { @@ -207,8 +226,8 @@ async fn commit_and_project_loaded_navigation_async( // Everything below observes a committed Browser Document. Inspection failure // settles DevTools work; it must never turn this navigation into a rollback. - let inspection_available = match commit.inspection_projection { - Ok(()) => true, + let (inspection_available, projection_fence) = match commit.inspection_projection { + Ok(fence) => (true, Some(fence)), Err(error) => { tracing::warn!(%error, session_id = state.owner.session_id(), "inspection projection failed after Browser navigation committed"); @@ -230,7 +249,7 @@ async fn commit_and_project_loaded_navigation_async( sessions, "Inspector rebind failed after navigation", ); - false + (false, None) } }; commit.previous_document_retirement.close().await; @@ -277,7 +296,7 @@ async fn commit_and_project_loaded_navigation_async( } out.extend_background_events_after_messages(events); } - Some(commit.lifecycle) + Some((commit.lifecycle, projection_fence)) } /// Rebind failures are session failures, never Browser crash or close commands. diff --git a/moli-protocol/src/domains/page/tests/document_content.rs b/moli-protocol/src/domains/page/tests/document_content.rs index e4ce2187db..6aa5de2a94 100644 --- a/moli-protocol/src/domains/page/tests/document_content.rs +++ b/moli-protocol/src/domains/page/tests/document_content.rs @@ -34,31 +34,33 @@ async fn install_document_content_test_page(ctx: &mut TestContext, url: &str) { .unwrap() .await .unwrap(); - let committed = browser_context + browser_context .commit_loaded_navigation(prepared) - .expect("document-content test Document should commit"); - assert!(committed.inspection_projection.is_ok()); - assert!( - browser_context - .active_page_target_mut() - .runtime_slot - .finish_renderer_document_navigation(&committed_document) - .expect("document-content test renderer navigation should finish") - .released_output - .is_empty(), - "the fixture should not leave buffered Inspector output behind" - ); - committed + .expect("document-content test Document should commit") }; + let projection_fence = committed + .inspection_projection + .expect("document-content test inspection projection should succeed"); committed.previous_document_retirement.close().await; - let (binding, _) = ctx.conn.bind_renderer_document_lifecycle_for_owner( + let (binding, _) = ctx.conn.project_committed_document_lifecycle_for_owner( &crate::conn::CommandOwnerScope::for_session("SID-1"), - artifacts, + committed.lifecycle, Some(committed_document), "TID-1".to_owned(), LOADER_ID.to_owned(), ); - assert!(binding.is_some(), "renderer lifecycle should bind"); + let binding = binding.expect("renderer lifecycle should bind"); + assert!( + ctx.conn + .publish_document_projection_fence_for_owner( + &crate::conn::CommandOwnerScope::for_session("SID-1"), + &binding, + projection_fence, + ) + .released_output + .is_empty(), + "the fixture should not leave buffered Inspector output behind" + ); // The fixture commits an already-running renderer Page directly instead // of going through the production navigation command. Route that exact // Document's initial lifecycle publication before enabling observers. diff --git a/moli-protocol/src/domains/performance.rs b/moli-protocol/src/domains/performance.rs index c9ddeff03d..b23511f355 100644 --- a/moli-protocol/src/domains/performance.rs +++ b/moli-protocol/src/domains/performance.rs @@ -64,6 +64,10 @@ pub(crate) enum PerformanceCommandTaskStep { } impl PendingPerformanceCommandDispatch { + pub(crate) const fn renderer_dispatch_lane(&self) -> Option { + Some(crate::conn::RendererDispatchLane::Io) + } + pub async fn wait(self) -> CompletedPerformanceCommandDispatch { let completed = match *self.pending { PendingPerformanceRendererCommand::IoAdapterReply { diff --git a/moli-protocol/src/domains/profiler.rs b/moli-protocol/src/domains/profiler.rs index dda1280da3..0f2fa0ae49 100644 --- a/moli-protocol/src/domains/profiler.rs +++ b/moli-protocol/src/domains/profiler.rs @@ -2,6 +2,10 @@ use crate::conn::{CdpConnection, Cmd, ProfilerAction, ProfilerInspectorCommand}; use crate::domains::command_output::CommandOutputPlan; use crate::domains::runtime::{RuntimeCommandTaskStep, start_profiler_inspector_command_dispatch}; +pub(crate) fn command_waits_for_document_projection(cmd: &Cmd<'_>) -> bool { + cmd.parse_action::().is_some() +} + pub(crate) fn try_start_profiler_command_dispatch( conn: &mut CdpConnection, cmd: &Cmd<'_>, diff --git a/moli-protocol/src/domains/runtime.rs b/moli-protocol/src/domains/runtime.rs index 7d08348f76..8311d83f2c 100644 --- a/moli-protocol/src/domains/runtime.rs +++ b/moli-protocol/src/domains/runtime.rs @@ -151,8 +151,8 @@ pub(in crate::domains) use dispatcher::replay_shared_worker_runtime_bindings_for pub(crate) use dispatcher::{ BidiPreloadFunctionDeclaration, CompletedRuntimeCommandDispatch, PendingRuntimeCommandDispatch, RuntimeCommandTaskStep, bidi_preload_function_declaration_source, - complete_pending_runtime_command_at_response_boundary, - devtools_deep_serialization_options_json, + command_waits_for_document_projection, complete_pending_runtime_command_at_response_boundary, + debugger_command_waits_for_document_projection, devtools_deep_serialization_options_json, execute_devtools_runtime_command_async_with_protocol_events, execute_runtime_listener_command_for_owner, start_bidi_preload_channel_listeners_for_execution_context_background_events_async, diff --git a/moli-protocol/src/domains/runtime/dispatcher.rs b/moli-protocol/src/domains/runtime/dispatcher.rs index 5a0fdce90a..c899b3cce2 100644 --- a/moli-protocol/src/domains/runtime/dispatcher.rs +++ b/moli-protocol/src/domains/runtime/dispatcher.rs @@ -24,8 +24,8 @@ use moli_page_types::RendererInspectorResponseDelivery; use crate::conn::{ BackgroundCommandResponsePayload, BackgroundCommandResponsePayloadRef, BackgroundProtocolEvent, BidiChannelListenerResidence, BidiChannelOwnerAction, BidiChannelPageOwner, CdpConnection, - CdpRendererCommandPolicy, CdpRendererDispatchLane, CdpSchedulerEvent, CdpSessionRoute, - ClaimedPendingInspectorAwait, Cmd, CommandOwnerScope, CompletedMoliDiagnosticsDispatch, + CdpRendererCommandPolicy, CdpSchedulerEvent, CdpSessionRoute, ClaimedPendingInspectorAwait, + Cmd, CommandOwnerScope, CompletedMoliDiagnosticsDispatch, CompletedRuntimeBindingPageCommandDispatch, CompletedRuntimeChildDefaultContextLookupDispatch, CompletedRuntimeEnableEventsDispatch, CompletedRuntimeProtocolMessageDispatch, CompletedServiceWorkerRuntimeProtocolMessageDispatch, @@ -36,11 +36,11 @@ use crate::conn::{ PendingRuntimeEnableEventsDispatch, PendingRuntimeProtocolMessageDispatch, PendingServiceWorkerRuntimeProtocolMessageDispatch, PendingSharedWorkerRuntimeProtocolMessageDispatch, ProfilerInspectorCommand, - RendererCommandDescriptor, RuntimeBindingDefinition, RuntimeEnableReplayEvent, - RuntimeInspectorAsyncCompletionReceiver, RuntimeInspectorResponseReady, - ServiceWorkerRuntimeExceptionSnapshot, SessionOwnerRuntimeFrontendEnableResult, - monotonic_timestamp_seconds, renderer_command_turn_frontend_protocol_response, - runtime_remote_object_ids_in_map, + RendererCommandDescriptor, RendererDispatchLane, RuntimeBindingDefinition, + RuntimeEnableReplayEvent, RuntimeInspectorAsyncCompletionReceiver, + RuntimeInspectorResponseReady, ServiceWorkerRuntimeExceptionSnapshot, + SessionOwnerRuntimeFrontendEnableResult, monotonic_timestamp_seconds, + renderer_command_turn_frontend_protocol_response, runtime_remote_object_ids_in_map, }; use crate::domains::actions::{ConsoleAction, HeapProfilerAction, RuntimeAction}; use crate::domains::command_output::{ @@ -521,6 +521,26 @@ impl RuntimeCommandCompletionMeta { } impl PendingRuntimeCommandDispatch { + pub(crate) fn renderer_dispatch_lane(&self) -> Option { + match &self.pending { + PendingRuntimeCommandKind::Inspector { pending } + | PendingRuntimeCommandKind::BindingInspector { pending, .. } => { + Some(renderer_dispatch_lane(pending.renderer_route())) + } + PendingRuntimeCommandKind::SharedWorkerInspector { .. } + | PendingRuntimeCommandKind::ServiceWorkerInspector { .. } => { + Some(RendererDispatchLane::Io) + } + PendingRuntimeCommandKind::Enable(_) + | PendingRuntimeCommandKind::BindingContextLookup { .. } => { + Some(RendererDispatchLane::Main) + } + PendingRuntimeCommandKind::InspectorDeferredReply { .. } + | PendingRuntimeCommandKind::MoliDiagnostics(_) + | PendingRuntimeCommandKind::BindingPage { .. } => None, + } + } + pub(crate) fn command_id(&self) -> Option { self.command_id } @@ -1021,6 +1041,21 @@ pub(crate) fn try_start_runtime_command_dispatch( } } +pub(crate) fn command_waits_for_document_projection(cmd: &Cmd<'_>) -> bool { + !matches!( + cmd.parse_action::(), + None | Some( + RuntimeAction::AddBinding + | RuntimeAction::RemoveBinding + | RuntimeAction::TerminateExecution + ) + ) +} + +pub(crate) fn debugger_command_waits_for_document_projection(cmd: &Cmd<'_>) -> bool { + debugger_renderer_dispatch_lane(cmd.action) == RendererDispatchLane::Main && can_dispatch(cmd) +} + fn start_main_runtime_inspector_command( conn: &mut CdpConnection, cmd: &Cmd<'_>, @@ -1080,10 +1115,16 @@ fn start_main_runtime_inspector_command_for_owner( )); } }; + let renderer_lane = if action == RuntimeAction::TerminateExecution { + RendererDispatchLane::Io + } else { + RendererDispatchLane::Main + }; let pending = match start_pending_runtime_routable_inspector_dispatch( conn, cmd, &owner_scope, + renderer_lane, inspector_json, cmd.terminal_response_delivery(), ) { @@ -1281,26 +1322,20 @@ pub(crate) fn start_debugger_inspector_command_dispatch( } let owner_scope = CommandOwnerScope::capture(conn, cmd.session_id); - let pending = match cmd.renderer_policy().renderer_lane() { - Some(CdpRendererDispatchLane::Main) => { - start_pending_runtime_inspector_dispatch_with_delivery( - conn, - cmd, - &owner_scope, - inspector_json, - cmd.terminal_response_delivery(), - ) - } - Some(CdpRendererDispatchLane::Io) => start_pending_runtime_io_inspector_dispatch( + let pending = match debugger_renderer_dispatch_lane(cmd.action) { + RendererDispatchLane::Main => start_pending_runtime_inspector_dispatch_with_delivery( conn, cmd, &owner_scope, inspector_json, cmd.terminal_response_delivery(), ), - None => Err( - "a command without renderer fallthrough cannot enter the Debugger Inspector dispatcher" - .to_owned(), + RendererDispatchLane::Io => start_pending_runtime_io_inspector_dispatch( + conn, + cmd, + &owner_scope, + inspector_json, + cmd.terminal_response_delivery(), ), }; let pending = match pending { @@ -1861,7 +1896,7 @@ fn try_start_pending_runtime_binding_command( || should_persist && conn .runtime_session_owner_slot_for_owner(&owner_scope) - .is_ok_and(|slot| slot.renderer_document_navigation_is_suspended()); + .is_ok_and(|slot| slot.document_projection_is_pending()); if live_page_update_unavailable { task.command_response = Some(RuntimeBindingCommandResponse::empty_success()); let meta = RuntimeCommandCompletionMeta { @@ -1938,33 +1973,52 @@ fn start_pending_runtime_routable_inspector_dispatch( conn: &mut CdpConnection, cmd: &Cmd<'_>, owner: &CommandOwnerScope, + lane: RendererDispatchLane, inspector_json: String, response_delivery: RendererInspectorResponseDelivery, ) -> Result { - match cmd.renderer_policy().renderer_lane() { - Some(CdpRendererDispatchLane::Main) => { - start_pending_runtime_inspector_dispatch_with_delivery( - conn, - cmd, - owner, - inspector_json, - response_delivery, - ) - } - Some(CdpRendererDispatchLane::Io) => start_pending_runtime_io_inspector_dispatch( + match lane { + RendererDispatchLane::Main => start_pending_runtime_inspector_dispatch_with_delivery( conn, cmd, owner, inspector_json, response_delivery, ), - None => Err( - "a command without renderer fallthrough cannot enter the Runtime Inspector dispatcher" - .to_owned(), + RendererDispatchLane::Io => start_pending_runtime_io_inspector_dispatch( + conn, + cmd, + owner, + inspector_json, + response_delivery, ), } } +fn renderer_dispatch_lane( + route: moli_core::page::RendererInspectorCommandRoute, +) -> RendererDispatchLane { + match route { + moli_core::page::RendererInspectorCommandRoute::MainThread => RendererDispatchLane::Main, + moli_core::page::RendererInspectorCommandRoute::Io => RendererDispatchLane::Io, + } +} + +fn debugger_renderer_dispatch_lane(action: &str) -> RendererDispatchLane { + match action { + "getPossibleBreakpoints" + | "getScriptSource" + | "getStackTrace" + | "pause" + | "removeBreakpoint" + | "resume" + | "setBreakpoint" + | "setBreakpointByUrl" + | "setBreakpointsActive" => RendererDispatchLane::Io, + _ => RendererDispatchLane::Main, + } +} + fn start_pending_runtime_io_inspector_dispatch( conn: &mut CdpConnection, cmd: &Cmd<'_>, @@ -2262,7 +2316,7 @@ fn pre_register_runtime_await_if_needed( owner, object_group, )?; - conn.register_runtime_await_job_for_owner(command_id, owner, object_group, action); + conn.trace_runtime_await_started(command_id, owner, object_group, action); conn.trace_runtime_await_pending_registered(command_id, owner.session_id()); Ok(Some(command_id)) } diff --git a/moli-protocol/src/domains/runtime/tests/bindings.rs b/moli-protocol/src/domains/runtime/tests/bindings.rs index 4257f092de..369affcb19 100644 --- a/moli-protocol/src/domains/runtime/tests/bindings.rs +++ b/moli-protocol/src/domains/runtime/tests/bindings.rs @@ -322,7 +322,7 @@ async fn add_binding_during_document_navigation_persists_without_touching_retiri ctx.conn.commit_declared_session_fixtures_for_test(); assert!( ctx.conn - .renderer_document_navigation_is_suspended_for_session_owner(Some("SID-1")) + .document_projection_is_pending_for_session_owner(Some("SID-1")) ); ctx.process_async(json!({ diff --git a/moli-protocol/src/domains/session.rs b/moli-protocol/src/domains/session.rs index 239a3fac70..d44fbc575c 100644 --- a/moli-protocol/src/domains/session.rs +++ b/moli-protocol/src/domains/session.rs @@ -1,5 +1,5 @@ use crate::conn::{ - BackgroundProtocolEvent, CdpConnection, SessionDisposalPlan, SessionDisposalTarget, + BackgroundProtocolEvent, CdpConnection, DevToolsSessionHandlerSet, SessionDisposalPlan, }; #[derive(Clone, Copy, Debug, PartialEq, Eq)] @@ -44,22 +44,13 @@ const WORKER_HANDLERS: &[DevToolsSessionDomainHandler] = &[ /// state or disable operation. Page Inspector resources have already retired /// through the renderer session lifecycle; these handlers revoke service /// contributions and reconcile Browser-owned policy without that Inspector. -struct DevToolsSessionHandlers { - handlers: &'static [DevToolsSessionDomainHandler], -} - -impl DevToolsSessionHandlers { - fn for_target(target: &SessionDisposalTarget) -> Self { - let handlers = match target { - SessionDisposalTarget::PageTarget { .. } => PAGE_HANDLERS, - SessionDisposalTarget::SharedWorkerTarget { .. } - | SessionDisposalTarget::DedicatedWorkerTarget { .. } - | SessionDisposalTarget::ServiceWorkerTarget { .. } => WORKER_HANDLERS, - SessionDisposalTarget::Browser | SessionDisposalTarget::TabTarget { .. } => { - CONNECTION_HANDLERS - } - }; - Self { handlers } +impl DevToolsSessionHandlerSet { + fn handlers(self) -> &'static [DevToolsSessionDomainHandler] { + match self { + Self::Connection => CONNECTION_HANDLERS, + Self::Page => PAGE_HANDLERS, + Self::Worker => WORKER_HANDLERS, + } } } @@ -92,7 +83,7 @@ pub(crate) async fn dispose_live_handlers_async( first_error: None, renderer_output_predecessor: None, }; - for handler in DevToolsSessionHandlers::for_target(plan.target()).handlers { + for handler in plan.handler_set().handlers() { let result = handler .dispose_async( conn, @@ -211,30 +202,16 @@ impl DevToolsSessionDomainHandler { #[cfg(test)] mod tests { use super::*; - use moli_page_types::DevToolsSessionKey; #[test] fn handler_sets_are_owned_by_session_target_kind() { - let page = SessionDisposalTarget::PageTarget { - browser_context_id: "BID".to_owned(), - target_id: "TID".to_owned(), - session_key: DevToolsSessionKey::Attached("SID".to_owned()), - }; + assert_eq!(DevToolsSessionHandlerSet::Page.handlers(), PAGE_HANDLERS); assert_eq!( - DevToolsSessionHandlers::for_target(&page).handlers, - PAGE_HANDLERS - ); - - let worker = SessionDisposalTarget::DedicatedWorkerTarget { - browser_context_id: "BID".to_owned(), - target_id: "WID".to_owned(), - }; - assert_eq!( - DevToolsSessionHandlers::for_target(&worker).handlers, + DevToolsSessionHandlerSet::Worker.handlers(), WORKER_HANDLERS ); assert_eq!( - DevToolsSessionHandlers::for_target(&SessionDisposalTarget::Browser).handlers, + DevToolsSessionHandlerSet::Connection.handlers(), CONNECTION_HANDLERS ); } diff --git a/moli-protocol/src/domains/target/session_disposal.rs b/moli-protocol/src/domains/target/session_disposal.rs index a71b663ff3..0bd4bc4bca 100644 --- a/moli-protocol/src/domains/target/session_disposal.rs +++ b/moli-protocol/src/domains/target/session_disposal.rs @@ -8,10 +8,8 @@ fn prepare_session_disposal( detachment: TargetSessionDetachCleanupPlan, ) -> anyhow::Result<(SessionDisposalPlan, TargetSessionDetachCleanupPlan)> { let session_id = detachment.session_id(); - let route = conn - .session_route(Some(session_id)) - .ok_or_else(|| anyhow::anyhow!("InvalidSessionId"))?; - let plan = SessionDisposalPlan::for_session_route(session_id, &route) + let plan = conn + .session_disposal_plan(session_id) .ok_or_else(|| anyhow::anyhow!("InvalidSessionId"))?; anyhow::ensure!( plan.target_id() == Some(detachment.target_id()), @@ -24,10 +22,8 @@ fn prepare_browser_session_disposal( conn: &CdpConnection, session_id: &str, ) -> anyhow::Result { - let route = conn - .session_route(Some(session_id)) - .ok_or_else(|| anyhow::anyhow!("InvalidSessionId"))?; - let plan = SessionDisposalPlan::for_session_route(session_id, &route) + let plan = conn + .session_disposal_plan(session_id) .ok_or_else(|| anyhow::anyhow!("InvalidSessionId"))?; anyhow::ensure!( matches!(plan.target(), SessionDisposalTarget::Browser), @@ -175,10 +171,8 @@ pub(super) async fn dispose_primary_page_session_preserving_frontend_async( protocol_events: &mut Vec, session_id: &str, ) -> anyhow::Result> { - let route = conn - .session_route(Some(session_id)) - .ok_or_else(|| anyhow::anyhow!("InvalidSessionId"))?; - let plan = SessionDisposalPlan::for_session_route(session_id, &route) + let plan = conn + .session_disposal_plan(session_id) .ok_or_else(|| anyhow::anyhow!("InvalidSessionId"))?; if !matches!( plan.target(), diff --git a/moli-protocol/src/domains/target/tests/tests_cdp_chromium_imports/p1_target_multipage.rs b/moli-protocol/src/domains/target/tests/tests_cdp_chromium_imports/p1_target_multipage.rs index 61cf1442a9..add2f03c88 100644 --- a/moli-protocol/src/domains/target/tests/tests_cdp_chromium_imports/p1_target_multipage.rs +++ b/moli-protocol/src/domains/target/tests/tests_cdp_chromium_imports/p1_target_multipage.rs @@ -1,6 +1,6 @@ use super::super::tests_cdp_smoke_fixture::SmokeFixtureServer; use super::super::*; -use crate::{CdpCommandTaskStep, CommandDispatchContext, ParsedCdpCommand}; +use crate::{AgentHostDispatchResult, CommandDispatchContext, ParsedCdpCommand}; use serde_json::{Value, json}; fn event<'a>(messages: &'a [Value], method: &str) -> &'a Value { @@ -280,7 +280,7 @@ async fn create_isolated_world_restart_does_not_inherit_the_stale_renderer_strea })) .expect("createIsolatedWorld command should parse"); let mut command_context = CommandDispatchContext::default(); - let CdpCommandTaskStep::Pending(first_pending) = ctx + let AgentHostDispatchResult::PendingService(first_pending) = ctx .conn .start_parsed_command_dispatch_with_context(&command, &mut command_context) else { @@ -298,7 +298,7 @@ async fn create_isolated_world_restart_does_not_inherit_the_stale_renderer_strea assert_eq!(navigation["result"]["frameId"], json!(target_id)); let first_completed = first_pending.wait().await; - let CdpCommandTaskStep::Pending(restarted) = ctx + let AgentHostDispatchResult::PendingService(restarted) = ctx .conn .complete_pending_command_dispatch_with_context(first_completed, &mut command_context) .await @@ -311,7 +311,7 @@ async fn create_isolated_world_restart_does_not_inherit_the_stale_renderer_strea ); let replacement_completed = restarted.wait().await; - let CdpCommandTaskStep::Complete(outcome) = ctx + let AgentHostDispatchResult::Complete(outcome) = ctx .conn .complete_pending_command_dispatch_with_context(replacement_completed, &mut command_context) .await diff --git a/moli-protocol/src/domains/target/worker_target.rs b/moli-protocol/src/domains/target/worker_target.rs index 2f8450d465..c6caf61bf0 100644 --- a/moli-protocol/src/domains/target/worker_target.rs +++ b/moli-protocol/src/domains/target/worker_target.rs @@ -2052,6 +2052,7 @@ fn remove_shared_worker_target_with_reason( ); CdpConnection::fail_pending_inspector_awaits_from_shared_worker_target_session_background_events_into( &mut pending_await_direct_events, + &mut pending_await_claimed_events, &mut target, session_id, reason, @@ -2220,11 +2221,14 @@ fn remove_service_worker_target_with_reason( ); } let mut target_pending_await_events = Vec::new(); + let mut target_claimed_await_events = Vec::new(); CdpConnection::fail_pending_inspector_awaits_from_service_worker_target_state_background_events_into( &mut target_pending_await_events, + &mut target_claimed_await_events, &mut target, reason, ); + target_pending_await_events.extend(target_claimed_await_events); push_service_worker_version_events(&mut outputs, version.clone(), target_pending_await_events); if let Some(retirement) = run_retirement { outputs.push(WorkerTargetLifecycleOutput::ServiceWorkerRunRetired { retirement }); diff --git a/moli-protocol/src/lib.rs b/moli-protocol/src/lib.rs index 794f3b8656..5b2cae6514 100644 --- a/moli-protocol/src/lib.rs +++ b/moli-protocol/src/lib.rs @@ -18,9 +18,9 @@ pub mod version; pub use devtools_runtime::*; pub use conn::{ - BackgroundCommandResponsePayload, BackgroundProtocolEvent, CdpCommandTaskStep, CdpConnection, - CdpInitialStoragePartition, CdpRendererCommandReplacement, CdpRendererCommandReplayDispatch, - CdpRendererDispatchLane, CdpRendererOwnerTurnOutcome, CdpSchedulerEvent, + AgentHostDispatchResult, BackgroundCommandResponsePayload, BackgroundProtocolEvent, + CdpCommandTaskStep, CdpConnection, CdpInitialStoragePartition, CdpRendererCommandReplacement, + CdpRendererCommandReplayDispatch, CdpRendererOwnerTurnOutcome, CdpSchedulerEvent, CdpTargetHostLifecycleDelta, CdpTargetHostLifecycleObserver, CdpTurnOutcome, CommandDispatchContext, CommandResponseFlushContext, CommandResponseFlushPermit, CompletedCdpCommandDispatch, CompletedDeferredMainDocumentLoadCompletion, @@ -31,12 +31,13 @@ pub use conn::{ DevToolsDocumentLifecycleWaitKey, DevToolsDocumentLifecycleWaitState, DevToolsDocumentNavigationState, DevToolsPageResidenceIdentity, ParsedCdpCommand, PendingCdpCommandDispatch, PendingDeferredMainDocumentLoadCompletion, - PendingRuntimeProtocolMessageDispatch, + PendingRuntimeProtocolMessageDispatch, RendererDispatch, RendererDispatchBinding, + RendererDispatchLane, RendererPageDispatchBinding, }; pub use domains::activity::{ ProtocolSchedulerWork, ProtocolSchedulerWorkKind, ProtocolWorkPublishSequence, - RuntimeCommandOutputBarrierCompletion, RuntimeCommandOutputBarrierPermit, - RuntimeCommandOutputBarrierTerminal, RuntimeCommandOutputBarriers, + RendererCommandResponseCompletion, RendererCommandResponseOrder, RendererCommandResponsePermit, + RendererCommandResponseTerminal, }; pub use domains::page::{ BackgroundNavigationCompletion, CompletedPageScreencastCapture, diff --git a/moli-protocol/src/testing.rs b/moli-protocol/src/testing.rs index c6f9961738..cc6f31f3ca 100644 --- a/moli-protocol/src/testing.rs +++ b/moli-protocol/src/testing.rs @@ -14,8 +14,8 @@ use super::conn::{ }; use crate::devtools_runtime::{DevToolsCommand, DevToolsCommandResult, DevToolsError}; use crate::domains::activity::{ - ProtocolSchedulerWork, ProtocolSchedulerWorkKind, RuntimeCommandOutputBarrierCompletion, - RuntimeCommandOutputBarrierPermit, RuntimeCommandOutputBarriers, + ProtocolSchedulerWork, ProtocolSchedulerWorkKind, RendererCommandResponseCompletion, + RendererCommandResponseOrder, RendererCommandResponsePermit, }; use moli_core::{ LayoutPolicy, OptionalResourceFetchMask, RendererOutputFence, RendererOutputStreamControl, @@ -42,7 +42,7 @@ pub struct TestContext { pub sent: Vec, pending_runtime_deferred_replies: VecDeque, pending_protocol_scheduler_work: VecDeque, - runtime_command_output_barriers: RuntimeCommandOutputBarriers, + renderer_command_response_order: RendererCommandResponseOrder, runtime_inspector_response_ready_rx: tokio::sync::mpsc::UnboundedReceiver, renderer_publication_rx: moli_core::RendererOutputTransportReceiver, @@ -58,19 +58,19 @@ pub struct TestContext { struct PendingTestRuntimeDeferredReply { pending: PendingCdpCommandDispatch, command_context: CommandDispatchContext, - runtime_output_barrier: Option, + renderer_response_permit: Option, } impl PendingTestRuntimeDeferredReply { fn new( pending: PendingCdpCommandDispatch, command_context: CommandDispatchContext, - runtime_output_barrier: Option, + renderer_response_permit: Option, ) -> Self { Self { pending, command_context, - runtime_output_barrier, + renderer_response_permit, } } @@ -86,14 +86,14 @@ enum TestSchedulerWork { BackgroundNavigationCompletion(crate::domains::page::BackgroundNavigationCompletion), RuntimeDeferredReplyReady(RuntimeInspectorResponseReady), RendererPublication(RendererOutputTransportMessage), - ReleaseRuntimeOutputBarrier(RuntimeCommandOutputBarrierPermit), - CancelRuntimeOutputBarrier(RuntimeCommandOutputBarrierPermit), + ReleaseRendererResponsePermit(RendererCommandResponsePermit), + CancelRendererResponsePermit(RendererCommandResponsePermit), } #[must_use = "the held test command response must be released exactly once"] pub(crate) struct TestCommandResponseFlushPermit { response_flush: CommandResponseFlushPermit, - runtime_output_barrier: Option, + renderer_response_permit: Option, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -215,7 +215,7 @@ impl TestContext { sent: Vec::new(), pending_runtime_deferred_replies: VecDeque::new(), pending_protocol_scheduler_work: VecDeque::new(), - runtime_command_output_barriers: RuntimeCommandOutputBarriers::default(), + renderer_command_response_order: RendererCommandResponseOrder::default(), runtime_inspector_response_ready_rx, renderer_publication_rx, background_event_tx, @@ -350,7 +350,9 @@ impl TestContext { .conn .commit_loaded_navigation(prepared) .expect("navigation fixture Page commit must succeed"); - assert!(page_commit.inspection_projection.is_ok()); + let projection_fence = page_commit + .inspection_projection + .expect("navigation fixture must rebind its renderer inspection endpoint"); assert!( page_commit .committed_document_post_response_continuation @@ -358,18 +360,6 @@ impl TestContext { "lifecycle-target fixture must not retain a DocumentCommit response gate" ); page_commit.previous_document_retirement.close().await; - let finished = self - .conn - .finish_renderer_document_navigation_for_owner(&owner, &token) - .expect("navigation fixture must finish its renderer suspension"); - assert!( - finished.released_output.is_empty(), - "fixture output is ingested after the Document binding" - ); - assert!( - finished.renderer_call_replacements.is_none(), - "fixture must not replace in-flight renderer calls" - ); let (binding, _) = self.conn.project_committed_document_lifecycle_for_owner( &owner, page_commit.lifecycle, @@ -379,6 +369,19 @@ impl TestContext { ); let binding = binding.expect("navigation fixture must install its exact renderer Document binding"); + let finished = self.conn.publish_document_projection_fence_for_owner( + &owner, + &binding, + projection_fence, + ); + assert!( + finished.released_output.is_empty(), + "fixture output is ingested after the Document binding" + ); + assert!( + finished.renderer_call_replacements.is_none(), + "fixture must not replace in-flight renderer calls" + ); assert_eq!( self.conn .target_root_document_lifecycle_identity_for_owner(&owner,), @@ -573,8 +576,9 @@ impl TestContext { let mut command_context = CommandDispatchContext::new(response_flush_context); let step = self .conn - .start_parsed_command_dispatch_with_context(&command, &mut command_context); - let mut runtime_output_barrier = self.admit_runtime_output_barrier(&command); + .start_parsed_command_dispatch_with_context(&command, &mut command_context) + .into(); + let mut renderer_response_permit = self.admit_renderer_response_permit(&command); let mut protocol_events = Vec::new(); let mut scheduler_events = Vec::new(); let completed = Box::pin(self.complete_command_step_like_scheduler( @@ -582,7 +586,7 @@ impl TestContext { &mut command_context, &mut protocol_events, &mut scheduler_events, - &mut runtime_output_barrier, + &mut renderer_response_permit, )) .await; assert!( @@ -596,7 +600,7 @@ impl TestContext { Box::pin(self.route_ready_test_command_response(command_id, response_start)).await; TestCommandResponseFlushPermit { response_flush: response_flush_permit, - runtime_output_barrier, + renderer_response_permit, } } @@ -611,9 +615,12 @@ impl TestContext { permit: TestCommandResponseFlushPermit, ) { permit.response_flush.finish(); - if let Some(runtime_output_barrier) = permit.runtime_output_barrier { + if let Some(renderer_response_permit) = permit.renderer_response_permit { Box::pin( - self.release_runtime_output_barrier_like_scheduler(runtime_output_barrier, true), + self.release_renderer_response_permit_like_scheduler( + renderer_response_permit, + true, + ), ) .await; } @@ -631,13 +638,13 @@ impl TestContext { }; let mut protocol_events = Vec::new(); let mut scheduler_events = Vec::new(); - let mut runtime_output_barrier = None; + let mut renderer_response_permit = None; let completed = Box::pin(self.complete_command_step_like_scheduler( step, &mut crate::conn::CommandDispatchContext::default(), &mut protocol_events, &mut scheduler_events, - &mut runtime_output_barrier, + &mut renderer_response_permit, )) .await; if completed { @@ -741,14 +748,15 @@ impl TestContext { let mut command_context = crate::conn::CommandDispatchContext::default(); let step = self .conn - .start_parsed_command_dispatch_with_context(command, &mut command_context); - let mut runtime_output_barrier = self.admit_runtime_output_barrier(command); + .start_parsed_command_dispatch_with_context(command, &mut command_context) + .into(); + let mut renderer_response_permit = self.admit_renderer_response_permit(command); let completed = Box::pin(self.complete_command_step_like_scheduler( step, &mut command_context, &mut protocol_events, &mut scheduler_events, - &mut runtime_output_barrier, + &mut renderer_response_permit, )) .await; @@ -768,17 +776,17 @@ impl TestContext { Box::pin(self.route_test_scheduler_causal_batch(protocol_events, scheduler_events)) .await; if completed { - if let Some(runtime_output_barrier) = runtime_output_barrier { - Box::pin(self.release_runtime_output_barrier_like_scheduler( - runtime_output_barrier, + if let Some(renderer_response_permit) = renderer_response_permit { + Box::pin(self.release_renderer_response_permit_like_scheduler( + renderer_response_permit, true, )) .await; } } else { assert!( - runtime_output_barrier.is_none(), - "a pending Runtime command must transfer its output barrier to the pending reply" + renderer_response_permit.is_none(), + "a pending renderer command must transfer its response permit to the pending reply" ); } if !completed && !self.pending_runtime_deferred_replies.is_empty() { @@ -788,10 +796,10 @@ impl TestContext { } else { Box::pin(self.route_test_scheduler_causal_batch(protocol_events, Vec::new())).await; if completed { - if let Some(runtime_output_barrier) = runtime_output_barrier { + if let Some(renderer_response_permit) = renderer_response_permit { let mut release_scheduler_events = - Box::pin(self.release_runtime_output_barrier_like_scheduler( - runtime_output_barrier, + Box::pin(self.release_renderer_response_permit_like_scheduler( + renderer_response_permit, false, )) .await; @@ -799,8 +807,8 @@ impl TestContext { } } else { assert!( - runtime_output_barrier.is_none(), - "a pending Runtime command must transfer its output barrier to the pending reply" + renderer_response_permit.is_none(), + "a pending renderer command must transfer its response permit to the pending reply" ); } if !completed && !self.pending_runtime_deferred_replies.is_empty() { @@ -810,14 +818,14 @@ impl TestContext { } } - fn admit_runtime_output_barrier( + fn admit_renderer_response_permit( &mut self, command: &ParsedCdpCommand, - ) -> Option { + ) -> Option { command .runtime_command_executes_page_javascript() .then(|| { - self.runtime_command_output_barriers.admit( + self.renderer_command_response_order.admit( &self.conn, command.request().id(), command.command_output_session_id(), @@ -832,7 +840,7 @@ impl TestContext { command_context: &mut CommandDispatchContext, protocol_events: &mut Vec, scheduler_events: &mut Vec, - runtime_output_barrier: &mut Option, + renderer_response_permit: &mut Option, ) -> bool { loop { match step { @@ -905,7 +913,7 @@ impl TestContext { self.enqueue_pending_runtime_deferred_reply( *pending, std::mem::take(command_context), - runtime_output_barrier.take(), + renderer_response_permit.take(), ); return false; } @@ -920,7 +928,8 @@ impl TestContext { completed, command_context, )) - .await; + .await + .into(); } } } @@ -1038,15 +1047,15 @@ impl TestContext { (self.sent.drain(sent_start..).collect(), scheduler_events) } - async fn release_runtime_output_barrier_like_scheduler( + async fn release_renderer_response_permit_like_scheduler( &mut self, - permit: RuntimeCommandOutputBarrierPermit, + permit: RendererCommandResponsePermit, route_scheduler_events: bool, ) -> Vec { let completion = self .conn - .release_runtime_command_output_barrier_turn_async( - &mut self.runtime_command_output_barriers, + .release_renderer_command_response_permit_turn_async( + &mut self.renderer_command_response_order, permit, ) .await; @@ -1062,9 +1071,9 @@ impl TestContext { } } - fn enqueue_runtime_output_barrier_completion( + fn enqueue_renderer_response_permit_completion( &mut self, - completion: RuntimeCommandOutputBarrierCompletion, + completion: RendererCommandResponseCompletion, work: &mut VecDeque, ) { let (protocol_events, scheduler_events) = @@ -1114,7 +1123,7 @@ impl TestContext { &mut self, mut pending: PendingCdpCommandDispatch, command_context: CommandDispatchContext, - runtime_output_barrier: Option, + renderer_response_permit: Option, ) { if let Some(command_id) = pending.command_id() && let Some(response_rx) = pending.take_scheduler_deferred_inspector_reply_receiver() @@ -1139,7 +1148,7 @@ impl TestContext { .push_back(PendingTestRuntimeDeferredReply::new( pending, command_context, - runtime_output_barrier, + renderer_response_permit, )); } @@ -1178,25 +1187,25 @@ impl TestContext { Box::pin(self.ingest_renderer_publication_like_scheduler(publication, work)) .await; } - TestSchedulerWork::ReleaseRuntimeOutputBarrier(permit) => { + TestSchedulerWork::ReleaseRendererResponsePermit(permit) => { let completion = self .conn - .release_runtime_command_output_barrier_turn_async( - &mut self.runtime_command_output_barriers, + .release_renderer_command_response_permit_turn_async( + &mut self.renderer_command_response_order, permit, ) .await; - self.enqueue_runtime_output_barrier_completion(completion, work); + self.enqueue_renderer_response_permit_completion(completion, work); } - TestSchedulerWork::CancelRuntimeOutputBarrier(permit) => { + TestSchedulerWork::CancelRendererResponsePermit(permit) => { let completion = self .conn - .cancel_runtime_command_output_barrier_turn_async( - &mut self.runtime_command_output_barriers, + .cancel_renderer_command_response_permit_turn_async( + &mut self.renderer_command_response_order, permit, ) .await; - self.enqueue_runtime_output_barrier_completion(completion, work); + self.enqueue_renderer_response_permit_completion(completion, work); } } self.route_ready_protocol_scheduler_work_for_test_context(work) @@ -1519,9 +1528,9 @@ impl TestContext { "message": "RuntimeDeferredReplyLooseProtocolResponse", }, })); - if let Some(runtime_output_barrier) = pending.runtime_output_barrier.take() { - work.push_front(TestSchedulerWork::CancelRuntimeOutputBarrier( - runtime_output_barrier, + if let Some(renderer_response_permit) = pending.renderer_response_permit.take() { + work.push_front(TestSchedulerWork::CancelRendererResponsePermit( + renderer_response_permit, )); } if !events.is_empty() { @@ -1632,7 +1641,7 @@ impl TestContext { .conn .ingest_renderer_output_turn_async( publication, - &mut self.runtime_command_output_barriers, + &mut self.renderer_command_response_order, ) .await; let (protocol_events, scheduler_events) = outcome.into_protocol_event_parts(); @@ -1738,7 +1747,8 @@ impl TestContext { let step = self .conn .complete_pending_command_dispatch_with_context(completed, &mut pending.command_context) - .await; + .await + .into(); let mut protocol_events = Vec::new(); let mut scheduler_events = Vec::new(); let completed = Box::pin(self.complete_command_step_like_scheduler( @@ -1746,22 +1756,22 @@ impl TestContext { &mut pending.command_context, &mut protocol_events, &mut scheduler_events, - &mut pending.runtime_output_barrier, + &mut pending.renderer_response_permit, )) .await; if !suffix_events.is_empty() { work.push_front(TestSchedulerWork::ProtocolEvents(suffix_events)); } if completed { - if let Some(runtime_output_barrier) = pending.runtime_output_barrier { - work.push_front(TestSchedulerWork::ReleaseRuntimeOutputBarrier( - runtime_output_barrier, + if let Some(renderer_response_permit) = pending.renderer_response_permit { + work.push_front(TestSchedulerWork::ReleaseRendererResponsePermit( + renderer_response_permit, )); } } else { assert!( - pending.runtime_output_barrier.is_none(), - "a still-pending Runtime command must transfer its output barrier" + pending.renderer_response_permit.is_none(), + "a still-pending renderer command must transfer its response permit" ); } if !scheduler_events.is_empty() { @@ -2209,7 +2219,7 @@ mod tests { sent: Vec::new(), pending_runtime_deferred_replies: VecDeque::new(), pending_protocol_scheduler_work: VecDeque::new(), - runtime_command_output_barriers: RuntimeCommandOutputBarriers::default(), + renderer_command_response_order: RendererCommandResponseOrder::default(), runtime_inspector_response_ready_rx: runtime_response_rx, renderer_publication_rx: publication_rx, background_event_tx,