diff --git a/README.md b/README.md index 74bebb3e4..718c6dcf8 100644 --- a/README.md +++ b/README.md @@ -356,6 +356,11 @@ Its current intentional boundaries include: paint architecture. - It does not pursue pixel-for-pixel parity with Chrome or provide high-fidelity Canvas/WebGL/media playback. +- Emulation supports offline mode, but CPU throttling, network latency/throughput, + packet shaping, and connection type overrides return a protocol error. CPU rates + at or below 1 disable throttling; non-positive latency and throughput values + disable network throttling. Unsupported combinations are rejected before + changing offline state. - `--layout` supports software screenshots and raster-backed CDP PDF generation, but not every Chrome screenshot or print mode is implemented. diff --git a/docs/README.zh-CN.md b/docs/README.zh-CN.md index 0de61ffee..5cfa8a7bc 100644 --- a/docs/README.zh-CN.md +++ b/docs/README.zh-CN.md @@ -263,6 +263,7 @@ Kitesurf 是远程服务,无法测量 CPU、内存和进程数,因此资源 - 不提供 GUI 浏览器、持久化窗口、GPU 合成器,也不实现保留式的多帧绘制架构。 - 不追求与 Chrome 像素级一致的渲染效果,也不提供高保真的 Canvas/WebGL/媒体播放能力。 +- 环境模拟支持 offline 切换;CPU 节流、网络延迟/吞吐、丢包及连接类型覆盖暂不支持,请求会返回协议错误。CPU rate ≤ 1 和非正的网络延迟/吞吐值用于取消节流;不支持的组合会在改变 offline 状态前整体拒绝。 - 在 `--layout` 模式下支持软件截图和基于光栅化的 CDP PDF 生成,但没有实现 Chrome 的全部截图/打印模式。 遇到不支持的协议路径,Moli 会直接明确报错——它不会假装某个浏览器操作、事件、网络观测或者视觉结果已经发生。 diff --git a/moli-protocol/src/conn/browser_context/network_owner.rs b/moli-protocol/src/conn/browser_context/network_owner.rs index e7f8e35a8..1470bfa02 100644 --- a/moli-protocol/src/conn/browser_context/network_owner.rs +++ b/moli-protocol/src/conn/browser_context/network_owner.rs @@ -17,21 +17,9 @@ impl TargetSessionStateMut<'_> { true } - fn set_emulated_network_conditions( - mut self, - offline: bool, - latency: f64, - download_throughput: f64, - upload_throughput: f64, - connection_type: Option, - ) -> bool { - self.network_policy_mut().set_emulated_network_conditions( - offline, - latency, - download_throughput, - upload_throughput, - connection_type, - ) + fn set_network_offline(mut self, offline: bool) -> bool { + self.network_policy_mut().set_network_offline(offline); + offline } } @@ -289,23 +277,12 @@ impl TargetSessionOwnerMut<'_> { self.mutate_session_state_ref(|state| state.set_tls_verify_host_override(enabled)) } - fn start_set_emulated_network_conditions( + fn start_set_network_offline( mut self, offline: bool, - latency: f64, - download_throughput: f64, - upload_throughput: f64, - connection_type: Option, ) -> Result, String> { - let effective_offline = self.mutate_session_state_ref(|state| { - state.set_emulated_network_conditions( - offline, - latency, - download_throughput, - upload_throughput, - connection_type, - ) - }); + let effective_offline = + self.mutate_session_state_ref(|state| state.set_network_offline(offline)); let Some(page) = self.runtime_slot_mut().loaded_page_mut() else { return Ok(None); }; @@ -818,45 +795,24 @@ impl CdpConnection { self.start_rebuild_resource_runtime_for_session_owner(session_id) } - pub(crate) fn start_set_emulated_network_conditions_for_session_owner( + pub(crate) fn start_set_network_offline_for_session_owner( &mut self, session_id: Option<&str>, offline: bool, - latency: f64, - download_throughput: f64, - upload_throughput: f64, - connection_type: Option, ) -> Result, String> { let owner = crate::conn::CommandOwnerScope::capture(self, session_id); - self.start_set_emulated_network_conditions_for_owner( - &owner, - offline, - latency, - download_throughput, - upload_throughput, - connection_type, - ) + self.start_set_network_offline_for_owner(&owner, offline) } - pub(crate) fn start_set_emulated_network_conditions_for_owner( + pub(crate) fn start_set_network_offline_for_owner( &mut self, command_owner: &crate::conn::CommandOwnerScope, offline: bool, - latency: f64, - download_throughput: f64, - upload_throughput: f64, - connection_type: Option, ) -> Result, String> { let Some(owner) = self.target_session_owner_mut_for_owner(command_owner) else { return Err("BrowserContextNotLoaded".to_owned()); }; - owner.start_set_emulated_network_conditions( - offline, - latency, - download_throughput, - upload_throughput, - connection_type, - ) + owner.start_set_network_offline(offline) } } @@ -958,13 +914,7 @@ mod tests { network.blocked_url_patterns = vec!["*://blocked.test/*".to_owned()]; network.extra_headers = vec![("X-Test".to_owned(), "active".to_owned())]; } - let active_offline = active_session_state_mut(&mut active).set_emulated_network_conditions( - true, - 25.0, - 1024.0, - 256.0, - Some("cellular3g".to_owned()), - ); + let active_offline = active_session_state_mut(&mut active).set_network_offline(true); assert!( active @@ -994,34 +944,6 @@ mod tests { ); assert!(active_offline); assert!(active.active_page_target().network_policy.network_offline()); - assert_eq!( - active - .active_page_target() - .network_policy - .emulated_network_latency(), - 25.0 - ); - assert_eq!( - active - .active_page_target() - .network_policy - .emulated_download_throughput(), - 1024.0 - ); - assert_eq!( - active - .active_page_target() - .network_policy - .emulated_upload_throughput(), - 256.0 - ); - assert_eq!( - active - .active_page_target() - .network_policy - .emulated_connection_type(), - Some("cellular3g") - ); let mut background = PageTargetHost::empty("TID-network-owner-test".to_owned()); { @@ -1035,14 +957,8 @@ mod tests { network.blocked_url_patterns = vec!["*://background-blocked.test/*".to_owned()]; network.extra_headers = vec![("X-Test".to_owned(), "background".to_owned())]; } - let background_offline = background_session_state_mut(&mut background) - .set_emulated_network_conditions( - true, - 50.0, - 2048.0, - 512.0, - Some("cellular4g".to_owned()), - ); + let background_offline = + background_session_state_mut(&mut background).set_network_offline(true); assert!(background.effective_policy().cache_disabled()); assert!(background.effective_policy().bypass_service_worker()); @@ -1056,19 +972,6 @@ mod tests { ); assert!(background_offline); assert!(background.network_policy.network_offline()); - assert_eq!(background.network_policy.emulated_network_latency(), 50.0); - assert_eq!( - background.network_policy.emulated_download_throughput(), - 2048.0 - ); - assert_eq!( - background.network_policy.emulated_upload_throughput(), - 512.0 - ); - assert_eq!( - background.network_policy.emulated_connection_type(), - Some("cellular4g") - ); } #[test] diff --git a/moli-protocol/src/conn/dispatch_tests.rs b/moli-protocol/src/conn/dispatch_tests.rs index 2a1ac334b..c9e513044 100644 --- a/moli-protocol/src/conn/dispatch_tests.rs +++ b/moli-protocol/src/conn/dispatch_tests.rs @@ -8000,10 +8000,9 @@ async fn command_dispatch_completes_live_network_emulation_without_legacy_fallba "method": "Network.emulateNetworkConditions", "params": { "offline": true, - "latency": 20, - "downloadThroughput": 1024, - "uploadThroughput": 512, - "connectionType": "cellular3g" + "latency": 0, + "downloadThroughput": -1, + "uploadThroughput": -1, } })) .unwrap(); diff --git a/moli-protocol/src/conn/state/session.rs b/moli-protocol/src/conn/state/session.rs index 3fc758ebc..8af138f1d 100644 --- a/moli-protocol/src/conn/state/session.rs +++ b/moli-protocol/src/conn/state/session.rs @@ -538,35 +538,16 @@ pub(crate) struct InspectorSessionState { pub(crate) v8_state: Option, } -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, Default)] pub(crate) struct TargetNetworkPolicyState { // Target-wide policy contributed by WebDriver BiDi or connection defaults. base_cache_disabled: bool, network_offline: bool, - emulated_network_latency: f64, - emulated_download_throughput: f64, - emulated_upload_throughput: f64, - emulated_connection_type: Option, base_browser_identity: super::BaseBrowserIdentityOverrideState, // Target-scoped headers contributed by WebDriver BiDi. base_extra_headers: Vec<(String, String)>, } -impl Default for TargetNetworkPolicyState { - fn default() -> Self { - Self { - base_cache_disabled: false, - network_offline: false, - emulated_network_latency: 0.0, - emulated_download_throughput: -1.0, - emulated_upload_throughput: -1.0, - emulated_connection_type: None, - base_browser_identity: super::BaseBrowserIdentityOverrideState::default(), - base_extra_headers: Vec::new(), - } - } -} - impl TargetNetworkPolicyState { pub(crate) fn set_base_cache_disabled(&mut self, cache_disabled: bool) { self.base_cache_disabled = cache_disabled; @@ -586,7 +567,6 @@ impl TargetNetworkPolicyState { self.network_offline } - #[cfg(test)] pub(crate) fn set_network_offline(&mut self, network_offline: bool) { self.network_offline = network_offline; } @@ -638,42 +618,6 @@ impl TargetNetworkPolicyState { self.base_browser_identity .set_accept_language(accept_language, fallback); } - - #[cfg(test)] - pub(crate) fn emulated_network_latency(&self) -> f64 { - self.emulated_network_latency - } - - #[cfg(test)] - pub(crate) fn emulated_download_throughput(&self) -> f64 { - self.emulated_download_throughput - } - - #[cfg(test)] - pub(crate) fn emulated_upload_throughput(&self) -> f64 { - self.emulated_upload_throughput - } - - #[cfg(test)] - pub(crate) fn emulated_connection_type(&self) -> Option<&str> { - self.emulated_connection_type.as_deref() - } - - pub(crate) fn set_emulated_network_conditions( - &mut self, - offline: bool, - latency: f64, - download_throughput: f64, - upload_throughput: f64, - connection_type: Option, - ) -> bool { - self.network_offline = offline; - self.emulated_network_latency = latency; - self.emulated_download_throughput = download_throughput; - self.emulated_upload_throughput = upload_throughput; - self.emulated_connection_type = connection_type; - self.network_offline - } } #[cfg(test)] diff --git a/moli-protocol/src/conn/tests/resource_runtime.rs b/moli-protocol/src/conn/tests/resource_runtime.rs index 9d749772a..eaf46ffbb 100644 --- a/moli-protocol/src/conn/tests/resource_runtime.rs +++ b/moli-protocol/src/conn/tests/resource_runtime.rs @@ -3298,7 +3298,7 @@ async fn direct_network_policy_routes_to_inactive_active_owner_without_activatin r#"{"id":3,"method":"Network.setBlockedURLs","sessionId":"SID-B","params":{"urls":["*://blocked.test/*"]}}"#, r#"{"id":4,"method":"Network.setExtraHTTPHeaders","sessionId":"SID-B","params":{"headers":{"X-Test":"direct"}}}"#, r#"{"id":5,"method":"Network.setUserAgentOverride","sessionId":"SID-B","params":{"userAgent":"Moli/Direct-UA"}}"#, - r#"{"id":6,"method":"Network.emulateNetworkConditions","sessionId":"SID-B","params":{"offline":true,"latency":25,"downloadThroughput":1024,"uploadThroughput":256,"connectionType":"cellular3g"}}"#, + r#"{"id":6,"method":"Network.emulateNetworkConditions","sessionId":"SID-B","params":{"offline":true,"latency": 0,"downloadThroughput": -1,"uploadThroughput": -1}}"#, ] { let response = conn.process_message_messages_only_for_test(raw).await; let request_id = serde_json::from_str::(raw) @@ -3361,34 +3361,6 @@ async fn direct_network_policy_routes_to_inactive_active_owner_without_activatin .network_policy .network_offline() ); - assert_eq!( - inactive - .active_page_target() - .network_policy - .emulated_network_latency(), - 25.0 - ); - assert_eq!( - inactive - .active_page_target() - .network_policy - .emulated_download_throughput(), - 1024.0 - ); - assert_eq!( - inactive - .active_page_target() - .network_policy - .emulated_upload_throughput(), - 256.0 - ); - assert_eq!( - inactive - .active_page_target() - .network_policy - .emulated_connection_type(), - Some("cellular3g") - ); } #[tokio::test] @@ -3465,7 +3437,7 @@ async fn direct_network_policy_routes_to_inactive_background_owner_without_activ r#"{"id":3,"method":"Network.setBlockedURLs","sessionId":"SID-background","params":{"urls":["*://blocked-background.test/*"]}}"#, r#"{"id":4,"method":"Network.setExtraHTTPHeaders","sessionId":"SID-background","params":{"headers":{"X-Background":"direct"}}}"#, r#"{"id":5,"method":"Network.setUserAgentOverride","sessionId":"SID-background","params":{"userAgent":"Moli/Background-UA"}}"#, - r#"{"id":6,"method":"Network.emulateNetworkConditions","sessionId":"SID-background","params":{"offline":true,"latency":50,"downloadThroughput":2048,"uploadThroughput":512,"connectionType":"wifi"}}"#, + r#"{"id":6,"method":"Network.emulateNetworkConditions","sessionId":"SID-background","params":{"offline":true,"latency": 0,"downloadThroughput": -1,"uploadThroughput": -1}}"#, ] { let response = conn.process_message_messages_only_for_test(raw).await; let request_id = serde_json::from_str::(raw) @@ -3510,13 +3482,6 @@ async fn direct_network_policy_routes_to_inactive_background_owner_without_activ Some("Moli/Background-UA") ); assert!(staged.network_policy.network_offline()); - assert_eq!(staged.network_policy.emulated_network_latency(), 50.0); - assert_eq!(staged.network_policy.emulated_download_throughput(), 2048.0); - assert_eq!(staged.network_policy.emulated_upload_throughput(), 512.0); - assert_eq!( - staged.network_policy.emulated_connection_type(), - Some("wifi") - ); } #[tokio::test] diff --git a/moli-protocol/src/domains/emulation.rs b/moli-protocol/src/domains/emulation.rs index cb91561fe..e8433c3b2 100644 --- a/moli-protocol/src/domains/emulation.rs +++ b/moli-protocol/src/domains/emulation.rs @@ -361,12 +361,18 @@ fn start_cpu_throttling_rate_command( )); } }; + if params.rate > 1.0 { + return EmulationCommandTaskStep::Complete(CommandOutputPlan::error( + -32000, + "CPU throttling is not supported", + )); + } if conn.browser_context.is_none() { return EmulationCommandTaskStep::Complete(CommandOutputPlan::result(json!({}))); } if !conn.update_emulation_state_for_session_owner(cmd.session_id, |state| { if let Some(mut state) = state { - state.set_cpu_throttling_rate(params.rate); + state.set_cpu_throttling_rate(1.0); } }) { return EmulationCommandTaskStep::Complete(CommandOutputPlan::error( @@ -378,7 +384,7 @@ fn start_cpu_throttling_rate_command( let Some(page) = loaded_page_mut_for_target_configuration(conn, cmd.session_id) else { return EmulationCommandTaskStep::Complete(CommandOutputPlan::result(json!({}))); }; - match page.start_set_cpu_throttling_rate(params.rate) { + match page.start_set_cpu_throttling_rate(1.0) { Ok(pending) => EmulationCommandTaskStep::Pending(single_pending_emulation_dispatch( cmd.id, owner_scope, @@ -1389,14 +1395,7 @@ fn start_network_conditions_update_for_current_route( }; let owner = CommandOwnerScope::for_route(route.clone()); let network_update = conn - .start_set_emulated_network_conditions_for_owner( - &owner, - effective_offline, - 0.0, - -1.0, - -1.0, - None, - ) + .start_set_network_offline_for_owner(&owner, effective_offline) .map_err(devtools_emulation_owner_error)?; let mut pending = Vec::new(); if let Some(network_update) = network_update { diff --git a/moli-protocol/src/domains/emulation/tests.rs b/moli-protocol/src/domains/emulation/tests.rs index cdde7918c..7662a15c5 100644 --- a/moli-protocol/src/domains/emulation/tests.rs +++ b/moli-protocol/src/domains/emulation/tests.rs @@ -935,11 +935,7 @@ async fn live_apply_emulation_commands_without_loaded_page_do_not_use_legacy_fal "Emulation.setGeolocationOverride", json!({ "latitude": 48.85837, "longitude": 2.294481, "accuracy": 7 }), ), - ( - 9128, - "Emulation.setCPUThrottlingRate", - json!({ "rate": 2.5 }), - ), + (9128, "Emulation.setCPUThrottlingRate", json!({ "rate": 1 })), ] { let raw = json!({ "id": id, @@ -967,7 +963,7 @@ async fn live_apply_emulation_commands_without_loaded_page_do_not_use_legacy_fal .active_page_target() .effective_emulation_state .cpu_throttling_rate, - 2.5 + 1.0 ); } @@ -1069,7 +1065,7 @@ async fn device_metrics_completion_survives_initial_page_replacement() { } #[tokio::test(flavor = "multi_thread")] -async fn live_cpu_throttling_rate_uses_pending_command_dispatch() { +async fn clearing_cpu_throttling_uses_pending_command_dispatch() { let mut ctx = TestContext::new(); load_session_page_for_pending_emulation_test(&mut ctx).await; @@ -1077,7 +1073,7 @@ async fn live_cpu_throttling_rate_uses_pending_command_dispatch() { "id": 9129, "sessionId": "SID-1", "method": "Emulation.setCPUThrottlingRate", - "params": { "rate": 3.0 } + "params": { "rate": 1 } }) .to_string(); let CdpCommandTaskStep::Pending(pending) = ctx.conn.start_command_dispatch(&raw) else { @@ -1103,7 +1099,7 @@ async fn live_cpu_throttling_rate_uses_pending_command_dispatch() { .active_page_target() .effective_emulation_state .cpu_throttling_rate, - 3.0 + 1.0 ); } @@ -1385,7 +1381,7 @@ async fn multi_session_emulation_separates_handler_input_from_target_effective_s 71_101, "SID-primary", "Emulation.setCPUThrottlingRate", - json!({ "rate": 4 }), + json!({ "rate": 1 }), ) .await; expect_session_command_result( @@ -1393,7 +1389,7 @@ async fn multi_session_emulation_separates_handler_input_from_target_effective_s 71_102, "SID-attached", "Emulation.setCPUThrottlingRate", - json!({ "rate": 2 }), + json!({ "rate": 1 }), ) .await; expect_session_command_result( @@ -1439,8 +1435,8 @@ async fn multi_session_emulation_separates_handler_input_from_target_effective_s .conn .emulation_session_state_for_session_owner(Some("SID-attached")) .expect("attached Emulation handler state"); - assert_eq!(primary.cpu_throttling_rate, 4.0); - assert_eq!(attached.cpu_throttling_rate, 2.0); + assert_eq!(primary.cpu_throttling_rate, 1.0); + assert_eq!(attached.cpu_throttling_rate, 1.0); assert_eq!( primary .emulated_device_metrics @@ -1464,7 +1460,7 @@ async fn multi_session_emulation_separates_handler_input_from_target_effective_s .as_ref() .expect("browser context") .active_page_target(); - assert_eq!(target.effective_emulation_state.cpu_throttling_rate, 2.0); + assert_eq!(target.effective_emulation_state.cpu_throttling_rate, 1.0); assert_eq!( target .effective_emulation_state @@ -1499,7 +1495,7 @@ async fn multi_session_emulation_separates_handler_input_from_target_effective_s .conn .emulation_session_state_for_session_owner(Some("SID-primary")) .expect("primary Emulation handler state survives attached disposal"); - assert_eq!(primary.cpu_throttling_rate, 4.0); + assert_eq!(primary.cpu_throttling_rate, 1.0); assert!(primary.emulated_device_metrics.is_some()); assert!(primary.focus_emulation_enabled); assert_eq!( @@ -3465,7 +3461,7 @@ async fn target_session_detach_disposes_non_aggregated_emulation_state_before_re "id": 1901, "method": "Emulation.setCPUThrottlingRate", "sessionId": session_id, - "params": { "rate": 4 } + "params": { "rate": 1 } })) .await; ctx.expect_result(1901, json!({}), Some(&session_id)); diff --git a/moli-protocol/src/domains/emulation/tests/native_navigator.rs b/moli-protocol/src/domains/emulation/tests/native_navigator.rs index 18ecc2f59..5574fbc77 100644 --- a/moli-protocol/src/domains/emulation/tests/native_navigator.rs +++ b/moli-protocol/src/domains/emulation/tests/native_navigator.rs @@ -410,3 +410,65 @@ async fn device_metrics_zero_axes_use_visible_size_and_clear_restores_native_def .await; assert_eq!(evaluate(&mut ctx, snapshot).await, baseline); } + +#[tokio::test(flavor = "multi_thread")] +async fn unsupported_throttling_rejects_before_changing_live_offline_state() { + let mut ctx = setup().await; + for offline in [false, true] { + expect_session_command_result( + &mut ctx, + 88001, + "SID-1", + "Network.emulateNetworkConditions", + json!({"offline":offline,"latency":0,"downloadThroughput":-1,"uploadThroughput":0}), + ) + .await; + for unsupported in [ + json!({"latency":100}), + json!({"downloadThroughput":1024}), + json!({"uploadThroughput":1024}), + json!({"connectionType":"cellular3g"}), + json!({"packetLoss":1}), + json!({"packetQueueLength":1}), + json!({"packetReordering":true}), + ] { + let mut params = json!({"offline": !offline,"latency":0,"downloadThroughput":-1,"uploadThroughput":-1}); + params + .as_object_mut() + .unwrap() + .extend(unsupported.as_object().unwrap().clone()); + ctx.process_async(json!({"id":88001,"sessionId":"SID-1","method":"Network.emulateNetworkConditions","params":params})).await; + ctx.expect_error( + 88001, + -32000, + "Network throttling and connection type overrides are not supported", + ); + assert_eq!( + evaluate(&mut ctx, "navigator.onLine").await, + json!(!offline) + ); + } + } + for rate in [-1.0, 0.0, 0.5, 1.0] { + expect_session_command_result( + &mut ctx, + 88001, + "SID-1", + "Emulation.setCPUThrottlingRate", + json!({"rate":rate}), + ) + .await; + } + ctx.process_async(json!({"id":88001,"sessionId":"SID-1","method":"Emulation.setCPUThrottlingRate","params":{"rate":4}})).await; + ctx.expect_error(88001, -32000, "CPU throttling is not supported"); + assert_eq!( + ctx.conn + .browser_context + .as_ref() + .unwrap() + .active_page_target() + .effective_emulation_state + .cpu_throttling_rate, + 1.0 + ); +} diff --git a/moli-protocol/src/domains/network.rs b/moli-protocol/src/domains/network.rs index d38e18de3..a29076e2f 100644 --- a/moli-protocol/src/domains/network.rs +++ b/moli-protocol/src/domains/network.rs @@ -610,8 +610,8 @@ fn start_emulate_network_conditions_command( conn: &mut CdpConnection, cmd: &Cmd<'_>, ) -> NetworkCommandTaskStep { - let conditions = match settings::emulated_network_conditions_for_command(cmd) { - Ok(conditions) => conditions, + let offline = match settings::network_offline_for_emulation_command(cmd) { + Ok(offline) => offline, Err(plan) => return NetworkCommandTaskStep::Complete(plan), }; pending_network_page_command_step( @@ -619,16 +619,7 @@ fn start_emulate_network_conditions_command( cmd.id, cmd.session_id, PendingNetworkCommandKind::EmulateNetworkConditions, - |conn| { - conn.start_set_emulated_network_conditions_for_session_owner( - cmd.session_id, - conditions.offline, - conditions.latency, - conditions.download_throughput, - conditions.upload_throughput, - conditions.connection_type, - ) - }, + |conn| conn.start_set_network_offline_for_session_owner(cmd.session_id, offline), ) } diff --git a/moli-protocol/src/domains/network/settings.rs b/moli-protocol/src/domains/network/settings.rs index ee6d7e496..0361d7d60 100644 --- a/moli-protocol/src/domains/network/settings.rs +++ b/moli-protocol/src/domains/network/settings.rs @@ -16,14 +16,6 @@ struct SetBlockedUrlsParams { urls: Vec, } -pub(super) struct EmulatedNetworkConditionsForCommand { - pub(super) offline: bool, - pub(super) latency: f64, - pub(super) download_throughput: f64, - pub(super) upload_throughput: f64, - pub(super) connection_type: Option, -} - pub(super) fn enabled_command_output_plan( conn: &mut CdpConnection, session_id: Option<&str>, @@ -82,24 +74,34 @@ pub(super) fn blocked_urls_for_command(cmd: &Cmd<'_>) -> Result, Com } #[allow(deprecated)] -pub(super) fn emulated_network_conditions_for_command( +pub(super) fn network_offline_for_emulation_command( cmd: &Cmd<'_>, -) -> Result { +) -> Result { let params: EmulateNetworkConditionsParams = match cmd.get_params() { Ok(Some(params)) => params, _ => return Err(CommandOutputPlan::error(-32602, "InvalidParams")), }; - let connection_type = params - .connection_type - .as_ref() - .and_then(cdp_connection_type_string); - Ok(EmulatedNetworkConditionsForCommand { - offline: params.offline, - latency: params.latency, - download_throughput: params.download_throughput, - upload_throughput: params.upload_throughput, - connection_type, - }) + if !params.latency.is_finite() + || !params.download_throughput.is_finite() + || !params.upload_throughput.is_finite() + || params.packet_loss.is_some_and(|value| !value.is_finite()) + { + return Err(CommandOutputPlan::error(-32602, "InvalidParams")); + } + if params.latency > 0.0 + || params.download_throughput > 0.0 + || params.upload_throughput > 0.0 + || params.connection_type.is_some() + || params.packet_loss.is_some_and(|value| value != 0.0) + || params.packet_queue_length.is_some_and(|value| value != 0) + || params.packet_reordering == Some(true) + { + return Err(CommandOutputPlan::error( + -32000, + "Network throttling and connection type overrides are not supported", + )); + } + Ok(params.offline) } pub(super) fn extra_http_headers_for_command( @@ -251,11 +253,3 @@ fn extra_http_headers_from_params( .collect::>() }) } - -fn cdp_connection_type_string( - connection_type: &chromiumoxide_cdp::cdp::browser_protocol::network::ConnectionType, -) -> Option { - serde_json::to_value(connection_type) - .ok() - .and_then(|value| value.as_str().map(ToOwned::to_owned)) -} diff --git a/moli-protocol/src/domains/network/tests/blocked_urls.rs b/moli-protocol/src/domains/network/tests/blocked_urls.rs index b0576e33d..27fb8e82d 100644 --- a/moli-protocol/src/domains/network/tests/blocked_urls.rs +++ b/moli-protocol/src/domains/network/tests/blocked_urls.rs @@ -1307,10 +1307,9 @@ async fn emulate_network_conditions_updates_browser_context_state() { "method": "Network.emulateNetworkConditions", "params": { "offline": true, - "latency": 150, - "downloadThroughput": 1024, - "uploadThroughput": 512, - "connectionType": "cellular3g" + "latency": 0, + "downloadThroughput": -1, + "uploadThroughput": -1, } })) .await; @@ -1318,30 +1317,6 @@ async fn emulate_network_conditions_updates_browser_context_state() { let bc = ctx.conn.browser_context.as_ref().unwrap(); assert!(bc.active_page_target().network_policy.network_offline()); - assert_eq!( - bc.active_page_target() - .network_policy - .emulated_network_latency(), - 150.0 - ); - assert_eq!( - bc.active_page_target() - .network_policy - .emulated_download_throughput(), - 1024.0 - ); - assert_eq!( - bc.active_page_target() - .network_policy - .emulated_upload_throughput(), - 512.0 - ); - assert_eq!( - bc.active_page_target() - .network_policy - .emulated_connection_type(), - Some("cellular3g") - ); } #[tokio::test(flavor = "multi_thread")] async fn emulate_network_conditions_offline_navigation_fails_before_completion_events() { diff --git a/moli-protocol/src/domains/target/tests/tests_background_staging.rs b/moli-protocol/src/domains/target/tests/tests_background_staging.rs index db7004853..71b2361d5 100644 --- a/moli-protocol/src/domains/target/tests/tests_background_staging.rs +++ b/moli-protocol/src/domains/target/tests/tests_background_staging.rs @@ -1554,10 +1554,9 @@ async fn same_context_background_session_can_stage_its_own_network_conditions_be "sessionId": "SID-active", "params": { "offline": false, - "latency": 10, - "downloadThroughput": 4096, - "uploadThroughput": 2048, - "connectionType": "wifi" + "latency": 0, + "downloadThroughput": -1, + "uploadThroughput": -1, } })) .await; @@ -1590,10 +1589,9 @@ async fn same_context_background_session_can_stage_its_own_network_conditions_be "sessionId": second_session_id, "params": { "offline": true, - "latency": 25, - "downloadThroughput": 1024, - "uploadThroughput": 256, - "connectionType": "cellular3g" + "latency": 0, + "downloadThroughput": -1, + "uploadThroughput": -1, } })) .await; @@ -1607,25 +1605,12 @@ async fn same_context_background_session_can_stage_its_own_network_conditions_be .expect("active browser context"); assert_eq!(active.active_target_id(), Some("TID-000000000PN")); assert!(!active.active_page_target().network_policy.network_offline()); - assert_eq!( - active - .active_page_target() - .network_policy - .emulated_network_latency(), - 10.0 - ); + let staged = active .background_target(&second_target_id) .filter(|target| target.has_non_default_session_state()) .expect("second target should have staged background page session state"); assert!(staged.network_policy.network_offline()); - assert_eq!(staged.network_policy.emulated_network_latency(), 25.0); - assert_eq!(staged.network_policy.emulated_download_throughput(), 1024.0); - assert_eq!(staged.network_policy.emulated_upload_throughput(), 256.0); - assert_eq!( - staged.network_policy.emulated_connection_type(), - Some("cellular3g") - ); } ctx.process_async(json!({ @@ -1673,34 +1658,6 @@ async fn same_context_background_session_can_stage_its_own_network_conditions_be .network_policy .network_offline() ); - assert_eq!( - activated - .active_page_target() - .network_policy - .emulated_network_latency(), - 25.0 - ); - assert_eq!( - activated - .active_page_target() - .network_policy - .emulated_download_throughput(), - 1024.0 - ); - assert_eq!( - activated - .active_page_target() - .network_policy - .emulated_upload_throughput(), - 256.0 - ); - assert_eq!( - activated - .active_page_target() - .network_policy - .emulated_connection_type(), - Some("cellular3g") - ); } ctx.process_async(json!({ @@ -1921,10 +1878,9 @@ async fn same_context_background_session_can_reset_its_own_network_conditions_be "sessionId": second_session_id, "params": { "offline": true, - "latency": 250, - "downloadThroughput": 1024, - "uploadThroughput": 512, - "connectionType": "cellular3g" + "latency": 0, + "downloadThroughput": -1, + "uploadThroughput": -1, } })) .await; @@ -1939,7 +1895,6 @@ async fn same_context_background_session_can_reset_its_own_network_conditions_be "latency": 0, "downloadThroughput": -1, "uploadThroughput": -1, - "connectionType": "none" } })) .await; @@ -1958,15 +1913,11 @@ async fn same_context_background_session_can_reset_its_own_network_conditions_be ); let staged = active .background_target(&second_target_id) - .filter(|target| target.has_non_default_session_state()) .expect("second target should have staged background page session state"); assert!(!staged.network_policy.network_offline()); - assert_eq!(staged.network_policy.emulated_network_latency(), 0.0); - assert_eq!(staged.network_policy.emulated_download_throughput(), -1.0); - assert_eq!(staged.network_policy.emulated_upload_throughput(), -1.0); - assert_eq!( - staged.network_policy.emulated_connection_type(), - Some("none") + assert!( + !staged.has_non_default_session_state(), + "offline reset should return to the default policy" ); } @@ -2011,34 +1962,6 @@ async fn same_context_background_session_can_reset_its_own_network_conditions_be .network_policy .network_offline() ); - assert_eq!( - activated - .active_page_target() - .network_policy - .emulated_network_latency(), - 0.0 - ); - assert_eq!( - activated - .active_page_target() - .network_policy - .emulated_download_throughput(), - -1.0 - ); - assert_eq!( - activated - .active_page_target() - .network_policy - .emulated_upload_throughput(), - -1.0 - ); - assert_eq!( - activated - .active_page_target() - .network_policy - .emulated_connection_type(), - Some("none") - ); } } diff --git a/moli-protocol/src/domains/target/tests/tests_same_context_overrides.rs b/moli-protocol/src/domains/target/tests/tests_same_context_overrides.rs index 5c3d41574..d860a9738 100644 --- a/moli-protocol/src/domains/target/tests/tests_same_context_overrides.rs +++ b/moli-protocol/src/domains/target/tests/tests_same_context_overrides.rs @@ -275,10 +275,9 @@ async fn same_context_targets_restore_their_own_network_conditions_after_session "sessionId": "SID-active", "params": { "offline": false, - "latency": 150, - "downloadThroughput": 1024, - "uploadThroughput": 512, - "connectionType": "cellular3g" + "latency": 0, + "downloadThroughput": -1, + "uploadThroughput": -1, } })) .await; @@ -333,10 +332,9 @@ async fn same_context_targets_restore_their_own_network_conditions_after_session "sessionId": second_session_id, "params": { "offline": true, - "latency": 25, - "downloadThroughput": 2048, - "uploadThroughput": 256, - "connectionType": "wifi" + "latency": 0, + "downloadThroughput": -1, + "uploadThroughput": -1, } })) .await; @@ -376,19 +374,6 @@ async fn same_context_targets_restore_their_own_network_conditions_after_session .filter(|target| target.has_non_default_session_state()) .expect("second target should keep background network state"); assert!(background.network_policy.network_offline()); - assert_eq!(background.network_policy.emulated_network_latency(), 25.0); - assert_eq!( - background.network_policy.emulated_download_throughput(), - 2048.0 - ); - assert_eq!( - background.network_policy.emulated_upload_throughput(), - 256.0 - ); - assert_eq!( - background.network_policy.emulated_connection_type(), - Some("wifi") - ); } ctx.process_async(json!({ @@ -409,34 +394,6 @@ async fn same_context_targets_restore_their_own_network_conditions_after_session .expect("active browser context after restoring first target"); assert_eq!(active.active_target_id(), Some("TID-000000000NA")); assert!(!active.active_page_target().network_policy.network_offline()); - assert_eq!( - active - .active_page_target() - .network_policy - .emulated_network_latency(), - 150.0 - ); - assert_eq!( - active - .active_page_target() - .network_policy - .emulated_download_throughput(), - 1024.0 - ); - assert_eq!( - active - .active_page_target() - .network_policy - .emulated_upload_throughput(), - 512.0 - ); - assert_eq!( - active - .active_page_target() - .network_policy - .emulated_connection_type(), - Some("cellular3g") - ); } } diff --git a/moli-renderer-v8/src/devtools/command.rs b/moli-renderer-v8/src/devtools/command.rs index d8b212537..74e78cf6c 100644 --- a/moli-renderer-v8/src/devtools/command.rs +++ b/moli-renderer-v8/src/devtools/command.rs @@ -629,18 +629,6 @@ impl RendererInspectorCommandEnvelope { _ => None, } } - - pub(crate) fn uses_cpu_throttling(&self) -> bool { - matches!( - self.main_thread_payload(), - RendererInspectorPageCommand::DispatchRuntimeProtocolMessage { .. } - | RendererInspectorPageCommand::DispatchRuntimeProtocolMessageWithDeferredResponse { .. } - | RendererInspectorPageCommand::DispatchRuntimeProtocolMessageWithContextResolution { .. } - | RendererInspectorPageCommand::DispatchRuntimeProtocolMessageWithContextResolutionAndDeferredResponse { .. } - | RendererInspectorPageCommand::DomDebuggerGetEventListeners { .. } - | RendererInspectorPageCommand::FocusDocumentNodeForObjectId { .. } - ) - } } fn main_protocol_can_dispatch_at_inspector_session_boundary( diff --git a/moli-renderer-v8/src/runtime/page_commands.rs b/moli-renderer-v8/src/runtime/page_commands.rs index 46aaf3564..656be1812 100644 --- a/moli-renderer-v8/src/runtime/page_commands.rs +++ b/moli-renderer-v8/src/runtime/page_commands.rs @@ -2,18 +2,6 @@ use super::page_surface::RendererInspectorPageCommand; use super::*; impl PageVm { - pub(in crate::runtime) async fn dispatch_renderer_page_command_async( - &mut self, - command: RendererPageCommand, - ) -> Result { - let throttling_started = - renderer_page_command_uses_cpu_throttling(&command).then(std::time::Instant::now); - let result = self.dispatch_renderer_page_command(command); - self.apply_cpu_throttling_delay_after_page_command(throttling_started) - .await; - result - } - pub(in crate::runtime) fn dispatch_renderer_page_command( &mut self, command: RendererPageCommand, @@ -1125,7 +1113,10 @@ impl PageVm { Ok(RendererPageReply::Unit) } RendererPageCommand::SetCpuThrottlingRate(rate) => { - self.set_cpu_throttling_rate(rate); + if !rate.is_finite() || rate > 1.0 { + return Err(anyhow!("CPU throttling is not supported")); + } + self.set_cpu_throttling_rate(1.0); Ok(RendererPageReply::Unit) } RendererPageCommand::SetEmulatedMedia(overrides) => { @@ -1385,31 +1376,6 @@ impl PageVm { } } } - - async fn apply_cpu_throttling_delay_after_page_command( - &self, - started: Option, - ) { - let Some(started) = started else { - return; - }; - let rate = self.cpu_throttling_rate; - if !rate.is_finite() || rate <= 1.0 { - return; - } - let elapsed = started.elapsed(); - if elapsed.is_zero() { - return; - } - let delay_secs = elapsed.as_secs_f64() * (rate - 1.0); - if !delay_secs.is_finite() || delay_secs <= 0.0 { - return; - } - tokio::time::sleep(std::time::Duration::from_secs_f64( - delay_secs.min(std::time::Duration::MAX.as_secs_f64()), - )) - .await; - } } fn renderer_page_command_action_barrier( @@ -1431,33 +1397,3 @@ fn renderer_page_command_action_barrier( _ => Some(moli_action_window::ActionBarrier::Explicit), } } - -fn renderer_page_command_uses_cpu_throttling(command: &RendererPageCommand) -> bool { - if let RendererPageCommand::Inspector(envelope) = command { - return envelope.uses_cpu_throttling(); - } - matches!( - command, - RendererPageCommand::EvaluateExpression { .. } - | RendererPageCommand::EvaluateExpressionByValue { .. } - | RendererPageCommand::EvaluateExpressionAndFollowPendingNavigation { .. } - | RendererPageCommand::EvaluateExpressionInExecutionContext { .. } - | RendererPageCommand::EvaluateExpressionInExecutionContextAndFollowPendingNavigation { .. } - | RendererPageCommand::DispatchMouseEventAtPoint { .. } - | RendererPageCommand::DispatchTouchEvent { .. } - | RendererPageCommand::DispatchDragEventAtPoint { .. } - | RendererPageCommand::InsertTextIntoActiveControl(_) - | RendererPageCommand::DispatchKeyEvent { .. } - | RendererPageCommand::DomDebuggerConfigureEventListenerBreakpoint { .. } - | RendererPageCommand::DomDebuggerConfigureXhrBreakpoint { .. } - | RendererPageCommand::DomDebuggerConfigureDomBreakpoint { .. } - | RendererPageCommand::PerformanceMetricSnapshot - | RendererPageCommand::CreateIsolatedWorldRuntimeActivity { .. } - | RendererPageCommand::AddDocumentStartScriptRuntimeActivity { .. } - | RendererPageCommand::RunPageSurfaceOverrideScript { .. } - | RendererPageCommand::MutateDocumentBackendNodeAttribute { .. } - | RendererPageCommand::EditDocumentNode { .. } - | RendererPageCommand::FocusDocumentBackendNode { .. } - | RendererPageCommand::SetDocumentContent { .. } - ) -} diff --git a/moli-renderer-v8/src/runtime/page_state.rs b/moli-renderer-v8/src/runtime/page_state.rs index 036fdc9d1..288ab0aaa 100644 --- a/moli-renderer-v8/src/runtime/page_state.rs +++ b/moli-renderer-v8/src/runtime/page_state.rs @@ -381,7 +381,7 @@ impl RendererPageSlotHandle { page_id.as_u64() ); self.begin_command(page_id, command_epoch)?; - let result = vm.dispatch_renderer_page_command_async(command).await; + let result = vm.dispatch_renderer_page_command(command); self.finish_command(page_id, command_epoch)?; let reply = result?; self.update_command_epoch(page_id, command_epoch)?; diff --git a/moli-renderer-v8/src/runtime/page_vm/tests/command_checkpoint.rs b/moli-renderer-v8/src/runtime/page_vm/tests/command_checkpoint.rs index d311275ca..aae4fdea4 100644 --- a/moli-renderer-v8/src/runtime/page_vm/tests/command_checkpoint.rs +++ b/moli-renderer-v8/src/runtime/page_vm/tests/command_checkpoint.rs @@ -173,12 +173,11 @@ new MutationObserver(records => { .expect("test Page should expose its root frame") .to_owned(); - let reply = page_vm - .dispatch_renderer_page_command_async(RendererPageCommand::SetDocumentContent { + let reply = + page_vm.dispatch_renderer_page_command(RendererPageCommand::SetDocumentContent { frame_id, html: "
after
".to_owned(), - }) - .await?; + })?; assert!(matches!( reply, RendererPageReply::SetDocumentContentResult(RendererSetDocumentContentResult::Updated) @@ -233,14 +232,12 @@ Promise.resolve().then(() => __fetchCommandCheckpoint += 1); )?; page_vm.vm_mut().enqueue_test_pending_runtime_source_load(); - let reply = page_vm - .dispatch_renderer_page_command_async( - RendererPageCommand::FailPendingSubresourceFetch { - internal_id: pending[0].internal_id, - error_text: "command checkpoint witness".to_owned(), - }, - ) - .await?; + let reply = page_vm.dispatch_renderer_page_command( + RendererPageCommand::FailPendingSubresourceFetch { + internal_id: pending[0].internal_id, + error_text: "command checkpoint witness".to_owned(), + }, + )?; assert!(matches!(reply, RendererPageReply::Unit)); assert_eq!( page_vm @@ -291,9 +288,8 @@ fetch("https://offline-command-checkpoint.test/resource").then( "the fixture should pause one Window Fetch" ); - let reply = page_vm - .dispatch_renderer_page_command_async(RendererPageCommand::SetNetworkOffline(true)) - .await?; + let reply = + page_vm.dispatch_renderer_page_command(RendererPageCommand::SetNetworkOffline(true))?; assert!(matches!(reply, RendererPageReply::Unit)); page_vm .vm_mut() @@ -306,19 +302,17 @@ Promise.resolve().then(() => __offlineContinueCheckpoint += 1); )?; page_vm.vm_mut().enqueue_test_pending_runtime_source_load(); - let reply = page_vm - .dispatch_renderer_page_command_async( - RendererPageCommand::ContinuePendingSubresourceFetch { - internal_id: pending[0].internal_id, - url: None, - method: None, - body: None, - headers: None, - intercept_response: false, - handle_auth_requests: false, - }, - ) - .await?; + let reply = page_vm.dispatch_renderer_page_command( + RendererPageCommand::ContinuePendingSubresourceFetch { + internal_id: pending[0].internal_id, + url: None, + method: None, + body: None, + headers: None, + intercept_response: false, + handle_auth_requests: false, + }, + )?; assert!(matches!( reply, RendererPageReply::PendingSubresourceContinueOutcome( @@ -377,15 +371,12 @@ Promise.resolve().then(() => __rejectedFetchCommandCheckpoint += 1); "#, )?; - let error = match page_vm - .dispatch_renderer_page_command_async( - RendererPageCommand::FailPendingSubresourceFetch { - internal_id: u64::MAX, - error_text: "missing request".to_owned(), - }, - ) - .await - { + let error = match page_vm.dispatch_renderer_page_command( + RendererPageCommand::FailPendingSubresourceFetch { + internal_id: u64::MAX, + error_text: "missing request".to_owned(), + }, + ) { Ok(_) => panic!("an unknown interception request should be rejected"), Err(error) => error, }; diff --git a/moli-renderer-v8/src/runtime/page_vm/tests/lifecycle.rs b/moli-renderer-v8/src/runtime/page_vm/tests/lifecycle.rs index 50b171827..44f4a4002 100644 --- a/moli-renderer-v8/src/runtime/page_vm/tests/lifecycle.rs +++ b/moli-renderer-v8/src/runtime/page_vm/tests/lifecycle.rs @@ -1461,10 +1461,7 @@ window.addEventListener("pageshow", () => { .eval("__stopLoadingLifecycleEvents.length = 0; 'cleared'")?; let reply = page_vm - .dispatch_renderer_page_command_async( - RendererPageCommand::StopDocumentLifecycle, - ) - .await?; + .dispatch_renderer_page_command(RendererPageCommand::StopDocumentLifecycle)?; assert!(matches!(reply, RendererPageReply::Unit)); assert_eq!( page_vm.vm_mut().eval( diff --git a/moli-renderer-v8/src/runtime/page_vm/tests/mod.rs b/moli-renderer-v8/src/runtime/page_vm/tests/mod.rs index f133328fc..75d84cc71 100644 --- a/moli-renderer-v8/src/runtime/page_vm/tests/mod.rs +++ b/moli-renderer-v8/src/runtime/page_vm/tests/mod.rs @@ -14192,8 +14192,7 @@ async fn direct_page_vm_wait_commands_fail_closed() { for (command, expected_error) in commands { let mut page_vm = test_page_vm(); let error = match page_vm - .dispatch_renderer_page_command_async(command) - .await + .dispatch_renderer_page_command(command) { Ok(_) => panic!("direct wait command should fail closed"), Err(error) => error, diff --git a/moli-renderer-v8/src/runtime/tests.rs b/moli-renderer-v8/src/runtime/tests.rs index 9587e9425..c9207b1c6 100644 --- a/moli-renderer-v8/src/runtime/tests.rs +++ b/moli-renderer-v8/src/runtime/tests.rs @@ -2177,43 +2177,29 @@ document.body.appendChild(frame); } #[tokio::test(flavor = "multi_thread")] -async fn cpu_throttling_rate_slows_runtime_evaluate_command() { +async fn cpu_throttling_rate_rejects_unavailable_rates() { let runtime = JsRuntime::initialize(); - let loader = - ResourceRequestClient::new(&moli_fetch::FetchConfig::default()).expect("default loader"); + let loader = ResourceRequestClient::new(&moli_fetch::FetchConfig::default()).unwrap(); let url = url::Url::parse("https://example.test/cpu-throttling").unwrap(); let mut page = create_test_html_page(&runtime, &loader, url, "").await; - - let (reply, _) = page + let error = page .run_async_command(RendererPageCommand::SetCpuThrottlingRate(3.0)) .await - .expect("CPU throttling rate should update live page"); - assert!(matches!(reply, RendererPageReply::Unit)); - - let started = std::time::Instant::now(); - let (reply, _) = page - .run_async_command(RendererPageCommand::EvaluateExpression { - expression: r#" -(() => { - const end = Date.now() + 40; - while (Date.now() < end) {} - return true; -})() -"# - .to_owned(), - await_promise: false, - }) - .await - .expect("throttled evaluate should run"); - assert_eq!(renderer_json_value(reply), Some(serde_json::json!(true))); + .err() + .expect("unavailable throttling must fail"); assert!( - started.elapsed() >= Duration::from_millis(75), - "rate=3 should add renderer-side delay to a CPU-bound evaluate command" + error + .to_string() + .contains("CPU throttling is not supported") ); - - page.close_async() - .await - .expect("CPU throttling test page should close"); + for rate in [0.0, 1.0] { + let (reply, _) = page + .run_async_command(RendererPageCommand::SetCpuThrottlingRate(rate)) + .await + .unwrap(); + assert!(matches!(reply, RendererPageReply::Unit)); + } + page.close_async().await.unwrap(); } #[tokio::test(flavor = "current_thread")]