Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Changelog

## Unreleased

**Bug Fixes**:

- Accept Android method traces recorded on Android 17, which contain new ART header keys. ([#6523](https://github.com/getsentry/relay/pull/6523))

## 26.10.0

**Breaking Changes**:
Expand Down
14 changes: 10 additions & 4 deletions relay-profiling/src/android/chunk.rs
Original file line number Diff line number Diff line change
Expand Up @@ -97,10 +97,7 @@ impl Chunk {
Ok(profile) => profile,
Err(_) => return Err(ProfileError::InvalidBase64Value),
};
profile.profile = match android_trace_log::parse(&profile_bytes) {
Ok(profile) => profile,
Err(_) => return Err(ProfileError::InvalidSampledProfile),
};
profile.profile = super::parse_android_trace(&profile_bytes)?;
}

if profile.profile.events.is_empty() {
Expand Down Expand Up @@ -188,6 +185,15 @@ mod tests {
assert!(Chunk::parse(&(data.unwrap())[..]).is_ok());
}

#[test]
fn test_android_17_header_keys() {
let payload = include_bytes!("../../tests/fixtures/android/chunk/android17.json");
let profile = Chunk::parse(payload).unwrap();
assert!(!profile.profile.threads.is_empty());
assert!(!profile.profile.methods.is_empty());
assert!(!profile.profile.events.is_empty());
}

#[test]
fn test_parse_corrects_android_trace_profile_version() {
let payload = include_bytes!("../../tests/fixtures/android/chunk/valid.json");
Expand Down
14 changes: 10 additions & 4 deletions relay-profiling/src/android/legacy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -135,10 +135,7 @@ impl AndroidProfilingEvent {
Ok(profile) => profile,
Err(_) => return Err(ProfileError::InvalidBase64Value),
};
self.profile = match android_trace_log::parse(&profile_bytes) {
Ok(profile) => profile,
Err(_) => return Err(ProfileError::InvalidSampledProfile),
};
self.profile = super::parse_android_trace(&profile_bytes)?;
Ok(())
}

Expand Down Expand Up @@ -294,6 +291,15 @@ mod tests {
);
}

#[test]
fn test_android_17_header_keys() {
let payload = include_bytes!("../../tests/fixtures/android/legacy/android17.json");
let profile = parse_profile(payload).unwrap();
assert!(!profile.profile.threads.is_empty());
assert!(!profile.profile.methods.is_empty());
assert!(!profile.profile.events.is_empty());
}

#[test]
fn test_no_transaction() {
let payload = include_bytes!("../../tests/fixtures/android/legacy/no_transaction.json");
Expand Down
150 changes: 150 additions & 0 deletions relay-profiling/src/android/mod.rs
Original file line number Diff line number Diff line change
@@ -1,2 +1,152 @@
use std::borrow::Cow;

use android_trace_log::AndroidTraceLog;

use crate::ProfileError;

pub mod chunk;
pub mod legacy;

/// Keys of the `*version` section that are ignored when parsing a method trace.
///
/// [`android_trace_log`] rejects any header key it does not know. These keys are written by ART
/// starting with Android 17 (`TraceWriter::CreateSummary` in `runtime/trace.cc`) and are not
/// used by Relay.
const IGNORED_HEADER_KEYS: &[&[u8]] = &[
b"is_streaming",
b"is_precise_trace",
b"end_time",
b"end_tsc",
b"compilation-filter",
];

/// Parses an Android method trace, skipping [`IGNORED_HEADER_KEYS`] in the `*version` header.
fn parse_android_trace(bytes: &[u8]) -> Result<AndroidTraceLog, ProfileError> {
android_trace_log::parse(&strip_ignored_header_keys(bytes))
.map_err(|_| ProfileError::InvalidSampledProfile)
}

/// Removes lines with [`IGNORED_HEADER_KEYS`] from the `*version` section of a method trace.
///
/// All other lines are kept, so unknown keys and malformed values are still rejected by the
/// parser. Only the text header is touched; the binary data section starting at the `SLOW`
/// magic is copied as is, and its offsets are relative to that magic.
///
/// Returns the input unchanged if there is nothing to remove.
fn strip_ignored_header_keys(bytes: &[u8]) -> Cow<'_, [u8]> {
const VERSION_SECTION: &[u8] = b"*version\n";

if !bytes.starts_with(VERSION_SECTION) {
return Cow::Borrowed(bytes);
}

// The first line of the section is the version number, followed by header lines.
let Some(version_len) = next_line_len(&bytes[VERSION_SECTION.len()..]) else {
return Cow::Borrowed(bytes);
};

let mut pos = VERSION_SECTION.len() + version_len + 1;
let mut stripped: Option<Vec<u8>> = None;

while let Some(len) = next_line_len(&bytes[pos..]) {
let line = &bytes[pos..pos + len];
let next = pos + len + 1;

if line.starts_with(b"*") {
break;
}

if is_ignored_header_line(line) {
stripped.get_or_insert_with(|| bytes[..pos].to_vec());
} else if let Some(stripped) = &mut stripped {
stripped.extend_from_slice(&bytes[pos..next]);
}

pos = next;
}

match stripped {
Some(mut stripped) => {
stripped.extend_from_slice(&bytes[pos..]);
Cow::Owned(stripped)
}
None => Cow::Borrowed(bytes),
}
}

/// Returns the length of the next line in `bytes`, excluding the trailing newline.
fn next_line_len(bytes: &[u8]) -> Option<usize> {
bytes.iter().position(|&b| b == b'\n')
}

fn is_ignored_header_line(line: &[u8]) -> bool {
line.iter()
.position(|&b| b == b'=')
.is_some_and(|eq| IGNORED_HEADER_KEYS.contains(&&line[..eq]))
}

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

const ANDROID_16_HEADER: &[u8] = b"*version\n3\ndata-file-overflow=false\nclock=dual\n\
elapsed-time-usec=9407646\nnum-method-calls=4341\nclock-call-overhead-nsec=273\n\
vm=art\npid=4471\n*threads\n1\tmain\n*methods\n*end\nSLOW\x03\x00";

const ANDROID_17_HEADER: &[u8] = b"*version\n3\ndata-file-overflow=false\nclock=dual\n\
elapsed-time-usec=9407646\nnum-method-calls=4341\nclock-call-overhead-nsec=273\n\
vm=art\npid=4471\nis_streaming=false\nis_precise_trace=true\n\
end_time=1642106300035\nend_tsc=7762889077040\ncompilation-filter=run-from-apk\n\
*threads\n1\tmain\n*methods\n*end\nSLOW\x03\x00";

#[test]
fn test_strip_android_16_header_unchanged() {
let stripped = strip_ignored_header_keys(ANDROID_16_HEADER);
assert!(matches!(stripped, Cow::Borrowed(_)));
assert_eq!(&*stripped, ANDROID_16_HEADER);
}

#[test]
fn test_strip_android_17_keys() {
let stripped = strip_ignored_header_keys(ANDROID_17_HEADER);
assert_eq!(&*stripped, ANDROID_16_HEADER);
}

#[test]
fn test_strip_ignored_key_between_known_keys() {
let input = b"*version\n3\nclock=dual\nend_tsc=1\nvm=art\n*threads\n*end\nSLOW=x\n";
let stripped = strip_ignored_header_keys(input);
assert_eq!(
&*stripped,
b"*version\n3\nclock=dual\nvm=art\n*threads\n*end\nSLOW=x\n"
);
}

#[test]
fn test_strip_keeps_unknown_keys() {
let input = b"*version\n3\nclock=dual\nfoo=bar\nend_time=1\n*threads\n";
let stripped = strip_ignored_header_keys(input);
assert_eq!(&*stripped, b"*version\n3\nclock=dual\nfoo=bar\n*threads\n");
}

#[test]
fn test_strip_keeps_lines_without_key() {
let input = b"*version\n3\ngarbage\n=value\nend_time\n*threads\n";
let stripped = strip_ignored_header_keys(input);
assert!(matches!(stripped, Cow::Borrowed(_)));
}

#[test]
fn test_strip_only_version_section() {
for input in [
&b""[..],
b"*version\n",
b"*threads\nend_time=1\n",
b"*version\n3\n*threads\nend_time=1\n",
b"\x00\x01\x02",
] {
let stripped = strip_ignored_header_keys(input);
assert!(matches!(stripped, Cow::Borrowed(_)));
}
}
}

Large diffs are not rendered by default.

31 changes: 31 additions & 0 deletions relay-profiling/tests/fixtures/android/legacy/android17.json

Large diffs are not rendered by default.

Loading