1use std::collections::{BTreeMap, HashMap, HashSet};
8use std::ops::Range;
9
10use chrono::{DateTime, Utc};
11use itertools::Itertools;
12use relay_event_schema::protocol::EventId;
13use serde::{Deserialize, Serialize};
14
15use crate::MAX_PROFILE_DURATION;
16use crate::error::ProfileError;
17use crate::measurements::LegacyMeasurement;
18use crate::sample::{DebugMeta, Frame, ThreadMetadata, Version};
19use crate::transaction_metadata::TransactionMetadata;
20use crate::types::ClientSdk;
21use crate::utils::{default_client_sdk, deserialize_number_from_string, string_is_null_or_empty};
22
23const MAX_PROFILE_DURATION_NS: u64 = MAX_PROFILE_DURATION.as_nanos() as u64;
24
25#[derive(Debug, Serialize, Deserialize, Clone)]
26struct Sample {
27 stack_id: usize,
28 #[serde(deserialize_with = "deserialize_number_from_string")]
29 thread_id: u64,
30 #[serde(deserialize_with = "deserialize_number_from_string")]
31 elapsed_since_start_ns: u64,
32
33 #[serde(default, skip_serializing_if = "Option::is_none")]
35 queue_address: Option<String>,
36}
37
38#[derive(Debug, Serialize, Deserialize, Clone)]
39struct QueueMetadata {
40 label: String,
41}
42
43#[derive(Debug, Serialize, Deserialize, Clone)]
44pub struct SampleProfile {
45 samples: Vec<Sample>,
46 stacks: Vec<Vec<usize>>,
47 frames: Vec<Frame>,
48
49 #[serde(default, skip_serializing_if = "Option::is_none")]
50 thread_metadata: Option<HashMap<String, ThreadMetadata>>,
51
52 #[serde(default, skip_serializing_if = "Option::is_none")]
54 queue_metadata: Option<HashMap<String, QueueMetadata>>,
55}
56
57impl SampleProfile {
58 pub fn normalize(&mut self, platform: &str, architecture: &str) -> Result<(), ProfileError> {
66 self.remove_idle_samples_at_the_edge();
68 self.remove_single_samples_per_thread();
69
70 if self.samples.is_empty() {
71 return Err(ProfileError::NotEnoughSamples);
72 }
73
74 if !self.all_stacks_referenced_by_samples_exist() {
75 return Err(ProfileError::MalformedSamples);
76 }
77
78 if !self.all_frames_referenced_by_stacks_exist() {
79 return Err(ProfileError::MalformedStacks);
80 }
81
82 if self.is_above_max_duration() {
83 return Err(ProfileError::DurationIsTooLong);
84 }
85
86 self.strip_pointer_authentication_code(platform, architecture);
87 self.remove_unreferenced_threads();
88 self.remove_unreferenced_queues();
89
90 Ok(())
91 }
92
93 fn strip_pointer_authentication_code(&mut self, platform: &str, architecture: &str) {
94 let addr = match (platform, architecture) {
95 ("cocoa", "arm64") | ("cocoa", "arm64e") => 0x0000000FFFFFFFFF,
97 _ => return,
98 };
99 for frame in &mut self.frames {
100 frame.strip_pointer_authentication_code(addr);
101 }
102 }
103
104 fn remove_idle_samples_at_the_edge(&mut self) {
105 let mut active_ranges: HashMap<u64, Range<usize>> = HashMap::new();
106
107 for (i, sample) in self.samples.iter().enumerate() {
108 if self
109 .stacks
110 .get(sample.stack_id)
111 .is_none_or(|stack| stack.is_empty())
112 {
113 continue;
114 }
115
116 active_ranges
117 .entry(sample.thread_id)
118 .and_modify(|range| range.end = i + 1)
119 .or_insert(i..i + 1);
120 }
121
122 self.samples = self
123 .samples
124 .drain(..)
125 .enumerate()
126 .filter(|(i, sample)| {
127 active_ranges
128 .get(&sample.thread_id)
129 .is_some_and(|range| range.contains(i))
130 })
131 .map(|(_, sample)| sample)
132 .collect();
133 }
134
135 fn remove_single_samples_per_thread(&mut self) {
137 let sample_count_by_thread_id = &self
138 .samples
139 .iter()
140 .counts_by(|sample| sample.thread_id)
141 .into_iter()
143 .filter(|(_, count)| *count > 1)
144 .collect::<HashMap<_, _>>();
145
146 self.samples
147 .retain(|sample| sample_count_by_thread_id.contains_key(&sample.thread_id));
148 }
149
150 fn all_stacks_referenced_by_samples_exist(&self) -> bool {
152 self.samples
153 .iter()
154 .all(|sample| self.stacks.get(sample.stack_id).is_some())
155 }
156
157 fn all_frames_referenced_by_stacks_exist(&self) -> bool {
159 self.stacks.iter().all(|stack| {
160 stack
161 .iter()
162 .all(|frame_id| self.frames.get(*frame_id).is_some())
163 })
164 }
165
166 fn is_above_max_duration(&self) -> bool {
168 self.samples
169 .last()
170 .is_some_and(|sample| sample.elapsed_since_start_ns > MAX_PROFILE_DURATION_NS)
171 }
172
173 fn remove_unreferenced_threads(&mut self) {
174 if let Some(thread_metadata) = &mut self.thread_metadata {
175 let thread_ids = self
176 .samples
177 .iter()
178 .map(|sample| sample.thread_id.to_string())
179 .collect::<HashSet<_>>();
180 thread_metadata.retain(|thread_id, _| thread_ids.contains(thread_id));
181 }
182 }
183
184 fn remove_unreferenced_queues(&mut self) {
185 if let Some(queue_metadata) = &mut self.queue_metadata {
186 let queue_addresses = self
187 .samples
188 .iter()
189 .filter_map(|sample| sample.queue_address.as_ref())
190 .collect::<HashSet<_>>();
191 queue_metadata.retain(|queue_address, _| queue_addresses.contains(&queue_address));
192 }
193 }
194}
195
196#[derive(Debug, Serialize, Deserialize, Clone)]
197struct OSMetadata {
198 name: String,
199 version: String,
200
201 #[serde(default, skip_serializing_if = "Option::is_none")]
202 build_number: Option<String>,
203}
204
205#[derive(Debug, Serialize, Deserialize, Clone)]
206struct RuntimeMetadata {
207 name: String,
208 version: String,
209}
210
211#[derive(Debug, Serialize, Deserialize, Clone)]
212struct DeviceMetadata {
213 architecture: String,
214
215 #[serde(default, skip_serializing_if = "Option::is_none")]
216 is_emulator: Option<bool>,
217 #[serde(default, skip_serializing_if = "Option::is_none")]
218 locale: Option<String>,
219 #[serde(default, skip_serializing_if = "Option::is_none")]
220 manufacturer: Option<String>,
221 #[serde(default, skip_serializing_if = "Option::is_none")]
222 model: Option<String>,
223}
224
225#[derive(Debug, Serialize, Deserialize, Clone)]
226pub struct ProfileMetadata {
227 version: Version,
228
229 #[serde(skip_serializing_if = "Option::is_none")]
230 debug_meta: Option<DebugMeta>,
231
232 device: DeviceMetadata,
233 os: OSMetadata,
234 #[serde(skip_serializing_if = "Option::is_none")]
235 runtime: Option<RuntimeMetadata>,
236
237 #[serde(default, skip_serializing_if = "String::is_empty")]
238 environment: String,
239 #[serde(alias = "profile_id")]
240 event_id: EventId,
241 platform: String,
242 timestamp: DateTime<Utc>,
243
244 #[serde(default, skip_serializing_if = "string_is_null_or_empty")]
245 release: Option<String>,
246 #[serde(default, skip_serializing_if = "String::is_empty")]
247 dist: String,
248
249 #[serde(default, skip_serializing_if = "Vec::is_empty")]
250 transactions: Vec<TransactionMetadata>,
251 #[serde(default, skip_serializing_if = "Option::is_none")]
252 transaction: Option<TransactionMetadata>,
253
254 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
255 transaction_metadata: BTreeMap<String, String>,
256
257 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
258 transaction_tags: BTreeMap<String, String>,
259
260 #[serde(skip_serializing_if = "Option::is_none")]
261 pub client_sdk: Option<ClientSdk>,
262}
263
264#[derive(Debug, Serialize, Deserialize, Clone)]
265pub struct ProfilingEvent {
266 #[serde(default, skip_serializing_if = "Option::is_none")]
267 measurements: Option<HashMap<String, LegacyMeasurement>>,
268 #[serde(flatten)]
269 metadata: ProfileMetadata,
270 profile: SampleProfile,
271}
272
273impl ProfilingEvent {
274 fn valid(&self) -> bool {
275 match self.metadata.platform.as_str() {
276 "cocoa" => {
277 self.metadata.os.build_number.is_some()
278 && self.metadata.device.is_emulator.is_some()
279 && self.metadata.device.locale.is_some()
280 && self.metadata.device.manufacturer.is_some()
281 && self.metadata.device.model.is_some()
282 }
283 _ => true,
284 }
285 }
286}
287
288fn parse_profile(payload: &[u8]) -> Result<ProfilingEvent, ProfileError> {
289 let d = &mut serde_json::Deserializer::from_slice(payload);
290 let mut profile: ProfilingEvent =
291 serde_path_to_error::deserialize(d).map_err(ProfileError::InvalidJson)?;
292
293 if !profile.valid() {
294 return Err(ProfileError::MissingProfileMetadata);
295 }
296
297 if profile.metadata.transaction.is_none() {
298 profile.metadata.transaction = profile.metadata.transactions.drain(..).next();
299 }
300
301 let transaction = profile
302 .metadata
303 .transaction
304 .as_ref()
305 .ok_or(ProfileError::NoTransactionAssociated)?;
306
307 if !transaction.valid() {
308 return Err(ProfileError::InvalidTransactionMetadata);
309 }
310
311 if transaction.relative_end_ns > 0 {
313 profile.profile.samples.retain(|sample| {
314 (transaction.relative_start_ns..=transaction.relative_end_ns)
315 .contains(&sample.elapsed_since_start_ns)
316 });
317 }
318
319 profile.profile.normalize(
320 profile.metadata.platform.as_str(),
321 profile.metadata.device.architecture.as_str(),
322 )?;
323
324 Ok(profile)
325}
326
327pub fn parse_sample_profile(
328 payload: &[u8],
329 transaction_metadata: BTreeMap<String, String>,
330 transaction_tags: BTreeMap<String, String>,
331) -> Result<Vec<u8>, ProfileError> {
332 let mut profile = parse_profile(payload)?;
333
334 if let Some(transaction_name) = transaction_metadata.get("transaction") {
335 if let Some(ref mut transaction) = profile.metadata.transaction {
336 transaction_name.clone_into(&mut transaction.name)
337 }
338 }
339
340 if profile.metadata.release.is_none() {
342 if let Some(release) = transaction_metadata.get("release") {
343 profile.metadata.release = Some(release.to_owned());
344 }
345 }
346
347 if let Some(dist) = transaction_metadata.get("dist") {
348 dist.clone_into(&mut profile.metadata.dist);
349 }
350
351 if let Some(environment) = transaction_metadata.get("environment") {
352 environment.clone_into(&mut profile.metadata.environment);
353 }
354
355 if let Some(segment_id) = transaction_metadata
356 .get("segment_id")
357 .and_then(|segment_id| segment_id.parse().ok())
358 {
359 if let Some(transaction_metadata) = profile.metadata.transaction.as_mut() {
360 transaction_metadata.segment_id = Some(segment_id);
361 }
362 }
363
364 profile.metadata.client_sdk = match (
365 transaction_metadata.get("client_sdk.name"),
366 transaction_metadata.get("client_sdk.version"),
367 ) {
368 (Some(name), Some(version)) => Some(ClientSdk {
369 name: name.to_owned(),
370 version: version.to_owned(),
371 }),
372 _ => default_client_sdk(profile.metadata.platform.as_str()),
373 };
374 profile.metadata.transaction_metadata = transaction_metadata;
375 profile.metadata.transaction_tags = transaction_tags;
376
377 serde_json::to_vec(&profile).map_err(|_| ProfileError::CannotSerializePayload)
378}
379
380#[cfg(test)]
381mod tests {
382 use super::*;
383 use std::time::Duration;
384
385 #[test]
386 fn test_roundtrip() {
387 let payload = include_bytes!("../../tests/fixtures/sample/v1/valid.json");
388 let profile = parse_profile(payload);
389 assert!(profile.is_ok());
390 let data = serde_json::to_vec(&profile.unwrap());
391 assert!(parse_profile(&data.unwrap()[..]).is_ok());
392 }
393
394 #[test]
395 fn test_expand() {
396 let payload = include_bytes!("../../tests/fixtures/sample/v1/valid.json");
397 let profile = parse_sample_profile(payload, BTreeMap::new(), BTreeMap::new());
398 assert!(profile.is_ok());
399 }
400
401 fn generate_profile() -> ProfilingEvent {
402 ProfilingEvent {
403 measurements: None,
404 metadata: ProfileMetadata {
405 debug_meta: Option::None,
406 version: Version::V1,
407 timestamp: Utc::now(),
408 runtime: Option::None,
409 device: DeviceMetadata {
410 architecture: "arm64e".to_owned(),
411 is_emulator: Some(true),
412 locale: Some("en_US".to_owned()),
413 manufacturer: Some("Apple".to_owned()),
414 model: Some("iPhome11,3".to_owned()),
415 },
416 os: OSMetadata {
417 build_number: Some("H3110".to_owned()),
418 name: "iOS".to_owned(),
419 version: "16.0".to_owned(),
420 },
421 environment: "testing".to_owned(),
422 platform: "cocoa".to_owned(),
423 event_id: EventId::new(),
424 transaction: Option::None,
425 transactions: Vec::new(),
426 release: Some("1.0".to_owned()),
427 dist: "9999".to_owned(),
428 transaction_metadata: BTreeMap::new(),
429 transaction_tags: BTreeMap::new(),
430 client_sdk: None,
431 },
432 profile: SampleProfile {
433 queue_metadata: Some(HashMap::new()),
434 samples: Vec::new(),
435 stacks: Vec::new(),
436 frames: Vec::new(),
437 thread_metadata: Some(HashMap::new()),
438 },
439 }
440 }
441
442 #[test]
443 fn test_filter_samples() {
444 let mut profile = generate_profile();
445
446 profile.profile.stacks.push(Vec::new());
447 profile.profile.samples.extend(vec![
448 Sample {
449 stack_id: 0,
450 queue_address: Some("0xdeadbeef".to_owned()),
451 elapsed_since_start_ns: 1,
452 thread_id: 1,
453 },
454 Sample {
455 stack_id: 0,
456 queue_address: Some("0xdeadbeef".to_owned()),
457 elapsed_since_start_ns: 1,
458 thread_id: 1,
459 },
460 Sample {
461 stack_id: 0,
462 queue_address: Some("0xdeadbeef".to_owned()),
463 elapsed_since_start_ns: 1,
464 thread_id: 2,
465 },
466 Sample {
467 stack_id: 0,
468 queue_address: Some("0xdeadbeef".to_owned()),
469 elapsed_since_start_ns: 1,
470 thread_id: 3,
471 },
472 ]);
473
474 profile.profile.remove_single_samples_per_thread();
475
476 assert!(profile.profile.samples.len() == 2);
477 }
478
479 #[test]
480 fn test_parse_profile_with_all_samples_filtered() {
481 let mut profile = generate_profile();
482
483 profile.profile.stacks.push(Vec::new());
484 profile.profile.samples.extend(vec![
485 Sample {
486 stack_id: 0,
487 queue_address: Some("0xdeadbeef".to_owned()),
488 elapsed_since_start_ns: 1,
489 thread_id: 1,
490 },
491 Sample {
492 stack_id: 0,
493 queue_address: Some("0xdeadbeef".to_owned()),
494 elapsed_since_start_ns: 1,
495 thread_id: 2,
496 },
497 Sample {
498 stack_id: 0,
499 queue_address: Some("0xdeadbeef".to_owned()),
500 elapsed_since_start_ns: 1,
501 thread_id: 3,
502 },
503 Sample {
504 stack_id: 0,
505 queue_address: Some("0xdeadbeef".to_owned()),
506 elapsed_since_start_ns: 1,
507 thread_id: 4,
508 },
509 ]);
510
511 let payload = serde_json::to_vec(&profile).unwrap();
512 assert!(parse_profile(&payload[..]).is_err());
513 }
514
515 #[test]
516 fn test_expand_with_samples_inclusive() {
517 let mut profile = generate_profile();
518
519 profile.profile.frames.push(Frame {
520 ..Default::default()
521 });
522 profile.metadata.transaction = Some(TransactionMetadata {
523 active_thread_id: 1,
524 id: EventId::new(),
525 name: "blah".to_owned(),
526 relative_cpu_end_ms: 0,
527 relative_cpu_start_ms: 0,
528 relative_end_ns: 30,
529 relative_start_ns: 10,
530 trace_id: EventId::new(),
531 segment_id: Some("bd2eb23da2beb459".parse().unwrap()),
532 });
533 profile.profile.stacks.push(vec![0]);
534 profile.profile.samples.extend([
535 Sample {
536 stack_id: 0,
537 queue_address: Some("0xdeadbeef".to_owned()),
538 elapsed_since_start_ns: 10,
539 thread_id: 1,
540 },
541 Sample {
542 stack_id: 0,
543 queue_address: Some("0xdeadbeef".to_owned()),
544 elapsed_since_start_ns: 20,
545 thread_id: 1,
546 },
547 Sample {
548 stack_id: 0,
549 queue_address: Some("0xdeadbeef".to_owned()),
550 elapsed_since_start_ns: 30,
551 thread_id: 1,
552 },
553 Sample {
554 stack_id: 0,
555 queue_address: Some("0xdeadbeef".to_owned()),
556 elapsed_since_start_ns: 40,
557 thread_id: 1,
558 },
559 ]);
560
561 let payload = serde_json::to_vec(&profile).unwrap();
562 let profile = parse_profile(&payload[..]).unwrap();
563
564 assert_eq!(profile.profile.samples.len(), 3);
565 }
566
567 #[test]
568 fn test_expand_with_all_samples_outside_transaction() {
569 let mut profile = generate_profile();
570
571 profile.profile.frames.push(Frame {
572 ..Default::default()
573 });
574 profile.metadata.transaction = Some(TransactionMetadata {
575 active_thread_id: 1,
576 id: EventId::new(),
577 name: "blah".to_owned(),
578 relative_cpu_end_ms: 0,
579 relative_cpu_start_ms: 0,
580 relative_end_ns: 100,
581 relative_start_ns: 50,
582 trace_id: EventId::new(),
583 segment_id: Some("bd2eb23da2beb459".parse().unwrap()),
584 });
585 profile.profile.stacks.push(vec![0]);
586 profile.profile.samples.extend(vec![
587 Sample {
588 stack_id: 0,
589 queue_address: Some("0xdeadbeef".to_owned()),
590 elapsed_since_start_ns: 10,
591 thread_id: 1,
592 },
593 Sample {
594 stack_id: 0,
595 queue_address: Some("0xdeadbeef".to_owned()),
596 elapsed_since_start_ns: 20,
597 thread_id: 1,
598 },
599 Sample {
600 stack_id: 0,
601 queue_address: Some("0xdeadbeef".to_owned()),
602 elapsed_since_start_ns: 30,
603 thread_id: 1,
604 },
605 Sample {
606 stack_id: 0,
607 queue_address: Some("0xdeadbeef".to_owned()),
608 elapsed_since_start_ns: 40,
609 thread_id: 1,
610 },
611 ]);
612
613 let payload = serde_json::to_vec(&profile).unwrap();
614 let data = parse_sample_profile(&payload[..], BTreeMap::new(), BTreeMap::new());
615
616 assert!(data.is_err());
617 }
618
619 #[test]
620 fn test_copying_transaction() {
621 let mut profile = generate_profile();
622 let transaction = TransactionMetadata {
623 active_thread_id: 1,
624 id: EventId::new(),
625 name: "blah".to_owned(),
626 relative_cpu_end_ms: 0,
627 relative_cpu_start_ms: 0,
628 relative_end_ns: 100,
629 relative_start_ns: 0,
630 trace_id: EventId::new(),
631 segment_id: Some("bd2eb23da2beb459".parse().unwrap()),
632 };
633
634 profile.metadata.transactions.push(transaction.clone());
635 profile.profile.frames.push(Frame {
636 ..Default::default()
637 });
638 profile.profile.stacks.push(vec![0]);
639 profile.profile.samples.extend(vec![
640 Sample {
641 stack_id: 0,
642 queue_address: Some("0xdeadbeef".to_owned()),
643 elapsed_since_start_ns: 10,
644 thread_id: 1,
645 },
646 Sample {
647 stack_id: 0,
648 queue_address: Some("0xdeadbeef".to_owned()),
649 elapsed_since_start_ns: 20,
650 thread_id: 1,
651 },
652 Sample {
653 stack_id: 0,
654 queue_address: Some("0xdeadbeef".to_owned()),
655 elapsed_since_start_ns: 30,
656 thread_id: 1,
657 },
658 Sample {
659 stack_id: 0,
660 queue_address: Some("0xdeadbeef".to_owned()),
661 elapsed_since_start_ns: 40,
662 thread_id: 1,
663 },
664 ]);
665
666 let payload = serde_json::to_vec(&profile).unwrap();
667 let profile = parse_profile(&payload[..]).unwrap();
668
669 assert_eq!(Some(transaction), profile.metadata.transaction);
670 assert!(profile.metadata.transactions.is_empty());
671 }
672
673 #[test]
674 fn test_parse_with_no_transaction() {
675 let profile = generate_profile();
676 let payload = serde_json::to_vec(&profile).unwrap();
677 assert!(parse_profile(&payload[..]).is_err());
678 }
679
680 #[test]
681 fn test_profile_remove_idle_samples_at_start_and_end() {
682 let mut profile = generate_profile();
683 let transaction = TransactionMetadata {
684 active_thread_id: 1,
685 id: EventId::new(),
686 name: "blah".to_owned(),
687 relative_cpu_end_ms: 0,
688 relative_cpu_start_ms: 0,
689 relative_end_ns: 100,
690 relative_start_ns: 0,
691 trace_id: EventId::new(),
692 segment_id: Some("bd2eb23da2beb459".parse().unwrap()),
693 };
694
695 profile.metadata.transaction = Some(transaction);
696 profile.profile.frames.push(Frame {
697 ..Default::default()
698 });
699 profile.profile.stacks = vec![vec![0], vec![]];
700 profile.profile.samples = vec![
701 Sample {
702 stack_id: 0,
703 queue_address: Some("0xdeadbeef".to_owned()),
704 elapsed_since_start_ns: 40,
705 thread_id: 2,
706 },
707 Sample {
708 stack_id: 1,
709 queue_address: Some("0xdeadbeef".to_owned()),
710 elapsed_since_start_ns: 50,
711 thread_id: 2,
712 },
713 Sample {
714 stack_id: 1,
715 queue_address: Some("0xdeadbeef".to_owned()),
716 elapsed_since_start_ns: 10,
717 thread_id: 1,
718 },
719 Sample {
720 stack_id: 1,
721 queue_address: Some("0xdeadbeef".to_owned()),
722 elapsed_since_start_ns: 20,
723 thread_id: 1,
724 },
725 Sample {
726 stack_id: 1,
727 queue_address: Some("0xdeadbeef".to_owned()),
728 elapsed_since_start_ns: 30,
729 thread_id: 1,
730 },
731 Sample {
732 stack_id: 0,
733 queue_address: Some("0xdeadbeef".to_owned()),
734 elapsed_since_start_ns: 40,
735 thread_id: 1,
736 },
737 Sample {
738 stack_id: 1,
739 queue_address: Some("0xdeadbeef".to_owned()),
740 elapsed_since_start_ns: 50,
741 thread_id: 1,
742 },
743 Sample {
744 stack_id: 0,
745 queue_address: Some("0xdeadbeef".to_owned()),
746 elapsed_since_start_ns: 60,
747 thread_id: 1,
748 },
749 Sample {
750 stack_id: 1,
751 queue_address: Some("0xdeadbeef".to_owned()),
752 elapsed_since_start_ns: 70,
753 thread_id: 1,
754 },
755 Sample {
756 stack_id: 1,
757 queue_address: Some("0xdeadbeef".to_owned()),
758 elapsed_since_start_ns: 90,
759 thread_id: 1,
760 },
761 Sample {
762 stack_id: 1,
763 queue_address: Some("0xdeadbeef".to_owned()),
764 elapsed_since_start_ns: 80,
765 thread_id: 3,
766 },
767 Sample {
768 stack_id: 1,
769 queue_address: Some("0xdeadbeef".to_owned()),
770 elapsed_since_start_ns: 90,
771 thread_id: 3,
772 },
773 Sample {
774 stack_id: 0,
775 queue_address: Some("0xdeadbeef".to_owned()),
776 elapsed_since_start_ns: 60,
777 thread_id: 2,
778 },
779 ];
780
781 profile.profile.remove_idle_samples_at_the_edge();
782
783 let mut sample_count_by_thread_id: HashMap<u64, u32> = HashMap::new();
784
785 for sample in &profile.profile.samples {
786 *sample_count_by_thread_id
787 .entry(sample.thread_id)
788 .or_default() += 1;
789 }
790
791 assert_eq!(sample_count_by_thread_id, HashMap::from([(1, 3), (2, 3),]));
792 }
793
794 #[test]
795 fn test_profile_cleanup_metadata() {
796 let mut profile = generate_profile();
797 let transaction = TransactionMetadata {
798 active_thread_id: 1,
799 id: EventId::new(),
800 name: "blah".to_owned(),
801 relative_cpu_end_ms: 0,
802 relative_cpu_start_ms: 0,
803 relative_end_ns: 100,
804 relative_start_ns: 0,
805 trace_id: EventId::new(),
806 segment_id: Some("bd2eb23da2beb459".parse().unwrap()),
807 };
808
809 profile.metadata.transaction = Some(transaction);
810 profile.profile.frames.push(Frame {
811 ..Default::default()
812 });
813 profile.profile.stacks = vec![vec![0]];
814
815 let mut thread_metadata: HashMap<String, ThreadMetadata> = HashMap::new();
816
817 thread_metadata.insert(
818 "1".to_owned(),
819 ThreadMetadata {
820 name: Some("".to_owned()),
821 priority: Some(1),
822 },
823 );
824 thread_metadata.insert(
825 "2".to_owned(),
826 ThreadMetadata {
827 name: Some("".to_owned()),
828 priority: Some(1),
829 },
830 );
831 thread_metadata.insert(
832 "3".to_owned(),
833 ThreadMetadata {
834 name: Some("".to_owned()),
835 priority: Some(1),
836 },
837 );
838 thread_metadata.insert(
839 "4".to_owned(),
840 ThreadMetadata {
841 name: Some("".to_owned()),
842 priority: Some(1),
843 },
844 );
845
846 let mut queue_metadata: HashMap<String, QueueMetadata> = HashMap::new();
847
848 queue_metadata.insert(
849 "0xdeadbeef".to_owned(),
850 QueueMetadata {
851 label: "com.apple.main-thread".to_owned(),
852 },
853 );
854
855 queue_metadata.insert(
856 "0x123456789".to_owned(),
857 QueueMetadata {
858 label: "some-label".to_owned(),
859 },
860 );
861
862 profile.profile.thread_metadata = Some(thread_metadata);
863 profile.profile.queue_metadata = Some(queue_metadata);
864 profile.profile.samples.extend(vec![
865 Sample {
866 stack_id: 0,
867 queue_address: Some("0xdeadbeef".to_owned()),
868 elapsed_since_start_ns: 10,
869 thread_id: 1,
870 },
871 Sample {
872 stack_id: 0,
873 queue_address: Some("0xdeadbeef".to_owned()),
874 elapsed_since_start_ns: 20,
875 thread_id: 2,
876 },
877 ]);
878
879 profile.profile.remove_unreferenced_threads();
880 profile.profile.remove_unreferenced_queues();
881
882 assert_eq!(profile.profile.thread_metadata.unwrap().len(), 2);
883 assert_eq!(profile.profile.queue_metadata.unwrap().len(), 1);
884 }
885
886 #[test]
887 fn test_extract_transaction_tags() {
888 let transaction_metadata = BTreeMap::from([(
889 "transaction".to_owned(),
890 "some-random-transaction".to_owned(),
891 )]);
892
893 let payload = include_bytes!("../../tests/fixtures/sample/v1/valid.json");
894 let profile_json = parse_sample_profile(payload, transaction_metadata, BTreeMap::new());
895 assert!(profile_json.is_ok());
896
897 let payload = profile_json.unwrap();
898 let d = &mut serde_json::Deserializer::from_slice(&payload[..]);
899 let output: ProfilingEvent = serde_path_to_error::deserialize(d)
900 .map_err(ProfileError::InvalidJson)
901 .unwrap();
902 assert_eq!(
903 output.metadata.transaction.unwrap().name,
904 "some-random-transaction".to_owned()
905 );
906 }
907
908 #[test]
909 fn test_keep_profile_under_max_duration() {
910 let mut profile = generate_profile();
911 profile.profile.samples.extend(vec![
912 Sample {
913 stack_id: 0,
914 queue_address: Some("0xdeadbeef".to_owned()),
915 elapsed_since_start_ns: 10,
916 thread_id: 1,
917 },
918 Sample {
919 stack_id: 0,
920 queue_address: Some("0xdeadbeef".to_owned()),
921 elapsed_since_start_ns: (MAX_PROFILE_DURATION - Duration::from_secs(1)).as_nanos()
922 as u64,
923 thread_id: 2,
924 },
925 ]);
926
927 assert!(!profile.profile.is_above_max_duration());
928 }
929
930 #[test]
931 fn test_reject_profile_over_max_duration() {
932 let mut profile = generate_profile();
933 profile.profile.samples.extend(vec![
934 Sample {
935 stack_id: 0,
936 queue_address: Some("0xdeadbeef".to_owned()),
937 elapsed_since_start_ns: 10,
938 thread_id: 1,
939 },
940 Sample {
941 stack_id: 0,
942 queue_address: Some("0xdeadbeef".to_owned()),
943 elapsed_since_start_ns: (MAX_PROFILE_DURATION + Duration::from_secs(1)).as_nanos()
944 as u64,
945 thread_id: 2,
946 },
947 ]);
948
949 assert!(profile.profile.is_above_max_duration());
950 }
951
952 #[test]
953 fn test_accept_null_or_empty_release() {
954 let payload = r#"{
955 "version":"1",
956 "device":{
957 "architecture":"arm64e",
958 "is_emulator":true,
959 "locale":"en_US",
960 "manufacturer":"Apple",
961 "model":"iPhome11,3"
962 },
963 "os":{
964 "name":"iOS",
965 "version":"16.0",
966 "build_number":"H3110"
967 },
968 "environment":"testing",
969 "event_id":"961d6b96017644db895eafd391682003",
970 "platform":"cocoa",
971 "release":null,
972 "timestamp":"2023-11-01T15:27:15.081230Z",
973 "transaction":{
974 "active_thread_id": 1,
975 "id":"9789498b-6970-4dda-b2a1-f9cb91d1a445",
976 "name":"blah",
977 "trace_id":"809ff2c0-e185-4c21-8f21-6a6fef009352",
978 "segment_id":"bd2eb23da2beb459"
979 },
980 "dist":"9999",
981 "profile":{
982 "samples":[
983 {
984 "stack_id":0,
985 "elapsed_since_start_ns":1,
986 "thread_id":1
987 },
988 {
989 "stack_id":0,
990 "elapsed_since_start_ns":2,
991 "thread_id":1
992 }
993 ],
994 "stacks":[[0]],
995 "frames":[{
996 "function":"main"
997 }],
998 "thread_metadata":{},
999 "queue_metadata":{}
1000 }
1001 }"#;
1002 let profile = parse_profile(payload.as_bytes());
1003 assert!(profile.is_ok());
1004 assert_eq!(profile.unwrap().metadata.release, None);
1005 }
1006}