Skip to content

Commit

Permalink
ref(profiling): Refactor profile processing into its own crate (#1340)
Browse files Browse the repository at this point in the history
  • Loading branch information
phacops authored Jul 22, 2022
1 parent 0576be7 commit 62d4172
Show file tree
Hide file tree
Showing 21 changed files with 22,284 additions and 497 deletions.
3 changes: 3 additions & 0 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,6 @@

# AWS Extension
/relay-aws-extension/ @getsentry/team-web-sdk-backend

# Profiling
/relay-profiling/ @getsentry/profiling
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
**Internal**:

- Support compressed project configs in redis cache. ([#1345](https://github.com/getsentry/relay/pull/1345))
- Refactor profile processing into its own crate. ([#1340](https://github.com/getsentry/relay/pull/1340))

## 22.7.0

Expand Down
16 changes: 15 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

22 changes: 22 additions & 0 deletions relay-profiling/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
[package]
name = "relay-profiling"
authors = ["Sentry <oss@sentry.io>"]
description = "Profiling processing for Relay"
homepage = "https://getsentry.github.io/relay/"
repository = "https://github.com/getsentry/relay"
version = "22.6.0"
edition = "2018"
license-file = "../LICENSE"
publish = false

[dependencies]
failure = "0.1.8"
android_trace_log = { version = "0.2.0", features = ["serde"] }
base64 = "0.10.1"
bytes = { version = "0.4.12", features = ["serde"] }
relay-general = { path = "../relay-general" }
serde = { version = "1.0.114", features = ["derive"] }
serde_json = "1.0.55"

[dev-dependencies]
serde_test = "1.0.125"
93 changes: 93 additions & 0 deletions relay-profiling/src/android.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
use android_trace_log::AndroidTraceLog;
use serde::{Deserialize, Serialize};

use relay_general::protocol::EventId;

use crate::utils::deserialize_number_from_string;
use crate::ProfileError;

#[derive(Debug, Serialize, Deserialize)]
struct AndroidProfile {
android_api_level: u16,

#[serde(default, skip_serializing_if = "String::is_empty")]
build_id: String,

device_cpu_frequencies: Vec<u32>,
device_is_emulator: bool,
device_locale: String,
device_manufacturer: String,
device_model: String,
device_os_name: String,
device_os_version: String,

#[serde(deserialize_with = "deserialize_number_from_string")]
device_physical_memory_bytes: u64,

#[serde(deserialize_with = "deserialize_number_from_string")]
duration_ns: u64,

#[serde(default, skip_serializing_if = "String::is_empty")]
environment: String,

platform: String,
profile_id: EventId,
trace_id: EventId,
transaction_id: EventId,
transaction_name: String,
version_code: String,
version_name: String,

#[serde(default, skip_serializing)]
sampled_profile: String,

#[serde(default)]
profile: Option<AndroidTraceLog>,
}

impl AndroidProfile {
fn parse(&mut self) -> Result<(), ProfileError> {
let profile_bytes = match base64::decode(&self.sampled_profile) {
Ok(profile) => profile,
Err(_) => return Err(ProfileError::InvalidBase64Value),
};
self.profile = match android_trace_log::parse(&profile_bytes) {
Ok(profile) => Some(profile),
Err(_) => return Err(ProfileError::InvalidSampledProfile),
};
Ok(())
}
}

pub fn parse_android_profile(payload: &[u8]) -> Result<Vec<u8>, ProfileError> {
let mut profile: AndroidProfile =
serde_json::from_slice(payload).map_err(ProfileError::InvalidJson)?;

if profile.sampled_profile.is_empty() {
return Ok(Vec::new());
}

profile.parse()?;

if profile.profile.as_ref().unwrap().events.len() < 2 {
return Err(ProfileError::NotEnoughSamples);
}

match serde_json::to_vec(&profile) {
Ok(payload) => Ok(payload),
Err(_) => Err(ProfileError::CannotSerializePayload),
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_roundtrip_android() {
let payload = include_bytes!("../tests/fixtures/profiles/android.json");
let data = parse_android_profile(payload);
assert!(data.is_ok());
assert!(parse_android_profile(&(data.unwrap())[..]).is_ok());
}
}
170 changes: 170 additions & 0 deletions relay-profiling/src/cocoa.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
use std::collections::HashMap;

use serde::{de, Deserialize, Serialize};

use relay_general::protocol::{Addr, DebugId, EventId, NativeImagePath};

use crate::utils::deserialize_number_from_string;
use crate::ProfileError;

fn strip_pointer_authentication_code<'de, D>(deserializer: D) -> Result<Addr, D::Error>
where
D: de::Deserializer<'de>,
{
let addr: Addr = Deserialize::deserialize(deserializer)?;
// https://github.com/microsoft/plcrashreporter/blob/748087386cfc517936315c107f722b146b0ad1ab/Source/PLCrashAsyncThread_arm.c#L84
Ok(Addr(addr.0 & 0x0000000FFFFFFFFF))
}

#[derive(Debug, Serialize, Deserialize)]
struct Frame {
#[serde(deserialize_with = "strip_pointer_authentication_code")]
instruction_addr: Addr,
}

#[derive(Debug, Serialize, Deserialize)]
struct Sample {
frames: Vec<Frame>,

#[serde(default, skip_serializing_if = "String::is_empty")]
queue_address: String,

#[serde(deserialize_with = "deserialize_number_from_string")]
relative_timestamp_ns: u64,

#[serde(deserialize_with = "deserialize_number_from_string")]
thread_id: u64,
}

#[derive(Debug, Serialize, Deserialize)]
struct ThreadMetadata {
#[serde(default)]
is_main_thread: bool,

#[serde(default, skip_serializing_if = "String::is_empty")]
name: String,
priority: u32,
}

#[derive(Debug, Serialize, Deserialize)]
struct QueueMetadata {
label: String,
}

#[derive(Debug, Serialize, Deserialize)]
struct SampledProfile {
samples: Vec<Sample>,

#[serde(default)]
thread_metadata: HashMap<String, ThreadMetadata>,

#[serde(default)]
queue_metadata: HashMap<String, QueueMetadata>,
}

#[derive(Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "lowercase")]
enum ImageType {
MachO,
Symbolic,
}

#[derive(Debug, Serialize, Deserialize, PartialEq)]
pub struct NativeDebugImage {
#[serde(alias = "name")]
code_file: NativeImagePath,
#[serde(alias = "id")]
debug_id: DebugId,
image_addr: Addr,

#[serde(default)]
image_vmaddr: Addr,

#[serde(deserialize_with = "deserialize_number_from_string")]
image_size: u64,

#[serde(rename = "type")]
image_type: ImageType,
}

#[derive(Debug, Serialize, Deserialize)]
struct DebugMeta {
images: Vec<NativeDebugImage>,
}

#[derive(Debug, Serialize, Deserialize)]
struct CocoaProfile {
debug_meta: DebugMeta,
device_is_emulator: bool,
device_locale: String,
device_manufacturer: String,
device_model: String,
device_os_build_number: String,
device_os_name: String,
device_os_version: String,

#[serde(deserialize_with = "deserialize_number_from_string")]
duration_ns: u64,

#[serde(default, skip_serializing_if = "String::is_empty")]
environment: String,

platform: String,
profile_id: EventId,
sampled_profile: SampledProfile,
trace_id: EventId,
transaction_id: EventId,
transaction_name: String,
version_code: String,
version_name: String,
}

pub fn parse_cocoa_profile(payload: &[u8]) -> Result<Vec<u8>, ProfileError> {
let profile: CocoaProfile =
serde_json::from_slice(payload).map_err(ProfileError::InvalidJson)?;

if profile.sampled_profile.samples.len() < 2 {
return Err(ProfileError::NotEnoughSamples);
}

match serde_json::to_vec(&profile) {
Ok(payload) => Ok(payload),
Err(_) => Err(ProfileError::CannotSerializePayload),
}
}

#[cfg(test)]
mod tests {
use super::*;

use relay_general::protocol::{DebugImage, NativeDebugImage as RelayNativeDebugImage};
use relay_general::types::{Annotated, Map};

#[test]
fn test_roundtrip_cocoa() {
let payload = include_bytes!("../tests/fixtures/profiles/cocoa.json");
let data = parse_cocoa_profile(payload);
assert!(data.is_ok());
assert!(parse_cocoa_profile(&data.unwrap()[..]).is_ok());
}

#[test]
fn test_ios_debug_image_compatibility() {
let image_json = r#"{"debug_id":"32420279-25E2-34E6-8BC7-8A006A8F2425","image_addr":"0x000000010258c000","code_file":"/private/var/containers/Bundle/Application/C3511752-DD67-4FE8-9DA2-ACE18ADFAA61/TrendingMovies.app/TrendingMovies","type":"macho","image_size":1720320,"image_vmaddr":"0x0000000100000000"}"#;
let image: NativeDebugImage = serde_json::from_str(image_json).unwrap();
let json = serde_json::to_string(&image).unwrap();
let annotated = Annotated::from_json(&json[..]).unwrap();
assert_eq!(
Annotated::new(DebugImage::MachO(Box::new(RelayNativeDebugImage {
arch: Annotated::empty(),
code_file: Annotated::new("/private/var/containers/Bundle/Application/C3511752-DD67-4FE8-9DA2-ACE18ADFAA61/TrendingMovies.app/TrendingMovies".into()),
code_id: Annotated::empty(),
debug_file: Annotated::empty(),
debug_id: Annotated::new("32420279-25E2-34E6-8BC7-8A006A8F2425".parse().unwrap()),
image_addr: Annotated::new(Addr(4334338048)),
image_size: Annotated::new(1720320),
image_vmaddr: Annotated::new(Addr(4294967296)),
other: Map::new(),
}))), annotated);
}
}
17 changes: 17 additions & 0 deletions relay-profiling/src/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
use failure::Fail;

#[derive(Debug, Fail)]
pub enum ProfileError {
#[fail(display = "invalid json in profile")]
InvalidJson(#[cause] serde_json::Error),
#[fail(display = "invalid base64 value")]
InvalidBase64Value,
#[fail(display = "invalid sampled profile")]
InvalidSampledProfile,
#[fail(display = "cannot serialize payload")]
CannotSerializePayload,
#[fail(display = "not enough samples")]
NotEnoughSamples,
#[fail(display = "platform not supported")]
PlatformNotSupported,
}
Loading

0 comments on commit 62d4172

Please sign in to comment.