use std::time::Duration;
use colored::Colorize as _;
use eyre::Result;
use crate::TestType;
use crate::control::{Handshake, TestTransport, perform_handshake};
use crate::performance::http::HttpVersion;
use crate::performance::http::client::run_http_test;
use crate::performance::quic::client::run_quic_client;
use crate::performance::tcp::client::run_tcp_client;
use crate::performance::udp::client::run_udp_client;
use crate::report::{
HttpTestConfig, PhaseParams, QuicTestConfig, SuiteReport, TcpTestConfig, ThroughputAccounting,
UdpTestConfig,
};
const SUITE_IO_SIZE: usize = 64 * 1024;
const SUITE_HTTP_PAYLOAD: usize = 8 * 1024 * 1024;
const SUITE_UDP_DATAGRAM: usize = 1200;
const MAX_AUTO_CONNECTIONS: usize = 8;
pub fn default_connections() -> usize {
std::thread::available_parallelism()
.map(|n| n.get())
.unwrap_or(4)
.clamp(1, MAX_AUTO_CONNECTIONS)
}
#[derive(Debug, Clone)]
pub struct SuiteConfig {
pub server: String,
pub control_port: u16,
pub phase_duration: Duration,
pub warmup: Duration,
pub connections: usize,
pub udp_target_rate_mbps: u64,
pub io_size: usize,
pub http_payload: usize,
pub accounting: ThroughputAccounting,
pub include_tls: bool,
}
impl SuiteConfig {
pub fn new(server: String) -> Self {
Self {
server,
control_port: crate::constants::DEFAULT_CONTROL_PORT,
phase_duration: Duration::from_secs(8),
warmup: Duration::from_secs(1),
connections: default_connections(),
udp_target_rate_mbps: 100,
io_size: SUITE_IO_SIZE,
http_payload: SUITE_HTTP_PAYLOAD,
accounting: ThroughputAccounting::Goodput,
include_tls: true,
}
}
}
pub async fn run_suite(cfg: SuiteConfig) -> Result<SuiteReport> {
let handshake = perform_handshake(&cfg.server, cfg.control_port).await?;
let mut suite = SuiteReport::new(cfg.server.clone());
if handshake.manifest.listener(TestTransport::TcpRaw).is_some() {
for tt in [
TestType::LatencyOnly,
TestType::Bidirectional,
TestType::FullDuplex,
] {
let params = tcp_quic_params(&cfg, tt);
let label = format!("tcp/{}", phase_suffix(tt));
run_phase(
&mut suite,
&label,
params.clone(),
run_tcp_phase(&handshake, &cfg, params),
)
.await;
}
} else {
suite.skip("tcp/*", "TCP listener not advertised by server");
}
if handshake
.manifest
.listener(TestTransport::UdpBlaster)
.is_some()
{
for tt in [TestType::LatencyOnly, TestType::Bidirectional] {
let params = udp_params(&cfg, tt);
let label = format!("udp/{}", phase_suffix(tt));
run_phase(
&mut suite,
&label,
params.clone(),
run_udp_phase(&handshake, &cfg, params),
)
.await;
}
} else {
suite.skip("udp/*", "UDP listener not advertised by server");
}
if handshake
.manifest
.listener(TestTransport::QuicRaw)
.is_some()
{
for tt in [
TestType::LatencyOnly,
TestType::Bidirectional,
TestType::FullDuplex,
] {
let params = tcp_quic_params(&cfg, tt);
let label = format!("quic/{}", phase_suffix(tt));
run_phase(
&mut suite,
&label,
params.clone(),
run_quic_phase(&handshake, &cfg, params),
)
.await;
}
} else {
suite.skip("quic/*", "raw-QUIC listener not advertised by server");
}
if handshake.manifest.listener(TestTransport::Http1).is_some() {
run_http_set(
&mut suite,
&handshake,
&cfg,
"http1",
HttpVersion::HTTP1,
TestTransport::Http1,
)
.await;
} else {
suite.skip("http1/*", "HTTP/1.1 listener not advertised by server");
}
if handshake.manifest.listener(TestTransport::H2c).is_some() {
run_http_set(
&mut suite,
&handshake,
&cfg,
"h2c",
HttpVersion::H2C,
TestTransport::H2c,
)
.await;
} else {
suite.skip("h2c/*", "h2c listener not advertised by server");
}
if !cfg.include_tls {
suite.skip("http2/*", "TLS phases skipped (--no-tls)");
} else if handshake
.manifest
.listener(TestTransport::Http2Tls)
.is_some()
{
run_http_set(
&mut suite,
&handshake,
&cfg,
"http2",
HttpVersion::HTTP2,
TestTransport::Http2Tls,
)
.await;
} else {
suite.skip("http2/*", "HTTP/2-TLS listener not advertised by server");
}
if !cfg.include_tls {
suite.skip("http3/*", "TLS phases skipped (--no-tls)");
} else if handshake.manifest.listener(TestTransport::Http3).is_some() {
run_http_set(
&mut suite,
&handshake,
&cfg,
"http3",
HttpVersion::HTTP3,
TestTransport::Http3,
)
.await;
} else {
suite.skip("http3/*", "HTTP/3 listener not advertised by server");
}
suite.finalize();
Ok(suite)
}
fn phase_suffix(tt: TestType) -> &'static str {
match tt {
TestType::LatencyOnly => "latency",
TestType::LatencyUnderLoad => "latency-under-load",
TestType::Bidirectional => "bidirectional",
TestType::FullDuplex | TestType::Simultaneous => "full-duplex",
TestType::Download => "download",
TestType::Upload => "upload",
}
}
fn tcp_quic_params(cfg: &SuiteConfig, test_type: TestType) -> PhaseParams {
let is_latency = matches!(test_type, TestType::LatencyOnly);
PhaseParams {
payload_size: (!is_latency).then_some(cfg.io_size),
io_unit: cfg.io_size,
connections: if is_latency { 1 } else { cfg.connections },
duration: cfg.phase_duration,
test_type,
deviations: Vec::new(),
}
}
fn udp_params(cfg: &SuiteConfig, test_type: TestType) -> PhaseParams {
let is_latency = matches!(test_type, TestType::LatencyOnly);
PhaseParams {
payload_size: (!is_latency).then_some(SUITE_UDP_DATAGRAM),
io_unit: SUITE_UDP_DATAGRAM,
connections: if is_latency { 1 } else { cfg.connections },
duration: cfg.phase_duration,
test_type,
deviations: vec![
"UDP datagram is MTU-bound (1200 B) by design; it intentionally does not use the \
64 KB TCP/QUIC I/O unit, to avoid IP fragmentation"
.to_string(),
],
}
}
fn http_params(cfg: &SuiteConfig, test_type: TestType) -> PhaseParams {
let is_latency = matches!(test_type, TestType::LatencyOnly);
let mut deviations = Vec::new();
if matches!(test_type, TestType::Simultaneous) {
deviations.push(
"HTTP is request/response; the full-duplex row runs Simultaneous \
(parallel up/down streams) as the closest analog"
.to_string(),
);
}
PhaseParams {
payload_size: (!is_latency).then_some(cfg.http_payload),
io_unit: cfg.io_size,
connections: if is_latency { 1 } else { cfg.connections },
duration: cfg.phase_duration,
test_type,
deviations,
}
}
async fn run_phase<F>(suite: &mut SuiteReport, label: &str, params: PhaseParams, fut: F)
where
F: std::future::Future<Output = Result<crate::report::TestReport>>,
{
tracing::info!(
"{}",
format!("\n── Suite phase: {label} ──")
.bright_magenta()
.bold()
);
match fut.await {
Ok(report) => suite.record(label, params, report),
Err(e) => {
tracing::warn!(phase = label, error = %e, "suite phase failed; continuing");
suite.skip(label, e.to_string());
}
}
}
async fn run_http_set(
suite: &mut SuiteReport,
handshake: &Handshake,
cfg: &SuiteConfig,
proto: &str,
version: HttpVersion,
transport: TestTransport,
) {
for tt in [
TestType::LatencyOnly,
TestType::Bidirectional,
TestType::Simultaneous,
] {
let params = http_params(cfg, tt);
let label = format!("{proto}/{}", phase_suffix(tt));
run_phase(
suite,
&label,
params.clone(),
run_http_phase(handshake, cfg, version, transport, params),
)
.await;
}
}
async fn run_tcp_phase(
handshake: &Handshake,
cfg: &SuiteConfig,
params: PhaseParams,
) -> Result<crate::report::TestReport> {
let (host, port) = handshake.endpoint(TestTransport::TcpRaw)?;
let payload_sizes: Vec<usize> = params.payload_size.into_iter().collect();
let conf = TcpTestConfig::new(
host,
Some(port),
params.duration.as_secs(),
params.connections,
params.test_type,
payload_sizes,
)
.with_warmup(cfg.warmup)
.with_accounting(cfg.accounting);
run_tcp_client(conf).await
}
async fn run_udp_phase(
handshake: &Handshake,
cfg: &SuiteConfig,
params: PhaseParams,
) -> Result<crate::report::TestReport> {
let (host, port) = handshake.endpoint(TestTransport::UdpBlaster)?;
let payload_sizes: Vec<usize> = params.payload_size.into_iter().collect();
let conf = UdpTestConfig::new(
host,
Some(port),
params.duration.as_secs(),
params.connections,
params.test_type,
payload_sizes,
)
.with_warmup(cfg.warmup)
.with_accounting(cfg.accounting)
.with_target_rate_bps(cfg.udp_target_rate_mbps.saturating_mul(1_000_000));
run_udp_client(conf).await
}
async fn run_quic_phase(
handshake: &Handshake,
cfg: &SuiteConfig,
params: PhaseParams,
) -> Result<crate::report::TestReport> {
let (host, port) = handshake.endpoint(TestTransport::QuicRaw)?;
let payload_sizes: Vec<usize> = params.payload_size.into_iter().collect();
let conf = QuicTestConfig::new(
host,
Some(port),
params.duration.as_secs(),
params.connections,
params.test_type,
payload_sizes,
)
.with_warmup(cfg.warmup)
.with_accounting(cfg.accounting);
run_quic_client(conf).await
}
async fn run_http_phase(
handshake: &Handshake,
cfg: &SuiteConfig,
version: HttpVersion,
transport: TestTransport,
params: PhaseParams,
) -> Result<crate::report::TestReport> {
let (host, port) = handshake.endpoint(transport)?;
let payload_sizes: Vec<usize> = vec![params.payload_size.unwrap_or(cfg.io_size)];
let conf = HttpTestConfig::new(
host,
Some(port),
params.duration.as_secs(),
params.connections,
params.test_type,
payload_sizes,
Some(cfg.io_size),
version,
)
.with_warmup(cfg.warmup)
.with_accounting(cfg.accounting);
run_http_test(conf).await
}