profile-inspect 0.1.3

Analyze V8 CPU and heap profiles from Node.js/Chrome DevTools
Documentation
use serde::Serialize;

/// Unique identifier for a frame
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize)]
pub struct FrameId(pub u32);

/// Classification of what kind of code a frame represents
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum FrameKind {
    /// Regular JavaScript/TypeScript function
    Function,
    /// Native C++ code
    Native,
    /// Garbage collection
    GC,
    /// eval() code
    Eval,
    /// WebAssembly code
    Wasm,
    /// V8 builtin function
    Builtin,
    /// Regular expression execution
    RegExp,
    /// CPU idle time
    Idle,
    /// Program root
    Program,
    /// Unknown frame type
    Unknown,
}

/// Category for filtering and grouping frames
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum FrameCategory {
    /// Application code (user's code)
    App,
    /// Third-party dependencies (node_modules)
    Deps,
    /// Node.js internal modules
    NodeInternal,
    /// V8 engine internals
    V8Internal,
    /// Native/builtin code
    Native,
}

impl FrameCategory {
    /// Check if this category represents "internal" code (Node/V8/Native)
    pub fn is_internal(&self) -> bool {
        matches!(self, Self::NodeInternal | Self::V8Internal | Self::Native)
    }
}

/// Normalized frame representation used across all analyses
#[derive(Debug, Clone, Serialize)]
pub struct Frame {
    /// Unique identifier for this frame
    pub id: FrameId,

    /// Function name (resolved via sourcemap if available)
    pub name: String,

    /// Source file path
    pub file: Option<String>,

    /// Line number (1-based)
    pub line: Option<u32>,

    /// Column number (1-based)
    pub col: Option<u32>,

    /// What kind of frame this is
    pub kind: FrameKind,

    /// Category for filtering
    pub category: FrameCategory,

    /// Original minified name if resolved via sourcemap
    pub minified_name: Option<String>,

    /// Original minified location if resolved via sourcemap
    pub minified_location: Option<String>,
}

impl Frame {
    /// Create a new frame with the given properties
    pub fn new(
        id: FrameId,
        name: String,
        file: Option<String>,
        line: Option<u32>,
        col: Option<u32>,
        kind: FrameKind,
        category: FrameCategory,
    ) -> Self {
        Self {
            id,
            name,
            file,
            line,
            col,
            kind,
            category,
            minified_name: None,
            minified_location: None,
        }
    }

    /// Get a display-friendly location string
    pub fn location(&self) -> String {
        match (&self.file, self.line, self.col) {
            (Some(file), Some(line), Some(col)) => {
                format!("{}:{line}:{col}", Self::clean_file_path(file))
            }
            (Some(file), Some(line), None) => {
                format!("{}:{line}", Self::clean_file_path(file))
            }
            (Some(file), None, None) => Self::clean_file_path(file),
            _ => "(unknown)".to_string(),
        }
    }

    /// Clean a file path for display (strip file:// prefix, etc.)
    fn clean_file_path(path: &str) -> String {
        path.strip_prefix("file://").unwrap_or(path).to_string()
    }

    /// Get the cleaned file path (without file:// prefix)
    pub fn clean_file(&self) -> Option<String> {
        self.file.as_ref().map(|f| Self::clean_file_path(f))
    }

    /// Get a short display name (function name or fallback)
    ///
    /// For anonymous functions, uses a location-based name if available.
    pub fn display_name(&self) -> String {
        if !self.name.is_empty() && self.name != "(anonymous)" {
            return self.name.clone();
        }

        // For anonymous functions with known location, create a descriptive name
        if let Some(file) = &self.file {
            // Clean the path and extract just the filename
            let clean_path = Self::clean_file_path(file);
            let filename = std::path::Path::new(&clean_path)
                .file_name()
                .map(|s| s.to_string_lossy())
                .unwrap_or_else(|| clean_path.as_str().into());

            if let Some(line) = self.line {
                return format!("(anonymous @ {filename}:{line})");
            }
            return format!("(anonymous @ {filename})");
        }

        "(anonymous)".to_string()
    }
}

impl std::fmt::Display for FrameKind {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Function => write!(f, "function"),
            Self::Native => write!(f, "native"),
            Self::GC => write!(f, "gc"),
            Self::Eval => write!(f, "eval"),
            Self::Wasm => write!(f, "wasm"),
            Self::Builtin => write!(f, "builtin"),
            Self::RegExp => write!(f, "regexp"),
            Self::Idle => write!(f, "idle"),
            Self::Program => write!(f, "program"),
            Self::Unknown => write!(f, "unknown"),
        }
    }
}

impl std::fmt::Display for FrameCategory {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::App => write!(f, "App"),
            Self::Deps => write!(f, "Dependencies"),
            Self::NodeInternal => write!(f, "Node.js Internal"),
            Self::V8Internal => write!(f, "V8 Internal"),
            Self::Native => write!(f, "Native"),
        }
    }
}