#![cfg_attr(docsrs, feature(doc_cfg))]
#![warn(missing_docs)]
#![warn(rust_2018_idioms)]
use std::sync::Arc;
use std::time::{Duration, Instant};
use dev_report::{CheckResult, Severity};
pub trait Workload: Send + Sync {
fn run_once(&self);
}
pub struct StressRun {
name: String,
iterations: usize,
threads: usize,
}
impl StressRun {
pub fn new(name: impl Into<String>) -> Self {
Self {
name: name.into(),
iterations: 1_000,
threads: 1,
}
}
pub fn iterations(mut self, n: usize) -> Self {
self.iterations = n;
self
}
pub fn threads(mut self, n: usize) -> Self {
self.threads = n.max(1);
self
}
pub fn execute<W: Workload + 'static>(&self, workload: &W) -> StressResult
where
W: Clone,
{
let per_thread = self.iterations / self.threads;
let leftover = self.iterations % self.threads;
let started = Instant::now();
let mut handles = Vec::with_capacity(self.threads);
let workload = Arc::new(workload.clone());
for t in 0..self.threads {
let count = per_thread + if t < leftover { 1 } else { 0 };
let w = workload.clone();
handles.push(std::thread::spawn(move || {
let start = Instant::now();
for _ in 0..count {
w.run_once();
}
start.elapsed()
}));
}
let mut thread_times = Vec::with_capacity(self.threads);
for h in handles {
thread_times.push(h.join().unwrap());
}
let total_elapsed = started.elapsed();
StressResult {
name: self.name.clone(),
iterations: self.iterations,
threads: self.threads,
total_elapsed,
thread_times,
}
}
}
#[derive(Debug, Clone)]
pub struct StressResult {
pub name: String,
pub iterations: usize,
pub threads: usize,
pub total_elapsed: Duration,
pub thread_times: Vec<Duration>,
}
impl StressResult {
pub fn ops_per_sec(&self) -> f64 {
if self.total_elapsed.is_zero() {
return 0.0;
}
self.iterations as f64 / self.total_elapsed.as_secs_f64()
}
pub fn thread_time_cv(&self) -> f64 {
if self.thread_times.is_empty() {
return 0.0;
}
let n = self.thread_times.len() as f64;
let mean: f64 = self
.thread_times
.iter()
.map(|d| d.as_secs_f64())
.sum::<f64>()
/ n;
if mean == 0.0 {
return 0.0;
}
let var = self
.thread_times
.iter()
.map(|d| (d.as_secs_f64() - mean).powi(2))
.sum::<f64>()
/ n;
var.sqrt() / mean
}
pub fn into_check_result(self, baseline_ops_per_sec: Option<f64>) -> CheckResult {
let ops = self.ops_per_sec();
let cv = self.thread_time_cv();
let detail = format!(
"iterations={}, threads={}, total={:.3}s, ops/sec={:.0}, thread_cv={:.3}",
self.iterations,
self.threads,
self.total_elapsed.as_secs_f64(),
ops,
cv
);
let name = format!("stress::{}", self.name);
match baseline_ops_per_sec {
None => CheckResult::pass(name).with_detail(detail),
Some(baseline) if ops < baseline * 0.9 => {
CheckResult::fail(name, Severity::Warning).with_detail(detail)
}
Some(_) => CheckResult::pass(name).with_detail(detail),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[derive(Clone)]
struct Noop;
impl Workload for Noop {
fn run_once(&self) {
std::hint::black_box(1 + 1);
}
}
#[test]
fn run_completes() {
let run = StressRun::new("noop").iterations(1_000).threads(2);
let r = run.execute(&Noop);
assert_eq!(r.iterations, 1_000);
assert_eq!(r.threads, 2);
assert!(r.ops_per_sec() > 0.0);
}
#[test]
fn no_baseline_passes() {
let run = StressRun::new("noop").iterations(100).threads(1);
let r = run.execute(&Noop);
let c = r.into_check_result(None);
assert!(matches!(c.verdict, dev_report::Verdict::Pass));
}
}