blob: 487d7ca9b33b040dd2b8049a1940b4e7dbabc85f [file]
use super::kind::BenchmarkKindCommand;
use super::output::BenchmarkOutputCommand;
use super::props::{BenchmarkKindProps, BenchmarkTransportProps};
use super::{defaults::*, transport::BenchmarkTransportCommand};
use clap::error::ErrorKind;
use clap::{CommandFactory, Parser};
use iggy::utils::byte_size::IggyByteSize;
use iggy::utils::duration::IggyDuration;
use iggy_bench_report::benchmark_kind::BenchmarkKind;
use iggy_bench_report::params::BenchmarkParams;
use iggy_bench_report::transport::BenchmarkTransport;
use integration::test_server::Transport;
use std::net::SocketAddr;
use std::num::NonZeroU32;
use std::path::Path;
use std::str::FromStr;
use tracing::info;
#[derive(Parser, Debug)]
#[command(author, version, about, long_about = None)]
pub struct IggyBenchArgs {
/// Benchmark kind
#[command(subcommand)]
pub benchmark_kind: BenchmarkKindCommand,
/// Number of messages per batch
#[arg(long, short = 'p', default_value_t = DEFAULT_MESSAGES_PER_BATCH)]
pub messages_per_batch: NonZeroU32,
/// Number of message batches
#[arg(long, short = 'b', default_value_t = DEFAULT_MESSAGE_BATCHES)]
pub message_batches: NonZeroU32,
/// Message size in bytes
#[arg(long, short = 'm', default_value_t = DEFAULT_MESSAGE_SIZE)]
pub message_size: NonZeroU32,
/// Start stream id
#[arg(long, short = 'S', default_value_t = DEFAULT_START_STREAM_ID)]
pub start_stream_id: NonZeroU32,
/// Optional rate limit per individual producer in bytes per second (not aggregate).
/// Accepts human-readable formats like "50KB", "10MB", or "1GB"
#[arg(long, short = 'r', verbatim_doc_comment)]
pub rate_limit: Option<IggyByteSize>,
/// Warmup time in human readable format, e.g. "1s", "2m", "3h"
#[arg(long, short = 'w', default_value_t = IggyDuration::from_str(DEFAULT_WARMUP_TIME).unwrap())]
pub warmup_time: IggyDuration,
/// Server stdout visibility
#[arg(long, short = 'v', default_value_t = DEFAULT_SERVER_STDOUT_VISIBILITY)]
pub verbose: bool,
/// Sampling time for metrics collection. It is also used as bucket size for time series calculations.
#[arg(long, short = 't', default_value_t = IggyDuration::from_str(DEFAULT_SAMPLING_TIME).unwrap(), value_parser = IggyDuration::from_str)]
pub sampling_time: IggyDuration,
/// Window size for moving average calculations in time series data
#[arg(long, short = 'W', default_value_t = DEFAULT_MOVING_AVERAGE_WINDOW)]
pub moving_average_window: u32,
/// Shutdown iggy-server and remove server local_data directory after the benchmark is finished.
/// Only applicable to local benchmarks.
#[arg(long, default_value_t = DEFAULT_PERFORM_CLEANUP, verbatim_doc_comment)]
pub cleanup: bool,
/// iggy-server executable path.
/// Only applicable to local benchmarks.
#[arg(long, short='e', default_value = None, value_parser = validate_server_executable_path)]
pub server_executable_path: Option<String>,
/// Skip server start.
/// Only applicable to local benchmarks.
#[arg(long, short = 'k', default_value_t = DEFAULT_SKIP_SERVER_START, verbatim_doc_comment)]
pub skip_server_start: bool,
}
fn validate_server_executable_path(v: &str) -> Result<String, String> {
if Path::new(v).exists() {
Ok(v.to_owned())
} else {
Err(format!("Provided server executable '{v}' does not exist."))
}
}
impl IggyBenchArgs {
pub fn transport_command(&self) -> &BenchmarkTransportCommand {
self.benchmark_kind.transport_command()
}
pub fn transport(&self) -> &Transport {
self.benchmark_kind.transport_command().transport()
}
pub fn nodelay(&self) -> bool {
self.benchmark_kind.transport_command().nodelay()
}
pub fn server_address(&self) -> &str {
self.benchmark_kind
.inner()
.transport_command()
.server_address()
}
pub fn start_stream_id(&self) -> u32 {
self.start_stream_id.get()
}
pub fn validate(&self) {
let server_address = self.server_address().parse::<SocketAddr>().unwrap();
if (self.cleanup || self.verbose) && !server_address.ip().is_loopback() {
IggyBenchArgs::command()
.error(
ErrorKind::ArgumentConflict,
format!(
"Cannot use cleanup or verbose flags with a non-loopback server address: {}",
self.server_address()
),
)
.exit();
}
if self.output_dir().is_none()
&& (self.gitref().is_some()
|| self.identifier().is_some()
|| self.remark().is_some()
|| self.extra_info().is_some()
|| self.gitref_date().is_some())
{
IggyBenchArgs::command()
.error(
ErrorKind::ArgumentConflict,
"--git-ref, --git-ref-date, --identifier, --remark, --extra-info can only be used with --output-dir",
)
.exit();
}
self.benchmark_kind.inner().validate()
}
pub fn messages_per_batch(&self) -> u32 {
self.messages_per_batch.get()
}
pub fn message_batches(&self) -> u32 {
self.message_batches.get()
}
pub fn message_size(&self) -> u32 {
self.message_size.get()
}
pub fn streams(&self) -> u32 {
self.benchmark_kind.inner().streams()
}
pub fn number_of_partitions(&self) -> u32 {
self.benchmark_kind.inner().partitions()
}
pub fn consumers(&self) -> u32 {
self.benchmark_kind.inner().consumers()
}
pub fn producers(&self) -> u32 {
self.benchmark_kind.inner().producers()
}
pub fn kind(&self) -> BenchmarkKind {
self.benchmark_kind.as_simple_kind()
}
pub fn number_of_consumer_groups(&self) -> u32 {
self.benchmark_kind.inner().number_of_consumer_groups()
}
pub fn warmup_time(&self) -> IggyDuration {
self.warmup_time
}
pub fn sampling_time(&self) -> IggyDuration {
self.sampling_time
}
pub fn moving_average_window(&self) -> u32 {
self.moving_average_window
}
pub fn rate_limit(&self) -> Option<IggyByteSize> {
self.rate_limit
}
pub fn output_dir(&self) -> Option<String> {
self.benchmark_kind
.inner()
.transport_command()
.output_command()
.as_ref()
.and_then(|cmd| match cmd {
BenchmarkOutputCommand::Output(args) => args.output_dir.clone(),
})
}
pub fn identifier(&self) -> Option<String> {
self.benchmark_kind
.inner()
.transport_command()
.output_command()
.as_ref()
.map(|cmd| match cmd {
BenchmarkOutputCommand::Output(args) => args.identifier.clone(),
})
}
pub fn remark(&self) -> Option<String> {
self.benchmark_kind
.inner()
.transport_command()
.output_command()
.as_ref()
.and_then(|cmd| match cmd {
BenchmarkOutputCommand::Output(args) => args.remark.clone(),
})
}
pub fn extra_info(&self) -> Option<String> {
self.benchmark_kind
.inner()
.transport_command()
.output_command()
.as_ref()
.and_then(|cmd| match cmd {
BenchmarkOutputCommand::Output(args) => args.extra_info.clone(),
})
}
pub fn gitref(&self) -> Option<String> {
self.benchmark_kind
.inner()
.transport_command()
.output_command()
.as_ref()
.and_then(|cmd| match cmd {
BenchmarkOutputCommand::Output(args) => args.gitref.clone(),
})
}
pub fn gitref_date(&self) -> Option<String> {
self.benchmark_kind
.inner()
.transport_command()
.output_command()
.as_ref()
.and_then(|cmd| match cmd {
BenchmarkOutputCommand::Output(args) => args.gitref_date.clone(),
})
}
pub fn open_charts(&self) -> bool {
self.benchmark_kind
.inner()
.transport_command()
.output_command()
.as_ref()
.is_some_and(|cmd| match cmd {
BenchmarkOutputCommand::Output(args) => args.open_charts,
})
}
pub fn max_topic_size(&self) -> Option<IggyByteSize> {
self.benchmark_kind.inner().max_topic_size()
}
/// Generates the output directory name based on benchmark parameters.
pub fn generate_dir_name(&self) -> String {
let benchmark_kind = match &self.benchmark_kind {
BenchmarkKindCommand::PinnedProducer(_) => "pinned_producer",
BenchmarkKindCommand::PinnedConsumer(_) => "pinned_consumer",
BenchmarkKindCommand::PinnedProducerAndConsumer(_) => "pinned_producer_and_consumer",
BenchmarkKindCommand::BalancedProducer(_) => "balanced_producer",
BenchmarkKindCommand::BalancedConsumerGroup(_) => "balanced_consumer_group",
BenchmarkKindCommand::BalancedProducerAndConsumerGroup(_) => {
"balanced_producer_and_consumer"
}
BenchmarkKindCommand::EndToEndProducingConsumer(_) => "end_to_end_producing_consumer",
BenchmarkKindCommand::EndToEndProducingConsumerGroup(_) => {
"end_to_end_producing_consumer_group"
}
BenchmarkKindCommand::Examples => unreachable!(),
};
let transport = match self.transport_command() {
BenchmarkTransportCommand::Tcp(_) => "tcp",
BenchmarkTransportCommand::Quic(_) => "quic",
BenchmarkTransportCommand::Http(_) => "http",
};
let actors = match &self.benchmark_kind {
BenchmarkKindCommand::PinnedProducer(_) => self.producers(),
BenchmarkKindCommand::PinnedConsumer(_) => self.consumers(),
BenchmarkKindCommand::PinnedProducerAndConsumer(_) => {
self.producers() + self.consumers()
}
BenchmarkKindCommand::BalancedProducer(_) => self.producers(),
BenchmarkKindCommand::BalancedConsumerGroup(_) => self.consumers(),
BenchmarkKindCommand::BalancedProducerAndConsumerGroup(_) => {
self.producers() + self.consumers()
}
BenchmarkKindCommand::EndToEndProducingConsumer(_) => self.producers(),
BenchmarkKindCommand::EndToEndProducingConsumerGroup(_) => self.producers(),
BenchmarkKindCommand::Examples => unreachable!(),
};
let mut parts = vec![
benchmark_kind.to_string(),
actors.to_string(),
self.message_size().to_string(),
self.messages_per_batch().to_string(),
self.message_batches().to_string(),
transport.to_string(),
];
if let Some(remark) = &self.remark() {
parts.push(remark.to_string());
}
if let Some(gitref) = &self.gitref() {
parts.push(gitref.to_string());
}
if let Some(identifier) = &self.identifier() {
parts.push(identifier.to_string());
}
parts.join("_")
}
/// Generates a human-readable pretty name for the benchmark
pub fn generate_pretty_name(&self) -> String {
let consumer_or_producer = match &self.benchmark_kind {
BenchmarkKindCommand::PinnedProducer(_) | BenchmarkKindCommand::BalancedProducer(_) => {
format!("{} producers", self.producers())
}
BenchmarkKindCommand::PinnedConsumer(_)
| BenchmarkKindCommand::BalancedConsumerGroup(_) => {
format!("{} consumers", self.consumers())
}
BenchmarkKindCommand::PinnedProducerAndConsumer(_)
| BenchmarkKindCommand::BalancedProducerAndConsumerGroup(_) => format!(
"{} producers/{} consumers",
self.producers(),
self.consumers()
),
BenchmarkKindCommand::EndToEndProducingConsumer(_) => {
format!("{} producing consumers", self.producers(),)
}
BenchmarkKindCommand::EndToEndProducingConsumerGroup(_) => {
format!(
"{} producing consumers/{} consumer groups",
self.producers(),
self.consumers()
)
}
BenchmarkKindCommand::Examples => unreachable!(),
};
let mut name = format!(
"{}, {}B msgs, {} msgs/batch",
consumer_or_producer,
self.message_size(),
self.messages_per_batch(),
);
if let Some(remark) = &self.remark() {
name.push_str(&format!(" ({})", remark));
}
name
}
}
fn recreate_bench_command(args: &IggyBenchArgs) -> String {
let mut parts = Vec::new();
// If using localhost, add env vars
let server_address = args.server_address();
let is_localhost = server_address
.split(':')
.next()
.map(|host| host == "localhost" || host == "127.0.0.1")
.unwrap_or(false);
if is_localhost {
// Get all env vars starting with IGGY_
let iggy_vars: Vec<_> = std::env::vars()
.filter(|(k, _)| k.starts_with("IGGY_"))
.collect();
if !iggy_vars.is_empty() {
info!("Found env vars starting with IGGY_: {:?}", iggy_vars);
parts.extend(iggy_vars.into_iter().map(|(k, v)| format!("{}={}", k, v)));
}
}
parts.push("iggy-bench".to_string());
let messages_per_batch = args.messages_per_batch();
if messages_per_batch != DEFAULT_MESSAGES_PER_BATCH.get() {
parts.push(format!("--messages-per-batch {}", messages_per_batch));
}
let message_batches = args.message_batches();
if message_batches != DEFAULT_MESSAGE_BATCHES.get() {
parts.push(format!("--message-batches {}", message_batches));
}
let message_size = args.message_size();
if message_size != DEFAULT_MESSAGE_SIZE.get() {
parts.push(format!("--message-size {}", message_size));
}
if let Some(rate_limit) = args.rate_limit() {
parts.push(format!("--rate-limit \'{}\'", rate_limit));
}
if args.warmup_time().to_string() != DEFAULT_WARMUP_TIME {
parts.push(format!("--warmup-time \'{}\'", args.warmup_time()));
}
let kind_str = match args.benchmark_kind.as_simple_kind() {
BenchmarkKind::PinnedProducer => "pinned-producer",
BenchmarkKind::PinnedConsumer => "pinned-consumer",
BenchmarkKind::PinnedProducerAndConsumer => "pinned-producer-and-consumer",
BenchmarkKind::BalancedProducer => "balanced-producer",
BenchmarkKind::BalancedConsumerGroup => "balanced-consumer-group",
BenchmarkKind::BalancedProducerAndConsumerGroup => "balanced-producer-and-consumer-group",
BenchmarkKind::EndToEndProducingConsumer => "end-to-end-producing-consumer",
BenchmarkKind::EndToEndProducingConsumerGroup => "end-to-end-producing-consumer-group",
};
parts.push(kind_str.to_string());
// Add benchmark params, skipping defaults
let producers = args.producers();
let consumers = args.consumers();
let number_of_consumer_groups = args.number_of_consumer_groups();
match args.benchmark_kind.as_simple_kind() {
BenchmarkKind::PinnedProducer
| BenchmarkKind::BalancedProducer
| BenchmarkKind::EndToEndProducingConsumer => {
if producers != DEFAULT_NUMBER_OF_PRODUCERS.get() {
parts.push(format!("--producers {}", producers));
}
}
BenchmarkKind::PinnedConsumer | BenchmarkKind::BalancedConsumerGroup => {
if consumers != DEFAULT_NUMBER_OF_CONSUMERS.get() {
parts.push(format!("--consumers {}", consumers));
}
}
BenchmarkKind::PinnedProducerAndConsumer
| BenchmarkKind::BalancedProducerAndConsumerGroup => {
if producers != DEFAULT_NUMBER_OF_PRODUCERS.get() {
parts.push(format!("--producers {}", producers));
}
if consumers != DEFAULT_NUMBER_OF_CONSUMERS.get() {
parts.push(format!("--consumers {}", consumers));
}
}
BenchmarkKind::EndToEndProducingConsumerGroup => {
if producers != DEFAULT_NUMBER_OF_PRODUCERS.get() {
parts.push(format!("--producers {}", producers));
}
if consumers != DEFAULT_NUMBER_OF_CONSUMERS.get() {
parts.push(format!("--consumers {}", consumers));
}
if number_of_consumer_groups != DEFAULT_NUMBER_OF_CONSUMER_GROUPS.get() {
parts.push(format!("--consumer-groups {}", number_of_consumer_groups));
}
}
}
let streams = args.streams();
let default_streams = match args.benchmark_kind.as_simple_kind() {
BenchmarkKind::BalancedProducerAndConsumerGroup
| BenchmarkKind::BalancedConsumerGroup
| BenchmarkKind::BalancedProducer => DEFAULT_BALANCED_NUMBER_OF_STREAMS.get(),
_ => DEFAULT_PINNED_NUMBER_OF_STREAMS.get(),
};
if streams != default_streams {
parts.push(format!("--streams {}", streams));
}
let partitions = args.number_of_partitions();
let default_partitions = match args.benchmark_kind.as_simple_kind() {
BenchmarkKind::BalancedProducerAndConsumerGroup
| BenchmarkKind::BalancedConsumerGroup
| BenchmarkKind::BalancedProducer => DEFAULT_BALANCED_NUMBER_OF_PARTITIONS.get(),
_ => DEFAULT_PINNED_NUMBER_OF_PARTITIONS.get(),
};
if partitions != default_partitions {
parts.push(format!("--partitions {}", partitions));
}
let consumer_groups = args.number_of_consumer_groups();
if args.benchmark_kind.as_simple_kind() == BenchmarkKind::BalancedConsumerGroup
|| args.benchmark_kind.as_simple_kind() == BenchmarkKind::BalancedProducerAndConsumerGroup
&& consumer_groups != DEFAULT_NUMBER_OF_CONSUMER_GROUPS.get()
{
parts.push(format!("--consumer-groups {}", consumer_groups));
}
if let Some(max_topic_size) = args.max_topic_size() {
parts.push(format!("--max-topic-size \'{}\'", max_topic_size));
}
// Add transport and server address, skipping if default
let transport = args.transport().to_string().to_lowercase();
parts.push(transport.clone());
let default_address = match transport.as_str() {
"tcp" => DEFAULT_TCP_SERVER_ADDRESS,
"quic" => DEFAULT_QUIC_SERVER_ADDRESS,
"http" => DEFAULT_HTTP_SERVER_ADDRESS,
_ => "",
};
if server_address != default_address {
parts.push(format!("--server-address {}", server_address));
}
parts.push("output".to_string());
parts.push("-o performance_results".to_string());
let remark = args.remark();
if let Some(remark) = remark {
parts.push(format!("--remark \'{}\'", remark));
}
parts.join(" ")
}
impl From<&IggyBenchArgs> for BenchmarkParams {
fn from(args: &IggyBenchArgs) -> Self {
let benchmark_kind = args.benchmark_kind.as_simple_kind();
// Ugly conversion but let it stay here to have `iggy-bench-report` not depend on `iggy` or `integration`
let transport = match args.transport() {
Transport::Tcp => BenchmarkTransport::Tcp,
Transport::Quic => BenchmarkTransport::Quic,
Transport::Http => BenchmarkTransport::Http,
};
let server_address = args.server_address().to_string();
let remark = args.remark();
let extra_info = args.extra_info();
let gitref = args.gitref();
let gitref_date = args.gitref_date();
let messages_per_batch = args.messages_per_batch();
let message_batches = args.message_batches();
let message_size = args.message_size();
let producers = args.producers();
let consumers = args.consumers();
let streams = args.streams();
let partitions = args.number_of_partitions();
let consumer_groups = args.number_of_consumer_groups();
let rate_limit = args.rate_limit().map(|limit| limit.to_string());
let pretty_name = args.generate_pretty_name();
let bench_command = recreate_bench_command(args);
let remark_for_identifier = remark
.clone()
.unwrap_or("no_remark".to_string())
.replace(' ', "_");
let params_identifier = vec![
benchmark_kind.to_string(),
transport.to_string(),
remark_for_identifier,
messages_per_batch.to_string(),
message_batches.to_string(),
message_size.to_string(),
producers.to_string(),
consumers.to_string(),
streams.to_string(),
partitions.to_string(),
consumer_groups.to_string(),
];
let params_identifier = params_identifier.join("_");
BenchmarkParams {
benchmark_kind,
transport,
server_address,
remark,
extra_info,
gitref,
gitref_date,
messages_per_batch,
message_batches,
message_size,
producers,
consumers,
streams,
partitions,
consumer_groups,
rate_limit,
pretty_name,
bench_command,
params_identifier,
}
}
}