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::v2::ProfileData;
21use crate::types::{ClientSdk, DebugMeta};
22use crate::{MAX_PROFILE_CHUNK_DURATION, ProfileError};
23
24#[derive(Debug, Serialize, Deserialize)]
25pub struct Metadata {
26    #[serde(default, skip_serializing_if = "String::is_empty")]
27    build_id: String,
28    chunk_id: EventId,
29    profiler_id: EventId,
30
31    client_sdk: ClientSdk,
32
33    #[serde(default, skip_serializing_if = "String::is_empty")]
34    environment: String,
35    platform: String,
36    release: String,
37
38    #[serde(skip_serializing_if = "Option::is_none")]
39    debug_meta: Option<DebugMeta>,
40
41    #[serde(default)]
42    duration_ns: u64,
43    timestamp: f64,
44}
45
46#[derive(Debug, Serialize, Deserialize)]
47pub struct Chunk {
48    #[serde(flatten)]
49    metadata: Metadata,
50
51    #[serde(default, skip_serializing)]
52    sampled_profile: String,
53
54    #[serde(default, skip_serializing_if = "Option::is_none")]
55    js_profile: Option<ProfileData>,
56
57    #[serde(default = "Chunk::default")]
58    profile: AndroidTraceLog,
59
60    #[serde(default, skip_serializing_if = "Option::is_none")]
61    measurements: Option<HashMap<String, ChunkMeasurement>>,
62}
63
64impl Chunk {
65    fn default() -> AndroidTraceLog {
66        AndroidTraceLog {
67            data_file_overflow: Default::default(),
68            clock: Clock::Global,
69            elapsed_time: Default::default(),
70            total_method_calls: Default::default(),
71            clock_call_overhead: Default::default(),
72            vm: Vm::Dalvik,
73            start_time: Utc::now(),
74            pid: Default::default(),
75            gc_trace: Default::default(),
76            threads: Default::default(),
77            methods: Default::default(),
78            events: Default::default(),
79        }
80    }
81
82    pub fn parse(payload: &[u8]) -> Result<Self, ProfileError> {
83        let d = &mut serde_json::Deserializer::from_slice(payload);
84        let mut profile: Chunk =
85            serde_path_to_error::deserialize(d).map_err(ProfileError::InvalidJson)?;
86
87        if let Some(ref mut js_profile) = profile.js_profile {
88            js_profile.normalize(profile.metadata.platform.as_str())?;
89        }
90
91        if !profile.sampled_profile.is_empty() {
92            let profile_bytes = match BASE64_NOPAD.decode(profile.sampled_profile.as_bytes()) {
93                Ok(profile) => profile,
94                Err(_) => return Err(ProfileError::InvalidBase64Value),
95            };
96            profile.profile = match android_trace_log::parse(&profile_bytes) {
97                Ok(profile) => profile,
98                Err(_) => return Err(ProfileError::InvalidSampledProfile),
99            };
100        }
101
102        if profile.profile.events.is_empty() {
103            return Err(ProfileError::NotEnoughSamples);
104        }
105
106        if profile.profile.elapsed_time > MAX_PROFILE_CHUNK_DURATION {
107            return Err(ProfileError::DurationIsTooLong);
108        }
109
110        if profile.profile.elapsed_time.is_zero() {
111            return Err(ProfileError::DurationIsZero);
112        }
113
114        // Use duration given by the profiler and not reported by the SDK.
115        profile.metadata.duration_ns = profile.profile.elapsed_time.as_nanos() as u64;
116
117        // If build_id is not empty but we don't have any DebugImage set,
118        // we create the proper Proguard image and set the uuid.
119        if !profile.metadata.build_id.is_empty() && profile.metadata.debug_meta.is_none() {
120            profile.metadata.debug_meta = Some(DebugMeta {
121                images: vec![get_proguard_image(&profile.metadata.build_id)?],
122            })
123        }
124
125        Ok(profile)
126    }
127
128    /// Serializes the [`Chunk`] into its JSON form.
129    pub fn serialize(&self) -> Result<Bytes, ProfileError> {
130        serde_json::to_vec(self)
131            .map(Bytes::from)
132            .map_err(|_| ProfileError::CannotSerializePayload)
133    }
134}
135
136impl crate::profile_chunk::ProfileChunk for Chunk {
137    fn platform(&self) -> &str {
138        &self.metadata.platform
139    }
140
141    fn normalize(&mut self) -> Result<(), ProfileError> {
142        Ok(())
143    }
144}
145
146impl relay_filter::Filterable for Chunk {
147    fn release(&self) -> Option<&str> {
148        Some(&self.metadata.release)
149    }
150}
151
152impl relay_protocol::Getter for Chunk {
153    fn get_value(&self, path: &str) -> Option<relay_protocol::Val<'_>> {
154        match path.strip_prefix(crate::PROFIL_GETTER_PREFIX)? {
155            "release" => Some(self.metadata.release.as_str().into()),
156            "platform" => Some(self.metadata.platform.as_str().into()),
157            _ => None,
158        }
159    }
160}
161
162#[cfg(test)]
163mod tests {
164    use super::*;
165
166    #[test]
167    fn test_roundtrip() {
168        let payload = include_bytes!("../../tests/fixtures/android/chunk/valid.json");
169        let profile = Chunk::parse(payload).unwrap();
170        let data = profile.serialize();
171        assert!(Chunk::parse(&(data.unwrap())[..]).is_ok());
172    }
173
174    #[test]
175    fn test_roundtrip_react_native() {
176        let payload = include_bytes!("../../tests/fixtures/android/chunk/valid-rn.json");
177        let profile = Chunk::parse(payload).unwrap();
178        let data = serde_json::to_vec(&profile);
179        assert!(Chunk::parse(&(data.unwrap())[..]).is_ok());
180    }
181
182    #[test]
183    fn test_remove_invalid_events() {
184        let payload =
185            include_bytes!("../../tests/fixtures/android/chunk/remove_invalid_events.json");
186        let _ = Chunk::parse(payload).unwrap_err();
187    }
188}