use serde::{Deserialize, Serialize};
use crate::ixit::Environment;
use crate::perf::OperationMeasurement;
use crate::perf_run::corpus::SeededCorpus;
use crate::perf_run::schedule::JourneyWorkload;
use crate::perf_run::window::run_window;
#[derive(Debug, Clone)]
pub struct StressOptions {
pub start_rate: f64,
pub max_rate: f64,
pub step_warmup_s: u64,
pub step_hold_s: u64,
pub bisections: u32,
pub p99_budget_ms: f64,
pub error_budget: f64,
}
impl Default for StressOptions {
fn default() -> Self {
Self {
start_rate: 2.0,
max_rate: 4096.0,
step_warmup_s: 30,
step_hold_s: 120,
bisections: 3,
p99_budget_ms: 1_000.0,
error_budget: 0.001,
}
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct LoadStep {
pub rate: f64,
pub offered_load_sustained: f64,
pub operations: Vec<OperationMeasurement>,
pub stable: bool,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub breaches: Vec<String>,
pub generator_bound: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub resources: Option<crate::perf::ResourcesRecord>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct StressReport {
pub corpus: String,
pub environment: Environment,
pub step_warmup_s: u64,
pub step_hold_s: u64,
pub p99_budget_ms: f64,
pub error_budget: f64,
pub steps: Vec<LoadStep>,
pub max_sustainable_throughput_per_s: f64,
pub ladder_capped: bool,
pub generator_bound: bool,
pub remark: String,
}
fn step_breaches(
operations: &[OperationMeasurement],
options: &StressOptions,
) -> Result<Vec<String>, String> {
let mut breaches = Vec::new();
let (mut requests, mut errors) = (0_u64, 0_u64);
for op in operations {
requests = requests.saturating_add(op.requests);
errors = errors.saturating_add(op.errors);
let histogram = op.decode_histogram()?;
#[expect(
clippy::as_conversions,
clippy::cast_precision_loss,
reason = "latencies << 2^52 µs"
)]
let p99_ms = histogram.value_at_quantile(0.99) as f64 / 1_000.0;
if p99_ms > options.p99_budget_ms {
breaches.push(format!(
"{} p99 {p99_ms:.1}ms > budget {:.0}ms",
op.operation, options.p99_budget_ms
));
}
}
#[expect(
clippy::as_conversions,
clippy::cast_precision_loss,
reason = "request counts << 2^52"
)]
let error_rate = if requests == 0 {
1.0
} else {
errors as f64 / requests as f64
};
if error_rate > options.error_budget {
breaches.push(format!(
"error rate {error_rate:.4} > tolerance {:.4}",
options.error_budget
));
}
Ok(breaches)
}
#[expect(
clippy::too_many_lines,
reason = "one linear procedure: climb → bisect → report"
)]
pub fn run_stress(
corpus: &SeededCorpus,
workload: &JourneyWorkload<'_>,
environment: &Environment,
containers: Option<&crate::ixit::Containers>,
options: &StressOptions,
progress: &(dyn Fn(String) + Sync),
) -> Result<StressReport, String> {
let mut steps: Vec<LoadStep> = Vec::new();
let mut last_good: f64 = 0.0;
let mut first_bad: Option<f64> = None;
let mut generator_bound = false;
let mut ladder_capped = false;
if containers.is_none() {
progress("resources: not sampled (ixit declares no `containers` block)".to_owned());
}
let run_step = |rate: f64,
steps: &mut Vec<LoadStep>,
generator_bound: &mut bool|
-> Result<bool, String> {
if let Some(c) = containers {
if let Err(e) = crate::perf_run::resources::settle_maintenance(&c.db) {
progress(format!("maintenance not settled: {e}"));
}
} else {
progress("maintenance not settled (no ixit `containers` block)".to_owned());
}
progress(format!(
"load step at {rate}/s ({}s hold)",
options.step_hold_s
));
let sampler = containers.map(|c| {
crate::perf_run::resources::ResourceSampler::start(
c,
options.step_warmup_s,
options.step_hold_s,
)
});
let window = run_window(
corpus,
workload,
rate,
options.step_warmup_s,
options.step_hold_s,
progress,
);
let resources = sampler.and_then(|sampler| {
let (series, notes) = sampler.stop();
for note in notes {
progress(note);
}
series
.iter()
.any(|s| !s.samples.is_empty())
.then_some(crate::perf::ResourcesRecord {
sample_interval_s: crate::perf_run::resources::SAMPLE_INTERVAL.as_secs(),
containers: series,
disk: None,
})
});
let window = window?;
let breaches = step_breaches(&window.operations, options)?;
let stable = breaches.is_empty() && !window.generator_bound;
if window.generator_bound {
*generator_bound = true;
progress(format!(
"generator bound at {rate}/s — the instrument, not the SUT, is the bottleneck"
));
}
let resource_note = resources.as_ref().map_or(String::new(), |r| {
let peaks: Vec<String> = r
.containers
.iter()
.map(|c| format!("{} peak {:.0}% cpu", c.role.label(), c.cpu_peak()))
.collect();
format!(", {}", peaks.join(", "))
});
progress(if stable {
format!(
"step {rate}/s: stable (sustained {:.1}/s{resource_note})",
window.offered_load_sustained
)
} else {
format!(
"step {rate}/s: BREACHED (sustained {:.1}/s{resource_note}) — {}",
window.offered_load_sustained,
breaches.join("; ")
)
});
steps.push(LoadStep {
rate,
offered_load_sustained: window.offered_load_sustained,
operations: window.operations,
stable,
breaches,
generator_bound: window.generator_bound,
resources,
});
Ok(stable)
};
let mut rate = options.start_rate.max(0.5);
loop {
if rate > options.max_rate {
ladder_capped = true;
break;
}
let stable = run_step(rate, &mut steps, &mut generator_bound)?;
if stable {
last_good = last_good.max(rate);
rate *= 2.0;
} else {
if !generator_bound {
first_bad = Some(rate);
}
break;
}
}
if let Some(mut bad) = first_bad {
let mut good = last_good;
for _ in 0..options.bisections {
let mid = f64::midpoint(good, bad);
if !(mid.is_finite() && mid > good && mid < bad) {
break;
}
progress(format!(
"bisecting between {good}/s (stable) and {bad}/s (breached)"
));
if run_step(mid, &mut steps, &mut generator_bound)? {
good = mid;
} else {
bad = mid;
}
}
last_good = good;
}
for step in &steps {
progress(format!(
"recap: {:>7}/s {} {}",
step.rate,
if step.stable { "stable " } else { "BREACHED" },
step.breaches.first().map_or("", String::as_str),
));
}
let remark = format!(
"Maximum sustainable throughput ≈ {last_good:.1} arrivals/s on the {} corpus \
({}s steps){}{}. Exploration only — never a conformance record; the first \
breached rung past this rate is where the system leaves the envelope.",
corpus.corpus,
options.step_hold_s,
if ladder_capped {
" — ladder capped without a breach"
} else {
""
},
if generator_bound {
" — generator bound (the instrument topped out first)"
} else {
""
},
);
progress(remark.clone());
Ok(StressReport {
corpus: corpus.corpus.clone(),
environment: environment.clone(),
step_warmup_s: options.step_warmup_s,
step_hold_s: options.step_hold_s,
p99_budget_ms: options.p99_budget_ms,
error_budget: options.error_budget,
steps,
max_sustainable_throughput_per_s: last_good,
ladder_capped,
generator_bound,
remark,
})
}
#[cfg(test)]
mod tests {
use hdrhistogram::Histogram;
use super::*;
fn record(p99_us: u64, errors: u64) -> OperationMeasurement {
let mut h = Histogram::<u64>::new(3).unwrap();
for _ in 0..95 {
h.record(1_000).unwrap();
}
for _ in 0..5 {
h.record(p99_us).unwrap();
}
OperationMeasurement::from_histogram("composition_read", &h, errors).unwrap()
}
#[test]
fn the_envelope_breaches_on_p99_and_errors() {
let options = StressOptions::default();
assert!(
step_breaches(&[record(20_000, 0)], &options)
.unwrap()
.is_empty()
);
let slow = step_breaches(&[record(3_000_000, 0)], &options).unwrap();
assert!(slow.iter().any(|b| b.contains("p99")), "{slow:?}");
let flaky = step_breaches(&[record(20_000, 5)], &options).unwrap();
assert!(flaky.iter().any(|b| b.contains("error rate")), "{flaky:?}");
}
}