diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml new file mode 100644 index 0000000..d6447b1 --- /dev/null +++ b/.github/workflows/rust.yml @@ -0,0 +1,33 @@ +name: Rust + +on: + [push, pull_request] + +env: + CARGO_TERM_COLOR: always + +jobs: + build: + strategy: + matrix: + host_os: + - ubuntu-latest + - macos-latest + # - windows-latest + + runs-on: ${{ matrix.host_os }} + + steps: + - uses: actions/checkout@v3 + - name: check + run: cargo check + - name: rustfmt + run: | + rustc --version + cargo fmt --all -- --check + - name: clippy + run: cargo clippy -- -D warnings + - name: Build + run: cargo build --verbose --all-features --tests --examples + - name: Run tests + run: cargo test --verbose diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..269985d --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +.vscode/ +.VSCodeCounter/ +build/ +tmp/ +Cargo.lock +target/ diff --git a/src/client.rs b/src/client.rs index 797c540..6afee76 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1,91 +1,112 @@ /* * Copyright (C) 2021 Evtech Solutions, Ltd., dba 3D-P * Copyright (C) 2021 Neil Tallim - * + * * This file is part of rperf. - * + * * rperf is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. - * + * * rperf is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. - * + * * You should have received a copy of the GNU General Public License * along with rperf. If not, see . */ use std::net::{IpAddr, Shutdown, ToSocketAddrs}; use std::sync::atomic::{AtomicBool, Ordering}; -use std::sync::{Arc, Mutex}; use std::sync::mpsc::channel; +use std::sync::{Arc, Mutex}; use std::thread; use std::time::{Duration, SystemTime, UNIX_EPOCH}; use clap::ArgMatches; -use mio::net::{TcpStream}; +use mio::net::TcpStream; use crate::protocol::communication::{receive, send, KEEPALIVE_DURATION}; use crate::protocol::messaging::{ - prepare_begin, prepare_end, - prepare_upload_configuration, prepare_download_configuration, + prepare_begin, prepare_download_configuration, prepare_end, prepare_upload_configuration, }; -use crate::protocol::results::{IntervalResult, IntervalResultKind, TestResults, TcpTestResults, UdpTestResults}; +use crate::protocol::results::{ + IntervalResultBox, IntervalResultKind, TcpTestResults, TestResults, UdpTestResults, +}; -use crate::stream::TestStream; use crate::stream::tcp; use crate::stream::udp; +use crate::stream::TestStream; use std::error::Error; -type BoxResult = Result>; +type BoxResult = Result>; /// when false, the system is shutting down -static ALIVE:AtomicBool = AtomicBool::new(true); +static ALIVE: AtomicBool = AtomicBool::new(true); /// a deferred kill-switch to handle shutdowns a bit more gracefully in the event of a probable disconnect -static mut KILL_TIMER_RELATIVE_START_TIME:f64 = 0.0; //the time at which the kill-timer was started -const KILL_TIMEOUT:f64 = 5.0; //once testing finishes, allow a few seconds for the server to respond +static mut KILL_TIMER_RELATIVE_START_TIME: f64 = 0.0; //the time at which the kill-timer was started +const KILL_TIMEOUT: f64 = 5.0; //once testing finishes, allow a few seconds for the server to respond -const CONNECT_TIMEOUT:Duration = Duration::from_secs(2); +const CONNECT_TIMEOUT: Duration = Duration::from_secs(2); -fn connect_to_server(address:&str, port:&u16) -> BoxResult { +fn connect_to_server(address: &str, port: &u16) -> BoxResult { let destination = format!("{}:{}", address, port); log::info!("connecting to server at {}...", destination); - + let server_addr = destination.to_socket_addrs()?.next(); if server_addr.is_none() { - return Err(Box::new(simple_error::simple_error!("unable to resolve {}", address))); + return Err(Box::new(simple_error::simple_error!( + "unable to resolve {}", + address + ))); } - let raw_stream = match std::net::TcpStream::connect_timeout(&server_addr.unwrap(), CONNECT_TIMEOUT) { - Ok(s) => s, - Err(e) => return Err(Box::new(simple_error::simple_error!("unable to connect: {}", e))), - }; + let raw_stream = + match std::net::TcpStream::connect_timeout(&server_addr.unwrap(), CONNECT_TIMEOUT) { + Ok(s) => s, + Err(e) => { + return Err(Box::new(simple_error::simple_error!( + "unable to connect: {}", + e + ))) + } + }; let stream = match TcpStream::from_stream(raw_stream) { Ok(s) => s, - Err(e) => return Err(Box::new(simple_error::simple_error!("unable to prepare TCP control-channel: {}", e))), + Err(e) => { + return Err(Box::new(simple_error::simple_error!( + "unable to prepare TCP control-channel: {}", + e + ))) + } }; log::info!("connected to server"); - - stream.set_nodelay(true).expect("cannot disable Nagle's algorithm"); - stream.set_keepalive(Some(KEEPALIVE_DURATION)).expect("unable to set TCP keepalive"); - + + stream + .set_nodelay(true) + .expect("cannot disable Nagle's algorithm"); + stream + .set_keepalive(Some(KEEPALIVE_DURATION)) + .expect("unable to set TCP keepalive"); + Ok(stream) } -fn prepare_test_results(is_udp:bool, stream_count:u8) -> Mutex> { - if is_udp { //UDP +fn prepare_test_results(is_udp: bool, stream_count: u8) -> Mutex> { + if is_udp { + //UDP let mut udp_test_results = UdpTestResults::new(); for i in 0..stream_count { udp_test_results.prepare_index(&i); } Mutex::new(Box::new(udp_test_results)) - } else { //TCP + } else { + //TCP let mut tcp_test_results = TcpTestResults::new(); for i in 0..stream_count { tcp_test_results.prepare_index(&i); @@ -94,10 +115,9 @@ fn prepare_test_results(is_udp:bool, stream_count:u8) -> Mutex BoxResult<()> { +pub fn execute(args: ArgMatches) -> BoxResult<()> { let mut complete = false; - + //config-parsing and pre-connection setup let mut tcp_port_pool = tcp::receiver::TcpPortPool::new( args.value_of("tcp_port_pool").unwrap().to_string(), @@ -107,107 +127,122 @@ pub fn execute(args:ArgMatches) -> BoxResult<()> { args.value_of("udp_port_pool").unwrap().to_string(), args.value_of("udp6_port_pool").unwrap().to_string(), ); - - let cpu_affinity_manager = Arc::new(Mutex::new(crate::utils::cpu_affinity::CpuAffinityManager::new(args.value_of("affinity").unwrap())?)); - - let display_json:bool; - let display_bit:bool; + + let cpu_affinity_manager = Arc::new(Mutex::new( + crate::utils::cpu_affinity::CpuAffinityManager::new(args.value_of("affinity").unwrap())?, + )); + + let display_json: bool; + let display_bit: bool; match args.value_of("format").unwrap() { "json" => { display_json = true; display_bit = false; - }, + } "megabit" => { display_json = false; display_bit = true; - }, + } "megabyte" => { display_json = false; display_bit = false; - }, + } _ => { log::error!("unsupported display-mode; defaulting to JSON"); display_json = true; display_bit = false; - }, + } } - + let is_udp = args.is_present("udp"); - + let test_id = uuid::Uuid::new_v4(); let mut upload_config = prepare_upload_configuration(&args, test_id.as_bytes())?; let mut download_config = prepare_download_configuration(&args, test_id.as_bytes())?; - - + //connect to the server - let mut stream = connect_to_server(&args.value_of("client").unwrap(), &(args.value_of("port").unwrap().parse()?))?; + let mut stream = connect_to_server( + args.value_of("client").unwrap(), + &(args.value_of("port").unwrap().parse()?), + )?; let server_addr = stream.peer_addr()?; - - + //scaffolding to track and relay the streams and stream-results associated with this test let stream_count = download_config.get("streams").unwrap().as_i64().unwrap() as usize; - let mut parallel_streams:Vec>> = Vec::with_capacity(stream_count); + let mut parallel_streams: Vec>> = + Vec::with_capacity(stream_count); let mut parallel_streams_joinhandles = Vec::with_capacity(stream_count); - let (results_tx, results_rx):(std::sync::mpsc::Sender>, std::sync::mpsc::Receiver>) = channel(); - - let test_results:Mutex> = prepare_test_results(is_udp, stream_count as u8); - + let (results_tx, results_rx): ( + std::sync::mpsc::Sender, + std::sync::mpsc::Receiver, + ) = channel(); + + let test_results: Mutex> = + prepare_test_results(is_udp, stream_count as u8); + //a closure used to pass results from stream-handlers to the test-result structure let mut results_handler = || -> BoxResult<()> { - loop { //drain all results every time this closer is invoked - match results_rx.try_recv() { //see if there's a result to pass on - Ok(result) => { - if !display_json { //since this runs in the main thread, which isn't involved in any testing, render things immediately - println!("{}", result.to_string(display_bit)); + //drain all results every time this closer is invoked + while let Ok(result) = results_rx.try_recv() { + //see if there's a result to pass on + if !display_json { + //since this runs in the main thread, which isn't involved in any testing, render things immediately + println!("{}", result.to_string(display_bit)); + } + + //update the test-results accordingly + let mut tr = test_results.lock().unwrap(); + match result.kind() { + IntervalResultKind::ClientDone | IntervalResultKind::ClientFailed => { + if result.kind() == IntervalResultKind::ClientDone { + log::info!("stream {} is done", result.get_stream_idx()); + } else { + log::warn!("stream {} failed", result.get_stream_idx()); } - - //update the test-results accordingly - let mut tr = test_results.lock().unwrap(); - match result.kind() { - IntervalResultKind::ClientDone | IntervalResultKind::ClientFailed => { - if result.kind() == IntervalResultKind::ClientDone { - log::info!("stream {} is done", result.get_stream_idx()); - } else { - log::warn!("stream {} failed", result.get_stream_idx()); - } - tr.mark_stream_done(&result.get_stream_idx(), result.kind() == IntervalResultKind::ClientDone); - if tr.count_in_progress_streams() == 0 { - complete = true; - - if tr.count_in_progress_streams_server() > 0 { - log::info!("giving the server a few seconds to report results..."); - start_kill_timer(); - } else { //all data gathered from both sides - kill(); - } - } - }, - _ => { - tr.update_from_json(result.to_json())?; + tr.mark_stream_done( + &result.get_stream_idx(), + result.kind() == IntervalResultKind::ClientDone, + ); + if tr.count_in_progress_streams() == 0 { + complete = true; + + if tr.count_in_progress_streams_server() > 0 { + log::info!("giving the server a few seconds to report results..."); + start_kill_timer(); + } else { + //all data gathered from both sides + kill(); } } - }, - Err(_) => break, //whether it's empty or disconnected, there's nothing to do + } + _ => { + tr.update_from_json(result.to_json())?; + } } } Ok(()) }; - + //depending on whether this is a forward- or reverse-test, the order of configuring test-streams will differ if args.is_present("reverse") { log::debug!("running in reverse-mode: server will be uploading data"); - + //when we're receiving data, we're also responsible for letting the server know where to send it let mut stream_ports = Vec::with_capacity(stream_count); - - if is_udp { //UDP - log::info!("preparing for reverse-UDP test with {} streams...", stream_count); - + + if is_udp { + //UDP + log::info!( + "preparing for reverse-UDP test with {} streams...", + stream_count + ); + let test_definition = udp::UdpTestDefinition::new(&download_config)?; for stream_idx in 0..stream_count { log::debug!("preparing UDP-receiver for stream {}...", stream_idx); let test = udp::receiver::UdpReceiver::new( - test_definition.clone(), &(stream_idx as u8), + test_definition.clone(), + &(stream_idx as u8), &mut udp_port_pool, &server_addr.ip(), &(download_config["receive_buffer"].as_i64().unwrap() as usize), @@ -215,67 +250,93 @@ pub fn execute(args:ArgMatches) -> BoxResult<()> { stream_ports.push(test.get_port()?); parallel_streams.push(Arc::new(Mutex::new(test))); } - } else { //TCP - log::info!("preparing for reverse-TCP test with {} streams...", stream_count); - + } else { + //TCP + log::info!( + "preparing for reverse-TCP test with {} streams...", + stream_count + ); + let test_definition = tcp::TcpTestDefinition::new(&download_config)?; for stream_idx in 0..stream_count { log::debug!("preparing TCP-receiver for stream {}...", stream_idx); let test = tcp::receiver::TcpReceiver::new( - test_definition.clone(), &(stream_idx as u8), + test_definition.clone(), + &(stream_idx as u8), &mut tcp_port_pool, &server_addr.ip(), &(download_config["receive_buffer"].as_i64().unwrap() as usize), )?; stream_ports.push(test.get_port()?); parallel_streams.push(Arc::new(Mutex::new(test))); - } + } } - + //add the port-list to the upload-config that the server will receive; this is in stream-index order upload_config["stream_ports"] = serde_json::json!(stream_ports); - + //let the server know what we're expecting send(&mut stream, &upload_config)?; } else { log::debug!("running in forward-mode: server will be receiving data"); - + //let the server know to prepare for us to connect send(&mut stream, &download_config)?; //NOTE: we don't prepare to send data at this point; that happens in the loop below, after the server signals that it's ready } - - + //now that the server knows what we need to do, we have to wait for its response let connection_payload = receive(&mut stream, is_alive, &mut results_handler)?; match connection_payload.get("kind") { Some(kind) => { match kind.as_str().unwrap_or_default() { - "connect" => { //we need to connect to the server - if is_udp { //UDP + "connect" => { + //we need to connect to the server + if is_udp { + //UDP log::info!("preparing for UDP test with {} streams...", stream_count); - + let test_definition = udp::UdpTestDefinition::new(&upload_config)?; - for (stream_idx, port) in connection_payload.get("stream_ports").unwrap().as_array().unwrap().iter().enumerate() { + for (stream_idx, port) in connection_payload + .get("stream_ports") + .unwrap() + .as_array() + .unwrap() + .iter() + .enumerate() + { log::debug!("preparing UDP-sender for stream {}...", stream_idx); let test = udp::sender::UdpSender::new( - test_definition.clone(), &(stream_idx as u8), - &0, &server_addr.ip(), &(port.as_i64().unwrap() as u16), + test_definition.clone(), + &(stream_idx as u8), + &0, + &server_addr.ip(), + &(port.as_i64().unwrap() as u16), &(upload_config["duration"].as_f64().unwrap() as f32), &(upload_config["send_interval"].as_f64().unwrap() as f32), &(upload_config["send_buffer"].as_i64().unwrap() as usize), )?; parallel_streams.push(Arc::new(Mutex::new(test))); } - } else { //TCP + } else { + //TCP log::info!("preparing for TCP test with {} streams...", stream_count); - + let test_definition = tcp::TcpTestDefinition::new(&upload_config)?; - for (stream_idx, port) in connection_payload.get("stream_ports").unwrap().as_array().unwrap().iter().enumerate() { + for (stream_idx, port) in connection_payload + .get("stream_ports") + .unwrap() + .as_array() + .unwrap() + .iter() + .enumerate() + { log::debug!("preparing TCP-sender for stream {}...", stream_idx); let test = tcp::sender::TcpSender::new( - test_definition.clone(), &(stream_idx as u8), - &server_addr.ip(), &(port.as_i64().unwrap() as u16), + test_definition.clone(), + &(stream_idx as u8), + &server_addr.ip(), + &(port.as_i64().unwrap() as u16), &(upload_config["duration"].as_f64().unwrap() as f32), &(upload_config["send_interval"].as_f64().unwrap() as f32), &(upload_config["send_buffer"].as_i64().unwrap() as usize), @@ -284,37 +345,46 @@ pub fn execute(args:ArgMatches) -> BoxResult<()> { parallel_streams.push(Arc::new(Mutex::new(test))); } } - }, + } "connect-ready" => { //server is ready to connect to us - //nothing more to do in this flow - }, + //nothing more to do in this flow + } _ => { - log::error!("invalid data from {}: {}", stream.peer_addr()?, serde_json::to_string(&connection_payload)?); + log::error!( + "invalid data from {}: {}", + stream.peer_addr()?, + serde_json::to_string(&connection_payload)? + ); kill(); - }, + } } - }, + } None => { - log::error!("invalid data from {}: {}", stream.peer_addr()?, serde_json::to_string(&connection_payload)?); + log::error!( + "invalid data from {}: {}", + stream.peer_addr()?, + serde_json::to_string(&connection_payload)? + ); kill(); - }, + } } - - - if is_alive() { //if interrupted while waiting for the server to respond, there's no reason to continue + + if is_alive() { + //if interrupted while waiting for the server to respond, there's no reason to continue log::info!("informing server that testing can begin..."); //tell the server to start send(&mut stream, &prepare_begin())?; - + log::debug!("spawning stream-threads"); //begin the test-streams for (stream_idx, parallel_stream) in parallel_streams.iter_mut().enumerate() { log::info!("beginning execution of stream {}...", stream_idx); - let c_ps = Arc::clone(¶llel_stream); + let c_ps = Arc::clone(parallel_stream); let c_results_tx = results_tx.clone(); let c_cam = cpu_affinity_manager.clone(); let handle = thread::spawn(move || { - { //set CPU affinity, if enabled + { + //set CPU affinity, if enabled c_cam.lock().unwrap().set_affinity(); } loop { @@ -326,31 +396,44 @@ pub fn execute(args:ArgMatches) -> BoxResult<()> { Ok(_) => (), Err(e) => { log::error!("unable to report interval-result: {}", e); - break - }, + break; + } }, Err(e) => { log::error!("unable to process stream: {}", e); - match c_results_tx.send(Box::new(crate::protocol::results::ClientFailedResult{stream_idx: test.get_idx()})) { + match c_results_tx.send(Box::new( + crate::protocol::results::ClientFailedResult { + stream_idx: test.get_idx(), + }, + )) { Ok(_) => (), - Err(e) => log::error!("unable to report interval-failed-result: {}", e), + Err(e) => log::error!( + "unable to report interval-failed-result: {}", + e + ), } break; - }, + } }, None => { - match c_results_tx.send(Box::new(crate::protocol::results::ClientDoneResult{stream_idx: test.get_idx()})) { + match c_results_tx.send(Box::new( + crate::protocol::results::ClientDoneResult { + stream_idx: test.get_idx(), + }, + )) { Ok(_) => (), - Err(e) => log::error!("unable to report interval-done-result: {}", e), + Err(e) => { + log::error!("unable to report interval-done-result: {}", e) + } } break; - }, + } } } }); parallel_streams_joinhandles.push(handle); } - + //watch for events from the server while is_alive() { match receive(&mut stream, is_alive, &mut results_handler) { @@ -381,7 +464,6 @@ pub fn execute(args:ArgMatches) -> BoxResult<()> { _ => (), //not possible } tr.mark_stream_done_server(&(idx64 as u8)); - if tr.count_in_progress_streams() == 0 && tr.count_in_progress_streams_server() == 0 { //all data gathered from both sides kill(); } @@ -395,36 +477,43 @@ pub fn execute(args:ArgMatches) -> BoxResult<()> { break; }, } - }, + } None => { - log::error!("invalid data from {}: {}", stream.peer_addr()?, serde_json::to_string(&connection_payload)?); + log::error!( + "invalid data from {}: {}", + stream.peer_addr()?, + serde_json::to_string(&connection_payload)? + ); break; - }, + } } - }, + } Err(e) => { - if !complete { //when complete, this also occurs + if !complete { + //when complete, this also occurs return Err(e); } break; - }, + } } } } - + //assume this is a controlled shutdown; if it isn't, this is just a very slight waste of time send(&mut stream, &prepare_end()).unwrap_or_default(); thread::sleep(Duration::from_millis(250)); //wait a moment for the "end" message to be queued for delivery to the server stream.shutdown(Shutdown::Both).unwrap_or_default(); - + log::debug!("stopping any still-in-progress streams"); for ps in parallel_streams.iter_mut() { let mut stream = match (*ps).lock() { Ok(guard) => guard, Err(poisoned) => { - log::error!("a stream-handler was poisoned; this indicates some sort of logic error"); + log::error!( + "a stream-handler was poisoned; this indicates some sort of logic error" + ); poisoned.into_inner() - }, + } }; stream.stop(); } @@ -435,8 +524,8 @@ pub fn execute(args:ArgMatches) -> BoxResult<()> { Err(e) => log::error!("error in parallel stream: {:?}", e), } } - - let common_config:serde_json::Value; + + let common_config: serde_json::Value; //sanitise the config structures for export { let upload_config_map = upload_config.as_object_mut().unwrap(); @@ -450,7 +539,7 @@ pub fn execute(args:ArgMatches) -> BoxResult<()> { if upload_config_map["send_buffer"].as_i64().unwrap() == 0 { upload_config_map.remove("send_buffer"); } - + let download_config_map = download_config.as_object_mut().unwrap(); download_config_map.remove("family"); download_config_map.remove("kind"); @@ -461,32 +550,41 @@ pub fn execute(args:ArgMatches) -> BoxResult<()> { if download_config_map["receive_buffer"].as_i64().unwrap() == 0 { download_config_map.remove("receive_buffer"); } - + common_config = serde_json::json!({ "family": cc_family, "length": cc_length, "streams": cc_streams, }); } - + log::debug!("displaying test results"); - let omit_seconds:usize = args.value_of("omit").unwrap().parse()?; + let omit_seconds: usize = args.value_of("omit").unwrap().parse()?; { let tr = test_results.lock().unwrap(); if display_json { - println!("{}", tr.to_json_string(omit_seconds, upload_config, download_config, common_config, serde_json::json!({ - "omit_seconds": omit_seconds, - "ip_version": match server_addr.ip() { - IpAddr::V4(_) => 4, - IpAddr::V6(_) => 6, - }, - "reverse": args.is_present("reverse"), - }))); + println!( + "{}", + tr.to_json_string( + omit_seconds, + upload_config, + download_config, + common_config, + serde_json::json!({ + "omit_seconds": omit_seconds, + "ip_version": match server_addr.ip() { + IpAddr::V4(_) => 4, + IpAddr::V6(_) => 6, + }, + "reverse": args.is_present("reverse"), + }) + ) + ); } else { println!("{}", tr.to_string(display_bit, omit_seconds)); } } - + Ok(()) } @@ -495,13 +593,23 @@ pub fn kill() -> bool { } fn start_kill_timer() { unsafe { - KILL_TIMER_RELATIVE_START_TIME = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs_f64(); + KILL_TIMER_RELATIVE_START_TIME = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_secs_f64(); } } fn is_alive() -> bool { unsafe { - if KILL_TIMER_RELATIVE_START_TIME != 0.0 { //initialised - if SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs_f64() - KILL_TIMER_RELATIVE_START_TIME >= KILL_TIMEOUT { + if KILL_TIMER_RELATIVE_START_TIME != 0.0 { + //initialised + if SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_secs_f64() + - KILL_TIMER_RELATIVE_START_TIME + >= KILL_TIMEOUT + { return false; } } diff --git a/src/main.rs b/src/main.rs index 6ec2614..d8c1122 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,33 +1,33 @@ /* * Copyright (C) 2021 Evtech Solutions, Ltd., dba 3D-P * Copyright (C) 2021 Neil Tallim - * + * * This file is part of rperf. - * + * * rperf is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. - * + * * rperf is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. - * + * * You should have received a copy of the GNU General Public License * along with rperf. If not, see . */ - -extern crate log; + extern crate env_logger; +extern crate log; use clap::{App, Arg}; +mod client; mod protocol; +mod server; mod stream; mod utils; -mod client; -mod server; fn main() { let args = App::new("rperf") @@ -44,7 +44,6 @@ fn main() { .required(false) .default_value("5199") ) - .arg( Arg::with_name("affinity") .help("specify logical CPUs, delimited by commas, across which to round-robin affinity; not supported on all systems") @@ -63,8 +62,6 @@ fn main() { .short("d") .required(false) ) - - .arg( Arg::with_name("server") .help("run in server mode") @@ -89,7 +86,6 @@ fn main() { .required(false) .default_value("0") ) - .arg( Arg::with_name("client") .help("run in client mode; value is the server's address") @@ -234,25 +230,27 @@ fn main() { .default_value("") ) .get_matches(); - - let mut env = env_logger::Env::default() - .filter_or("RUST_LOG", "info"); + + let mut env = env_logger::Env::default().filter_or("RUST_LOG", "info"); if args.is_present("debug") { env = env.filter_or("RUST_LOG", "debug"); } env_logger::init_from_env(env); - + if args.is_present("server") { log::debug!("registering SIGINT handler..."); ctrlc::set_handler(move || { if server::kill() { - log::warn!("shutdown requested; please allow a moment for any in-progress tests to stop"); + log::warn!( + "shutdown requested; please allow a moment for any in-progress tests to stop" + ); } else { log::warn!("forcing shutdown immediately"); std::process::exit(3); } - }).expect("unable to set SIGINT handler"); - + }) + .expect("unable to set SIGINT handler"); + log::debug!("beginning normal operation..."); let service = server::serve(args); if service.is_err() { @@ -263,13 +261,16 @@ fn main() { log::debug!("registering SIGINT handler..."); ctrlc::set_handler(move || { if client::kill() { - log::warn!("shutdown requested; please allow a moment for any in-progress tests to stop"); + log::warn!( + "shutdown requested; please allow a moment for any in-progress tests to stop" + ); } else { log::warn!("forcing shutdown immediately"); std::process::exit(3); } - }).expect("unable to set SIGINT handler"); - + }) + .expect("unable to set SIGINT handler"); + log::debug!("connecting to server..."); let execution = client::execute(args); if execution.is_err() { diff --git a/src/protocol/communication.rs b/src/protocol/communication.rs index 5eaae27..b398417 100644 --- a/src/protocol/communication.rs +++ b/src/protocol/communication.rs @@ -1,19 +1,19 @@ /* * Copyright (C) 2021 Evtech Solutions, Ltd., dba 3D-P * Copyright (C) 2021 Neil Tallim - * + * * This file is part of rperf. - * + * * rperf is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. - * + * * rperf is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. - * + * * You should have received a copy of the GNU General Public License * along with rperf. If not, see . */ @@ -21,34 +21,43 @@ use std::io::{self, Read, Write}; use std::time::Duration; -use mio::{Events, Ready, Poll, PollOpt, Token}; -use mio::net::{TcpStream}; +use mio::net::TcpStream; +use mio::{Events, Poll, PollOpt, Ready, Token}; use std::error::Error; -type BoxResult = Result>; +type BoxResult = Result>; /// how long to wait for keepalive events // the communications channels typically exchange data every second, so 2s is reasonable to avoid excess noise -pub const KEEPALIVE_DURATION:Duration = Duration::from_secs(2); +pub const KEEPALIVE_DURATION: Duration = Duration::from_secs(2); /// how long to block on polling operations -const POLL_TIMEOUT:Duration = Duration::from_millis(50); +const POLL_TIMEOUT: Duration = Duration::from_millis(50); /// sends JSON data over a client-server communications stream -pub fn send(stream:&mut TcpStream, message:&serde_json::Value) -> BoxResult<()> { +pub fn send(stream: &mut TcpStream, message: &serde_json::Value) -> BoxResult<()> { let serialised_message = serde_json::to_vec(message)?; - - log::debug!("sending message of length {}, {:?}, to {}...", serialised_message.len(), message, stream.peer_addr()?); - let mut output_buffer = vec![0_u8; (serialised_message.len() + 2).into()]; + + log::debug!( + "sending message of length {}, {:?}, to {}...", + serialised_message.len(), + message, + stream.peer_addr()? + ); + let mut output_buffer = vec![0_u8; serialised_message.len() + 2]; output_buffer[..2].copy_from_slice(&(serialised_message.len() as u16).to_be_bytes()); output_buffer[2..].copy_from_slice(serialised_message.as_slice()); Ok(stream.write_all(&output_buffer)?) } /// receives the length-count of a pending message over a client-server communications stream -fn receive_length(stream:&mut TcpStream, alive_check:fn() -> bool, results_handler:&mut dyn FnMut() -> BoxResult<()>) -> BoxResult { +fn receive_length( + stream: &mut TcpStream, + alive_check: fn() -> bool, + results_handler: &mut dyn FnMut() -> BoxResult<()>, +) -> BoxResult { let mut cloned_stream = stream.try_clone()?; - + let mio_token = Token(0); let poll = Poll::new()?; poll.register( @@ -58,51 +67,71 @@ fn receive_length(stream:&mut TcpStream, alive_check:fn() -> bool, results_handl PollOpt::edge(), )?; let mut events = Events::with_capacity(1); //only interacting with one stream - + let mut length_bytes_read = 0; - let mut length_spec:[u8; 2] = [0; 2]; - while alive_check() { //waiting to find out how long the next message is + let mut length_spec: [u8; 2] = [0; 2]; + while alive_check() { + //waiting to find out how long the next message is results_handler()?; //send any outstanding results between cycles poll.poll(&mut events, Some(POLL_TIMEOUT))?; for event in events.iter() { - match event.token() { - _ => loop { - match cloned_stream.read(&mut length_spec[length_bytes_read..]) { - Ok(size) => { - if size == 0 { - if alive_check() { - return Err(Box::new(simple_error::simple_error!("connection lost"))); - } else { //shutting down; a disconnect is expected - return Err(Box::new(simple_error::simple_error!("local shutdown requested"))); - } - } - - length_bytes_read += size; - if length_bytes_read == 2 { - let length = u16::from_be_bytes(length_spec); - log::debug!("received length-spec of {} from {}", length, stream.peer_addr()?); - return Ok(length); + event.token(); + loop { + match cloned_stream.read(&mut length_spec[length_bytes_read..]) { + Ok(size) => { + if size == 0 { + if alive_check() { + return Err(Box::new(simple_error::simple_error!( + "connection lost" + ))); } else { - log::debug!("received partial length-spec from {}", stream.peer_addr()?); + //shutting down; a disconnect is expected + return Err(Box::new(simple_error::simple_error!( + "local shutdown requested" + ))); } - }, - Err(ref e) if e.kind() == io::ErrorKind::WouldBlock => { //nothing left to process - break; - }, - Err(e) => { - return Err(Box::new(e)); - }, + } + + length_bytes_read += size; + if length_bytes_read == 2 { + let length = u16::from_be_bytes(length_spec); + log::debug!( + "received length-spec of {} from {}", + length, + stream.peer_addr()? + ); + return Ok(length); + } else { + log::debug!( + "received partial length-spec from {}", + stream.peer_addr()? + ); + } + } + Err(ref e) if e.kind() == io::ErrorKind::WouldBlock => { + //nothing left to process + break; } - }, + Err(e) => { + return Err(Box::new(e)); + } + } } } } - Err(Box::new(simple_error::simple_error!("system shutting down"))) + Err(Box::new(simple_error::simple_error!( + "system shutting down" + ))) } /// receives the data-value of a pending message over a client-server communications stream -fn receive_payload(stream:&mut TcpStream, alive_check:fn() -> bool, results_handler:&mut dyn FnMut() -> BoxResult<()>, length:u16) -> BoxResult { +fn receive_payload( + stream: &mut TcpStream, + alive_check: fn() -> bool, + results_handler: &mut dyn FnMut() -> BoxResult<()>, + length: u16, +) -> BoxResult { let mut cloned_stream = stream.try_clone()?; - + let mio_token = Token(0); let poll = Poll::new()?; poll.register( @@ -112,57 +141,69 @@ fn receive_payload(stream:&mut TcpStream, alive_check:fn() -> bool, results_hand PollOpt::edge(), )?; let mut events = Events::with_capacity(1); //only interacting with one stream - + let mut bytes_read = 0; let mut buffer = vec![0_u8; length.into()]; - while alive_check() { //waiting to receive the payload + while alive_check() { + //waiting to receive the payload results_handler()?; //send any outstanding results between cycles poll.poll(&mut events, Some(POLL_TIMEOUT))?; for event in events.iter() { - match event.token() { - _ => loop { - match cloned_stream.read(&mut buffer[bytes_read..]) { - Ok(size) => { - if size == 0 { - if alive_check() { - return Err(Box::new(simple_error::simple_error!("connection lost"))); - } else { //shutting down; a disconnect is expected - return Err(Box::new(simple_error::simple_error!("local shutdown requested"))); - } + event.token(); + loop { + match cloned_stream.read(&mut buffer[bytes_read..]) { + Ok(size) => { + if size == 0 { + if alive_check() { + return Err(Box::new(simple_error::simple_error!( + "connection lost" + ))); + } else { + //shutting down; a disconnect is expected + return Err(Box::new(simple_error::simple_error!( + "local shutdown requested" + ))); } - - bytes_read += size; - if bytes_read == length as usize { - match serde_json::from_slice(&buffer) { - Ok(v) => { - log::debug!("received {:?} from {}", v, stream.peer_addr()?); - return Ok(v); - }, - Err(e) => { - return Err(Box::new(e)); - }, + } + + bytes_read += size; + if bytes_read == length as usize { + match serde_json::from_slice(&buffer) { + Ok(v) => { + log::debug!("received {:?} from {}", v, stream.peer_addr()?); + return Ok(v); + } + Err(e) => { + return Err(Box::new(e)); } - } else { - log::debug!("received partial payload from {}", stream.peer_addr()?); } - }, - Err(ref e) if e.kind() == io::ErrorKind::WouldBlock => { //nothing left to process - break; - }, - Err(e) => { - return Err(Box::new(e)); - }, + } else { + log::debug!("received partial payload from {}", stream.peer_addr()?); + } + } + Err(ref e) if e.kind() == io::ErrorKind::WouldBlock => { + //nothing left to process + break; + } + Err(e) => { + return Err(Box::new(e)); } - }, + } } } } - Err(Box::new(simple_error::simple_error!("system shutting down"))) + Err(Box::new(simple_error::simple_error!( + "system shutting down" + ))) } /// handles the full process of retrieving a message from a client-server communications stream -pub fn receive(mut stream:&mut TcpStream, alive_check:fn() -> bool, results_handler:&mut dyn FnMut() -> BoxResult<()>) -> BoxResult { +pub fn receive( + stream: &mut TcpStream, + alive_check: fn() -> bool, + results_handler: &mut dyn FnMut() -> BoxResult<()>, +) -> BoxResult { log::debug!("awaiting length-value from {}...", stream.peer_addr()?); - let length = receive_length(&mut stream, alive_check, results_handler)?; + let length = receive_length(stream, alive_check, results_handler)?; log::debug!("awaiting payload from {}...", stream.peer_addr()?); - receive_payload(&mut stream, alive_check, results_handler, length) + receive_payload(stream, alive_check, results_handler, length) } diff --git a/src/protocol/messaging.rs b/src/protocol/messaging.rs index a4ec012..880af8b 100644 --- a/src/protocol/messaging.rs +++ b/src/protocol/messaging.rs @@ -1,25 +1,25 @@ /* * Copyright (C) 2021 Evtech Solutions, Ltd., dba 3D-P * Copyright (C) 2021 Neil Tallim - * + * * This file is part of rperf. - * + * * rperf is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. - * + * * rperf is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. - * + * * You should have received a copy of the GNU General Public License * along with rperf. If not, see . */ use std::error::Error; -type BoxResult = Result>; +type BoxResult = Result>; /// prepares a message used to tell the server to begin operations pub fn prepare_begin() -> serde_json::Value { @@ -29,10 +29,10 @@ pub fn prepare_begin() -> serde_json::Value { } /// prepares a message used to tell the client to connect its test-streams -pub fn prepare_connect(stream_ports:&[u16]) -> serde_json::Value { +pub fn prepare_connect(stream_ports: &[u16]) -> serde_json::Value { serde_json::json!({ "kind": "connect", - + "stream_ports": stream_ports, }) } @@ -51,86 +51,108 @@ pub fn prepare_end() -> serde_json::Value { }) } - - - /// prepares a message used to describe the upload role of a TCP test -fn prepare_configuration_tcp_upload(test_id:&[u8; 16], streams:u8, bandwidth:u64, seconds:f32, length:usize, send_interval:f32, send_buffer:u32, no_delay:bool) -> serde_json::Value { +#[allow(clippy::too_many_arguments)] +fn prepare_configuration_tcp_upload( + test_id: &[u8; 16], + streams: u8, + bandwidth: u64, + seconds: f32, + length: usize, + send_interval: f32, + send_buffer: u32, + no_delay: bool, +) -> serde_json::Value { serde_json::json!({ "kind": "configuration", - + "family": "tcp", "role": "upload", - + "test_id": test_id, "streams": validate_streams(streams), - + "bandwidth": validate_bandwidth(bandwidth), "duration": seconds, "length": calculate_length_tcp(length), "send_interval": validate_send_interval(send_interval), - + "send_buffer": send_buffer, "no_delay": no_delay, }) } /// prepares a message used to describe the download role of a TCP test -fn prepare_configuration_tcp_download(test_id:&[u8; 16], streams:u8, length:usize, receive_buffer:u32) -> serde_json::Value { +fn prepare_configuration_tcp_download( + test_id: &[u8; 16], + streams: u8, + length: usize, + receive_buffer: u32, +) -> serde_json::Value { serde_json::json!({ "kind": "configuration", - + "family": "tcp", "role": "download", - + "test_id": test_id, "streams": validate_streams(streams), - + "length": calculate_length_tcp(length), "receive_buffer": receive_buffer, }) } /// prepares a message used to describe the upload role of a UDP test -fn prepare_configuration_udp_upload(test_id:&[u8; 16], streams:u8, bandwidth:u64, seconds:f32, length:u16, send_interval:f32, send_buffer:u32) -> serde_json::Value { +fn prepare_configuration_udp_upload( + test_id: &[u8; 16], + streams: u8, + bandwidth: u64, + seconds: f32, + length: u16, + send_interval: f32, + send_buffer: u32, +) -> serde_json::Value { serde_json::json!({ "kind": "configuration", - + "family": "udp", "role": "upload", - + "test_id": test_id, "streams": validate_streams(streams), - + "bandwidth": validate_bandwidth(bandwidth), "duration": seconds, "length": calculate_length_udp(length), "send_interval": validate_send_interval(send_interval), - + "send_buffer": send_buffer, }) } /// prepares a message used to describe the download role of a UDP test -fn prepare_configuration_udp_download(test_id:&[u8; 16], streams:u8, length:u16, receive_buffer:u32) -> serde_json::Value { +fn prepare_configuration_udp_download( + test_id: &[u8; 16], + streams: u8, + length: u16, + receive_buffer: u32, +) -> serde_json::Value { serde_json::json!({ "kind": "configuration", - + "family": "udp", "role": "download", - + "test_id": test_id, "streams": validate_streams(streams), - + "length": calculate_length_udp(length), "receive_buffer": receive_buffer, }) } - - - -fn validate_streams(streams:u8) -> u8 { +fn validate_streams(streams: u8) -> u8 { if streams > 0 { streams } else { @@ -139,7 +161,7 @@ fn validate_streams(streams:u8) -> u8 { } } -fn validate_bandwidth(bandwidth:u64) -> u64 { +fn validate_bandwidth(bandwidth: u64) -> u64 { if bandwidth > 0 { bandwidth } else { @@ -148,7 +170,7 @@ fn validate_bandwidth(bandwidth:u64) -> u64 { } } -fn validate_send_interval(send_interval:f32) -> f32 { +fn validate_send_interval(send_interval: f32) -> f32 { if send_interval > 0.0 && send_interval <= 1.0 { send_interval } else { @@ -157,90 +179,109 @@ fn validate_send_interval(send_interval:f32) -> f32 { } } -fn calculate_length_tcp(length:usize) -> usize { - if length < crate::stream::tcp::TEST_HEADER_SIZE { //length must be at least enough to hold the test data +fn calculate_length_tcp(length: usize) -> usize { + if length < crate::stream::tcp::TEST_HEADER_SIZE { + //length must be at least enough to hold the test data crate::stream::tcp::TEST_HEADER_SIZE } else { length } } -fn calculate_length_udp(length:u16) -> u16 { - if length < crate::stream::udp::TEST_HEADER_SIZE { //length must be at least enough to hold the test data +fn calculate_length_udp(length: u16) -> u16 { + if length < crate::stream::udp::TEST_HEADER_SIZE { + //length must be at least enough to hold the test data crate::stream::udp::TEST_HEADER_SIZE } else { length } } - /// prepares a message used to describe the upload role in a test -pub fn prepare_upload_configuration(args:&clap::ArgMatches, test_id:&[u8; 16]) -> BoxResult { - let parallel_streams:u8 = args.value_of("parallel").unwrap().parse()?; - let mut seconds:f32 = args.value_of("time").unwrap().parse()?; - let mut send_interval:f32 = args.value_of("send_interval").unwrap().parse()?; - let mut length:u32 = args.value_of("length").unwrap().parse()?; - - let mut send_buffer:u32 = args.value_of("send_buffer").unwrap().parse()?; - +pub fn prepare_upload_configuration( + args: &clap::ArgMatches, + test_id: &[u8; 16], +) -> BoxResult { + let parallel_streams: u8 = args.value_of("parallel").unwrap().parse()?; + let mut seconds: f32 = args.value_of("time").unwrap().parse()?; + let mut send_interval: f32 = args.value_of("send_interval").unwrap().parse()?; + let mut length: u32 = args.value_of("length").unwrap().parse()?; + + let mut send_buffer: u32 = args.value_of("send_buffer").unwrap().parse()?; + let mut bandwidth_string = args.value_of("bandwidth").unwrap(); - let bandwidth:u64; - let bandwidth_multiplier:f64; + let bandwidth: u64; + let bandwidth_multiplier: f64; match bandwidth_string.chars().last() { Some(v) => { match v { - 'k' => { //kilobits + 'k' => { + //kilobits bandwidth_multiplier = 1000.0 / 8.0; - }, - 'K' => { //kilobytes + } + 'K' => { + //kilobytes bandwidth_multiplier = 1000.0; - }, - 'm' => { //megabits + } + 'm' => { + //megabits bandwidth_multiplier = 1000.0 * 1000.0 / 8.0; - }, - 'M' => { //megabytes + } + 'M' => { + //megabytes bandwidth_multiplier = 1000.0 * 1000.0; - }, - 'g' => { //gigabits + } + 'g' => { + //gigabits bandwidth_multiplier = 1000.0 * 1000.0 * 1000.0 / 8.0; - }, - 'G' => { //gigabytes + } + 'G' => { + //gigabytes bandwidth_multiplier = 1000.0 * 1000.0 * 1000.0; - }, + } _ => { bandwidth_multiplier = 1.0; - }, + } } - - if bandwidth_multiplier != 1.0 { //the value uses a suffix - bandwidth_string = &bandwidth_string[0..(bandwidth_string.len() - 1)]; + + if bandwidth_multiplier != 1.0 { + //the value uses a suffix + bandwidth_string = &bandwidth_string[0..(bandwidth_string.len() - 1)]; } - + match bandwidth_string.parse::() { Ok(v2) => { bandwidth = (v2 * bandwidth_multiplier) as u64; - }, - Err(_) => { //invalid input; fall back to 1mbps - log::warn!("invalid bandwidth: {}; setting value to 1mbps", args.value_of("bandwidth").unwrap()); + } + Err(_) => { + //invalid input; fall back to 1mbps + log::warn!( + "invalid bandwidth: {}; setting value to 1mbps", + args.value_of("bandwidth").unwrap() + ); bandwidth = 125000; - }, + } } - }, - None => { //invalid input; fall back to 1mbps - log::warn!("invalid bandwidth: {}; setting value to 1mbps", args.value_of("bandwidth").unwrap()); + } + None => { + //invalid input; fall back to 1mbps + log::warn!( + "invalid bandwidth: {}; setting value to 1mbps", + args.value_of("bandwidth").unwrap() + ); bandwidth = 125000; - }, + } } - + if seconds <= 0.0 { log::warn!("time was not in an acceptable range and has been set to 0.0"); seconds = 0.0 } - + if send_interval > 1.0 || send_interval <= 0.0 { log::warn!("send-interval was not in an acceptable range and has been set to 0.05"); send_interval = 0.05 } - + if args.is_present("udp") { log::debug!("preparing UDP upload config..."); if length == 0 { @@ -250,7 +291,15 @@ pub fn prepare_upload_configuration(args:&clap::ArgMatches, test_id:&[u8; 16]) - log::warn!("requested send-buffer, {}, is too small to hold the data to be exchanged; it will be increased to {}", send_buffer, length * 2); send_buffer = length * 2; } - Ok(prepare_configuration_udp_upload(test_id, parallel_streams, bandwidth, seconds, length as u16, send_interval, send_buffer)) + Ok(prepare_configuration_udp_upload( + test_id, + parallel_streams, + bandwidth, + seconds, + length as u16, + send_interval, + send_buffer, + )) } else { log::debug!("preparing TCP upload config..."); if length == 0 { @@ -260,18 +309,30 @@ pub fn prepare_upload_configuration(args:&clap::ArgMatches, test_id:&[u8; 16]) - log::warn!("requested send-buffer, {}, is too small to hold the data to be exchanged; it will be increased to {}", send_buffer, length * 2); send_buffer = length * 2; } - - let no_delay:bool = args.is_present("no_delay"); - - Ok(prepare_configuration_tcp_upload(test_id, parallel_streams, bandwidth, seconds, length as usize, send_interval, send_buffer, no_delay)) + + let no_delay: bool = args.is_present("no_delay"); + + Ok(prepare_configuration_tcp_upload( + test_id, + parallel_streams, + bandwidth, + seconds, + length as usize, + send_interval, + send_buffer, + no_delay, + )) } } /// prepares a message used to describe the download role in a test -pub fn prepare_download_configuration(args:&clap::ArgMatches, test_id:&[u8; 16]) -> BoxResult { - let parallel_streams:u8 = args.value_of("parallel").unwrap().parse()?; - let mut length:u32 = args.value_of("length").unwrap().parse()?; - let mut receive_buffer:u32 = args.value_of("receive_buffer").unwrap().parse()?; - +pub fn prepare_download_configuration( + args: &clap::ArgMatches, + test_id: &[u8; 16], +) -> BoxResult { + let parallel_streams: u8 = args.value_of("parallel").unwrap().parse()?; + let mut length: u32 = args.value_of("length").unwrap().parse()?; + let mut receive_buffer: u32 = args.value_of("receive_buffer").unwrap().parse()?; + if args.is_present("udp") { log::debug!("preparing UDP download config..."); if length == 0 { @@ -281,7 +342,12 @@ pub fn prepare_download_configuration(args:&clap::ArgMatches, test_id:&[u8; 16]) log::warn!("requested receive-buffer, {}, is too small to hold the data to be exchanged; it will be increased to {}", receive_buffer, length * 2); receive_buffer = length * 2; } - Ok(prepare_configuration_udp_download(test_id, parallel_streams, length as u16, receive_buffer)) + Ok(prepare_configuration_udp_download( + test_id, + parallel_streams, + length as u16, + receive_buffer, + )) } else { log::debug!("preparing TCP download config..."); if length == 0 { @@ -291,6 +357,11 @@ pub fn prepare_download_configuration(args:&clap::ArgMatches, test_id:&[u8; 16]) log::warn!("requested receive-buffer, {}, is too small to hold the data to be exchanged; it will be increased to {}", receive_buffer, length * 2); receive_buffer = length * 2; } - Ok(prepare_configuration_tcp_download(test_id, parallel_streams, length as usize, receive_buffer)) + Ok(prepare_configuration_tcp_download( + test_id, + parallel_streams, + length as usize, + receive_buffer, + )) } } diff --git a/src/protocol/mod.rs b/src/protocol/mod.rs index 8e4a093..afedd6d 100644 --- a/src/protocol/mod.rs +++ b/src/protocol/mod.rs @@ -1,19 +1,19 @@ /* * Copyright (C) 2021 Evtech Solutions, Ltd., dba 3D-P * Copyright (C) 2021 Neil Tallim - * + * * This file is part of rperf. - * + * * rperf is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. - * + * * rperf is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. - * + * * You should have received a copy of the GNU General Public License * along with rperf. If not, see . */ diff --git a/src/protocol/results.rs b/src/protocol/results.rs index 852361f..d59f833 100644 --- a/src/protocol/results.rs +++ b/src/protocol/results.rs @@ -1,19 +1,19 @@ /* * Copyright (C) 2021 Evtech Solutions, Ltd., dba 3D-P * Copyright (C) 2021 Neil Tallim - * + * * This file is part of rperf. - * + * * rperf is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. - * + * * rperf is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. - * + * * You should have received a copy of the GNU General Public License * along with rperf. If not, see . */ @@ -21,17 +21,16 @@ use std::collections::{HashMap, HashSet}; use std::time::{SystemTime, UNIX_EPOCH}; -use serde::{Serialize, Deserialize}; +use serde::{Deserialize, Serialize}; use std::error::Error; -type BoxResult = Result>; +type BoxResult = Result>; /* This module contains structures used to represent and collect the results of tests. * Since everything is basically just a data-container with representation methods, * it isn't extensively documented. */ - #[derive(PartialEq)] pub enum IntervalResultKind { ClientDone, @@ -53,27 +52,29 @@ pub fn get_unix_timestamp() -> f64 { pub trait IntervalResult { fn kind(&self) -> IntervalResultKind; - + fn get_stream_idx(&self) -> u8; - + fn to_json(&self) -> serde_json::Value; - + //produces test-results in tabular form - fn to_string(&self, bit:bool) -> String; + fn to_string(&self, bit: bool) -> String; } +pub type IntervalResultBox = Box; + pub struct ClientDoneResult { pub stream_idx: u8, } impl IntervalResult for ClientDoneResult { - fn kind(&self) -> IntervalResultKind{ + fn kind(&self) -> IntervalResultKind { IntervalResultKind::ClientDone } - + fn get_stream_idx(&self) -> u8 { self.stream_idx } - + fn to_json(&self) -> serde_json::Value { serde_json::json!({ "kind": "done", @@ -81,11 +82,12 @@ impl IntervalResult for ClientDoneResult { "stream_idx": self.stream_idx, }) } - - fn to_string(&self, _bit:bool) -> String { - format!("----------\n\ + + fn to_string(&self, _bit: bool) -> String { + format!( + "----------\n\ End of stream from client | stream: {}", - self.stream_idx, + self.stream_idx, ) } } @@ -93,14 +95,14 @@ pub struct ServerDoneResult { pub stream_idx: u8, } impl IntervalResult for ServerDoneResult { - fn kind(&self) -> IntervalResultKind{ + fn kind(&self) -> IntervalResultKind { IntervalResultKind::ServerDone } - + fn get_stream_idx(&self) -> u8 { self.stream_idx } - + fn to_json(&self) -> serde_json::Value { serde_json::json!({ "kind": "done", @@ -108,11 +110,12 @@ impl IntervalResult for ServerDoneResult { "stream_idx": self.stream_idx, }) } - - fn to_string(&self, _bit:bool) -> String { - format!("----------\n\ + + fn to_string(&self, _bit: bool) -> String { + format!( + "----------\n\ End of stream from server | stream: {}", - self.stream_idx, + self.stream_idx, ) } } @@ -121,14 +124,14 @@ pub struct ClientFailedResult { pub stream_idx: u8, } impl IntervalResult for ClientFailedResult { - fn kind(&self) -> IntervalResultKind{ + fn kind(&self) -> IntervalResultKind { IntervalResultKind::ClientFailed } - + fn get_stream_idx(&self) -> u8 { self.stream_idx } - + fn to_json(&self) -> serde_json::Value { serde_json::json!({ "kind": "failed", @@ -136,11 +139,12 @@ impl IntervalResult for ClientFailedResult { "stream_idx": self.stream_idx, }) } - - fn to_string(&self, _bit:bool) -> String { - format!("----------\n\ + + fn to_string(&self, _bit: bool) -> String { + format!( + "----------\n\ Failure in client stream | stream: {}", - self.stream_idx, + self.stream_idx, ) } } @@ -148,14 +152,14 @@ pub struct ServerFailedResult { pub stream_idx: u8, } impl IntervalResult for ServerFailedResult { - fn kind(&self) -> IntervalResultKind{ + fn kind(&self) -> IntervalResultKind { IntervalResultKind::ServerFailed } - + fn get_stream_idx(&self) -> u8 { self.stream_idx } - + fn to_json(&self) -> serde_json::Value { serde_json::json!({ "kind": "failed", @@ -163,32 +167,32 @@ impl IntervalResult for ServerFailedResult { "stream_idx": self.stream_idx, }) } - - fn to_string(&self, _bit:bool) -> String { - format!("----------\n\ + + fn to_string(&self, _bit: bool) -> String { + format!( + "----------\n\ Failure in server stream | stream: {}", - self.stream_idx, + self.stream_idx, ) } } - #[derive(Serialize, Deserialize)] pub struct TcpReceiveResult { pub timestamp: f64, - + pub stream_idx: u8, - + pub duration: f32, - + pub bytes_received: u64, } impl TcpReceiveResult { - fn from_json(value:serde_json::Value) -> BoxResult { - let receive_result:TcpReceiveResult = serde_json::from_value(value)?; + fn from_json(value: serde_json::Value) -> BoxResult { + let receive_result: TcpReceiveResult = serde_json::from_value(value)?; Ok(receive_result) } - + fn to_result_json(&self) -> serde_json::Value { let mut serialised = serde_json::to_value(self).unwrap(); serialised.as_object_mut().unwrap().remove("stream_idx"); @@ -196,41 +200,44 @@ impl TcpReceiveResult { } } impl IntervalResult for TcpReceiveResult { - fn kind(&self) -> IntervalResultKind{ + fn kind(&self) -> IntervalResultKind { IntervalResultKind::TcpReceive } - + fn get_stream_idx(&self) -> u8 { self.stream_idx } - + fn to_json(&self) -> serde_json::Value { let mut serialised = serde_json::to_value(self).unwrap(); serialised["family"] = serde_json::json!("tcp"); serialised["kind"] = serde_json::json!("receive"); serialised } - - fn to_string(&self, bit:bool) -> String { - let duration_divisor; - if self.duration == 0.0 { //avoid zerodiv, which can happen if the stream fails - duration_divisor = 1.0; + + fn to_string(&self, bit: bool) -> String { + let duration_divisor = if self.duration == 0.0 { + //avoid zerodiv, which can happen if the stream fails + 1.0 } else { - duration_divisor = self.duration; - } - + self.duration + }; + let bytes_per_second = self.bytes_received as f32 / duration_divisor; - + let throughput = match bit { - true => format!("megabits/second: {:.3}", bytes_per_second / (1_000_000.00 / 8.0)), + true => format!( + "megabits/second: {:.3}", + bytes_per_second / (1_000_000.00 / 8.0) + ), false => format!("megabytes/second: {:.3}", bytes_per_second / 1_000_000.00), }; - - format!("----------\n\ + + format!( + "----------\n\ TCP receive result over {:.2}s | stream: {}\n\ bytes: {} | per second: {:.3} | {}", - self.duration, self.stream_idx, - self.bytes_received, bytes_per_second, throughput, + self.duration, self.stream_idx, self.bytes_received, bytes_per_second, throughput, ) } } @@ -238,25 +245,26 @@ impl IntervalResult for TcpReceiveResult { #[derive(Serialize, Deserialize)] pub struct TcpSendResult { pub timestamp: f64, - + pub stream_idx: u8, - + pub duration: f32, - + pub bytes_sent: u64, pub sends_blocked: u64, } impl TcpSendResult { - fn from_json(value:serde_json::Value) -> BoxResult { + fn from_json(value: serde_json::Value) -> BoxResult { let mut local_value = value.clone(); - if local_value.get("sends_blocked").is_none() { //pre-0.1.8 peer - local_value["sends_blocked"] = serde_json::json!(0 as u64); //report pre-0.1.8 status + if local_value.get("sends_blocked").is_none() { + //pre-0.1.8 peer + local_value["sends_blocked"] = serde_json::json!(0_u64); //report pre-0.1.8 status } - - let send_result:TcpSendResult = serde_json::from_value(local_value)?; + + let send_result: TcpSendResult = serde_json::from_value(local_value)?; Ok(send_result) } - + fn to_result_json(&self) -> serde_json::Value { let mut serialised = serde_json::to_value(self).unwrap(); serialised.as_object_mut().unwrap().remove("stream_idx"); @@ -264,73 +272,78 @@ impl TcpSendResult { } } impl IntervalResult for TcpSendResult { - fn kind(&self) -> IntervalResultKind{ + fn kind(&self) -> IntervalResultKind { IntervalResultKind::TcpSend } - + fn get_stream_idx(&self) -> u8 { self.stream_idx } - + fn to_json(&self) -> serde_json::Value { let mut serialised = serde_json::to_value(self).unwrap(); serialised["family"] = serde_json::json!("tcp"); serialised["kind"] = serde_json::json!("send"); serialised } - - fn to_string(&self, bit:bool) -> String { - let duration_divisor; - if self.duration == 0.0 { //avoid zerodiv, which can happen if the stream fails - duration_divisor = 1.0; + + fn to_string(&self, bit: bool) -> String { + let duration_divisor = if self.duration == 0.0 { + //avoid zerodiv, which can happen if the stream fails + 1.0 } else { - duration_divisor = self.duration; - } - + self.duration + }; + let bytes_per_second = self.bytes_sent as f32 / duration_divisor; - + let throughput = match bit { - true => format!("megabits/second: {:.3}", bytes_per_second / (1_000_000.00 / 8.0)), + true => format!( + "megabits/second: {:.3}", + bytes_per_second / (1_000_000.00 / 8.0) + ), false => format!("megabytes/second: {:.3}", bytes_per_second / 1_000_000.00), }; - - let mut output = format!("----------\n\ + + let mut output = format!( + "----------\n\ TCP send result over {:.2}s | stream: {}\n\ bytes: {} | per second: {:.3} | {}", - self.duration, self.stream_idx, - self.bytes_sent, bytes_per_second, throughput, + self.duration, self.stream_idx, self.bytes_sent, bytes_per_second, throughput, ); if self.sends_blocked > 0 { - output.push_str(&format!("\nstalls due to full send-buffer: {}", self.sends_blocked)); + output.push_str(&format!( + "\nstalls due to full send-buffer: {}", + self.sends_blocked + )); } output } } - #[derive(Serialize, Deserialize)] pub struct UdpReceiveResult { pub timestamp: f64, - + pub stream_idx: u8, - + pub duration: f32, - + pub bytes_received: u64, pub packets_received: u64, pub packets_lost: i64, pub packets_out_of_order: u64, pub packets_duplicated: u64, - + pub unbroken_sequence: u64, pub jitter_seconds: Option, } impl UdpReceiveResult { - fn from_json(value:serde_json::Value) -> BoxResult { - let receive_result:UdpReceiveResult = serde_json::from_value(value)?; + fn from_json(value: serde_json::Value) -> BoxResult { + let receive_result: UdpReceiveResult = serde_json::from_value(value)?; Ok(receive_result) } - + fn to_result_json(&self) -> serde_json::Value { let mut serialised = serde_json::to_value(self).unwrap(); serialised.as_object_mut().unwrap().remove("stream_idx"); @@ -338,36 +351,39 @@ impl UdpReceiveResult { } } impl IntervalResult for UdpReceiveResult { - fn kind(&self) -> IntervalResultKind{ + fn kind(&self) -> IntervalResultKind { IntervalResultKind::UdpReceive } - + fn get_stream_idx(&self) -> u8 { self.stream_idx } - + fn to_json(&self) -> serde_json::Value { let mut serialised = serde_json::to_value(self).unwrap(); serialised["family"] = serde_json::json!("udp"); serialised["kind"] = serde_json::json!("receive"); serialised } - - fn to_string(&self, bit:bool) -> String { - let duration_divisor; - if self.duration == 0.0 { //avoid zerodiv, which can happen if the stream fails - duration_divisor = 1.0; + + fn to_string(&self, bit: bool) -> String { + let duration_divisor = if self.duration == 0.0 { + //avoid zerodiv, which can happen if the stream fails + 1.0 } else { - duration_divisor = self.duration; - } - + self.duration + }; + let bytes_per_second = self.bytes_received as f32 / duration_divisor; - + let throughput = match bit { - true => format!("megabits/second: {:.3}", bytes_per_second / (1_000_000.00 / 8.0)), + true => format!( + "megabits/second: {:.3}", + bytes_per_second / (1_000_000.00 / 8.0) + ), false => format!("megabytes/second: {:.3}", bytes_per_second / 1_000_000.00), }; - + let mut output = format!("----------\n\ UDP receive result over {:.2}s | stream: {}\n\ bytes: {} | per second: {:.3} | {}\n\ @@ -377,7 +393,11 @@ impl IntervalResult for UdpReceiveResult { self.packets_received, self.packets_lost, self.packets_out_of_order, self.packets_duplicated, self.packets_received as f32 / duration_divisor, ); if self.jitter_seconds.is_some() { - output.push_str(&format!("\njitter: {:.6}s over {} consecutive packets", self.jitter_seconds.unwrap(), self.unbroken_sequence)); + output.push_str(&format!( + "\njitter: {:.6}s over {} consecutive packets", + self.jitter_seconds.unwrap(), + self.unbroken_sequence + )); } output } @@ -386,26 +406,27 @@ impl IntervalResult for UdpReceiveResult { #[derive(Serialize, Deserialize)] pub struct UdpSendResult { pub timestamp: f64, - + pub stream_idx: u8, - + pub duration: f32, - + pub bytes_sent: u64, pub packets_sent: u64, pub sends_blocked: u64, } impl UdpSendResult { - fn from_json(value:serde_json::Value) -> BoxResult { + fn from_json(value: serde_json::Value) -> BoxResult { let mut local_value = value.clone(); - if local_value.get("sends_blocked").is_none() { //pre-0.1.8 peer - local_value["sends_blocked"] = serde_json::json!(0 as u64); //report pre-0.1.8 status + if local_value.get("sends_blocked").is_none() { + //pre-0.1.8 peer + local_value["sends_blocked"] = serde_json::json!(0_u64); //report pre-0.1.8 status } - - let send_result:UdpSendResult = serde_json::from_value(local_value)?; + + let send_result: UdpSendResult = serde_json::from_value(local_value)?; Ok(send_result) } - + fn to_result_json(&self) -> serde_json::Value { let mut serialised = serde_json::to_value(self).unwrap(); serialised.as_object_mut().unwrap().remove("stream_idx"); @@ -413,53 +434,63 @@ impl UdpSendResult { } } impl IntervalResult for UdpSendResult { - fn kind(&self) -> IntervalResultKind{ + fn kind(&self) -> IntervalResultKind { IntervalResultKind::UdpSend } - + fn get_stream_idx(&self) -> u8 { self.stream_idx } - + fn to_json(&self) -> serde_json::Value { let mut serialised = serde_json::to_value(self).unwrap(); serialised["family"] = serde_json::json!("udp"); serialised["kind"] = serde_json::json!("send"); serialised } - - fn to_string(&self, bit:bool) -> String { - let duration_divisor; - if self.duration == 0.0 { //avoid zerodiv, which can happen if the stream fails - duration_divisor = 1.0; + + fn to_string(&self, bit: bool) -> String { + let duration_divisor = if self.duration == 0.0 { + //avoid zerodiv, which can happen if the stream fails + 1.0 } else { - duration_divisor = self.duration; - } - + self.duration + }; + let bytes_per_second = self.bytes_sent as f32 / duration_divisor; - + let throughput = match bit { - true => format!("megabits/second: {:.3}", bytes_per_second / (1_000_000.00 / 8.0)), + true => format!( + "megabits/second: {:.3}", + bytes_per_second / (1_000_000.00 / 8.0) + ), false => format!("megabytes/second: {:.3}", bytes_per_second / 1_000_000.00), }; - - let mut output = format!("----------\n\ + + let mut output = format!( + "----------\n\ UDP send result over {:.2}s | stream: {}\n\ bytes: {} | per second: {:.3} | {}\n\ packets: {} per second: {:.3}", - self.duration, self.stream_idx, - self.bytes_sent, bytes_per_second, throughput, - self.packets_sent, self.packets_sent as f32 / duration_divisor, + self.duration, + self.stream_idx, + self.bytes_sent, + bytes_per_second, + throughput, + self.packets_sent, + self.packets_sent as f32 / duration_divisor, ); if self.sends_blocked > 0 { - output.push_str(&format!("\nstalls due to full send-buffer: {}", self.sends_blocked)); + output.push_str(&format!( + "\nstalls due to full send-buffer: {}", + self.sends_blocked + )); } output } } - -pub fn interval_result_from_json(value:serde_json::Value) -> BoxResult> { +pub fn interval_result_from_json(value: serde_json::Value) -> BoxResult> { match value.get("family") { Some(f) => match f.as_str() { Some(family) => match family { @@ -468,38 +499,56 @@ pub fn interval_result_from_json(value:serde_json::Value) -> BoxResult match kind { "receive" => Ok(Box::new(TcpReceiveResult::from_json(value)?)), "send" => Ok(Box::new(TcpSendResult::from_json(value)?)), - _ => Err(Box::new(simple_error::simple_error!("unsupported interval-result kind: {}", kind))), + _ => Err(Box::new(simple_error::simple_error!( + "unsupported interval-result kind: {}", + kind + ))), }, - None => Err(Box::new(simple_error::simple_error!("interval-result's kind is not a string"))), + None => Err(Box::new(simple_error::simple_error!( + "interval-result's kind is not a string" + ))), }, - None => Err(Box::new(simple_error::simple_error!("interval-result has no kind"))), + None => Err(Box::new(simple_error::simple_error!( + "interval-result has no kind" + ))), }, "udp" => match value.get("kind") { Some(k) => match k.as_str() { Some(kind) => match kind { "receive" => Ok(Box::new(UdpReceiveResult::from_json(value)?)), "send" => Ok(Box::new(UdpSendResult::from_json(value)?)), - _ => Err(Box::new(simple_error::simple_error!("unsupported interval-result kind: {}", kind))), + _ => Err(Box::new(simple_error::simple_error!( + "unsupported interval-result kind: {}", + kind + ))), }, - None => Err(Box::new(simple_error::simple_error!("interval-result's kind is not a string"))), + None => Err(Box::new(simple_error::simple_error!( + "interval-result's kind is not a string" + ))), }, - None => Err(Box::new(simple_error::simple_error!("interval-result has no kind"))), + None => Err(Box::new(simple_error::simple_error!( + "interval-result has no kind" + ))), }, - _ => Err(Box::new(simple_error::simple_error!("unsupported interval-result family: {}", family))), + _ => Err(Box::new(simple_error::simple_error!( + "unsupported interval-result family: {}", + family + ))), }, - None => Err(Box::new(simple_error::simple_error!("interval-result's family is not a string"))), + None => Err(Box::new(simple_error::simple_error!( + "interval-result's family is not a string" + ))), }, - None => Err(Box::new(simple_error::simple_error!("interval-result has no family"))), + None => Err(Box::new(simple_error::simple_error!( + "interval-result has no family" + ))), } } - - - pub trait StreamResults { - fn update_from_json(&mut self, value:serde_json::Value) -> BoxResult<()>; - - fn to_json(&self, omit_seconds:usize) -> serde_json::Value; + fn update_from_json(&mut self, value: serde_json::Value) -> BoxResult<()>; + + fn to_json(&self, omit_seconds: usize) -> serde_json::Value; } struct TcpStreamResults { @@ -507,53 +556,67 @@ struct TcpStreamResults { send_results: Vec, } impl StreamResults for TcpStreamResults { - fn update_from_json(&mut self, value:serde_json::Value) -> BoxResult<()> { + fn update_from_json(&mut self, value: serde_json::Value) -> BoxResult<()> { match value.get("kind") { Some(k) => match k.as_str() { Some(kind) => match kind { - "send" => Ok(self.send_results.push(TcpSendResult::from_json(value)?)), - "receive" => Ok(self.receive_results.push(TcpReceiveResult::from_json(value)?)), - _ => Err(Box::new(simple_error::simple_error!("unsupported kind for TCP stream-result: {}", kind))), + "send" => { + self.send_results.push(TcpSendResult::from_json(value)?); + Ok(()) + } + "receive" => { + self.receive_results + .push(TcpReceiveResult::from_json(value)?); + Ok(()) + } + _ => Err(Box::new(simple_error::simple_error!( + "unsupported kind for TCP stream-result: {}", + kind + ))), }, - None => Err(Box::new(simple_error::simple_error!("kind must be a string for TCP stream-result"))), + None => Err(Box::new(simple_error::simple_error!( + "kind must be a string for TCP stream-result" + ))), }, - None => Err(Box::new(simple_error::simple_error!("no kind specified for TCP stream-result"))), + None => Err(Box::new(simple_error::simple_error!( + "no kind specified for TCP stream-result" + ))), } } - - fn to_json(&self, omit_seconds:usize) -> serde_json::Value { - let mut duration_send:f64 = 0.0; - let mut bytes_sent:u64 = 0; - - let mut duration_receive:f64 = 0.0; - let mut bytes_received:u64 = 0; - + + fn to_json(&self, omit_seconds: usize) -> serde_json::Value { + let mut duration_send: f64 = 0.0; + let mut bytes_sent: u64 = 0; + + let mut duration_receive: f64 = 0.0; + let mut bytes_received: u64 = 0; + for (i, sr) in self.send_results.iter().enumerate() { if i < omit_seconds { continue; } - + duration_send += sr.duration as f64; bytes_sent += sr.bytes_sent; } - + for (i, rr) in self.receive_results.iter().enumerate() { if i < omit_seconds { continue; } - + duration_receive += rr.duration as f64; bytes_received += rr.bytes_received; } - + let summary = serde_json::json!({ "duration_send": duration_send, "bytes_sent": bytes_sent, - + "duration_receive": duration_receive, "bytes_received": bytes_received, }); - + serde_json::json!({ "receive": self.receive_results.iter().map(|rr| rr.to_result_json()).collect::>(), "send": self.send_results.iter().map(|sr| sr.to_result_json()).collect::>(), @@ -567,78 +630,92 @@ struct UdpStreamResults { send_results: Vec, } impl StreamResults for UdpStreamResults { - fn update_from_json(&mut self, value:serde_json::Value) -> BoxResult<()> { + fn update_from_json(&mut self, value: serde_json::Value) -> BoxResult<()> { match value.get("kind") { Some(k) => match k.as_str() { Some(kind) => match kind { - "send" => Ok(self.send_results.push(UdpSendResult::from_json(value)?)), - "receive" => Ok(self.receive_results.push(UdpReceiveResult::from_json(value)?)), - _ => Err(Box::new(simple_error::simple_error!("unsupported kind for UDP stream-result: {}", kind))), + "send" => { + self.send_results.push(UdpSendResult::from_json(value)?); + Ok(()) + } + "receive" => { + self.receive_results + .push(UdpReceiveResult::from_json(value)?); + Ok(()) + } + _ => Err(Box::new(simple_error::simple_error!( + "unsupported kind for UDP stream-result: {}", + kind + ))), }, - None => Err(Box::new(simple_error::simple_error!("kind must be a string for UDP stream-result"))), + None => Err(Box::new(simple_error::simple_error!( + "kind must be a string for UDP stream-result" + ))), }, - None => Err(Box::new(simple_error::simple_error!("no kind specified for UDP stream-result"))), + None => Err(Box::new(simple_error::simple_error!( + "no kind specified for UDP stream-result" + ))), } } - - fn to_json(&self, omit_seconds:usize) -> serde_json::Value { - let mut duration_send:f64 = 0.0; - - let mut bytes_sent:u64 = 0; - let mut packets_sent:u64 = 0; - - - let mut duration_receive:f64 = 0.0; - - let mut bytes_received:u64 = 0; - let mut packets_received:u64 = 0; - let mut packets_out_of_order:u64 = 0; - let mut packets_duplicated:u64 = 0; - + + fn to_json(&self, omit_seconds: usize) -> serde_json::Value { + let mut duration_send: f64 = 0.0; + + let mut bytes_sent: u64 = 0; + let mut packets_sent: u64 = 0; + + let mut duration_receive: f64 = 0.0; + + let mut bytes_received: u64 = 0; + let mut packets_received: u64 = 0; + let mut packets_out_of_order: u64 = 0; + let mut packets_duplicated: u64 = 0; + let mut jitter_calculated = false; - let mut unbroken_sequence_count:u64 = 0; - let mut jitter_weight:f64 = 0.0; - + let mut unbroken_sequence_count: u64 = 0; + let mut jitter_weight: f64 = 0.0; + for (i, sr) in self.send_results.iter().enumerate() { if i < omit_seconds { continue; } - + duration_send += sr.duration as f64; - + bytes_sent += sr.bytes_sent; packets_sent += sr.packets_sent; } - + for (i, rr) in self.receive_results.iter().enumerate() { if i < omit_seconds { continue; } - + duration_receive += rr.duration as f64; - + bytes_received += rr.bytes_received; packets_received += rr.packets_received; packets_out_of_order += rr.packets_out_of_order; packets_duplicated += rr.packets_duplicated; - + if rr.jitter_seconds.is_some() { - jitter_weight += (rr.unbroken_sequence as f64) * (rr.jitter_seconds.unwrap() as f64); + jitter_weight += + (rr.unbroken_sequence as f64) * (rr.jitter_seconds.unwrap() as f64); unbroken_sequence_count += rr.unbroken_sequence; - + jitter_calculated = true; } } - + let mut summary = serde_json::json!({ "duration_send": duration_send, - + "bytes_sent": bytes_sent, "packets_sent": packets_sent, - - + + "duration_receive": duration_receive, - + "bytes_received": bytes_received, "packets_received": packets_received, "packets_lost": packets_sent - packets_received, @@ -649,10 +726,11 @@ impl StreamResults for UdpStreamResults { summary["framed_packet_size"] = serde_json::json!(bytes_sent / packets_sent); } if jitter_calculated { - summary["jitter_average"] = serde_json::json!(jitter_weight / (unbroken_sequence_count as f64)); + summary["jitter_average"] = + serde_json::json!(jitter_weight / (unbroken_sequence_count as f64)); summary["jitter_packets_consecutive"] = serde_json::json!(unbroken_sequence_count); } - + serde_json::json!({ "receive": self.receive_results.iter().map(|rr| rr.to_result_json()).collect::>(), "send": self.send_results.iter().map(|sr| sr.to_result_json()).collect::>(), @@ -661,27 +739,47 @@ impl StreamResults for UdpStreamResults { } } - pub trait TestResults { fn count_in_progress_streams(&self) -> u8; - fn mark_stream_done(&mut self, idx:&u8, success:bool); - + fn mark_stream_done(&mut self, idx: &u8, success: bool); + fn count_in_progress_streams_server(&self) -> u8; - fn mark_stream_done_server(&mut self, idx:&u8); - + fn mark_stream_done_server(&mut self, idx: &u8); + fn is_success(&self) -> bool; - - fn update_from_json(&mut self, value:serde_json::Value) -> BoxResult<()>; - - fn to_json(&self, omit_seconds:usize, upload_config:serde_json::Value, download_config:serde_json::Value, common_config:serde_json::Value, additional_config:serde_json::Value) -> serde_json::Value; - + + fn update_from_json(&mut self, value: serde_json::Value) -> BoxResult<()>; + + fn to_json( + &self, + omit_seconds: usize, + upload_config: serde_json::Value, + download_config: serde_json::Value, + common_config: serde_json::Value, + additional_config: serde_json::Value, + ) -> serde_json::Value; + //produces a pretty-printed JSON string with the test results - fn to_json_string(&self, omit_seconds:usize, upload_config:serde_json::Value, download_config:serde_json::Value, common_config:serde_json::Value, additional_config:serde_json::Value) -> String { - serde_json::to_string_pretty(&self.to_json(omit_seconds, upload_config, download_config, common_config, additional_config)).unwrap() + fn to_json_string( + &self, + omit_seconds: usize, + upload_config: serde_json::Value, + download_config: serde_json::Value, + common_config: serde_json::Value, + additional_config: serde_json::Value, + ) -> String { + serde_json::to_string_pretty(&self.to_json( + omit_seconds, + upload_config, + download_config, + common_config, + additional_config, + )) + .unwrap() } - + //produces test-results in tabular form - fn to_string(&self, bit:bool, omit_seconds:usize) -> String; + fn to_string(&self, bit: bool, omit_seconds: usize) -> String; } pub struct TcpTestResults { @@ -692,18 +790,21 @@ pub struct TcpTestResults { } impl TcpTestResults { pub fn new() -> TcpTestResults { - TcpTestResults{ + TcpTestResults { stream_results: HashMap::new(), pending_tests: HashSet::new(), failed_tests: HashSet::new(), server_tests_finished: HashSet::new(), } } - pub fn prepare_index(&mut self, idx:&u8) { - self.stream_results.insert(*idx, TcpStreamResults{ - receive_results: Vec::new(), - send_results: Vec::new(), - }); + pub fn prepare_index(&mut self, idx: &u8) { + self.stream_results.insert( + *idx, + TcpStreamResults { + receive_results: Vec::new(), + send_results: Vec::new(), + }, + ); self.pending_tests.insert(*idx); } } @@ -711,15 +812,15 @@ impl TestResults for TcpTestResults { fn count_in_progress_streams(&self) -> u8 { self.pending_tests.len() as u8 } - fn mark_stream_done(&mut self, idx:&u8, success:bool) { + fn mark_stream_done(&mut self, idx: &u8, success: bool) { self.pending_tests.remove(idx); if !success { self.failed_tests.insert(*idx); } } - + fn count_in_progress_streams_server(&self) -> u8 { - let mut count:u8 = 0; + let mut count: u8 = 0; for idx in self.stream_results.keys() { if !self.server_tests_finished.contains(idx) { count += 1; @@ -727,15 +828,15 @@ impl TestResults for TcpTestResults { } count } - fn mark_stream_done_server(&mut self, idx:&u8) { + fn mark_stream_done_server(&mut self, idx: &u8) { self.server_tests_finished.insert(*idx); } - + fn is_success(&self) -> bool { - self.pending_tests.len() == 0 && self.failed_tests.len() == 0 + self.pending_tests.is_empty() && self.failed_tests.is_empty() } - - fn update_from_json(&mut self, value:serde_json::Value) -> BoxResult<()> { + + fn update_from_json(&mut self, value: serde_json::Value) -> BoxResult<()> { match value.get("family") { Some(f) => match f.as_str() { Some(family) => match family { @@ -743,28 +844,48 @@ impl TestResults for TcpTestResults { Some(idx) => match idx.as_i64() { Some(idx64) => match self.stream_results.get_mut(&(idx64 as u8)) { Some(stream_results) => stream_results.update_from_json(value), - None => Err(Box::new(simple_error::simple_error!("stream-index {} is not a valid identifier", idx64))), + None => Err(Box::new(simple_error::simple_error!( + "stream-index {} is not a valid identifier", + idx64 + ))), }, - None => Err(Box::new(simple_error::simple_error!("stream-index is not an integer"))), + None => Err(Box::new(simple_error::simple_error!( + "stream-index is not an integer" + ))), }, - None => Err(Box::new(simple_error::simple_error!("no stream-index specified"))), + None => Err(Box::new(simple_error::simple_error!( + "no stream-index specified" + ))), }, - _ => Err(Box::new(simple_error::simple_error!("unsupported family for TCP stream-result: {}", family))), + _ => Err(Box::new(simple_error::simple_error!( + "unsupported family for TCP stream-result: {}", + family + ))), }, - None => Err(Box::new(simple_error::simple_error!("kind must be a string for TCP stream-result"))), + None => Err(Box::new(simple_error::simple_error!( + "kind must be a string for TCP stream-result" + ))), }, - None => Err(Box::new(simple_error::simple_error!("no kind specified for TCP stream-result"))), + None => Err(Box::new(simple_error::simple_error!( + "no kind specified for TCP stream-result" + ))), } } - - fn to_json(&self, omit_seconds:usize, upload_config:serde_json::Value, download_config:serde_json::Value, common_config:serde_json::Value, additional_config:serde_json::Value) -> serde_json::Value { - let mut duration_send:f64 = 0.0; - let mut bytes_sent:u64 = 0; - - let mut duration_receive:f64 = 0.0; - let mut bytes_received:u64 = 0; - - + + fn to_json( + &self, + omit_seconds: usize, + upload_config: serde_json::Value, + download_config: serde_json::Value, + common_config: serde_json::Value, + additional_config: serde_json::Value, + ) -> serde_json::Value { + let mut duration_send: f64 = 0.0; + let mut bytes_sent: u64 = 0; + + let mut duration_receive: f64 = 0.0; + let mut bytes_received: u64 = 0; + let mut streams = Vec::with_capacity(self.stream_results.len()); for (idx, stream) in self.stream_results.iter() { streams.push(serde_json::json!({ @@ -772,34 +893,34 @@ impl TestResults for TcpTestResults { "abandoned": self.pending_tests.contains(idx), "failed": self.failed_tests.contains(idx), })); - + for (i, sr) in stream.send_results.iter().enumerate() { if i < omit_seconds { continue; } - + duration_send += sr.duration as f64; bytes_sent += sr.bytes_sent; } - + for (i, rr) in stream.receive_results.iter().enumerate() { if i < omit_seconds { continue; } - + duration_receive += rr.duration as f64; bytes_received += rr.bytes_received; } } - + let summary = serde_json::json!({ "duration_send": duration_send, "bytes_sent": bytes_sent, - + "duration_receive": duration_receive, "bytes_received": bytes_received, }); - + serde_json::json!({ "config": { "upload": upload_config, @@ -812,81 +933,106 @@ impl TestResults for TcpTestResults { "success": self.is_success(), }) } - - fn to_string(&self, bit:bool, omit_seconds:usize) -> String { + + fn to_string(&self, bit: bool, omit_seconds: usize) -> String { let stream_count = self.stream_results.len(); let mut stream_send_durations = vec![0.0; stream_count]; let mut stream_receive_durations = vec![0.0; stream_count]; - - let mut duration_send:f64 = 0.0; - let mut bytes_sent:u64 = 0; - - let mut duration_receive:f64 = 0.0; - let mut bytes_received:u64 = 0; - + + let mut duration_send: f64 = 0.0; + let mut bytes_sent: u64 = 0; + + let mut duration_receive: f64 = 0.0; + let mut bytes_received: u64 = 0; + let mut sends_blocked = false; - + for (stream_idx, stream) in self.stream_results.values().enumerate() { for (i, sr) in stream.send_results.iter().enumerate() { if i < omit_seconds { continue; } - + duration_send += sr.duration as f64; stream_send_durations[stream_idx] += sr.duration as f64; - + bytes_sent += sr.bytes_sent; - + sends_blocked |= sr.sends_blocked > 0; } - + for (i, rr) in stream.receive_results.iter().enumerate() { if i < omit_seconds { continue; } - + duration_receive += rr.duration as f64; stream_receive_durations[stream_idx] += rr.duration as f64; - + bytes_received += rr.bytes_received; } } stream_send_durations.sort_by(|a, b| a.partial_cmp(b).unwrap()); stream_receive_durations.sort_by(|a, b| a.partial_cmp(b).unwrap()); - - let send_duration_divisor; - if duration_send == 0.0 { //avoid zerodiv, which can happen if all streams fail - send_duration_divisor = 1.0; + + let send_duration_divisor = if duration_send == 0.0 { + //avoid zerodiv, which can happen if all streams fail + 1.0 } else { - send_duration_divisor = duration_send; - } + duration_send + }; let send_bytes_per_second = bytes_sent as f64 / send_duration_divisor; let send_throughput = match bit { - true => format!("megabits/second: {:.3}", send_bytes_per_second / (1_000_000.00 / 8.0)), - false => format!("megabytes/second: {:.3}", send_bytes_per_second / 1_000_000.00), + true => format!( + "megabits/second: {:.3}", + send_bytes_per_second / (1_000_000.00 / 8.0) + ), + false => format!( + "megabytes/second: {:.3}", + send_bytes_per_second / 1_000_000.00 + ), }; let total_send_throughput = match bit { - true => format!("megabits/second: {:.3}", (send_bytes_per_second / (1_000_000.00 / 8.0)) * stream_count as f64), - false => format!("megabytes/second: {:.3}", (send_bytes_per_second / 1_000_000.00) * stream_count as f64), + true => format!( + "megabits/second: {:.3}", + (send_bytes_per_second / (1_000_000.00 / 8.0)) * stream_count as f64 + ), + false => format!( + "megabytes/second: {:.3}", + (send_bytes_per_second / 1_000_000.00) * stream_count as f64 + ), }; - - let receive_duration_divisor; - if duration_receive == 0.0 { //avoid zerodiv, which can happen if all streams fail - receive_duration_divisor = 1.0; + + let receive_duration_divisor = if duration_receive == 0.0 { + //avoid zerodiv, which can happen if all streams fail + 1.0 } else { - receive_duration_divisor = duration_receive; - } + duration_receive + }; let receive_bytes_per_second = bytes_received as f64 / receive_duration_divisor; let receive_throughput = match bit { - true => format!("megabits/second: {:.3}", receive_bytes_per_second / (1_000_000.00 / 8.0)), - false => format!("megabytes/second: {:.3}", receive_bytes_per_second / 1_000_000.00), + true => format!( + "megabits/second: {:.3}", + receive_bytes_per_second / (1_000_000.00 / 8.0) + ), + false => format!( + "megabytes/second: {:.3}", + receive_bytes_per_second / 1_000_000.00 + ), }; let total_receive_throughput = match bit { - true => format!("megabits/second: {:.3}", (receive_bytes_per_second / (1_000_000.00 / 8.0)) * stream_count as f64), - false => format!("megabytes/second: {:.3}", (receive_bytes_per_second / 1_000_000.00) * stream_count as f64), + true => format!( + "megabits/second: {:.3}", + (receive_bytes_per_second / (1_000_000.00 / 8.0)) * stream_count as f64 + ), + false => format!( + "megabytes/second: {:.3}", + (receive_bytes_per_second / 1_000_000.00) * stream_count as f64 + ), }; - - let mut output = format!("==========\n\ + + let mut output = format!( + "==========\n\ TCP send result over {:.2}s | streams: {}\n\ stream-average bytes per second: {:.3} | {}\n\ total bytes: {} | per second: {:.3} | {}\n\ @@ -894,21 +1040,28 @@ impl TestResults for TcpTestResults { TCP receive result over {:.2}s | streams: {}\n\ stream-average bytes per second: {:.3} | {}\n\ total bytes: {} | per second: {:.3} | {}", - stream_send_durations[stream_send_durations.len() - 1], stream_count, - send_bytes_per_second, send_throughput, - bytes_sent, send_bytes_per_second * stream_count as f64, total_send_throughput, - - stream_receive_durations[stream_receive_durations.len() - 1], stream_count, - receive_bytes_per_second, receive_throughput, - bytes_received, receive_bytes_per_second * stream_count as f64, total_receive_throughput, + stream_send_durations[stream_send_durations.len() - 1], + stream_count, + send_bytes_per_second, + send_throughput, + bytes_sent, + send_bytes_per_second * stream_count as f64, + total_send_throughput, + stream_receive_durations[stream_receive_durations.len() - 1], + stream_count, + receive_bytes_per_second, + receive_throughput, + bytes_received, + receive_bytes_per_second * stream_count as f64, + total_receive_throughput, ); if sends_blocked { - output.push_str(&format!("\nthroughput throttled by buffer limitations")); + output.push_str("\nthroughput throttled by buffer limitations"); } if !self.is_success() { - output.push_str(&format!("\nTESTING DID NOT COMPLETE SUCCESSFULLY")); + output.push_str("\nTESTING DID NOT COMPLETE SUCCESSFULLY"); } - + output } } @@ -921,18 +1074,21 @@ pub struct UdpTestResults { } impl UdpTestResults { pub fn new() -> UdpTestResults { - UdpTestResults{ + UdpTestResults { stream_results: HashMap::new(), pending_tests: HashSet::new(), failed_tests: HashSet::new(), server_tests_finished: HashSet::new(), } } - pub fn prepare_index(&mut self, idx:&u8) { - self.stream_results.insert(*idx, UdpStreamResults{ - receive_results: Vec::new(), - send_results: Vec::new(), - }); + pub fn prepare_index(&mut self, idx: &u8) { + self.stream_results.insert( + *idx, + UdpStreamResults { + receive_results: Vec::new(), + send_results: Vec::new(), + }, + ); self.pending_tests.insert(*idx); } } @@ -940,15 +1096,15 @@ impl TestResults for UdpTestResults { fn count_in_progress_streams(&self) -> u8 { self.pending_tests.len() as u8 } - fn mark_stream_done(&mut self, idx:&u8, success:bool) { + fn mark_stream_done(&mut self, idx: &u8, success: bool) { self.pending_tests.remove(idx); if !success { self.failed_tests.insert(*idx); } } - + fn count_in_progress_streams_server(&self) -> u8 { - let mut count:u8 = 0; + let mut count: u8 = 0; for idx in self.stream_results.keys() { if !self.server_tests_finished.contains(idx) { count += 1; @@ -956,15 +1112,15 @@ impl TestResults for UdpTestResults { } count } - fn mark_stream_done_server(&mut self, idx:&u8) { + fn mark_stream_done_server(&mut self, idx: &u8) { self.server_tests_finished.insert(*idx); } - + fn is_success(&self) -> bool { - self.pending_tests.len() == 0 && self.failed_tests.len() == 0 + self.pending_tests.is_empty() && self.failed_tests.is_empty() } - - fn update_from_json(&mut self, value:serde_json::Value) -> BoxResult<()> { + + fn update_from_json(&mut self, value: serde_json::Value) -> BoxResult<()> { match value.get("family") { Some(f) => match f.as_str() { Some(family) => match family { @@ -972,39 +1128,58 @@ impl TestResults for UdpTestResults { Some(idx) => match idx.as_i64() { Some(idx64) => match self.stream_results.get_mut(&(idx64 as u8)) { Some(stream_results) => stream_results.update_from_json(value), - None => Err(Box::new(simple_error::simple_error!("stream-index {} is not a valid identifier", idx64))), + None => Err(Box::new(simple_error::simple_error!( + "stream-index {} is not a valid identifier", + idx64 + ))), }, - None => Err(Box::new(simple_error::simple_error!("stream-index is not an integer"))), + None => Err(Box::new(simple_error::simple_error!( + "stream-index is not an integer" + ))), }, - None => Err(Box::new(simple_error::simple_error!("no stream-index specified"))), + None => Err(Box::new(simple_error::simple_error!( + "no stream-index specified" + ))), }, - _ => Err(Box::new(simple_error::simple_error!("unsupported family for UDP stream-result: {}", family))), + _ => Err(Box::new(simple_error::simple_error!( + "unsupported family for UDP stream-result: {}", + family + ))), }, - None => Err(Box::new(simple_error::simple_error!("kind must be a string for UDP stream-result"))), + None => Err(Box::new(simple_error::simple_error!( + "kind must be a string for UDP stream-result" + ))), }, - None => Err(Box::new(simple_error::simple_error!("no kind specified for UDP stream-result"))), + None => Err(Box::new(simple_error::simple_error!( + "no kind specified for UDP stream-result" + ))), } } - - fn to_json(&self, omit_seconds:usize, upload_config:serde_json::Value, download_config:serde_json::Value, common_config:serde_json::Value, additional_config:serde_json::Value) -> serde_json::Value { - let mut duration_send:f64 = 0.0; - - let mut bytes_sent:u64 = 0; - let mut packets_sent:u64 = 0; - - - let mut duration_receive:f64 = 0.0; - - let mut bytes_received:u64 = 0; - let mut packets_received:u64 = 0; - let mut packets_out_of_order:u64 = 0; - let mut packets_duplicated:u64 = 0; - + + fn to_json( + &self, + omit_seconds: usize, + upload_config: serde_json::Value, + download_config: serde_json::Value, + common_config: serde_json::Value, + additional_config: serde_json::Value, + ) -> serde_json::Value { + let mut duration_send: f64 = 0.0; + + let mut bytes_sent: u64 = 0; + let mut packets_sent: u64 = 0; + + let mut duration_receive: f64 = 0.0; + + let mut bytes_received: u64 = 0; + let mut packets_received: u64 = 0; + let mut packets_out_of_order: u64 = 0; + let mut packets_duplicated: u64 = 0; + let mut jitter_calculated = false; - let mut unbroken_sequence_count:u64 = 0; - let mut jitter_weight:f64 = 0.0; - - + let mut unbroken_sequence_count: u64 = 0; + let mut jitter_weight: f64 = 0.0; + let mut streams = Vec::with_capacity(self.stream_results.len()); for (idx, stream) in self.stream_results.iter() { streams.push(serde_json::json!({ @@ -1012,48 +1187,49 @@ impl TestResults for UdpTestResults { "abandoned": self.pending_tests.contains(idx), "failed": self.failed_tests.contains(idx), })); - + for (i, sr) in stream.send_results.iter().enumerate() { if i < omit_seconds { continue; } - + duration_send += sr.duration as f64; - + bytes_sent += sr.bytes_sent; packets_sent += sr.packets_sent; } - + for (i, rr) in stream.receive_results.iter().enumerate() { if i < omit_seconds { continue; } - + duration_receive += rr.duration as f64; - + bytes_received += rr.bytes_received; packets_received += rr.packets_received; packets_out_of_order += rr.packets_out_of_order; packets_duplicated += rr.packets_duplicated; - + if rr.jitter_seconds.is_some() { - jitter_weight += (rr.unbroken_sequence as f64) * (rr.jitter_seconds.unwrap() as f64); + jitter_weight += + (rr.unbroken_sequence as f64) * (rr.jitter_seconds.unwrap() as f64); unbroken_sequence_count += rr.unbroken_sequence; - + jitter_calculated = true; } } } - + let mut summary = serde_json::json!({ "duration_send": duration_send, - + "bytes_sent": bytes_sent, "packets_sent": packets_sent, - - + + "duration_receive": duration_receive, - + "bytes_received": bytes_received, "packets_received": packets_received, "packets_lost": packets_sent - packets_received, @@ -1064,10 +1240,11 @@ impl TestResults for UdpTestResults { summary["framed_packet_size"] = serde_json::json!(bytes_sent / packets_sent); } if jitter_calculated { - summary["jitter_average"] = serde_json::json!(jitter_weight / (unbroken_sequence_count as f64)); + summary["jitter_average"] = + serde_json::json!(jitter_weight / (unbroken_sequence_count as f64)); summary["jitter_packets_consecutive"] = serde_json::json!(unbroken_sequence_count); } - + serde_json::json!({ "config": { "upload": upload_config, @@ -1080,109 +1257,133 @@ impl TestResults for UdpTestResults { "success": self.is_success(), }) } - - fn to_string(&self, bit:bool, omit_seconds:usize) -> String { + + fn to_string(&self, bit: bool, omit_seconds: usize) -> String { let stream_count = self.stream_results.len(); let mut stream_send_durations = vec![0.0; stream_count]; let mut stream_receive_durations = vec![0.0; stream_count]; - - let mut duration_send:f64 = 0.0; - - let mut bytes_sent:u64 = 0; - let mut packets_sent:u64 = 0; - - - let mut duration_receive:f64 = 0.0; - - let mut bytes_received:u64 = 0; - let mut packets_received:u64 = 0; - let mut packets_out_of_order:u64 = 0; - let mut packets_duplicated:u64 = 0; - + + let mut duration_send: f64 = 0.0; + + let mut bytes_sent: u64 = 0; + let mut packets_sent: u64 = 0; + + let mut duration_receive: f64 = 0.0; + + let mut bytes_received: u64 = 0; + let mut packets_received: u64 = 0; + let mut packets_out_of_order: u64 = 0; + let mut packets_duplicated: u64 = 0; + let mut jitter_calculated = false; - let mut unbroken_sequence_count:u64 = 0; - let mut jitter_weight:f64 = 0.0; - + let mut unbroken_sequence_count: u64 = 0; + let mut jitter_weight: f64 = 0.0; + let mut sends_blocked = false; - + for (stream_idx, stream) in self.stream_results.values().enumerate() { for (i, sr) in stream.send_results.iter().enumerate() { if i < omit_seconds { continue; } - + duration_send += sr.duration as f64; stream_send_durations[stream_idx] += sr.duration as f64; - + bytes_sent += sr.bytes_sent; packets_sent += sr.packets_sent; - + sends_blocked |= sr.sends_blocked > 0; } - + for (i, rr) in stream.receive_results.iter().enumerate() { if i < omit_seconds { continue; } - + duration_receive += rr.duration as f64; stream_receive_durations[stream_idx] += rr.duration as f64; - + bytes_received += rr.bytes_received; packets_received += rr.packets_received; packets_out_of_order += rr.packets_out_of_order; packets_duplicated += rr.packets_duplicated; - + if rr.jitter_seconds.is_some() { - jitter_weight += (rr.unbroken_sequence as f64) * (rr.jitter_seconds.unwrap() as f64); + jitter_weight += + (rr.unbroken_sequence as f64) * (rr.jitter_seconds.unwrap() as f64); unbroken_sequence_count += rr.unbroken_sequence; - + jitter_calculated = true; } } } stream_send_durations.sort_by(|a, b| a.partial_cmp(b).unwrap()); stream_receive_durations.sort_by(|a, b| a.partial_cmp(b).unwrap()); - - let send_duration_divisor; - if duration_send == 0.0 { //avoid zerodiv, which can happen if all streams fail - send_duration_divisor = 1.0; + + let send_duration_divisor = if duration_send == 0.0 { + //avoid zerodiv, which can happen if all streams fail + 1.0 } else { - send_duration_divisor = duration_send; - } + duration_send + }; let send_bytes_per_second = bytes_sent as f64 / send_duration_divisor; let send_throughput = match bit { - true => format!("megabits/second: {:.3}", send_bytes_per_second / (1_000_000.00 / 8.0)), - false => format!("megabytes/second: {:.3}", send_bytes_per_second / 1_000_000.00), + true => format!( + "megabits/second: {:.3}", + send_bytes_per_second / (1_000_000.00 / 8.0) + ), + false => format!( + "megabytes/second: {:.3}", + send_bytes_per_second / 1_000_000.00 + ), }; let total_send_throughput = match bit { - true => format!("megabits/second: {:.3}", (send_bytes_per_second / (1_000_000.00 / 8.0)) * stream_count as f64), - false => format!("megabytes/second: {:.3}", (send_bytes_per_second / 1_000_000.00) * stream_count as f64), + true => format!( + "megabits/second: {:.3}", + (send_bytes_per_second / (1_000_000.00 / 8.0)) * stream_count as f64 + ), + false => format!( + "megabytes/second: {:.3}", + (send_bytes_per_second / 1_000_000.00) * stream_count as f64 + ), }; - - let receive_duration_divisor; - if duration_receive == 0.0 { //avoid zerodiv, which can happen if all streams fail - receive_duration_divisor = 1.0; + + let receive_duration_divisor = if duration_receive == 0.0 { + //avoid zerodiv, which can happen if all streams fail + 1.0 } else { - receive_duration_divisor = duration_receive; - } + duration_receive + }; let receive_bytes_per_second = bytes_received as f64 / receive_duration_divisor; let receive_throughput = match bit { - true => format!("megabits/second: {:.3}", receive_bytes_per_second / (1_000_000.00 / 8.0)), - false => format!("megabytes/second: {:.3}", receive_bytes_per_second / 1_000_000.00), + true => format!( + "megabits/second: {:.3}", + receive_bytes_per_second / (1_000_000.00 / 8.0) + ), + false => format!( + "megabytes/second: {:.3}", + receive_bytes_per_second / 1_000_000.00 + ), }; let total_receive_throughput = match bit { - true => format!("megabits/second: {:.3}", (receive_bytes_per_second / (1_000_000.00 / 8.0)) * stream_count as f64), - false => format!("megabytes/second: {:.3}", (receive_bytes_per_second / 1_000_000.00) * stream_count as f64), + true => format!( + "megabits/second: {:.3}", + (receive_bytes_per_second / (1_000_000.00 / 8.0)) * stream_count as f64 + ), + false => format!( + "megabytes/second: {:.3}", + (receive_bytes_per_second / 1_000_000.00) * stream_count as f64 + ), }; - + let packets_lost = packets_sent - (packets_received - packets_duplicated); - let packets_sent_divisor; - if packets_sent == 0 { //avoid zerodiv, which can happen if all streams fail - packets_sent_divisor = 1.0 + let packets_sent_divisor = if packets_sent == 0 { + //avoid zerodiv, which can happen if all streams fail + 1.0 } else { - packets_sent_divisor = packets_sent as f64; - } + packets_sent as f64 + }; let mut output = format!("==========\n\ UDP send result over {:.2}s | streams: {}\n\ stream-average bytes per second: {:.3} | {}\n\ @@ -1197,22 +1398,25 @@ impl TestResults for UdpTestResults { send_bytes_per_second, send_throughput, bytes_sent, send_bytes_per_second * stream_count as f64, total_send_throughput, packets_sent, (packets_sent as f64 / send_duration_divisor) * stream_count as f64, - stream_receive_durations[stream_receive_durations.len() - 1], stream_count, receive_bytes_per_second, receive_throughput, bytes_received, receive_bytes_per_second * stream_count as f64, total_receive_throughput, packets_received, packets_lost, (packets_lost as f64 / packets_sent_divisor) * 100.0, packets_out_of_order, packets_duplicated, (packets_received as f64 / receive_duration_divisor) * stream_count as f64, ); if jitter_calculated { - output.push_str(&format!("\njitter: {:.6}s over {} consecutive packets", jitter_weight / (unbroken_sequence_count as f64), unbroken_sequence_count)); + output.push_str(&format!( + "\njitter: {:.6}s over {} consecutive packets", + jitter_weight / (unbroken_sequence_count as f64), + unbroken_sequence_count + )); } if sends_blocked { - output.push_str(&format!("\nthroughput throttled by buffer limitations")); + output.push_str("\nthroughput throttled by buffer limitations"); } if !self.is_success() { - output.push_str(&format!("\nTESTING DID NOT COMPLETE SUCCESSFULLY")); + output.push_str("\nTESTING DID NOT COMPLETE SUCCESSFULLY"); } - + output } } diff --git a/src/server.rs b/src/server.rs index 7256226..dd2ae56 100644 --- a/src/server.rs +++ b/src/server.rs @@ -1,19 +1,19 @@ /* * Copyright (C) 2021 Evtech Solutions, Ltd., dba 3D-P * Copyright (C) 2021 Neil Tallim - * + * * This file is part of rperf. - * + * * rperf is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. - * + * * rperf is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. - * + * * You should have received a copy of the GNU General Public License * along with rperf. If not, see . */ @@ -22,91 +22,116 @@ use std::error::Error; use std::io; use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, Shutdown, SocketAddr}; use std::sync::atomic::{AtomicBool, AtomicU16, Ordering}; -use std::sync::{Arc, Mutex}; use std::sync::mpsc::channel; +use std::sync::{Arc, Mutex}; use std::thread; -use std::time::{Duration}; +use std::time::Duration; use clap::ArgMatches; use mio::net::{TcpListener, TcpStream}; -use mio::{Events, Ready, Poll, PollOpt, Token}; +use mio::{Events, Poll, PollOpt, Ready, Token}; use crate::protocol::communication::{receive, send, KEEPALIVE_DURATION}; -use crate::protocol::messaging::{ - prepare_connect, prepare_connect_ready, -}; +use crate::protocol::messaging::{prepare_connect, prepare_connect_ready}; -use crate::stream::TestStream; use crate::stream::tcp; use crate::stream::udp; +use crate::stream::TestStream; -type BoxResult = Result>; +type BoxResult = Result>; -const POLL_TIMEOUT:Duration = Duration::from_millis(500); +const POLL_TIMEOUT: Duration = Duration::from_millis(500); /// when false, the system is shutting down -static ALIVE:AtomicBool = AtomicBool::new(true); +static ALIVE: AtomicBool = AtomicBool::new(true); /// a count of connected clients -static CLIENTS:AtomicU16 = AtomicU16::new(0); - +static CLIENTS: AtomicU16 = AtomicU16::new(0); fn handle_client( - stream:&mut TcpStream, - cpu_affinity_manager:Arc>, - tcp_port_pool:Arc>, - udp_port_pool:Arc>, + stream: &mut TcpStream, + cpu_affinity_manager: Arc>, + tcp_port_pool: Arc>, + udp_port_pool: Arc>, ) -> BoxResult<()> { let mut started = false; let peer_addr = stream.peer_addr()?; - - + //scaffolding to track and relay the streams and stream-results associated with this client - let mut parallel_streams:Vec>> = Vec::new(); + let mut parallel_streams: Vec>> = Vec::new(); let mut parallel_streams_joinhandles = Vec::new(); - let (results_tx, results_rx):(std::sync::mpsc::Sender>, std::sync::mpsc::Receiver>) = channel(); - + + let (results_tx, results_rx): ( + std::sync::mpsc::Sender, + std::sync::mpsc::Receiver, + ) = channel(); + //a closure used to pass results from stream-handlers to the client-communication stream let mut forwarding_send_stream = stream.try_clone()?; let mut results_handler = || -> BoxResult<()> { - loop { //drain all results every time this closer is invoked - match results_rx.try_recv() { //if there's something to forward, write it to the client - Ok(result) => { - send(&mut forwarding_send_stream, &result.to_json())?; - }, - Err(_) => break, //whether it's empty or disconnected, there's nothing to do - } + // drain all results every time this closer is invoked + while let Ok(result) = results_rx.try_recv() { + // if there's something to forward, write it to the client + send(&mut forwarding_send_stream, &result.to_json())?; } Ok(()) }; - - + //server operations are entirely driven by client-signalling, making this a (simple) state-machine while is_alive() { let payload = receive(stream, is_alive, &mut results_handler)?; match payload.get("kind") { Some(kind) => { match kind.as_str().unwrap() { - "configuration" => { //we either need to connect streams to the client or prepare to receive connections - if payload.get("role").unwrap_or(&serde_json::json!("download")).as_str().unwrap() == "download" { - log::info!("[{}] running in forward-mode: server will be receiving data", &peer_addr); - - let stream_count = payload.get("streams").unwrap_or(&serde_json::json!(1)).as_i64().unwrap(); + "configuration" => { + //we either need to connect streams to the client or prepare to receive connections + if payload + .get("role") + .unwrap_or(&serde_json::json!("download")) + .as_str() + .unwrap() + == "download" + { + log::info!( + "[{}] running in forward-mode: server will be receiving data", + &peer_addr + ); + + let stream_count = payload + .get("streams") + .unwrap_or(&serde_json::json!(1)) + .as_i64() + .unwrap(); //since we're receiving data, we're also responsible for letting the client know where to send it let mut stream_ports = Vec::with_capacity(stream_count as usize); - - if payload.get("family").unwrap_or(&serde_json::json!("tcp")).as_str().unwrap() == "udp" { - log::info!("[{}] preparing for UDP test with {} streams...", &peer_addr, stream_count); - + + if payload + .get("family") + .unwrap_or(&serde_json::json!("tcp")) + .as_str() + .unwrap() + == "udp" + { + log::info!( + "[{}] preparing for UDP test with {} streams...", + &peer_addr, + stream_count + ); + let mut c_udp_port_pool = udp_port_pool.lock().unwrap(); - + let test_definition = udp::UdpTestDefinition::new(&payload)?; for stream_idx in 0..stream_count { - log::debug!("[{}] preparing UDP-receiver for stream {}...", &peer_addr, stream_idx); + log::debug!( + "[{}] preparing UDP-receiver for stream {}...", + &peer_addr, + stream_idx + ); let test = udp::receiver::UdpReceiver::new( - test_definition.clone(), &(stream_idx as u8), + test_definition.clone(), + &(stream_idx as u8), &mut c_udp_port_pool, &peer_addr.ip(), &(payload["receive_buffer"].as_i64().unwrap() as usize), @@ -114,16 +139,26 @@ fn handle_client( stream_ports.push(test.get_port()?); parallel_streams.push(Arc::new(Mutex::new(test))); } - } else { //TCP - log::info!("[{}] preparing for TCP test with {} streams...", &peer_addr, stream_count); - + } else { + //TCP + log::info!( + "[{}] preparing for TCP test with {} streams...", + &peer_addr, + stream_count + ); + let mut c_tcp_port_pool = tcp_port_pool.lock().unwrap(); - + let test_definition = tcp::TcpTestDefinition::new(&payload)?; for stream_idx in 0..stream_count { - log::debug!("[{}] preparing TCP-receiver for stream {}...", &peer_addr, stream_idx); + log::debug!( + "[{}] preparing TCP-receiver for stream {}...", + &peer_addr, + stream_idx + ); let test = tcp::receiver::TcpReceiver::new( - test_definition.clone(), &(stream_idx as u8), + test_definition.clone(), + &(stream_idx as u8), &mut c_tcp_port_pool, &peer_addr.ip(), &(payload["receive_buffer"].as_i64().unwrap() as usize), @@ -132,38 +167,81 @@ fn handle_client( parallel_streams.push(Arc::new(Mutex::new(test))); } } - + //let the client know we're ready to receive the connection; stream-ports are in stream-index order send(stream, &prepare_connect(&stream_ports))?; - } else { //upload - log::info!("[{}] running in reverse-mode: server will be uploading data", &peer_addr); - - let stream_ports = payload.get("stream_ports").unwrap().as_array().unwrap(); - - if payload.get("family").unwrap_or(&serde_json::json!("tcp")).as_str().unwrap() == "udp" { - log::info!("[{}] preparing for UDP test with {} streams...", &peer_addr, stream_ports.len()); - + } else { + //upload + log::info!( + "[{}] running in reverse-mode: server will be uploading data", + &peer_addr + ); + + let stream_ports = + payload.get("stream_ports").unwrap().as_array().unwrap(); + + if payload + .get("family") + .unwrap_or(&serde_json::json!("tcp")) + .as_str() + .unwrap() + == "udp" + { + log::info!( + "[{}] preparing for UDP test with {} streams...", + &peer_addr, + stream_ports.len() + ); + let test_definition = udp::UdpTestDefinition::new(&payload)?; for (stream_idx, port) in stream_ports.iter().enumerate() { - log::debug!("[{}] preparing UDP-sender for stream {}...", &peer_addr, stream_idx); + log::debug!( + "[{}] preparing UDP-sender for stream {}...", + &peer_addr, + stream_idx + ); let test = udp::sender::UdpSender::new( - test_definition.clone(), &(stream_idx as u8), - &0, &peer_addr.ip(), &(port.as_i64().unwrap_or(0) as u16), - &(payload.get("duration").unwrap_or(&serde_json::json!(0.0)).as_f64().unwrap() as f32), - &(payload.get("send_interval").unwrap_or(&serde_json::json!(1.0)).as_f64().unwrap() as f32), + test_definition.clone(), + &(stream_idx as u8), + &0, + &peer_addr.ip(), + &(port.as_i64().unwrap_or(0) as u16), + &(payload + .get("duration") + .unwrap_or(&serde_json::json!(0.0)) + .as_f64() + .unwrap() + as f32), + &(payload + .get("send_interval") + .unwrap_or(&serde_json::json!(1.0)) + .as_f64() + .unwrap() + as f32), &(payload["send_buffer"].as_i64().unwrap() as usize), )?; parallel_streams.push(Arc::new(Mutex::new(test))); } - } else { //TCP - log::info!("[{}] preparing for TCP test with {} streams...", &peer_addr, stream_ports.len()); - + } else { + //TCP + log::info!( + "[{}] preparing for TCP test with {} streams...", + &peer_addr, + stream_ports.len() + ); + let test_definition = tcp::TcpTestDefinition::new(&payload)?; for (stream_idx, port) in stream_ports.iter().enumerate() { - log::debug!("[{}] preparing TCP-sender for stream {}...", &peer_addr, stream_idx); + log::debug!( + "[{}] preparing TCP-sender for stream {}...", + &peer_addr, + stream_idx + ); let test = tcp::sender::TcpSender::new( - test_definition.clone(), &(stream_idx as u8), - &peer_addr.ip(), &(port.as_i64().unwrap() as u16), + test_definition.clone(), + &(stream_idx as u8), + &peer_addr.ip(), + &(port.as_i64().unwrap() as u16), &(payload["duration"].as_f64().unwrap() as f32), &(payload["send_interval"].as_f64().unwrap() as f32), &(payload["send_buffer"].as_i64().unwrap() as usize), @@ -172,42 +250,59 @@ fn handle_client( parallel_streams.push(Arc::new(Mutex::new(test))); } } - + //let the client know we're ready to begin send(stream, &prepare_connect_ready())?; } - }, - "begin" => { //the client has indicated that testing can begin - if !started { //a simple guard to protect against reinitialisaion - for (stream_idx, parallel_stream) in parallel_streams.iter_mut().enumerate() { - log::info!("[{}] beginning execution of stream {}...", &peer_addr, stream_idx); - let c_ps = Arc::clone(¶llel_stream); + } + "begin" => { + //the client has indicated that testing can begin + if !started { + //a simple guard to protect against reinitialisaion + for (stream_idx, parallel_stream) in + parallel_streams.iter_mut().enumerate() + { + log::info!( + "[{}] beginning execution of stream {}...", + &peer_addr, + stream_idx + ); + let c_ps = Arc::clone(parallel_stream); let c_results_tx = results_tx.clone(); let c_cam = cpu_affinity_manager.clone(); let handle = thread::spawn(move || { - { //set CPU affinity, if enabled + { + //set CPU affinity, if enabled c_cam.lock().unwrap().set_affinity(); } loop { let mut test = c_ps.lock().unwrap(); - log::debug!("[{}] beginning test-interval for stream {}", &peer_addr, test.get_idx()); + log::debug!( + "[{}] beginning test-interval for stream {}", + &peer_addr, + test.get_idx() + ); match test.run_interval() { Some(interval_result) => match interval_result { Ok(ir) => match c_results_tx.send(ir) { Ok(_) => (), Err(e) => { log::error!("[{}] unable to process interval-result: {}", &peer_addr, e); - break - }, + break; + } }, Err(e) => { - log::error!("[{}] unable to process stream: {}", peer_addr, e); + log::error!( + "[{}] unable to process stream: {}", + peer_addr, + e + ); match c_results_tx.send(Box::new(crate::protocol::results::ServerFailedResult{stream_idx: test.get_idx()})) { Ok(_) => (), Err(e) => log::error!("[{}] unable to report interval-failed-result: {}", &peer_addr, e), } break; - }, + } }, None => { match c_results_tx.send(Box::new(crate::protocol::results::ServerDoneResult{stream_idx: test.get_idx()})) { @@ -215,43 +310,48 @@ fn handle_client( Err(e) => log::error!("[{}] unable to report interval-done-result: {}", &peer_addr, e), } break; - }, + } } } }); parallel_streams_joinhandles.push(handle); } started = true; - } else { //this can only happen in case of malicious action + } else { + //this can only happen in case of malicious action log::error!("[{}] duplicate begin-signal", &peer_addr); break; } - }, - "end" => { //the client has indicated that testing is done; stop cleanly + } + "end" => { + //the client has indicated that testing is done; stop cleanly log::info!("[{}] end of testing signaled", &peer_addr); break; - }, + } _ => { log::error!("[{}] invalid data", &peer_addr); break; - }, + } } - }, + } None => { log::error!("[{}] invalid data", &peer_addr); break; - }, + } } } - + log::debug!("[{}] stopping any still-in-progress streams", &peer_addr); for ps in parallel_streams.iter_mut() { let mut stream = match (*ps).lock() { Ok(guard) => guard, Err(poisoned) => { - log::error!("[{}] a stream-handler was poisoned; this indicates some sort of logic error", &peer_addr); + log::error!( + "[{}] a stream-handler was poisoned; this indicates some sort of logic error", + &peer_addr + ); poisoned.into_inner() - }, + } }; stream.stop(); } @@ -262,7 +362,7 @@ fn handle_client( Err(e) => log::error!("[{}] error in parallel stream: {:?}", &peer_addr, e), } } - + Ok(()) } @@ -273,7 +373,7 @@ struct ClientThreadMonitor { impl Drop for ClientThreadMonitor { fn drop(&mut self) { CLIENTS.fetch_sub(1, Ordering::Relaxed); - if thread::panicking(){ + if thread::panicking() { log::warn!("{} disconnecting due to panic", self.client_address); } else { log::info!("{} disconnected", self.client_address); @@ -281,7 +381,7 @@ impl Drop for ClientThreadMonitor { } } -pub fn serve(args:ArgMatches) -> BoxResult<()> { +pub fn serve(args: ArgMatches) -> BoxResult<()> { //config-parsing and pre-connection setup let tcp_port_pool = Arc::new(Mutex::new(tcp::receiver::TcpPortPool::new( args.value_of("tcp_port_pool").unwrap().to_string(), @@ -291,85 +391,95 @@ pub fn serve(args:ArgMatches) -> BoxResult<()> { args.value_of("udp_port_pool").unwrap().to_string(), args.value_of("udp6_port_pool").unwrap().to_string(), ))); - - let cpu_affinity_manager = Arc::new(Mutex::new(crate::utils::cpu_affinity::CpuAffinityManager::new(args.value_of("affinity").unwrap())?)); - - let client_limit:u16 = args.value_of("client_limit").unwrap().parse()?; + + let cpu_affinity_manager = Arc::new(Mutex::new( + crate::utils::cpu_affinity::CpuAffinityManager::new(args.value_of("affinity").unwrap())?, + )); + + let client_limit: u16 = args.value_of("client_limit").unwrap().parse()?; if client_limit > 0 { log::debug!("limiting service to {} concurrent clients", client_limit); } - + //start listening for connections - let port:u16 = args.value_of("port").unwrap().parse()?; - let mut listener:TcpListener; - if args.is_present("version6") { - listener = TcpListener::bind(&SocketAddr::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), port)).expect(format!("failed to bind TCP socket, port {}", port).as_str()); + let port: u16 = args.value_of("port").unwrap().parse()?; + let listener: TcpListener = if args.is_present("version6") { + TcpListener::bind(&SocketAddr::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), port)) + .unwrap_or_else(|_| panic!("failed to bind TCP socket, port {}", port)) } else { - listener = TcpListener::bind(&SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), port)).expect(format!("failed to bind TCP socket, port {}", port).as_str()); - } + TcpListener::bind(&SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), port)) + .unwrap_or_else(|_| panic!("failed to bind TCP socket, port {}", port)) + }; log::info!("server listening on {}", listener.local_addr()?); - + let mio_token = Token(0); let poll = Poll::new()?; - poll.register( - &mut listener, - mio_token, - Ready::readable(), - PollOpt::edge(), - )?; + poll.register(&listener, mio_token, Ready::readable(), PollOpt::edge())?; let mut events = Events::with_capacity(32); - + while is_alive() { poll.poll(&mut events, Some(POLL_TIMEOUT))?; for event in events.iter() { - match event.token() { - _ => loop { - match listener.accept() { - Ok((mut stream, address)) => { - log::info!("connection from {}", address); - - stream.set_nodelay(true).expect("cannot disable Nagle's algorithm"); - stream.set_keepalive(Some(KEEPALIVE_DURATION)).expect("unable to set TCP keepalive"); - - let client_count = CLIENTS.fetch_add(1, Ordering::Relaxed) + 1; - if client_limit > 0 && client_count > client_limit { - log::warn!("client-limit ({}) reached; disconnecting {}...", client_limit, address.to_string()); + event.token(); + loop { + match listener.accept() { + Ok((mut stream, address)) => { + log::info!("connection from {}", address); + + stream + .set_nodelay(true) + .expect("cannot disable Nagle's algorithm"); + stream + .set_keepalive(Some(KEEPALIVE_DURATION)) + .expect("unable to set TCP keepalive"); + + let client_count = CLIENTS.fetch_add(1, Ordering::Relaxed) + 1; + if client_limit > 0 && client_count > client_limit { + log::warn!( + "client-limit ({}) reached; disconnecting {}...", + client_limit, + address.to_string() + ); + stream.shutdown(Shutdown::Both).unwrap_or_default(); + CLIENTS.fetch_sub(1, Ordering::Relaxed); + } else { + let c_cam = cpu_affinity_manager.clone(); + let c_tcp_port_pool = tcp_port_pool.clone(); + let c_udp_port_pool = udp_port_pool.clone(); + let thread_builder = thread::Builder::new().name(address.to_string()); + thread_builder.spawn(move || { + //ensure the client is accounted-for even if the handler panics + let _client_thread_monitor = ClientThreadMonitor { + client_address: address.to_string(), + }; + + match handle_client( + &mut stream, + c_cam, + c_tcp_port_pool, + c_udp_port_pool, + ) { + Ok(_) => (), + Err(e) => log::error!("error in client-handler: {}", e), + } + + //in the event of panic, this will happen when the stream is dropped stream.shutdown(Shutdown::Both).unwrap_or_default(); - CLIENTS.fetch_sub(1, Ordering::Relaxed); - } else { - let c_cam = cpu_affinity_manager.clone(); - let c_tcp_port_pool = tcp_port_pool.clone(); - let c_udp_port_pool = udp_port_pool.clone(); - let thread_builder = thread::Builder::new() - .name(address.to_string().into()); - thread_builder.spawn(move || { - //ensure the client is accounted-for even if the handler panics - let _client_thread_monitor = ClientThreadMonitor{ - client_address: address.to_string(), - }; - - match handle_client(&mut stream, c_cam, c_tcp_port_pool, c_udp_port_pool) { - Ok(_) => (), - Err(e) => log::error!("error in client-handler: {}", e), - } - - //in the event of panic, this will happen when the stream is dropped - stream.shutdown(Shutdown::Both).unwrap_or_default(); - })?; - } - }, - Err(ref e) if e.kind() == io::ErrorKind::WouldBlock => { //nothing to do - break; - }, - Err(e) => { - return Err(Box::new(e)); - }, + })?; + } + } + Err(ref e) if e.kind() == io::ErrorKind::WouldBlock => { + //nothing to do + break; } - }, + Err(e) => { + return Err(Box::new(e)); + } + } } } } - + //wait until all clients have been disconnected loop { let clients_count = CLIENTS.load(Ordering::Relaxed); diff --git a/src/stream/mod.rs b/src/stream/mod.rs index 62e768a..30eab74 100644 --- a/src/stream/mod.rs +++ b/src/stream/mod.rs @@ -1,37 +1,37 @@ /* * Copyright (C) 2021 Evtech Solutions, Ltd., dba 3D-P * Copyright (C) 2021 Neil Tallim - * + * * This file is part of rperf. - * + * * rperf is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. - * + * * rperf is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. - * + * * You should have received a copy of the GNU General Public License * along with rperf. If not, see . */ - + pub mod tcp; pub mod udp; use std::error::Error; -type BoxResult = Result>; +type BoxResult = Result>; -pub const INTERVAL:std::time::Duration = std::time::Duration::from_secs(1); +pub const INTERVAL: std::time::Duration = std::time::Duration::from_secs(1); /// tests in this system are self-contained iterator-likes, where run_interval() may be invoked multiple times /// until it returns None, indicating that the test has run its course; each invocation blocks for up to approximately /// INTERVAL while gathering data. pub trait TestStream { /// gather data; returns None when the test is over - fn run_interval(&mut self) -> Option>>; + fn run_interval(&mut self) -> Option>; /// return the port associated with the test-stream; this may vary over the test's lifetime fn get_port(&self) -> BoxResult; /// returns the index of the test, used to match client and server data @@ -40,7 +40,7 @@ pub trait TestStream { fn stop(&mut self); } -fn parse_port_spec(port_spec:String) -> Vec { +fn parse_port_spec(port_spec: String) -> Vec { let mut ports = Vec::::new(); if !port_spec.is_empty() { for range in port_spec.split(',') { @@ -48,7 +48,7 @@ fn parse_port_spec(port_spec:String) -> Vec { let mut range_spec = range.split('-'); let range_first = range_spec.next().unwrap().parse::().unwrap(); let range_last = range_spec.last().unwrap().parse::().unwrap(); - + for port in range_first..=range_last { ports.push(port); } @@ -56,9 +56,9 @@ fn parse_port_spec(port_spec:String) -> Vec { ports.push(range.parse::().unwrap()); } } - + ports.sort(); } - - return ports; + + ports } diff --git a/src/stream/tcp.rs b/src/stream/tcp.rs index 721ed4a..308e427 100644 --- a/src/stream/tcp.rs +++ b/src/stream/tcp.rs @@ -1,19 +1,19 @@ /* * Copyright (C) 2021 Evtech Solutions, Ltd., dba 3D-P * Copyright (C) 2021 Neil Tallim - * + * * This file is part of rperf. - * + * * rperf is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. - * + * * rperf is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. - * + * * You should have received a copy of the GNU General Public License * along with rperf. If not, see . */ @@ -22,14 +22,16 @@ extern crate nix; use nix::sys::socket::{setsockopt, sockopt::RcvBuf, sockopt::SndBuf}; -use crate::protocol::results::{IntervalResult, TcpReceiveResult, TcpSendResult, get_unix_timestamp}; +use crate::protocol::results::{ + get_unix_timestamp, IntervalResult, TcpReceiveResult, TcpSendResult, +}; -use super::{INTERVAL, TestStream, parse_port_spec}; +use super::{parse_port_spec, TestStream, INTERVAL}; use std::error::Error; -type BoxResult = Result>; +type BoxResult = Result>; -pub const TEST_HEADER_SIZE:usize = 16; +pub const TEST_HEADER_SIZE: usize = 16; #[derive(Clone)] pub struct TcpTestDefinition { @@ -41,42 +43,61 @@ pub struct TcpTestDefinition { pub length: usize, } impl TcpTestDefinition { - pub fn new(details:&serde_json::Value) -> super::BoxResult { + pub fn new(details: &serde_json::Value) -> super::BoxResult { let mut test_id_bytes = [0_u8; 16]; - for (i, v) in details.get("test_id").unwrap_or(&serde_json::json!([])).as_array().unwrap().iter().enumerate() { - if i >= 16 { //avoid out-of-bounds if given malicious data + for (i, v) in details + .get("test_id") + .unwrap_or(&serde_json::json!([])) + .as_array() + .unwrap() + .iter() + .enumerate() + { + if i >= 16 { + //avoid out-of-bounds if given malicious data break; } test_id_bytes[i] = v.as_i64().unwrap_or(0) as u8; } - - let length = details.get("length").unwrap_or(&serde_json::json!(TEST_HEADER_SIZE)).as_i64().unwrap() as usize; + + let length = details + .get("length") + .unwrap_or(&serde_json::json!(TEST_HEADER_SIZE)) + .as_i64() + .unwrap() as usize; if length < TEST_HEADER_SIZE { - return Err(Box::new(simple_error::simple_error!(std::format!("{} is too short of a length to satisfy testing requirements", length)))); + return Err(Box::new(simple_error::simple_error!(std::format!( + "{} is too short of a length to satisfy testing requirements", + length + )))); } - - Ok(TcpTestDefinition{ + + Ok(TcpTestDefinition { test_id: test_id_bytes, - bandwidth: details.get("bandwidth").unwrap_or(&serde_json::json!(0.0)).as_f64().unwrap() as u64, - length: length, + bandwidth: details + .get("bandwidth") + .unwrap_or(&serde_json::json!(0.0)) + .as_f64() + .unwrap() as u64, + length, }) } } - pub mod receiver { use std::io::Read; use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}; use std::os::unix::io::AsRawFd; - use std::sync::{Mutex}; + use std::sync::atomic::{AtomicBool, Ordering::Relaxed}; + use std::sync::Mutex; use std::time::{Duration, Instant}; - + use mio::net::{TcpListener, TcpStream}; - use mio::{Events, Ready, Poll, PollOpt, Token}; - - const POLL_TIMEOUT:Duration = Duration::from_millis(250); - const RECEIVE_TIMEOUT:Duration = Duration::from_secs(3); - + use mio::{Events, Poll, PollOpt, Ready, Token}; + + const POLL_TIMEOUT: Duration = Duration::from_millis(250); + const RECEIVE_TIMEOUT: Duration = Duration::from_secs(3); + pub struct TcpPortPool { pub ports_ip4: Vec, pub ports_ip6: Vec, @@ -86,286 +107,380 @@ pub mod receiver { lock_ip6: Mutex, } impl TcpPortPool { - pub fn new(port_spec:String, port_spec6:String) -> TcpPortPool { + pub fn new(port_spec: String, port_spec6: String) -> TcpPortPool { let ports = super::parse_port_spec(port_spec); if !ports.is_empty() { log::debug!("configured IPv4 TCP port pool: {:?}", ports); } else { log::debug!("using OS assignment for IPv4 TCP ports"); } - + let ports6 = super::parse_port_spec(port_spec6); if !ports.is_empty() { log::debug!("configured IPv6 TCP port pool: {:?}", ports6); } else { log::debug!("using OS assignment for IPv6 TCP ports"); } - + TcpPortPool { ports_ip4: ports, pos_ip4: 0, lock_ip4: Mutex::new(0), - + ports_ip6: ports6, pos_ip6: 0, lock_ip6: Mutex::new(0), } } - - pub fn bind(&mut self, peer_ip:&IpAddr) -> super::BoxResult { + + pub fn bind(&mut self, peer_ip: &IpAddr) -> super::BoxResult { match peer_ip { IpAddr::V6(_) => { if self.ports_ip6.is_empty() { - return Ok(TcpListener::bind(&SocketAddr::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), 0)).expect(format!("failed to bind OS-assigned IPv6 TCP socket").as_str())); + return Ok(TcpListener::bind(&SocketAddr::new( + IpAddr::V6(Ipv6Addr::UNSPECIFIED), + 0, + )) + .expect("failed to bind OS-assigned IPv6 TCP socket")); } else { let _guard = self.lock_ip6.lock().unwrap(); - - for port_idx in (self.pos_ip6 + 1)..self.ports_ip6.len() { //iterate to the end of the pool; this will skip the first element in the pool initially, but that's fine - let listener_result = TcpListener::bind(&SocketAddr::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), self.ports_ip6[port_idx])); - if listener_result.is_ok() { + + for port_idx in (self.pos_ip6 + 1)..self.ports_ip6.len() { + //iterate to the end of the pool; this will skip the first element in the pool initially, but that's fine + let listener_result = TcpListener::bind(&SocketAddr::new( + IpAddr::V6(Ipv6Addr::UNSPECIFIED), + self.ports_ip6[port_idx], + )); + if let Ok(listener_result) = listener_result { self.pos_ip6 = port_idx; - return Ok(listener_result.unwrap()); + return Ok(listener_result); } else { - log::warn!("unable to bind IPv6 TCP port {}", self.ports_ip6[port_idx]); + log::warn!( + "unable to bind IPv6 TCP port {}", + self.ports_ip6[port_idx] + ); } } - for port_idx in 0..=self.pos_ip6 { //circle back to where the search started - let listener_result = TcpListener::bind(&SocketAddr::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), self.ports_ip6[port_idx])); - if listener_result.is_ok() { + for port_idx in 0..=self.pos_ip6 { + //circle back to where the search started + let listener_result = TcpListener::bind(&SocketAddr::new( + IpAddr::V6(Ipv6Addr::UNSPECIFIED), + self.ports_ip6[port_idx], + )); + if let Ok(listener_result) = listener_result { self.pos_ip6 = port_idx; - return Ok(listener_result.unwrap()); + return Ok(listener_result); } else { - log::warn!("unable to bind IPv6 TCP port {}", self.ports_ip6[port_idx]); + log::warn!( + "unable to bind IPv6 TCP port {}", + self.ports_ip6[port_idx] + ); } } } - return Err(Box::new(simple_error::simple_error!("unable to allocate IPv6 TCP port"))); - }, + Err(Box::new(simple_error::simple_error!( + "unable to allocate IPv6 TCP port" + ))) + } IpAddr::V4(_) => { if self.ports_ip4.is_empty() { - return Ok(TcpListener::bind(&SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 0)).expect(format!("failed to bind OS-assigned IPv4 TCP socket").as_str())); + return Ok(TcpListener::bind(&SocketAddr::new( + IpAddr::V4(Ipv4Addr::UNSPECIFIED), + 0, + )) + .expect("failed to bind OS-assigned IPv4 TCP socket")); } else { let _guard = self.lock_ip4.lock().unwrap(); - - for port_idx in (self.pos_ip4 + 1)..self.ports_ip4.len() { //iterate to the end of the pool; this will skip the first element in the pool initially, but that's fine - let listener_result = TcpListener::bind(&SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), self.ports_ip4[port_idx])); - if listener_result.is_ok() { + + for port_idx in (self.pos_ip4 + 1)..self.ports_ip4.len() { + //iterate to the end of the pool; this will skip the first element in the pool initially, but that's fine + let listener_result = TcpListener::bind(&SocketAddr::new( + IpAddr::V4(Ipv4Addr::UNSPECIFIED), + self.ports_ip4[port_idx], + )); + if let Ok(listener_result) = listener_result { self.pos_ip4 = port_idx; - return Ok(listener_result.unwrap()); + return Ok(listener_result); } else { - log::warn!("unable to bind IPv4 TCP port {}", self.ports_ip4[port_idx]); + log::warn!( + "unable to bind IPv4 TCP port {}", + self.ports_ip4[port_idx] + ); } } - for port_idx in 0..=self.pos_ip4 { //circle back to where the search started - let listener_result = TcpListener::bind(&SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), self.ports_ip4[port_idx])); - if listener_result.is_ok() { + for port_idx in 0..=self.pos_ip4 { + //circle back to where the search started + let listener_result = TcpListener::bind(&SocketAddr::new( + IpAddr::V4(Ipv4Addr::UNSPECIFIED), + self.ports_ip4[port_idx], + )); + if let Ok(listener_result) = listener_result { self.pos_ip4 = port_idx; - return Ok(listener_result.unwrap()); + return Ok(listener_result); } else { - log::warn!("unable to bind IPv4 TCP port {}", self.ports_ip4[port_idx]); + log::warn!( + "unable to bind IPv4 TCP port {}", + self.ports_ip4[port_idx] + ); } } } - return Err(Box::new(simple_error::simple_error!("unable to allocate IPv4 TCP port"))); - }, - }; + Err(Box::new(simple_error::simple_error!( + "unable to allocate IPv4 TCP port" + ))) + } + } } } - + pub struct TcpReceiver { - active: bool, + active: AtomicBool, test_definition: super::TcpTestDefinition, stream_idx: u8, - + listener: Option, stream: Option, mio_poll_token: Token, mio_poll: Poll, - + receive_buffer: usize, } impl TcpReceiver { - pub fn new(test_definition:super::TcpTestDefinition, stream_idx:&u8, port_pool:&mut TcpPortPool, peer_ip:&IpAddr, receive_buffer:&usize) -> super::BoxResult { + pub fn new( + test_definition: super::TcpTestDefinition, + stream_idx: &u8, + port_pool: &mut TcpPortPool, + peer_ip: &IpAddr, + receive_buffer: &usize, + ) -> super::BoxResult { log::debug!("binding TCP listener for stream {}...", stream_idx); - let listener:TcpListener = port_pool.bind(peer_ip).expect(format!("failed to bind TCP socket").as_str()); - log::debug!("bound TCP listener for stream {}: {}", stream_idx, listener.local_addr()?); - + let listener: TcpListener = port_pool.bind(peer_ip).expect("failed to bind TCP socket"); + log::debug!( + "bound TCP listener for stream {}: {}", + stream_idx, + listener.local_addr()? + ); + let mio_poll_token = Token(0); let mio_poll = Poll::new()?; - - Ok(TcpReceiver{ - active: true, - test_definition: test_definition, + + Ok(TcpReceiver { + active: AtomicBool::new(true), + test_definition, stream_idx: stream_idx.to_owned(), - + listener: Some(listener), stream: None, - mio_poll_token: mio_poll_token, - mio_poll: mio_poll, - + mio_poll_token, + mio_poll, + receive_buffer: receive_buffer.to_owned(), }) } - + fn process_connection(&mut self) -> super::BoxResult { - log::debug!("preparing to receive TCP stream {} connection...", self.stream_idx); - + log::debug!( + "preparing to receive TCP stream {} connection...", + self.stream_idx + ); + let listener = self.listener.as_mut().unwrap(); let mio_token = Token(0); let poll = Poll::new()?; - poll.register( - listener, - mio_token, - Ready::readable(), - PollOpt::edge(), - )?; + poll.register(listener, mio_token, Ready::readable(), PollOpt::edge())?; let mut events = Events::with_capacity(1); - + let start = Instant::now(); - - while self.active { + + while self.active.load(Relaxed) { if start.elapsed() >= RECEIVE_TIMEOUT { - return Err(Box::new(simple_error::simple_error!("TCP listening for stream {} timed out", self.stream_idx))); + return Err(Box::new(simple_error::simple_error!( + "TCP listening for stream {} timed out", + self.stream_idx + ))); } - + poll.poll(&mut events, Some(POLL_TIMEOUT))?; for event in events.iter() { - match event.token() { - _ => loop { - match listener.accept() { - Ok((stream, address)) => { - log::debug!("received TCP stream {} connection from {}", self.stream_idx, address); - - let mut verification_stream = stream.try_clone()?; - let mio_token2 = Token(0); - let poll2 = Poll::new()?; - poll2.register( - &verification_stream, - mio_token2, - Ready::readable(), - PollOpt::edge(), - )?; - - let mut buffer = [0_u8; 16]; - let mut events2 = Events::with_capacity(1); - poll2.poll(&mut events2, Some(RECEIVE_TIMEOUT))?; - for event2 in events2.iter() { - match event2.token() { - _ => match verification_stream.read(&mut buffer) { - Ok(_) => { - if buffer == self.test_definition.test_id { - log::debug!("validated TCP stream {} connection from {}", self.stream_idx, address); - if !cfg!(windows) { //NOTE: features unsupported on Windows - if self.receive_buffer != 0 { - log::debug!("setting receive-buffer to {}...", self.receive_buffer); - super::setsockopt(stream.as_raw_fd(), super::RcvBuf, &self.receive_buffer)?; - } - } - - self.mio_poll.register( - &stream, - self.mio_poll_token, - Ready::readable(), - PollOpt::edge(), + event.token(); + loop { + match listener.accept() { + Ok((stream, address)) => { + log::debug!( + "received TCP stream {} connection from {}", + self.stream_idx, + address + ); + + let mut verification_stream = stream.try_clone()?; + let mio_token2 = Token(0); + let poll2 = Poll::new()?; + poll2.register( + &verification_stream, + mio_token2, + Ready::readable(), + PollOpt::edge(), + )?; + + let mut buffer = [0_u8; 16]; + let mut events2 = Events::with_capacity(1); + poll2.poll(&mut events2, Some(RECEIVE_TIMEOUT))?; + for event2 in events2.iter() { + event2.token(); + match verification_stream.read(&mut buffer) { + Ok(_) => { + if buffer == self.test_definition.test_id { + log::debug!( + "validated TCP stream {} connection from {}", + self.stream_idx, + address + ); + if !cfg!(windows) { + //NOTE: features unsupported on Windows + if self.receive_buffer != 0 { + log::debug!( + "setting receive-buffer to {}...", + self.receive_buffer + ); + super::setsockopt( + stream.as_raw_fd(), + super::RcvBuf, + &self.receive_buffer, )?; - return Ok(stream); } - }, - Err(ref e) if e.kind() == std::io::ErrorKind::WouldBlock => { //client didn't provide anything - break; - }, - Err(e) => { - return Err(Box::new(e)); - }, + } + + self.mio_poll.register( + &stream, + self.mio_poll_token, + Ready::readable(), + PollOpt::edge(), + )?; + return Ok(stream); } } + Err(ref e) + if e.kind() == std::io::ErrorKind::WouldBlock => + { + //client didn't provide anything + break; + } + Err(e) => { + return Err(Box::new(e)); + } } - log::warn!("could not validate TCP stream {} connection from {}", self.stream_idx, address); - }, - Err(ref e) if e.kind() == std::io::ErrorKind::WouldBlock => { //nothing to do - break; - }, - Err(e) => { - return Err(Box::new(e)); - }, + } + log::warn!( + "could not validate TCP stream {} connection from {}", + self.stream_idx, + address + ); } - }, + Err(ref e) if e.kind() == std::io::ErrorKind::WouldBlock => { + //nothing to do + break; + } + Err(e) => { + return Err(Box::new(e)); + } + } } } } - Err(Box::new(simple_error::simple_error!("did not receive a connection"))) + Err(Box::new(simple_error::simple_error!( + "did not receive a connection" + ))) } } impl super::TestStream for TcpReceiver { - fn run_interval(&mut self) -> Option>> { - let mut bytes_received:u64 = 0; - - if self.stream.is_none() { //if still in the setup phase, receive the sender + fn run_interval( + &mut self, + ) -> Option>> { + let mut bytes_received: u64 = 0; + + if self.stream.is_none() { + //if still in the setup phase, receive the sender match self.process_connection() { Ok(stream) => { self.stream = Some(stream); //NOTE: the connection process consumes the test-header; account for those bytes bytes_received += super::TEST_HEADER_SIZE as u64; - }, + } Err(e) => { return Some(Err(e)); - }, + } } self.listener = None; //drop it, closing the socket } let stream = self.stream.as_mut().unwrap(); - + let mut events = Events::with_capacity(1); //only watching one socket let mut buf = vec![0_u8; self.test_definition.length]; - + let peer_addr = match stream.peer_addr() { Ok(pa) => pa, Err(e) => return Some(Err(Box::new(e))), }; let start = Instant::now(); - - while self.active { + + while self.active.load(Relaxed) { if start.elapsed() >= RECEIVE_TIMEOUT { - return Some(Err(Box::new(simple_error::simple_error!("TCP reception for stream {} from {} timed out", self.stream_idx, peer_addr)))); + return Some(Err(Box::new(simple_error::simple_error!( + "TCP reception for stream {} from {} timed out", + self.stream_idx, + peer_addr + )))); } - - log::trace!("awaiting TCP stream {} from {}...", self.stream_idx, peer_addr); + + log::trace!( + "awaiting TCP stream {} from {}...", + self.stream_idx, + peer_addr + ); let poll_result = self.mio_poll.poll(&mut events, Some(POLL_TIMEOUT)); - if poll_result.is_err() { - return Some(Err(Box::new(poll_result.unwrap_err()))); + if let Err(err) = poll_result { + return Some(Err(Box::new(err))); } for event in events.iter() { if event.token() == self.mio_poll_token { loop { match stream.read(&mut buf) { Ok(packet_size) => { - log::trace!("received {} bytes in TCP stream {} from {}", packet_size, self.stream_idx, peer_addr); - if packet_size == 0 { //test's over - self.active = false; //HACK: can't call self.stop() because it's a double-borrow due to the unwrapped stream + log::trace!( + "received {} bytes in TCP stream {} from {}", + packet_size, + self.stream_idx, + peer_addr + ); + if packet_size == 0 { + //test's over + //HACK: can't call self.stop() because it's a double-borrow due to the unwrapped stream + self.active.store(false, Relaxed); break; } - + bytes_received += packet_size as u64; - + let elapsed_time = start.elapsed(); if elapsed_time >= super::INTERVAL { - return Some(Ok(Box::new(super::TcpReceiveResult{ + return Some(Ok(Box::new(super::TcpReceiveResult { timestamp: super::get_unix_timestamp(), - + stream_idx: self.stream_idx, - + duration: elapsed_time.as_secs_f32(), - - bytes_received: bytes_received, - }))) + + bytes_received, + }))); } - }, - Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => { //receive timeout + } + Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => { + //receive timeout break; - }, + } Err(e) => { return Some(Err(Box::new(e))); - }, + } } } } else { @@ -374,119 +489,152 @@ pub mod receiver { } } if bytes_received > 0 { - Some(Ok(Box::new(super::TcpReceiveResult{ + Some(Ok(Box::new(super::TcpReceiveResult { timestamp: super::get_unix_timestamp(), - + stream_idx: self.stream_idx, - + duration: start.elapsed().as_secs_f32(), - - bytes_received: bytes_received, + + bytes_received, }))) } else { None } } - + fn get_port(&self) -> super::BoxResult { match &self.listener { Some(listener) => Ok(listener.local_addr()?.port()), None => match &self.stream { Some(stream) => Ok(stream.local_addr()?.port()), - None => Err(Box::new(simple_error::simple_error!("no port currently bound"))), - } + None => Err(Box::new(simple_error::simple_error!( + "no port currently bound" + ))), + }, } } - + fn get_idx(&self) -> u8 { self.stream_idx.to_owned() } - + fn stop(&mut self) { - self.active = false; + self.active.store(false, Relaxed); } } } - - pub mod sender { use std::io::Write; use std::net::{IpAddr, SocketAddr}; use std::os::unix::io::AsRawFd; use std::time::{Duration, Instant}; - + use mio::net::TcpStream; - - use std::thread::{sleep}; - - const CONNECT_TIMEOUT:Duration = Duration::from_secs(2); - const WRITE_TIMEOUT:Duration = Duration::from_millis(50); - const BUFFER_FULL_TIMEOUT:Duration = Duration::from_millis(1); - + + use std::thread::sleep; + + const CONNECT_TIMEOUT: Duration = Duration::from_secs(2); + const WRITE_TIMEOUT: Duration = Duration::from_millis(50); + const BUFFER_FULL_TIMEOUT: Duration = Duration::from_millis(1); + pub struct TcpSender { active: bool, test_definition: super::TcpTestDefinition, stream_idx: u8, - + socket_addr: SocketAddr, stream: Option, - + //the interval, in seconds, at which to send data send_interval: f32, - + remaining_duration: f32, staged_buffer: Vec, - - send_buffer:usize, - no_delay:bool, + + send_buffer: usize, + no_delay: bool, } impl TcpSender { - pub fn new(test_definition:super::TcpTestDefinition, stream_idx:&u8, receiver_ip:&IpAddr, receiver_port:&u16, send_duration:&f32, send_interval:&f32, send_buffer:&usize, no_delay:&bool) -> super::BoxResult { + #[allow(clippy::too_many_arguments)] + pub fn new( + test_definition: super::TcpTestDefinition, + stream_idx: &u8, + receiver_ip: &IpAddr, + receiver_port: &u16, + send_duration: &f32, + send_interval: &f32, + send_buffer: &usize, + no_delay: &bool, + ) -> super::BoxResult { let mut staged_buffer = vec![0_u8; test_definition.length]; - for i in super::TEST_HEADER_SIZE..(staged_buffer.len()) { //fill the packet with a fixed sequence - staged_buffer[i] = (i % 256) as u8; + for (i, staged_buffer_i) in staged_buffer + .iter_mut() + .enumerate() + .skip(super::TEST_HEADER_SIZE) + { + //fill the packet with a fixed sequence + *staged_buffer_i = (i % 256) as u8; } //embed the test ID staged_buffer[0..16].copy_from_slice(&test_definition.test_id); - - Ok(TcpSender{ + + Ok(TcpSender { active: true, - test_definition: test_definition, + test_definition, stream_idx: stream_idx.to_owned(), - + socket_addr: SocketAddr::new(*receiver_ip, *receiver_port), stream: None, - + send_interval: send_interval.to_owned(), - + remaining_duration: send_duration.to_owned(), - staged_buffer: staged_buffer, - + staged_buffer, + send_buffer: send_buffer.to_owned(), no_delay: no_delay.to_owned(), }) } - + fn process_connection(&mut self) -> super::BoxResult { log::debug!("preparing to connect TCP stream {}...", self.stream_idx); - - let raw_stream = match std::net::TcpStream::connect_timeout(&self.socket_addr, CONNECT_TIMEOUT) { - Ok(s) => s, - Err(e) => return Err(Box::new(simple_error::simple_error!("unable to connect stream {}: {}", self.stream_idx, e))), - }; + + let raw_stream = + match std::net::TcpStream::connect_timeout(&self.socket_addr, CONNECT_TIMEOUT) { + Ok(s) => s, + Err(e) => { + return Err(Box::new(simple_error::simple_error!( + "unable to connect stream {}: {}", + self.stream_idx, + e + ))) + } + }; raw_stream.set_write_timeout(Some(WRITE_TIMEOUT))?; let stream = match TcpStream::from_stream(raw_stream) { Ok(s) => s, - Err(e) => return Err(Box::new(simple_error::simple_error!("unable to prepare TCP stream {}: {}", self.stream_idx, e))), + Err(e) => { + return Err(Box::new(simple_error::simple_error!( + "unable to prepare TCP stream {}: {}", + self.stream_idx, + e + ))) + } }; - log::debug!("connected TCP stream {} to {}", self.stream_idx, stream.peer_addr()?); - + log::debug!( + "connected TCP stream {} to {}", + self.stream_idx, + stream.peer_addr()? + ); + if self.no_delay { log::debug!("setting no-delay..."); stream.set_nodelay(true)?; } - if !cfg!(windows) { //NOTE: features unsupported on Windows + if !cfg!(windows) { + //NOTE: features unsupported on Windows if self.send_buffer != 0 { log::debug!("setting send-buffer to {}...", self.send_buffer); super::setsockopt(stream.as_raw_fd(), super::SndBuf, &self.send_buffer)?; @@ -496,81 +644,99 @@ pub mod sender { } } impl super::TestStream for TcpSender { - fn run_interval(&mut self) -> Option>> { - if self.stream.is_none() { //if still in the setup phase, connect to the receiver + fn run_interval( + &mut self, + ) -> Option>> { + if self.stream.is_none() { + //if still in the setup phase, connect to the receiver match self.process_connection() { Ok(stream) => { self.stream = Some(stream); - }, + } Err(e) => { return Some(Err(e)); - }, + } } } let stream = self.stream.as_mut().unwrap(); - - + let interval_duration = Duration::from_secs_f32(self.send_interval); let mut interval_iteration = 0; - let bytes_to_send = ((self.test_definition.bandwidth as f32) * super::INTERVAL.as_secs_f32()) as i64; + let bytes_to_send = + ((self.test_definition.bandwidth as f32) * super::INTERVAL.as_secs_f32()) as i64; let mut bytes_to_send_remaining = bytes_to_send; - let bytes_to_send_per_interval_slice = ((bytes_to_send as f32) * self.send_interval) as i64; + let bytes_to_send_per_interval_slice = + ((bytes_to_send as f32) * self.send_interval) as i64; let mut bytes_to_send_per_interval_slice_remaining = bytes_to_send_per_interval_slice; - - let mut sends_blocked:u64 = 0; - let mut bytes_sent:u64 = 0; - + + let mut sends_blocked: u64 = 0; + let mut bytes_sent: u64 = 0; + let peer_addr = match stream.peer_addr() { Ok(pa) => pa, Err(e) => return Some(Err(Box::new(e))), }; let cycle_start = Instant::now(); - + while self.active && self.remaining_duration > 0.0 && bytes_to_send_remaining > 0 { - log::trace!("writing {} bytes in TCP stream {} to {}...", self.staged_buffer.len(), self.stream_idx, peer_addr); + log::trace!( + "writing {} bytes in TCP stream {} to {}...", + self.staged_buffer.len(), + self.stream_idx, + peer_addr + ); let packet_start = Instant::now(); - - match stream.write(&self.staged_buffer) { //it doesn't matter if the whole thing couldn't be written, since it's just garbage data + + match stream.write(&self.staged_buffer) { + //it doesn't matter if the whole thing couldn't be written, since it's just garbage data Ok(packet_size) => { - log::trace!("wrote {} bytes in TCP stream {} to {}", packet_size, self.stream_idx, peer_addr); - + log::trace!( + "wrote {} bytes in TCP stream {} to {}", + packet_size, + self.stream_idx, + peer_addr + ); + let bytes_written = packet_size as i64; bytes_sent += bytes_written as u64; bytes_to_send_remaining -= bytes_written; bytes_to_send_per_interval_slice_remaining -= bytes_written; - + let elapsed_time = cycle_start.elapsed(); if elapsed_time >= super::INTERVAL { self.remaining_duration -= packet_start.elapsed().as_secs_f32(); - - return Some(Ok(Box::new(super::TcpSendResult{ + + return Some(Ok(Box::new(super::TcpSendResult { timestamp: super::get_unix_timestamp(), - + stream_idx: self.stream_idx, - + duration: elapsed_time.as_secs_f32(), - - bytes_sent: bytes_sent, - sends_blocked: sends_blocked, - }))) + + bytes_sent, + sends_blocked, + }))); } - }, - Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => { //send-buffer is full + } + Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => { + //send-buffer is full //nothing to do, but avoid burning CPU cycles sleep(BUFFER_FULL_TIMEOUT); sends_blocked += 1; - }, + } Err(e) => { return Some(Err(Box::new(e))); - }, + } } - - if bytes_to_send_remaining <= 0 { //interval's target is exhausted, so sleep until the end + + if bytes_to_send_remaining <= 0 { + //interval's target is exhausted, so sleep until the end let elapsed_time = cycle_start.elapsed(); if super::INTERVAL > elapsed_time { sleep(super::INTERVAL - elapsed_time); } - } else if bytes_to_send_per_interval_slice_remaining <= 0 { // interval subsection exhausted + } else if bytes_to_send_per_interval_slice_remaining <= 0 { + // interval subsection exhausted interval_iteration += 1; bytes_to_send_per_interval_slice_remaining = bytes_to_send_per_interval_slice; let elapsed_time = cycle_start.elapsed(); @@ -582,15 +748,15 @@ pub mod sender { self.remaining_duration -= packet_start.elapsed().as_secs_f32(); } if bytes_sent > 0 { - Some(Ok(Box::new(super::TcpSendResult{ + Some(Ok(Box::new(super::TcpSendResult { timestamp: super::get_unix_timestamp(), - + stream_idx: self.stream_idx, - + duration: cycle_start.elapsed().as_secs_f32(), - - bytes_sent: bytes_sent, - sends_blocked: sends_blocked, + + bytes_sent, + sends_blocked, }))) } else { //indicate that the test is over by dropping the stream @@ -598,18 +764,20 @@ pub mod sender { None } } - + fn get_port(&self) -> super::BoxResult { match &self.stream { Some(stream) => Ok(stream.local_addr()?.port()), - None => Err(Box::new(simple_error::simple_error!("no stream currently exists"))), + None => Err(Box::new(simple_error::simple_error!( + "no stream currently exists" + ))), } } - + fn get_idx(&self) -> u8 { self.stream_idx.to_owned() } - + fn stop(&mut self) { self.active = false; } diff --git a/src/stream/udp.rs b/src/stream/udp.rs index 80404bd..63ebc0d 100644 --- a/src/stream/udp.rs +++ b/src/stream/udp.rs @@ -1,23 +1,23 @@ /* * Copyright (C) 2021 Evtech Solutions, Ltd., dba 3D-P * Copyright (C) 2021 Neil Tallim - * + * * This file is part of rperf. - * + * * rperf is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. - * + * * rperf is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. - * + * * You should have received a copy of the GNU General Public License * along with rperf. If not, see . */ - + extern crate log; extern crate nix; @@ -25,14 +25,16 @@ use std::error::Error; use nix::sys::socket::{setsockopt, sockopt::RcvBuf, sockopt::SndBuf}; -use crate::protocol::results::{IntervalResult, UdpReceiveResult, UdpSendResult, get_unix_timestamp}; +use crate::protocol::results::{ + get_unix_timestamp, IntervalResult, UdpReceiveResult, UdpSendResult, +}; -use super::{INTERVAL, TestStream, parse_port_spec}; +use super::{parse_port_spec, TestStream, INTERVAL}; -type BoxResult = Result>; +type BoxResult = Result>; -pub const TEST_HEADER_SIZE:u16 = 36; -const UDP_HEADER_SIZE:u16 = 8; +pub const TEST_HEADER_SIZE: u16 = 36; +const UDP_HEADER_SIZE: u16 = 8; #[derive(Clone)] pub struct UdpTestDefinition { @@ -44,24 +46,43 @@ pub struct UdpTestDefinition { pub length: u16, } impl UdpTestDefinition { - pub fn new(details:&serde_json::Value) -> super::BoxResult { + pub fn new(details: &serde_json::Value) -> super::BoxResult { let mut test_id_bytes = [0_u8; 16]; - for (i, v) in details.get("test_id").unwrap_or(&serde_json::json!([])).as_array().unwrap().iter().enumerate() { - if i >= 16 { //avoid out-of-bounds if given malicious data + for (i, v) in details + .get("test_id") + .unwrap_or(&serde_json::json!([])) + .as_array() + .unwrap() + .iter() + .enumerate() + { + if i >= 16 { + //avoid out-of-bounds if given malicious data break; } test_id_bytes[i] = v.as_i64().unwrap_or(0) as u8; } - - let length = details.get("length").unwrap_or(&serde_json::json!(TEST_HEADER_SIZE)).as_i64().unwrap() as u16; + + let length = details + .get("length") + .unwrap_or(&serde_json::json!(TEST_HEADER_SIZE)) + .as_i64() + .unwrap() as u16; if length < TEST_HEADER_SIZE { - return Err(Box::new(simple_error::simple_error!(std::format!("{} is too short of a length to satisfy testing requirements", length)))); + return Err(Box::new(simple_error::simple_error!(std::format!( + "{} is too short of a length to satisfy testing requirements", + length + )))); } - - Ok(UdpTestDefinition{ + + Ok(UdpTestDefinition { test_id: test_id_bytes, - bandwidth: details.get("bandwidth").unwrap_or(&serde_json::json!(0.0)).as_f64().unwrap() as u64, - length: length, + bandwidth: details + .get("bandwidth") + .unwrap_or(&serde_json::json!(0.0)) + .as_f64() + .unwrap() as u64, + length, }) } } @@ -70,240 +91,328 @@ pub mod receiver { use std::convert::TryInto; use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}; use std::os::unix::io::AsRawFd; - use std::sync::{Mutex}; + use std::sync::Mutex; use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; - - use chrono::{NaiveDateTime}; - + + use chrono::NaiveDateTime; + use std::net::UdpSocket; - - const READ_TIMEOUT:Duration = Duration::from_millis(50); - const RECEIVE_TIMEOUT:Duration = Duration::from_secs(3); - + + const READ_TIMEOUT: Duration = Duration::from_millis(50); + const RECEIVE_TIMEOUT: Duration = Duration::from_secs(3); + pub struct UdpPortPool { pub ports_ip4: Vec, pos_ip4: usize, lock_ip4: Mutex, - + pub ports_ip6: Vec, pos_ip6: usize, lock_ip6: Mutex, } impl UdpPortPool { - pub fn new(port_spec:String, port_spec6:String) -> UdpPortPool { + pub fn new(port_spec: String, port_spec6: String) -> UdpPortPool { let ports = super::parse_port_spec(port_spec); if !ports.is_empty() { log::debug!("configured IPv4 UDP port pool: {:?}", ports); } else { log::debug!("using OS assignment for IPv4 UDP ports"); } - + let ports6 = super::parse_port_spec(port_spec6); if !ports.is_empty() { log::debug!("configured IPv6 UDP port pool: {:?}", ports6); } else { log::debug!("using OS assignment for IPv6 UDP ports"); } - + UdpPortPool { ports_ip4: ports, pos_ip4: 0, lock_ip4: Mutex::new(0), - + ports_ip6: ports6, pos_ip6: 0, lock_ip6: Mutex::new(0), } } - - pub fn bind(&mut self, peer_ip:&IpAddr) -> super::BoxResult { + + pub fn bind(&mut self, peer_ip: &IpAddr) -> super::BoxResult { match peer_ip { IpAddr::V6(_) => { if self.ports_ip6.is_empty() { - return Ok(UdpSocket::bind(&SocketAddr::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), 0)).expect(format!("failed to bind OS-assigned IPv6 UDP socket").as_str())); + return Ok(UdpSocket::bind(SocketAddr::new( + IpAddr::V6(Ipv6Addr::UNSPECIFIED), + 0, + )) + .expect("failed to bind OS-assigned IPv6 UDP socket")); } else { let _guard = self.lock_ip6.lock().unwrap(); - - for port_idx in (self.pos_ip6 + 1)..self.ports_ip6.len() { //iterate to the end of the pool; this will skip the first element in the pool initially, but that's fine - let listener_result = UdpSocket::bind(&SocketAddr::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), self.ports_ip6[port_idx])); - if listener_result.is_ok() { + + for port_idx in (self.pos_ip6 + 1)..self.ports_ip6.len() { + //iterate to the end of the pool; this will skip the first element in the pool initially, but that's fine + let listener_result = UdpSocket::bind(SocketAddr::new( + IpAddr::V6(Ipv6Addr::UNSPECIFIED), + self.ports_ip6[port_idx], + )); + if let Ok(listener_result) = listener_result { self.pos_ip6 = port_idx; - return Ok(listener_result.unwrap()); + return Ok(listener_result); } else { - log::warn!("unable to bind IPv6 UDP port {}", self.ports_ip6[port_idx]); + log::warn!( + "unable to bind IPv6 UDP port {}", + self.ports_ip6[port_idx] + ); } } - for port_idx in 0..=self.pos_ip6 { //circle back to where the search started - let listener_result = UdpSocket::bind(&SocketAddr::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), self.ports_ip6[port_idx])); - if listener_result.is_ok() { + for port_idx in 0..=self.pos_ip6 { + //circle back to where the search started + let listener_result = UdpSocket::bind(SocketAddr::new( + IpAddr::V6(Ipv6Addr::UNSPECIFIED), + self.ports_ip6[port_idx], + )); + if let Ok(listener_result) = listener_result { self.pos_ip6 = port_idx; - return Ok(listener_result.unwrap()); + return Ok(listener_result); } else { - log::warn!("unable to bind IPv6 UDP port {}", self.ports_ip6[port_idx]); + log::warn!( + "unable to bind IPv6 UDP port {}", + self.ports_ip6[port_idx] + ); } } } - return Err(Box::new(simple_error::simple_error!("unable to allocate IPv6 UDP port"))); - }, + Err(Box::new(simple_error::simple_error!( + "unable to allocate IPv6 UDP port" + ))) + } IpAddr::V4(_) => { if self.ports_ip4.is_empty() { - return Ok(UdpSocket::bind(&SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 0)).expect(format!("failed to bind OS-assigned IPv4 UDP socket").as_str())); + return Ok(UdpSocket::bind(SocketAddr::new( + IpAddr::V4(Ipv4Addr::UNSPECIFIED), + 0, + )) + .expect("failed to bind OS-assigned IPv4 UDP socket")); } else { let _guard = self.lock_ip4.lock().unwrap(); - - for port_idx in (self.pos_ip4 + 1)..self.ports_ip4.len() { //iterate to the end of the pool; this will skip the first element in the pool initially, but that's fine - let listener_result = UdpSocket::bind(&SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), self.ports_ip4[port_idx])); - if listener_result.is_ok() { + + for port_idx in (self.pos_ip4 + 1)..self.ports_ip4.len() { + //iterate to the end of the pool; this will skip the first element in the pool initially, but that's fine + let listener_result = UdpSocket::bind(SocketAddr::new( + IpAddr::V4(Ipv4Addr::UNSPECIFIED), + self.ports_ip4[port_idx], + )); + if let Ok(listener_result) = listener_result { self.pos_ip4 = port_idx; - return Ok(listener_result.unwrap()); + return Ok(listener_result); } else { - log::warn!("unable to bind IPv4 UDP port {}", self.ports_ip4[port_idx]); + log::warn!( + "unable to bind IPv4 UDP port {}", + self.ports_ip4[port_idx] + ); } } - for port_idx in 0..=self.pos_ip4 { //circle back to where the search started - let listener_result = UdpSocket::bind(&SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), self.ports_ip4[port_idx])); - if listener_result.is_ok() { + for port_idx in 0..=self.pos_ip4 { + //circle back to where the search started + let listener_result = UdpSocket::bind(SocketAddr::new( + IpAddr::V4(Ipv4Addr::UNSPECIFIED), + self.ports_ip4[port_idx], + )); + if let Ok(listener_result) = listener_result { self.pos_ip4 = port_idx; - return Ok(listener_result.unwrap()); + return Ok(listener_result); } else { - log::warn!("unable to bind IPv4 UDP port {}", self.ports_ip4[port_idx]); + log::warn!( + "unable to bind IPv4 UDP port {}", + self.ports_ip4[port_idx] + ); } } } - return Err(Box::new(simple_error::simple_error!("unable to allocate IPv4 UDP port"))); - }, - }; + Err(Box::new(simple_error::simple_error!( + "unable to allocate IPv4 UDP port" + ))) + } + } } } - + struct UdpReceiverIntervalHistory { packets_received: u64, - + packets_lost: i64, packets_out_of_order: u64, packets_duplicated: u64, - + unbroken_sequence: u64, jitter_seconds: Option, longest_unbroken_sequence: u64, longest_jitter_seconds: Option, previous_time_delta_nanoseconds: i64, } - + pub struct UdpReceiver { active: bool, test_definition: super::UdpTestDefinition, stream_idx: u8, next_packet_id: u64, - + socket: UdpSocket, } impl UdpReceiver { - pub fn new(test_definition:super::UdpTestDefinition, stream_idx:&u8, port_pool:&mut UdpPortPool, peer_ip:&IpAddr, receive_buffer:&usize) -> super::BoxResult { + pub fn new( + test_definition: super::UdpTestDefinition, + stream_idx: &u8, + port_pool: &mut UdpPortPool, + peer_ip: &IpAddr, + receive_buffer: &usize, + ) -> super::BoxResult { log::debug!("binding UDP receive socket for stream {}...", stream_idx); - let socket:UdpSocket = port_pool.bind(peer_ip).expect(format!("failed to bind UDP socket").as_str()); + let socket: UdpSocket = port_pool.bind(peer_ip).expect("failed to bind UDP socket"); socket.set_read_timeout(Some(READ_TIMEOUT))?; - if !cfg!(windows) { //NOTE: features unsupported on Windows + if !cfg!(windows) { + //NOTE: features unsupported on Windows if *receive_buffer != 0 { log::debug!("setting receive-buffer to {}...", receive_buffer); super::setsockopt(socket.as_raw_fd(), super::RcvBuf, receive_buffer)?; } } - log::debug!("bound UDP receive socket for stream {}: {}", stream_idx, socket.local_addr()?); - - Ok(UdpReceiver{ + log::debug!( + "bound UDP receive socket for stream {}: {}", + stream_idx, + socket.local_addr()? + ); + + Ok(UdpReceiver { active: true, - test_definition: test_definition, + test_definition, stream_idx: stream_idx.to_owned(), - + next_packet_id: 0, - - socket: socket, + + socket, }) } - - fn process_packets_ordering(&mut self, packet_id:u64, mut history:&mut UdpReceiverIntervalHistory) -> bool { + + fn process_packets_ordering( + &mut self, + packet_id: u64, + history: &mut UdpReceiverIntervalHistory, + ) -> bool { /* the algorithm from iperf3 provides a pretty decent approximation * for tracking lost and out-of-order packets efficiently, so it's * been minimally reimplemented here, with corrections. - * + * * returns true if packet-ordering is as-expected */ - if packet_id == self.next_packet_id { //expected sequential-ordering case - self.next_packet_id += 1; - return true; - } else if packet_id > self.next_packet_id { //something was either lost or there's an ordering problem - let lost_packet_count = (packet_id - self.next_packet_id) as i64; - log::debug!("UDP reception for stream {} observed a gap of {} packets", self.stream_idx, lost_packet_count); - history.packets_lost += lost_packet_count; //assume everything in-between has been lost - self.next_packet_id = packet_id + 1; //anticipate that ordered receipt will resume - } else { //a packet with a previous ID was received; this is either a duplicate or an ordering issue - //CAUTION: this is where the approximation part of the algorithm comes into play - if history.packets_lost > 0 { //assume it's an ordering issue in the common case - history.packets_lost -= 1; - history.packets_out_of_order += 1; - } else { //the only other thing it could be is a duplicate; in practice, duplicates don't tend to show up alongside losses; non-zero is always bad, though - history.packets_duplicated += 1; + match packet_id.cmp(&self.next_packet_id) { + std::cmp::Ordering::Equal => { + //expected sequential-ordering case + self.next_packet_id += 1; + return true; + } + std::cmp::Ordering::Greater => { + //something was either lost or there's an ordering problem + let lost_packet_count = (packet_id - self.next_packet_id) as i64; + log::debug!( + "UDP reception for stream {} observed a gap of {} packets", + self.stream_idx, + lost_packet_count + ); + history.packets_lost += lost_packet_count; //assume everything in-between has been lost + self.next_packet_id = packet_id + 1; //anticipate that ordered receipt will resume + } + std::cmp::Ordering::Less => { + //a packet with a previous ID was received; this is either a duplicate or an ordering issue + //CAUTION: this is where the approximation part of the algorithm comes into play + if history.packets_lost > 0 { + //assume it's an ordering issue in the common case + history.packets_lost -= 1; + history.packets_out_of_order += 1; + } else { + //the only other thing it could be is a duplicate; in practice, duplicates don't tend to show up alongside losses; non-zero is always bad, though + history.packets_duplicated += 1; + } } } - return false; + false } - - fn process_jitter(&mut self, timestamp:&NaiveDateTime, history:&mut UdpReceiverIntervalHistory) { + + fn process_jitter( + &mut self, + timestamp: &NaiveDateTime, + history: &mut UdpReceiverIntervalHistory, + ) { /* this is a pretty straightforward implementation of RFC 1889, Appendix 8 * it works on an assumption that the timestamp delta between sender and receiver * will remain effectively constant during the testing window */ - let now = SystemTime::now().duration_since(UNIX_EPOCH).expect("system time before UNIX epoch"); - let current_timestamp = NaiveDateTime::from_timestamp(now.as_secs() as i64, now.subsec_nanos()); - + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("system time before UNIX epoch"); + let current_timestamp = + NaiveDateTime::from_timestamp_opt(now.as_secs() as i64, now.subsec_nanos()) + .unwrap(); + let time_delta = current_timestamp - *timestamp; - + let time_delta_nanoseconds = match time_delta.num_nanoseconds() { Some(ns) => ns, None => { - log::warn!("sender and receiver clocks are too out-of-sync to calculate jitter"); + log::warn!( + "sender and receiver clocks are too out-of-sync to calculate jitter" + ); return; - }, + } }; - - if history.unbroken_sequence > 1 { //do jitter calculation - let delta_seconds = (time_delta_nanoseconds - history.previous_time_delta_nanoseconds).abs() as f32 / 1_000_000_000.00; - - if history.unbroken_sequence > 2 { //normal jitter-calculation, per the RFC + + if history.unbroken_sequence > 1 { + //do jitter calculation + let delta_seconds = + (time_delta_nanoseconds - history.previous_time_delta_nanoseconds).abs() as f32 + / 1_000_000_000.00; + + if history.unbroken_sequence > 2 { + //normal jitter-calculation, per the RFC let mut jitter_seconds = history.jitter_seconds.unwrap(); //since we have a chain, this won't be None jitter_seconds += (delta_seconds - jitter_seconds) / 16.0; history.jitter_seconds = Some(jitter_seconds); - } else { //first observed transition; use this as the calibration baseline + } else { + //first observed transition; use this as the calibration baseline history.jitter_seconds = Some(delta_seconds); } } //update time-delta history.previous_time_delta_nanoseconds = time_delta_nanoseconds; } - - fn process_packet(&mut self, packet:&[u8], mut history:&mut UdpReceiverIntervalHistory) -> bool { + + fn process_packet( + &mut self, + packet: &[u8], + history: &mut UdpReceiverIntervalHistory, + ) -> bool { //the first sixteen bytes are the test's ID if packet[0..16] != self.test_definition.test_id { - return false + return false; } - + //the next eight bytes are the packet's ID, in big-endian order let packet_id = u64::from_be_bytes(packet[16..24].try_into().unwrap()); - + //except for the timestamp, nothing else in the packet actually matters - + history.packets_received += 1; - if self.process_packets_ordering(packet_id, &mut history) { + if self.process_packets_ordering(packet_id, history) { //the second eight are the number of seconds since the UNIX epoch, in big-endian order let origin_seconds = i64::from_be_bytes(packet[24..32].try_into().unwrap()); //and the following four are the number of nanoseconds since the UNIX epoch let origin_nanoseconds = u32::from_be_bytes(packet[32..36].try_into().unwrap()); - let source_timestamp = NaiveDateTime::from_timestamp(origin_seconds, origin_nanoseconds); - + let source_timestamp = + NaiveDateTime::from_timestamp_opt(origin_seconds, origin_nanoseconds).unwrap(); + history.unbroken_sequence += 1; - self.process_jitter(&source_timestamp, &mut history); - + self.process_jitter(&source_timestamp, history); + if history.unbroken_sequence > history.longest_unbroken_sequence { history.longest_unbroken_sequence = history.unbroken_sequence; history.longest_jitter_seconds = history.jitter_seconds; @@ -312,43 +421,52 @@ pub mod receiver { history.unbroken_sequence = 0; history.jitter_seconds = None; } - return true; + true } } impl super::TestStream for UdpReceiver { - fn run_interval(&mut self) -> Option>> { + fn run_interval( + &mut self, + ) -> Option>> { let mut buf = vec![0_u8; self.test_definition.length.into()]; - - let mut bytes_received:u64 = 0; - - let mut history = UdpReceiverIntervalHistory{ + + let mut bytes_received: u64 = 0; + + let mut history = UdpReceiverIntervalHistory { packets_received: 0, - + packets_lost: 0, packets_out_of_order: 0, packets_duplicated: 0, - + unbroken_sequence: 0, jitter_seconds: None, longest_unbroken_sequence: 0, longest_jitter_seconds: None, previous_time_delta_nanoseconds: 0, }; - + let start = Instant::now(); - + while self.active { if start.elapsed() >= RECEIVE_TIMEOUT { return Some(Err(Box::new(simple_error::simple_error!("UDP reception for stream {} timed out, likely because the end-signal was lost", self.stream_idx)))); } - + log::trace!("awaiting UDP packets on stream {}...", self.stream_idx); loop { match self.socket.recv_from(&mut buf) { Ok((packet_size, peer_addr)) => { - log::trace!("received {} bytes in UDP packet {} from {}", packet_size, self.stream_idx, peer_addr); - if packet_size == 16 { //possible end-of-test message - if &buf[0..16] == self.test_definition.test_id { //test's over + log::trace!( + "received {} bytes in UDP packet {} from {}", + packet_size, + self.stream_idx, + peer_addr + ); + if packet_size == 16 { + //possible end-of-test message + if buf[0..16] == self.test_definition.test_id { + //test's over self.stop(); break; } @@ -357,58 +475,64 @@ pub mod receiver { log::warn!("received malformed packet with size {} for UDP stream {} from {}", packet_size, self.stream_idx, peer_addr); continue; } - + if self.process_packet(&buf, &mut history) { //NOTE: duplicate packets increase this count; this is intentional because the stack still processed data - bytes_received += packet_size as u64 + super::UDP_HEADER_SIZE as u64; - + bytes_received += + packet_size as u64 + super::UDP_HEADER_SIZE as u64; + let elapsed_time = start.elapsed(); if elapsed_time >= super::INTERVAL { - return Some(Ok(Box::new(super::UdpReceiveResult{ + return Some(Ok(Box::new(super::UdpReceiveResult { timestamp: super::get_unix_timestamp(), - + stream_idx: self.stream_idx, - + duration: elapsed_time.as_secs_f32(), - - bytes_received: bytes_received, + + bytes_received, packets_received: history.packets_received, packets_lost: history.packets_lost, packets_out_of_order: history.packets_out_of_order, packets_duplicated: history.packets_duplicated, - + unbroken_sequence: history.longest_unbroken_sequence, jitter_seconds: history.longest_jitter_seconds, - }))) + }))); } } else { - log::warn!("received packet unrelated to UDP stream {} from {}", self.stream_idx, peer_addr); + log::warn!( + "received packet unrelated to UDP stream {} from {}", + self.stream_idx, + peer_addr + ); continue; } - }, - Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => { //receive timeout + } + Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => { + //receive timeout break; - }, + } Err(e) => { return Some(Err(Box::new(e))); - }, + } } } } if bytes_received > 0 { - Some(Ok(Box::new(super::UdpReceiveResult{ + Some(Ok(Box::new(super::UdpReceiveResult { timestamp: super::get_unix_timestamp(), - + stream_idx: self.stream_idx, - + duration: start.elapsed().as_secs_f32(), - - bytes_received: bytes_received, + + bytes_received, packets_received: history.packets_received, packets_lost: history.packets_lost, packets_out_of_order: history.packets_out_of_order, packets_duplicated: history.packets_duplicated, - + unbroken_sequence: history.longest_unbroken_sequence, jitter_seconds: history.longest_jitter_seconds, }))) @@ -416,167 +540,204 @@ pub mod receiver { None } } - + fn get_port(&self) -> super::BoxResult { let socket_addr = self.socket.local_addr()?; Ok(socket_addr.port()) } - + fn get_idx(&self) -> u8 { self.stream_idx.to_owned() } - + fn stop(&mut self) { self.active = false; } } } - - pub mod sender { use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}; use std::os::unix::io::AsRawFd; use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; - + use std::net::UdpSocket; - - use std::thread::{sleep}; - - const WRITE_TIMEOUT:Duration = Duration::from_millis(50); - const BUFFER_FULL_TIMEOUT:Duration = Duration::from_millis(1); - + + use std::thread::sleep; + + const WRITE_TIMEOUT: Duration = Duration::from_millis(50); + const BUFFER_FULL_TIMEOUT: Duration = Duration::from_millis(1); + pub struct UdpSender { active: bool, test_definition: super::UdpTestDefinition, stream_idx: u8, - + socket: UdpSocket, - + //the interval, in seconds, at which to send data send_interval: f32, - + remaining_duration: f32, next_packet_id: u64, staged_packet: Vec, } impl UdpSender { - pub fn new(test_definition:super::UdpTestDefinition, stream_idx:&u8, port:&u16, receiver_ip:&IpAddr, receiver_port:&u16, send_duration:&f32, send_interval:&f32, send_buffer:&usize) -> super::BoxResult { + #[allow(clippy::too_many_arguments)] + pub fn new( + test_definition: super::UdpTestDefinition, + stream_idx: &u8, + port: &u16, + receiver_ip: &IpAddr, + receiver_port: &u16, + send_duration: &f32, + send_interval: &f32, + send_buffer: &usize, + ) -> super::BoxResult { log::debug!("preparing to connect UDP stream {}...", stream_idx); let socket_addr_receiver = SocketAddr::new(*receiver_ip, *receiver_port); let socket = match receiver_ip { - IpAddr::V6(_) => UdpSocket::bind(&SocketAddr::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), *port)).expect(format!("failed to bind UDP socket, port {}", port).as_str()), - IpAddr::V4(_) => UdpSocket::bind(&SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), *port)).expect(format!("failed to bind UDP socket, port {}", port).as_str()), + IpAddr::V6(_) => { + UdpSocket::bind(SocketAddr::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), *port)) + .unwrap_or_else(|_| panic!("failed to bind UDP socket, port {}", port)) + } + IpAddr::V4(_) => { + UdpSocket::bind(SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), *port)) + .unwrap_or_else(|_| panic!("failed to bind UDP socket, port {}", port)) + } }; socket.set_write_timeout(Some(WRITE_TIMEOUT))?; - if !cfg!(windows) { //NOTE: features unsupported on Windows + if !cfg!(windows) { + //NOTE: features unsupported on Windows if *send_buffer != 0 { log::debug!("setting send-buffer to {}...", send_buffer); super::setsockopt(socket.as_raw_fd(), super::SndBuf, send_buffer)?; } } socket.connect(socket_addr_receiver)?; - log::debug!("connected UDP stream {} to {}", stream_idx, socket_addr_receiver); - + log::debug!( + "connected UDP stream {} to {}", + stream_idx, + socket_addr_receiver + ); + let mut staged_packet = vec![0_u8; test_definition.length.into()]; - for i in super::TEST_HEADER_SIZE..(staged_packet.len() as u16) { //fill the packet with a fixed sequence + for i in super::TEST_HEADER_SIZE..(staged_packet.len() as u16) { + //fill the packet with a fixed sequence staged_packet[i as usize] = (i % 256) as u8; } //embed the test ID staged_packet[0..16].copy_from_slice(&test_definition.test_id); - - Ok(UdpSender{ + + Ok(UdpSender { active: true, - test_definition: test_definition, + test_definition, stream_idx: stream_idx.to_owned(), - - socket: socket, - + + socket, + send_interval: send_interval.to_owned(), - + remaining_duration: send_duration.to_owned(), next_packet_id: 0, - staged_packet: staged_packet, + staged_packet, }) } - + fn prepare_packet(&mut self) { - let now = SystemTime::now().duration_since(UNIX_EPOCH).expect("system time before UNIX epoch"); - + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("system time before UNIX epoch"); + //eight bytes after the test ID are the packet's ID, in big-endian order self.staged_packet[16..24].copy_from_slice(&self.next_packet_id.to_be_bytes()); - + //the next eight are the seconds part of the UNIX timestamp and the following four are the nanoseconds self.staged_packet[24..32].copy_from_slice(&now.as_secs().to_be_bytes()); self.staged_packet[32..36].copy_from_slice(&now.subsec_nanos().to_be_bytes()); } } impl super::TestStream for UdpSender { - fn run_interval(&mut self) -> Option>> { + fn run_interval( + &mut self, + ) -> Option>> { let interval_duration = Duration::from_secs_f32(self.send_interval); let mut interval_iteration = 0; - let bytes_to_send = ((self.test_definition.bandwidth as f32) * super::INTERVAL.as_secs_f32()) as i64; + let bytes_to_send = + ((self.test_definition.bandwidth as f32) * super::INTERVAL.as_secs_f32()) as i64; let mut bytes_to_send_remaining = bytes_to_send; - let bytes_to_send_per_interval_slice = ((bytes_to_send as f32) * self.send_interval) as i64; + let bytes_to_send_per_interval_slice = + ((bytes_to_send as f32) * self.send_interval) as i64; let mut bytes_to_send_per_interval_slice_remaining = bytes_to_send_per_interval_slice; - - let mut packets_sent:u64 = 0; - let mut sends_blocked:u64 = 0; - let mut bytes_sent:u64 = 0; - + + let mut packets_sent: u64 = 0; + let mut sends_blocked: u64 = 0; + let mut bytes_sent: u64 = 0; + let cycle_start = Instant::now(); - + while self.active && self.remaining_duration > 0.0 && bytes_to_send_remaining > 0 { - log::trace!("writing {} bytes in UDP stream {}...", self.staged_packet.len(), self.stream_idx); + log::trace!( + "writing {} bytes in UDP stream {}...", + self.staged_packet.len(), + self.stream_idx + ); let packet_start = Instant::now(); - + self.prepare_packet(); match self.socket.send(&self.staged_packet) { Ok(packet_size) => { - log::trace!("wrote {} bytes in UDP stream {}", packet_size, self.stream_idx); - + log::trace!( + "wrote {} bytes in UDP stream {}", + packet_size, + self.stream_idx + ); + packets_sent += 1; //reflect that a packet is in-flight self.next_packet_id += 1; - + let bytes_written = packet_size as i64 + super::UDP_HEADER_SIZE as i64; bytes_sent += bytes_written as u64; bytes_to_send_remaining -= bytes_written; bytes_to_send_per_interval_slice_remaining -= bytes_written; - + let elapsed_time = cycle_start.elapsed(); if elapsed_time >= super::INTERVAL { self.remaining_duration -= packet_start.elapsed().as_secs_f32(); - - return Some(Ok(Box::new(super::UdpSendResult{ + + return Some(Ok(Box::new(super::UdpSendResult { timestamp: super::get_unix_timestamp(), - + stream_idx: self.stream_idx, - + duration: elapsed_time.as_secs_f32(), - - bytes_sent: bytes_sent, - packets_sent: packets_sent, - sends_blocked: sends_blocked, - }))) + + bytes_sent, + packets_sent, + sends_blocked, + }))); } - }, - Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => { //send-buffer is full + } + Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => { + //send-buffer is full //nothing to do, but avoid burning CPU cycles sleep(BUFFER_FULL_TIMEOUT); sends_blocked += 1; - }, + } Err(e) => { return Some(Err(Box::new(e))); - }, + } } - - if bytes_to_send_remaining <= 0 { //interval's target is exhausted, so sleep until the end + + if bytes_to_send_remaining <= 0 { + //interval's target is exhausted, so sleep until the end let elapsed_time = cycle_start.elapsed(); if super::INTERVAL > elapsed_time { sleep(super::INTERVAL - elapsed_time); } - } else if bytes_to_send_per_interval_slice_remaining <= 0 { // interval subsection exhausted + } else if bytes_to_send_per_interval_slice_remaining <= 0 { + // interval subsection exhausted interval_iteration += 1; bytes_to_send_per_interval_slice_remaining = bytes_to_send_per_interval_slice; let elapsed_time = cycle_start.elapsed(); @@ -588,48 +749,54 @@ pub mod sender { self.remaining_duration -= packet_start.elapsed().as_secs_f32(); } if bytes_sent > 0 { - Some(Ok(Box::new(super::UdpSendResult{ + Some(Ok(Box::new(super::UdpSendResult { timestamp: super::get_unix_timestamp(), - + stream_idx: self.stream_idx, - + duration: cycle_start.elapsed().as_secs_f32(), - - bytes_sent: bytes_sent, - packets_sent: packets_sent, - sends_blocked: sends_blocked, + + bytes_sent, + packets_sent, + sends_blocked, }))) } else { //indicate that the test is over by sending the test ID by itself let mut remaining_announcements = 5; - while remaining_announcements > 0 { //do it a few times in case of loss + while remaining_announcements > 0 { + //do it a few times in case of loss match self.socket.send(&self.staged_packet[0..16]) { Ok(packet_size) => { - log::trace!("wrote {} bytes of test-end signal in UDP stream {}", packet_size, self.stream_idx); + log::trace!( + "wrote {} bytes of test-end signal in UDP stream {}", + packet_size, + self.stream_idx + ); remaining_announcements -= 1; - }, - Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => { //send-buffer is full + } + Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => { + //send-buffer is full //wait to try again and avoid burning CPU cycles sleep(BUFFER_FULL_TIMEOUT); - }, + } Err(e) => { return Some(Err(Box::new(e))); - }, + } } } None } } - + fn get_port(&self) -> super::BoxResult { let socket_addr = self.socket.local_addr()?; Ok(socket_addr.port()) } - + fn get_idx(&self) -> u8 { self.stream_idx.to_owned() } - + fn stop(&mut self) { self.active = false; } diff --git a/src/utils/cpu_affinity.rs b/src/utils/cpu_affinity.rs index 063d488..4f29935 100644 --- a/src/utils/cpu_affinity.rs +++ b/src/utils/cpu_affinity.rs @@ -1,47 +1,50 @@ /* * Copyright (C) 2021 Evtech Solutions, Ltd., dba 3D-P * Copyright (C) 2021 Neil Tallim - * + * * This file is part of rperf. - * + * * rperf is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. - * + * * rperf is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. - * + * * You should have received a copy of the GNU General Public License * along with rperf. If not, see . */ - + extern crate core_affinity; use std::error::Error; -type BoxResult = Result>; +type BoxResult = Result>; pub struct CpuAffinityManager { enabled_cores: Vec, last_core_pointer: usize, } impl CpuAffinityManager { - pub fn new(cores:&str) -> BoxResult { + pub fn new(cores: &str) -> BoxResult { let core_ids = core_affinity::get_core_ids().unwrap_or_default(); - log::debug!("enumerated CPU cores: {:?}", core_ids.iter().map(|c| c.id).collect::>()); - + log::debug!( + "enumerated CPU cores: {:?}", + core_ids.iter().map(|c| c.id).collect::>() + ); + let mut enabled_cores = Vec::new(); for cid in cores.split(',') { if cid.is_empty() { continue; } - let cid_usize:usize = cid.parse()?; + let cid_usize: usize = cid.parse()?; let mut cid_valid = false; for core_id in &core_ids { if core_id.id == cid_usize { - enabled_cores.push(core_id.clone()); + enabled_cores.push(*core_id); cid_valid = true; } } @@ -49,20 +52,20 @@ impl CpuAffinityManager { log::warn!("unrecognised CPU core: {}", cid_usize); } } - if enabled_cores.len() > 0 { + if !enabled_cores.is_empty() { log::debug!("selecting from CPU cores {:?}", enabled_cores); } else { log::debug!("not applying CPU core affinity"); } - - Ok(CpuAffinityManager{ - enabled_cores: enabled_cores, + + Ok(CpuAffinityManager { + enabled_cores, last_core_pointer: 0, }) } - + pub fn set_affinity(&mut self) { - if self.enabled_cores.len() > 0 { + if !self.enabled_cores.is_empty() { let core_id = self.enabled_cores[self.last_core_pointer]; log::debug!("setting CPU affinity to {}", core_id.id); core_affinity::set_for_current(core_id); diff --git a/src/utils/mod.rs b/src/utils/mod.rs index c81d644..d4505fd 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -1,21 +1,21 @@ /* * Copyright (C) 2021 Evtech Solutions, Ltd., dba 3D-P * Copyright (C) 2021 Neil Tallim - * + * * This file is part of rperf. - * + * * rperf is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. - * + * * rperf is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. - * + * * You should have received a copy of the GNU General Public License * along with rperf. If not, see . */ - + pub mod cpu_affinity;