Skip to main content

foundry_evm_traces/speedscope/
schema.rs

1//! Speedscope JSON format types for profile generation.
2//!
3//! This module provides Rust types that serialize to the speedscope file format.
4//! See: <https://github.com/jlfwong/speedscope/wiki/Importing-from-custom-sources>
5//!
6//! The format supports two profile types:
7//! - **Evented**: Open/close frame events with timestamps (like function calls)
8//! - **Sampled**: Periodic stack samples with weights
9
10use serde::{Deserialize, Serialize};
11use std::borrow::Cow;
12
13/// The speedscope JSON schema URL.
14pub const SCHEMA: &str = "https://www.speedscope.app/file-format-schema.json";
15
16/// Root container for a speedscope profile file.
17#[derive(Debug, Clone, Serialize, Deserialize)]
18#[serde(rename_all = "camelCase")]
19pub struct SpeedscopeFile<'a> {
20    /// JSON schema reference.
21    #[serde(rename = "$schema")]
22    pub schema: &'static str,
23
24    /// Shared data between profiles (primarily the frame definitions).
25    pub shared: Shared<'a>,
26
27    /// List of profiles in this file.
28    pub profiles: Vec<Profile<'a>>,
29
30    /// Optional name for the profile file.
31    #[serde(skip_serializing_if = "Option::is_none")]
32    pub name: Option<Cow<'a, str>>,
33
34    /// Index of the initially active profile.
35    #[serde(skip_serializing_if = "Option::is_none")]
36    pub active_profile_index: Option<usize>,
37
38    /// Name of the tool that exported this profile.
39    #[serde(skip_serializing_if = "Option::is_none")]
40    pub exporter: Option<Cow<'a, str>>,
41}
42
43impl<'a> SpeedscopeFile<'a> {
44    /// Creates a new speedscope file with the given name.
45    pub fn new(name: impl Into<Cow<'a, str>>) -> Self {
46        Self {
47            schema: SCHEMA,
48            shared: Shared::default(),
49            profiles: Vec::new(),
50            name: Some(name.into()),
51            active_profile_index: None,
52            exporter: Some(Cow::Borrowed("foundry")),
53        }
54    }
55
56    /// Adds a frame and returns its index.
57    pub fn add_frame(&mut self, frame: Frame<'a>) -> usize {
58        let idx = self.shared.frames.len();
59        self.shared.frames.push(frame);
60        idx
61    }
62
63    /// Adds a profile to this file.
64    pub fn add_profile(&mut self, profile: Profile<'a>) {
65        self.profiles.push(profile);
66    }
67}
68
69/// Shared data between profiles, primarily frame definitions.
70#[derive(Debug, Clone, Default, Serialize, Deserialize)]
71pub struct Shared<'a> {
72    /// All frames referenced by profiles in this file.
73    pub frames: Vec<Frame<'a>>,
74}
75
76/// A stack frame definition.
77#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
78pub struct Frame<'a> {
79    /// The name of the frame (function name, etc.).
80    pub name: Cow<'a, str>,
81
82    /// Optional source file path.
83    #[serde(skip_serializing_if = "Option::is_none")]
84    pub file: Option<Cow<'a, str>>,
85
86    /// Optional line number in the source file.
87    #[serde(skip_serializing_if = "Option::is_none")]
88    pub line: Option<u32>,
89
90    /// Optional column number in the source file.
91    #[serde(skip_serializing_if = "Option::is_none")]
92    pub col: Option<u32>,
93}
94
95impl<'a> Frame<'a> {
96    /// Creates a new frame with just a name.
97    pub fn new(name: impl Into<Cow<'a, str>>) -> Self {
98        Self { name: name.into(), file: None, line: None, col: None }
99    }
100}
101
102/// A profile within a speedscope file.
103#[derive(Debug, Clone, Serialize, Deserialize)]
104#[serde(tag = "type", rename_all = "camelCase")]
105pub enum Profile<'a> {
106    /// Event-based profile with explicit open/close events.
107    #[serde(rename = "evented")]
108    Evented(EventedProfile<'a>),
109
110    /// Sample-based profile with periodic stack snapshots.
111    #[serde(rename = "sampled")]
112    Sampled(SampledProfile<'a>),
113}
114
115/// An event-based profile where function calls are explicit open/close events.
116#[derive(Debug, Clone, Serialize, Deserialize)]
117#[serde(rename_all = "camelCase")]
118pub struct EventedProfile<'a> {
119    /// Name of this profile (e.g., thread name or test name).
120    pub name: Cow<'a, str>,
121
122    /// Unit for the values (e.g., "nanoseconds", "bytes").
123    pub unit: ValueUnit,
124
125    /// Starting value (usually 0).
126    pub start_value: u64,
127
128    /// Ending value (total time/gas/etc.).
129    pub end_value: u64,
130
131    /// List of frame open/close events.
132    pub events: Vec<Event>,
133}
134
135impl<'a> EventedProfile<'a> {
136    /// Creates a new evented profile with the given name and unit.
137    pub fn new(name: impl Into<Cow<'a, str>>, unit: ValueUnit) -> Self {
138        Self { name: name.into(), unit, start_value: 0, end_value: 0, events: Vec::new() }
139    }
140
141    /// Adds an open frame event at the given timestamp.
142    pub fn open_frame(&mut self, frame: usize, at: u64) {
143        self.events.push(Event { event_type: EventType::Open, frame, at });
144    }
145
146    /// Adds a close frame event at the given timestamp.
147    pub fn close_frame(&mut self, frame: usize, at: u64) {
148        self.events.push(Event { event_type: EventType::Close, frame, at });
149    }
150
151    /// Sets the end value based on the final timestamp.
152    pub const fn set_end_value(&mut self, end_value: u64) {
153        self.end_value = end_value;
154    }
155}
156
157/// A sample-based profile with periodic stack snapshots.
158#[derive(Debug, Clone, Serialize, Deserialize)]
159#[serde(rename_all = "camelCase")]
160pub struct SampledProfile<'a> {
161    /// Name of this profile.
162    pub name: Cow<'a, str>,
163
164    /// Unit for the weight values.
165    pub unit: ValueUnit,
166
167    /// Starting value.
168    pub start_value: u64,
169
170    /// Ending value.
171    pub end_value: u64,
172
173    /// Stack samples (each sample is a list of frame indices, bottom to top).
174    pub samples: Vec<Vec<usize>>,
175
176    /// Weights for each sample.
177    pub weights: Vec<u64>,
178}
179
180/// Units for profile values.
181#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
182#[serde(rename_all = "lowercase")]
183pub enum ValueUnit {
184    /// No unit (raw counts).
185    None,
186    /// Nanoseconds.
187    Nanoseconds,
188    /// Microseconds.
189    Microseconds,
190    /// Milliseconds.
191    Milliseconds,
192    /// Seconds.
193    Seconds,
194    /// Bytes.
195    Bytes,
196}
197
198/// A frame open or close event in an evented profile.
199#[derive(Debug, Clone, Serialize, Deserialize)]
200pub struct Event {
201    /// Type of event (open or close).
202    #[serde(rename = "type")]
203    pub event_type: EventType,
204
205    /// Index into the shared frames array.
206    pub frame: usize,
207
208    /// Timestamp when this event occurred.
209    pub at: u64,
210}
211
212/// Type of frame event.
213#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
214pub enum EventType {
215    /// Frame was opened (function entered).
216    #[serde(rename = "O")]
217    Open,
218    /// Frame was closed (function returned).
219    #[serde(rename = "C")]
220    Close,
221}
222
223#[cfg(test)]
224mod tests {
225    use super::*;
226    use snapbox::prelude::*;
227
228    #[test]
229    fn test_serialize_empty_file() {
230        let file = SpeedscopeFile::new("test");
231        let json = serde_json::to_string_pretty(&file).unwrap();
232
233        snapbox::assert_data_eq!(
234            json.is_json(),
235            (snapbox::str![[r#"
236{
237  "$schema": "https://www.speedscope.app/file-format-schema.json",
238  "shared": {
239    "frames": []
240  },
241  "profiles": [],
242  "name": "test",
243  "exporter": "foundry"
244}
245"#]])
246            .is_json(),
247        );
248    }
249
250    #[test]
251    fn test_serialize_evented_profile() {
252        let mut file = SpeedscopeFile::new("test");
253        let frame_a = file.add_frame(Frame::new("a"));
254        let frame_b = file.add_frame(Frame::new("b"));
255
256        let mut profile = EventedProfile::new("main", ValueUnit::Nanoseconds);
257        profile.open_frame(frame_a, 0);
258        profile.open_frame(frame_b, 0);
259        profile.close_frame(frame_b, 100);
260        profile.close_frame(frame_a, 200);
261        profile.set_end_value(200);
262
263        file.add_profile(Profile::Evented(profile));
264
265        let json = serde_json::to_string_pretty(&file).unwrap();
266
267        snapbox::assert_data_eq!(
268            json.is_json(),
269            (snapbox::str![[r#"
270{
271  "$schema": "https://www.speedscope.app/file-format-schema.json",
272  "shared": {
273    "frames": [
274      {
275        "name": "a"
276      },
277      {
278        "name": "b"
279      }
280    ]
281  },
282  "profiles": [
283    {
284      "type": "evented",
285      "name": "main",
286      "unit": "nanoseconds",
287      "startValue": 0,
288      "endValue": 200,
289      "events": [
290        {
291          "type": "O",
292          "frame": 0,
293          "at": 0
294        },
295        {
296          "type": "O",
297          "frame": 1,
298          "at": 0
299        },
300        {
301          "type": "C",
302          "frame": 1,
303          "at": 100
304        },
305        {
306          "type": "C",
307          "frame": 0,
308          "at": 200
309        }
310      ]
311    }
312  ],
313  "name": "test",
314  "exporter": "foundry"
315}
316"#]])
317            .is_json(),
318        );
319    }
320}