profile-inspect 0.1.3

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

use crate::classify::FrameClassifier;
use crate::ir::{Frame, FrameId, ProfileIR, Sample, Stack, StackId};
use crate::types::CpuProfile;

use super::ParseError;

/// Parser for V8 CPU profiles
pub struct CpuProfileParser {
    classifier: FrameClassifier,
}

impl CpuProfileParser {
    /// Create a new CPU profile parser
    pub fn new(classifier: FrameClassifier) -> Self {
        Self { classifier }
    }

    /// Parse a CPU profile from a file path
    ///
    /// # Errors
    /// Returns an error if the file cannot be read or parsed.
    pub fn parse_file(&self, path: &Path) -> Result<ProfileIR, ParseError> {
        let content = std::fs::read_to_string(path)?;
        let source_file = path.file_name().map(|s| s.to_string_lossy().to_string());
        self.parse_str(&content, source_file)
    }

    /// Parse a CPU profile from a JSON string
    ///
    /// # Errors
    /// Returns an error if the JSON is invalid.
    pub fn parse_str(
        &self,
        json: &str,
        source_file: Option<String>,
    ) -> Result<ProfileIR, ParseError> {
        let profile = CpuProfile::from_json(json)?;
        Ok(self.convert_to_ir(&profile, source_file))
    }

    /// Convert a raw CPU profile to the intermediate representation
    #[expect(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
    fn convert_to_ir(&self, profile: &CpuProfile, source_file: Option<String>) -> ProfileIR {
        // Build parent map: node_id -> parent_id
        let parent_map = Self::build_parent_map(profile);

        // Convert nodes to frames
        let mut frames = Vec::with_capacity(profile.nodes.len());
        let mut node_to_frame: HashMap<u32, FrameId> = HashMap::new();

        for node in &profile.nodes {
            let frame_id = FrameId(node.id);
            let cf = &node.call_frame;

            let (kind, category) = self.classifier.classify(&cf.url, &cf.function_name);

            let frame = Frame::new(
                frame_id,
                cf.function_name.clone(),
                if cf.url.is_empty() {
                    None
                } else {
                    Some(cf.url.clone())
                },
                if cf.line_number >= 0 {
                    Some((cf.line_number + 1) as u32) // Convert to 1-based
                } else {
                    None
                },
                if cf.column_number >= 0 {
                    Some((cf.column_number + 1) as u32) // Convert to 1-based
                } else {
                    None
                },
                kind,
                category,
            );

            node_to_frame.insert(node.id, frame_id);
            frames.push(frame);
        }

        // Build unique stacks from samples
        let mut stack_map: HashMap<Vec<FrameId>, StackId> = HashMap::new();
        let mut stacks = Vec::new();
        let mut samples = Vec::with_capacity(profile.samples.len());

        let mut timestamp_us: u64 = 0;

        for (i, &sample_node_id) in profile.samples.iter().enumerate() {
            // Get time delta for this sample
            let time_delta = profile.time_deltas.get(i).copied().unwrap_or(0).max(0) as u64;

            // Reconstruct the stack from leaf to root, then reverse
            let stack_frames = Self::reconstruct_stack(sample_node_id, &parent_map, &node_to_frame);

            // Get or create stack ID
            let stack_id = if let Some(&id) = stack_map.get(&stack_frames) {
                id
            } else {
                let id = StackId(stacks.len() as u32);
                stack_map.insert(stack_frames.clone(), id);
                stacks.push(Stack::new(id, stack_frames));
                id
            };

            samples.push(Sample::new(timestamp_us, stack_id, time_delta));
            timestamp_us += time_delta;
        }

        ProfileIR::new_cpu(frames, stacks, samples, profile.duration_us(), source_file)
    }

    /// Build a map from node ID to parent node ID
    fn build_parent_map(profile: &CpuProfile) -> HashMap<u32, u32> {
        let mut parent_map = HashMap::new();

        for node in &profile.nodes {
            for &child_id in &node.children {
                parent_map.insert(child_id, node.id);
            }
        }

        parent_map
    }

    /// Reconstruct the call stack for a sample node
    ///
    /// Returns frames in root-to-leaf order (caller to callee)
    fn reconstruct_stack(
        leaf_node_id: u32,
        parent_map: &HashMap<u32, u32>,
        node_to_frame: &HashMap<u32, FrameId>,
    ) -> Vec<FrameId> {
        let mut stack = Vec::new();
        let mut current_node = leaf_node_id;

        // Walk from leaf to root
        loop {
            if let Some(&frame_id) = node_to_frame.get(&current_node) {
                stack.push(frame_id);
            }

            if let Some(&parent_id) = parent_map.get(&current_node) {
                current_node = parent_id;
            } else {
                break;
            }
        }

        // Reverse to get root-to-leaf order
        stack.reverse();
        stack
    }
}

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