Skip to main content

profile_inspect/parser/
heap_profile.rs

1use std::collections::HashMap;
2use std::path::Path;
3
4use crate::classify::FrameClassifier;
5use crate::ir::{Frame, FrameId, ProfileIR, Sample, Stack, StackId};
6use crate::types::{HeapProfile, HeapProfileNode};
7
8use super::ParseError;
9
10/// Parser for V8 heap profiles
11pub struct HeapProfileParser {
12    classifier: FrameClassifier,
13}
14
15impl HeapProfileParser {
16    /// Create a new heap profile parser
17    pub fn new(classifier: FrameClassifier) -> Self {
18        Self { classifier }
19    }
20
21    /// Parse a heap profile from a file path
22    ///
23    /// # Errors
24    /// Returns an error if the file cannot be read or parsed.
25    pub fn parse_file(&self, path: &Path) -> Result<ProfileIR, ParseError> {
26        let content = std::fs::read_to_string(path)?;
27        let source_file = path.file_name().map(|s| s.to_string_lossy().to_string());
28        self.parse_str(&content, source_file)
29    }
30
31    /// Parse a heap profile from a JSON string
32    ///
33    /// # Errors
34    /// Returns an error if the JSON is invalid.
35    pub fn parse_str(
36        &self,
37        json: &str,
38        source_file: Option<String>,
39    ) -> Result<ProfileIR, ParseError> {
40        let profile = HeapProfile::from_json(json)?;
41        Ok(self.convert_to_ir(&profile, source_file))
42    }
43
44    /// Convert a raw heap profile to the intermediate representation
45    #[expect(clippy::cast_possible_truncation)]
46    fn convert_to_ir(&self, profile: &HeapProfile, source_file: Option<String>) -> ProfileIR {
47        let mut frames = Vec::new();
48        let mut stacks = Vec::new();
49        let mut samples = Vec::new();
50
51        let mut node_to_frame: HashMap<u32, FrameId> = HashMap::new();
52        let mut stack_map: HashMap<Vec<FrameId>, StackId> = HashMap::new();
53
54        // Walk the tree and collect all allocations
55        let mut next_frame_id = 0u32;
56        self.walk_tree(
57            &profile.head,
58            &[],
59            &mut frames,
60            &mut stacks,
61            &mut samples,
62            &mut node_to_frame,
63            &mut stack_map,
64            &mut next_frame_id,
65        );
66
67        // Also process explicit samples if present
68        for sample in &profile.samples {
69            // Find the node and its stack
70            if let Some(&frame_id) = node_to_frame.get(&sample.node_id) {
71                // Try to find or create a stack containing just this frame
72                // (samples often reference specific allocation points)
73                let stack_frames = vec![frame_id];
74                let stack_id = if let Some(&id) = stack_map.get(&stack_frames) {
75                    id
76                } else {
77                    let id = StackId(stacks.len() as u32);
78                    stack_map.insert(stack_frames.clone(), id);
79                    stacks.push(Stack::new(id, stack_frames));
80                    id
81                };
82
83                samples.push(Sample::new(
84                    u64::from(sample.ordinal),
85                    stack_id,
86                    sample.size,
87                ));
88            }
89        }
90
91        ProfileIR::new_heap(frames, stacks, samples, source_file)
92    }
93
94    /// Recursively walk the heap profile tree
95    #[expect(
96        clippy::too_many_arguments,
97        clippy::cast_possible_truncation,
98        clippy::cast_sign_loss
99    )]
100    fn walk_tree(
101        &self,
102        node: &HeapProfileNode,
103        parent_stack: &[FrameId],
104        frames: &mut Vec<Frame>,
105        stacks: &mut Vec<Stack>,
106        samples: &mut Vec<Sample>,
107        node_to_frame: &mut HashMap<u32, FrameId>,
108        stack_map: &mut HashMap<Vec<FrameId>, StackId>,
109        next_frame_id: &mut u32,
110    ) {
111        let cf = &node.call_frame;
112
113        // Create frame for this node
114        let frame_id = FrameId(*next_frame_id);
115        *next_frame_id += 1;
116
117        let (kind, category) = self.classifier.classify(&cf.url, &cf.function_name);
118
119        let frame = Frame::new(
120            frame_id,
121            cf.function_name.clone(),
122            if cf.url.is_empty() {
123                None
124            } else {
125                Some(cf.url.clone())
126            },
127            if cf.line_number > 0 {
128                Some(cf.line_number as u32)
129            } else {
130                None
131            },
132            if cf.column_number > 0 {
133                Some(cf.column_number as u32)
134            } else {
135                None
136            },
137            kind,
138            category,
139        );
140
141        node_to_frame.insert(node.id, frame_id);
142        frames.push(frame);
143
144        // Build current stack (parent frames + this frame)
145        let mut current_stack: Vec<FrameId> = parent_stack.to_vec();
146        current_stack.push(frame_id);
147
148        // If this node has self_size, record it as a sample
149        if node.self_size > 0 {
150            let stack_id = if let Some(&id) = stack_map.get(&current_stack) {
151                id
152            } else {
153                let id = StackId(stacks.len() as u32);
154                stack_map.insert(current_stack.clone(), id);
155                stacks.push(Stack::new(id, current_stack.clone()));
156                id
157            };
158
159            samples.push(Sample::new(0, stack_id, node.self_size));
160        }
161
162        // Recurse into children
163        for child in &node.children {
164            self.walk_tree(
165                child,
166                &current_stack,
167                frames,
168                stacks,
169                samples,
170                node_to_frame,
171                stack_map,
172                next_frame_id,
173            );
174        }
175    }
176}
177
178impl Default for HeapProfileParser {
179    fn default() -> Self {
180        Self::new(FrameClassifier::default())
181    }
182}