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::{HeapProfile, HeapProfileNode};

use super::ParseError;

/// Parser for V8 heap profiles
pub struct HeapProfileParser {
    classifier: FrameClassifier,
}

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

    /// Parse a heap 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 heap 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 = HeapProfile::from_json(json)?;
        Ok(self.convert_to_ir(&profile, source_file))
    }

    /// Convert a raw heap profile to the intermediate representation
    #[expect(clippy::cast_possible_truncation)]
    fn convert_to_ir(&self, profile: &HeapProfile, source_file: Option<String>) -> ProfileIR {
        let mut frames = Vec::new();
        let mut stacks = Vec::new();
        let mut samples = Vec::new();

        let mut node_to_frame: HashMap<u32, FrameId> = HashMap::new();
        let mut stack_map: HashMap<Vec<FrameId>, StackId> = HashMap::new();

        // Walk the tree and collect all allocations
        let mut next_frame_id = 0u32;
        self.walk_tree(
            &profile.head,
            &[],
            &mut frames,
            &mut stacks,
            &mut samples,
            &mut node_to_frame,
            &mut stack_map,
            &mut next_frame_id,
        );

        // Also process explicit samples if present
        for sample in &profile.samples {
            // Find the node and its stack
            if let Some(&frame_id) = node_to_frame.get(&sample.node_id) {
                // Try to find or create a stack containing just this frame
                // (samples often reference specific allocation points)
                let stack_frames = vec![frame_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(
                    u64::from(sample.ordinal),
                    stack_id,
                    sample.size,
                ));
            }
        }

        ProfileIR::new_heap(frames, stacks, samples, source_file)
    }

    /// Recursively walk the heap profile tree
    #[expect(
        clippy::too_many_arguments,
        clippy::cast_possible_truncation,
        clippy::cast_sign_loss
    )]
    fn walk_tree(
        &self,
        node: &HeapProfileNode,
        parent_stack: &[FrameId],
        frames: &mut Vec<Frame>,
        stacks: &mut Vec<Stack>,
        samples: &mut Vec<Sample>,
        node_to_frame: &mut HashMap<u32, FrameId>,
        stack_map: &mut HashMap<Vec<FrameId>, StackId>,
        next_frame_id: &mut u32,
    ) {
        let cf = &node.call_frame;

        // Create frame for this node
        let frame_id = FrameId(*next_frame_id);
        *next_frame_id += 1;

        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 as u32)
            } else {
                None
            },
            if cf.column_number > 0 {
                Some(cf.column_number as u32)
            } else {
                None
            },
            kind,
            category,
        );

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

        // Build current stack (parent frames + this frame)
        let mut current_stack: Vec<FrameId> = parent_stack.to_vec();
        current_stack.push(frame_id);

        // If this node has self_size, record it as a sample
        if node.self_size > 0 {
            let stack_id = if let Some(&id) = stack_map.get(&current_stack) {
                id
            } else {
                let id = StackId(stacks.len() as u32);
                stack_map.insert(current_stack.clone(), id);
                stacks.push(Stack::new(id, current_stack.clone()));
                id
            };

            samples.push(Sample::new(0, stack_id, node.self_size));
        }

        // Recurse into children
        for child in &node.children {
            self.walk_tree(
                child,
                &current_stack,
                frames,
                stacks,
                samples,
                node_to_frame,
                stack_map,
                next_frame_id,
            );
        }
    }
}

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