1#![cfg_attr(docsrs, feature(doc_cfg))]
38#![warn(missing_docs)]
39#![warn(rust_2018_idioms)]
40
41use std::sync::Arc;
42use std::time::{Duration, Instant};
43
44use dev_report::{CheckResult, Severity};
45
46pub trait Workload: Send + Sync {
48 fn run_once(&self);
51}
52
53pub struct StressRun {
55 name: String,
56 iterations: usize,
57 threads: usize,
58}
59
60impl StressRun {
61 pub fn new(name: impl Into<String>) -> Self {
63 Self {
64 name: name.into(),
65 iterations: 1_000,
66 threads: 1,
67 }
68 }
69
70 pub fn iterations(mut self, n: usize) -> Self {
72 self.iterations = n;
73 self
74 }
75
76 pub fn threads(mut self, n: usize) -> Self {
78 self.threads = n.max(1);
79 self
80 }
81
82 pub fn execute<W: Workload + 'static>(&self, workload: &W) -> StressResult
84 where
85 W: Clone,
86 {
87 let per_thread = self.iterations / self.threads;
88 let leftover = self.iterations % self.threads;
89 let started = Instant::now();
90 let mut handles = Vec::with_capacity(self.threads);
91 let workload = Arc::new(workload.clone());
92
93 for t in 0..self.threads {
94 let count = per_thread + if t < leftover { 1 } else { 0 };
95 let w = workload.clone();
96 handles.push(std::thread::spawn(move || {
97 let start = Instant::now();
98 for _ in 0..count {
99 w.run_once();
100 }
101 start.elapsed()
102 }));
103 }
104
105 let mut thread_times = Vec::with_capacity(self.threads);
106 for h in handles {
107 thread_times.push(h.join().unwrap());
108 }
109 let total_elapsed = started.elapsed();
110
111 StressResult {
112 name: self.name.clone(),
113 iterations: self.iterations,
114 threads: self.threads,
115 total_elapsed,
116 thread_times,
117 }
118 }
119}
120
121#[derive(Debug, Clone)]
123pub struct StressResult {
124 pub name: String,
126 pub iterations: usize,
128 pub threads: usize,
130 pub total_elapsed: Duration,
132 pub thread_times: Vec<Duration>,
134}
135
136impl StressResult {
137 pub fn ops_per_sec(&self) -> f64 {
139 if self.total_elapsed.is_zero() {
140 return 0.0;
141 }
142 self.iterations as f64 / self.total_elapsed.as_secs_f64()
143 }
144
145 pub fn thread_time_cv(&self) -> f64 {
148 if self.thread_times.is_empty() {
149 return 0.0;
150 }
151 let n = self.thread_times.len() as f64;
152 let mean: f64 = self
153 .thread_times
154 .iter()
155 .map(|d| d.as_secs_f64())
156 .sum::<f64>()
157 / n;
158 if mean == 0.0 {
159 return 0.0;
160 }
161 let var = self
162 .thread_times
163 .iter()
164 .map(|d| (d.as_secs_f64() - mean).powi(2))
165 .sum::<f64>()
166 / n;
167 var.sqrt() / mean
168 }
169
170 pub fn into_check_result(self, baseline_ops_per_sec: Option<f64>) -> CheckResult {
173 let ops = self.ops_per_sec();
174 let cv = self.thread_time_cv();
175 let detail = format!(
176 "iterations={}, threads={}, total={:.3}s, ops/sec={:.0}, thread_cv={:.3}",
177 self.iterations,
178 self.threads,
179 self.total_elapsed.as_secs_f64(),
180 ops,
181 cv
182 );
183 let name = format!("stress::{}", self.name);
184 match baseline_ops_per_sec {
185 None => CheckResult::pass(name).with_detail(detail),
186 Some(baseline) if ops < baseline * 0.9 => {
187 CheckResult::fail(name, Severity::Warning).with_detail(detail)
188 }
189 Some(_) => CheckResult::pass(name).with_detail(detail),
190 }
191 }
192}
193
194#[cfg(test)]
195mod tests {
196 use super::*;
197
198 #[derive(Clone)]
199 struct Noop;
200 impl Workload for Noop {
201 fn run_once(&self) {
202 std::hint::black_box(1 + 1);
203 }
204 }
205
206 #[test]
207 fn run_completes() {
208 let run = StressRun::new("noop").iterations(1_000).threads(2);
209 let r = run.execute(&Noop);
210 assert_eq!(r.iterations, 1_000);
211 assert_eq!(r.threads, 2);
212 assert!(r.ops_per_sec() > 0.0);
213 }
214
215 #[test]
216 fn no_baseline_passes() {
217 let run = StressRun::new("noop").iterations(100).threads(1);
218 let r = run.execute(&Noop);
219 let c = r.into_check_result(None);
220 assert!(matches!(c.verdict, dev_report::Verdict::Pass));
221 }
222}