profile-inspect 0.1.3

Analyze V8 CPU and heap profiles from Node.js/Chrome DevTools
Documentation
use crate::ir::{FrameCategory, FrameKind};

/// Classifies frames based on their URL and function name
pub struct FrameClassifier {
    /// Base path for the application (to distinguish app from deps)
    app_base_path: Option<String>,
}

impl FrameClassifier {
    /// Create a new classifier with an optional app base path
    pub fn new(app_base_path: Option<String>) -> Self {
        Self { app_base_path }
    }

    /// Classify the kind of frame based on function name
    pub fn classify_kind(&self, name: &str, url: &str) -> FrameKind {
        // V8 internal markers
        if name.starts_with('(') && name.ends_with(')') {
            return match name {
                "(garbage collector)" => FrameKind::GC,
                "(idle)" => FrameKind::Idle,
                "(program)" => FrameKind::Program,
                _ => FrameKind::Native,
            };
        }

        // Native builtins
        if name.contains("Builtin:") || name == "(native)" {
            return FrameKind::Builtin;
        }

        // Eval code
        if url.contains("eval at") || name.contains("eval") {
            return FrameKind::Eval;
        }

        // WebAssembly
        if url.starts_with("wasm://") || name.starts_with("wasm-") {
            return FrameKind::Wasm;
        }

        // Regular expressions
        if name.starts_with("RegExp:") {
            return FrameKind::RegExp;
        }

        // Native code (no URL)
        if url.is_empty() && !name.is_empty() {
            return FrameKind::Native;
        }

        // Default to regular function
        FrameKind::Function
    }

    /// Classify the category of a frame for filtering
    pub fn classify_category(&self, url: &str, name: &str) -> FrameCategory {
        // Node.js internals
        if url.starts_with("node:") || url.contains("internal/") {
            return FrameCategory::NodeInternal;
        }

        // V8 internals (special names)
        if name.starts_with('(') && name.ends_with(')') {
            match name {
                "(garbage collector)" | "(idle)" | "(program)" | "(root)" => {
                    return FrameCategory::V8Internal;
                }
                _ => {}
            }
        }

        // Native/Builtin code
        if url.is_empty() || name.contains("Builtin:") || name == "(native)" {
            return FrameCategory::Native;
        }

        // Dependencies (node_modules)
        if url.contains("node_modules") {
            return FrameCategory::Deps;
        }

        // Check if it's outside the app base path (likely a dep)
        if let Some(base) = &self.app_base_path
            && !url.is_empty()
            && !url.starts_with(base)
            && !url.starts_with("file://")
            // Could be a bundled dependency
            && (url.contains("/dist/") || url.contains("/build/"))
        {
            return FrameCategory::App; // Likely bundled app code
        }

        // Default to application code
        FrameCategory::App
    }

    /// Classify both kind and category at once
    pub fn classify(&self, url: &str, name: &str) -> (FrameKind, FrameCategory) {
        (
            self.classify_kind(name, url),
            self.classify_category(url, name),
        )
    }
}

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

#[cfg(test)]
mod tests {
    use super::*;

    // ========================================================================
    // FrameKind Classification Tests
    // ========================================================================

    #[test]
    fn test_gc_classification() {
        let classifier = FrameClassifier::default();
        assert_eq!(
            classifier.classify_kind("(garbage collector)", ""),
            FrameKind::GC
        );
        assert_eq!(
            classifier.classify_category("", "(garbage collector)"),
            FrameCategory::V8Internal
        );
    }

    #[test]
    fn test_idle_classification() {
        let classifier = FrameClassifier::default();
        assert_eq!(classifier.classify_kind("(idle)", ""), FrameKind::Idle);
        assert_eq!(
            classifier.classify_category("", "(idle)"),
            FrameCategory::V8Internal
        );
    }

    #[test]
    fn test_program_classification() {
        let classifier = FrameClassifier::default();
        assert_eq!(
            classifier.classify_kind("(program)", ""),
            FrameKind::Program
        );
        assert_eq!(
            classifier.classify_category("", "(program)"),
            FrameCategory::V8Internal
        );
    }

    #[test]
    fn test_root_classification() {
        let classifier = FrameClassifier::default();
        assert_eq!(
            classifier.classify_category("", "(root)"),
            FrameCategory::V8Internal
        );
    }

    #[test]
    fn test_builtin_classification() {
        let classifier = FrameClassifier::default();

        assert_eq!(
            classifier.classify_kind("Builtin:ArrayPush", ""),
            FrameKind::Builtin
        );
        // Note: (native) matches the parenthesized pattern first, so it's Native
        // Only names containing "Builtin:" are classified as Builtin
        assert_eq!(classifier.classify_kind("(native)", ""), FrameKind::Native);
    }

    #[test]
    fn test_eval_classification() {
        let classifier = FrameClassifier::default();

        assert_eq!(
            classifier.classify_kind("anonymous", "eval at <anonymous>"),
            FrameKind::Eval
        );
        assert_eq!(
            classifier.classify_kind("eval", "/src/main.js"),
            FrameKind::Eval
        );
    }

    #[test]
    fn test_wasm_classification() {
        let classifier = FrameClassifier::default();

        assert_eq!(
            classifier.classify_kind("funcName", "wasm://wasm/123456"),
            FrameKind::Wasm
        );
        assert_eq!(
            classifier.classify_kind("wasm-function[42]", ""),
            FrameKind::Wasm
        );
    }

    #[test]
    fn test_regexp_classification() {
        let classifier = FrameClassifier::default();
        assert_eq!(
            classifier.classify_kind("RegExp: /\\d+/", ""),
            FrameKind::RegExp
        );
    }

    #[test]
    fn test_native_no_url() {
        let classifier = FrameClassifier::default();
        // Function with name but no URL is native
        assert_eq!(
            classifier.classify_kind("nativeFunction", ""),
            FrameKind::Native
        );
    }

    #[test]
    fn test_regular_function() {
        let classifier = FrameClassifier::default();
        assert_eq!(
            classifier.classify_kind("myFunction", "/src/main.js"),
            FrameKind::Function
        );
    }

    // ========================================================================
    // FrameCategory Classification Tests
    // ========================================================================

    #[test]
    fn test_node_internal() {
        let classifier = FrameClassifier::default();
        assert_eq!(
            classifier.classify_category("node:fs", "readFile"),
            FrameCategory::NodeInternal
        );
        assert_eq!(
            classifier.classify_category("node:internal/modules/cjs/loader", "load"),
            FrameCategory::NodeInternal
        );
        assert_eq!(
            classifier.classify_category("/internal/bootstrap.js", "startup"),
            FrameCategory::NodeInternal
        );
    }

    #[test]
    fn test_deps_classification() {
        let classifier = FrameClassifier::default();
        assert_eq!(
            classifier.classify_category("/project/node_modules/lodash/index.js", "map"),
            FrameCategory::Deps
        );
        assert_eq!(
            classifier.classify_category("/node_modules/@babel/core/lib/index.js", "transform"),
            FrameCategory::Deps
        );
        assert_eq!(
            classifier.classify_category(
                "file:///Users/test/project/node_modules/vitest/dist/index.js",
                "run"
            ),
            FrameCategory::Deps
        );
    }

    #[test]
    fn test_app_code() {
        let classifier = FrameClassifier::default();
        assert_eq!(
            classifier.classify_category("/project/src/main.ts", "processData"),
            FrameCategory::App
        );
        assert_eq!(
            classifier.classify_category("file:///Users/test/project/src/utils.js", "helper"),
            FrameCategory::App
        );
    }

    #[test]
    fn test_native_category() {
        let classifier = FrameClassifier::default();

        // Empty URL defaults to Native
        assert_eq!(
            classifier.classify_category("", "someFunction"),
            FrameCategory::Native
        );

        // Builtins are Native
        assert_eq!(
            classifier.classify_category("", "Builtin:ArrayPush"),
            FrameCategory::Native
        );
    }

    // ========================================================================
    // Combined classify() Tests
    // ========================================================================

    #[test]
    fn test_classify_combined() {
        let classifier = FrameClassifier::default();

        let (kind, category) = classifier.classify("", "(garbage collector)");
        assert_eq!(kind, FrameKind::GC);
        assert_eq!(category, FrameCategory::V8Internal);

        let (kind, category) = classifier.classify("/src/main.js", "processData");
        assert_eq!(kind, FrameKind::Function);
        assert_eq!(category, FrameCategory::App);

        let (kind, category) = classifier.classify("node:fs", "readFile");
        assert_eq!(kind, FrameKind::Function);
        assert_eq!(category, FrameCategory::NodeInternal);
    }

    // ========================================================================
    // Custom App Base Path Tests
    // ========================================================================

    #[test]
    fn test_custom_app_base_path() {
        let classifier = FrameClassifier::new(Some("/home/user/myproject".to_string()));

        // Within base path is App
        assert_eq!(
            classifier.classify_category("/home/user/myproject/src/main.ts", "func"),
            FrameCategory::App
        );

        // node_modules is still Deps
        assert_eq!(
            classifier
                .classify_category("/home/user/myproject/node_modules/lodash/index.js", "map"),
            FrameCategory::Deps
        );
    }
}