pyroscope_rbspy_oncpu/ui/
speedscope.rs

1use std::collections::HashMap;
2use std::io::Write;
3use std::time::SystemTime;
4
5use crate::core::process::Pid;
6use crate::core::types::{StackFrame, StackTrace};
7
8use anyhow::Result;
9
10/*
11 * This file contains code to export rbspy profiles for use in https://speedscope.app
12 *
13 * The TypeScript definitions that define this file format can be found here:
14 * https://github.com/jlfwong/speedscope/blob/9d13d9/src/lib/file-format-spec.ts
15 *
16 * From the TypeScript definition, a JSON schema is generated. The latest
17 * schema can be found here: https://speedscope.app/file-format-schema.json
18 *
19 * This JSON schema conveniently allows to generate type bindings for generating JSON.
20 * You can use https://app.quicktype.io/ to generate serde_json Rust bindings for the
21 * given JSON schema.
22 *
23 * There are multiple variants of the file format. The variant we're going to generate
24 * is the "type: sampled" profile, since it most closely maps to rbspy's data recording
25 * structure.
26 */
27
28#[derive(Debug, Serialize, Deserialize)]
29struct SpeedscopeFile {
30    #[serde(rename = "$schema")]
31    schema: String,
32    profiles: Vec<Profile>,
33    shared: Shared,
34
35    #[serde(rename = "activeProfileIndex")]
36    active_profile_index: Option<f64>,
37
38    exporter: Option<String>,
39
40    name: Option<String>,
41}
42
43#[derive(Debug, Serialize, Deserialize)]
44struct Profile {
45    #[serde(rename = "type")]
46    profile_type: ProfileType,
47
48    name: String,
49    unit: ValueUnit,
50
51    #[serde(rename = "startValue")]
52    start_value: f64,
53
54    #[serde(rename = "endValue")]
55    end_value: f64,
56
57    samples: Vec<Vec<usize>>,
58    weights: Vec<f64>,
59}
60
61#[derive(Debug, Serialize, Deserialize)]
62struct Shared {
63    frames: Vec<Frame>,
64}
65
66#[derive(Debug, Clone, Serialize, Deserialize)]
67struct Frame {
68    name: String,
69    file: Option<String>,
70    line: Option<usize>,
71    col: Option<usize>,
72}
73
74#[derive(Debug, Serialize, Deserialize)]
75enum ProfileType {
76    #[serde(rename = "evented")]
77    Evented,
78    #[serde(rename = "sampled")]
79    Sampled,
80}
81
82#[derive(Debug, Serialize, Deserialize)]
83enum ValueUnit {
84    #[serde(rename = "bytes")]
85    Bytes,
86    #[serde(rename = "microseconds")]
87    Microseconds,
88    #[serde(rename = "milliseconds")]
89    Milliseconds,
90    #[serde(rename = "nanoseconds")]
91    Nanoseconds,
92    #[serde(rename = "none")]
93    None,
94    #[serde(rename = "seconds")]
95    Seconds,
96}
97
98impl SpeedscopeFile {
99    pub fn new(
100        samples: HashMap<Option<Pid>, Vec<Vec<usize>>>,
101        frames: Vec<Frame>,
102        weights: Vec<f64>,
103    ) -> SpeedscopeFile {
104        let end_value = samples.len();
105
106        SpeedscopeFile {
107            // This is always the same
108            schema: "https://www.speedscope.app/file-format-schema.json".to_string(),
109
110            active_profile_index: None,
111
112            name: Some("rbspy profile".to_string()),
113
114            exporter: Some(format!("rbspy@{}", env!("CARGO_PKG_VERSION"))),
115
116            profiles: samples
117                .iter()
118                .map(|(option_pid, samples)| Profile {
119                    profile_type: ProfileType::Sampled,
120
121                    name: option_pid.map_or("rbspy profile".to_string(), |pid| {
122                        format!("rbspy profile - pid {}", pid)
123                    }),
124
125                    unit: ValueUnit::Seconds,
126
127                    start_value: 0.0,
128                    end_value: end_value as f64,
129
130                    samples: samples.clone(),
131                    weights: weights.clone(),
132                })
133                .collect(),
134
135            shared: Shared { frames },
136        }
137    }
138}
139
140impl Frame {
141    pub fn new(stack_frame: &StackFrame) -> Frame {
142        Frame {
143            name: stack_frame.name.clone(),
144            file: Some(stack_frame.relative_path.clone()),
145            line: stack_frame.lineno,
146            col: None,
147        }
148    }
149}
150
151#[derive(Default)]
152pub struct Stats {
153    samples: HashMap<Option<Pid>, Vec<Vec<usize>>>,
154    frames: Vec<Frame>,
155    frame_to_index: HashMap<StackFrame, usize>,
156    weights: Vec<f64>,
157    prev_time: Option<SystemTime>,
158}
159
160impl Stats {
161    pub fn new() -> Stats {
162        Default::default()
163    }
164
165    pub fn record(&mut self, stack: &StackTrace) -> Result<()> {
166        let mut frame_indices: Vec<usize> = stack
167            .trace
168            .iter()
169            .map(|frame| {
170                let frames = &mut self.frames;
171                *self.frame_to_index.entry(frame.clone()).or_insert_with(|| {
172                    let len = frames.len();
173                    frames.push(Frame::new(&frame));
174                    len
175                })
176            })
177            .collect();
178        frame_indices.reverse();
179
180        self.samples
181            .entry(stack.pid)
182            .or_insert_with(|| vec![])
183            .push(frame_indices);
184
185        if let Some(time) = stack.time {
186            if let Some(prev_time) = self.prev_time {
187                let delta = time.duration_since(prev_time)?;
188                self.weights.push(delta.as_secs_f64());
189            } else {
190                // drop first sample, since we have no delta to compare against
191                self.weights.push(0.0);
192            }
193            self.prev_time = stack.time;
194        } else {
195            // support for import from old profiles that have no timestamps
196            self.weights.push(1.0);
197        }
198
199        Ok(())
200    }
201
202    pub fn write(&self, mut w: &mut dyn Write) -> Result<()> {
203        let json = serde_json::to_string(&SpeedscopeFile::new(
204            self.samples.clone(),
205            self.frames.clone(),
206            self.weights.clone(),
207        ))?;
208        writeln!(&mut w, "{}", json)?;
209        Ok(())
210    }
211}