// Copyright 2024 RustFS Team // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. //! Reed-Solomon SIMD erasure coding performance benchmarks. //! //! This benchmark tests the performance of the high-performance SIMD Reed-Solomon implementation. //! //! ## Running Benchmarks //! //! ```bash //! # Run all benchmarks //! cargo bench //! //! # Run specific benchmark //! cargo bench --bench erasure_benchmark //! //! # Generate HTML report //! cargo bench --bench erasure_benchmark -- --output-format html //! //! # Test encoding performance only //! cargo bench encode //! //! # Test decoding performance only //! cargo bench decode //! ``` //! //! ## Test Configurations //! //! The benchmarks test various scenarios: //! - Different data sizes: 1KB, 64KB, 1MB, 16MB //! - Different erasure coding configurations: (4,2), (6,3), (8,4) //! - Both encoding and decoding operations //! - SIMD optimization for different shard sizes use criterion::{BenchmarkId, Criterion, Throughput, criterion_group, criterion_main}; use rustfs_utils::HashAlgorithm; mod storage_api; use std::hint::black_box; use std::io::Cursor; use std::time::Duration; use storage_api::erasure::{BitrotReader, BitrotWriter, Erasure, calc_shard_size}; use tokio::runtime::Runtime; /// Benchmark configuration structure #[derive(Clone, Debug)] struct BenchConfig { /// Number of data shards data_shards: usize, /// Number of parity shards parity_shards: usize, /// Test data size (bytes) data_size: usize, /// Block size (bytes) block_size: usize, /// Configuration name name: String, } impl BenchConfig { fn new(data_shards: usize, parity_shards: usize, data_size: usize, block_size: usize) -> Self { Self { data_shards, parity_shards, data_size, block_size, name: format!("{}+{}_{}KB_{}KB-block", data_shards, parity_shards, data_size / 1024, block_size / 1024), } } } /// Generate test data fn generate_test_data(size: usize) -> Vec { (0..size).map(|i| (i % 256) as u8).collect() } /// Benchmark: Encoding performance fn bench_encode_performance(c: &mut Criterion) { let configs = vec![ // Small data tests - 1KB BenchConfig::new(4, 2, 1024, 1024), BenchConfig::new(6, 3, 1024, 1024), BenchConfig::new(8, 4, 1024, 1024), // Medium data tests - 64KB BenchConfig::new(4, 2, 64 * 1024, 64 * 1024), BenchConfig::new(6, 3, 64 * 1024, 64 * 1024), BenchConfig::new(8, 4, 64 * 1024, 64 * 1024), // Large data tests - 1MB BenchConfig::new(4, 2, 1024 * 1024, 1024 * 1024), BenchConfig::new(6, 3, 1024 * 1024, 1024 * 1024), BenchConfig::new(8, 4, 1024 * 1024, 1024 * 1024), // Extra large data tests - 16MB BenchConfig::new(4, 2, 16 * 1024 * 1024, 16 * 1024 * 1024), BenchConfig::new(6, 3, 16 * 1024 * 1024, 16 * 1024 * 1024), ]; for config in configs { let data = generate_test_data(config.data_size); // Test SIMD encoding performance let mut group = c.benchmark_group("encode_simd"); group.throughput(Throughput::Bytes(config.data_size as u64)); group.sample_size(10); group.measurement_time(Duration::from_secs(5)); group.bench_with_input(BenchmarkId::new("simd_impl", &config.name), &(&data, &config), |b, (data, config)| { let erasure = Erasure::new(config.data_shards, config.parity_shards, config.block_size); b.iter(|| { let shards = erasure.encode_data(black_box(data)).unwrap(); black_box(shards); }); }); group.finish(); // Test direct reed-solomon-erasure implementation for large shards (>= 512 bytes) let shard_size = calc_shard_size(config.data_size, config.data_shards); if shard_size >= 512 && config.parity_shards > 0 { use reed_solomon_erasure::galois_8::ReedSolomon; let mut rse_group = c.benchmark_group("encode_rse_direct"); rse_group.throughput(Throughput::Bytes(config.data_size as u64)); rse_group.sample_size(10); rse_group.measurement_time(Duration::from_secs(5)); if let Ok(rs) = ReedSolomon::new(config.data_shards, config.parity_shards) { let total_shards = config.data_shards + config.parity_shards; let per_shard_size = calc_shard_size(config.data_size, config.data_shards); let need_total = per_shard_size * total_shards; rse_group.bench_with_input( BenchmarkId::new("rse_direct", &config.name), &(&data, need_total, per_shard_size), |b, (data, need_total, per_shard_size)| { b.iter(|| { let mut buffer = vec![0u8; *need_total]; let copy_len = data.len().min(buffer.len()); buffer[..copy_len].copy_from_slice(&data[..copy_len]); let mut slices: Vec<&mut [u8]> = buffer.chunks_exact_mut(*per_shard_size).collect(); rs.encode(&mut slices).unwrap(); black_box(buffer); }); }, ); } rse_group.finish(); } } } /// Benchmark: Decoding performance fn bench_decode_performance(c: &mut Criterion) { let configs = vec![ // Medium data tests - 64KB BenchConfig::new(4, 2, 64 * 1024, 64 * 1024), BenchConfig::new(6, 3, 64 * 1024, 64 * 1024), // Large data tests - 1MB BenchConfig::new(4, 2, 1024 * 1024, 1024 * 1024), BenchConfig::new(6, 3, 1024 * 1024, 1024 * 1024), // Extra large data tests - 16MB BenchConfig::new(4, 2, 16 * 1024 * 1024, 16 * 1024 * 1024), ]; for config in configs { let data = generate_test_data(config.data_size); let erasure = Erasure::new(config.data_shards, config.parity_shards, config.block_size); // Pre-encode data let encoded_shards = erasure.encode_data(&data).unwrap(); // Test SIMD decoding performance let mut group = c.benchmark_group("decode_simd"); group.throughput(Throughput::Bytes(config.data_size as u64)); group.sample_size(10); group.measurement_time(Duration::from_secs(5)); group.bench_with_input( BenchmarkId::new("simd_impl", &config.name), &(&encoded_shards, &config), |b, (shards, config)| { let erasure = Erasure::new(config.data_shards, config.parity_shards, config.block_size); b.iter(|| { // Simulate data loss - lose one data shard and one parity shard let mut shards_opt: Vec>> = shards.iter().map(|shard| Some(shard.to_vec())).collect(); // Lose last data shard and first parity shard shards_opt[config.data_shards - 1] = None; shards_opt[config.data_shards] = None; erasure.decode_data(black_box(&mut shards_opt)).unwrap(); black_box(&shards_opt); }); }, ); group.finish(); // Test direct reed-solomon-erasure decoding for large shards let shard_size = calc_shard_size(config.data_size, config.data_shards); if shard_size >= 512 && config.parity_shards > 0 { use reed_solomon_erasure::galois_8::ReedSolomon; if let Ok(rs) = ReedSolomon::new(config.data_shards, config.parity_shards) { let mut rse_group = c.benchmark_group("decode_rse_direct"); rse_group.throughput(Throughput::Bytes(config.data_size as u64)); rse_group.sample_size(10); rse_group.measurement_time(Duration::from_secs(5)); rse_group.bench_with_input( BenchmarkId::new("rse_direct", &config.name), &(&encoded_shards, &config), |b, (shards, config)| { b.iter(|| { let mut shards_opt: Vec>> = shards.iter().map(|s| Some(s.to_vec())).collect(); shards_opt[config.data_shards - 1] = None; shards_opt[config.data_shards] = None; rs.reconstruct_data(&mut shards_opt).unwrap(); black_box(shards_opt); }); }, ); rse_group.finish(); } } } } /// Benchmark: Impact of different shard sizes on performance fn bench_shard_size_impact(c: &mut Criterion) { let shard_sizes = vec![64, 128, 256, 512, 1024, 2048, 4096, 8192]; let data_shards = 4; let parity_shards = 2; let mut group = c.benchmark_group("shard_size_impact"); group.sample_size(10); group.measurement_time(Duration::from_secs(3)); for shard_size in shard_sizes { let total_data_size = shard_size * data_shards; let data = generate_test_data(total_data_size); group.throughput(Throughput::Bytes(total_data_size as u64)); // Test SIMD implementation group.bench_with_input(BenchmarkId::new("simd", format!("shard_{shard_size}B")), &data, |b, data| { let erasure = Erasure::new(data_shards, parity_shards, total_data_size); b.iter(|| { let shards = erasure.encode_data(black_box(data)).unwrap(); black_box(shards); }); }); } group.finish(); } /// Benchmark: Impact of coding configurations on performance fn bench_coding_configurations(c: &mut Criterion) { let configs = vec![ (2, 1), // Minimal redundancy (3, 2), // Medium redundancy (4, 2), // Common configuration (6, 3), // 50% redundancy (8, 4), // 50% redundancy, more shards (10, 5), // 50% redundancy, many shards (12, 6), // 50% redundancy, very many shards ]; let data_size = 1024 * 1024; // 1MB test data let data = generate_test_data(data_size); let mut group = c.benchmark_group("coding_configurations"); group.throughput(Throughput::Bytes(data_size as u64)); group.sample_size(10); group.measurement_time(Duration::from_secs(5)); for (data_shards, parity_shards) in configs { let config_name = format!("{data_shards}+{parity_shards}"); group.bench_with_input(BenchmarkId::new("encode", &config_name), &data, |b, data| { let erasure = Erasure::new(data_shards, parity_shards, data_size); b.iter(|| { let shards = erasure.encode_data(black_box(data)).unwrap(); black_box(shards); }); }); } group.finish(); } /// Benchmark: Memory usage patterns fn bench_memory_patterns(c: &mut Criterion) { let data_shards = 4; let parity_shards = 2; let block_size = 1024 * 1024; // 1MB block let mut group = c.benchmark_group("memory_patterns"); group.sample_size(10); group.measurement_time(Duration::from_secs(5)); // Test reusing the same Erasure instance group.bench_function("reuse_erasure_instance", |b| { let erasure = Erasure::new(data_shards, parity_shards, block_size); let data = generate_test_data(block_size); b.iter(|| { let shards = erasure.encode_data(black_box(&data)).unwrap(); black_box(shards); }); }); // Test creating new Erasure instance each time group.bench_function("new_erasure_instance", |b| { let data = generate_test_data(block_size); b.iter(|| { let erasure = Erasure::new(data_shards, parity_shards, block_size); let shards = erasure.encode_data(black_box(&data)).unwrap(); black_box(shards); }); }); group.finish(); } /// Benchmark: end-to-end streaming decode through `Erasure::decode`. /// /// Unlike `bench_decode_performance` (which calls `decode_data` on in-memory /// shards), this drives the async `ParallelReader` path that reads each shard /// through a `BitrotReader` per erasure stripe. That is the path executed on /// every object GET, and the one where per-stripe shard buffers are allocated. fn bench_streaming_decode(c: &mut Criterion) { let rt = Runtime::new().unwrap(); let hash_algo = HashAlgorithm::HighwayHash256; // (data_shards, parity_shards, object_size, block_size) let configs = vec![ (4usize, 2usize, 16 * 1024 * 1024usize, 1024 * 1024usize), (6, 3, 24 * 1024 * 1024, 1024 * 1024), (4, 2, 4 * 1024 * 1024, 64 * 1024), ]; for (data_shards, parity_shards, data_size, block_size) in configs { let data = generate_test_data(data_size); let erasure = Erasure::new(data_shards, parity_shards, block_size); let shard_size = erasure.shard_size(); let total_len = data.len(); // Pre-encode the object into per-shard bitrot streams once (setup). let total_shards = data_shards + parity_shards; let shard_bufs: Vec> = rt.block_on(async { let mut writers: Vec>>> = (0..total_shards) .map(|_| BitrotWriter::new(Cursor::new(Vec::new()), shard_size, hash_algo.clone())) .collect(); let mut off = 0; while off < data.len() { let end = (off + block_size).min(data.len()); let shards = erasure.encode_data(&data[off..end]).unwrap(); for (i, shard) in shards.iter().enumerate() { writers[i].write(shard).await.unwrap(); } off = end; } writers.into_iter().map(|w| w.into_inner().into_inner()).collect() }); // verify=true mirrors the production default (bitrot verification on); // verify=false isolates the buffer-handling cost from the hash pass. for verify in [true, false] { let skip_verify = !verify; let mut group = c.benchmark_group("streaming_decode"); group.throughput(Throughput::Bytes(total_len as u64)); group.sample_size(10); group.measurement_time(Duration::from_secs(5)); let name = format!( "{}+{}_{}KB_{}KB-block_verify-{}", data_shards, parity_shards, data_size / 1024, block_size / 1024, verify ); // Validate decode once per config (untimed) so a regression in // success or output length fails the benchmark without putting // assertions inside the measured loop. { let readers: Vec>>> = shard_bufs .iter() .map(|buf| Some(BitrotReader::new(Cursor::new(buf.as_slice()), shard_size, hash_algo.clone(), skip_verify))) .collect(); let mut sink = tokio::io::sink(); let (written, err) = rt.block_on(erasure.decode(&mut sink, readers, 0, total_len, total_len)); assert!(err.is_none(), "decode failed: {err:?}"); assert_eq!(written, total_len, "decode wrote {written} of {total_len} bytes"); } group.bench_function(BenchmarkId::new("decode", name), |b| { b.iter_batched( || { // Setup (untimed): per-shard readers positioned at the // object start, plus the no-op sink, so reader/UUID and // sink construction stay out of the timed decode path. let readers: Vec>>> = shard_bufs .iter() .map(|buf| { Some(BitrotReader::new(Cursor::new(buf.as_slice()), shard_size, hash_algo.clone(), skip_verify)) }) .collect(); (readers, tokio::io::sink()) }, |(readers, mut sink)| { rt.block_on(async { let (written, _err) = erasure.decode(black_box(&mut sink), readers, 0, total_len, total_len).await; black_box(written); }); }, criterion::BatchSize::SmallInput, ); }); group.finish(); } } } // Benchmark group configuration criterion_group!( benches, bench_encode_performance, bench_decode_performance, bench_streaming_decode, bench_shard_size_impact, bench_coding_configurations, bench_memory_patterns ); criterion_main!(benches);