fix(emulation): reject unavailable throttling modes

This commit is contained in:
ldm0
2026-09-15 14:46:31 +08:00
committed by Donough Liu
parent 4bef2b6dfd
commit 73f2e785ea
21 changed files with 207 additions and 596 deletions
+5
View File
@@ -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.
+1
View File
@@ -263,6 +263,7 @@ Kitesurf 是远程服务,无法测量 CPU、内存和进程数,因此资源
- 不提供 GUI 浏览器、持久化窗口、GPU 合成器,也不实现保留式的多帧绘制架构。
- 不追求与 Chrome 像素级一致的渲染效果,也不提供高保真的 Canvas/WebGL/媒体播放能力。
- 环境模拟支持 offline 切换;CPU 节流、网络延迟/吞吐、丢包及连接类型覆盖暂不支持,请求会返回协议错误。CPU rate ≤ 1 和非正的网络延迟/吞吐值用于取消节流;不支持的组合会在改变 offline 状态前整体拒绝。
-`--layout` 模式下支持软件截图和基于光栅化的 CDP PDF 生成,但没有实现 Chrome 的全部截图/打印模式。
遇到不支持的协议路径,Moli 会直接明确报错——它不会假装某个浏览器操作、事件、网络观测或者视觉结果已经发生。
@@ -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<String>,
) -> 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<String>,
) -> Result<Option<PendingPageCommand>, 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<String>,
) -> Result<Option<PendingPageCommand>, 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<String>,
) -> Result<Option<PendingPageCommand>, 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]
+3 -4
View File
@@ -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();
+1 -57
View File
@@ -538,35 +538,16 @@ pub(crate) struct InspectorSessionState {
pub(crate) v8_state: Option<V8InspectorSessionState>,
}
#[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<String>,
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<String>,
) -> 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)]
@@ -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::<serde_json::Value>(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::<serde_json::Value>(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]
+9 -10
View File
@@ -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 {
+12 -16
View File
@@ -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));
@@ -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
);
}
+3 -12
View File
@@ -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),
)
}
+23 -29
View File
@@ -16,14 +16,6 @@ struct SetBlockedUrlsParams {
urls: Vec<String>,
}
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<String>,
}
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<Vec<String>, Com
}
#[allow(deprecated)]
pub(super) fn emulated_network_conditions_for_command(
pub(super) fn network_offline_for_emulation_command(
cmd: &Cmd<'_>,
) -> Result<EmulatedNetworkConditionsForCommand, CommandOutputPlan> {
) -> Result<bool, CommandOutputPlan> {
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::<Vec<_>>()
})
}
fn cdp_connection_type_string(
connection_type: &chromiumoxide_cdp::cdp::browser_protocol::network::ConnectionType,
) -> Option<String> {
serde_json::to_value(connection_type)
.ok()
.and_then(|value| value.as_str().map(ToOwned::to_owned))
}
@@ -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() {
@@ -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")
);
}
}
@@ -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")
);
}
}
-12
View File
@@ -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(
+4 -68
View File
@@ -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<RendererPageReply> {
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<std::time::Instant>,
) {
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 { .. }
)
}
+1 -1
View File
@@ -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)?;
@@ -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: "<main>after</main>".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,
};
@@ -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(
@@ -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,
+16 -30
View File
@@ -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, "<!doctype html>").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")]