Skip to main content

ecat_bench/
detailed.rs

1// Copyright (c) 2026 erik <erik@erik.xyz> — https://erik.xyz
2//! 细粒度延迟统计:在原有 p50/p95/p99 之外记录 p99.9、最大值、错误数与
3//! 超过阈值的请求数。用于验证“最大值 < 10ms”这类尾部 SLO。
4use std::future::Future;
5use std::sync::Arc;
6use std::time::{Duration, Instant};
7
8/// 一次压测的完整统计;错误请求也计入延迟样本,并单独计数。
9#[derive(Debug, Clone)]
10pub struct DetailedBenchReport {
11    pub name: String,
12    pub total_requests: u64,
13    pub total_duration: Duration,
14    pub avg_latency_us: f64,
15    pub p50_latency_us: f64,
16    pub p95_latency_us: f64,
17    pub p99_latency_us: f64,
18    pub p999_latency_us: f64,
19    pub max_latency_us: f64,
20    pub throughput_rps: f64,
21    pub errors: u64,
22    pub over_threshold: u64,
23    pub threshold_us: f64,
24}
25
26impl DetailedBenchReport {
27    pub fn print(&self) {
28        println!("=== {} ===", self.name);
29        println!("  requests:   {}", self.total_requests);
30        println!("  errors:     {}", self.errors);
31        println!("  duration:   {:.2?}", self.total_duration);
32        println!("  throughput: {:.0} req/s", self.throughput_rps);
33        println!("  avg:        {:.0} µs", self.avg_latency_us);
34        println!("  p50:        {:.0} µs", self.p50_latency_us);
35        println!("  p95:        {:.0} µs", self.p95_latency_us);
36        println!("  p99:        {:.0} µs", self.p99_latency_us);
37        println!("  p99.9:      {:.0} µs", self.p999_latency_us);
38        println!("  max:        {:.0} µs", self.max_latency_us);
39        println!("  >= {:.0} µs:  {}", self.threshold_us, self.over_threshold);
40    }
41
42    /// 转成旧版摘要,便于复用已有打印/对比代码。
43    pub fn summary(&self) -> super::BenchResult {
44        super::BenchResult {
45            name: self.name.clone(),
46            total_requests: self.total_requests,
47            total_duration: self.total_duration,
48            avg_latency_us: self.avg_latency_us,
49            p50_latency_us: self.p50_latency_us,
50            p95_latency_us: self.p95_latency_us,
51            p99_latency_us: self.p99_latency_us,
52            throughput_rps: self.throughput_rps,
53        }
54    }
55}
56
57/// 与 [`super::run_bench_with_warmup`] 相同的并发/预热语义,但闭包返回
58/// `Result`,错误请求计入 `errors`;同时报告 p99.9、最大值和阈值越界数。
59pub async fn run_bench_detailed<F, Fut>(
60    name: &str,
61    concurrency: usize,
62    total: u64,
63    warmup: u64,
64    threshold: Duration,
65    f: F,
66) -> DetailedBenchReport
67where
68    F: Fn() -> Fut + Send + Sync + 'static,
69    Fut: Future<Output = Result<(), String>> + Send,
70{
71    if concurrency == 0 || total == 0 {
72        return DetailedBenchReport {
73            name: name.to_string(),
74            total_requests: 0,
75            total_duration: Duration::ZERO,
76            avg_latency_us: 0.0,
77            p50_latency_us: 0.0,
78            p95_latency_us: 0.0,
79            p99_latency_us: 0.0,
80            p999_latency_us: 0.0,
81            max_latency_us: 0.0,
82            throughput_rps: 0.0,
83            errors: 0,
84            over_threshold: 0,
85            threshold_us: threshold.as_micros() as f64,
86        };
87    }
88
89    let chunk_size = total / concurrency as u64;
90    let remainder = total % concurrency as u64;
91    let warmup_chunk = warmup / concurrency as u64;
92    let warmup_remainder = warmup % concurrency as u64;
93    let f = Arc::new(f);
94    let barrier = Arc::new(tokio::sync::Barrier::new(concurrency + 1));
95    let mut handles = Vec::with_capacity(concurrency);
96
97    for index in 0..concurrency {
98        let f = Arc::clone(&f);
99        let barrier = Arc::clone(&barrier);
100        let n = chunk_size + u64::from((index as u64) < remainder);
101        let planned = n;
102        let wn = warmup_chunk + u64::from((index as u64) < warmup_remainder);
103        let handle = tokio::spawn(async move {
104            for _ in 0..wn {
105                let _ = f().await;
106            }
107            barrier.wait().await;
108            let mut latencies = Vec::with_capacity(n as usize);
109            let mut errors = 0_u64;
110            for _ in 0..n {
111                let started = Instant::now();
112                if f().await.is_err() {
113                    errors += 1;
114                }
115                latencies.push(started.elapsed().as_micros() as f64);
116            }
117            (latencies, errors)
118        });
119        handles.push((handle, planned));
120    }
121
122    // worker 若在预热阶段 panic,屏障永远不会释放——超时失败而不是挂死。
123    let _ = tokio::time::timeout(Duration::from_secs(30), barrier.wait())
124        .await
125        .unwrap_or_else(|_| panic!("bench workers failed to reach steady state (warmup panic?)"));
126    let started = Instant::now();
127
128    let mut latencies = Vec::with_capacity(total as usize);
129    let mut errors = 0_u64;
130    for (handle, planned) in handles {
131        match handle.await {
132            Ok((worker_latencies, worker_errors)) => {
133                latencies.extend(worker_latencies);
134                errors += worker_errors;
135            }
136            // panic 的 worker 没有样本;按整块计划量记错误,避免报告“零错误”。
137            Err(_) => errors += planned,
138        }
139    }
140    let total_duration = started.elapsed();
141    latencies.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
142
143    let count = latencies.len();
144    let threshold_us = threshold.as_micros() as f64;
145    let avg = if count == 0 {
146        0.0
147    } else {
148        latencies.iter().sum::<f64>() / count as f64
149    };
150    let pct = |p: f64| percentile(&latencies, p);
151    DetailedBenchReport {
152        name: name.to_string(),
153        total_requests: count as u64,
154        total_duration,
155        avg_latency_us: avg,
156        p50_latency_us: pct(0.50),
157        p95_latency_us: pct(0.95),
158        p99_latency_us: pct(0.99),
159        p999_latency_us: pct(0.999),
160        max_latency_us: latencies.last().copied().unwrap_or(0.0),
161        throughput_rps: if total_duration.is_zero() {
162            0.0
163        } else {
164            count as f64 / total_duration.as_secs_f64()
165        },
166        errors,
167        over_threshold: latencies
168            .iter()
169            .filter(|value| **value >= threshold_us)
170            .count() as u64,
171        threshold_us,
172    }
173}
174
175/// 最近秩百分位:p=0.999、n=10000 时取第 9990 个样本(1-based)。
176fn percentile(sorted: &[f64], p: f64) -> f64 {
177    if sorted.is_empty() {
178        return 0.0;
179    }
180    let rank = (p * sorted.len() as f64).ceil() as usize;
181    sorted[rank.saturating_sub(1).min(sorted.len() - 1)]
182}
183
184#[cfg(test)]
185mod tests {
186    use super::*;
187    use std::sync::atomic::{AtomicU64, Ordering};
188
189    #[tokio::test]
190    async fn counts_errors_and_latencies() {
191        let calls = Arc::new(AtomicU64::new(0));
192        let f = {
193            let calls = Arc::clone(&calls);
194            move || {
195                let calls = Arc::clone(&calls);
196                async move {
197                    let n = calls.fetch_add(1, Ordering::SeqCst);
198                    if n.is_multiple_of(3) {
199                        Err("boom".to_string())
200                    } else {
201                        Ok(())
202                    }
203                }
204            }
205        };
206        let report = run_bench_detailed("errors", 2, 30, 0, Duration::from_millis(10), f).await;
207        assert_eq!(report.total_requests, 30);
208        assert_eq!(report.errors, 10);
209        assert_eq!(report.over_threshold, 0);
210    }
211
212    #[tokio::test]
213    async fn max_and_p999_cover_tail() {
214        let calls = Arc::new(AtomicU64::new(0));
215        let f = {
216            let calls = Arc::clone(&calls);
217            move || {
218                let calls = Arc::clone(&calls);
219                async move {
220                    if calls.fetch_add(1, Ordering::SeqCst) == 0 {
221                        tokio::time::sleep(Duration::from_millis(5)).await;
222                    }
223                    Ok::<(), String>(())
224                }
225            }
226        };
227        let report = run_bench_detailed("tail", 1, 20, 0, Duration::from_millis(1), f).await;
228        assert!(
229            report.max_latency_us >= 4_000.0,
230            "max must capture the 5ms sample: {}",
231            report.max_latency_us
232        );
233        assert!(
234            report.p999_latency_us <= report.max_latency_us,
235            "p99.9 must not exceed max"
236        );
237        assert!(report.over_threshold >= 1);
238    }
239
240    #[test]
241    fn percentile_uses_nearest_rank() {
242        let values: Vec<f64> = (1..=1000).map(f64::from).collect();
243        assert_eq!(percentile(&values, 0.50), 500.0);
244        assert_eq!(percentile(&values, 0.99), 990.0);
245        assert_eq!(percentile(&values, 0.999), 999.0);
246        assert_eq!(percentile(&[], 0.999), 0.0);
247    }
248
249    #[test]
250    fn p999_equals_max_below_1000_samples() {
251        for n in 1..1000 {
252            let values: Vec<f64> = (1..=n).map(f64::from).collect();
253            assert_eq!(
254                percentile(&values, 0.999),
255                n as f64,
256                "p99.9 must be the last sample for n={n}"
257            );
258        }
259    }
260
261    #[tokio::test]
262    async fn zero_total_returns_empty_report() {
263        let report = run_bench_detailed("zero", 4, 0, 10, Duration::from_millis(10), || async {
264            Ok(())
265        })
266        .await;
267        assert_eq!(report.total_requests, 0);
268        assert_eq!(report.errors, 0);
269        assert_eq!(report.max_latency_us, 0.0);
270    }
271}