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::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 profile.metadata.duration_ns = profile.profile.elapsed_time.as_nanos() as u64;
116
117 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 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}