Skip to main content

profile_inspect/parser/
cpu_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::CpuProfile;
7
8use super::ParseError;
9
10/// Parser for V8 CPU profiles
11pub struct CpuProfileParser {
12    classifier: FrameClassifier,
13}
14
15impl CpuProfileParser {
16    /// Create a new CPU profile parser
17    pub fn new(classifier: FrameClassifier) -> Self {
18        Self { classifier }
19    }
20
21    /// Parse a CPU 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 CPU 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 = CpuProfile::from_json(json)?;
41        Ok(self.convert_to_ir(&profile, source_file))
42    }
43
44    /// Convert a raw CPU profile to the intermediate representation
45    #[expect(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
46    fn convert_to_ir(&self, profile: &CpuProfile, source_file: Option<String>) -> ProfileIR {
47        // Build parent map: node_id -> parent_id
48        let parent_map = Self::build_parent_map(profile);
49
50        // Convert nodes to frames
51        let mut frames = Vec::with_capacity(profile.nodes.len());
52        let mut node_to_frame: HashMap<u32, FrameId> = HashMap::new();
53
54        for node in &profile.nodes {
55            let frame_id = FrameId(node.id);
56            let cf = &node.call_frame;
57
58            let (kind, category) = self.classifier.classify(&cf.url, &cf.function_name);
59
60            let frame = Frame::new(
61                frame_id,
62                cf.function_name.clone(),
63                if cf.url.is_empty() {
64                    None
65                } else {
66                    Some(cf.url.clone())
67                },
68                if cf.line_number >= 0 {
69                    Some((cf.line_number + 1) as u32) // Convert to 1-based
70                } else {
71                    None
72                },
73                if cf.column_number >= 0 {
74                    Some((cf.column_number + 1) as u32) // Convert to 1-based
75                } else {
76                    None
77                },
78                kind,
79                category,
80            );
81
82            node_to_frame.insert(node.id, frame_id);
83            frames.push(frame);
84        }
85
86        // Build unique stacks from samples
87        let mut stack_map: HashMap<Vec<FrameId>, StackId> = HashMap::new();
88        let mut stacks = Vec::new();
89        let mut samples = Vec::with_capacity(profile.samples.len());
90
91        let mut timestamp_us: u64 = 0;
92
93        for (i, &sample_node_id) in profile.samples.iter().enumerate() {
94            // Get time delta for this sample
95            let time_delta = profile.time_deltas.get(i).copied().unwrap_or(0).max(0) as u64;
96
97            // Reconstruct the stack from leaf to root, then reverse
98            let stack_frames = Self::reconstruct_stack(sample_node_id, &parent_map, &node_to_frame);
99
100            // Get or create stack ID
101            let stack_id = if let Some(&id) = stack_map.get(&stack_frames) {
102                id
103            } else {
104                let id = StackId(stacks.len() as u32);
105                stack_map.insert(stack_frames.clone(), id);
106                stacks.push(Stack::new(id, stack_frames));
107                id
108            };
109
110            samples.push(Sample::new(timestamp_us, stack_id, time_delta));
111            timestamp_us += time_delta;
112        }
113
114        ProfileIR::new_cpu(frames, stacks, samples, profile.duration_us(), source_file)
115    }
116
117    /// Build a map from node ID to parent node ID
118    fn build_parent_map(profile: &CpuProfile) -> HashMap<u32, u32> {
119        let mut parent_map = HashMap::new();
120
121        for node in &profile.nodes {
122            for &child_id in &node.children {
123                parent_map.insert(child_id, node.id);
124            }
125        }
126
127        parent_map
128    }
129
130    /// Reconstruct the call stack for a sample node
131    ///
132    /// Returns frames in root-to-leaf order (caller to callee)
133    fn reconstruct_stack(
134        leaf_node_id: u32,
135        parent_map: &HashMap<u32, u32>,
136        node_to_frame: &HashMap<u32, FrameId>,
137    ) -> Vec<FrameId> {
138        let mut stack = Vec::new();
139        let mut current_node = leaf_node_id;
140
141        // Walk from leaf to root
142        loop {
143            if let Some(&frame_id) = node_to_frame.get(&current_node) {
144                stack.push(frame_id);
145            }
146
147            if let Some(&parent_id) = parent_map.get(&current_node) {
148                current_node = parent_id;
149            } else {
150                break;
151            }
152        }
153
154        // Reverse to get root-to-leaf order
155        stack.reverse();
156        stack
157    }
158}
159
160impl Default for CpuProfileParser {
161    fn default() -> Self {
162        Self::new(FrameClassifier::default())
163    }
164}