/home/noah/src/trueno/src/simulation/stress.rs
Line | Count | Source |
1 | | //! Stress Testing (Heijunka: Leveled Workload Testing) |
2 | | //! |
3 | | //! Provides configurable stress testing infrastructure for validating |
4 | | //! compute operations under various load conditions. |
5 | | |
6 | | use crate::Backend; |
7 | | |
8 | | /// Stress test configuration for trueno operations |
9 | | #[derive(Debug, Clone)] |
10 | | pub struct StressTestConfig { |
11 | | /// Number of cycles per backend |
12 | | pub cycles_per_backend: u32, |
13 | | /// Input sizes to test (leveled) |
14 | | pub input_sizes: Vec<usize>, |
15 | | /// Backends to stress test |
16 | | pub backends: Vec<Backend>, |
17 | | /// Performance thresholds |
18 | | pub thresholds: StressThresholds, |
19 | | /// Master seed for RNG |
20 | | pub master_seed: u64, |
21 | | } |
22 | | |
23 | | impl Default for StressTestConfig { |
24 | 0 | fn default() -> Self { |
25 | 0 | Self { |
26 | 0 | cycles_per_backend: 100, |
27 | 0 | input_sizes: vec![100, 1_000, 10_000, 100_000, 1_000_000], |
28 | 0 | backends: vec![Backend::Scalar, Backend::AVX2], |
29 | 0 | thresholds: StressThresholds::default(), |
30 | 0 | master_seed: 42, |
31 | 0 | } |
32 | 0 | } |
33 | | } |
34 | | |
35 | | impl StressTestConfig { |
36 | | /// Create new stress test config |
37 | | #[must_use] |
38 | 0 | pub fn new(master_seed: u64) -> Self { |
39 | 0 | Self { |
40 | 0 | master_seed, |
41 | 0 | ..Default::default() |
42 | 0 | } |
43 | 0 | } |
44 | | |
45 | | /// Set cycles per backend |
46 | | #[must_use] |
47 | 0 | pub const fn with_cycles(mut self, cycles: u32) -> Self { |
48 | 0 | self.cycles_per_backend = cycles; |
49 | 0 | self |
50 | 0 | } |
51 | | |
52 | | /// Set input sizes |
53 | | #[must_use] |
54 | 0 | pub fn with_input_sizes(mut self, sizes: Vec<usize>) -> Self { |
55 | 0 | self.input_sizes = sizes; |
56 | 0 | self |
57 | 0 | } |
58 | | |
59 | | /// Set backends to test |
60 | | #[must_use] |
61 | 0 | pub fn with_backends(mut self, backends: Vec<Backend>) -> Self { |
62 | 0 | self.backends = backends; |
63 | 0 | self |
64 | 0 | } |
65 | | |
66 | | /// Set performance thresholds |
67 | | #[must_use] |
68 | 0 | pub fn with_thresholds(mut self, thresholds: StressThresholds) -> Self { |
69 | 0 | self.thresholds = thresholds; |
70 | 0 | self |
71 | 0 | } |
72 | | |
73 | | /// Calculate total test count |
74 | | #[must_use] |
75 | 0 | pub fn total_tests(&self) -> usize { |
76 | 0 | self.backends.len() * self.input_sizes.len() * self.cycles_per_backend as usize |
77 | 0 | } |
78 | | } |
79 | | |
80 | | /// Performance thresholds for stress testing |
81 | | #[derive(Debug, Clone)] |
82 | | pub struct StressThresholds { |
83 | | /// Max time per operation (ms) |
84 | | pub max_op_time_ms: u64, |
85 | | /// Max memory per operation (bytes) |
86 | | pub max_memory_bytes: usize, |
87 | | /// Max variance in operation times (coefficient of variation) |
88 | | pub max_timing_variance: f64, |
89 | | /// Max allowed failure rate (0.0 to 1.0) |
90 | | pub max_failure_rate: f64, |
91 | | } |
92 | | |
93 | | impl Default for StressThresholds { |
94 | 0 | fn default() -> Self { |
95 | 0 | Self { |
96 | 0 | max_op_time_ms: 1000, // 1s max per op |
97 | 0 | max_memory_bytes: 256 * 1024 * 1024, // 256MB max |
98 | 0 | max_timing_variance: 0.5, // 50% max variance |
99 | 0 | max_failure_rate: 0.0, // Zero failures allowed |
100 | 0 | } |
101 | 0 | } |
102 | | } |
103 | | |
104 | | impl StressThresholds { |
105 | | /// Strict thresholds for CI |
106 | | #[must_use] |
107 | 0 | pub const fn strict() -> Self { |
108 | 0 | Self { |
109 | 0 | max_op_time_ms: 100, |
110 | 0 | max_memory_bytes: 64 * 1024 * 1024, |
111 | 0 | max_timing_variance: 0.2, |
112 | 0 | max_failure_rate: 0.0, |
113 | 0 | } |
114 | 0 | } |
115 | | |
116 | | /// Relaxed thresholds for development |
117 | | #[must_use] |
118 | 0 | pub const fn relaxed() -> Self { |
119 | 0 | Self { |
120 | 0 | max_op_time_ms: 5000, |
121 | 0 | max_memory_bytes: 512 * 1024 * 1024, |
122 | 0 | max_timing_variance: 1.0, |
123 | 0 | max_failure_rate: 0.01, |
124 | 0 | } |
125 | 0 | } |
126 | | } |
127 | | |
128 | | /// Stress test result for a single operation |
129 | | #[derive(Debug, Clone)] |
130 | | pub struct StressResult { |
131 | | /// Backend used |
132 | | pub backend: Backend, |
133 | | /// Input size |
134 | | pub input_size: usize, |
135 | | /// Cycles completed |
136 | | pub cycles_completed: u32, |
137 | | /// Total tests passed |
138 | | pub tests_passed: u32, |
139 | | /// Total tests failed |
140 | | pub tests_failed: u32, |
141 | | /// Mean operation time (ms) |
142 | | pub mean_op_time_ms: f64, |
143 | | /// Max operation time (ms) |
144 | | pub max_op_time_ms: u64, |
145 | | /// Timing variance (coefficient of variation) |
146 | | pub timing_variance: f64, |
147 | | /// Detected anomalies |
148 | | pub anomalies: Vec<StressAnomaly>, |
149 | | } |
150 | | |
151 | | impl StressResult { |
152 | | /// Check if all tests passed |
153 | | #[must_use] |
154 | 0 | pub fn passed(&self) -> bool { |
155 | 0 | self.tests_failed == 0 && self.anomalies.is_empty() |
156 | 0 | } |
157 | | |
158 | | /// Calculate pass rate |
159 | | #[must_use] |
160 | 0 | pub fn pass_rate(&self) -> f64 { |
161 | 0 | let total = self.tests_passed + self.tests_failed; |
162 | 0 | if total == 0 { |
163 | 0 | 1.0 |
164 | | } else { |
165 | 0 | self.tests_passed as f64 / total as f64 |
166 | | } |
167 | 0 | } |
168 | | } |
169 | | |
170 | | /// Anomaly detected during stress testing |
171 | | #[derive(Debug, Clone)] |
172 | | pub struct StressAnomaly { |
173 | | /// Cycle where anomaly was detected |
174 | | pub cycle: u32, |
175 | | /// Type of anomaly |
176 | | pub kind: StressAnomalyKind, |
177 | | /// Description |
178 | | pub description: String, |
179 | | } |
180 | | |
181 | | /// Types of stress test anomalies |
182 | | #[derive(Debug, Clone, Copy, PartialEq, Eq)] |
183 | | pub enum StressAnomalyKind { |
184 | | /// Operation too slow |
185 | | SlowOperation, |
186 | | /// High memory usage |
187 | | HighMemory, |
188 | | /// Test failure |
189 | | TestFailure, |
190 | | /// Timing spike |
191 | | TimingSpike, |
192 | | /// Non-deterministic output |
193 | | NonDeterministic, |
194 | | } |
195 | | |
196 | | #[cfg(test)] |
197 | | mod tests { |
198 | | use super::*; |
199 | | |
200 | | #[test] |
201 | | fn test_stress_test_config_default() { |
202 | | let config = StressTestConfig::default(); |
203 | | |
204 | | assert_eq!(config.cycles_per_backend, 100); |
205 | | assert_eq!(config.input_sizes.len(), 5); |
206 | | assert_eq!(config.backends.len(), 2); |
207 | | assert_eq!(config.master_seed, 42); |
208 | | } |
209 | | |
210 | | #[test] |
211 | | fn test_stress_test_config_builder() { |
212 | | let config = StressTestConfig::new(123) |
213 | | .with_cycles(50) |
214 | | .with_input_sizes(vec![100, 1000]) |
215 | | .with_backends(vec![Backend::Scalar]) |
216 | | .with_thresholds(StressThresholds::strict()); |
217 | | |
218 | | assert_eq!(config.master_seed, 123); |
219 | | assert_eq!(config.cycles_per_backend, 50); |
220 | | assert_eq!(config.input_sizes.len(), 2); |
221 | | assert_eq!(config.backends.len(), 1); |
222 | | } |
223 | | |
224 | | #[test] |
225 | | fn test_stress_test_config_total_tests() { |
226 | | let config = StressTestConfig::default() |
227 | | .with_cycles(10) |
228 | | .with_input_sizes(vec![100, 1000, 10000]) |
229 | | .with_backends(vec![Backend::Scalar, Backend::AVX2]); |
230 | | |
231 | | // 2 backends * 3 sizes * 10 cycles = 60 tests |
232 | | assert_eq!(config.total_tests(), 60); |
233 | | } |
234 | | |
235 | | #[test] |
236 | | fn test_stress_thresholds_default() { |
237 | | let thresholds = StressThresholds::default(); |
238 | | |
239 | | assert_eq!(thresholds.max_op_time_ms, 1000); |
240 | | assert_eq!(thresholds.max_memory_bytes, 256 * 1024 * 1024); |
241 | | assert!((thresholds.max_timing_variance - 0.5).abs() < 0.001); |
242 | | assert_eq!(thresholds.max_failure_rate, 0.0); |
243 | | } |
244 | | |
245 | | #[test] |
246 | | fn test_stress_thresholds_strict() { |
247 | | let thresholds = StressThresholds::strict(); |
248 | | |
249 | | assert_eq!(thresholds.max_op_time_ms, 100); |
250 | | assert_eq!(thresholds.max_memory_bytes, 64 * 1024 * 1024); |
251 | | assert!((thresholds.max_timing_variance - 0.2).abs() < 0.001); |
252 | | } |
253 | | |
254 | | #[test] |
255 | | fn test_stress_thresholds_relaxed() { |
256 | | let thresholds = StressThresholds::relaxed(); |
257 | | |
258 | | assert_eq!(thresholds.max_op_time_ms, 5000); |
259 | | assert_eq!(thresholds.max_memory_bytes, 512 * 1024 * 1024); |
260 | | assert!((thresholds.max_timing_variance - 1.0).abs() < 0.001); |
261 | | } |
262 | | |
263 | | #[test] |
264 | | fn test_stress_result_passed() { |
265 | | let result = StressResult { |
266 | | backend: Backend::Scalar, |
267 | | input_size: 1000, |
268 | | cycles_completed: 10, |
269 | | tests_passed: 100, |
270 | | tests_failed: 0, |
271 | | mean_op_time_ms: 50.0, |
272 | | max_op_time_ms: 100, |
273 | | timing_variance: 0.1, |
274 | | anomalies: vec![], |
275 | | }; |
276 | | |
277 | | assert!(result.passed()); |
278 | | assert_eq!(result.pass_rate(), 1.0); |
279 | | } |
280 | | |
281 | | #[test] |
282 | | fn test_stress_result_failed() { |
283 | | let result = StressResult { |
284 | | backend: Backend::AVX2, |
285 | | input_size: 10000, |
286 | | cycles_completed: 10, |
287 | | tests_passed: 95, |
288 | | tests_failed: 5, |
289 | | mean_op_time_ms: 100.0, |
290 | | max_op_time_ms: 500, |
291 | | timing_variance: 0.3, |
292 | | anomalies: vec![], |
293 | | }; |
294 | | |
295 | | assert!(!result.passed()); // Failed because tests_failed > 0 |
296 | | assert!((result.pass_rate() - 0.95).abs() < 0.001); |
297 | | } |
298 | | |
299 | | #[test] |
300 | | fn test_stress_result_with_anomaly() { |
301 | | let result = StressResult { |
302 | | backend: Backend::Scalar, |
303 | | input_size: 1000, |
304 | | cycles_completed: 10, |
305 | | tests_passed: 100, |
306 | | tests_failed: 0, |
307 | | mean_op_time_ms: 50.0, |
308 | | max_op_time_ms: 100, |
309 | | timing_variance: 0.1, |
310 | | anomalies: vec![StressAnomaly { |
311 | | cycle: 5, |
312 | | kind: StressAnomalyKind::SlowOperation, |
313 | | description: "Operation took 200ms".to_string(), |
314 | | }], |
315 | | }; |
316 | | |
317 | | assert!(!result.passed()); // Failed because anomalies not empty |
318 | | } |
319 | | |
320 | | #[test] |
321 | | fn test_stress_anomaly_kinds() { |
322 | | assert_eq!( |
323 | | StressAnomalyKind::SlowOperation, |
324 | | StressAnomalyKind::SlowOperation |
325 | | ); |
326 | | assert_ne!( |
327 | | StressAnomalyKind::SlowOperation, |
328 | | StressAnomalyKind::TestFailure |
329 | | ); |
330 | | |
331 | | // Test all variants exist |
332 | | let _slow = StressAnomalyKind::SlowOperation; |
333 | | let _mem = StressAnomalyKind::HighMemory; |
334 | | let _fail = StressAnomalyKind::TestFailure; |
335 | | let _spike = StressAnomalyKind::TimingSpike; |
336 | | let _ndet = StressAnomalyKind::NonDeterministic; |
337 | | } |
338 | | |
339 | | #[test] |
340 | | fn test_stress_result_zero_tests() { |
341 | | let result = StressResult { |
342 | | backend: Backend::Scalar, |
343 | | input_size: 0, |
344 | | cycles_completed: 0, |
345 | | tests_passed: 0, |
346 | | tests_failed: 0, |
347 | | mean_op_time_ms: 0.0, |
348 | | max_op_time_ms: 0, |
349 | | timing_variance: 0.0, |
350 | | anomalies: vec![], |
351 | | }; |
352 | | |
353 | | // Zero tests should still pass with pass_rate of 1.0 |
354 | | assert!(result.passed()); |
355 | | assert_eq!(result.pass_rate(), 1.0); |
356 | | } |
357 | | } |