#![expect( clippy::redundant_pub_crate, reason = "criterion bench entry points are re-exported only to the private bench root module" )] use super::support::{ BTreeMap, BenchmarkId, Criterion, InProcessRpcDispatcher, RpcMethod, RpcValue, RuntimeConfig, Throughput, rpc_request, }; #[derive(Clone, Copy, Debug)] struct PressureScenario { task_count: usize, rounds: usize, split: usize, max_connections_per_server: usize, } impl PressureScenario { fn runtime(self) -> RuntimeConfig { RuntimeConfig { split: self.split, max_connections_per_server: self.max_connections_per_server, max_connection_per_server: self.max_connections_per_server, min_split_size: 1024, piece_length: 1024, ..RuntimeConfig::default() } } } #[derive(Clone, Copy, Debug)] struct ResourceLimitScenario { task_count: usize, rounds: usize, global_download_limit: u64, per_download_limit: u64, global_upload_limit: u64, per_upload_limit: u64, disk_cache_bytes: u64, } #[derive(Clone, Copy, Debug)] struct SharedRuntimeFairnessScenario { label: &'static str, task_count: usize, rounds: usize, global_download_limit: u64, default_per_download_limit: u64, constrained_per_download_limit: u64, global_upload_limit: u64, default_per_upload_limit: u64, constrained_per_upload_limit: u64, } fn seed_pressure_dispatcher(scenario: PressureScenario) -> (InProcessRpcDispatcher, Vec) { let mut dispatcher = InProcessRpcDispatcher::with_runtime(scenario.runtime()); let mut gids = Vec::with_capacity(scenario.task_count); for i in 0..scenario.task_count { let magnet = format!( "magnet:?xt=urn:btih:{:040x}&dn=bench-{i}&tr=http%3A%2F%2Ftracker.example.org%2Fannounce", i + 40_001 ); let response = dispatcher.dispatch_json(rpc_request( RpcMethod::Aria2AddUri, vec![RpcValue::String(magnet)], )); match response.result { Some(RpcValue::String(gid)) => gids.push(gid), other => panic!("unexpected addUri result in bench setup: {other:?}"), } } for (index, gid) in gids.iter().enumerate() { dispatcher .apply_bt_runtime_tick( gid, 1024 + (index % 4) as u64 * 256, 256 + (index % 3) as u64 * 64, 300 + index as u64 % 80, 120 + index as u64 % 40, 1, 1, false, Some(4 + (index % 4) as u32), ) .expect("bench setup runtime tick should succeed"); } (dispatcher, gids) } fn seed_limited_dispatcher( scenario: ResourceLimitScenario, ) -> (InProcessRpcDispatcher, Vec) { let base = PressureScenario { task_count: scenario.task_count, rounds: scenario.rounds, split: 6, max_connections_per_server: 6, }; let (mut dispatcher, gids) = seed_pressure_dispatcher(base); let change_global = dispatcher.dispatch_json(rpc_request( RpcMethod::Aria2ChangeGlobalOption, vec![RpcValue::Object(BTreeMap::from([ ( "max-overall-download-limit".to_owned(), RpcValue::String(scenario.global_download_limit.to_string()), ), ( "max-overall-upload-limit".to_owned(), RpcValue::String(scenario.global_upload_limit.to_string()), ), ( "disk-cache".to_owned(), RpcValue::String(scenario.disk_cache_bytes.to_string()), ), ]))], )); assert!( change_global.error.is_none(), "changeGlobalOption should succeed in benchmark setup" ); for gid in &gids { let change_option = dispatcher.dispatch_json(rpc_request( RpcMethod::Aria2ChangeOption, vec![ RpcValue::String(gid.clone()), RpcValue::Object(BTreeMap::from([ ( "max-download-limit".to_owned(), RpcValue::String(scenario.per_download_limit.to_string()), ), ( "max-upload-limit".to_owned(), RpcValue::String(scenario.per_upload_limit.to_string()), ), ])), ], )); assert!( change_option.error.is_none(), "changeOption should succeed in benchmark setup" ); } (dispatcher, gids) } fn seed_shared_runtime_fairness_dispatcher( scenario: SharedRuntimeFairnessScenario, ) -> (InProcessRpcDispatcher, Vec) { let base = PressureScenario { task_count: scenario.task_count, rounds: scenario.rounds, split: 6, max_connections_per_server: 6, }; let (mut dispatcher, gids) = seed_pressure_dispatcher(base); let change_global = dispatcher.dispatch_json(rpc_request( RpcMethod::Aria2ChangeGlobalOption, vec![RpcValue::Object(BTreeMap::from([ ( "max-overall-download-limit".to_owned(), RpcValue::String(scenario.global_download_limit.to_string()), ), ( "max-overall-upload-limit".to_owned(), RpcValue::String(scenario.global_upload_limit.to_string()), ), ]))], )); assert!( change_global.error.is_none(), "changeGlobalOption should succeed in shared-runtime fairness setup" ); for (index, gid) in gids.iter().enumerate() { let (download_limit, upload_limit) = if index == 0 { ( scenario.constrained_per_download_limit, scenario.constrained_per_upload_limit, ) } else { ( scenario.default_per_download_limit, scenario.default_per_upload_limit, ) }; let change_option = dispatcher.dispatch_json(rpc_request( RpcMethod::Aria2ChangeOption, vec![ RpcValue::String(gid.clone()), RpcValue::Object(BTreeMap::from([ ( "max-download-limit".to_owned(), RpcValue::String(download_limit.to_string()), ), ( "max-upload-limit".to_owned(), RpcValue::String(upload_limit.to_string()), ), ])), ], )); assert!( change_option.error.is_none(), "changeOption should succeed in shared-runtime fairness setup" ); } (dispatcher, gids) } fn run_tell_status_pressure( dispatcher: &mut InProcessRpcDispatcher, gids: &[String], rounds: usize, ) -> usize { let mut calls = 0usize; for round in 0..rounds { let gid = &gids[round % gids.len()]; dispatcher .apply_bt_runtime_tick( gid, 0, 0, 400 + round as u64 * 10, 160 + round as u64 * 5, 0, 0, false, Some(8), ) .expect("bench tellStatus churn should succeed"); for gid in gids { let response = dispatcher.dispatch_json(rpc_request( RpcMethod::Aria2TellStatus, vec![RpcValue::String(gid.clone())], )); match response.result { Some(RpcValue::Object(payload)) => { assert_eq!(payload.get("isBt"), Some(&RpcValue::Bool(true))); calls += 1; } other => panic!("unexpected tellStatus payload in bench: {other:?}"), } } } calls } fn run_resource_limit_pressure( dispatcher: &mut InProcessRpcDispatcher, gids: &[String], scenario: ResourceLimitScenario, ) -> usize { let mut calls = 0usize; let expected_download_speed = (scenario.global_download_limit / scenario.task_count as u64) .min(scenario.per_download_limit) .max(1); let expected_upload_speed = (scenario.global_upload_limit / scenario.task_count as u64) .min(scenario.per_upload_limit) .max(1); for round in 0..scenario.rounds { for gid in gids { dispatcher .apply_bt_runtime_tick(gid, 64, 32, 5_000, 2_000, 1, 1, false, Some(6)) .expect("bench limited runtime tick should succeed"); } let gid = &gids[round % gids.len()]; let tell_status = dispatcher.dispatch_json(rpc_request( RpcMethod::Aria2TellStatus, vec![RpcValue::String(gid.clone())], )); match tell_status.result { Some(RpcValue::Object(payload)) => { assert_eq!( payload.get("downloadSpeed"), Some(&RpcValue::String(expected_download_speed.to_string())) ); assert_eq!( payload.get("uploadSpeed"), Some(&RpcValue::String(expected_upload_speed.to_string())) ); calls += 1; } other => panic!("unexpected tellStatus payload under resource caps: {other:?}"), } let tell_global = dispatcher.dispatch_json(rpc_request(RpcMethod::Aria2TellGlobalStat, Vec::new())); match tell_global.result { Some(RpcValue::Object(payload)) => { assert_eq!( payload.get("downloadSpeed"), Some(&RpcValue::String( expected_download_speed .saturating_mul(scenario.task_count as u64) .to_string(), )) ); assert_eq!( payload.get("uploadSpeed"), Some(&RpcValue::String( expected_upload_speed .saturating_mul(scenario.task_count as u64) .to_string(), )) ); calls += 1; } other => panic!("unexpected tellGlobalStat payload under resource caps: {other:?}"), } } calls } fn run_mixed_rpc_pressure( dispatcher: &mut InProcessRpcDispatcher, gids: &[String], rounds: usize, ) -> usize { let mut calls = 0usize; for round in 0..rounds { let gid = &gids[round % gids.len()]; dispatcher .apply_bt_runtime_tick( gid, 128, 64, 500 + round as u64 * 15, 180 + round as u64 * 6, 1, 1, round + 1 >= rounds, Some(6 + (round % 4) as u32), ) .expect("bench mixed churn should succeed"); let tell_status = dispatcher.dispatch_json(rpc_request( RpcMethod::Aria2TellStatus, vec![RpcValue::String(gid.clone())], )); assert!(matches!(tell_status.result, Some(RpcValue::Object(_)))); calls += 1; let tell_active = dispatcher.dispatch_json(rpc_request(RpcMethod::Aria2TellActive, Vec::new())); assert!(matches!(tell_active.result, Some(RpcValue::Array(_)))); calls += 1; let tell_global = dispatcher.dispatch_json(rpc_request(RpcMethod::Aria2TellGlobalStat, Vec::new())); assert!(matches!(tell_global.result, Some(RpcValue::Object(_)))); calls += 1; } calls } fn run_shared_runtime_fairness_pressure( dispatcher: &mut InProcessRpcDispatcher, gids: &[String], scenario: SharedRuntimeFairnessScenario, ) -> usize { let mut calls = 0usize; let initial_shared_download_speed = (scenario.global_download_limit / scenario.task_count as u64).max(1); let initial_shared_upload_speed = (scenario.global_upload_limit / scenario.task_count as u64).max(1); let constrained_initial_download_speed = initial_shared_download_speed .min(scenario.constrained_per_download_limit) .max(1); let constrained_initial_upload_speed = initial_shared_upload_speed .min(scenario.constrained_per_upload_limit) .max(1); let rebalanced_active_count = scenario.task_count.saturating_sub(1).max(1); let rebalanced_download_speed = (scenario.global_download_limit / rebalanced_active_count as u64) .min(scenario.default_per_download_limit) .max(1); let rebalanced_upload_speed = (scenario.global_upload_limit / rebalanced_active_count as u64) .min(scenario.default_per_upload_limit) .max(1); for round in 0..scenario.rounds { let active_slice = if round == 0 { gids } else { &gids[1..] }; for gid in active_slice { dispatcher .apply_bt_runtime_tick(gid, 128, 64, 5_000, 2_000, 0, 0, false, Some(6)) .expect("shared-runtime fairness tick should succeed"); } for (index, gid) in gids.iter().enumerate() { let tell_status = dispatcher.dispatch_json(rpc_request( RpcMethod::Aria2TellStatus, vec![RpcValue::String(gid.clone())], )); match tell_status.result { Some(RpcValue::Object(payload)) => { if round > 0 && index == 0 { assert_eq!( payload.get("status"), Some(&RpcValue::String("complete".to_owned())) ); } else { let expected_download_speed = if index == 0 { constrained_initial_download_speed } else if round == 0 { initial_shared_download_speed .min(scenario.default_per_download_limit) .max(1) } else { rebalanced_download_speed }; let expected_upload_speed = if index == 0 { constrained_initial_upload_speed } else if round == 0 { initial_shared_upload_speed .min(scenario.default_per_upload_limit) .max(1) } else { rebalanced_upload_speed }; assert_eq!( payload.get("downloadSpeed"), Some(&RpcValue::String(expected_download_speed.to_string())) ); assert_eq!( payload.get("uploadSpeed"), Some(&RpcValue::String(expected_upload_speed.to_string())) ); } calls += 1; } other => panic!( "unexpected tellStatus payload in shared-runtime fairness bench: {other:?}" ), } } let tell_global = dispatcher.dispatch_json(rpc_request(RpcMethod::Aria2TellGlobalStat, Vec::new())); match tell_global.result { Some(RpcValue::Object(payload)) => { let (expected_download_speed, expected_upload_speed) = if round == 0 { ( constrained_initial_download_speed.saturating_add( initial_shared_download_speed .min(scenario.default_per_download_limit) .max(1) .saturating_mul(rebalanced_active_count as u64), ), constrained_initial_upload_speed.saturating_add( initial_shared_upload_speed .min(scenario.default_per_upload_limit) .max(1) .saturating_mul(rebalanced_active_count as u64), ), ) } else { ( constrained_initial_download_speed .saturating_add( rebalanced_download_speed .saturating_mul(rebalanced_active_count as u64), ) .min(scenario.global_download_limit), constrained_initial_upload_speed .saturating_add( rebalanced_upload_speed .saturating_mul(rebalanced_active_count as u64), ) .min(scenario.global_upload_limit), ) }; assert_eq!( payload.get("downloadSpeed"), Some(&RpcValue::String(expected_download_speed.to_string())) ); assert_eq!( payload.get("uploadSpeed"), Some(&RpcValue::String(expected_upload_speed.to_string())) ); calls += 1; } other => panic!( "unexpected tellGlobalStat payload in shared-runtime fairness bench: {other:?}" ), } if round == 0 { dispatcher .mark_complete(&gids[0]) .expect("shared-runtime fairness benchmark should complete one gid"); } } calls } pub(super) fn bench_tell_status_pressure(c: &mut Criterion) { let mut group = c.benchmark_group("rpc_tell_status_pressure"); for scenario in [ PressureScenario { task_count: 64, rounds: 4, split: 4, max_connections_per_server: 4, }, PressureScenario { task_count: 128, rounds: 4, split: 8, max_connections_per_server: 8, }, ] { group.throughput(Throughput::Elements( (scenario.task_count * scenario.rounds) as u64, )); group.bench_with_input( BenchmarkId::new("tell_status", scenario.task_count), &scenario, |b, &scenario| { b.iter_batched( || seed_pressure_dispatcher(scenario), |(mut dispatcher, gids)| { let calls = run_tell_status_pressure(&mut dispatcher, &gids, scenario.rounds); assert_eq!(calls, scenario.task_count * scenario.rounds); }, criterion::BatchSize::SmallInput, ); }, ); } group.finish(); } pub(super) fn bench_mixed_rpc_pressure(c: &mut Criterion) { let mut group = c.benchmark_group("rpc_mixed_pressure"); for scenario in [ PressureScenario { task_count: 96, rounds: 6, split: 6, max_connections_per_server: 6, }, PressureScenario { task_count: 192, rounds: 6, split: 8, max_connections_per_server: 8, }, ] { group.throughput(Throughput::Elements((scenario.rounds * 3) as u64)); group.bench_with_input( BenchmarkId::new("mixed_rpc", scenario.task_count), &scenario, |b, &scenario| { b.iter_batched( || seed_pressure_dispatcher(scenario), |(mut dispatcher, gids)| { let calls = run_mixed_rpc_pressure(&mut dispatcher, &gids, scenario.rounds); assert_eq!(calls, scenario.rounds * 3); }, criterion::BatchSize::SmallInput, ); }, ); } group.finish(); } pub(super) fn bench_resource_limit_pressure(c: &mut Criterion) { let mut group = c.benchmark_group("rpc_speed_limit_pressure"); for scenario in [ ResourceLimitScenario { task_count: 32, rounds: 6, global_download_limit: 2_560, per_download_limit: 160, global_upload_limit: 1_280, per_upload_limit: 80, disk_cache_bytes: 8 * 1024 * 1024, }, ResourceLimitScenario { task_count: 64, rounds: 6, global_download_limit: 5_120, per_download_limit: 120, global_upload_limit: 2_560, per_upload_limit: 60, disk_cache_bytes: 16 * 1024 * 1024, }, ] { group.throughput(Throughput::Elements((scenario.rounds * 2) as u64)); group.bench_with_input( BenchmarkId::new("speed_limit", scenario.task_count), &scenario, |b, &scenario| { b.iter_batched( || seed_limited_dispatcher(scenario), |(mut dispatcher, gids)| { let calls = run_resource_limit_pressure(&mut dispatcher, &gids, scenario); assert_eq!(calls, scenario.rounds * 2); }, criterion::BatchSize::SmallInput, ); }, ); } group.finish(); } pub(super) fn bench_shared_runtime_fairness_pressure(c: &mut Criterion) { let mut group = c.benchmark_group("rpc_shared_runtime_fairness_pressure"); for scenario in [ SharedRuntimeFairnessScenario { label: "three_way_rebalance", task_count: 3, rounds: 4, global_download_limit: 1_200, default_per_download_limit: 900, constrained_per_download_limit: 250, global_upload_limit: 600, default_per_upload_limit: 500, constrained_per_upload_limit: 120, }, SharedRuntimeFairnessScenario { label: "four_way_rebalance", task_count: 4, rounds: 4, global_download_limit: 1_600, default_per_download_limit: 900, constrained_per_download_limit: 220, global_upload_limit: 800, default_per_upload_limit: 500, constrained_per_upload_limit: 100, }, ] { group.throughput(Throughput::Elements( (scenario.task_count * scenario.rounds) as u64, )); group.bench_with_input( BenchmarkId::new("shared_runtime_fairness", scenario.label), &scenario, |b, &scenario| { b.iter_batched( || seed_shared_runtime_fairness_dispatcher(scenario), |(mut dispatcher, gids)| { let calls = run_shared_runtime_fairness_pressure(&mut dispatcher, &gids, scenario); assert_eq!(calls, scenario.rounds * (scenario.task_count + 1)); }, criterion::BatchSize::SmallInput, ); }, ); } group.finish(); }