Skip to main content

relay_profiling/
profile_chunk.rs

1use serde::Deserialize;
2
3use crate::{
4    AndroidProfileChunk, PerfettoProfileChunk, ProfileError, ProfileType, V2ProfileChunk,
5    sample::Version,
6};
7
8/// Minimum interface all profile chunk types must implement.
9pub trait ProfileChunk {
10    /// Returns the platform this profile chunk is associated with.
11    fn platform(&self) -> &str;
12
13    /// Returns the [`ProfileType`] of this profile chunk.
14    ///
15    /// By default this is inferred from the [`Self::platform`].
16    fn profile_type(&self) -> ProfileType {
17        ProfileType::from_platform(self.platform())
18    }
19
20    /// Normalizes the profile chunk.
21    fn normalize(&mut self) -> Result<(), ProfileError>;
22}
23
24/// Supported profile chunks for continous profiling.
25#[derive(Debug)]
26pub enum AnyProfileChunk {
27    Android(Box<AndroidProfileChunk>),
28    Perfetto(Box<PerfettoProfileChunk>),
29    V2(Box<V2ProfileChunk>),
30}
31
32impl From<Box<V2ProfileChunk>> for AnyProfileChunk {
33    fn from(chunk: Box<V2ProfileChunk>) -> Self {
34        Self::V2(chunk)
35    }
36}
37
38impl From<Box<AndroidProfileChunk>> for AnyProfileChunk {
39    fn from(chunk: Box<AndroidProfileChunk>) -> Self {
40        Self::Android(chunk)
41    }
42}
43
44impl From<Box<PerfettoProfileChunk>> for AnyProfileChunk {
45    fn from(chunk: Box<PerfettoProfileChunk>) -> Self {
46        Self::Perfetto(chunk)
47    }
48}
49
50impl From<AndroidOrV2ProfileChunk> for AnyProfileChunk {
51    fn from(chunk: AndroidOrV2ProfileChunk) -> Self {
52        match chunk {
53            AndroidOrV2ProfileChunk::Android(c) => Self::Android(c),
54            AndroidOrV2ProfileChunk::V2(c) => Self::V2(c),
55        }
56    }
57}
58
59impl ProfileChunk for AnyProfileChunk {
60    fn platform(&self) -> &str {
61        match self {
62            AnyProfileChunk::Android(chunk) => chunk.platform(),
63            AnyProfileChunk::Perfetto(chunk) => chunk.platform(),
64            AnyProfileChunk::V2(chunk) => chunk.platform(),
65        }
66    }
67
68    fn normalize(&mut self) -> Result<(), ProfileError> {
69        match self {
70            AnyProfileChunk::Android(chunk) => chunk.normalize(),
71            AnyProfileChunk::Perfetto(chunk) => chunk.normalize(),
72            AnyProfileChunk::V2(chunk) => chunk.normalize(),
73        }
74    }
75}
76
77impl relay_protocol::Getter for AnyProfileChunk {
78    fn get_value(&self, path: &str) -> Option<relay_protocol::Val<'_>> {
79        match self {
80            AnyProfileChunk::Android(chunk) => chunk.get_value(path),
81            AnyProfileChunk::Perfetto(chunk) => chunk.get_value(path),
82            AnyProfileChunk::V2(chunk) => chunk.get_value(path),
83        }
84    }
85}
86
87impl relay_filter::Filterable for AnyProfileChunk {
88    fn release(&self) -> Option<&str> {
89        match self {
90            AnyProfileChunk::Android(chunk) => chunk.release(),
91            AnyProfileChunk::Perfetto(chunk) => chunk.release(),
92            AnyProfileChunk::V2(chunk) => chunk.release(),
93        }
94    }
95}
96
97/// Either an [`AndroidProfileChunk`] or a [`V2ProfileChunk`].
98#[derive(Debug)]
99pub enum AndroidOrV2ProfileChunk {
100    Android(Box<AndroidProfileChunk>),
101    V2(Box<V2ProfileChunk>),
102}
103
104impl ProfileChunk for AndroidOrV2ProfileChunk {
105    fn platform(&self) -> &str {
106        match self {
107            AndroidOrV2ProfileChunk::Android(chunk) => chunk.platform(),
108            AndroidOrV2ProfileChunk::V2(chunk) => chunk.platform(),
109        }
110    }
111
112    fn normalize(&mut self) -> Result<(), ProfileError> {
113        match self {
114            AndroidOrV2ProfileChunk::Android(chunk) => chunk.normalize(),
115            AndroidOrV2ProfileChunk::V2(chunk) => chunk.normalize(),
116        }
117    }
118}
119
120impl AndroidOrV2ProfileChunk {
121    /// Parses either a [`AndroidOrV2ProfileChunk`] or [`ProfileChunk`] from a slice of bytes.
122    pub fn parse(data: &[u8]) -> Result<Self, ProfileError> {
123        #[derive(Debug, Deserialize)]
124        struct MinimalProfile {
125            platform: String,
126            #[serde(default)]
127            version: Version,
128            #[serde(default)]
129            sampled_profile: Option<serde::de::IgnoredAny>,
130        }
131
132        let minimal: MinimalProfile = {
133            let d = &mut serde_json::Deserializer::from_slice(data);
134            serde_path_to_error::deserialize(d)
135        }?;
136
137        // Android SDKs produce two profile_chunk types that pass through this method: trace
138        // profiles and Application-Not-Responding (ANR) profiles. They come in multiple
139        // varieties, each of which needs to be accounted for.
140
141        // Android trace profiles:
142        // ---------------
143        // Version: 2 (incorrect), 2.android-trace (corrected)
144        // Platform: android
145        // Content field: sampled_profile (i.e., Android Runtime's event-based format, aka
146        //   "traces")
147        // Destination type: AndroidProfileChunk
148
149        // Android ANR profiles:
150        // ---------------
151        // Version: 2
152        // Platform: java (incorrect), android (corrected)
153        // Content field: profile (i.e., standardized stacks/frames/samples format)
154        // Destination type: V2ProfileChunk
155
156        // We also need to handle non-Android profile chunks.
157
158        // Non-Android profiles:
159        // ---------------
160        // Version: 2
161        // Platform: cocoa, javascript, etc.
162        // Content field: profile (i.e., standardized stacks/frames/samples format)
163        // Destination type: V2ProfileChunk
164
165        let is_android_trace_profile =
166            minimal.platform == "android" && minimal.sampled_profile.is_some();
167
168        match minimal.version {
169            Version::V2AndroidTrace => AndroidProfileChunk::parse(data)
170                .map(Box::new)
171                .map(Self::Android),
172            // Account for legacy submissions that don't use the 2.android-trace version.
173            Version::V2 if is_android_trace_profile => AndroidProfileChunk::parse(data)
174                .map(Box::new)
175                .map(Self::Android),
176            Version::V2 => V2ProfileChunk::parse(data).map(Box::new).map(Self::V2),
177            Version::V1 | Version::Unknown => Err(ProfileError::PlatformNotSupported),
178        }
179    }
180}
181
182#[cfg(test)]
183mod tests {
184    use std::assert_matches;
185
186    use serde_json::{Value, json};
187
188    use super::*;
189
190    #[test]
191    fn test_parse_correctly_versioned_android_trace_profile_into_android_profile_chunk() {
192        let mut payload: Value =
193            serde_json::from_slice(include_bytes!("../tests/fixtures/android/chunk/valid.json"))
194                .unwrap();
195        payload["version"] = json!("2.android-trace");
196        let data = serde_json::to_vec(&payload).unwrap();
197
198        // 1. Fresh SDK-shaped payload: `sampled_profile` populated, `profile` absent.
199        let sdk_chunk = AndroidOrV2ProfileChunk::parse(&data).unwrap();
200        assert_matches!(sdk_chunk, AndroidOrV2ProfileChunk::Android(_));
201
202        // 2. Relay's own re-serialized shape: `sampled_profile` absent, `profile` populated.
203        let AndroidOrV2ProfileChunk::Android(android_chunk) = sdk_chunk else {
204            unreachable!()
205        };
206        let reserialized = serde_json::to_vec(&android_chunk).unwrap();
207        let value: Value = serde_json::from_slice(&reserialized).unwrap();
208        assert!(value.get("sampled_profile").is_none());
209        assert!(value.get("profile").is_some());
210
211        let round_tripped = AndroidOrV2ProfileChunk::parse(&reserialized).unwrap();
212        assert_matches!(round_tripped, AndroidOrV2ProfileChunk::Android(_));
213    }
214
215    #[test]
216    fn test_parse_legacy_versioned_android_trace_profile_into_android_profile_chunk() {
217        let mut payload: Value =
218            serde_json::from_slice(include_bytes!("../tests/fixtures/android/chunk/valid.json"))
219                .unwrap();
220        payload["version"] = json!("2");
221        let data = serde_json::to_vec(&payload).unwrap();
222
223        let chunk = AndroidOrV2ProfileChunk::parse(&data).unwrap();
224        assert_matches!(chunk, AndroidOrV2ProfileChunk::Android(_));
225    }
226
227    #[test]
228    fn test_parse_sample_v2_profile_into_v2_profile_chunk() {
229        let base_payload: Value =
230            serde_json::from_slice(include_bytes!("../tests/fixtures/sample/v2/valid.json"))
231                .unwrap();
232
233        for platform in ["android", "cocoa", "javascript", "python"] {
234            let mut payload = base_payload.clone();
235            payload["platform"] = json!(platform);
236            let data = serde_json::to_vec(&payload).unwrap();
237
238            let chunk = AndroidOrV2ProfileChunk::parse(&data).unwrap();
239
240            assert_matches!(chunk, AndroidOrV2ProfileChunk::V2(_));
241        }
242    }
243
244    #[test]
245    fn test_return_error_for_version_1_profile() {
246        for payload in [
247            &include_bytes!("../tests/fixtures/sample/v2/valid.json")[..],
248            &include_bytes!("../tests/fixtures/android/chunk/valid.json")[..],
249            &include_bytes!("../tests/fixtures/android/chunk/valid-rn.json")[..],
250        ] {
251            let mut payload: Value = serde_json::from_slice(payload).unwrap();
252            payload["version"] = json!("1");
253            let data = serde_json::to_vec(&payload).unwrap();
254
255            let err = AndroidOrV2ProfileChunk::parse(&data).unwrap_err();
256            assert_matches!(err, ProfileError::PlatformNotSupported);
257        }
258    }
259
260    #[test]
261    fn test_return_error_for_unknown_version_profile() {
262        for payload in [
263            &include_bytes!("../tests/fixtures/sample/v2/valid.json")[..],
264            &include_bytes!("../tests/fixtures/android/chunk/valid.json")[..],
265            &include_bytes!("../tests/fixtures/android/chunk/valid-rn.json")[..],
266        ] {
267            let mut payload: Value = serde_json::from_slice(payload).unwrap();
268            payload.as_object_mut().unwrap().remove("version");
269            let data = serde_json::to_vec(&payload).unwrap();
270
271            let err = AndroidOrV2ProfileChunk::parse(&data).unwrap_err();
272            assert_matches!(err, ProfileError::PlatformNotSupported);
273        }
274    }
275}