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