mod config;
mod r#protocol;
#[cfg(test)]
#[path = "../../../tests/loadgen/mod.rs"]
mod tests;
pub use r#protocol::r#enum::{Mode, Protocol};
pub(crate) use config::*;
pub(crate) use r#protocol::*;
use std::{
env::args,
fs::File,
io::{BufReader, Error},
net::{AddrParseError, SocketAddr},
process::exit,
sync::Arc,
task::Context,
time::{Duration, Instant},
};
use bytes::{Buf, Bytes};
use futures::future::poll_fn;
use h2::{RecvStream, client::handshake};
use h3::{
client::{RequestStream, builder},
error::StreamError,
};
use h3_quinn::BidiStream;
use http::{
HeaderMap, HeaderValue, Method, Request, Response, Uri,
header::{ACCEPT, HOST, HeaderName, USER_AGENT},
};
use quinn::{ConnectError, Connecting, ConnectionError, Endpoint};
use rustls::{
ClientConfig, RootCertStore, SupportedProtocolVersion,
crypto::ring::default_provider,
pki_types::{
CertificateDer, InvalidDnsNameError, ServerName,
pem::{PemObject, ReadIter},
},
version::{TLS12, TLS13},
};
use tokio::{
io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt},
net::{TcpStream, lookup_host},
spawn,
sync::{Barrier, BarrierWaitResult},
task::JoinHandle,
time::sleep,
};
use tokio_rustls::{TlsConnector, client::TlsStream};
pub(crate) type H2Error = h2::Error;
pub(crate) type QuicClientConfig = quinn::ClientConfig;
pub(crate) type QuicConnection = quinn::Connection;
pub(crate) type QuicCryptoConfig = quinn::crypto::rustls::QuicClientConfig;
pub(crate) type Http3Transport = h3_quinn::Connection;
pub(crate) type Http3Connection = h3::client::Connection<Http3Transport, Bytes>;
pub(crate) type Http3ConnectionError = h3::error::ConnectionError;
#[derive(Clone, Debug)]
pub struct Config {
address: String,
connections: usize,
duration_secs: u64,
warmup_secs: u64,
protocol: Protocol,
mode: Mode,
ca_path: Option<String>,
rate_per_sec: u64,
}
impl Config {
pub fn get_address(&self) -> &String {
&self.address
}
pub fn get_connections(&self) -> usize {
self.connections
}
pub fn get_duration_secs(&self) -> u64 {
self.duration_secs
}
pub fn get_warmup_secs(&self) -> u64 {
self.warmup_secs
}
pub fn get_protocol(&self) -> Protocol {
self.protocol
}
pub fn get_mode(&self) -> Mode {
self.mode
}
pub fn get_ca_path(&self) -> Option<&String> {
self.ca_path.as_ref()
}
pub fn get_rate_per_sec(&self) -> u64 {
self.rate_per_sec
}
fn set_address(&mut self, address: String) {
self.address = address;
}
fn set_connections(&mut self, connections: usize) {
self.connections = connections;
}
fn set_duration_secs(&mut self, duration_secs: u64) {
self.duration_secs = duration_secs;
}
fn set_warmup_secs(&mut self, warmup_secs: u64) {
self.warmup_secs = warmup_secs;
}
fn set_protocol(&mut self, protocol: Protocol) {
self.protocol = protocol;
}
fn set_mode(&mut self, mode: Mode) {
self.mode = mode;
}
fn set_ca_path(&mut self, ca_path: String) {
self.ca_path = Some(ca_path);
}
fn set_rate_per_sec(&mut self, rate_per_sec: u64) {
self.rate_per_sec = rate_per_sec;
}
}
impl Default for Config {
fn default() -> Self {
Self {
address: DEFAULT_ADDR.to_owned(),
connections: DEFAULT_CONNECTIONS,
duration_secs: DEFAULT_DURATION_SECS,
warmup_secs: DEFAULT_WARMUP_SECS,
protocol: Protocol::Http1,
mode: Mode::KeepAlive,
ca_path: None,
rate_per_sec: DEFAULT_RATE_PER_SEC,
}
}
}
impl Config {
pub fn from_args(args: &[String]) -> Result<Config, String> {
let mut config: Config = Config::default();
let mut index: usize = 0;
while index < args.len() {
let flag: &str = args[index].as_str();
let value: Option<&str> = match args.get(index + 1) {
Some(value) => Some(value.as_str()),
None => None,
};
match flag {
ARG_ADDR => {
let Some(value) = value else {
return Err(format!("{ARG_ADDR} needs a value"));
};
config.set_address(value.to_owned());
index += 2;
}
ARG_PROTOCOL => {
let Some(value) = value else {
return Err(format!("{ARG_PROTOCOL} needs a value"));
};
let Some(protocol) = Protocol::from_name(value) else {
return Err(format!("unknown protocol {value}"));
};
config.set_protocol(protocol);
index += 2;
}
ARG_MODE => {
let Some(value) = value else {
return Err(format!("{ARG_MODE} needs a value"));
};
let Some(mode) = Mode::from_name(value) else {
return Err(format!("unknown connection mode {value}"));
};
config.set_mode(mode);
index += 2;
}
ARG_CA => {
let Some(value) = value else {
return Err(format!("{ARG_CA} needs a value"));
};
config.set_ca_path(value.to_owned());
index += 2;
}
ARG_RATE => {
let Some(value) = value else {
return Err(format!("{ARG_RATE} needs a value"));
};
let rate: u64 = match value.parse::<u64>() {
Ok(rate) => rate,
Err(error) => return Err(error.to_string()),
};
if rate == 0 {
return Err(ERROR_RATE_ZERO.to_owned());
}
config.set_rate_per_sec(rate);
index += 2;
}
ARG_CONNECTIONS => {
let Some(value) = value else {
return Err(format!("{ARG_CONNECTIONS} needs a value"));
};
let connections: usize = match value.parse::<usize>() {
Ok(connections) => connections,
Err(error) => return Err(error.to_string()),
};
config.set_connections(connections);
index += 2;
}
ARG_DURATION => {
let Some(value) = value else {
return Err(format!("{ARG_DURATION} needs a value"));
};
let duration: u64 = match value.parse::<u64>() {
Ok(duration) => duration,
Err(error) => return Err(error.to_string()),
};
config.set_duration_secs(duration);
index += 2;
}
ARG_WARMUP => {
let Some(value) = value else {
return Err(format!("{ARG_WARMUP} needs a value"));
};
let warmup: u64 = match value.parse::<u64>() {
Ok(warmup) => warmup,
Err(error) => return Err(error.to_string()),
};
config.set_warmup_secs(warmup);
index += 2;
}
other => {
return Err(format!("unknown argument {other}"));
}
}
}
Ok(config)
}
}
#[derive(Clone, Debug, Default)]
pub struct Outcome {
completed: u64,
errors: u64,
latencies_us: Vec<u64>,
}
impl Outcome {
fn get_completed(&self) -> u64 {
self.completed
}
fn get_errors(&self) -> u64 {
self.errors
}
fn get_latencies_us(&self) -> &Vec<u64> {
&self.latencies_us
}
fn rps(&self, config: &Config) -> f64 {
if config.get_duration_secs() == 0 {
return 0.0;
}
self.get_completed() as f64 / config.get_duration_secs() as f64
}
fn mean_us(&self) -> f64 {
let latencies: &Vec<u64> = self.get_latencies_us();
if latencies.is_empty() {
return 0.0;
}
let total: u64 = latencies.iter().sum();
total as f64 / latencies.len() as f64
}
pub fn to_json(&self, config: &Config) -> String {
let mut sorted: Vec<u64> = self.get_latencies_us().clone();
sorted.sort_unstable();
let max_us: u64 = match sorted.last() {
Some(value) => *value,
None => 0,
};
format!(
"{{\"connections\":{},\"duration_s\":{},\"requests\":{},\"errors\":{},\"rps\":{:.1},\"p50_us\":{},\"p90_us\":{},\"p99_us\":{},\"p999_us\":{},\"max_us\":{},\"mean_us\":{:.1}}}",
config.get_connections(),
config.get_duration_secs(),
self.get_completed(),
self.get_errors(),
self.rps(config),
percentile(&sorted, 0.5),
percentile(&sorted, 0.9),
percentile(&sorted, 0.99),
percentile(&sorted, 0.999),
max_us,
self.mean_us(),
)
}
}
#[inline(always)]
pub fn percentile(sorted: &[u64], fraction: f64) -> u64 {
if sorted.is_empty() {
return 0;
}
let position: f64 = fraction * (sorted.len() - 1) as f64;
let lower: usize = position.floor() as usize;
let upper: usize = position.ceil() as usize;
let Some(lower_value) = sorted.get(lower) else {
return 0;
};
if lower == upper {
return *lower_value;
}
let Some(upper_value) = sorted.get(upper) else {
return *lower_value;
};
let weight: f64 = position - lower as f64;
let interpolated: f64 = *lower_value as f64 * (1.0 - weight) + *upper_value as f64 * weight;
interpolated.round() as u64
}
#[inline(always)]
pub fn find_subsequence(haystack: &[u8], needle: &[u8]) -> Option<usize> {
if needle.is_empty() || haystack.len() < needle.len() {
return None;
}
haystack
.windows(needle.len())
.position(|window: &[u8]| window == needle)
}
fn starts_with_name_ignore_case(line: &[u8], name: &[u8]) -> bool {
if line.len() < name.len() {
return false;
}
let Some(prefix) = line.get(..name.len()) else {
return false;
};
for (index, byte) in prefix.iter().enumerate() {
let Some(expected) = name.get(index) else {
return false;
};
if !byte.eq_ignore_ascii_case(expected) {
return false;
}
}
true
}
pub fn parse_content_length(head: &[u8]) -> usize {
let mut index: usize = 0;
while index < head.len() {
let rest: &[u8] = &head[index..];
if starts_with_name_ignore_case(rest, CONTENT_LENGTH_NAME)
&& let Some(length) = parse_content_length_line(rest)
{
return length;
}
match find_subsequence(rest, LINE_TERMINATOR) {
Some(offset) => index += offset + LINE_TERMINATOR.len(),
None => break,
}
}
0
}
fn parse_content_length_line(line: &[u8]) -> Option<usize> {
let mut index: usize = CONTENT_LENGTH_NAME.len();
if line.get(index) != Some(&b':') {
return None;
}
index += 1;
while matches!(line.get(index), Some(b' ') | Some(b'\t')) {
index += 1;
}
let mut value: usize = 0;
let mut digits: usize = 0;
while let Some(byte) = line.get(index) {
if !byte.is_ascii_digit() {
break;
}
value = value * 10 + usize::from(*byte - b'0');
digits += 1;
index += 1;
}
if digits == 0 {
return None;
}
Some(value)
}
#[derive(Clone, Debug)]
struct Case {
bytes: Vec<u8>,
body: Vec<u8>,
expect_len: usize,
}
fn build_cases() -> Vec<Case> {
let mut cases: Vec<Case> = Vec::new();
for _ in 0..4 {
cases.push(Case {
bytes: REQUEST_ROOT.as_bytes().to_vec(),
body: Vec::new(),
expect_len: EXPECT_ROOT,
});
}
for _ in 0..2 {
cases.push(Case {
bytes: REQUEST_USER.as_bytes().to_vec(),
body: Vec::new(),
expect_len: EXPECT_USER,
});
}
cases.push(Case {
bytes: REQUEST_SEARCH.as_bytes().to_vec(),
body: Vec::new(),
expect_len: EXPECT_SEARCH,
});
cases.push(Case {
bytes: REQUEST_HEADERS.as_bytes().to_vec(),
body: Vec::new(),
expect_len: EXPECT_HEADERS,
});
cases.push(Case {
bytes: REQUEST_ECHO_HEAD.as_bytes().to_vec(),
body: BODY_ECHO.as_bytes().to_vec(),
expect_len: EXPECT_ECHO,
});
cases
}
fn to_protocol_cases(cases: Vec<Case>) -> Vec<ProtocolCase> {
cases
.into_iter()
.map(|case: Case| ProtocolCase {
bytes: case.bytes,
body: case.body,
expect_len: case.expect_len,
})
.collect()
}
async fn run_short(run: Run, cases: Vec<Case>) -> ConnStats {
let mut stats: ConnStats = ConnStats::default();
let mut body: Vec<u8> = Vec::with_capacity(READ_BUFFER_SIZE);
let mut index: usize = 0;
let _: BarrierWaitResult = run.barrier.wait().await;
let interval: Duration = Duration::from_nanos(
1_000_000_000 * u64::try_from(run.connections).unwrap_or(1) / run.rate_per_sec.max(1),
);
let mut next_open: Instant = Instant::now();
loop {
let now: Instant = Instant::now();
if now >= run.deadline {
break;
}
let Some(case) = cases.get(index % cases.len()) else {
break;
};
index = index.wrapping_add(1);
if now < next_open {
sleep(next_open - now).await;
}
next_open += interval;
let transport: Box<dyn Transport> =
match open_transport(&run.address, run.protocol, run.ca_path.as_deref()).await {
Ok(transport) => transport,
Err(_) => {
stats.errors += 1;
continue;
}
};
let mut connection: Connection = Connection::from_transport(transport);
let wrote: Result<(), Error> = connection.get_mut_stream().write_all(&case.bytes).await;
if wrote.is_err() {
stats.errors += 1;
continue;
}
if !case.body.is_empty() {
let wrote: Result<(), Error> = connection.get_mut_stream().write_all(&case.body).await;
if wrote.is_err() {
stats.errors += 1;
continue;
}
}
match connection.read_response(&mut body).await {
Ok(length) => {
if length == case.expect_len {
stats.completed += 1;
} else {
stats.errors += 1;
}
}
Err(_) => {
stats.errors += 1;
}
}
let micros: u128 = now.elapsed().as_micros();
if now >= run.warmup_until {
stats
.latencies_us
.push(u64::try_from(micros).unwrap_or(u64::MAX));
}
}
stats
}
#[derive(Clone, Debug, Default)]
struct ConnStats {
completed: u64,
errors: u64,
latencies_us: Vec<u64>,
}
struct Connection {
stream: Box<dyn Transport>,
buffer: Vec<u8>,
start: usize,
end: usize,
chunk: Vec<u8>,
}
impl Connection {
fn get_mut_stream(&mut self) -> &mut Box<dyn Transport> {
&mut self.stream
}
fn get_buffer(&self) -> &Vec<u8> {
&self.buffer
}
fn get_start(&self) -> usize {
self.start
}
fn get_end(&self) -> usize {
self.end
}
fn set_start(&mut self, start: usize) {
self.start = start;
}
fn from_transport(stream: Box<dyn Transport>) -> Connection {
Connection {
stream,
buffer: Vec::with_capacity(READ_BUFFER_SIZE),
start: 0,
end: 0,
chunk: vec![0; READ_BUFFER_SIZE],
}
}
async fn open(
address: &str,
protocol: Protocol,
ca_path: Option<&str>,
) -> Result<Connection, String> {
let stream: Box<dyn Transport> = open_transport(address, protocol, ca_path).await?;
Ok(Connection::from_transport(stream))
}
async fn fill(&mut self) -> Result<usize, String> {
let Self {
stream,
buffer,
chunk,
start,
end,
} = self;
let read: usize = match stream.read(chunk).await {
Ok(read) => read,
Err(error) => return Err(error.to_string()),
};
if read == 0 {
return Err("the connection was closed".to_owned());
}
if *start == *end {
buffer.clear();
*start = 0;
*end = 0;
}
buffer.extend_from_slice(&chunk[..read]);
*end += read;
Ok(read)
}
#[inline(always)]
fn available(&self) -> &[u8] {
match self.get_buffer().get(self.get_start()..self.get_end()) {
Some(bytes) => bytes,
None => &[],
}
}
#[inline(always)]
fn consume(&mut self, amount: usize) {
let limit: usize = self.get_end().saturating_sub(self.get_start());
self.set_start(self.get_start() + amount.min(limit));
}
async fn read_response(&mut self, body: &mut Vec<u8>) -> Result<usize, String> {
let content_length: usize = loop {
let head_end: usize = match find_subsequence(self.available(), HEAD_TERMINATOR) {
Some(offset) => offset + HEAD_TERMINATOR.len(),
None => {
self.fill().await?;
continue;
}
};
let head: &[u8] = match self.available().get(..head_end) {
Some(head) => head,
None => &[],
};
let length: usize = parse_content_length(head);
self.consume(head_end);
break length;
};
body.clear();
while body.len() < content_length {
let take: usize = self.available().len().min(content_length - body.len());
if take > 0 {
body.extend_from_slice(&self.available()[..take]);
self.consume(take);
} else {
self.fill().await?;
}
}
Ok(body.len())
}
}
async fn run_connection(
address: String,
cases: Vec<Case>,
barrier: Arc<Barrier>,
warmup_until: Instant,
deadline: Instant,
protocol: Protocol,
ca_path: Option<String>,
) -> ConnStats {
let mut stats: ConnStats = ConnStats::default();
let mut connection: Connection =
match Connection::open(&address, protocol, ca_path.as_deref()).await {
Ok(connection) => connection,
Err(_) => return stats,
};
let mut body: Vec<u8> = Vec::with_capacity(READ_BUFFER_SIZE);
let mut index: usize = 0;
let _: BarrierWaitResult = barrier.wait().await;
loop {
let now: Instant = Instant::now();
if now >= deadline {
break;
}
let Some(case) = cases.get(index % cases.len()) else {
break;
};
index = index.wrapping_add(1);
if connection
.get_mut_stream()
.write_all(&case.bytes)
.await
.is_err()
{
stats.errors += 1;
break;
}
if !case.body.is_empty()
&& connection
.get_mut_stream()
.write_all(&case.body)
.await
.is_err()
{
stats.errors += 1;
break;
}
match connection.read_response(&mut body).await {
Ok(length) => {
if length == case.expect_len {
stats.completed += 1;
} else {
stats.errors += 1;
}
}
Err(_) => {
stats.errors += 1;
break;
}
}
let micros: u128 = now.elapsed().as_micros();
if now >= warmup_until {
stats
.latencies_us
.push(u64::try_from(micros).unwrap_or(u64::MAX));
}
}
stats
}
pub async fn run(config: Config) -> Result<Outcome, String> {
let cases: Vec<Case> = build_cases();
let barrier: Arc<Barrier> = Arc::new(Barrier::new(config.get_connections()));
let warmup_until: Instant = Instant::now() + Duration::from_secs(config.get_warmup_secs());
let deadline: Instant = warmup_until + Duration::from_secs(config.get_duration_secs());
let protocol: Protocol = config.get_protocol();
let mode: Mode = config.get_mode();
let ca_path: Option<String> = config.get_ca_path().cloned();
let rate_per_sec: u64 = config.get_rate_per_sec();
let connections: usize = config.get_connections();
let mut handles: Vec<JoinHandle<ConnStats>> = Vec::new();
for _ in 0..connections {
let address: String = config.get_address().clone();
let cases: Vec<Case> = cases.clone();
let barrier: Arc<Barrier> = Arc::clone(&barrier);
let ca_path: Option<String> = ca_path.clone();
handles.push(spawn(async move {
match protocol {
Protocol::Http3 => match mode {
Mode::KeepAlive => {
let run: Run = Run {
address,
cases: to_protocol_cases(cases),
barrier,
warmup_until,
deadline,
protocol,
ca_path,
rate_per_sec,
connections,
};
run_http3(run).await
}
Mode::Short => {
let run: Run = Run {
address,
cases: to_protocol_cases(cases),
barrier,
warmup_until,
deadline,
protocol,
ca_path,
rate_per_sec,
connections,
};
run_http3_short(run).await
}
},
Protocol::H2c | Protocol::H2Tls => match mode {
Mode::KeepAlive => {
let run: Run = Run {
address,
cases: to_protocol_cases(cases),
barrier,
warmup_until,
deadline,
protocol,
ca_path,
rate_per_sec,
connections,
};
run_http2(run).await
}
Mode::Short => {
let run: Run = Run {
address,
cases: to_protocol_cases(cases),
barrier,
warmup_until,
deadline,
protocol,
ca_path,
rate_per_sec,
connections,
};
run_http2_short(run).await
}
},
Protocol::Http1 | Protocol::Http1Tls => match mode {
Mode::KeepAlive => {
run_connection(
address,
cases,
barrier,
warmup_until,
deadline,
protocol,
ca_path,
)
.await
}
Mode::Short => {
let run: Run = Run {
address,
cases: to_protocol_cases(cases.clone()),
barrier,
warmup_until,
deadline,
protocol,
ca_path,
rate_per_sec,
connections,
};
run_short(run, cases).await
}
},
}
}));
}
let mut outcome: Outcome = Outcome::default();
for handle in handles {
match handle.await {
Ok(stats) => {
outcome.completed += stats.completed;
outcome.errors += stats.errors;
outcome.latencies_us.extend(stats.latencies_us);
}
Err(error) => return Err(error.to_string()),
}
}
Ok(outcome)
}
#[tokio::main]
async fn main() {
let args: Vec<String> = args().skip(1).collect();
let config: Config = match Config::from_args(&args) {
Ok(config) => config,
Err(error) => {
eprintln!("{error}");
exit(2);
}
};
let outcome: Outcome = match run(config.clone()).await {
Ok(outcome) => outcome,
Err(error) => {
eprintln!("{error}");
exit(1);
}
};
println!("{}", outcome.to_json(&config));
}