profile_inspect/output/
speedscope.rs1use 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
11pub struct SpeedscopeFormatter;
13
14#[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 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 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 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 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}