Got a ton of benchmarking and stress testing code spit out by the LLM. Did a not-so-thorough vetting, not ready for merge into master, but it mostly makes sense. Needs to actually fail on the stress tests, not anticipate failure and call it success. The benchmarks also need a more thorough going over, in order to validate that they make sense. Then we can work on improving our metrics.
199 lines
5.1 KiB
Rust
199 lines
5.1 KiB
Rust
//! Stress test utilities and result reporting.
|
|
//!
|
|
//! Provides a simple framework for stress tests with JSON + pretty output.
|
|
|
|
#![allow(dead_code)] // Utilities may not all be used in every test
|
|
|
|
pub mod concurrency;
|
|
pub mod saturation;
|
|
|
|
use std::time::{Duration, Instant};
|
|
|
|
/// Results from a stress test
|
|
#[derive(Debug)]
|
|
pub struct StressResult {
|
|
pub name: String,
|
|
pub duration: Duration,
|
|
pub operations: u64,
|
|
pub successes: u64,
|
|
pub failures: u64,
|
|
pub notes: Vec<String>,
|
|
}
|
|
|
|
impl StressResult {
|
|
pub fn new(name: impl Into<String>) -> Self {
|
|
Self {
|
|
name: name.into(),
|
|
duration: Duration::ZERO,
|
|
operations: 0,
|
|
successes: 0,
|
|
failures: 0,
|
|
notes: Vec::new(),
|
|
}
|
|
}
|
|
|
|
pub fn failure_rate(&self) -> f64 {
|
|
if self.operations == 0 {
|
|
0.0
|
|
} else {
|
|
(self.failures as f64 / self.operations as f64) * 100.0
|
|
}
|
|
}
|
|
|
|
pub fn throughput(&self) -> f64 {
|
|
let secs = self.duration.as_secs_f64();
|
|
if secs > 0.0 {
|
|
self.operations as f64 / secs
|
|
} else {
|
|
0.0
|
|
}
|
|
}
|
|
|
|
pub fn note(&mut self, msg: impl Into<String>) {
|
|
self.notes.push(msg.into());
|
|
}
|
|
|
|
pub fn print(&self) {
|
|
println!("\n{}", "=".repeat(60));
|
|
println!(" STRESS: {}", self.name);
|
|
println!("{}", "=".repeat(60));
|
|
println!(" Duration: {:?}", self.duration);
|
|
println!(" Operations: {}", self.operations);
|
|
println!(" Successes: {}", self.successes);
|
|
println!(" Failures: {}", self.failures);
|
|
println!(" Failure Rate: {:.2}%", self.failure_rate());
|
|
println!(" Throughput: {:.2} ops/sec", self.throughput());
|
|
|
|
if !self.notes.is_empty() {
|
|
println!();
|
|
println!(" Notes:");
|
|
for note in &self.notes {
|
|
println!(" - {}", note);
|
|
}
|
|
}
|
|
println!("{}", "=".repeat(60));
|
|
}
|
|
|
|
pub fn to_json(&self) -> String {
|
|
format!(
|
|
r#"{{"name":"{}","duration_ms":{},"operations":{},"successes":{},"failures":{},"failure_rate_pct":{:.2},"throughput":{:.2},"notes":{:?}}}"#,
|
|
self.name,
|
|
self.duration.as_millis(),
|
|
self.operations,
|
|
self.successes,
|
|
self.failures,
|
|
self.failure_rate(),
|
|
self.throughput(),
|
|
self.notes
|
|
)
|
|
}
|
|
}
|
|
|
|
/// A simple stress test runner
|
|
pub struct Stress {
|
|
name: String,
|
|
duration: Option<Duration>,
|
|
iterations: Option<u64>,
|
|
}
|
|
|
|
impl Stress {
|
|
pub fn new(name: impl Into<String>) -> Self {
|
|
Self {
|
|
name: name.into(),
|
|
duration: None,
|
|
iterations: None,
|
|
}
|
|
}
|
|
|
|
/// Run for a fixed duration
|
|
pub fn for_duration(mut self, d: Duration) -> Self {
|
|
self.duration = Some(d);
|
|
self
|
|
}
|
|
|
|
/// Run for a fixed number of iterations
|
|
pub fn for_iterations(mut self, n: u64) -> Self {
|
|
self.iterations = Some(n);
|
|
self
|
|
}
|
|
|
|
/// Run the stress test, counting successes and failures
|
|
pub fn run<F>(self, mut f: F) -> StressResult
|
|
where
|
|
F: FnMut() -> bool, // returns true on success, false on failure
|
|
{
|
|
let mut result = StressResult::new(&self.name);
|
|
let start = Instant::now();
|
|
|
|
match (self.duration, self.iterations) {
|
|
(Some(duration), _) => {
|
|
while start.elapsed() < duration {
|
|
if f() {
|
|
result.successes += 1;
|
|
} else {
|
|
result.failures += 1;
|
|
}
|
|
result.operations += 1;
|
|
}
|
|
}
|
|
(None, Some(iterations)) => {
|
|
for _ in 0..iterations {
|
|
if f() {
|
|
result.successes += 1;
|
|
} else {
|
|
result.failures += 1;
|
|
}
|
|
result.operations += 1;
|
|
}
|
|
}
|
|
(None, None) => {
|
|
// Default: 1000 iterations
|
|
for _ in 0..1000 {
|
|
if f() {
|
|
result.successes += 1;
|
|
} else {
|
|
result.failures += 1;
|
|
}
|
|
result.operations += 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
result.duration = start.elapsed();
|
|
result
|
|
}
|
|
}
|
|
|
|
// Test actors used across stress tests
|
|
use swactor::{actor::ActorInterface, runtime::Runtime};
|
|
|
|
/// An actor that just absorbs messages
|
|
pub struct BlackHole;
|
|
|
|
#[derive(Clone)]
|
|
pub struct Msg;
|
|
|
|
impl ActorInterface for BlackHole {
|
|
type Incoming = Msg;
|
|
type Response = ();
|
|
fn handle(&mut self, _ctx: &Runtime, _msg: Msg) {}
|
|
}
|
|
|
|
/// An actor that counts messages received
|
|
pub struct Counter {
|
|
pub count: usize,
|
|
}
|
|
|
|
impl Counter {
|
|
pub fn new() -> Self {
|
|
Self { count: 0 }
|
|
}
|
|
}
|
|
|
|
impl ActorInterface for Counter {
|
|
type Incoming = Msg;
|
|
type Response = ();
|
|
fn handle(&mut self, _ctx: &Runtime, _msg: Msg) {
|
|
self.count += 1;
|
|
}
|
|
}
|