foundry_evm_traces/speedscope/
schema.rs1use serde::{Deserialize, Serialize};
11use std::borrow::Cow;
12
13pub const SCHEMA: &str = "https://www.speedscope.app/file-format-schema.json";
15
16#[derive(Debug, Clone, Serialize, Deserialize)]
18#[serde(rename_all = "camelCase")]
19pub struct SpeedscopeFile<'a> {
20 #[serde(rename = "$schema")]
22 pub schema: &'static str,
23
24 pub shared: Shared<'a>,
26
27 pub profiles: Vec<Profile<'a>>,
29
30 #[serde(skip_serializing_if = "Option::is_none")]
32 pub name: Option<Cow<'a, str>>,
33
34 #[serde(skip_serializing_if = "Option::is_none")]
36 pub active_profile_index: Option<usize>,
37
38 #[serde(skip_serializing_if = "Option::is_none")]
40 pub exporter: Option<Cow<'a, str>>,
41}
42
43impl<'a> SpeedscopeFile<'a> {
44 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 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 pub fn add_profile(&mut self, profile: Profile<'a>) {
65 self.profiles.push(profile);
66 }
67}
68
69#[derive(Debug, Clone, Default, Serialize, Deserialize)]
71pub struct Shared<'a> {
72 pub frames: Vec<Frame<'a>>,
74}
75
76#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
78pub struct Frame<'a> {
79 pub name: Cow<'a, str>,
81
82 #[serde(skip_serializing_if = "Option::is_none")]
84 pub file: Option<Cow<'a, str>>,
85
86 #[serde(skip_serializing_if = "Option::is_none")]
88 pub line: Option<u32>,
89
90 #[serde(skip_serializing_if = "Option::is_none")]
92 pub col: Option<u32>,
93}
94
95impl<'a> Frame<'a> {
96 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#[derive(Debug, Clone, Serialize, Deserialize)]
104#[serde(tag = "type", rename_all = "camelCase")]
105pub enum Profile<'a> {
106 #[serde(rename = "evented")]
108 Evented(EventedProfile<'a>),
109
110 #[serde(rename = "sampled")]
112 Sampled(SampledProfile<'a>),
113}
114
115#[derive(Debug, Clone, Serialize, Deserialize)]
117#[serde(rename_all = "camelCase")]
118pub struct EventedProfile<'a> {
119 pub name: Cow<'a, str>,
121
122 pub unit: ValueUnit,
124
125 pub start_value: u64,
127
128 pub end_value: u64,
130
131 pub events: Vec<Event>,
133}
134
135impl<'a> EventedProfile<'a> {
136 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 pub fn open_frame(&mut self, frame: usize, at: u64) {
143 self.events.push(Event { event_type: EventType::Open, frame, at });
144 }
145
146 pub fn close_frame(&mut self, frame: usize, at: u64) {
148 self.events.push(Event { event_type: EventType::Close, frame, at });
149 }
150
151 pub const fn set_end_value(&mut self, end_value: u64) {
153 self.end_value = end_value;
154 }
155}
156
157#[derive(Debug, Clone, Serialize, Deserialize)]
159#[serde(rename_all = "camelCase")]
160pub struct SampledProfile<'a> {
161 pub name: Cow<'a, str>,
163
164 pub unit: ValueUnit,
166
167 pub start_value: u64,
169
170 pub end_value: u64,
172
173 pub samples: Vec<Vec<usize>>,
175
176 pub weights: Vec<u64>,
178}
179
180#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
182#[serde(rename_all = "lowercase")]
183pub enum ValueUnit {
184 None,
186 Nanoseconds,
188 Microseconds,
190 Milliseconds,
192 Seconds,
194 Bytes,
196}
197
198#[derive(Debug, Clone, Serialize, Deserialize)]
200pub struct Event {
201 #[serde(rename = "type")]
203 pub event_type: EventType,
204
205 pub frame: usize,
207
208 pub at: u64,
210}
211
212#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
214pub enum EventType {
215 #[serde(rename = "O")]
217 Open,
218 #[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}