ipfrs_transport/
load_tester.rs

1//! Load testing utilities for transport layer
2//!
3//! This module provides tools to test the transport layer under various load conditions.
4//!
5//! # Example
6//!
7//! ```
8//! use ipfrs_transport::load_tester::{LoadTester, LoadTestConfig, LoadPattern};
9//!
10//! let config = LoadTestConfig {
11//!     duration_secs: 10,
12//!     pattern: LoadPattern::Constant(100),
13//!     block_size_bytes: 1024,
14//!     concurrent_requests: 10,
15//! };
16//!
17//! let tester = LoadTester::new(config);
18//! let stats = tester.stats();
19//! assert_eq!(stats.total_requests, 0);
20//! ```
21
22use std::collections::VecDeque;
23use std::time::{Duration, Instant};
24
25/// Load pattern for testing
26#[derive(Debug, Clone)]
27pub enum LoadPattern {
28    /// Constant rate (requests per second)
29    Constant(usize),
30    /// Linear ramp from min to max requests per second
31    Ramp { min: usize, max: usize },
32    /// Step pattern with different rates
33    Step {
34        steps: Vec<(usize, Duration)>, // (requests_per_sec, duration)
35    },
36    /// Spike pattern (burst followed by normal)
37    Spike {
38        normal_rate: usize,
39        spike_rate: usize,
40        spike_duration: Duration,
41        spike_interval: Duration,
42    },
43    /// Random rate between min and max
44    Random { min: usize, max: usize },
45}
46
47/// Configuration for load testing
48#[derive(Debug, Clone)]
49pub struct LoadTestConfig {
50    /// Test duration in seconds
51    pub duration_secs: u64,
52    /// Load pattern to use
53    pub pattern: LoadPattern,
54    /// Size of blocks to request (bytes)
55    pub block_size_bytes: usize,
56    /// Number of concurrent requests
57    pub concurrent_requests: usize,
58}
59
60impl Default for LoadTestConfig {
61    fn default() -> Self {
62        Self {
63            duration_secs: 60,
64            pattern: LoadPattern::Constant(100),
65            block_size_bytes: 1024,
66            concurrent_requests: 10,
67        }
68    }
69}
70
71/// Statistics from a load test
72#[derive(Debug, Clone)]
73pub struct LoadTestStats {
74    /// Total number of requests sent
75    pub total_requests: usize,
76    /// Total number of successful responses
77    pub successful_responses: usize,
78    /// Total number of failures
79    pub failures: usize,
80    /// Total bytes transferred
81    pub bytes_transferred: u64,
82    /// Test duration
83    pub duration: Duration,
84    /// Average latency (milliseconds)
85    pub avg_latency_ms: f64,
86    /// p50 latency (milliseconds)
87    pub p50_latency_ms: f64,
88    /// p95 latency (milliseconds)
89    pub p95_latency_ms: f64,
90    /// p99 latency (milliseconds)
91    pub p99_latency_ms: f64,
92    /// Requests per second achieved
93    pub requests_per_second: f64,
94    /// Throughput in bytes per second
95    pub throughput_bps: f64,
96}
97
98impl Default for LoadTestStats {
99    fn default() -> Self {
100        Self {
101            total_requests: 0,
102            successful_responses: 0,
103            failures: 0,
104            bytes_transferred: 0,
105            duration: Duration::from_secs(0),
106            avg_latency_ms: 0.0,
107            p50_latency_ms: 0.0,
108            p95_latency_ms: 0.0,
109            p99_latency_ms: 0.0,
110            requests_per_second: 0.0,
111            throughput_bps: 0.0,
112        }
113    }
114}
115
116impl std::fmt::Display for LoadTestStats {
117    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
118        writeln!(f, "Load Test Results:")?;
119        writeln!(f, "  Duration: {:?}", self.duration)?;
120        writeln!(f, "  Total Requests: {}", self.total_requests)?;
121        writeln!(f, "  Successful: {}", self.successful_responses)?;
122        writeln!(f, "  Failures: {}", self.failures)?;
123        writeln!(f, "  Bytes Transferred: {}", self.bytes_transferred)?;
124        writeln!(f, "  Requests/sec: {:.2}", self.requests_per_second)?;
125        writeln!(
126            f,
127            "  Throughput: {:.2} MB/s",
128            self.throughput_bps / 1_000_000.0
129        )?;
130        writeln!(f, "  Avg Latency: {:.2}ms", self.avg_latency_ms)?;
131        writeln!(f, "  p50 Latency: {:.2}ms", self.p50_latency_ms)?;
132        writeln!(f, "  p95 Latency: {:.2}ms", self.p95_latency_ms)?;
133        writeln!(f, "  p99 Latency: {:.2}ms", self.p99_latency_ms)?;
134        Ok(())
135    }
136}
137
138/// Load tester for transport layer
139pub struct LoadTester {
140    config: LoadTestConfig,
141    stats: LoadTestStats,
142    latencies: VecDeque<u64>,
143    start_time: Option<Instant>,
144}
145
146impl LoadTester {
147    /// Create a new load tester
148    pub fn new(config: LoadTestConfig) -> Self {
149        Self {
150            config,
151            stats: LoadTestStats::default(),
152            latencies: VecDeque::new(),
153            start_time: None,
154        }
155    }
156
157    /// Start the load test
158    pub fn start(&mut self) {
159        self.start_time = Some(Instant::now());
160        self.stats = LoadTestStats::default();
161        self.latencies.clear();
162    }
163
164    /// Record a successful request
165    pub fn record_success(&mut self, latency_ms: u64, bytes: usize) {
166        self.stats.total_requests += 1;
167        self.stats.successful_responses += 1;
168        self.stats.bytes_transferred += bytes as u64;
169        self.latencies.push_back(latency_ms);
170
171        // Keep only recent latencies (for memory efficiency)
172        if self.latencies.len() > 10000 {
173            self.latencies.pop_front();
174        }
175    }
176
177    /// Record a failed request
178    pub fn record_failure(&mut self) {
179        self.stats.total_requests += 1;
180        self.stats.failures += 1;
181    }
182
183    /// Get current statistics
184    pub fn stats(&self) -> &LoadTestStats {
185        &self.stats
186    }
187
188    /// Calculate and finalize statistics
189    pub fn finalize(&mut self) -> LoadTestStats {
190        if let Some(start) = self.start_time {
191            self.stats.duration = start.elapsed();
192        }
193
194        // Calculate latency percentiles
195        if !self.latencies.is_empty() {
196            let mut sorted: Vec<u64> = self.latencies.iter().copied().collect();
197            sorted.sort_unstable();
198
199            let sum: u64 = sorted.iter().sum();
200            self.stats.avg_latency_ms = sum as f64 / sorted.len() as f64;
201
202            let p50_idx = (sorted.len() as f64 * 0.50) as usize;
203            let p95_idx = (sorted.len() as f64 * 0.95) as usize;
204            let p99_idx = (sorted.len() as f64 * 0.99) as usize;
205
206            self.stats.p50_latency_ms = sorted.get(p50_idx).copied().unwrap_or(0) as f64;
207            self.stats.p95_latency_ms = sorted.get(p95_idx).copied().unwrap_or(0) as f64;
208            self.stats.p99_latency_ms = sorted.get(p99_idx).copied().unwrap_or(0) as f64;
209        }
210
211        // Calculate throughput
212        let duration_secs = self.stats.duration.as_secs_f64();
213        if duration_secs > 0.0 {
214            self.stats.requests_per_second = self.stats.total_requests as f64 / duration_secs;
215            self.stats.throughput_bps = self.stats.bytes_transferred as f64 / duration_secs;
216        }
217
218        self.stats.clone()
219    }
220
221    /// Get the target request rate at a given time offset
222    pub fn get_target_rate(&self, elapsed: Duration) -> usize {
223        match &self.config.pattern {
224            LoadPattern::Constant(rate) => *rate,
225            LoadPattern::Ramp { min, max } => {
226                let progress = elapsed.as_secs_f64() / self.config.duration_secs as f64;
227                let range = (*max - *min) as f64;
228                (*min as f64 + range * progress) as usize
229            }
230            LoadPattern::Step { steps } => {
231                let mut accumulated = Duration::from_secs(0);
232                for (rate, duration) in steps {
233                    accumulated += *duration;
234                    if elapsed < accumulated {
235                        return *rate;
236                    }
237                }
238                steps.last().map(|(rate, _)| *rate).unwrap_or(0)
239            }
240            LoadPattern::Spike {
241                normal_rate,
242                spike_rate,
243                spike_duration,
244                spike_interval,
245            } => {
246                let cycle_time = elapsed.as_secs_f64() % spike_interval.as_secs_f64();
247                if cycle_time < spike_duration.as_secs_f64() {
248                    *spike_rate
249                } else {
250                    *normal_rate
251                }
252            }
253            LoadPattern::Random { min, max } => {
254                // Simple pseudo-random (not cryptographically secure)
255                let seed = elapsed.as_millis() as usize;
256                min + (seed % (max - min + 1))
257            }
258        }
259    }
260
261    /// Get configuration
262    pub fn config(&self) -> &LoadTestConfig {
263        &self.config
264    }
265
266    /// Reset the tester
267    pub fn reset(&mut self) {
268        self.stats = LoadTestStats::default();
269        self.latencies.clear();
270        self.start_time = None;
271    }
272}
273
274/// Builder for load test configuration
275pub struct LoadTestConfigBuilder {
276    config: LoadTestConfig,
277}
278
279impl LoadTestConfigBuilder {
280    /// Create a new builder
281    pub fn new() -> Self {
282        Self {
283            config: LoadTestConfig::default(),
284        }
285    }
286
287    /// Set test duration
288    pub fn duration_secs(mut self, secs: u64) -> Self {
289        self.config.duration_secs = secs;
290        self
291    }
292
293    /// Set load pattern
294    pub fn pattern(mut self, pattern: LoadPattern) -> Self {
295        self.config.pattern = pattern;
296        self
297    }
298
299    /// Set block size
300    pub fn block_size_bytes(mut self, bytes: usize) -> Self {
301        self.config.block_size_bytes = bytes;
302        self
303    }
304
305    /// Set concurrent requests
306    pub fn concurrent_requests(mut self, count: usize) -> Self {
307        self.config.concurrent_requests = count;
308        self
309    }
310
311    /// Build the configuration
312    pub fn build(self) -> LoadTestConfig {
313        self.config
314    }
315}
316
317impl Default for LoadTestConfigBuilder {
318    fn default() -> Self {
319        Self::new()
320    }
321}
322
323#[cfg(test)]
324mod tests {
325    use super::*;
326
327    #[test]
328    fn test_load_tester_creation() {
329        let config = LoadTestConfig::default();
330        let tester = LoadTester::new(config);
331        assert_eq!(tester.stats().total_requests, 0);
332    }
333
334    #[test]
335    fn test_record_success() {
336        let config = LoadTestConfig::default();
337        let mut tester = LoadTester::new(config);
338
339        tester.start();
340        tester.record_success(50, 1024);
341
342        assert_eq!(tester.stats().total_requests, 1);
343        assert_eq!(tester.stats().successful_responses, 1);
344        assert_eq!(tester.stats().bytes_transferred, 1024);
345    }
346
347    #[test]
348    fn test_record_failure() {
349        let config = LoadTestConfig::default();
350        let mut tester = LoadTester::new(config);
351
352        tester.start();
353        tester.record_failure();
354
355        assert_eq!(tester.stats().total_requests, 1);
356        assert_eq!(tester.stats().failures, 1);
357    }
358
359    #[test]
360    fn test_finalize_stats() {
361        let config = LoadTestConfig::default();
362        let mut tester = LoadTester::new(config);
363
364        tester.start();
365        tester.record_success(50, 1024);
366        tester.record_success(60, 1024);
367        tester.record_success(70, 1024);
368
369        // Ensure measurable elapsed time for throughput calculation
370        std::thread::sleep(Duration::from_millis(1));
371
372        let stats = tester.finalize();
373        assert_eq!(stats.total_requests, 3);
374        assert!(stats.avg_latency_ms > 0.0);
375        assert!(stats.throughput_bps > 0.0);
376    }
377
378    #[test]
379    fn test_constant_load_pattern() {
380        let config = LoadTestConfig {
381            pattern: LoadPattern::Constant(100),
382            ..Default::default()
383        };
384        let tester = LoadTester::new(config);
385
386        assert_eq!(tester.get_target_rate(Duration::from_secs(0)), 100);
387        assert_eq!(tester.get_target_rate(Duration::from_secs(30)), 100);
388    }
389
390    #[test]
391    fn test_ramp_load_pattern() {
392        let config = LoadTestConfig {
393            duration_secs: 10,
394            pattern: LoadPattern::Ramp { min: 10, max: 100 },
395            ..Default::default()
396        };
397        let tester = LoadTester::new(config);
398
399        let rate_start = tester.get_target_rate(Duration::from_secs(0));
400        let rate_end = tester.get_target_rate(Duration::from_secs(10));
401
402        assert_eq!(rate_start, 10);
403        assert_eq!(rate_end, 100);
404    }
405
406    #[test]
407    fn test_step_load_pattern() {
408        let config = LoadTestConfig {
409            pattern: LoadPattern::Step {
410                steps: vec![
411                    (10, Duration::from_secs(5)),
412                    (50, Duration::from_secs(5)),
413                    (100, Duration::from_secs(5)),
414                ],
415            },
416            ..Default::default()
417        };
418        let tester = LoadTester::new(config);
419
420        assert_eq!(tester.get_target_rate(Duration::from_secs(2)), 10);
421        assert_eq!(tester.get_target_rate(Duration::from_secs(7)), 50);
422        assert_eq!(tester.get_target_rate(Duration::from_secs(12)), 100);
423    }
424
425    #[test]
426    fn test_spike_load_pattern() {
427        let config = LoadTestConfig {
428            pattern: LoadPattern::Spike {
429                normal_rate: 10,
430                spike_rate: 100,
431                spike_duration: Duration::from_secs(2),
432                spike_interval: Duration::from_secs(10),
433            },
434            ..Default::default()
435        };
436        let tester = LoadTester::new(config);
437
438        assert_eq!(tester.get_target_rate(Duration::from_secs(1)), 100); // In spike
439        assert_eq!(tester.get_target_rate(Duration::from_secs(5)), 10); // Normal
440    }
441
442    #[test]
443    fn test_config_builder() {
444        let config = LoadTestConfigBuilder::new()
445            .duration_secs(30)
446            .pattern(LoadPattern::Constant(50))
447            .block_size_bytes(2048)
448            .concurrent_requests(20)
449            .build();
450
451        assert_eq!(config.duration_secs, 30);
452        assert_eq!(config.block_size_bytes, 2048);
453        assert_eq!(config.concurrent_requests, 20);
454    }
455
456    #[test]
457    fn test_reset() {
458        let config = LoadTestConfig::default();
459        let mut tester = LoadTester::new(config);
460
461        tester.start();
462        tester.record_success(50, 1024);
463
464        assert_eq!(tester.stats().total_requests, 1);
465
466        tester.reset();
467        assert_eq!(tester.stats().total_requests, 0);
468    }
469
470    #[test]
471    fn test_percentile_calculation() {
472        let config = LoadTestConfig::default();
473        let mut tester = LoadTester::new(config);
474
475        tester.start();
476        for i in 1..=100 {
477            tester.record_success(i, 1024);
478        }
479
480        let stats = tester.finalize();
481        assert!(stats.p50_latency_ms >= 45.0 && stats.p50_latency_ms <= 55.0);
482        assert!(stats.p95_latency_ms >= 90.0 && stats.p95_latency_ms <= 100.0);
483        assert!(stats.p99_latency_ms >= 95.0 && stats.p99_latency_ms <= 100.0);
484    }
485
486    #[test]
487    fn test_stats_display() {
488        let stats = LoadTestStats {
489            total_requests: 100,
490            successful_responses: 95,
491            failures: 5,
492            bytes_transferred: 102400,
493            duration: Duration::from_secs(10),
494            avg_latency_ms: 50.0,
495            p50_latency_ms: 45.0,
496            p95_latency_ms: 90.0,
497            p99_latency_ms: 95.0,
498            requests_per_second: 10.0,
499            throughput_bps: 10240.0,
500        };
501
502        let display = format!("{}", stats);
503        assert!(display.contains("Total Requests: 100"));
504        assert!(display.contains("Successful: 95"));
505    }
506}