use std::fs;
use std::path::{Path, PathBuf};
use std::time::Instant;
const BASELINE_PATH: &str = ".planning/phases/33-verification/PERFORMANCE_BASELINE.json";
const MAX_REGRESSION: f64 = 0.05;
const _MIN_POOL_SPEEDUP: f64 = 1.10;
#[derive(Debug, serde::Serialize, serde::Deserialize)]
struct PerformanceBaseline {
test_name: String,
file_count: usize,
duration_ms: u128,
timestamp: String,
}
fn _create_rust_files(dir: &Path, count: usize) -> Vec<PathBuf> {
let mut files = Vec::new();
for i in 0..count {
let file_path = dir.join(format!("file_{}.rs", i));
let content = format!(
r#"//! Module {i}
/// Function {i} documentation
pub fn function_{i}() -> usize {{
{i}
}}
/// Struct {i} documentation
#[derive(Debug, Clone)]
pub struct Struct{i} {{
/// Field documentation
pub field: usize,
/// Optional field
pub optional: Option<String>,
}}
impl Struct{i} {{
/// Create a new instance
pub fn new() -> Self {{
Self {{
field: {i},
optional: None,
}}
}}
/// Calculate value
pub fn calculate(&self, x: usize) -> usize {{
self.field + x
}}
}}
/// Nested function {i}
pub fn nested_function_{i}(x: usize) -> usize {{
x + {i}
}}
/// Trait definition
pub trait Process{i} {{
fn process(&self) -> usize;
}}
impl Process{i} for Struct{i} {{
fn process(&self) -> usize {{
self.field
}}
}}
/// Constant
pub const CONSTANT_{i}: usize = {i};
/// Type alias
pub type Result{i} = std::result::Result<usize, String>;
/// Async function
pub async fn async_function_{i}() -> usize {{
{i}
}}
"#
);
fs::write(&file_path, content).unwrap();
files.push(file_path);
}
files
}
pub fn measure_duration<F, R>(name: &str, f: F) -> (R, u128)
where
F: FnOnce() -> R,
{
let start = Instant::now();
let result = f();
let duration_ms = start.elapsed().as_millis();
println!("[PERF] {} took {}ms", name, duration_ms);
(result, duration_ms)
}
#[allow(dead_code)]
fn save_baseline(name: &str, file_count: usize, duration_ms: u128) {
if let Some(parent) = Path::new(BASELINE_PATH).parent() {
fs::create_dir_all(parent).unwrap();
}
let baseline = PerformanceBaseline {
test_name: name.to_string(),
file_count,
duration_ms,
timestamp: chrono::Utc::now().to_rfc3339(),
};
let json = serde_json::to_string_pretty(&baseline).unwrap();
fs::write(BASELINE_PATH, json).unwrap();
println!("[PERF] Baseline saved to {}", BASELINE_PATH);
}
#[allow(dead_code)]
fn load_baseline() -> Option<PerformanceBaseline> {
let content = fs::read_to_string(BASELINE_PATH).ok()?;
serde_json::from_str(&content).ok()
}
#[allow(dead_code)]
fn check_regression(current_ms: u128) -> Result<(), f64> {
let baseline = load_baseline();
if let Some(baseline) = baseline {
let regression =
(current_ms as f64 - baseline.duration_ms as f64) / baseline.duration_ms as f64;
println!("[PERF] {}", baseline.test_name);
println!("[PERF] Current: {}ms", current_ms);
println!("[PERF] Baseline: {}ms", baseline.duration_ms);
println!("[PERF] Regression: {:.2}%", regression * 100.0);
if regression < 0.0 {
println!(
"[PERF] PASS: Improvement detected (-{:.2}%)",
-regression * 100.0
);
} else if regression <= MAX_REGRESSION {
println!(
"[PERF] PASS: Regression {:.2}% is within threshold of {:.0}%",
regression * 100.0,
MAX_REGRESSION * 100.0
);
} else {
println!(
"[PERF] FAIL: Regression {:.2}% exceeds threshold of {:.0}%",
regression * 100.0,
MAX_REGRESSION * 100.0
);
return Err(regression);
}
Ok(())
} else {
println!("[PERF] No baseline found - creating baseline");
Ok(())
}
}
#[test]
#[cfg(not(debug_assertions))]
fn test_baseline_indexing_performance() {
let temp_dir = tempfile::TempDir::new().unwrap();
let db_path = temp_dir.path().join("test.db");
let test_dir = temp_dir.path().join("src");
fs::create_dir_all(&test_dir).unwrap();
let files = create_rust_files(&test_dir, 100);
println!(
"[PERF] Testing baseline indexing performance with {} files",
files.len()
);
let mut graph = CodeGraph::open(&db_path).unwrap();
let (_indexed, duration_ms) = measure_duration("baseline_indexing_100_files", || {
let filter = FileFilter::new(temp_dir.path(), &[], &[]).unwrap();
magellan::graph::scan::scan_directory_with_filter(
&mut graph,
temp_dir.path(),
&filter,
None,
)
.unwrap()
.indexed
});
if let Err(regression) = check_regression(duration_ms) {
save_baseline("baseline_indexing_100_files", 100, duration_ms);
panic!(
"Performance regression detected: {:.2}% (threshold: {:.0}%)",
regression * 100.0,
MAX_REGRESSION * 100.0
);
}
if load_baseline().is_none() {
save_baseline("baseline_indexing_100_files", 100, duration_ms);
}
}
#[test]
#[cfg(not(debug_assertions))]
fn test_parser_pool_effectiveness() {
let temp_dir = tempfile::TempDir::new().unwrap();
let db_path_pool = temp_dir.path().join("pool.db");
let db_path_nopool = temp_dir.path().join("nopool.db");
let test_dir = temp_dir.path().join("src");
fs::create_dir_all(&test_dir).unwrap();
let files = create_rust_files(&test_dir, 50);
println!(
"[PERF] Testing parser pool effectiveness with {} files",
files.len()
);
let mut graph_pool = CodeGraph::open(&db_path_pool).unwrap();
let (_indexed_pool, duration_pool) = measure_duration("indexing_with_parser_pool", || {
let filter = FileFilter::new(temp_dir.path(), &[], &[]).unwrap();
magellan::graph::scan::scan_directory_with_filter(
&mut graph_pool,
temp_dir.path(),
&filter,
None,
)
.unwrap()
.indexed
});
let mut graph_nopool = CodeGraph::open(&db_path_nopool).unwrap();
let (_indexed_nopool, duration_nopool) =
measure_duration("indexing_without_parser_pool", || {
let mut indexed = 0;
for file in &files {
let source = fs::read(file).unwrap();
let path_str = file.to_string_lossy().to_string();
let _ = graph_nopool.delete_file(&path_str);
let _ = graph_nopool.index_file(&path_str, &source);
let _ = graph_nopool.index_references(&path_str, &source);
indexed += 1;
}
indexed
});
let speedup = duration_nopool as f64 / duration_pool as f64;
println!(
"[PERF] Parser pool speedup: {:.2}x (with: {}ms, without: {}ms)",
speedup, duration_pool, duration_nopool
);
if speedup >= MIN_POOL_SPEEDUP {
println!(
"[PERF] PASS: Parser pool provides {:.0}% speedup (threshold: {:.0}%)",
(speedup - 1.0) * 100.0,
(MIN_POOL_SPEEDUP - 1.0) * 100.0
);
} else {
println!(
"[PERF] WARNING: Parser pool speedup {:.2}x is below threshold {:.2}x",
speedup, MIN_POOL_SPEEDUP
);
}
}
#[test]
#[cfg(not(debug_assertions))]
fn test_sequential_vs_parallel_indexing() {
println!("[PERF] Testing sequential vs parallel indexing");
let temp_dir = tempfile::TempDir::new().unwrap();
let db_path = temp_dir.path().join("test.db");
let test_dir = temp_dir.path().join("src");
fs::create_dir_all(&test_dir).unwrap();
let files = create_rust_files(&test_dir, 100);
let mut graph = CodeGraph::open(&db_path).unwrap();
let (indexed, duration_parallel) = measure_duration("parallel_scan_100_files", || {
let filter = FileFilter::new(temp_dir.path(), &[], &[]).unwrap();
magellan::graph::scan::scan_directory_with_filter(
&mut graph,
temp_dir.path(),
&filter,
None,
)
.unwrap()
.indexed
});
println!(
"[PERF] Parallel scan: {}ms for {} files",
duration_parallel, indexed
);
assert_eq!(indexed, 100, "Should index all 100 files");
}
#[test]
#[cfg(not(debug_assertions))]
fn test_regression_check() {
let temp_dir = tempfile::TempDir::new().unwrap();
let db_path = temp_dir.path().join("test.db");
let test_dir = temp_dir.path().join("src");
fs::create_dir_all(&test_dir).unwrap();
let files = create_rust_files(&test_dir, 100);
let mut graph = CodeGraph::open(&db_path).unwrap();
let (_indexed, current_ms) = measure_duration("regression_check_100_files", || {
let filter = FileFilter::new(temp_dir.path(), &[], &[]).unwrap();
magellan::graph::scan::scan_directory_with_filter(
&mut graph,
temp_dir.path(),
&filter,
None,
)
.unwrap()
.indexed
});
if let Err(regression) = check_regression(current_ms) {
panic!(
"Performance regression detected: {:.2}% exceeds threshold {:.0}%",
regression * 100.0,
MAX_REGRESSION * 100.0
);
}
}
#[cfg(test)]
mod additional_diagnostics {
#[allow(unused_imports)]
use super::*;
#[test]
#[cfg(not(debug_assertions))]
fn test_performance_breakdown() {
let temp_dir = tempfile::TempDir::new().unwrap();
let db_path = temp_dir.path().join("test.db");
let test_dir = temp_dir.path().join("src");
fs::create_dir_all(&test_dir).unwrap();
let _files = create_rust_files(&test_dir, 50);
println!("\n[PERF] Performance breakdown for 50 files:");
let start_io = Instant::now();
let mut total_bytes = 0;
for entry in fs::read_dir(&test_dir).unwrap() {
let entry = entry.unwrap();
if entry.path().is_file() {
let content = fs::read(entry.path()).unwrap();
total_bytes += content.len();
}
}
let io_duration = start_io.elapsed().as_millis();
let mut graph = CodeGraph::open(&db_path).unwrap();
let start_index = Instant::now();
let filter = FileFilter::new(temp_dir.path(), &[], &[]).unwrap();
let indexed = magellan::graph::scan::scan_directory_with_filter(
&mut graph,
temp_dir.path(),
&filter,
None,
)
.unwrap()
.indexed;
let index_duration = start_index.elapsed().as_millis();
let total = io_duration + index_duration;
let io_pct = if total > 0 {
(io_duration as f64 / total as f64) * 100.0
} else {
0.0
};
let index_pct = if total > 0 {
(index_duration as f64 / total as f64) * 100.0
} else {
0.0
};
println!(
"[PERF] Standalone File I/O: {}ms ({:.1}%)",
io_duration, io_pct
);
println!(
"[PERF] Scan with I/O + Parsing + DB: {}ms ({:.1}%)",
index_duration, index_pct
);
println!("[PERF] Total breakdown: {}ms", total);
println!("[PERF] Files indexed: {}", indexed);
println!("[PERF] Total bytes read: {}", total_bytes);
if indexed > 0 {
let avg_ms = index_duration as f64 / indexed as f64;
println!("[PERF] Avg per file: {:.2}ms", avg_ms);
}
}
}