Skip to main content

profile_inspect/output/
speedscope.rs

1use std::io::Write;
2
3use indexmap::IndexMap;
4use serde::Serialize;
5
6use crate::analysis::{CpuAnalysis, HeapAnalysis};
7use crate::ir::ProfileIR;
8
9use super::{Formatter, OutputError};
10
11/// Speedscope JSON formatter for visualization
12pub struct SpeedscopeFormatter;
13
14/// Speedscope file format
15/// See: https://www.speedscope.app/file-format-spec.pdf
16#[derive(Serialize)]
17#[serde(rename_all = "camelCase")]
18struct SpeedscopeFile {
19    #[serde(rename = "$schema")]
20    schema: &'static str,
21    shared: SpeedscopeShared,
22    profiles: Vec<SpeedscopeProfile>,
23    name: Option<String>,
24    active_profile_index: Option<usize>,
25    exporter: Option<String>,
26}
27
28#[derive(Serialize)]
29struct SpeedscopeShared {
30    frames: Vec<SpeedscopeFrame>,
31}
32
33#[derive(Serialize)]
34struct SpeedscopeFrame {
35    name: String,
36    #[serde(skip_serializing_if = "Option::is_none")]
37    file: Option<String>,
38    #[serde(skip_serializing_if = "Option::is_none")]
39    line: Option<u32>,
40    #[serde(skip_serializing_if = "Option::is_none")]
41    col: Option<u32>,
42}
43
44#[derive(Serialize)]
45#[serde(tag = "type")]
46enum SpeedscopeProfile {
47    #[serde(rename = "sampled")]
48    Sampled {
49        name: String,
50        unit: &'static str,
51        #[serde(rename = "startValue")]
52        start_value: u64,
53        #[serde(rename = "endValue")]
54        end_value: u64,
55        samples: Vec<Vec<usize>>,
56        weights: Vec<u64>,
57    },
58}
59
60impl Formatter for SpeedscopeFormatter {
61    fn write_cpu_analysis(
62        &self,
63        profile: &ProfileIR,
64        analysis: &CpuAnalysis,
65        writer: &mut dyn Write,
66    ) -> Result<(), OutputError> {
67        // Build frame index (map from FrameId to speedscope index)
68        let mut frame_to_index: IndexMap<crate::ir::FrameId, usize> = IndexMap::new();
69        let mut speedscope_frames: Vec<SpeedscopeFrame> = Vec::new();
70
71        for frame in &profile.frames {
72            let idx = speedscope_frames.len();
73            frame_to_index.insert(frame.id, idx);
74
75            speedscope_frames.push(SpeedscopeFrame {
76                name: if frame.name.is_empty() {
77                    "(anonymous)".to_string()
78                } else {
79                    frame.name.clone()
80                },
81                file: frame.clean_file(),
82                line: frame.line,
83                col: frame.col,
84            });
85        }
86
87        // Build samples and weights
88        let mut samples: Vec<Vec<usize>> = Vec::with_capacity(profile.samples.len());
89        let mut weights: Vec<u64> = Vec::with_capacity(profile.samples.len());
90
91        for sample in &profile.samples {
92            if let Some(stack) = profile.get_stack(sample.stack_id) {
93                let sample_indices: Vec<usize> = stack
94                    .frames
95                    .iter()
96                    .filter_map(|fid| frame_to_index.get(fid).copied())
97                    .collect();
98
99                if !sample_indices.is_empty() {
100                    samples.push(sample_indices);
101                    weights.push(sample.weight);
102                }
103            }
104        }
105
106        let file = SpeedscopeFile {
107            schema: "https://www.speedscope.app/file-format-schema.json",
108            shared: SpeedscopeShared {
109                frames: speedscope_frames,
110            },
111            profiles: vec![SpeedscopeProfile::Sampled {
112                name: profile
113                    .source_file
114                    .clone()
115                    .unwrap_or_else(|| "CPU Profile".to_string()),
116                unit: "microseconds",
117                start_value: 0,
118                end_value: analysis.total_time,
119                samples,
120                weights,
121            }],
122            name: profile.source_file.clone(),
123            active_profile_index: Some(0),
124            exporter: Some("profile-inspect".to_string()),
125        };
126
127        serde_json::to_writer(writer, &file)?;
128        Ok(())
129    }
130
131    fn write_heap_analysis(
132        &self,
133        profile: &ProfileIR,
134        analysis: &HeapAnalysis,
135        writer: &mut dyn Write,
136    ) -> Result<(), OutputError> {
137        // Build frame index
138        let mut frame_to_index: IndexMap<crate::ir::FrameId, usize> = IndexMap::new();
139        let mut speedscope_frames: Vec<SpeedscopeFrame> = Vec::new();
140
141        for frame in &profile.frames {
142            let idx = speedscope_frames.len();
143            frame_to_index.insert(frame.id, idx);
144
145            speedscope_frames.push(SpeedscopeFrame {
146                name: if frame.name.is_empty() {
147                    "(anonymous)".to_string()
148                } else {
149                    frame.name.clone()
150                },
151                file: frame.clean_file(),
152                line: frame.line,
153                col: frame.col,
154            });
155        }
156
157        // Build samples and weights (bytes instead of time)
158        let mut samples: Vec<Vec<usize>> = Vec::with_capacity(profile.samples.len());
159        let mut weights: Vec<u64> = Vec::with_capacity(profile.samples.len());
160
161        for sample in &profile.samples {
162            if let Some(stack) = profile.get_stack(sample.stack_id) {
163                let sample_indices: Vec<usize> = stack
164                    .frames
165                    .iter()
166                    .filter_map(|fid| frame_to_index.get(fid).copied())
167                    .collect();
168
169                if !sample_indices.is_empty() {
170                    samples.push(sample_indices);
171                    weights.push(sample.weight);
172                }
173            }
174        }
175
176        let file = SpeedscopeFile {
177            schema: "https://www.speedscope.app/file-format-schema.json",
178            shared: SpeedscopeShared {
179                frames: speedscope_frames,
180            },
181            profiles: vec![SpeedscopeProfile::Sampled {
182                name: profile
183                    .source_file
184                    .clone()
185                    .unwrap_or_else(|| "Heap Profile".to_string()),
186                unit: "bytes",
187                start_value: 0,
188                end_value: analysis.total_size,
189                samples,
190                weights,
191            }],
192            name: profile.source_file.clone(),
193            active_profile_index: Some(0),
194            exporter: Some("profile-inspect".to_string()),
195        };
196
197        serde_json::to_writer(writer, &file)?;
198        Ok(())
199    }
200}