use std::{
fmt::{Debug, Display},
hash::Hash,
str::FromStr,
time::Duration,
};
use serde::{de::DeserializeOwned, Deserialize, Serialize};
use crate::{faults::FaultsType, measurement::MeasurementsCollection};
pub trait BenchmarkType:
Serialize
+ DeserializeOwned
+ Default
+ Clone
+ FromStr
+ Display
+ Debug
+ PartialEq
+ Eq
+ Hash
+ PartialOrd
+ Ord
+ FromStr
{
}
#[derive(Serialize, Deserialize, Clone)]
pub struct BenchmarkParameters<T> {
pub benchmark_type: T,
pub nodes: usize,
pub faults: FaultsType,
pub load: usize,
pub duration: Duration,
}
impl<T: BenchmarkType> Default for BenchmarkParameters<T> {
fn default() -> Self {
Self {
benchmark_type: T::default(),
nodes: 4,
faults: FaultsType::default(),
load: 500,
duration: Duration::from_secs(60),
}
}
}
impl<T: BenchmarkType> Debug for BenchmarkParameters<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{:?}-{:?}-{}-{}",
self.benchmark_type, self.faults, self.nodes, self.load
)
}
}
impl<T> Display for BenchmarkParameters<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{} nodes ({}) - {} tx/s",
self.nodes, self.faults, self.load
)
}
}
impl<T> BenchmarkParameters<T> {
pub fn new(
benchmark_type: T,
nodes: usize,
faults: FaultsType,
load: usize,
duration: Duration,
) -> Self {
Self {
benchmark_type,
nodes,
faults,
load,
duration,
}
}
}
pub enum LoadType {
Fixed(Vec<usize>),
#[allow(dead_code)]
Search {
starting_load: usize,
max_iterations: usize,
},
}
pub struct BenchmarkParametersGenerator<T> {
benchmark_type: T,
pub nodes: usize,
load_type: LoadType,
pub faults: FaultsType,
duration: Duration,
next_load: Option<usize>,
lower_bound_result: Option<MeasurementsCollection<T>>,
upper_bound_result: Option<MeasurementsCollection<T>>,
iterations: usize,
}
impl<T: BenchmarkType> Iterator for BenchmarkParametersGenerator<T> {
type Item = BenchmarkParameters<T>;
fn next(&mut self) -> Option<Self::Item> {
self.next_load.map(|load| {
BenchmarkParameters::new(
self.benchmark_type.clone(),
self.nodes,
self.faults.clone(),
load,
self.duration,
)
})
}
}
impl<T: BenchmarkType> BenchmarkParametersGenerator<T> {
const DEFAULT_DURATION: Duration = Duration::from_secs(180);
pub fn new(nodes: usize, mut load_type: LoadType) -> Self {
let next_load = match &mut load_type {
LoadType::Fixed(loads) => {
if loads.is_empty() {
None
} else {
Some(loads.remove(0))
}
}
LoadType::Search { starting_load, .. } => Some(*starting_load),
};
Self {
benchmark_type: T::default(),
nodes,
load_type,
faults: FaultsType::default(),
duration: Self::DEFAULT_DURATION,
next_load,
lower_bound_result: None,
upper_bound_result: None,
iterations: 0,
}
}
pub fn with_benchmark_type(mut self, benchmark_type: T) -> Self {
self.benchmark_type = benchmark_type;
self
}
pub fn with_faults(mut self, faults: FaultsType) -> Self {
self.faults = faults;
self
}
pub fn with_custom_duration(mut self, duration: Duration) -> Self {
self.duration = duration;
self
}
fn out_of_capacity(
last_result: &MeasurementsCollection<T>,
new_result: &MeasurementsCollection<T>,
) -> bool {
let threshold = last_result.aggregate_average_latency() * 5;
let high_latency = new_result.aggregate_average_latency() > threshold;
let last_load = new_result.transaction_load() as u64;
let no_throughput_increase = new_result.aggregate_tps() < (2 * last_load / 3);
high_latency || no_throughput_increase
}
pub fn register_result(&mut self, result: MeasurementsCollection<T>) {
self.next_load = match &mut self.load_type {
LoadType::Fixed(loads) => {
if loads.is_empty() {
None
} else {
Some(loads.remove(0))
}
}
LoadType::Search { max_iterations, .. } => {
if self.iterations >= *max_iterations {
None
} else {
self.iterations += 1;
match (&mut self.lower_bound_result, &mut self.upper_bound_result) {
(None, None) => {
let next = result.transaction_load() * 2;
self.lower_bound_result = Some(result);
Some(next)
}
(Some(lower), None) => {
if Self::out_of_capacity(lower, &result) {
let next =
(lower.transaction_load() + result.transaction_load()) / 2;
self.upper_bound_result = Some(result);
Some(next)
} else {
let next = result.transaction_load() * 2;
*lower = result;
Some(next)
}
}
(Some(lower), Some(upper)) => {
if Self::out_of_capacity(lower, &result) {
*upper = result;
} else {
*lower = result;
}
Some((lower.transaction_load() + upper.transaction_load()) / 2)
}
_ => panic!("Benchmark parameters generator is in an incoherent state"),
}
}
}
};
}
}
#[cfg(test)]
pub mod test {
use std::{fmt::Display, str::FromStr};
use serde::{Deserialize, Serialize};
use crate::{
measurement::{Measurement, MeasurementsCollection},
settings::Settings,
};
use super::{BenchmarkParametersGenerator, BenchmarkType, LoadType};
#[derive(
Serialize, Deserialize, Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash, Default,
)]
pub struct TestBenchmarkType;
impl Display for TestBenchmarkType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "TestBenchmarkType")
}
}
impl FromStr for TestBenchmarkType {
type Err = ();
fn from_str(_s: &str) -> Result<Self, Self::Err> {
Ok(Self {})
}
}
impl BenchmarkType for TestBenchmarkType {}
#[test]
fn set_lower_bound() {
let settings = Settings::new_for_test();
let nodes = 4;
let load = LoadType::Search {
starting_load: 100,
max_iterations: 10,
};
let mut generator = BenchmarkParametersGenerator::<TestBenchmarkType>::new(nodes, load);
let parameters = generator.next().unwrap();
let collection = MeasurementsCollection::new(&settings, parameters);
generator.register_result(collection);
let next_parameters = generator.next();
assert!(next_parameters.is_some());
assert_eq!(next_parameters.unwrap().load, 200);
assert!(generator.lower_bound_result.is_some());
assert_eq!(
generator.lower_bound_result.unwrap().transaction_load(),
100
);
assert!(generator.upper_bound_result.is_none());
}
#[test]
fn set_upper_bound() {
let settings = Settings::new_for_test();
let nodes = 4;
let load = LoadType::Search {
starting_load: 100,
max_iterations: 10,
};
let mut generator = BenchmarkParametersGenerator::<TestBenchmarkType>::new(nodes, load);
let first_parameters = generator.next().unwrap();
let collection = MeasurementsCollection::new(&settings, first_parameters);
generator.register_result(collection);
let second_parameters = generator.next().unwrap();
let mut collection = MeasurementsCollection::new(&settings, second_parameters);
let measurement = Measurement::new_for_test();
collection.scrapers.insert(1, vec![measurement]);
generator.register_result(collection);
let third_parameters = generator.next();
assert!(third_parameters.is_some());
assert_eq!(third_parameters.unwrap().load, 150);
assert!(generator.lower_bound_result.is_some());
assert_eq!(
generator.lower_bound_result.unwrap().transaction_load(),
100
);
assert!(generator.upper_bound_result.is_some());
assert_eq!(
generator.upper_bound_result.unwrap().transaction_load(),
200
);
}
#[test]
fn max_iterations() {
let settings = Settings::new_for_test();
let nodes = 4;
let load = LoadType::Search {
starting_load: 100,
max_iterations: 0,
};
let mut generator = BenchmarkParametersGenerator::<TestBenchmarkType>::new(nodes, load);
let parameters = generator.next().unwrap();
let collection = MeasurementsCollection::new(&settings, parameters);
generator.register_result(collection);
let next_parameters = generator.next();
assert!(next_parameters.is_none());
}
}