profile-inspect 0.1.2

Analyze V8 CPU and heap profiles from Node.js/Chrome DevTools
Documentation
use std::collections::HashMap;

use crate::ir::ProfileIR;

use super::{CpuAnalyzer, FunctionStats};

/// Delta between two profile measurements for a function
#[derive(Debug, Clone)]
pub struct FunctionDelta {
    /// Function name
    pub name: String,
    /// Source location
    pub location: String,
    /// Time in the "before" profile (microseconds)
    pub before_time: u64,
    /// Time in the "after" profile (microseconds)
    pub after_time: u64,
    /// Absolute change in time
    pub delta_time: i64,
    /// Percentage change (positive = regression, negative = improvement)
    pub delta_percent: f64,
}

impl FunctionDelta {
    /// Check if this is a regression (got slower)
    pub fn is_regression(&self) -> bool {
        self.delta_time > 0
    }

    /// Check if this is an improvement (got faster)
    pub fn is_improvement(&self) -> bool {
        self.delta_time < 0
    }
}

/// Result of comparing two profiles
#[derive(Debug)]
pub struct ProfileDiff {
    /// Total time in "before" profile
    pub before_total: u64,
    /// Total time in "after" profile
    pub after_total: u64,
    /// Overall change percentage
    pub overall_delta_percent: f64,
    /// Functions that got slower
    pub regressions: Vec<FunctionDelta>,
    /// Functions that got faster
    pub improvements: Vec<FunctionDelta>,
    /// New hotspots (functions that didn't exist in "before")
    pub new_functions: Vec<FunctionStats>,
    /// Removed functions (functions that don't exist in "after")
    pub removed_functions: Vec<FunctionStats>,
}

/// Compare two CPU profiles to find regressions and improvements
pub struct ProfileDiffer {
    /// Minimum percentage change to report
    min_delta_percent: f64,
    /// Maximum number of results per category
    top_n: usize,
}

impl ProfileDiffer {
    /// Create a new differ with default settings
    pub fn new() -> Self {
        Self {
            min_delta_percent: 1.0, // 1% minimum change
            top_n: 20,
        }
    }

    /// Set minimum percentage change threshold
    pub fn min_delta_percent(mut self, percent: f64) -> Self {
        self.min_delta_percent = percent;
        self
    }

    /// Set maximum number of results
    pub fn top_n(mut self, n: usize) -> Self {
        self.top_n = n;
        self
    }

    /// Compare two profiles
    #[expect(clippy::cast_precision_loss)]
    pub fn diff(&self, before: &ProfileIR, after: &ProfileIR) -> ProfileDiff {
        let analyzer = CpuAnalyzer::new().include_internals(true).top_n(1000);

        let before_analysis = analyzer.analyze(before);
        let after_analysis = analyzer.analyze(after);

        let before_total = before_analysis.total_time;
        let after_total = after_analysis.total_time;

        let overall_delta_percent = if before_total > 0 {
            ((after_total as f64 - before_total as f64) / before_total as f64) * 100.0
        } else {
            0.0
        };

        // Build maps by function key (name + location)
        let before_map: HashMap<String, &FunctionStats> = before_analysis
            .functions
            .iter()
            .map(|f| (format!("{}@{}", f.name, f.location), f))
            .collect();

        let after_map: HashMap<String, &FunctionStats> = after_analysis
            .functions
            .iter()
            .map(|f| (format!("{}@{}", f.name, f.location), f))
            .collect();

        let mut regressions = Vec::new();
        let mut improvements = Vec::new();
        let mut new_functions = Vec::new();
        let mut removed_functions = Vec::new();

        // Find changes and new functions
        for (key, after_stats) in &after_map {
            if let Some(before_stats) = before_map.get(key) {
                // Function exists in both - calculate delta
                let delta_time = after_stats.self_time as i64 - before_stats.self_time as i64;
                let delta_percent = if before_stats.self_time > 0 {
                    (delta_time as f64 / before_stats.self_time as f64) * 100.0
                } else if after_stats.self_time > 0 {
                    100.0 // New time from 0
                } else {
                    0.0
                };

                if delta_percent.abs() >= self.min_delta_percent {
                    let delta = FunctionDelta {
                        name: after_stats.name.clone(),
                        location: after_stats.location.clone(),
                        before_time: before_stats.self_time,
                        after_time: after_stats.self_time,
                        delta_time,
                        delta_percent,
                    };

                    if delta.is_regression() {
                        regressions.push(delta);
                    } else if delta.is_improvement() {
                        improvements.push(delta);
                    }
                }
            } else {
                // New function
                new_functions.push((*after_stats).clone());
            }
        }

        // Find removed functions
        for (key, before_stats) in &before_map {
            if !after_map.contains_key(key) {
                removed_functions.push((*before_stats).clone());
            }
        }

        // Sort and truncate
        regressions.sort_by(|a, b| {
            b.delta_time
                .abs()
                .partial_cmp(&a.delta_time.abs())
                .unwrap_or(std::cmp::Ordering::Equal)
        });
        improvements.sort_by(|a, b| {
            b.delta_time
                .abs()
                .partial_cmp(&a.delta_time.abs())
                .unwrap_or(std::cmp::Ordering::Equal)
        });
        new_functions.sort_by(|a, b| b.self_time.cmp(&a.self_time));
        removed_functions.sort_by(|a, b| b.self_time.cmp(&a.self_time));

        regressions.truncate(self.top_n);
        improvements.truncate(self.top_n);
        new_functions.truncate(self.top_n);
        removed_functions.truncate(self.top_n);

        ProfileDiff {
            before_total,
            after_total,
            overall_delta_percent,
            regressions,
            improvements,
            new_functions,
            removed_functions,
        }
    }
}

impl Default for ProfileDiffer {
    fn default() -> Self {
        Self::new()
    }
}