Skip to main content

relay_profiling/perfetto/
mod.rs

1use bytes::Bytes;
2
3use crate::sample::v2;
4use crate::{ProfileError, V2ProfileChunk};
5
6mod convert;
7#[allow(dead_code)]
8mod proto;
9
10/// A parsed Perfetto profiling chunk.
11#[derive(Debug)]
12pub struct Chunk {
13    inner: v2::ProfileChunk,
14    perfetto: Bytes,
15}
16
17impl Chunk {
18    /// Parses a [`Chunk`] from the required [`v2::ProfileChunk`] and a Perfetto profile.
19    ///
20    /// A Perfetto profile always requires an associated [`v2::ProfileChunk`] for additional
21    /// metadata. The resulting [`Chunk`] contains all metadata from the [`v2::ProfileChunk`]
22    /// and samples from the `perfetto` profile.
23    ///
24    /// Note: if the parsed `sample` already contains profiling information, the frames in the
25    /// Perfetto profile are not extracted again.
26    pub fn parse(sample: &[u8], perfetto: Bytes) -> Result<Self, ProfileError> {
27        let mut inner: v2::ProfileChunk = {
28            let deserializer = &mut serde_json::Deserializer::from_slice(sample);
29            serde_path_to_error::deserialize(deserializer).map_err(ProfileError::InvalidJson)?
30        };
31
32        if inner.profile.is_empty() {
33            let (profile_data, debug_images) = convert::convert(&perfetto)?;
34            inner.profile = profile_data;
35            inner.metadata.debug_meta.images = debug_images;
36        }
37
38        Ok(Self { inner, perfetto })
39    }
40
41    /// Returns the Perfetto profile this [`Chunk`] was parsed from.
42    pub fn perfetto(&self) -> &Bytes {
43        &self.perfetto
44    }
45
46    /// Returns the combined metadata and Perfetto profile as a [`V2ProfileChunk`].
47    pub fn as_v2(&self) -> &V2ProfileChunk {
48        &self.inner
49    }
50}
51
52impl crate::profile_chunk::ProfileChunk for Chunk {
53    fn platform(&self) -> &str {
54        &self.inner.metadata.platform
55    }
56
57    fn normalize(&mut self) -> Result<(), ProfileError> {
58        self.inner.normalize()
59    }
60}
61
62impl relay_filter::Filterable for Chunk {
63    fn release(&self) -> Option<&str> {
64        self.inner.metadata.release.as_deref()
65    }
66}
67
68impl relay_protocol::Getter for Chunk {
69    fn get_value(&self, path: &str) -> Option<relay_protocol::Val<'_>> {
70        self.inner.get_value(path)
71    }
72}
73
74#[cfg(test)]
75mod tests {
76    use super::*;
77
78    use crate::{ProfileChunk, ProfileType};
79
80    const PERFETTO_ANDROID: Bytes = Bytes::from_static(include_bytes!(
81        "../../tests/fixtures/android/perfetto/android.pftrace"
82    ));
83
84    #[test]
85    fn test_parse_perfetto() {
86        let metadata_json = serde_json::json!({
87            "version": "2",
88            "chunk_id": "0432a0a4c25f4697bf9f0a2fcbe6a814",
89            "profiler_id": "4d229f1d3807421ba62a5f8bc295d836",
90            "platform": "android",
91            "content_type": "perfetto",
92            "client_sdk": {"name": "sentry-android", "version": "1.0"},
93        });
94        let metadata_bytes = serde_json::to_vec(&metadata_json).unwrap();
95
96        let chunk = Chunk::parse(&metadata_bytes, PERFETTO_ANDROID).unwrap();
97
98        assert_eq!(chunk.inner.metadata.platform, "android");
99        assert_eq!(chunk.profile_type(), ProfileType::Ui);
100
101        insta::assert_json_snapshot!(chunk.inner);
102    }
103
104    #[test]
105    fn test_parse_perfetto_invalid_metadata() {
106        let result = Chunk::parse(b"not json", PERFETTO_ANDROID);
107        assert!(result.is_err());
108    }
109
110    #[test]
111    fn test_parse_perfetto_empty_trace() {
112        // Valid metadata but no profiling samples in the binary → should fail.
113        let metadata_bytes = serde_json::to_vec(&serde_json::json!({
114            "version": "2",
115            "chunk_id": "0432a0a4c25f4697bf9f0a2fcbe6a814",
116            "profiler_id": "4d229f1d3807421ba62a5f8bc295d836",
117            "platform": "android",
118            "content_type": "perfetto",
119            "client_sdk": {"name": "sentry-android", "version": "1.0"},
120        }))
121        .unwrap();
122
123        let _ = Chunk::parse(&metadata_bytes, Bytes::from_static(b"")).unwrap_err();
124    }
125
126    #[test]
127    fn test_parse_perfetto_missing_required_field() {
128        // metadata is missing the required `chunk_id` field → de-serialization error.
129        let metadata_bytes = serde_json::to_vec(&serde_json::json!({
130            "version": "2",
131            "profiler_id": "4d229f1d3807421ba62a5f8bc295d836",
132            "platform": "android",
133            "client_sdk": {"name": "sentry-android", "version": "1.0"},
134        }))
135        .unwrap();
136
137        let result = Chunk::parse(&metadata_bytes, PERFETTO_ANDROID);
138        assert!(
139            matches!(result, Err(ProfileError::InvalidJson(_))),
140            "expected InvalidJson, got {result:?}"
141        );
142    }
143}