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;
pub struct CpuProfileParser {
classifier: FrameClassifier,
}
impl CpuProfileParser {
pub fn new(classifier: FrameClassifier) -> Self {
Self { classifier }
}
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)
}
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))
}
#[expect(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
fn convert_to_ir(&self, profile: &CpuProfile, source_file: Option<String>) -> ProfileIR {
let parent_map = Self::build_parent_map(profile);
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) } else {
None
},
if cf.column_number >= 0 {
Some((cf.column_number + 1) as u32) } else {
None
},
kind,
category,
);
node_to_frame.insert(node.id, frame_id);
frames.push(frame);
}
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() {
let time_delta = profile.time_deltas.get(i).copied().unwrap_or(0).max(0) as u64;
let stack_frames = Self::reconstruct_stack(sample_node_id, &parent_map, &node_to_frame);
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)
}
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
}
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;
loop {
if let Some(&frame_id) = node_to_frame.get(¤t_node) {
stack.push(frame_id);
}
if let Some(&parent_id) = parent_map.get(¤t_node) {
current_node = parent_id;
} else {
break;
}
}
stack.reverse();
stack
}
}
impl Default for CpuProfileParser {
fn default() -> Self {
Self::new(FrameClassifier::default())
}
}