|
| 1 | +use std::sync::Arc; |
| 2 | +use std::sync::atomic::{AtomicU64, Ordering}; |
| 3 | +use std::time::Instant; |
| 4 | + |
| 5 | +use tokio::sync::Mutex; |
| 6 | +use tonic::transport::Channel; |
| 7 | + |
| 8 | +pub mod proto { |
| 9 | + tonic::include_proto!("mnemos"); |
| 10 | +} |
| 11 | + |
| 12 | +use proto::mnemos_service_client::MnemosServiceClient; |
| 13 | + |
| 14 | +#[derive(Debug, Clone)] |
| 15 | +struct BenchConfig { |
| 16 | + addr: String, |
| 17 | + namespace: String, |
| 18 | + vector_dim: usize, |
| 19 | + insert_ops: u64, |
| 20 | + query_ops: u64, |
| 21 | + concurrency: usize, |
| 22 | + top_k: u32, |
| 23 | +} |
| 24 | + |
| 25 | +#[derive(Debug, Clone)] |
| 26 | +struct PhaseSummary { |
| 27 | + name: &'static str, |
| 28 | + ops: u64, |
| 29 | + concurrency: usize, |
| 30 | + elapsed_ms: u128, |
| 31 | + throughput_ops_s: f64, |
| 32 | + avg_ms: f64, |
| 33 | + p50_ms: f64, |
| 34 | + p95_ms: f64, |
| 35 | + p99_ms: f64, |
| 36 | + max_ms: f64, |
| 37 | +} |
| 38 | + |
| 39 | +#[tokio::main] |
| 40 | +async fn main() -> Result<(), Box<dyn std::error::Error>> { |
| 41 | + let cfg = parse_args(); |
| 42 | + let endpoint = format!("http://{}", cfg.addr); |
| 43 | + let channel = Channel::from_shared(endpoint)?.connect().await?; |
| 44 | + |
| 45 | + println!("Running Mnemos gRPC benchmark with config: {:?}", cfg); |
| 46 | + |
| 47 | + let insert = run_insert_phase(&cfg, channel.clone()).await?; |
| 48 | + let query = run_query_phase(&cfg, channel).await?; |
| 49 | + |
| 50 | + print_markdown_summary(&cfg, &insert, &query); |
| 51 | + Ok(()) |
| 52 | +} |
| 53 | + |
| 54 | +async fn run_insert_phase( |
| 55 | + cfg: &BenchConfig, |
| 56 | + channel: Channel, |
| 57 | +) -> Result<PhaseSummary, Box<dyn std::error::Error>> { |
| 58 | + let counter = Arc::new(AtomicU64::new(0)); |
| 59 | + let latencies = Arc::new(Mutex::new(Vec::<u128>::with_capacity( |
| 60 | + cfg.insert_ops as usize, |
| 61 | + ))); |
| 62 | + let started = Instant::now(); |
| 63 | + |
| 64 | + let mut handles = Vec::with_capacity(cfg.concurrency); |
| 65 | + for worker in 0..cfg.concurrency { |
| 66 | + let counter = Arc::clone(&counter); |
| 67 | + let latencies = Arc::clone(&latencies); |
| 68 | + let namespace = cfg.namespace.clone(); |
| 69 | + let dim = cfg.vector_dim; |
| 70 | + let ops = cfg.insert_ops; |
| 71 | + let mut client = MnemosServiceClient::new(channel.clone()); |
| 72 | + |
| 73 | + handles.push(tokio::spawn(async move { |
| 74 | + loop { |
| 75 | + let i = counter.fetch_add(1, Ordering::Relaxed); |
| 76 | + if i >= ops { |
| 77 | + break; |
| 78 | + } |
| 79 | + let memory_id = 10_000_000 + i; |
| 80 | + let embedding = make_embedding(memory_id, dim); |
| 81 | + let req = proto::InsertMemoryRequest { |
| 82 | + memory: Some(proto::Memory { |
| 83 | + id: memory_id, |
| 84 | + namespace: namespace.clone(), |
| 85 | + content: format!("bench-memory-{}-w{}", i, worker).into_bytes(), |
| 86 | + embedding, |
| 87 | + created_at: 1_700_000_000 + i, |
| 88 | + importance: 0.5, |
| 89 | + metadata: vec![], |
| 90 | + }), |
| 91 | + }; |
| 92 | + |
| 93 | + let t0 = Instant::now(); |
| 94 | + client.insert_memory(req).await?; |
| 95 | + let elapsed = t0.elapsed().as_micros(); |
| 96 | + latencies.lock().await.push(elapsed); |
| 97 | + } |
| 98 | + Ok::<(), tonic::Status>(()) |
| 99 | + })); |
| 100 | + } |
| 101 | + |
| 102 | + for h in handles { |
| 103 | + h.await??; |
| 104 | + } |
| 105 | + let elapsed = started.elapsed(); |
| 106 | + let lats = latencies.lock().await.clone(); |
| 107 | + Ok(summarize_phase("insert", cfg.insert_ops, cfg.concurrency, elapsed, lats)) |
| 108 | +} |
| 109 | + |
| 110 | +async fn run_query_phase( |
| 111 | + cfg: &BenchConfig, |
| 112 | + channel: Channel, |
| 113 | +) -> Result<PhaseSummary, Box<dyn std::error::Error>> { |
| 114 | + let counter = Arc::new(AtomicU64::new(0)); |
| 115 | + let latencies = Arc::new(Mutex::new(Vec::<u128>::with_capacity( |
| 116 | + cfg.query_ops as usize, |
| 117 | + ))); |
| 118 | + let started = Instant::now(); |
| 119 | + |
| 120 | + let mut handles = Vec::with_capacity(cfg.concurrency); |
| 121 | + for _ in 0..cfg.concurrency { |
| 122 | + let counter = Arc::clone(&counter); |
| 123 | + let latencies = Arc::clone(&latencies); |
| 124 | + let namespace = cfg.namespace.clone(); |
| 125 | + let dim = cfg.vector_dim; |
| 126 | + let ops = cfg.query_ops; |
| 127 | + let top_k = cfg.top_k; |
| 128 | + let mut client = MnemosServiceClient::new(channel.clone()); |
| 129 | + |
| 130 | + handles.push(tokio::spawn(async move { |
| 131 | + loop { |
| 132 | + let i = counter.fetch_add(1, Ordering::Relaxed); |
| 133 | + if i >= ops { |
| 134 | + break; |
| 135 | + } |
| 136 | + let query_vec = make_embedding(10_000_000 + (i % 1024), dim); |
| 137 | + let req = proto::QueryRequest { |
| 138 | + query_embedding: query_vec, |
| 139 | + top_k, |
| 140 | + namespace: Some(namespace.clone()), |
| 141 | + time_start: None, |
| 142 | + time_end: None, |
| 143 | + graph_hops: None, |
| 144 | + candidate_multiplier: 0, |
| 145 | + similarity_pct: 0, |
| 146 | + importance_pct: 0, |
| 147 | + recency_pct: 0, |
| 148 | + }; |
| 149 | + let t0 = Instant::now(); |
| 150 | + client.query(req).await?; |
| 151 | + let elapsed = t0.elapsed().as_micros(); |
| 152 | + latencies.lock().await.push(elapsed); |
| 153 | + } |
| 154 | + Ok::<(), tonic::Status>(()) |
| 155 | + })); |
| 156 | + } |
| 157 | + |
| 158 | + for h in handles { |
| 159 | + h.await??; |
| 160 | + } |
| 161 | + let elapsed = started.elapsed(); |
| 162 | + let lats = latencies.lock().await.clone(); |
| 163 | + Ok(summarize_phase("query", cfg.query_ops, cfg.concurrency, elapsed, lats)) |
| 164 | +} |
| 165 | + |
| 166 | +fn summarize_phase( |
| 167 | + name: &'static str, |
| 168 | + ops: u64, |
| 169 | + concurrency: usize, |
| 170 | + elapsed: std::time::Duration, |
| 171 | + mut lats_us: Vec<u128>, |
| 172 | +) -> PhaseSummary { |
| 173 | + lats_us.sort_unstable(); |
| 174 | + let elapsed_ms = elapsed.as_millis(); |
| 175 | + let throughput_ops_s = if elapsed.as_secs_f64() > 0.0 { |
| 176 | + ops as f64 / elapsed.as_secs_f64() |
| 177 | + } else { |
| 178 | + 0.0 |
| 179 | + }; |
| 180 | + |
| 181 | + let avg_us = if lats_us.is_empty() { |
| 182 | + 0.0 |
| 183 | + } else { |
| 184 | + lats_us.iter().sum::<u128>() as f64 / lats_us.len() as f64 |
| 185 | + }; |
| 186 | + let p50 = percentile_us(&lats_us, 50.0); |
| 187 | + let p95 = percentile_us(&lats_us, 95.0); |
| 188 | + let p99 = percentile_us(&lats_us, 99.0); |
| 189 | + let max = lats_us.last().copied().unwrap_or(0) as f64; |
| 190 | + |
| 191 | + PhaseSummary { |
| 192 | + name, |
| 193 | + ops, |
| 194 | + concurrency, |
| 195 | + elapsed_ms, |
| 196 | + throughput_ops_s, |
| 197 | + avg_ms: avg_us / 1000.0, |
| 198 | + p50_ms: p50 / 1000.0, |
| 199 | + p95_ms: p95 / 1000.0, |
| 200 | + p99_ms: p99 / 1000.0, |
| 201 | + max_ms: max / 1000.0, |
| 202 | + } |
| 203 | +} |
| 204 | + |
| 205 | +fn percentile_us(sorted: &[u128], p: f64) -> f64 { |
| 206 | + if sorted.is_empty() { |
| 207 | + return 0.0; |
| 208 | + } |
| 209 | + let rank = (p / 100.0) * (sorted.len().saturating_sub(1) as f64); |
| 210 | + let lo = rank.floor() as usize; |
| 211 | + let hi = rank.ceil() as usize; |
| 212 | + if lo == hi { |
| 213 | + return sorted[lo] as f64; |
| 214 | + } |
| 215 | + let w = rank - lo as f64; |
| 216 | + (1.0 - w) * (sorted[lo] as f64) + w * (sorted[hi] as f64) |
| 217 | +} |
| 218 | + |
| 219 | +fn make_embedding(seed: u64, dim: usize) -> Vec<f32> { |
| 220 | + (0..dim) |
| 221 | + .map(|i| { |
| 222 | + let x = seed.wrapping_mul(6364136223846793005).wrapping_add(i as u64); |
| 223 | + ((x % 10_000) as f32) / 10_000.0 |
| 224 | + }) |
| 225 | + .collect() |
| 226 | +} |
| 227 | + |
| 228 | +fn print_markdown_summary(cfg: &BenchConfig, insert: &PhaseSummary, query: &PhaseSummary) { |
| 229 | + println!(); |
| 230 | + println!("Benchmark summary"); |
| 231 | + println!( |
| 232 | + "Workload: namespace={}, dim={}, insert_ops={}, query_ops={}, concurrency={}, top_k={}", |
| 233 | + cfg.namespace, cfg.vector_dim, cfg.insert_ops, cfg.query_ops, cfg.concurrency, cfg.top_k |
| 234 | + ); |
| 235 | + println!(); |
| 236 | + println!("| Phase | Ops | Conc | Total ms | Ops/s | Avg ms | P50 ms | P95 ms | P99 ms | Max ms |"); |
| 237 | + println!("|---|---:|---:|---:|---:|---:|---:|---:|---:|---:|"); |
| 238 | + for p in [insert, query] { |
| 239 | + println!( |
| 240 | + "| {} | {} | {} | {} | {:.2} | {:.3} | {:.3} | {:.3} | {:.3} | {:.3} |", |
| 241 | + p.name, |
| 242 | + p.ops, |
| 243 | + p.concurrency, |
| 244 | + p.elapsed_ms, |
| 245 | + p.throughput_ops_s, |
| 246 | + p.avg_ms, |
| 247 | + p.p50_ms, |
| 248 | + p.p95_ms, |
| 249 | + p.p99_ms, |
| 250 | + p.max_ms |
| 251 | + ); |
| 252 | + } |
| 253 | +} |
| 254 | + |
| 255 | +fn parse_args() -> BenchConfig { |
| 256 | + BenchConfig { |
| 257 | + addr: arg("--addr").unwrap_or_else(|| "127.0.0.1:50051".to_string()), |
| 258 | + namespace: arg("--namespace").unwrap_or_else(|| "bench".to_string()), |
| 259 | + vector_dim: arg("--vector-dim") |
| 260 | + .and_then(|v| v.parse().ok()) |
| 261 | + .unwrap_or(3), |
| 262 | + insert_ops: arg("--insert-ops") |
| 263 | + .and_then(|v| v.parse().ok()) |
| 264 | + .unwrap_or(5000), |
| 265 | + query_ops: arg("--query-ops") |
| 266 | + .and_then(|v| v.parse().ok()) |
| 267 | + .unwrap_or(5000), |
| 268 | + concurrency: arg("--concurrency") |
| 269 | + .and_then(|v| v.parse().ok()) |
| 270 | + .unwrap_or(32), |
| 271 | + top_k: arg("--top-k").and_then(|v| v.parse().ok()).unwrap_or(10), |
| 272 | + } |
| 273 | +} |
| 274 | + |
| 275 | +fn arg(flag: &str) -> Option<String> { |
| 276 | + let args = std::env::args().collect::<Vec<_>>(); |
| 277 | + args.windows(2).find_map(|w| { |
| 278 | + if w[0] == flag { |
| 279 | + Some(w[1].clone()) |
| 280 | + } else { |
| 281 | + None |
| 282 | + } |
| 283 | + }) |
| 284 | +} |
0 commit comments