Skip to content

feat(profiling): Add Perfetto trace format support#5659

Open
markushi wants to merge 40 commits into
masterfrom
feat/markushi/perfetto-profiling-support
Open

feat(profiling): Add Perfetto trace format support#5659
markushi wants to merge 40 commits into
masterfrom
feat/markushi/perfetto-profiling-support

Conversation

@markushi
Copy link
Copy Markdown
Member

@markushi markushi commented Feb 25, 2026

This PR adds the ability to ingest binary Perfetto traces (.pftrace) and convert them into the existing Sample v2 JSON format used by Sentry's profiling pipeline. This targets Android
profiling, where Perfetto is the native tracing format.

Key Changes

  1. New feature flag organizations:continuous-profiling-perfetto — gates all Perfetto processing.
  2. Compound item format via meta_length — profile chunk items can now carry [JSON metadata][binary blob], split at meta_length. This is how Perfetto traces arrive: metadata describes the
    profile, the binary blob is the raw .pftrace.
  3. Perfetto → Sample v2 conversion — new relay-profiling/src/perfetto/ module (proto definitions + conversion logic) parses binary Perfetto traces and produces the existing Sample v2
    JSON format.
  4. Raw profile passthrough to Kafka — after expansion, the original Perfetto binary is preserved alongside the expanded JSON. Two new fields on ProfileChunkKafkaMessage: raw_profile and
    raw_profile_content_type.
  5. New fields on existing structs:
    - Frame.package — library/container path for native/Java frames
    - ProfileMetadata.content_type — carries "perfetto" through the pipeline
    - DebugImage::native_image() constructor — creates debug images from Perfetto mapping data

This PR is part of a stack:

markushi and others added 11 commits February 25, 2026 09:16
Add support for ingesting binary Perfetto traces (.pftrace) as profile
chunks. The SDK sends an envelope with a ProfileChunk metadata item
paired with a ProfileChunkData item containing the raw Perfetto protobuf.

Relay decodes the Perfetto trace, extracts CPU profiling samples
(PerfSample and StreamingProfilePacket), converts them to the internal
Sample v2 format, and forwards both the expanded JSON and the original
binary blob to Kafka for downstream processing.

Key changes:
- New `perfetto` module in relay-profiling for protobuf decoding and
  conversion to Sample v2
- New `ProfileChunkData` envelope item type for binary profile payloads
- Pairing logic to associate ProfileChunk metadata with ProfileChunkData
- Raw profile blob preserved through to Kafka for further processing

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
# Conflicts:
#	relay-server/src/envelope/item.rs
@markushi
Copy link
Copy Markdown
Member Author

@sentry review

Comment thread relay-profiling/src/perfetto/mod.rs Outdated
markushi and others added 2 commits March 23, 2026 09:01
…fer main thread

Separate the shared `strings` intern table into distinct `function_names`,
`mapping_paths`, and `build_ids` tables matching the Perfetto spec where
each InternedData field has its own ID namespace. Also infer the main
thread name when tid equals pid and no explicit name is provided.

Co-Authored-By: Claude <noreply@anthropic.com>
@markushi markushi marked this pull request as ready for review March 23, 2026 08:57
@markushi markushi requested a review from a team as a code owner March 23, 2026 08:57
Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Autofix Details

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: First timestamp delta skipped in StreamingProfilePacket processing
    • Removed the 'i > 0' guard so that timestamp_delta_us[0] is now correctly applied to the first sample's timestamp.

Create PR

Or push these changes by commenting:

@cursor push ab5f3ec796
Preview (ab5f3ec796)
diff --git a/relay-profiling/src/perfetto/mod.rs b/relay-profiling/src/perfetto/mod.rs
--- a/relay-profiling/src/perfetto/mod.rs
+++ b/relay-profiling/src/perfetto/mod.rs
@@ -229,9 +229,7 @@
             Some(Data::StreamingProfilePacket(spp)) => {
                 let mut ts = packet.timestamp.unwrap_or(0);
                 for (i, &cs_iid) in spp.callstack_iid.iter().enumerate() {
-                    if i > 0
-                        && let Some(&delta) = spp.timestamp_delta_us.get(i)
-                    {
+                    if let Some(&delta) = spp.timestamp_delta_us.get(i) {
                         // `delta` is i64 (can be negative for out-of-order samples).
                         // Casting to u64 wraps negative values, which is correct because
                         // `wrapping_add` of a wrapped negative value subtracts as expected.

This Bugbot Autofix run was free. To enable autofix for future PRs, go to the Cursor dashboard.

Comment thread relay-profiling/src/perfetto/mod.rs
markushi and others added 2 commits March 23, 2026 10:27
Replace `get().is_none()` with `!contains_key()` to satisfy clippy
and add a CHANGELOG entry for the Perfetto interning changes.

Co-Authored-By: Claude <noreply@anthropic.com>
The first delta in timestamp_delta_us was skipped due to an i > 0
guard, but per the Perfetto spec the first sample's timestamp should
be TracePacket.timestamp + timestamp_delta_us[0]. Update tests to use
non-zero first deltas to verify the fix.

Co-Authored-By: Claude <noreply@anthropic.com>
Comment thread relay-profiling/src/perfetto/mod.rs Outdated
markushi and others added 2 commits April 1, 2026 08:59
…kDescriptor

StreamingProfilePacket samples were all assigned tid=0 because the code
never resolved the trusted_packet_sequence_id → TrackDescriptor →
ThreadDescriptor chain. This collapsed multi-thread profiles into a
single thread. Now we build a seq_id→tid mapping from TrackDescriptor
packets and use it when processing StreamingProfilePacket samples.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Comment thread relay-profiling/src/perfetto/mod.rs Outdated
…al state resets

The two-pass architecture resolved all samples against the final state
of the intern tables. If a trace contained an incremental state reset
that reused an interning ID, samples collected before the reset would
silently resolve to the wrong function names. This merges the two
passes into a single pass that resolves callstacks immediately using
the current intern table state, and extracts debug images inline.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Comment thread relay-server/src/processing/profile_chunks/mod.rs Outdated
markushi and others added 2 commits April 1, 2026 10:36
process_compound_item was parsing the entire metadata JSON into a
serde_json::Value tree just to read the content_type field, and
split_item_payload was doing the same on the expanded JSON (potentially
hundreds of KB). Instead, surface content_type from the already-
deserialized ProfileMetadata via ExpandedPerfettoChunk, and hardcode
"perfetto" in split_item_payload since compound items are always
validated as perfetto by process_compound_item.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The previous commit moved expand_perfetto before the content_type
check, which would waste work on non-perfetto payloads and produce
confusing errors. Restore the early content_type check using a minimal
serde struct that only deserializes the one field, and remove the
unused content_type field from ExpandedPerfettoChunk.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Comment thread relay-server/src/processing/profile_chunks/mod.rs Outdated
Comment thread relay-server/src/processing/profile_chunks/process.rs Outdated
Replace the hard-coded "perfetto" content type in split_item_payload
with a lightweight deserialization that reads only the content_type
field from the metadata JSON. This avoids a silent coupling that would
produce incorrect values if compound items support other formats.

Also remove a dead item.set_platform() call that wrote to an item
immediately replaced by a new one.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Comment thread relay-profiling/src/perfetto/mod.rs Outdated
Comment thread relay-profiling/src/perfetto/mod.rs Outdated
Comment thread relay-server/src/processing/profile_chunks/process.rs
Comment thread relay-profiling/src/lib.rs Outdated
Comment thread relay-server/src/services/store.rs
Comment thread relay-server/src/processing/profile_chunks/mod.rs
Comment thread relay-profiling/src/perfetto/mod.rs
Comment thread relay-profiling/src/sample/v2.rs Outdated
Comment thread relay-profiling/src/lib.rs
Comment thread relay-profiling/src/perfetto/mod.rs
Copy link
Copy Markdown
Member

@Dav1dde Dav1dde left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A few small things in the conversion code. I tried to mostly follow along logically but the interning and things are quite complicated according to the docs, so I mostly left like small improvements there.

For the actual processing pipeline, see my larger comment in the processor, we need to get some type safety in here. It will also help with code duplication of the duplicated split functions.
Let me know if you want me to help out with that.

Comment thread tests/integration/test_profile_chunks_perfetto.py
Comment thread tests/integration/test_profile_chunks_perfetto.py Outdated
Comment on lines +73 to +74
#[serde(rename = "organizations:continuous-profiling-perfetto")]
ContinuousProfilingPerfetto,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I assume the long-term plan is to roll this out to everyone as long as they have continous profiling enabled?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, exactly! The flag is meant for the initial rollout, but should be removed once the feature is GA.

Comment thread relay-profiling/src/perfetto/mod.rs Outdated
Comment thread relay-profiling/src/perfetto/mod.rs Outdated
Comment thread relay-profiling/src/perfetto/mod.rs Outdated
Comment thread relay-server/src/processing/profile_chunks/process.rs
Comment thread relay-server/src/processing/profile_chunks/process.rs
Comment thread relay-server/src/processing/profile_chunks/process.rs
Comment thread relay-server/src/processing/profile_chunks/mod.rs
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fine with me!

/// For native frames this is the dynamic library path (e.g. `libc.so`).
/// For Java frames this is the container (e.g. `boot-framework.oat`).
#[serde(skip_serializing_if = "Option::is_none")]
pub package: Option<String>,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need review from @getsentry/product-owners-profiling for these protocol changes?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment thread relay-profiling/src/debug_image.rs
Comment thread relay-server/src/envelope/content_type.rs Outdated
Comment thread relay-profiling/src/perfetto/mod.rs Outdated
Comment on lines +151 to +154
function: Option<String>,
module: Option<String>,
package: Option<String>,
instruction_addr: Option<u64>,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are all of the combinations of None / Some valid here? For example, a FrameKey with four Nones? If not, an enum might be more suitable.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When looking at the protocol spec all fields are optional, so in theory it could be valid, but yeah it has little to no information when all is blank.

I can imagine to have something like this, but I guess it doesn't really improve the overall situation either.

enum FrameKey {
    Java {
        function: Option<String>,
        module: Option<String>,
        package: String,  // always present (is_java requires mapping_path)
    },
    Native {
        function: Option<String>,
        package: Option<String>,
        instruction_addr: Option<u64>,
    },
}

Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit ac8dc49. Configure here.

Comment thread relay-profiling/src/perfetto/mod.rs
Comment thread relay-server/src/processing/profile_chunks/process.rs
Jvm,
}

/// A debug information image referenced by a profile.
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: The deduplication key (code_file, image_addr) is insufficient when a mapping's start address is missing, causing valid debug images to be incorrectly discarded.
Severity: MEDIUM

Suggested Fix

The deduplication key should be made more specific to prevent collisions when the image_addr defaults to 0. Consider including the debug_id or another unique identifier from the mapping in the deduplication tuple to distinguish between different images that might otherwise appear identical.

Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent. Verify if this is a real issue. If it is, propose a fix; if not, explain why it's
not valid.

Location: relay-profiling/src/debug_image.rs#L20

Potential issue: In the `collect_debug_image` function, debug images are deduplicated
using a `HashSet` with a key of `(code_file, image_addr)`. The `image_addr` is derived
from `mapping.start`, which defaults to `0` if `mapping.start` is `None`. This creates a
bug where two genuinely different debug images (e.g., with different `build_id`s) that
share the same `code_file` and both lack a `start` address will be treated as
duplicates. The first image is processed, but the second is incorrectly discarded,
leading to loss of profiling information for that image.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants