anya_core/testing/performance/
database.rs1use crate::testing::performance::{
3 MetricType, PerformanceTestable, Result, TestConfig, TestResult, Timer,
4};
5use rand::{thread_rng, Rng};
6use std::collections::HashMap;
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub enum DbOperation {
11 Read,
13
14 Write,
16
17 Update,
19
20 Delete,
22}
23
24#[derive(Debug, Clone)]
26pub struct DbConfig {
27 pub db_type: String,
29
30 pub connection_string: String,
32
33 pub batch_size: usize,
35
36 pub use_prepared_statements: bool,
38
39 pub cache_size_mb: usize,
41}
42
43impl Default for DbConfig {
44 fn default() -> Self {
45 Self {
46 db_type: "sqlite".to_string(),
47 connection_string: ":memory:".to_string(),
48 batch_size: 100,
49 use_prepared_statements: true,
50 cache_size_mb: 10,
51 }
52 }
53}
54
55#[derive(Debug)]
57pub struct MockDatabase {
58 config: DbConfig,
60
61 data: HashMap<String, Vec<u8>>,
63
64 cache: HashMap<String, Vec<u8>>,
66
67 stats: DbStats,
69}
70
71#[derive(Debug, Default, Clone)]
73pub struct DbStats {
74 pub reads: usize,
76
77 pub writes: usize,
79
80 pub updates: usize,
82
83 pub deletes: usize,
85
86 pub cache_hits: usize,
88
89 pub cache_misses: usize,
91
92 pub read_time_ms: u64,
94
95 pub write_time_ms: u64,
97
98 pub update_time_ms: u64,
100
101 pub delete_time_ms: u64,
103}
104
105impl MockDatabase {
106 pub fn new(config: DbConfig) -> Self {
108 Self {
109 config,
110 data: HashMap::new(),
111 cache: HashMap::new(),
112 stats: DbStats::default(),
113 }
114 }
115
116 pub fn read(&mut self, key: &str) -> Option<Vec<u8>> {
118 let mut timer = Timer::new();
119 timer.start();
120
121 if let Some(value) = self.cache.get(key) {
123 self.stats.cache_hits += 1;
124 timer.stop();
125 if let Ok(elapsed) = timer.elapsed_ms() {
126 self.stats.read_time_ms += elapsed;
127 }
128 self.stats.reads += 1;
129 return Some(value.clone());
130 }
131
132 self.stats.cache_misses += 1;
133
134 let result = self.data.get(key).cloned();
136
137 if let Some(value) = &result {
139 self.cache.insert(key.to_string(), value.clone());
140
141 if self.cache.len() > self.config.cache_size_mb * 1024 * 1024 / 100 {
143 if let Some(key) = self.cache.keys().next().cloned() {
145 self.cache.remove(&key);
146 }
147 }
148 }
149
150 timer.stop();
151 if let Ok(elapsed) = timer.elapsed_ms() {
152 self.stats.read_time_ms += elapsed;
153 }
154 self.stats.reads += 1;
155
156 result
157 }
158
159 pub fn write(&mut self, key: &str, value: Vec<u8>) {
161 let mut timer = Timer::new();
162 timer.start();
163
164 self.data.insert(key.to_string(), value.clone());
165 self.cache.insert(key.to_string(), value);
166
167 timer.stop();
168 if let Ok(elapsed) = timer.elapsed_ms() {
169 self.stats.write_time_ms += elapsed;
170 }
171 self.stats.writes += 1;
172 }
173
174 pub fn update(&mut self, key: &str, value: Vec<u8>) -> bool {
176 let mut timer = Timer::new();
177 timer.start();
178
179 let result = self.data.insert(key.to_string(), value.clone()).is_some();
180 self.cache.insert(key.to_string(), value);
181
182 timer.stop();
183 if let Ok(elapsed) = timer.elapsed_ms() {
184 self.stats.update_time_ms += elapsed;
185 }
186 self.stats.updates += 1;
187
188 result
189 }
190
191 pub fn delete(&mut self, key: &str) -> bool {
193 let mut timer = Timer::new();
194 timer.start();
195
196 let result = self.data.remove(key).is_some();
197 self.cache.remove(key);
198
199 timer.stop();
200 if let Ok(elapsed) = timer.elapsed_ms() {
201 self.stats.delete_time_ms += elapsed;
202 }
203 self.stats.deletes += 1;
204
205 result
206 }
207
208 pub fn get_stats(&self) -> DbStats {
210 self.stats.clone()
211 }
212
213 pub fn reset_stats(&mut self) {
215 self.stats = DbStats::default();
216 }
217}
218
219pub struct DatabaseAccessTest {
221 db: MockDatabase,
223
224 operations: Vec<DbOperation>,
226
227 key_space_size: usize,
229
230 value_size: usize,
232}
233
234impl DatabaseAccessTest {
235 pub fn new(
237 config: DbConfig,
238 operations: Vec<DbOperation>,
239 key_space_size: usize,
240 value_size: usize,
241 ) -> Self {
242 Self {
243 db: MockDatabase::new(config),
244 operations,
245 key_space_size,
246 value_size,
247 }
248 }
249
250 fn random_key(&self) -> String {
252 let mut rng = thread_rng();
253 format!("key_{}", rng.gen_range(0..self.key_space_size))
254 }
255
256 fn random_data(&self) -> Vec<u8> {
258 let mut rng = thread_rng();
259 let mut data = Vec::with_capacity(self.value_size);
260 for _ in 0..self.value_size {
261 data.push(rng.gen());
262 }
263 data
264 }
265
266 fn run_operation_test(&mut self, operation: DbOperation, iterations: usize) -> Result<f64> {
268 self.db.reset_stats();
269
270 let mut timer = Timer::new();
271 timer.start();
272
273 for _ in 0..iterations {
274 match operation {
275 DbOperation::Read => {
276 let key = self.random_key();
277 let _ = self.db.read(&key);
278 }
279 DbOperation::Write => {
280 let key = self.random_key();
281 let data = self.random_data();
282 self.db.write(&key, data);
283 }
284 DbOperation::Update => {
285 let key = self.random_key();
286 let data = self.random_data();
287 self.db.update(&key, data);
288 }
289 DbOperation::Delete => {
290 let key = self.random_key();
291 let _ = self.db.delete(&key);
292 }
293 }
294 }
295
296 timer.stop();
297
298 let ops_per_second = (iterations as f64) / (timer.elapsed_secs()?);
299
300 Ok(ops_per_second)
301 }
302}
303
304impl PerformanceTestable for DatabaseAccessTest {
305 fn run_test(&self, config: &TestConfig) -> Result<TestResult> {
306 let iterations = config.iterations;
307 let warmup_iterations = config.warmup_iterations;
308
309 let mut test = DatabaseAccessTest {
311 db: MockDatabase::new(self.db.config.clone()),
312 operations: self.operations.clone(),
313 key_space_size: self.key_space_size,
314 value_size: self.value_size,
315 };
316
317 let mut parameters = HashMap::new();
319 parameters.insert("db_type".to_string(), test.db.config.db_type.clone());
320 parameters.insert(
321 "batch_size".to_string(),
322 test.db.config.batch_size.to_string(),
323 );
324 parameters.insert(
325 "use_prepared_statements".to_string(),
326 test.db.config.use_prepared_statements.to_string(),
327 );
328 parameters.insert(
329 "cache_size_mb".to_string(),
330 test.db.config.cache_size_mb.to_string(),
331 );
332 parameters.insert(
333 "key_space_size".to_string(),
334 test.key_space_size.to_string(),
335 );
336 parameters.insert("value_size".to_string(), test.value_size.to_string());
337
338 println!("Warming up database for {warmup_iterations} iterations...");
340 for _ in 0..warmup_iterations / 10 {
342 let key = test.random_key();
343 let data = test.random_data();
344 test.db.write(&key, data);
345 }
346
347 test.db.reset_stats();
349
350 println!("Running database test for {iterations} iterations...");
352
353 let mut timer = Timer::new();
354 timer.start();
355
356 let mut metrics = HashMap::new();
357 let mut metric_types = HashMap::new();
358
359 let operations = test.operations.clone();
361
362 for operation in &operations {
363 println!("Testing {operation:?} operations...");
364 let ops_per_second =
365 test.run_operation_test(*operation, iterations / operations.len())?;
366
367 let metric_name = match operation {
368 DbOperation::Read => "read_ops_per_second",
369 DbOperation::Write => "write_ops_per_second",
370 DbOperation::Update => "update_ops_per_second",
371 DbOperation::Delete => "delete_ops_per_second",
372 };
373
374 metrics.insert(metric_name.to_string(), ops_per_second);
375 metric_types.insert(metric_name.to_string(), MetricType::DbOpsPerSecond);
376 }
377
378 let stats = test.db.get_stats();
380
381 let total_reads = stats.cache_hits + stats.cache_misses;
383 let cache_hit_rate = if total_reads > 0 {
384 (stats.cache_hits as f64) / (total_reads as f64) * 100.0
385 } else {
386 0.0
387 };
388
389 metrics.insert("cache_hit_rate".to_string(), cache_hit_rate);
390 metric_types.insert("cache_hit_rate".to_string(), MetricType::CacheHitRate);
391
392 if stats.reads > 0 {
394 metrics.insert(
395 "avg_read_time_ms".to_string(),
396 (stats.read_time_ms as f64) / (stats.reads as f64),
397 );
398 metric_types.insert("avg_read_time_ms".to_string(), MetricType::LatencyMs);
399 }
400
401 if stats.writes > 0 {
402 metrics.insert(
403 "avg_write_time_ms".to_string(),
404 (stats.write_time_ms as f64) / (stats.writes as f64),
405 );
406 metric_types.insert("avg_write_time_ms".to_string(), MetricType::LatencyMs);
407 }
408
409 if stats.updates > 0 {
410 metrics.insert(
411 "avg_update_time_ms".to_string(),
412 (stats.update_time_ms as f64) / (stats.updates as f64),
413 );
414 metric_types.insert("avg_update_time_ms".to_string(), MetricType::LatencyMs);
415 }
416
417 if stats.deletes > 0 {
418 metrics.insert(
419 "avg_delete_time_ms".to_string(),
420 (stats.delete_time_ms as f64) / (stats.deletes as f64),
421 );
422 metric_types.insert("avg_delete_time_ms".to_string(), MetricType::LatencyMs);
423 }
424
425 timer.stop();
426
427 Ok(TestResult {
428 name: self.name().to_string(),
429 timestamp: chrono::Utc::now().to_rfc3339(),
430 duration_ms: timer.elapsed_ms()?,
431 metrics,
432 metric_types,
433 parameters,
434 })
435 }
436
437 fn name(&self) -> &str {
438 "database_access"
439 }
440}