relay_profiling/android/
chunk.rs1use 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 profile.metadata.duration_ns = profile.profile.elapsed_time.as_nanos() as u64;
121
122 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}