profile_inspect/parser/
cpu_profile.rs1use 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
10pub struct CpuProfileParser {
12 classifier: FrameClassifier,
13}
14
15impl CpuProfileParser {
16 pub fn new(classifier: FrameClassifier) -> Self {
18 Self { classifier }
19 }
20
21 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 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 #[expect(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
46 fn convert_to_ir(&self, profile: &CpuProfile, source_file: Option<String>) -> ProfileIR {
47 let parent_map = Self::build_parent_map(profile);
49
50 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) } else {
71 None
72 },
73 if cf.column_number >= 0 {
74 Some((cf.column_number + 1) as u32) } 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 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 let time_delta = profile.time_deltas.get(i).copied().unwrap_or(0).max(0) as u64;
96
97 let stack_frames = Self::reconstruct_stack(sample_node_id, &parent_map, &node_to_frame);
99
100 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 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 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 loop {
143 if let Some(&frame_id) = node_to_frame.get(¤t_node) {
144 stack.push(frame_id);
145 }
146
147 if let Some(&parent_id) = parent_map.get(¤t_node) {
148 current_node = parent_id;
149 } else {
150 break;
151 }
152 }
153
154 stack.reverse();
156 stack
157 }
158}
159
160impl Default for CpuProfileParser {
161 fn default() -> Self {
162 Self::new(FrameClassifier::default())
163 }
164}