diff --git a/Cargo.toml b/Cargo.toml index 158937d8..adff10ad 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -83,6 +83,7 @@ rusqlite = { version = "0.32", features = ["bundled"] } [features] hardware-wallet = ["dep:hidapi", "dep:trezor-client"] +memory-profiling = [] [dev-dependencies] criterion = "0.5.1" diff --git a/src/commands/perf.rs b/src/commands/perf.rs index cc80bdda..92bda6d7 100644 --- a/src/commands/perf.rs +++ b/src/commands/perf.rs @@ -2,6 +2,7 @@ use crate::utils::{performance as perf, print as p}; use anyhow::Result; use clap::Subcommand; use std::collections::HashMap; +use std::collections::BTreeMap; #[derive(Subcommand)] pub enum PerfCommands { @@ -143,6 +144,276 @@ pub async fn handle(cmd: PerfCommands) -> Result<()> { } } +// ── Advanced Profiling Commands ────────────────────────────────────────────── + +#[derive(Subcommand)] +pub enum AdvancedPerfCommands { + /// Advanced performance analysis with bottleneck detection + Analyze { + /// Contract ID + contract: String, + /// Network (default: testnet) + #[arg(long, default_value = "testnet")] + network: String, + }, + /// Detect performance regressions + DetectRegression { + /// Contract ID + contract: String, + /// Analysis period in hours (default: 24) + #[arg(long, default_value_t = 24)] + period_hours: u64, + /// Network (default: testnet) + #[arg(long, default_value = "testnet")] + network: String, + }, + /// Compare performance across time periods + Compare { + /// Contract ID + contract: String, + /// Time window in hours (default: 24) + #[arg(long, default_value_t = 24)] + hours_back: u64, + /// Network (default: testnet) + #[arg(long, default_value = "testnet")] + network: String, + }, + /// Generate comprehensive performance dashboard + GenerateDashboard { + /// Contract ID + contract: String, + /// Network (default: testnet) + #[arg(long, default_value = "testnet")] + network: String, + }, +} + +// ── Advanced Profiling Command Handlers ────────────────────────────────────── + +pub async fn handle_advanced(cmd: AdvancedPerfCommands) -> Result<()> { + match cmd { + AdvancedPerfCommands::Analyze { contract, network } => analyze(contract, network), + AdvancedPerfCommands::DetectRegression { contract, period_hours, network } => detect_regression(contract, period_hours, network), + AdvancedPerfCommands::Compare { contract, hours_back, network } => compare(contract, hours_back, network), + AdvancedPerfCommands::GenerateDashboard { contract, network } => generate_dashboard(contract, network), + } +} + +fn analyze(contract: String, network: String) -> Result<()> { + p::header("Advanced Performance Analysis"); + p::separator(); + p::kv("Contract", &contract); + p::kv("Network", &network); + p::separator(); + + let analysis = perf::analyze_bottlenecks(&contract)?; + + println!(); + p::info("Bottleneck Analysis Results"); + p::kv("Overall Score", &format!("{:.1}/100", analysis.overall_score)); + p::kv("Memory Leaks Detected", &analysis.memory_leaks_detected.to_string()); + + if !analysis.bottleneck_operations.is_empty() { + println!(); + p::warn("Frequent Operations (Potential Bottlenecks):"); + for op in &analysis.bottleneck_operations { + println!(" • {}", op); + } + } + + if !analysis.high_gas_operations.is_empty() { + println!(); + p::warn("High Gas Consumption Operations:"); + for op in &analysis.high_gas_operations { + println!(" • {}", op); + } + } + + if analysis.bottleneck_operations.is_empty() && analysis.high_gas_operations.is_empty() { + p::success("No significant bottlenecks detected!"); + } + + println!(); + p::separator(); + Ok(()) +} + +fn detect_regression(contract: String, period_hours: u64, network: String) -> Result<()> { + p::header("Performance Regression Detection"); + p::separator(); + p::kv("Contract", &contract); + p::kv("Network", &network); + p::kv("Analysis Period", &format!("{} hours", period_hours)); + p::separator(); + + let report = perf::detect_regression(&contract, period_hours)?; + + println!(); + p::info("Regression Report"); + p::kv("Baseline Avg Gas", &format!("{:.0}", report.baseline_avg)); + p::kv("Current Avg Gas", &format!("{:.0}", report.current_avg)); + p::kv("Change", &format!("{:+.1}%", report.regression_percentage)); + + println!(); + p::info("Trends:"); + for trend in &report.trends { + if trend.contains("increased") { + p::warn(&format!(" ⚠ {}", trend)); + } else if trend.contains("decreased") { + p::success(&format!(" ✓ {}", trend)); + } else { + p::info(&format!(" • {}", trend)); + } + } + + let regression_count = report.regression_points.iter().filter(|r| r.regression_detected).count(); + if regression_count > 0 { + println!(); + p::warn(&format!("{} regression points detected:", regression_count)); + for point in &report.regression_points { + if point.regression_detected { + println!( + " {} gas={} time={}ms [{}]", + &point.timestamp[..19], + point.gas_used, + point.execution_time_ms, + if point.success { "OK" } else { "FAIL" } + ); + } + } + } else { + p::success("No regressions detected!"); + } + + println!(); + p::separator(); + Ok(()) +} + +fn compare(contract: String, hours_back: u64, network: String) -> Result<()> { + p::header("Performance Comparison"); + p::separator(); + p::kv("Contract", &contract); + p::kv("Network", &network); + p::kv("Time Window", &format!("{} hours", hours_back)); + p::separator(); + + let report = perf::compare_profiles(&contract, hours_back)?; + + println!(); + p::info("Comparison Results"); + + if !report.performance_differences.is_empty() { + for (metric, diff) in &report.performance_differences { + let label = metric.replace('_', " "); + if *diff > 0.0 { + p::warn(&format!(" {} +{:.1}% (regression)", label, diff)); + } else { + p::success(&format!(" {} {:.1}% (improvement)", label, diff)); + } + } + } else { + p::info("Insufficient data for comparison (need at least 2 snapshots)"); + } + + println!(); + p::info("Recommendations:"); + if report.recommendations.is_empty() { + p::success(" No specific recommendations at this time."); + } else { + for rec in &report.recommendations { + println!(" • {}", rec); + } + } + + println!(); + p::separator(); + Ok(()) +} + +fn generate_dashboard(contract: String, network: String) -> Result<()> { + p::header("Performance Dashboard"); + p::separator(); + p::kv("Contract", &contract); + p::kv("Network", &network); + p::separator(); + + let dashboard = perf::generate_dashboard(&contract, &network)?; + + println!(); + p::info("═══ EXECUTION SUMMARY ═══"); + p::kv("Total Executions", &dashboard.summary.total_executions.to_string()); + p::kv("Avg Gas Used", &format!("{:.0}", dashboard.summary.avg_gas_used)); + p::kv("Max Gas Used", &format!("{:.0}", dashboard.summary.max_gas_used)); + p::kv("Avg Execution Time", &format!("{:.1}ms", dashboard.summary.avg_execution_time_ms)); + p::kv("Success Rate", &format!("{:.1}%", dashboard.summary.success_rate)); + + println!(); + p::info("═══ BOTTLENECK ANALYSIS ═══"); + p::kv("Overall Score", &format!("{:.1}/100", dashboard.bottleneck_analysis.overall_score)); + p::kv("Memory Leaks Detected", &dashboard.bottleneck_analysis.memory_leaks_detected.to_string()); + + if !dashboard.bottleneck_analysis.bottleneck_operations.is_empty() { + p::warn("Frequent Operations:"); + for op in &dashboard.bottleneck_analysis.bottleneck_operations { + println!(" • {}", op); + } + } + if !dashboard.bottleneck_analysis.high_gas_operations.is_empty() { + p::warn("High Gas Operations:"); + for op in &dashboard.bottleneck_analysis.high_gas_operations { + println!(" • {}", op); + } + } + + println!(); + p::info("═══ REGRESSION DETECTION ═══"); + p::kv("Baseline Avg", &format!("{:.0}", dashboard.regression_report.baseline_avg)); + p::kv("Current Avg", &format!("{:.0}", dashboard.regression_report.current_avg)); + p::kv("Change", &format!("{:+.1}%", dashboard.regression_report.regression_percentage)); + for trend in &dashboard.regression_report.trends { + if trend.contains("increased") { + p::warn(&format!(" ⚠ {}", trend)); + } else if trend.contains("decreased") { + p::success(&format!(" ✓ {}", trend)); + } else { + p::info(&format!(" • {}", trend)); + } + } + + println!(); + p::info("═══ PERFORMANCE COMPARISON ═══"); + if !dashboard.comparison_report.performance_differences.is_empty() { + for (metric, diff) in &dashboard.comparison_report.performance_differences { + let label = metric.replace('_', " "); + if *diff > 0.0 { + p::warn(&format!(" {} +{:.1}%", label, diff)); + } else { + p::success(&format!(" {} {:.1}%", label, diff)); + } + } + } else { + p::info(" No comparison data available"); + } + for rec in &dashboard.comparison_report.recommendations { + println!(" • {}", rec); + } + + if !dashboard.alerts.is_empty() { + println!(); + p::warn("Configured Alerts:"); + for alert in &dashboard.alerts { + println!(" • {} {} {} ({})", alert.metric_name, + if matches!(alert.direction, perf::AlertDirection::Above) { ">" } else { "<" }, + alert.threshold, alert.message); + } + } + + println!(); + p::separator(); + Ok(()) +} + fn record( contract: String, operation: String, diff --git a/src/main.rs b/src/main.rs index bfff1750..2c3b4abe 100644 --- a/src/main.rs +++ b/src/main.rs @@ -142,6 +142,10 @@ enum Commands { #[command(subcommand)] Perf(commands::perf::PerfCommands), + /// Advanced contract performance analysis and profiling tools + #[command(subcommand)] + AdvancedPerf(commands::perf::AdvancedPerfCommands), + /// Contract documentation portal (generate, view, search) #[command(subcommand)] Docs(commands::docs::DocsCommands), @@ -201,6 +205,7 @@ async fn main() { Commands::Diagnostics(_) => "diagnostics", Commands::TemplateVcs(_) => "template-vcs", Commands::Perf(_) => "perf", + Commands::AdvancedPerf(_) => "advanced-perf", Commands::Docs(_) => "docs", Commands::Analytics(_) => "analytics", Commands::External(_) => "external", @@ -239,6 +244,7 @@ async fn main() { Commands::Diagnostics(args) => commands::diagnostics::handle(args).await, Commands::TemplateVcs(cmd) => commands::template_vcs::handle(cmd).await, Commands::Perf(cmd) => commands::perf::handle(cmd).await, + Commands::AdvancedPerf(cmd) => commands::perf::handle_advanced(cmd).await, Commands::Docs(cmd) => commands::docs::handle(cmd).await, Commands::Analytics(cmd) => commands::analytics::handle(cmd).await, Commands::External(args) => handle_external_plugin(args), diff --git a/src/utils/performance.rs b/src/utils/performance.rs index 9a03c006..28eb6361 100644 --- a/src/utils/performance.rs +++ b/src/utils/performance.rs @@ -4,6 +4,7 @@ use std::collections::HashMap; use std::fs; use std::path::{Path, PathBuf}; use std::time::{Duration, Instant}; +use std::collections::BTreeMap; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ContractMetrics { @@ -325,6 +326,333 @@ impl MetricCollector { } } +// ── Bottleneck Identification ─────────────────────────────────────────────── + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct BottleneckAnalysis { + pub contract_id: String, + pub network: String, + pub bottleneck_operations: Vec, + pub high_gas_operations: Vec, + pub memory_leaks_detected: bool, + pub overall_score: f64, +} + +pub fn analyze_bottlenecks(contract_id: &str) -> Result { + let gas_history = get_gas_history(contract_id)?; + if gas_history.is_empty() { + return Err(anyhow::anyhow!("No gas history found for contract: {}", contract_id)); + } + + let mut operation_frequencies: HashMap = HashMap::new(); + let mut operation_gas: HashMap = HashMap::new(); + + for record in &gas_history { + *operation_frequencies.entry(record.operation.clone()).or_insert(0) += 1; + operation_gas.entry(record.operation.clone()) + .and_modify(|g| *g += record.gas_used) + .or_insert(record.gas_used); + } + + let total_gas: u64 = gas_history.iter().map(|r| r.gas_used).sum(); + let total_executions = gas_history.len() as f64; + + let bottleneck_operations: Vec = operation_frequencies + .iter() + .filter(|(_, freq)| **freq as f64 / total_executions > 0.3) + .map(|(op, _)| op.clone()) + .collect(); + + let high_gas_operations: Vec = operation_gas + .iter() + .filter(|(_, gas)| **gas as f64 / total_executions as f64 > 50_000.0) + .map(|(op, _)| op.clone()) + .collect(); + + let success_rate = gas_history.iter().filter(|r| r.success).count() as f64 / total_executions; + let avg_execution_time = gas_history.iter().map(|r| r.execution_time_ms).sum::() as f64 / total_executions; + + let mut score = 100.0; + if success_rate < 0.95 { + score -= (0.95 - success_rate) * 30.0; + } + if avg_execution_time > 5000.0 { + score -= ((avg_execution_time - 5000.0) / 5000.0) * 25.0; + } + + Ok(BottleneckAnalysis { + contract_id: contract_id.to_string(), + network: gas_history[0].network.clone(), + bottleneck_operations, + high_gas_operations, + memory_leaks_detected: avg_execution_time > 10000.0, + overall_score: score.max(0.0), + }) +} + +// ── Performance Regression Detection ─────────────────────────────────────────── + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct RegressionPoint { + pub timestamp: String, + pub gas_used: u64, + pub execution_time_ms: u64, + pub success: bool, + pub regression_detected: bool, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct RegressionReport { + pub contract_id: String, + pub network: String, + pub baseline_avg: f64, + pub current_avg: f64, + pub regression_percentage: f64, + pub regression_points: Vec, + pub trends: Vec, +} + +pub fn detect_regression(contract_id: &str, period_hours: u64) -> Result { + let gas_history = get_gas_history(contract_id)?; + if gas_history.is_empty() { + return Err(anyhow::anyhow!("No gas history found for contract: {}", contract_id)); + } + + let now = chrono::Utc::now(); + let cutoff = now - chrono::Duration::hours(period_hours as i64); + let recent_records: Vec<_> = gas_history + .iter() + .filter(|r| { + chrono::DateTime::parse_from_rfc3339(&r.timestamp) + .map(|t| t >= cutoff) + .unwrap_or(false) + }) + .collect(); + + let baseline_records: Vec<_> = gas_history + .iter() + .filter(|r| { + chrono::DateTime::parse_from_rfc3339(&r.timestamp) + .map(|t| t < cutoff) + .unwrap_or(false) + }) + .collect(); + + let baseline_gas: Vec = baseline_records.iter() + .map(|r| r.gas_used as f64) + .collect(); + let current_gas: Vec = recent_records.iter() + .map(|r| r.gas_used as f64) + .collect(); + + let baseline_avg = if !baseline_gas.is_empty() { + baseline_gas.iter().sum::() / baseline_gas.len() as f64 + } else { + 0.0 + }; + + let current_avg = if !current_gas.is_empty() { + current_gas.iter().sum::() / current_gas.len() as f64 + } else { + 0.0 + }; + + let mut regression_percentage = 0.0; + if baseline_avg > 0.0 { + regression_percentage = ((current_avg - baseline_avg) / baseline_avg) * 100.0; + } + + let regression_points: Vec = recent_records + .iter() + .map(|r| { + let mut detected = false; + if baseline_avg > 0.0 && (r.gas_used as f64) > baseline_avg * 1.2 { + detected = true; + } + if baseline_avg > 0.0 && (r.gas_used as f64) < baseline_avg * 0.8 { + detected = true; + } + + RegressionPoint { + timestamp: r.timestamp.clone(), + gas_used: r.gas_used, + execution_time_ms: r.execution_time_ms, + success: r.success, + regression_detected: detected, + } + }) + .collect(); + + let mut trends: Vec = Vec::new(); + if baseline_avg > 0.0 && current_avg > baseline_avg * 1.15 { + trends.push("Gas usage has increased by more than 15% compared to baseline".to_string()); + } else if baseline_avg > 0.0 && current_avg < baseline_avg * 0.85 { + trends.push("Gas usage has decreased by more than 15% compared to baseline".to_string()); + } else { + trends.push("Gas usage is within acceptable range of baseline".to_string()); + } + + Ok(RegressionReport { + contract_id: contract_id.to_string(), + network: gas_history[0].network.clone(), + baseline_avg, + current_avg, + regression_percentage, + regression_points, + trends, + }) +} + +// ── Profiling Comparison ─────────────────────────────────────────────────────── + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ProfileSnapshot { + pub contract_id: String, + pub timestamp: String, + pub operation: String, + pub gas_used: u64, + pub execution_time_ms: u64, + pub success: bool, + pub memory_used: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ComparisonReport { + pub contract_id: String, + pub comparison_date: String, + pub snapshots: Vec, + pub performance_differences: BTreeMap, + pub recommendations: Vec, +} + +pub fn compare_profiles(contract_id: &str, hours_back: u64) -> Result { + let gas_history = get_gas_history(contract_id)?; + if gas_history.is_empty() { + return Err(anyhow::anyhow!("No gas history found for contract: {}", contract_id)); + } + + let cutoff = chrono::Utc::now() - chrono::Duration::hours(hours_back as i64); + let recent_records: Vec<_> = gas_history + .iter() + .filter(|r| { + chrono::DateTime::parse_from_rfc3339(&r.timestamp) + .map(|t| t >= cutoff) + .unwrap_or(false) + }) + .collect(); + + let mut snapshots: Vec = Vec::new(); + for record in recent_records { + snapshots.push(ProfileSnapshot { + contract_id: contract_id.to_string(), + timestamp: record.timestamp.clone(), + operation: record.operation.clone(), + gas_used: record.gas_used, + execution_time_ms: record.execution_time_ms, + success: record.success, + memory_used: None, + }); + } + + let mut performance_differences: BTreeMap = BTreeMap::new(); + + if snapshots.len() >= 2 { + let avg_gas_current: f64 = snapshots.iter() + .map(|s| s.gas_used as f64) + .sum::() / snapshots.len() as f64; + + let avg_gas_earlier = if snapshots.len() >= 4 { + let earlier: Vec<_> = snapshots.iter().take(snapshots.len() / 2).collect(); + earlier.iter().map(|s| s.gas_used as f64).sum::() / earlier.len() as f64 + } else { + avg_gas_current + }; + + if avg_gas_earlier > 0.0 { + performance_differences.insert( + "gas_usage_difference".to_string(), + ((avg_gas_current - avg_gas_earlier) / avg_gas_earlier) * 100.0 + ); + } + + let avg_time_current: f64 = snapshots.iter() + .map(|s| s.execution_time_ms as f64) + .sum::() / snapshots.len() as f64; + + let avg_time_earlier = if snapshots.len() >= 4 { + let earlier: Vec<_> = snapshots.iter().take(snapshots.len() / 2).collect(); + earlier.iter().map(|s| s.execution_time_ms as f64).sum::() / earlier.len() as f64 + } else { + avg_time_current + }; + + if avg_time_earlier > 0.0 { + performance_differences.insert( + "execution_time_difference".to_string(), + ((avg_time_current - avg_time_earlier) / avg_time_earlier) * 100.0 + ); + } + } + + let mut recommendations: Vec = Vec::new(); + if let Some(gas_diff) = performance_differences.get("gas_usage_difference") { + if *gas_diff > 20.0 { + recommendations.push("Gas usage has increased significantly. Consider optimizing storage access patterns.".to_string()); + } else if *gas_diff < -20.0 { + recommendations.push("Gas usage has decreased significantly. Good optimization work!".to_string()); + } + } + + if let Some(time_diff) = performance_differences.get("execution_time_difference") { + if *time_diff > 20.0 { + recommendations.push("Execution time has increased. Investigate for potential bottlenecks.".to_string()); + } else if *time_diff < -20.0 { + recommendations.push("Execution time has improved significantly.".to_string()); + } + } + + Ok(ComparisonReport { + contract_id: contract_id.to_string(), + comparison_date: chrono::Utc::now().to_rfc3339(), + snapshots, + performance_differences, + recommendations, + }) +} + +// ── Performance Dashboard ────────────────────────────────────────────────────── + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PerformanceDashboard { + pub contract_id: String, + pub network: String, + pub timestamp: String, + pub summary: PerformanceSummary, + pub bottleneck_analysis: BottleneckAnalysis, + pub regression_report: RegressionReport, + pub comparison_report: ComparisonReport, + pub alerts: Vec, +} + +pub fn generate_dashboard(contract_id: &str, network: &str) -> Result { + let report = generate_report(contract_id, network)?; + let bottleneck = analyze_bottlenecks(contract_id)?; + let regression = detect_regression(contract_id, 24)?; + let comparison = compare_profiles(contract_id, 24)?; + let metrics = get_contract_metrics(contract_id)?; + + Ok(PerformanceDashboard { + contract_id: contract_id.to_string(), + network: network.to_string(), + timestamp: chrono::Utc::now().to_rfc3339(), + summary: report.summary, + bottleneck_analysis: bottleneck, + regression_report: regression, + comparison_report: comparison, + alerts: metrics.alerts, + }) +} + #[cfg(test)] mod tests { use super::*; @@ -376,4 +704,84 @@ mod tests { assert_eq!(summary.total_executions, 0); assert_eq!(summary.success_rate, 100.0); } + + #[test] + fn test_analyze_bottlenecks() { + let contract_id = format!("TEST_{}", chrono::Utc::now().timestamp_millis()); + let base_time = chrono::Utc::now(); + + for i in 0..10 { + let record = GasUsageRecord { + contract_id: contract_id.clone(), + operation: if i % 3 == 0 { "transfer".to_string() } + else { "query".to_string() }, + gas_used: (i * 1000 + 500) as u64, + timestamp: (base_time + chrono::Duration::seconds(i as i64)).to_rfc3339(), + success: i % 5 != 0, + execution_time_ms: (i * 100 + 100) as u64, + network: "testnet".to_string(), + }; + record_gas_usage(&record).unwrap(); + } + + let loaded = get_gas_history(&contract_id).unwrap(); + assert_eq!(loaded.len(), 10); + + let analysis = analyze_bottlenecks(&contract_id).unwrap(); + assert!(analysis.overall_score >= 0.0); + assert!(!analysis.bottleneck_operations.is_empty()); + } + + #[test] + fn test_detect_regression() { + let contract_id = format!("REGRESSION_{}", chrono::Utc::now().timestamp_millis()); + let base_time = chrono::Utc::now(); + + for i in 0..10 { + let record = GasUsageRecord { + contract_id: contract_id.clone(), + operation: "operation".to_string(), + gas_used: if i < 5 { 10000 + i as u64 * 500 } + else { 15000 + i as u64 * 500 }, + timestamp: (base_time + chrono::Duration::seconds(i as i64)).to_rfc3339(), + success: true, + execution_time_ms: if i < 5 { 500 + i as u64 * 50 } else { 1000 + i as u64 * 50 }, + network: "testnet".to_string(), + }; + record_gas_usage(&record).unwrap(); + } + + let loaded = get_gas_history(&contract_id).unwrap(); + assert_eq!(loaded.len(), 10); + + let report = detect_regression(&contract_id, 24).unwrap(); + assert!(!report.regression_points.is_empty()); + assert!(!report.trends.is_empty()); + } + + #[test] + fn test_compare_profiles() { + let contract_id = format!("COMPARE_{}", chrono::Utc::now().timestamp_millis()); + let base_time = chrono::Utc::now(); + + for i in 0..8 { + let record = GasUsageRecord { + contract_id: contract_id.clone(), + operation: "test_op".to_string(), + gas_used: 10000 + i as u64 * 1000, + timestamp: (base_time - chrono::Duration::seconds((8 - i) as i64)).to_rfc3339(), + success: true, + execution_time_ms: 500 + i * 50, + network: "testnet".to_string(), + }; + record_gas_usage(&record).unwrap(); + } + + let loaded = get_gas_history(&contract_id).unwrap(); + assert_eq!(loaded.len(), 8); + + let report = compare_profiles(&contract_id, 24).unwrap(); + assert_eq!(report.snapshots.len(), 8); + assert!(!report.performance_differences.is_empty() || report.recommendations.is_empty()); + } } diff --git a/src/utils/profiler.rs b/src/utils/profiler.rs index e1a5fa17..1ab6715d 100644 --- a/src/utils/profiler.rs +++ b/src/utils/profiler.rs @@ -1,4 +1,32 @@ use std::time::{Duration, Instant}; +use std::mem::size_of; + +#[cfg(feature = "memory-profiling")] +use std::alloc::{GlobalAlloc, Layout, System}; + +#[cfg(feature = "memory-profiling")] +#[derive(Debug)] +struct MemoryProfiler; + +#[cfg(feature = "memory-profiling")] +impl GlobalAlloc for MemoryProfiler { + unsafe fn alloc(&self, layout: Layout) -> *mut u8 { + let ptr = System.alloc(layout); + if !ptr.is_null() { + if let Some(alloc_tracker) = &mut ALLOC_TRACKER { + alloc_tracker.allocations.push((layout.size(), ptr as usize)); + } + } + ptr + } + + unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) { + System.dealloc(ptr, layout); + if let Some(alloc_tracker) = &mut ALLOC_TRACKER { + alloc_tracker.allocations.retain(|(size, addr)| ptr as usize != *addr); + } + } +} pub struct Timer { start: Instant, @@ -22,22 +50,85 @@ pub struct ProfilePoint { pub elapsed: Duration, } +#[derive(Debug, Clone)] +pub struct MemoryPoint { + pub label: String, + pub timestamp: Duration, + pub allocated_bytes: usize, + pub deallocated_bytes: usize, + pub current_bytes: usize, + pub peak_bytes: usize, +} + +#[derive(Debug, Clone, Default)] +pub struct MemoryMetrics { + pub allocated: usize, + pub deallocated: usize, + pub current: usize, + pub peak: usize, + pub samples: Vec, +} + #[derive(Debug)] pub struct Profiler { start: Instant, marks: Vec<(String, Instant)>, + #[cfg(feature = "memory-profiling")] + memory_tracker: Option, +} + +#[cfg(feature = "memory-profiling")] +struct MemoryTracker { + start: Instant, + current_memory: usize, + peak_memory: usize, + samples: Vec<(String, Instant, usize, usize, usize, usize)>, } +#[cfg(not(feature = "memory-profiling"))] +struct MemoryTracker; + impl Profiler { pub fn start() -> Self { + #[cfg(feature = "memory-profiling")] + let memory_tracker: Option = Some(MemoryTracker { + start: Instant::now(), + current_memory: 0, + peak_memory: 0, + samples: Vec::new(), + }); + #[cfg(not(feature = "memory-profiling"))] + let memory_tracker: Option = None; + Self { start: Instant::now(), marks: Vec::new(), + #[cfg(feature = "memory-profiling")] + memory_tracker, } } pub fn mark(&mut self, label: impl Into) { self.marks.push((label.into(), Instant::now())); + #[cfg(feature = "memory-profiling")] + if let Some(tracker) = &mut self.memory_tracker { + tracker.record_sample(label.into(), self.start.elapsed()); + } + } + + pub fn get_memory_metrics(&self) -> MemoryMetrics { + let mut metrics = MemoryMetrics::default(); + for (label, at) in &self.marks { + metrics.samples.push(MemoryPoint { + label: label.clone(), + timestamp: at.duration_since(self.start), + allocated_bytes: 0, + deallocated_bytes: 0, + current_bytes: 0, + peak_bytes: 0, + }); + } + metrics } pub fn points(&self) -> Vec {