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    ///
27    /// Any debug images supplied in the `sample` metadata (e.g. a Proguard image for Android
28    /// deobfuscation) are retained; debug images extracted from the Perfetto profile are
29    /// appended to them.
30    pub fn parse(sample: &[u8], perfetto: Bytes) -> Result<Self, ProfileError> {
31        let mut inner: v2::ProfileChunk = {
32            let deserializer = &mut serde_json::Deserializer::from_slice(sample);
33            serde_path_to_error::deserialize(deserializer).map_err(ProfileError::InvalidJson)?
34        };
35
36        if inner.profile.is_empty() {
37            let (profile_data, debug_images) = convert::convert(&perfetto)?;
38            inner.profile = profile_data;
39            inner.metadata.debug_meta.images.extend(debug_images);
40        }
41
42        Ok(Self { inner, perfetto })
43    }
44
45    /// Returns the Perfetto profile this [`Chunk`] was parsed from.
46    pub fn perfetto(&self) -> &Bytes {
47        &self.perfetto
48    }
49
50    /// Returns the combined metadata and Perfetto profile as a [`V2ProfileChunk`].
51    pub fn as_v2(&self) -> &V2ProfileChunk {
52        &self.inner
53    }
54}
55
56impl crate::profile_chunk::ProfileChunk for Chunk {
57    fn platform(&self) -> &str {
58        &self.inner.metadata.platform
59    }
60
61    fn normalize(&mut self) -> Result<(), ProfileError> {
62        self.inner.normalize()
63    }
64}
65
66impl relay_filter::Filterable for Chunk {
67    fn release(&self) -> Option<&str> {
68        self.inner.metadata.release.as_deref()
69    }
70}
71
72impl relay_protocol::Getter for Chunk {
73    fn get_value(&self, path: &str) -> Option<relay_protocol::Val<'_>> {
74        self.inner.get_value(path)
75    }
76}
77
78#[cfg(test)]
79mod tests {
80    use super::*;
81
82    use crate::debug_image::ImageType;
83    use crate::{ProfileChunk, ProfileType};
84
85    const PERFETTO_ANDROID: Bytes = Bytes::from_static(include_bytes!(
86        "../../tests/fixtures/android/perfetto/android.pftrace"
87    ));
88
89    #[test]
90    fn test_parse_perfetto() {
91        let metadata_json = serde_json::json!({
92            "version": "2",
93            "chunk_id": "0432a0a4c25f4697bf9f0a2fcbe6a814",
94            "profiler_id": "4d229f1d3807421ba62a5f8bc295d836",
95            "platform": "android",
96            "content_type": "perfetto",
97            "client_sdk": {"name": "sentry-android", "version": "1.0"},
98            "debug_meta": {
99                "images": [
100                    {
101                        "uuid": "32420279-25e2-34e6-8bc7-8a006a8f2425",
102                        "type": "proguard",
103                    },
104                ],
105            },
106        });
107        let metadata_bytes = serde_json::to_vec(&metadata_json).unwrap();
108
109        let chunk = Chunk::parse(&metadata_bytes, PERFETTO_ANDROID).unwrap();
110
111        assert_eq!(chunk.inner.metadata.platform, "android");
112        assert_eq!(chunk.profile_type(), ProfileType::Ui);
113
114        let images = &chunk.inner.metadata.debug_meta.images;
115        assert_eq!(images[0].image_type, ImageType::Proguard);
116        assert!(
117            images.len() > 1,
118            "expected native images to be appended after the Proguard image"
119        );
120        insta::assert_json_snapshot!(chunk.inner);
121    }
122
123    #[test]
124    fn test_parse_perfetto_invalid_metadata() {
125        let result = Chunk::parse(b"not json", PERFETTO_ANDROID);
126        assert!(result.is_err());
127    }
128
129    #[test]
130    fn test_parse_perfetto_empty_trace() {
131        // Valid metadata but no profiling samples in the binary → should fail.
132        let metadata_bytes = serde_json::to_vec(&serde_json::json!({
133            "version": "2",
134            "chunk_id": "0432a0a4c25f4697bf9f0a2fcbe6a814",
135            "profiler_id": "4d229f1d3807421ba62a5f8bc295d836",
136            "platform": "android",
137            "content_type": "perfetto",
138            "client_sdk": {"name": "sentry-android", "version": "1.0"},
139        }))
140        .unwrap();
141
142        let _ = Chunk::parse(&metadata_bytes, Bytes::from_static(b"")).unwrap_err();
143    }
144
145    #[test]
146    fn test_parse_perfetto_missing_required_field() {
147        // metadata is missing the required `chunk_id` field → de-serialization error.
148        let metadata_bytes = serde_json::to_vec(&serde_json::json!({
149            "version": "2",
150            "profiler_id": "4d229f1d3807421ba62a5f8bc295d836",
151            "platform": "android",
152            "client_sdk": {"name": "sentry-android", "version": "1.0"},
153        }))
154        .unwrap();
155
156        let result = Chunk::parse(&metadata_bytes, PERFETTO_ANDROID);
157        assert!(
158            matches!(result, Err(ProfileError::InvalidJson(_))),
159            "expected InvalidJson, got {result:?}"
160        );
161    }
162}