Performance Tuning
This guide covers optimization strategies for OpenDB deployments.
Profiling
Before optimizing, measure your bottleneck:
#![allow(unused)] fn main() { use std::time::Instant; let start = Instant::now(); db.insert_memory(&memory)?; println!("Insert took: {:?}", start.elapsed()); }
RocksDB Tuning
Write Buffer Size
Larger write buffers improve write throughput:
#![allow(unused)] fn main() { // Default: 128 MB // For write-heavy workloads, increase: opts.set_write_buffer_size(256 * 1024 * 1024); // 256 MB }
Trade-offs:
- ✅ Fewer flushes to disk
- ✅ Better write throughput
- ❌ More memory usage
- ❌ Longer recovery time after crash
Block Cache
RocksDB's internal cache for disk blocks:
#![allow(unused)] fn main() { opts.set_block_cache_size(512 * 1024 * 1024); // 512 MB }
Trade-offs:
- ✅ Faster reads
- ❌ More memory usage
Compression
Balance CPU vs storage:
#![allow(unused)] fn main() { use rocksdb::DBCompressionType; // Default: LZ4 (fast, moderate compression) opts.set_compression_type(DBCompressionType::Lz4); // For better compression (slower writes): opts.set_compression_type(DBCompressionType::Zstd); // For faster writes (larger storage): opts.set_compression_type(DBCompressionType::None); }
Parallelism
Increase background threads for compaction:
#![allow(unused)] fn main() { opts.increase_parallelism(4); // Use 4 threads }
Cache Tuning
Cache Sizes
Adjust cache capacity based on workload:
#![allow(unused)] fn main() { use opendb::OpenDBOptions; let mut options = OpenDBOptions::default(); // For read-heavy workloads options.kv_cache_size = 10_000; options.record_cache_size = 5_000; // For write-heavy workloads (smaller cache) options.kv_cache_size = 1_000; options.record_cache_size = 500; let db = OpenDB::open_with_options("./db", options)?; }
Cache Hit Rate
Monitor cache effectiveness:
#![allow(unused)] fn main() { // Implement hit rate tracking (example) struct CacheStats { hits: AtomicU64, misses: AtomicU64, } impl CacheStats { fn hit_rate(&self) -> f64 { let hits = self.hits.load(Ordering::Relaxed) as f64; let misses = self.misses.load(Ordering::Relaxed) as f64; hits / (hits + misses) } } }
Target hit rates:
- > 90%: Excellent
- 70-90%: Good
- < 70%: Increase cache size
Batch Operations
Batch Inserts
Use transactions for bulk inserts:
#![allow(unused)] fn main() { // ❌ Slow: Individual commits for memory in memories { db.insert_memory(&memory)?; } // ✅ Fast: Batch commit (future API) let mut txn = db.begin_transaction()?; for memory in memories { // Insert via transaction } txn.commit()?; }
Flush Control
Control when data is flushed to disk:
#![allow(unused)] fn main() { // Insert many records for i in 0..10_000 { db.insert_memory(&memory)?; } // Explicit flush db.flush()?; }
Vector Search Optimization
Index Parameters
Tune HNSW parameters for your use case:
#![allow(unused)] fn main() { // High accuracy (slower, better recall) HnswParams::high_accuracy() // ef=400, neighbors=32 // High speed (faster, lower recall) HnswParams::high_speed() // ef=100, neighbors=8 }
Rebuild Strategy
Rebuild index strategically:
#![allow(unused)] fn main() { // ❌ Bad: Rebuild after every insert for memory in memories { db.insert_memory(&memory)?; db.rebuild_vector_index()?; // Expensive! } // ✅ Good: Rebuild once after batch for memory in memories { db.insert_memory(&memory)?; } db.rebuild_vector_index()?; // Once }
Dimension Reduction
Lower dimensions = faster search:
#![allow(unused)] fn main() { // 768D (high quality, slower) options.vector_dimension = 768; // 384D (balanced) options.vector_dimension = 384; // 128D (fast, lower quality) options.vector_dimension = 128; }
Graph Optimization
Link Batching
Batch graph operations:
#![allow(unused)] fn main() { // Create all memories first for memory in memories { db.insert_memory(&memory)?; } // Then create all links for (from, to, relation) in edges { db.link(from, to, relation)?; } }
Prune Unused Relations
Remove stale edges periodically:
#![allow(unused)] fn main() { fn prune_orphaned_edges(db: &OpenDB) -> Result<()> { let all_ids: HashSet<_> = db.list_memory_ids()?.into_iter().collect(); for id in db.list_memory_ids()? { let outgoing = db.get_outgoing(&id)?; for edge in outgoing { if !all_ids.contains(&edge.to) { db.unlink(&edge.from, &edge.to, &edge.relation)?; } } } Ok(()) } }
Memory Usage
Estimate Memory Footprint
Total Memory =
RocksDB Write Buffers +
RocksDB Block Cache +
Application Caches +
HNSW Index +
Overhead
Example:
128 MB (write buffers) +
256 MB (block cache) +
10 MB (app caches, 10k entries × 1KB avg) +
30 MB (HNSW, 10k vectors × 384D × 4 bytes × 2x overhead) +
50 MB (overhead)
= ~474 MB
Reduce Memory Usage
- Smaller caches:
#![allow(unused)] fn main() { options.kv_cache_size = 100; options.record_cache_size = 100; }
- Lower RocksDB buffers:
#![allow(unused)] fn main() { opts.set_write_buffer_size(64 * 1024 * 1024); // 64 MB opts.set_block_cache_size(128 * 1024 * 1024); // 128 MB }
- Smaller embeddings:
#![allow(unused)] fn main() { options.vector_dimension = 128; // Instead of 768 }
Disk Usage
Compaction
Force compaction to reclaim space:
#![allow(unused)] fn main() { // Manual compaction (future API) db.compact_range(None, None)?; }
Monitoring
Check database size:
#![allow(unused)] fn main() { // On Linux std::process::Command::new("du") .args(&["-sh", "./db"]) .output()?; }
Benchmarking
Use Criterion for accurate benchmarks:
#![allow(unused)] fn main() { use criterion::{black_box, criterion_group, criterion_main, Criterion}; fn benchmark_insert(c: &mut Criterion) { let db = OpenDB::open("./bench_db").unwrap(); c.bench_function("insert_memory", |b| { b.iter(|| { let memory = Memory::new("id".to_string(), "content".to_string()); db.insert_memory(black_box(&memory)).unwrap(); }); }); } criterion_group!(benches, benchmark_insert); criterion_main!(benches); }
Monitoring Metrics
Implement metrics collection:
#![allow(unused)] fn main() { struct Metrics { reads: AtomicU64, writes: AtomicU64, cache_hits: AtomicU64, cache_misses: AtomicU64, } impl Metrics { fn report(&self) { println!("Reads: {}", self.reads.load(Ordering::Relaxed)); println!("Writes: {}", self.writes.load(Ordering::Relaxed)); println!("Cache hit rate: {:.2}%", self.cache_hits.load(Ordering::Relaxed) as f64 / (self.cache_hits.load(Ordering::Relaxed) + self.cache_misses.load(Ordering::Relaxed)) as f64 * 100.0 ); } } }
Platform-Specific Tips
Linux
- Use
io_uringfor async I/O (future RocksDB feature) - Disable transparent huge pages for lower latency
- Use
fallocatefor preallocating disk space
macOS
- APFS filesystem has good performance
- Use
F_NOCACHEfor large scans (avoid cache pollution)
Windows
- Use NTFS for best RocksDB performance
- Disable indexing on database directory
- Use SSD for best performance
Common Bottlenecks
- Slow writes: Increase write buffer size, disable compression
- Slow reads: Increase cache sizes, use SSD
- High memory: Reduce cache sizes, lower embedding dimension
- Slow vector search: Reduce HNSW parameters, lower dimension
- Large database size: Enable compression, run compaction