1use serde::{Deserialize, Serialize};
15use std::fmt;
16
17#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
23pub enum BenchmarkOp {
24 Write,
26 Read,
28 Delete,
30 Mixed,
32}
33
34impl fmt::Display for BenchmarkOp {
35 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
36 match self {
37 BenchmarkOp::Write => write!(f, "Write"),
38 BenchmarkOp::Read => write!(f, "Read"),
39 BenchmarkOp::Delete => write!(f, "Delete"),
40 BenchmarkOp::Mixed => write!(f, "Mixed"),
41 }
42 }
43}
44
45#[derive(Debug, Clone, Serialize, Deserialize)]
47pub struct BenchmarkConfig {
48 pub op: BenchmarkOp,
50 pub block_size: usize,
52 pub num_operations: usize,
54 pub warmup_ops: usize,
56 pub seed: u64,
58}
59
60impl BenchmarkConfig {
61 pub fn default_write() -> Self {
67 Self {
68 op: BenchmarkOp::Write,
69 block_size: 4096,
70 num_operations: 1_000,
71 warmup_ops: 100,
72 seed: 0xdeadbeef_cafebabe,
73 }
74 }
75
76 pub fn default_read() -> Self {
78 Self {
79 op: BenchmarkOp::Read,
80 block_size: 4096,
81 num_operations: 1_000,
82 warmup_ops: 100,
83 seed: 0xcafebabe_deadbeef,
84 }
85 }
86
87 pub fn default_mixed() -> Self {
89 Self {
90 op: BenchmarkOp::Mixed,
91 block_size: 65536,
92 num_operations: 500,
93 warmup_ops: 50,
94 seed: 0x1234567890abcdef,
95 }
96 }
97}
98
99impl Default for BenchmarkConfig {
100 fn default() -> Self {
101 Self::default_write()
102 }
103}
104
105#[derive(Debug, Clone, Serialize, Deserialize)]
107pub struct LatencySample {
108 pub op: BenchmarkOp,
110 pub latency_us: u64,
112 pub bytes: usize,
114 pub success: bool,
116}
117
118impl LatencySample {
119 pub fn new(op: BenchmarkOp, latency_us: u64, bytes: usize, success: bool) -> Self {
121 Self {
122 op,
123 latency_us,
124 bytes,
125 success,
126 }
127 }
128}
129
130#[derive(Debug, Clone, Serialize, Deserialize)]
132pub struct BenchmarkResult {
133 pub config: BenchmarkConfig,
135 pub total_ops: usize,
137 pub successful_ops: usize,
139 pub failed_ops: usize,
141 pub total_bytes: u64,
143 pub duration_us: u64,
145 pub throughput_mbps: f64,
147 pub latency_p50_us: u64,
149 pub latency_p95_us: u64,
151 pub latency_p99_us: u64,
153 pub latency_min_us: u64,
155 pub latency_max_us: u64,
157}
158
159#[derive(Debug, Clone, Default, Serialize, Deserialize)]
161pub struct BenchmarkStats {
162 pub runs: u64,
164 pub total_ops: u64,
166 pub total_bytes: u64,
168}
169
170impl BenchmarkStats {
171 pub fn record(&mut self, result: &BenchmarkResult) {
173 self.runs = self.runs.saturating_add(1);
174 self.total_ops = self.total_ops.saturating_add(result.total_ops as u64);
175 self.total_bytes = self.total_bytes.saturating_add(result.total_bytes);
176 }
177
178 pub fn avg_ops_per_run(&self) -> f64 {
180 if self.runs == 0 {
181 0.0
182 } else {
183 self.total_ops as f64 / self.runs as f64
184 }
185 }
186
187 pub fn avg_bytes_per_run(&self) -> f64 {
189 if self.runs == 0 {
190 0.0
191 } else {
192 self.total_bytes as f64 / self.runs as f64
193 }
194 }
195}
196
197pub struct StorageBenchmark {
222 config: BenchmarkConfig,
223 samples: Vec<LatencySample>,
224 rng_state: u64,
226}
227
228impl StorageBenchmark {
229 pub fn new(config: BenchmarkConfig) -> Self {
234 let rng_state = if config.seed == 0 {
236 0x9e3779b97f4a7c15
237 } else {
238 config.seed
239 };
240 Self {
241 config,
242 samples: Vec::new(),
243 rng_state,
244 }
245 }
246
247 pub fn add_sample(&mut self, sample: LatencySample) {
251 self.samples.push(sample);
252 }
253
254 pub fn record_op(&mut self, op: BenchmarkOp, latency_us: u64, bytes: usize, success: bool) {
257 self.add_sample(LatencySample::new(op, latency_us, bytes, success));
258 }
259
260 pub fn sample_count(&self) -> usize {
262 self.samples.len()
263 }
264
265 pub fn success_rate(&self) -> f64 {
267 if self.samples.is_empty() {
268 return 0.0;
269 }
270 let successes = self.samples.iter().filter(|s| s.success).count();
271 successes as f64 / self.samples.len() as f64
272 }
273
274 pub fn reset(&mut self) {
276 self.samples.clear();
277 self.rng_state = if self.config.seed == 0 {
278 0x9e3779b97f4a7c15
279 } else {
280 self.config.seed
281 };
282 }
283
284 pub fn compute_result(&self, duration_us: u64) -> BenchmarkResult {
292 let total_ops = self.samples.len();
293 let successful_ops = self.samples.iter().filter(|s| s.success).count();
294 let failed_ops = total_ops - successful_ops;
295
296 let total_bytes: u64 = self
297 .samples
298 .iter()
299 .filter(|s| s.success)
300 .map(|s| s.bytes as u64)
301 .fold(0u64, |acc, b| acc.saturating_add(b));
302
303 let throughput_mbps = Self::compute_throughput(total_bytes, duration_us);
304
305 let mut latencies: Vec<u64> = self.samples.iter().map(|s| s.latency_us).collect();
307
308 let (latency_min_us, latency_max_us) = if latencies.is_empty() {
309 (0, 0)
310 } else {
311 let min = latencies.iter().copied().min().unwrap_or(0);
312 let max = latencies.iter().copied().max().unwrap_or(0);
313 (min, max)
314 };
315
316 let latency_p50_us = Self::compute_percentile(&mut latencies, 50.0);
317 let latency_p95_us = Self::compute_percentile(&mut latencies, 95.0);
318 let latency_p99_us = Self::compute_percentile(&mut latencies, 99.0);
319
320 BenchmarkResult {
321 config: self.config.clone(),
322 total_ops,
323 successful_ops,
324 failed_ops,
325 total_bytes,
326 duration_us,
327 throughput_mbps,
328 latency_p50_us,
329 latency_p95_us,
330 latency_p99_us,
331 latency_min_us,
332 latency_max_us,
333 }
334 }
335
336 pub fn compute_percentile(samples: &mut [u64], percentile: f64) -> u64 {
342 if samples.is_empty() {
343 return 0;
344 }
345 samples.sort_unstable();
346
347 let pct = percentile.clamp(0.0, 100.0);
348 let rank = ((pct / 100.0) * samples.len() as f64).ceil() as usize;
350 let idx = rank.saturating_sub(1).min(samples.len() - 1);
351 samples[idx]
352 }
353
354 pub fn compute_throughput(bytes: u64, duration_us: u64) -> f64 {
358 if duration_us == 0 {
359 return 0.0;
360 }
361 bytes as f64 / duration_us as f64
363 }
364
365 pub fn generate_block(&mut self, size: usize) -> Vec<u8> {
373 let mut out = Vec::with_capacity(size);
374 let mut state = self.rng_state;
375
376 let full_words = size / 8;
377 let remainder = size % 8;
378
379 for _ in 0..full_words {
380 state = xorshift64(state);
381 out.extend_from_slice(&state.to_le_bytes());
382 }
383
384 if remainder > 0 {
385 state = xorshift64(state);
386 let word_bytes = state.to_le_bytes();
387 out.extend_from_slice(&word_bytes[..remainder]);
388 }
389
390 self.rng_state = state;
391 out
392 }
393
394 pub fn format_result(result: &BenchmarkResult) -> String {
398 let success_pct = if result.total_ops == 0 {
399 0.0
400 } else {
401 100.0 * result.successful_ops as f64 / result.total_ops as f64
402 };
403
404 let duration_ms = result.duration_us as f64 / 1_000.0;
405 let block_kb = result.config.block_size as f64 / 1024.0;
406 let total_mb = result.total_bytes as f64 / (1024.0 * 1024.0);
407
408 format!(
409 "=== Benchmark Result ===\n\
410 Operation : {op}\n\
411 Block size : {block_kb:.1} KiB\n\
412 Operations : {total_ops} total / {successful_ops} ok / {failed_ops} failed ({success_pct:.1}%)\n\
413 Data : {total_mb:.2} MiB in {duration_ms:.2} ms\n\
414 Throughput : {throughput_mbps:.3} MB/s\n\
415 Latency : min={min}µs p50={p50}µs p95={p95}µs p99={p99}µs max={max}µs",
416 op = result.config.op,
417 block_kb = block_kb,
418 total_ops = result.total_ops,
419 successful_ops = result.successful_ops,
420 failed_ops = result.failed_ops,
421 success_pct = success_pct,
422 total_mb = total_mb,
423 duration_ms = duration_ms,
424 throughput_mbps = result.throughput_mbps,
425 min = result.latency_min_us,
426 p50 = result.latency_p50_us,
427 p95 = result.latency_p95_us,
428 p99 = result.latency_p99_us,
429 max = result.latency_max_us,
430 )
431 }
432}
433
434#[inline]
443fn xorshift64(mut state: u64) -> u64 {
444 debug_assert_ne!(state, 0, "xorshift64 state must not be zero");
445 state ^= state << 13;
446 state ^= state >> 7;
447 state ^= state << 17;
448 state
449}
450
451#[cfg(test)]
456mod tests {
457 use super::*;
458
459 fn default_bench() -> StorageBenchmark {
462 StorageBenchmark::new(BenchmarkConfig::default_write())
463 }
464
465 fn bench_with_seed(seed: u64) -> StorageBenchmark {
466 let mut cfg = BenchmarkConfig::default_write();
467 cfg.seed = seed;
468 StorageBenchmark::new(cfg)
469 }
470
471 fn make_sample(latency: u64, bytes: usize, success: bool) -> LatencySample {
472 LatencySample::new(BenchmarkOp::Write, latency, bytes, success)
473 }
474
475 #[test]
478 fn test_add_sample_increments_count() {
479 let mut bench = default_bench();
480 assert_eq!(bench.sample_count(), 0);
481 bench.add_sample(make_sample(100, 4096, true));
482 assert_eq!(bench.sample_count(), 1);
483 bench.add_sample(make_sample(200, 4096, false));
484 assert_eq!(bench.sample_count(), 2);
485 }
486
487 #[test]
490 fn test_record_op_stores_sample() {
491 let mut bench = default_bench();
492 bench.record_op(BenchmarkOp::Read, 50, 8192, true);
493 assert_eq!(bench.sample_count(), 1);
494 let s = &bench.samples[0];
495 assert_eq!(s.latency_us, 50);
496 assert_eq!(s.bytes, 8192);
497 assert!(s.success);
498 }
499
500 #[test]
503 fn test_success_rate_empty() {
504 let bench = default_bench();
505 assert_eq!(bench.success_rate(), 0.0);
506 }
507
508 #[test]
511 fn test_success_rate_all_success() {
512 let mut bench = default_bench();
513 for _ in 0..10 {
514 bench.record_op(BenchmarkOp::Write, 100, 4096, true);
515 }
516 let rate = bench.success_rate();
517 assert!((rate - 1.0).abs() < f64::EPSILON);
518 }
519
520 #[test]
523 fn test_success_rate_all_failure() {
524 let mut bench = default_bench();
525 for _ in 0..5 {
526 bench.record_op(BenchmarkOp::Write, 300, 4096, false);
527 }
528 assert_eq!(bench.success_rate(), 0.0);
529 }
530
531 #[test]
534 fn test_success_rate_mixed() {
535 let mut bench = default_bench();
536 bench.record_op(BenchmarkOp::Write, 100, 4096, true);
537 bench.record_op(BenchmarkOp::Write, 200, 4096, false);
538 let rate = bench.success_rate();
539 assert!((rate - 0.5).abs() < f64::EPSILON);
540 }
541
542 #[test]
545 fn test_reset_clears_samples() {
546 let mut bench = default_bench();
547 bench.record_op(BenchmarkOp::Write, 100, 4096, true);
548 bench.record_op(BenchmarkOp::Write, 200, 4096, true);
549 bench.reset();
550 assert_eq!(bench.sample_count(), 0);
551 }
552
553 #[test]
554 fn test_reset_restores_prng() {
555 let mut bench = bench_with_seed(42);
556 let block_a = bench.generate_block(16);
557 bench.reset();
558 let block_b = bench.generate_block(16);
559 assert_eq!(block_a, block_b);
561 }
562
563 #[test]
566 fn test_generate_block_exact_size() {
567 let mut bench = bench_with_seed(1);
568 for size in [0, 1, 7, 8, 9, 15, 16, 63, 64, 100, 4096, 65536] {
569 let block = bench.generate_block(size);
570 assert_eq!(block.len(), size, "expected {size} bytes");
571 }
572 }
573
574 #[test]
577 fn test_generate_block_deterministic() {
578 let mut a = bench_with_seed(999);
579 let mut b = bench_with_seed(999);
580 assert_eq!(a.generate_block(64), b.generate_block(64));
581 }
582
583 #[test]
586 fn test_generate_block_advances_prng() {
587 let mut bench = bench_with_seed(7);
588 let b1 = bench.generate_block(8);
589 let b2 = bench.generate_block(8);
590 assert_ne!(b1, b2);
591 }
592
593 #[test]
596 fn test_percentile_empty() {
597 let mut v: Vec<u64> = Vec::new();
598 assert_eq!(StorageBenchmark::compute_percentile(&mut v, 50.0), 0);
599 }
600
601 #[test]
604 fn test_percentile_p50_known() {
605 let mut v = vec![5, 1, 3, 2, 4];
607 assert_eq!(StorageBenchmark::compute_percentile(&mut v, 50.0), 3);
608 }
609
610 #[test]
613 fn test_percentile_p95() {
614 let mut v: Vec<u64> = (1..=20).collect();
616 fastrand::shuffle(&mut v);
617 assert_eq!(StorageBenchmark::compute_percentile(&mut v, 95.0), 19);
618 }
619
620 #[test]
623 fn test_percentile_p99() {
624 let mut v: Vec<u64> = (1..=100).collect();
626 fastrand::shuffle(&mut v);
627 assert_eq!(StorageBenchmark::compute_percentile(&mut v, 99.0), 99);
628 }
629
630 #[test]
633 fn test_percentile_single() {
634 let mut v = vec![42u64];
635 assert_eq!(StorageBenchmark::compute_percentile(&mut v, 50.0), 42);
636 assert_eq!(StorageBenchmark::compute_percentile(&mut v, 99.0), 42);
637 }
638
639 #[test]
642 fn test_throughput_formula() {
643 let mbps = StorageBenchmark::compute_throughput(1_048_576, 1_000_000);
647 assert!((mbps - 1.048576).abs() < 1e-6, "got {mbps}");
648 }
649
650 #[test]
653 fn test_throughput_zero_duration() {
654 assert_eq!(StorageBenchmark::compute_throughput(1_000_000, 0), 0.0);
655 }
656
657 #[test]
660 fn test_compute_result_known_data() {
661 let mut bench = default_bench();
662 for i in 1u64..=5 {
664 bench.record_op(BenchmarkOp::Write, i * 10, 4096, true);
665 }
666 let result = bench.compute_result(1_000_000);
668
669 assert_eq!(result.total_ops, 5);
670 assert_eq!(result.successful_ops, 5);
671 assert_eq!(result.failed_ops, 0);
672 assert_eq!(result.total_bytes, 5 * 4096);
673 assert_eq!(result.latency_min_us, 10);
674 assert_eq!(result.latency_max_us, 50);
675 assert_eq!(result.latency_p50_us, 30);
677 }
678
679 #[test]
682 fn test_compute_result_all_failures() {
683 let mut bench = default_bench();
684 for _ in 0..10 {
685 bench.record_op(BenchmarkOp::Write, 500, 4096, false);
686 }
687 let result = bench.compute_result(500_000);
688 assert_eq!(result.successful_ops, 0);
689 assert_eq!(result.failed_ops, 10);
690 assert_eq!(result.total_bytes, 0);
691 assert_eq!(result.throughput_mbps, 0.0);
693 }
694
695 #[test]
698 fn test_compute_result_zero_duration() {
699 let mut bench = default_bench();
700 bench.record_op(BenchmarkOp::Write, 100, 4096, true);
701 let result = bench.compute_result(0);
702 assert_eq!(result.throughput_mbps, 0.0);
703 }
704
705 #[test]
708 fn test_compute_result_empty() {
709 let bench = default_bench();
710 let result = bench.compute_result(1_000_000);
711 assert_eq!(result.total_ops, 0);
712 assert_eq!(result.successful_ops, 0);
713 assert_eq!(result.total_bytes, 0);
714 assert_eq!(result.latency_min_us, 0);
715 assert_eq!(result.latency_max_us, 0);
716 }
717
718 #[test]
721 fn test_mixed_op_results() {
722 let mut cfg = BenchmarkConfig::default_mixed();
723 cfg.seed = 1;
724 let mut bench = StorageBenchmark::new(cfg);
725
726 bench.record_op(BenchmarkOp::Write, 80, 4096, true);
727 bench.record_op(BenchmarkOp::Read, 40, 4096, true);
728 bench.record_op(BenchmarkOp::Delete, 20, 0, true);
729 bench.record_op(BenchmarkOp::Write, 200, 4096, false);
730
731 let result = bench.compute_result(1_000_000);
732 assert_eq!(result.total_ops, 4);
733 assert_eq!(result.successful_ops, 3);
734 assert_eq!(result.failed_ops, 1);
735 assert_eq!(result.total_bytes, 8192);
737 }
738
739 #[test]
742 fn test_large_sample_set() {
743 let mut bench = bench_with_seed(12345);
744 let mut state = 12345u64;
745 for _ in 0..1000 {
746 state = xorshift64(state);
747 let latency = (state % 10_000) + 1; bench.record_op(BenchmarkOp::Write, latency, 4096, true);
749 }
750 let result = bench.compute_result(5_000_000);
751
752 assert_eq!(result.total_ops, 1000);
753 assert_eq!(result.successful_ops, 1000);
754 assert!(result.latency_p50_us > 0);
755 assert!(result.latency_p95_us >= result.latency_p50_us);
756 assert!(result.latency_p99_us >= result.latency_p95_us);
757 assert!(result.latency_max_us >= result.latency_p99_us);
758 assert!(result.latency_min_us <= result.latency_p50_us);
759 }
760
761 #[test]
764 fn test_warmup_ops_not_in_samples() {
765 let mut cfg = BenchmarkConfig::default_write();
769 cfg.warmup_ops = 10;
770 cfg.num_operations = 5;
771 let mut bench = StorageBenchmark::new(cfg);
772
773 for _ in 0..5 {
775 bench.record_op(BenchmarkOp::Write, 100, 4096, true);
776 }
777
778 let result = bench.compute_result(500_000);
779 assert_eq!(result.total_ops, 5);
781 }
782
783 #[test]
786 fn test_format_result_contains_key_info() {
787 let mut bench = default_bench();
788 for i in 1u64..=4 {
789 bench.record_op(BenchmarkOp::Write, i * 25, 4096, true);
790 }
791 let result = bench.compute_result(1_000_000);
792 let s = StorageBenchmark::format_result(&result);
793
794 assert!(s.contains("Write"), "op type missing");
795 assert!(s.contains("Throughput"), "throughput label missing");
796 assert!(s.contains("Latency"), "latency label missing");
797 assert!(s.contains("p50"), "p50 label missing");
798 assert!(s.contains("p95"), "p95 label missing");
799 assert!(s.contains("p99"), "p99 label missing");
800 assert!(s.contains("Operations"), "operations label missing");
801 }
802
803 #[test]
806 fn test_benchmark_stats_record() {
807 let mut stats = BenchmarkStats::default();
808 assert_eq!(stats.runs, 0);
809
810 let mut bench = default_bench();
811 bench.record_op(BenchmarkOp::Write, 100, 4096, true);
812 bench.record_op(BenchmarkOp::Write, 200, 4096, false);
813 let r1 = bench.compute_result(1_000_000);
814 stats.record(&r1);
815
816 assert_eq!(stats.runs, 1);
817 assert_eq!(stats.total_ops, 2);
818 assert_eq!(stats.total_bytes, 4096);
820 }
821
822 #[test]
825 fn test_benchmark_stats_averages() {
826 let mut stats = BenchmarkStats::default();
827
828 let mut bench = default_bench();
829 bench.record_op(BenchmarkOp::Write, 100, 4096, true);
830 let r = bench.compute_result(1_000_000);
831 stats.record(&r);
832
833 bench.reset();
834 bench.record_op(BenchmarkOp::Write, 100, 4096, true);
835 bench.record_op(BenchmarkOp::Write, 100, 4096, true);
836 let r2 = bench.compute_result(1_000_000);
837 stats.record(&r2);
838
839 assert!((stats.avg_ops_per_run() - 1.5).abs() < f64::EPSILON);
841 }
842
843 #[test]
846 fn test_benchmark_stats_avg_zero_runs() {
847 let stats = BenchmarkStats::default();
848 assert_eq!(stats.avg_ops_per_run(), 0.0);
849 assert_eq!(stats.avg_bytes_per_run(), 0.0);
850 }
851
852 #[test]
855 fn test_generate_block_zero_bytes() {
856 let mut bench = bench_with_seed(1);
857 let block = bench.generate_block(0);
858 assert!(block.is_empty());
859 }
860
861 #[test]
864 fn test_benchmark_op_display() {
865 assert_eq!(format!("{}", BenchmarkOp::Write), "Write");
866 assert_eq!(format!("{}", BenchmarkOp::Read), "Read");
867 assert_eq!(format!("{}", BenchmarkOp::Delete), "Delete");
868 assert_eq!(format!("{}", BenchmarkOp::Mixed), "Mixed");
869 }
870}