fn report_throughput(label: &str, actual: f64, required: f64, extra: &str) -> Result<()> {
println!(
"{} {label}: {actual:.0} LOC/s (target: ≥{required:.0} LOC/s){extra}",
if actual < required { "❌" } else { "✅" }
);
if actual < required {
anyhow::bail!("{label}: {actual:.0} LOC/s is below the required {required:.0} LOC/s");
}
Ok(())
}
pub async fn test_single_threaded_throughput() -> Result<()> {
let targets = PerformanceTargets::default();
let test_lines = 10_000;
let temp_dir = tempdir()?;
let test_file = temp_dir.path().join("test.rs");
let test_code = generate_test_code(test_lines);
fs::write(&test_file, &test_code)?;
let start = Instant::now();
use crate::cli::handlers::complexity_handlers;
complexity_handlers::handle_analyze_complexity(
temp_dir.path().to_path_buf(),
Some(test_file.clone()), vec![], None, crate::cli::enums::ComplexityOutputFormat::Json,
None, Some(20), Some(15), vec![], false, 10, false, 60, )
.await?;
let duration = start.elapsed();
let actual_throughput = (test_lines as f64) / duration.as_secs_f64();
let required = targets.loc_per_sec_st as f64 * 0.8;
report_throughput("Single-threaded throughput", actual_throughput, required, "")
}
pub async fn test_realistic_project_analysis() -> Result<()> {
let test_lines = 50_000;
let temp_dir = tempdir()?;
let src_dir = temp_dir.path().join("src");
fs::create_dir(&src_dir)?;
for i in 0..10 {
let file_path = src_dir.join(format!("module_{i}.rs"));
let file_code = generate_test_code(test_lines / 10);
fs::write(&file_path, &file_code)?;
}
let start = Instant::now();
use crate::cli::handlers::complexity_handlers;
complexity_handlers::handle_analyze_complexity(
temp_dir.path().to_path_buf(),
None, vec![], None, crate::cli::enums::ComplexityOutputFormat::Summary,
None, Some(20), Some(15), vec![], false, 10, false, 60, )
.await?;
let duration = start.elapsed();
let actual_throughput = (test_lines as f64) / duration.as_secs_f64();
let min_throughput = 100_000; report_throughput(
"Multi-file analysis",
actual_throughput,
f64::from(min_throughput),
&format!(", duration: {duration:?}"),
)
}
const ANALYSIS_BUDGET_SECS: u64 = 60;
pub async fn test_large_file_performance() -> Result<()> {
let test_lines = 100_000;
let temp_dir = tempdir()?;
let test_file = temp_dir.path().join("large_file.rs");
let test_code = generate_test_code(test_lines);
fs::write(&test_file, &test_code)?;
let start = Instant::now();
use crate::cli::handlers::complexity_handlers;
let analysis = tokio::spawn(complexity_handlers::handle_analyze_complexity(
temp_dir.path().to_path_buf(),
Some(test_file), vec![], None, crate::cli::enums::ComplexityOutputFormat::Summary,
None, Some(20), Some(15), vec![], false, 10, false, ANALYSIS_BUDGET_SECS, ));
match tokio::time::timeout(Duration::from_secs(ANALYSIS_BUDGET_SECS), analysis).await {
Ok(joined) => joined??,
Err(_) => {
println!(
"❌ Large file performance: no result within {ANALYSIS_BUDGET_SECS}s for {test_lines} LOC"
);
anyhow::bail!(
"Large file analysis exceeded its own {ANALYSIS_BUDGET_SECS}s budget for {test_lines} LOC"
);
}
}
let duration = start.elapsed();
let max_duration_secs = 30; let throughput = (test_lines as f64) / duration.as_secs_f64();
if duration.as_secs() > max_duration_secs {
println!(
"❌ Large file performance: {throughput:.0} LOC/s, duration: {duration:?} (budget: ≤{max_duration_secs}s for 100K LOC)"
);
anyhow::bail!(
"Large file analysis took {}s, over the {}s budget for 100K LOC",
duration.as_secs(),
max_duration_secs
);
}
println!("✅ Large file performance: {throughput:.0} LOC/s, duration: {duration:?}");
Ok(())
}
pub async fn test_memory_usage_patterns() -> Result<()> {
let test_lines = 20_000;
let temp_dir = tempdir()?;
let test_file = temp_dir.path().join("memory_test.rs");
let test_code = generate_test_code(test_lines);
fs::write(&test_file, &test_code)?;
let before = MemorySample::read()?;
use crate::cli::handlers::complexity_handlers;
complexity_handlers::handle_analyze_complexity(
temp_dir.path().to_path_buf(),
Some(test_file), vec![], None, crate::cli::enums::ComplexityOutputFormat::Json,
None, Some(20), Some(15), vec![], false, 10, false, 60, )
.await?;
let after = MemorySample::read()?;
let peak_growth_kb = after.peak_kb.saturating_sub(before.peak_kb);
const BUDGET_KB: u64 = 10 * 1024; if peak_growth_kb > BUDGET_KB {
println!(
"❌ Memory usage: peak grew {peak_growth_kb} KB for {}K LOC (budget: ≤{BUDGET_KB} KB)",
test_lines / 1000
);
anyhow::bail!(
"Peak resident memory grew {peak_growth_kb} KB for {}K LOC, over the {BUDGET_KB} KB budget",
test_lines / 1000
);
}
println!(
"✅ Memory usage: peak RSS {} KB (was {} KB), analysis added {} KB for {}K LOC",
after.peak_kb,
before.peak_kb,
peak_growth_kb,
test_lines / 1000
);
Ok(())
}
pub async fn test_performance_regression_detection() -> Result<()> {
const ITERATIONS: usize = 5;
let test_lines = 5_000;
let temp_dir = tempdir()?;
let test_file = temp_dir.path().join("regression_test.rs");
let test_code = generate_test_code(test_lines);
fs::write(&test_file, &test_code)?;
let mut durations = Vec::with_capacity(ITERATIONS);
for _ in 0..ITERATIONS {
let start = Instant::now();
use crate::cli::handlers::complexity_handlers;
complexity_handlers::handle_analyze_complexity(
temp_dir.path().to_path_buf(),
Some(test_file.clone()), vec![], None, crate::cli::enums::ComplexityOutputFormat::Json,
None, Some(20), Some(15), vec![], false, 10, false, 60, )
.await?;
durations.push(start.elapsed());
}
let avg_duration = durations.iter().sum::<Duration>() / ITERATIONS as u32;
let max_duration = durations.iter().max().expect("internal error");
let min_duration = durations.iter().min().expect("internal error");
let variance_ratio = max_duration.as_millis() as f64 / min_duration.as_millis() as f64;
assert!(
variance_ratio <= 2.0,
"High performance variance: min={}ms, max={}ms, ratio={:.2}",
min_duration.as_millis(),
max_duration.as_millis(),
variance_ratio
);
println!(
"✅ Performance consistency: avg={}ms, min={}ms, max={}ms",
avg_duration.as_millis(),
min_duration.as_millis(),
max_duration.as_millis()
);
Ok(())
}
fn parse_vmrss_kb(status: &str) -> Option<u64> {
parse_status_kb(status, "VmRSS:")
}
fn parse_status_kb(status: &str, field: &str) -> Option<u64> {
status
.lines()
.find(|line| line.starts_with(field))
.and_then(|line| line.split_whitespace().nth(1))
.and_then(|kb_str| kb_str.parse::<u64>().ok())
}
#[derive(Debug, Clone, Copy)]
pub struct MemorySample {
pub rss_kb: u64,
pub peak_kb: u64,
}
impl MemorySample {
pub fn read() -> Result<Self> {
#[cfg(target_os = "linux")]
{
let status = std::fs::read_to_string("/proc/self/status").map_err(|e| {
anyhow::anyhow!("cannot measure memory: /proc/self/status unreadable: {e}")
})?;
let rss_kb = parse_status_kb(&status, "VmRSS:")
.ok_or_else(|| anyhow::anyhow!("cannot measure memory: no VmRSS in status"))?;
let peak_kb = parse_status_kb(&status, "VmHWM:")
.ok_or_else(|| anyhow::anyhow!("cannot measure memory: no VmHWM in status"))?;
Ok(Self { rss_kb, peak_kb })
}
#[cfg(not(target_os = "linux"))]
anyhow::bail!("cannot measure memory: resident-set reporting is Linux-only on this build")
}
}
#[must_use]
pub fn get_memory_usage_mb() -> u64 {
#[cfg(target_os = "linux")]
{
if let Ok(status) = std::fs::read_to_string("/proc/self/status") {
return parse_vmrss_kb(&status).unwrap_or(0) / 1024;
}
}
0
}
#[cfg(test)]
mod throughput_verdict_tests {
use super::*;
#[test]
fn test_report_throughput_fails_below_target() {
let err = report_throughput("Single-threaded throughput", 1593.0, 487_000.0, "")
.expect_err("1593 LOC/s against a 487000 LOC/s target is a failure");
assert!(err.to_string().contains("below the required"), "{err}");
assert!(
report_throughput("Multi-file analysis", 67944.0, 100_000.0, "").is_err(),
"67944 LOC/s against a 100000 LOC/s target is a failure"
);
}
#[test]
fn test_report_throughput_passes_at_or_above_target() {
assert!(report_throughput("t", 100_000.0, 100_000.0, "").is_ok());
assert!(report_throughput("t", 500_000.0, 487_000.0, "").is_ok());
}
#[tokio::test]
async fn test_suite_with_nothing_enabled_is_an_error() {
let config = PerformanceTestConfig {
enable_regression_tests: false,
enable_memory_tests: false,
enable_throughput_tests: false,
test_iterations: 3,
};
let err = run_performance_test_suite(config)
.await
.expect_err("a run that measured nothing must not report success");
assert!(err.to_string().contains("nothing was measured"), "{err}");
}
#[test]
fn test_memory_sample_reads_kb_resolution() {
let sample = MemorySample::read().expect("linux test host exposes /proc/self/status");
assert!(
sample.rss_kb > 1024,
"RSS {} KB looks unmeasured",
sample.rss_kb
);
assert!(
sample.peak_kb >= sample.rss_kb,
"peak {} KB below current {} KB",
sample.peak_kb,
sample.rss_kb
);
let before = "VmRSS:\t 102400 kB\nVmHWM:\t 102400 kB\n";
let after = "VmRSS:\t 102900 kB\nVmHWM:\t 102900 kB\n";
let before_kb = parse_status_kb(before, "VmHWM:").unwrap();
let after_kb = parse_status_kb(after, "VmHWM:").unwrap();
assert_eq!(after_kb - before_kb, 500);
assert_eq!(after_kb / 1024 - before_kb / 1024, 0, "the old MB view");
}
#[test]
fn test_parse_status_kb_reads_peak_field() {
let status = "Name:\tpmat\nVmRSS:\t 123456 kB\nVmHWM:\t 234567 kB\n";
assert_eq!(parse_status_kb(status, "VmRSS:"), Some(123_456));
assert_eq!(parse_status_kb(status, "VmHWM:"), Some(234_567));
assert_eq!(parse_status_kb(status, "VmNope:"), None);
}
}
pub async fn run_performance_test_suite(config: PerformanceTestConfig) -> Result<()> {
println!("🏃 Running PMAT Performance Test Suite (SPECIFICATION.md Section 30)");
println!("================================================================");
if !config.enable_throughput_tests
&& !config.enable_regression_tests
&& !config.enable_memory_tests
{
anyhow::bail!(
"no performance sub-suite was enabled, so nothing was measured (expected at least one of throughput/regression/memory)"
);
}
if config.enable_throughput_tests {
println!("\n📊 Throughput Tests:");
test_single_threaded_throughput().await?;
test_realistic_project_analysis().await?;
test_large_file_performance().await?;
}
if config.enable_regression_tests {
println!("\n🔍 Regression Tests:");
test_performance_regression_detection().await?;
}
if config.enable_memory_tests {
println!("\n💾 Memory Tests:");
test_memory_usage_patterns().await?;
}
println!("\n✅ All performance tests passed!");
println!("Performance characteristics meet SPECIFICATION.md Section 1.4 requirements");
Ok(())
}