Skip to main content

relay_profiling/android/
chunk.rs

1//! Android Format
2//!
3//! Relay is expecting a JSON object with some mandatory metadata and a `sampled_profile` key
4//! containing the raw Android profile.
5//!
6//! `android` has a specific binary representation of its profile and Relay is responsible to
7//! unpack it before it's forwarded down the line.
8//!
9use std::collections::HashMap;
10
11use android_trace_log::chrono::Utc;
12use android_trace_log::{AndroidTraceLog, Clock, Vm};
13use bytes::Bytes;
14use data_encoding::BASE64_NOPAD;
15use relay_event_schema::protocol::EventId;
16use serde::{Deserialize, Serialize};
17
18use crate::debug_image::get_proguard_image;
19use crate::measurements::ChunkMeasurement;
20use crate::sample::Version;
21use crate::sample::v2::ProfileData;
22use crate::types::{ClientSdk, DebugMeta};
23use crate::{MAX_PROFILE_CHUNK_DURATION, ProfileError};
24
25#[derive(Debug, Serialize, Deserialize)]
26pub struct Metadata {
27    #[serde(default, skip_serializing_if = "String::is_empty")]
28    build_id: String,
29    chunk_id: EventId,
30    profiler_id: EventId,
31
32    client_sdk: ClientSdk,
33
34    #[serde(default, skip_serializing_if = "String::is_empty")]
35    environment: String,
36    platform: String,
37    release: String,
38
39    #[serde(default)]
40    version: Version,
41
42    #[serde(skip_serializing_if = "Option::is_none")]
43    debug_meta: Option<DebugMeta>,
44
45    #[serde(default)]
46    duration_ns: u64,
47    timestamp: f64,
48}
49
50#[derive(Debug, Serialize, Deserialize)]
51pub struct Chunk {
52    #[serde(flatten)]
53    metadata: Metadata,
54
55    #[serde(default, skip_serializing)]
56    sampled_profile: String,
57
58    #[serde(default, skip_serializing_if = "Option::is_none")]
59    js_profile: Option<ProfileData>,
60
61    #[serde(default = "Chunk::default")]
62    profile: AndroidTraceLog,
63
64    #[serde(default, skip_serializing_if = "Option::is_none")]
65    measurements: Option<HashMap<String, ChunkMeasurement>>,
66}
67
68impl Chunk {
69    fn default() -> AndroidTraceLog {
70        AndroidTraceLog {
71            data_file_overflow: Default::default(),
72            clock: Clock::Global,
73            elapsed_time: Default::default(),
74            total_method_calls: Default::default(),
75            clock_call_overhead: Default::default(),
76            vm: Vm::Dalvik,
77            start_time: Utc::now(),
78            pid: Default::default(),
79            gc_trace: Default::default(),
80            threads: Default::default(),
81            methods: Default::default(),
82            events: Default::default(),
83        }
84    }
85
86    pub fn parse(payload: &[u8]) -> Result<Self, ProfileError> {
87        let d = &mut serde_json::Deserializer::from_slice(payload);
88        let mut profile: Chunk =
89            serde_path_to_error::deserialize(d).map_err(ProfileError::InvalidJson)?;
90
91        if let Some(ref mut js_profile) = profile.js_profile {
92            js_profile.normalize(profile.metadata.platform.as_str())?;
93        }
94
95        if !profile.sampled_profile.is_empty() {
96            let profile_bytes = match BASE64_NOPAD.decode(profile.sampled_profile.as_bytes()) {
97                Ok(profile) => profile,
98                Err(_) => return Err(ProfileError::InvalidBase64Value),
99            };
100            profile.profile = match android_trace_log::parse(&profile_bytes) {
101                Ok(profile) => profile,
102                Err(_) => return Err(ProfileError::InvalidSampledProfile),
103            };
104        }
105
106        if profile.profile.events.is_empty() {
107            return Err(ProfileError::NotEnoughSamples);
108        }
109
110        if profile.profile.elapsed_time > MAX_PROFILE_CHUNK_DURATION {
111            return Err(ProfileError::DurationIsTooLong);
112        }
113
114        if profile.profile.elapsed_time.is_zero() {
115            return Err(ProfileError::DurationIsZero);
116        }
117
118        // Use duration given by the profiler and not reported by the SDK.
119        profile.metadata.duration_ns = profile.profile.elapsed_time.as_nanos() as u64;
120
121        // Convert legacy Android trace version ("2") to the corrected version
122        // ("2.android-trace"). We do so during parsing rather than
123        // serialization because raw serde doesn't validate the trace payload.
124        profile.metadata.version = Version::V2AndroidTrace;
125
126        // If build_id is not empty but we don't have any DebugImage set,
127        // we create the proper Proguard image and set the uuid.
128        if !profile.metadata.build_id.is_empty() && profile.metadata.debug_meta.is_none() {
129            profile.metadata.debug_meta = Some(DebugMeta {
130                images: vec![get_proguard_image(&profile.metadata.build_id)?],
131            })
132        }
133
134        Ok(profile)
135    }
136
137    /// Serializes the [`Chunk`] into its JSON form.
138    pub fn serialize(&self) -> Result<Bytes, ProfileError> {
139        serde_json::to_vec(self)
140            .map(Bytes::from)
141            .map_err(|_| ProfileError::CannotSerializePayload)
142    }
143}
144
145impl crate::profile_chunk::ProfileChunk for Chunk {
146    fn platform(&self) -> &str {
147        &self.metadata.platform
148    }
149
150    fn normalize(&mut self) -> Result<(), ProfileError> {
151        Ok(())
152    }
153}
154
155impl relay_filter::Filterable for Chunk {
156    fn release(&self) -> Option<&str> {
157        Some(&self.metadata.release)
158    }
159}
160
161impl relay_protocol::Getter for Chunk {
162    fn get_value(&self, path: &str) -> Option<relay_protocol::Val<'_>> {
163        match path.strip_prefix(crate::PROFIL_GETTER_PREFIX)? {
164            "release" => Some(self.metadata.release.as_str().into()),
165            "platform" => Some(self.metadata.platform.as_str().into()),
166            _ => None,
167        }
168    }
169}
170
171#[cfg(test)]
172mod tests {
173    use super::*;
174
175    #[test]
176    fn test_roundtrip() {
177        let payload = include_bytes!("../../tests/fixtures/android/chunk/valid.json");
178        let profile = Chunk::parse(payload).unwrap();
179        let data = profile.serialize();
180        assert!(Chunk::parse(&(data.unwrap())[..]).is_ok());
181    }
182
183    #[test]
184    fn test_roundtrip_react_native() {
185        let payload = include_bytes!("../../tests/fixtures/android/chunk/valid-rn.json");
186        let profile = Chunk::parse(payload).unwrap();
187        let data = serde_json::to_vec(&profile);
188        assert!(Chunk::parse(&(data.unwrap())[..]).is_ok());
189    }
190
191    #[test]
192    fn test_parse_corrects_android_trace_profile_version() {
193        let payload = include_bytes!("../../tests/fixtures/android/chunk/valid.json");
194        let input: serde_json::Value = serde_json::from_slice(payload).unwrap();
195        assert_eq!(input["version"], "2");
196
197        let profile = Chunk::parse(payload).unwrap();
198        assert_eq!(profile.metadata.version, Version::V2AndroidTrace);
199
200        let output = serde_json::to_value(&profile).unwrap();
201
202        assert_eq!(output["version"], "2.android-trace");
203        assert!(output.get("sampled_profile").is_none());
204    }
205
206    #[test]
207    fn test_remove_invalid_events() {
208        let payload =
209            include_bytes!("../../tests/fixtures/android/chunk/remove_invalid_events.json");
210        let _ = Chunk::parse(payload).unwrap_err();
211    }
212}