blob: 8c1c535a5c5b8c3457b1d1b35fca7568008ab0c4 [file]
/* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
use super::kind::BenchmarkKindCommand;
use super::output::BenchmarkOutputCommand;
use super::props::{BenchmarkKindProps, BenchmarkTransportProps};
use super::{
defaults::{
DEFAULT_MESSAGE_BATCHES, DEFAULT_MESSAGE_SIZE, DEFAULT_MESSAGES_PER_BATCH,
DEFAULT_MOVING_AVERAGE_WINDOW, DEFAULT_SAMPLING_TIME, DEFAULT_WARMUP_TIME,
},
transport::BenchmarkTransportCommand,
};
use bench_report::benchmark_kind::BenchmarkKind;
use bench_report::numeric_parameter::BenchmarkNumericParameter;
use clap::error::ErrorKind;
use clap::{CommandFactory, Parser};
use iggy::prelude::{
DEFAULT_ROOT_PASSWORD, DEFAULT_ROOT_USERNAME, IggyByteSize, IggyDuration, IggyExpiry,
TransportProtocol,
};
use std::num::NonZeroU32;
use std::str::FromStr;
#[derive(Parser, Debug)]
#[command(author, version, about, long_about = None)]
#[allow(clippy::struct_excessive_bools)]
pub struct IggyBenchArgs {
/// Benchmark kind
#[command(subcommand)]
pub benchmark_kind: BenchmarkKindCommand,
/// Message size in bytes. Accepts either a single value or a range (e.g. "200..500")
#[arg(long, short = 'm', value_parser = BenchmarkNumericParameter::from_str, default_value_t = BenchmarkNumericParameter::Value(DEFAULT_MESSAGE_SIZE.get()))]
pub message_size: BenchmarkNumericParameter,
/// Number of messages per batch
#[arg(long, short = 'P', value_parser = BenchmarkNumericParameter::from_str, default_value_t = BenchmarkNumericParameter::Value(DEFAULT_MESSAGES_PER_BATCH.get()))]
pub messages_per_batch: BenchmarkNumericParameter,
/// Number of message batches per actor (producer / consumer / producing consumer).
/// This argument is mutually exclusive with `total_messages_size`.
#[arg(long, short = 'b', group = "data_to_process")]
pub message_batches: Option<NonZeroU32>,
/// Total size of all messages to process in bytes (aggregate, for all actors).
/// This argument is mutually exclusive with `message_batches`.
#[arg(long, short = 'T', group = "data_to_process")]
pub total_data: Option<IggyByteSize>,
/// Optional total rate limit (aggregate, for all actors)
/// 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,
/// 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,
/// Use high-level API for actors
#[arg(long, short = 'H', default_value_t = false)]
pub high_level_api: bool,
/// Username for server authentication
#[arg(long, short = 'u', default_value_t = DEFAULT_ROOT_USERNAME.to_string())]
pub username: String,
/// Password for server authentication
#[arg(long, short = 'p', default_value_t = DEFAULT_ROOT_PASSWORD.to_string())]
pub password: String,
}
impl IggyBenchArgs {
pub fn transport_command(&self) -> &BenchmarkTransportCommand {
self.benchmark_kind.transport_command()
}
pub fn transport(&self) -> &TransportProtocol {
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 validate(&mut self) {
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())
{
Self::command()
.error(
ErrorKind::ArgumentConflict,
"--git-ref, --git-ref-date, --identifier, --remark, --extra-info can only be used with --output-dir",
)
.exit();
}
if (self.message_batches, self.total_data) == (None, None) {
self.message_batches = Some(DEFAULT_MESSAGE_BATCHES);
}
if let Some(total_data) = self.total_data {
let samples = total_data.as_bytes_u64() / u64::from(self.message_size().min());
if samples <= 1 {
Self::command()
.error(
ErrorKind::ArgumentConflict,
"--total-messages-size must be at least 2x greater than --message-size",
)
.exit();
}
}
if self.high_level_api && !self.messages_per_batch.is_fixed() {
Self::command()
.error(
ErrorKind::ArgumentConflict,
"High-level consumer API (--high-level-api) requires fixed batch size, but random batch size was specified. Use a single value instead of a range for --messages-per-batch.",
)
.exit();
}
self.benchmark_kind.inner().validate();
}
pub const fn messages_per_batch(&self) -> BenchmarkNumericParameter {
self.messages_per_batch
}
pub const fn message_batches(&self) -> Option<NonZeroU32> {
self.message_batches
}
pub const fn message_size(&self) -> BenchmarkNumericParameter {
self.message_size
}
pub const fn total_data(&self) -> Option<IggyByteSize> {
self.total_data
}
// Used only for generation of unique directory name
pub fn data_volume_identifier(&self) -> String {
self.total_data().map_or_else(
|| self.message_batches().unwrap().to_string(),
|total_messages_size| format!("{}B", total_messages_size.as_bytes_u64()),
)
}
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 const fn warmup_time(&self) -> IggyDuration {
self.warmup_time
}
pub const fn sampling_time(&self) -> IggyDuration {
self.sampling_time
}
pub const fn moving_average_window(&self) -> u32 {
self.moving_average_window
}
pub const 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()
.map(|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()
}
pub fn message_expiry(&self) -> IggyExpiry {
self.benchmark_kind.inner().message_expiry()
}
pub fn read_amplification(&self) -> Option<f32> {
self.benchmark_kind.inner().read_amplification()
}
pub const fn high_level_api(&self) -> bool {
self.high_level_api
}
pub fn username(&self) -> &str {
&self.username
}
pub fn password(&self) -> &str {
&self.password
}
/// 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",
BenchmarkTransportCommand::WebSocket(_) => "ws",
};
let actors = match &self.benchmark_kind {
BenchmarkKindCommand::PinnedProducer(_)
| BenchmarkKindCommand::BalancedProducer(_)
| BenchmarkKindCommand::EndToEndProducingConsumer(_)
| BenchmarkKindCommand::EndToEndProducingConsumerGroup(_) => self.producers(),
BenchmarkKindCommand::PinnedConsumer(_)
| BenchmarkKindCommand::BalancedConsumerGroup(_) => self.consumers(),
BenchmarkKindCommand::PinnedProducerAndConsumer(_)
| BenchmarkKindCommand::BalancedProducerAndConsumerGroup(_) => {
self.producers() + self.consumers()
}
BenchmarkKindCommand::Examples => unreachable!(),
};
let data_volume_arg = match (self.total_data, self.message_batches) {
(Some(total), None) => format!("{total}"),
(None, Some(batches)) => format!("{batches}"),
_ => unreachable!(),
};
let mut parts = vec![
benchmark_kind.to_string(),
actors.to_string(),
self.message_size().to_string(),
self.messages_per_batch().to_string(),
data_volume_arg,
transport.to_string(),
];
if let Some(remark) = &self.remark() {
parts.push(remark.clone());
}
if let Some(gitref) = &self.gitref() {
parts.push(gitref.clone());
}
if let Some(identifier) = &self.identifier() {
parts.push(identifier.clone());
}
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 = format!("{name} ({remark})");
}
name
}
}