relay_profiling/android/
chunk.rs1use 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 profile.metadata.duration_ns = profile.profile.elapsed_time.as_nanos() as u64;
120
121 profile.metadata.version = Version::V2AndroidTrace;
125
126 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 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}