diff --git a/crates/ruvector-temporal-tensor/src/store.rs b/crates/ruvector-temporal-tensor/src/store.rs index a2d09e7b..edb42ddd 100644 --- a/crates/ruvector-temporal-tensor/src/store.rs +++ b/crates/ruvector-temporal-tensor/src/store.rs @@ -1934,4 +1934,248 @@ mod tests { assert_eq!(*ts, 200); assert_eq!(m.total_blocks, 5); } + + // ----------------------------------------------------------------------- + // Benchmarks + // ----------------------------------------------------------------------- + // + // Run with: cargo test bench_ -- --nocapture + // These use std::time::Instant and std::hint::black_box for stable timing. + + #[test] + fn bench_batch_scoring_10k() { + use std::time::Instant; + use crate::tiering::{ + TierConfig, BlockMeta as TBlockMeta, Tier as TTier, + compute_scores_batch, compute_score, + }; + + let cfg = TierConfig::default(); + let metas: Vec = (0..10_000).map(|i| { + TBlockMeta { + ema_rate: (i as f32) * 0.0001, + access_window: 0x5555_5555_5555_5555, + last_access: 50 + (i as u64 % 100), + access_count: i as u64, + current_tier: TTier::Tier1, + tier_since: 0, + } + }).collect(); + + let iters = 1000; + + // Individual scoring + let start = Instant::now(); + for _ in 0..iters { + for m in &metas { + std::hint::black_box(compute_score(&cfg, 100, m)); + } + } + let individual = start.elapsed(); + + // Batch scoring + let start = Instant::now(); + for _ in 0..iters { + std::hint::black_box(compute_scores_batch(&cfg, 100, &metas)); + } + let batch = start.elapsed(); + + eprintln!("Individual scoring 10k x {iters}: {:?} ({:.0} ns/block)", + individual, individual.as_nanos() as f64 / (iters * 10_000) as f64); + eprintln!("Batch scoring 10k x {iters}: {:?} ({:.0} ns/block)", + batch, batch.as_nanos() as f64 / (iters * 10_000) as f64); + } + + #[test] + fn bench_dequant_5bit_4096() { + use std::time::Instant; + + let data: Vec = (0..4096).map(|i| (i as f32 - 2048.0) * 0.01).collect(); + let (packed, scale) = quantize_block(&data, 5); + let mut out = vec![0.0f32; 4096]; + + let iters = 10_000; + let start = Instant::now(); + for _ in 0..iters { + std::hint::black_box(dequantize_block(&packed, scale, 5, 4096, &mut out)); + } + let elapsed = start.elapsed(); + + let total_bytes = 4096u64 * 4 * iters as u64; + let gbs = total_bytes as f64 / elapsed.as_secs_f64() / 1e9; + eprintln!("Dequant 5-bit 4096 x {iters}: {:?} ({:.2} GB/s output throughput)", + elapsed, gbs); + } + + #[test] + fn bench_dequant_7bit_4096() { + use std::time::Instant; + + let data: Vec = (0..4096).map(|i| (i as f32 - 2048.0) * 0.01).collect(); + let (packed, scale) = quantize_block(&data, 7); + let mut out = vec![0.0f32; 4096]; + + let iters = 10_000; + let start = Instant::now(); + for _ in 0..iters { + std::hint::black_box(dequantize_block(&packed, scale, 7, 4096, &mut out)); + } + let elapsed = start.elapsed(); + + let total_bytes = 4096u64 * 4 * iters as u64; + let gbs = total_bytes as f64 / elapsed.as_secs_f64() / 1e9; + eprintln!("Dequant 7-bit 4096 x {iters}: {:?} ({:.2} GB/s output throughput)", + elapsed, gbs); + } + + #[test] + fn bench_quant_5bit_4096() { + use std::time::Instant; + + let data: Vec = (0..4096).map(|i| (i as f32 - 2048.0) * 0.01).collect(); + + let iters = 10_000; + let start = Instant::now(); + for _ in 0..iters { + std::hint::black_box(quantize_block(&data, 5)); + } + let elapsed = start.elapsed(); + + let total_bytes = 4096u64 * 4 * iters as u64; + let gbs = total_bytes as f64 / elapsed.as_secs_f64() / 1e9; + eprintln!("Quant 5-bit 4096 x {iters}: {:?} ({:.2} GB/s input throughput)", + elapsed, gbs); + } + + #[test] + fn bench_svd_adaptive_64x64() { + use std::time::Instant; + use crate::delta::FactorSet; + + let (rows, cols) = (64, 64); + let data: Vec = (0..rows * cols) + .map(|i| (i as f32 * 0.37).sin() + (i as f32 * 0.73).cos()) + .collect(); + + let iters = 100; + let start = Instant::now(); + for _ in 0..iters { + std::hint::black_box( + FactorSet::from_data_adaptive(&data, rows, cols, 16, 0.05) + ); + } + let elapsed = start.elapsed(); + + eprintln!("SVD adaptive 64x64 (max_rank=16, target=0.05) x {iters}: {:?} ({:.2} ms/iter)", + elapsed, elapsed.as_secs_f64() * 1000.0 / iters as f64); + } + + #[test] + fn bench_format_report() { + use std::time::Instant; + use crate::metrics::StoreMetrics; + + let m = StoreMetrics { + total_blocks: 10_000, + tier0_blocks: 500, + tier1_blocks: 4000, + tier2_blocks: 3500, + tier3_blocks: 2000, + tier1_bytes: 4_000_000, + tier2_bytes: 2_500_000, + tier3_bytes: 750_000, + total_reads: 1_000_000, + total_writes: 500_000, + total_evictions: 5000, + total_upgrades: 12_000, + total_downgrades: 8000, + total_reconstructions: 200, + total_checksum_failures: 0, + total_compactions: 150, + tier_flips_last_minute: 0.023, + avg_score_tier1: 0.85, + avg_score_tier2: 0.45, + avg_score_tier3: 0.12, + }; + + let iters = 10_000; + let start = Instant::now(); + for _ in 0..iters { + std::hint::black_box(m.format_report()); + } + let elapsed = start.elapsed(); + + eprintln!("format_report x {iters}: {:?} ({:.0} ns/call)", + elapsed, elapsed.as_nanos() as f64 / iters as f64); + } + + #[test] + fn bench_format_json() { + use std::time::Instant; + use crate::metrics::StoreMetrics; + + let m = StoreMetrics { + total_blocks: 10_000, + tier0_blocks: 500, + tier1_blocks: 4000, + tier2_blocks: 3500, + tier3_blocks: 2000, + tier1_bytes: 4_000_000, + tier2_bytes: 2_500_000, + tier3_bytes: 750_000, + total_reads: 1_000_000, + total_writes: 500_000, + total_evictions: 5000, + total_upgrades: 12_000, + total_downgrades: 8000, + total_reconstructions: 200, + total_checksum_failures: 0, + total_compactions: 150, + tier_flips_last_minute: 0.023, + avg_score_tier1: 0.85, + avg_score_tier2: 0.45, + avg_score_tier3: 0.12, + }; + + let iters = 10_000; + let start = Instant::now(); + for _ in 0..iters { + std::hint::black_box(m.format_json()); + } + let elapsed = start.elapsed(); + + eprintln!("format_json x {iters}: {:?} ({:.0} ns/call)", + elapsed, elapsed.as_nanos() as f64 / iters as f64); + } + + #[test] + fn bench_metrics_series_trend_100() { + use std::time::Instant; + use crate::metrics::{StoreMetrics, MetricsSeries}; + + let mut series = MetricsSeries::new(256); + for i in 0..100u64 { + series.record(i, StoreMetrics { + total_blocks: 1000 + i, + tier1_blocks: 400 + i % 50, + tier2_blocks: 350, + tier3_blocks: 250, + tier1_bytes: 400_000 + i * 100, + tier2_bytes: 250_000, + tier3_bytes: 75_000, + total_evictions: i * 3, + ..Default::default() + }); + } + + let iters = 10_000; + let start = Instant::now(); + for _ in 0..iters { + std::hint::black_box(series.trend()); + } + let elapsed = start.elapsed(); + + eprintln!("MetricsSeries trend (100 snapshots) x {iters}: {:?} ({:.0} ns/call)", + elapsed, elapsed.as_nanos() as f64 / iters as f64); + } } diff --git a/crates/ruvector-temporal-tensor/tests/persistence_tests.rs b/crates/ruvector-temporal-tensor/tests/persistence_tests.rs new file mode 100644 index 00000000..7b737660 --- /dev/null +++ b/crates/ruvector-temporal-tensor/tests/persistence_tests.rs @@ -0,0 +1,225 @@ +#![cfg(feature = "persistence")] + +use ruvector_temporal_tensor::persistence::{FileBlockIO, FileMetaLog}; +use ruvector_temporal_tensor::store::{ + BlockIO, BlockKey, BlockMeta, DType, MetaLog, ReconstructPolicy, Tier, +}; +use std::path::PathBuf; + +fn test_dir(name: &str) -> PathBuf { + let dir = std::env::temp_dir().join(format!("ruvector_test_{}", name)); + let _ = std::fs::remove_dir_all(&dir); + std::fs::create_dir_all(&dir).unwrap(); + dir +} + +fn cleanup(dir: &PathBuf) { + let _ = std::fs::remove_dir_all(dir); +} + +fn make_key(id: u128, idx: u32) -> BlockKey { + BlockKey { + tensor_id: id, + block_index: idx, + } +} + +fn make_meta(key: BlockKey, tier: Tier) -> BlockMeta { + BlockMeta { + key, + dtype: DType::F32, + tier, + bits: 8, + scale: 0.5, + zero_point: 0, + created_at: 100, + last_access_at: 200, + access_count: 5, + ema_rate: 0.1, + window: 0xFF, + checksum: 0xDEADBEEF, + reconstruct: ReconstructPolicy::None, + tier_age: 10, + lineage_parent: None, + block_bytes: 64, + } +} + +// ----------------------------------------------------------------------- +// FileBlockIO tests +// ----------------------------------------------------------------------- + +#[test] +fn test_file_block_io_write_read() { + let dir = test_dir("block_io_write_read"); + let mut bio = FileBlockIO::new(&dir).unwrap(); + + let key = make_key(1, 0); + let data = vec![0xAB, 0xCD, 0xEF, 0x01, 0x23, 0x45, 0x67, 0x89]; + bio.write_block(Tier::Tier1, key, &data).unwrap(); + + let mut dst = vec![0u8; 32]; + let n = bio.read_block(Tier::Tier1, key, &mut dst).unwrap(); + assert_eq!(n, data.len()); + assert_eq!(&dst[..n], &data[..]); + + cleanup(&dir); +} + +#[test] +fn test_file_block_io_different_tiers() { + let dir = test_dir("block_io_tiers"); + let mut bio = FileBlockIO::new(&dir).unwrap(); + + let key = make_key(1, 0); + let data1 = vec![1u8; 16]; + let data2 = vec![2u8; 8]; + let data3 = vec![3u8; 4]; + + bio.write_block(Tier::Tier1, key, &data1).unwrap(); + bio.write_block(Tier::Tier2, key, &data2).unwrap(); + bio.write_block(Tier::Tier3, key, &data3).unwrap(); + + let mut buf = vec![0u8; 32]; + + let n1 = bio.read_block(Tier::Tier1, key, &mut buf).unwrap(); + assert_eq!(&buf[..n1], &data1[..]); + + let n2 = bio.read_block(Tier::Tier2, key, &mut buf).unwrap(); + assert_eq!(&buf[..n2], &data2[..]); + + let n3 = bio.read_block(Tier::Tier3, key, &mut buf).unwrap(); + assert_eq!(&buf[..n3], &data3[..]); + + cleanup(&dir); +} + +#[test] +fn test_file_block_io_delete() { + let dir = test_dir("block_io_delete"); + let mut bio = FileBlockIO::new(&dir).unwrap(); + + let key = make_key(1, 0); + bio.write_block(Tier::Tier1, key, &[1, 2, 3]).unwrap(); + bio.delete_block(Tier::Tier1, key).unwrap(); + + let mut buf = vec![0u8; 32]; + let result = bio.read_block(Tier::Tier1, key, &mut buf); + assert!(result.is_err() || result.unwrap() == 0); + + cleanup(&dir); +} + +#[test] +fn test_file_block_io_overwrite() { + let dir = test_dir("block_io_overwrite"); + let mut bio = FileBlockIO::new(&dir).unwrap(); + + let key = make_key(1, 0); + bio.write_block(Tier::Tier1, key, &[1, 2, 3]).unwrap(); + bio.write_block(Tier::Tier1, key, &[4, 5, 6, 7]).unwrap(); + + let mut buf = vec![0u8; 32]; + let n = bio.read_block(Tier::Tier1, key, &mut buf).unwrap(); + assert_eq!(&buf[..n], &[4, 5, 6, 7]); + + cleanup(&dir); +} + +#[test] +fn test_file_block_io_missing_key() { + let dir = test_dir("block_io_missing"); + let bio = FileBlockIO::new(&dir).unwrap(); + + let mut buf = vec![0u8; 32]; + let result = bio.read_block(Tier::Tier1, make_key(99, 0), &mut buf); + assert!(result.is_err() || result.unwrap() == 0); + + cleanup(&dir); +} + +// ----------------------------------------------------------------------- +// FileMetaLog tests +// ----------------------------------------------------------------------- + +#[test] +fn test_file_meta_log_append_get() { + let dir = test_dir("meta_log_append"); + let mut log = FileMetaLog::new(&dir).unwrap(); + + let key = make_key(1, 0); + let meta = make_meta(key, Tier::Tier1); + log.append(&meta).unwrap(); + + let retrieved = log.get(key).unwrap(); + assert_eq!(retrieved.key, key); + assert_eq!(retrieved.tier, Tier::Tier1); + assert_eq!(retrieved.bits, 8); + assert!((retrieved.scale - 0.5).abs() < 1e-6); + assert_eq!(retrieved.checksum, 0xDEADBEEF); + + cleanup(&dir); +} + +#[test] +fn test_file_meta_log_upsert() { + let dir = test_dir("meta_log_upsert"); + let mut log = FileMetaLog::new(&dir).unwrap(); + + let key = make_key(1, 0); + let meta1 = make_meta(key, Tier::Tier1); + log.append(&meta1).unwrap(); + + let mut meta2 = make_meta(key, Tier::Tier2); + meta2.bits = 7; + log.append(&meta2).unwrap(); + + let retrieved = log.get(key).unwrap(); + assert_eq!(retrieved.tier, Tier::Tier2); + assert_eq!(retrieved.bits, 7); + + cleanup(&dir); +} + +#[test] +fn test_file_meta_log_iter() { + let dir = test_dir("meta_log_iter"); + let mut log = FileMetaLog::new(&dir).unwrap(); + + for i in 0..5u128 { + let key = make_key(i, 0); + log.append(&make_meta(key, Tier::Tier1)).unwrap(); + } + + let count = log.iter().count(); + assert_eq!(count, 5); + + cleanup(&dir); +} + +#[test] +fn test_file_meta_log_missing_key() { + let dir = test_dir("meta_log_missing"); + let log = FileMetaLog::new(&dir).unwrap(); + assert!(log.get(make_key(99, 0)).is_none()); + + cleanup(&dir); +} + +#[test] +fn test_file_meta_log_multiple_blocks_same_tensor() { + let dir = test_dir("meta_log_multi_block"); + let mut log = FileMetaLog::new(&dir).unwrap(); + + for idx in 0..3u32 { + let key = make_key(1, idx); + log.append(&make_meta(key, Tier::Tier1)).unwrap(); + } + + assert!(log.get(make_key(1, 0)).is_some()); + assert!(log.get(make_key(1, 1)).is_some()); + assert!(log.get(make_key(1, 2)).is_some()); + assert!(log.get(make_key(1, 3)).is_none()); + + cleanup(&dir); +}