feat: Add benchmarks for new features + persistence integration tests

Benchmarks (store.rs, 8 new bench tests):
- Batch scoring 10k blocks vs individual scoring
- 5-bit and 7-bit dequant fast paths (4096 values)
- 5-bit quantize fast path (4096 values)
- SVD adaptive rank selection (64x64 matrix)
- format_report and format_json throughput
- MetricsSeries trend computation (100 snapshots)

Persistence tests (10 tests, feature-gated):
- FileBlockIO: write/read, multi-tier, delete, overwrite, missing key
- FileMetaLog: append/get, upsert, iter, missing key, multi-block

354 tests pass (with --features persistence).

https://claude.ai/code/session_01Ksy165BL5nGpVoWaAfTE7t
This commit is contained in:
Claude 2026-02-08 04:56:40 +00:00
parent 8fa851c917
commit f73f13c08a
2 changed files with 469 additions and 0 deletions

View file

@ -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<TBlockMeta> = (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<f32> = (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<f32> = (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<f32> = (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<f32> = (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);
}
}

View file

@ -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);
}