Skip to main content

optirs_bench/regression_tester/
database.rs

1// Performance database operations for regression testing
2//
3// This module handles persistence and retrieval of performance history data,
4// including database management, record storage, and history maintenance.
5
6use crate::error::Result;
7use crate::regression_tester::types::{DatabaseMetadata, PerformanceRecord};
8use scirs2_core::numeric::Float;
9use serde::{Deserialize, Serialize};
10use std::collections::{HashMap, VecDeque};
11use std::fmt::Debug;
12use std::fs;
13use std::path::Path;
14use std::time::{SystemTime, UNIX_EPOCH};
15
16/// Historical performance database
17#[derive(Debug, Serialize, Deserialize)]
18pub struct PerformanceDatabase<A: Float> {
19    /// Performance history by optimizer and test
20    history: HashMap<String, VecDeque<PerformanceRecord<A>>>,
21    /// Database metadata
22    metadata: DatabaseMetadata,
23}
24
25impl<A: Float + Debug + Serialize + for<'de> Deserialize<'de> + Send + Sync>
26    PerformanceDatabase<A>
27{
28    /// Create a new empty performance database
29    pub fn new() -> Self {
30        Self {
31            history: HashMap::new(),
32            metadata: DatabaseMetadata {
33                version: "1.0".to_string(),
34                created_at: SystemTime::now()
35                    .duration_since(UNIX_EPOCH)
36                    .unwrap_or_default()
37                    .as_secs(),
38                last_updated: SystemTime::now()
39                    .duration_since(UNIX_EPOCH)
40                    .unwrap_or_default()
41                    .as_secs(),
42                total_records: 0,
43            },
44        }
45    }
46
47    /// Load database from disk
48    pub fn load(basedir: &Path) -> Result<Self> {
49        let db_path = basedir.join("performance_db.json");
50        if db_path.exists() {
51            let data = fs::read_to_string(&db_path)?;
52            let db = serde_json::from_str(&data)?;
53            Ok(db)
54        } else {
55            Ok(Self::new())
56        }
57    }
58
59    /// Save database to disk
60    pub fn save(&self, basedir: &Path) -> Result<()> {
61        fs::create_dir_all(basedir)?;
62        let db_path = basedir.join("performance_db.json");
63        let data = serde_json::to_string_pretty(self)?;
64        fs::write(&db_path, data)?;
65        Ok(())
66    }
67
68    /// Add a performance record with history size management
69    pub fn add_record(&mut self, key: String, record: PerformanceRecord<A>) {
70        self.add_record_with_limit(key, record, 1000);
71    }
72
73    /// Add a performance record with custom history limit
74    pub fn add_record_with_limit(
75        &mut self,
76        key: String,
77        record: PerformanceRecord<A>,
78        max_history: usize,
79    ) {
80        let history = self.history.entry(key).or_default();
81        history.push_back(record);
82
83        // Maintain reasonable history size
84        while history.len() > max_history {
85            history.pop_front();
86        }
87
88        self.metadata.total_records += 1;
89        self.metadata.last_updated = SystemTime::now()
90            .duration_since(UNIX_EPOCH)
91            .unwrap_or_default()
92            .as_secs();
93    }
94
95    /// Get performance history for a specific key
96    pub fn get_history(&self, key: &str) -> Option<&VecDeque<PerformanceRecord<A>>> {
97        self.history.get(key)
98    }
99
100    /// Get mutable performance history for a specific key
101    pub fn get_history_mut(&mut self, key: &str) -> Option<&mut VecDeque<PerformanceRecord<A>>> {
102        self.history.get_mut(key)
103    }
104
105    /// Get all keys with performance history
106    pub fn get_keys(&self) -> Vec<&String> {
107        self.history.keys().collect()
108    }
109
110    /// Get recent records for a key (last N records)
111    pub fn get_recent_records(&self, key: &str, count: usize) -> Vec<&PerformanceRecord<A>> {
112        if let Some(history) = self.history.get(key) {
113            history.iter().rev().take(count).collect()
114        } else {
115            Vec::new()
116        }
117    }
118
119    /// Remove old records based on timestamp (older than given timestamp)
120    pub fn cleanup_old_records(&mut self, cutoff_timestamp: u64) -> usize {
121        let mut removed_count = 0;
122
123        for history in self.history.values_mut() {
124            let original_len = history.len();
125            history.retain(|record| record.timestamp >= cutoff_timestamp);
126            removed_count += original_len - history.len();
127        }
128
129        // Remove empty entries
130        self.history.retain(|_, history| !history.is_empty());
131
132        // Update metadata
133        self.metadata.total_records = self.metadata.total_records.saturating_sub(removed_count);
134        self.metadata.last_updated = SystemTime::now()
135            .duration_since(UNIX_EPOCH)
136            .unwrap_or_default()
137            .as_secs();
138
139        removed_count
140    }
141
142    /// Get total number of records across all keys
143    pub fn total_records(&self) -> usize {
144        self.history.values().map(|h| h.len()).sum()
145    }
146
147    /// Get number of unique keys
148    pub fn key_count(&self) -> usize {
149        self.history.len()
150    }
151
152    /// Get database metadata
153    pub fn metadata(&self) -> &DatabaseMetadata {
154        &self.metadata
155    }
156
157    /// Check if database has any data
158    pub fn is_empty(&self) -> bool {
159        self.history.is_empty()
160    }
161
162    /// Get records for a specific commit hash
163    pub fn get_records_by_commit(
164        &self,
165        commit_hash: &str,
166    ) -> Vec<(&String, &PerformanceRecord<A>)> {
167        let mut results = Vec::new();
168        for (key, history) in &self.history {
169            for record in history {
170                if let Some(ref hash) = record.commit_hash {
171                    if hash == commit_hash {
172                        results.push((key, record));
173                    }
174                }
175            }
176        }
177        results
178    }
179
180    /// Get records for a specific branch
181    pub fn get_records_by_branch(&self, branch: &str) -> Vec<(&String, &PerformanceRecord<A>)> {
182        let mut results = Vec::new();
183        for (key, history) in &self.history {
184            for record in history {
185                if let Some(ref record_branch) = record.branch {
186                    if record_branch == branch {
187                        results.push((key, record));
188                    }
189                }
190            }
191        }
192        results
193    }
194
195    /// Get records within a time range
196    pub fn get_records_by_time_range(
197        &self,
198        start_timestamp: u64,
199        end_timestamp: u64,
200    ) -> Vec<(&String, &PerformanceRecord<A>)> {
201        let mut results = Vec::new();
202        for (key, history) in &self.history {
203            for record in history {
204                if record.timestamp >= start_timestamp && record.timestamp <= end_timestamp {
205                    results.push((key, record));
206                }
207            }
208        }
209        results
210    }
211
212    /// Export database to JSON string
213    pub fn export_json(&self) -> Result<String> {
214        Ok(serde_json::to_string_pretty(self)?)
215    }
216
217    /// Import database from JSON string
218    pub fn import_json(json_data: &str) -> Result<Self> {
219        Ok(serde_json::from_str(json_data)?)
220    }
221
222    /// Merge another database into this one
223    pub fn merge(&mut self, other: PerformanceDatabase<A>) {
224        for (key, mut other_history) in other.history {
225            let history = self.history.entry(key).or_default();
226
227            // Merge histories and sort by timestamp
228            history.append(&mut other_history);
229            let mut sorted_records: Vec<_> = history.drain(..).collect();
230            sorted_records.sort_by_key(|record| record.timestamp);
231
232            // Rebuild the deque
233            *history = sorted_records.into();
234        }
235
236        // Update metadata
237        self.metadata.total_records = self.total_records();
238        self.metadata.last_updated = SystemTime::now()
239            .duration_since(UNIX_EPOCH)
240            .unwrap_or_default()
241            .as_secs();
242    }
243
244    /// Compact database by removing duplicate records
245    pub fn compact(&mut self) -> usize {
246        let mut removed_count = 0;
247
248        for history in self.history.values_mut() {
249            let original_len = history.len();
250
251            // Remove duplicates based on timestamp and commit hash
252            let mut seen = std::collections::HashSet::new();
253            history.retain(|record| {
254                let key = (record.timestamp, record.commit_hash.clone());
255                if seen.contains(&key) {
256                    false
257                } else {
258                    seen.insert(key);
259                    true
260                }
261            });
262
263            removed_count += original_len - history.len();
264        }
265
266        // Update metadata
267        self.metadata.total_records = self.total_records();
268        self.metadata.last_updated = SystemTime::now()
269            .duration_since(UNIX_EPOCH)
270            .unwrap_or_default()
271            .as_secs();
272
273        removed_count
274    }
275}
276
277impl<A: Float + Debug + Serialize + for<'de> Deserialize<'de> + Send + Sync> Default
278    for PerformanceDatabase<A>
279{
280    fn default() -> Self {
281        Self::new()
282    }
283}
284
285#[cfg(test)]
286mod tests {
287    use super::*;
288    use crate::regression_tester::config::TestEnvironment;
289    use crate::regression_tester::types::PerformanceMetrics;
290
291    fn create_test_record() -> PerformanceRecord<f64> {
292        PerformanceRecord {
293            timestamp: 1000000,
294            commit_hash: Some("abc123".to_string()),
295            branch: Some("main".to_string()),
296            environment: TestEnvironment::default(),
297            metrics: PerformanceMetrics::default(),
298            metadata: HashMap::new(),
299        }
300    }
301
302    #[test]
303    fn test_database_creation() {
304        let db: PerformanceDatabase<f64> = PerformanceDatabase::new();
305        assert!(db.is_empty());
306        assert_eq!(db.total_records(), 0);
307        assert_eq!(db.key_count(), 0);
308    }
309
310    #[test]
311    fn test_add_record() {
312        let mut db: PerformanceDatabase<f64> = PerformanceDatabase::new();
313        let record = create_test_record();
314
315        db.add_record("test_key".to_string(), record);
316
317        assert!(!db.is_empty());
318        assert_eq!(db.total_records(), 1);
319        assert_eq!(db.key_count(), 1);
320        assert!(db.get_history("test_key").is_some());
321    }
322
323    #[test]
324    fn test_history_limit() {
325        let mut db: PerformanceDatabase<f64> = PerformanceDatabase::new();
326
327        // Add records beyond the limit
328        for i in 0..1005 {
329            let mut record = create_test_record();
330            record.timestamp = i;
331            db.add_record_with_limit("test_key".to_string(), record, 1000);
332        }
333
334        // Should maintain limit
335        assert_eq!(
336            db.get_history("test_key").expect("unwrap failed").len(),
337            1000
338        );
339    }
340
341    #[test]
342    fn test_get_recent_records() {
343        let mut db: PerformanceDatabase<f64> = PerformanceDatabase::new();
344
345        // Add 5 records
346        for i in 0..5 {
347            let mut record = create_test_record();
348            record.timestamp = i;
349            db.add_record("test_key".to_string(), record);
350        }
351
352        let recent = db.get_recent_records("test_key", 3);
353        assert_eq!(recent.len(), 3);
354        // Should be in reverse order (most recent first)
355        assert_eq!(recent[0].timestamp, 4);
356        assert_eq!(recent[1].timestamp, 3);
357        assert_eq!(recent[2].timestamp, 2);
358    }
359
360    #[test]
361    fn test_cleanup_old_records() {
362        let mut db: PerformanceDatabase<f64> = PerformanceDatabase::new();
363
364        // Add records with different timestamps
365        for i in 0..10 {
366            let mut record = create_test_record();
367            record.timestamp = i * 1000;
368            db.add_record("test_key".to_string(), record);
369        }
370
371        // Remove records older than 5000
372        let removed = db.cleanup_old_records(5000);
373        assert_eq!(removed, 5); // Records 0-4000 should be removed
374        assert_eq!(db.total_records(), 5); // Records 5000-9000 should remain
375    }
376
377    #[test]
378    fn test_get_records_by_commit() {
379        let mut db: PerformanceDatabase<f64> = PerformanceDatabase::new();
380
381        let mut record1 = create_test_record();
382        record1.commit_hash = Some("commit1".to_string());
383        db.add_record("key1".to_string(), record1);
384
385        let mut record2 = create_test_record();
386        record2.commit_hash = Some("commit2".to_string());
387        db.add_record("key2".to_string(), record2);
388
389        let results = db.get_records_by_commit("commit1");
390        assert_eq!(results.len(), 1);
391        assert_eq!(results[0].0, "key1");
392    }
393
394    #[test]
395    fn test_compact_database() {
396        let mut db: PerformanceDatabase<f64> = PerformanceDatabase::new();
397
398        // Add duplicate records (same timestamp and commit)
399        for _ in 0..3 {
400            let record = create_test_record();
401            db.add_record("test_key".to_string(), record);
402        }
403
404        assert_eq!(db.total_records(), 3);
405
406        let removed = db.compact();
407        assert_eq!(removed, 2); // 2 duplicates removed
408        assert_eq!(db.total_records(), 1); // 1 unique record remains
409    }
410}