Skip to content

Commit fdd395a

Browse files
authored
Merge 9d1f94f into ce4b2c1
2 parents ce4b2c1 + 9d1f94f commit fdd395a

12 files changed

Lines changed: 716 additions & 48 deletions

File tree

.claude/skills/.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,5 @@
66
!create-java-pr/**
77
!test/
88
!test/**
9+
!btrace-perfetto/
10+
!btrace-perfetto/**
Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
---
2+
name: btrace-perfetto
3+
description: Capture and compare Perfetto traces using btrace 3.0 on an Android device. Use when asked to "profile", "capture trace", "perfetto trace", "btrace", "compare traces", "record perfetto", "trace touch events", "measure performance on device", or benchmark Android SDK changes between branches.
4+
allowed-tools: Bash, Read, Write, Edit, Glob, Grep, WebFetch, AskUserQuestion
5+
argument-hint: "[branch1] [branch2] [duration] [sql-query]"
6+
---
7+
8+
# btrace Perfetto Trace Capture
9+
10+
Capture Perfetto traces with btrace 3.0 on a connected Android device, optionally comparing two branches. Opens results in Perfetto UI with a prefilled SQL query.
11+
12+
## Prerequisites
13+
14+
Before starting, verify:
15+
16+
1. **Connected device**: `adb devices` shows a device (Android 8.0+, 64-bit)
17+
2. **btrace CLI jar**: Check if `tools/btrace/rhea-trace-shell.jar` exists. If not, download it:
18+
```bash
19+
mkdir -p tools/btrace/traces
20+
curl -sL "https://repo1.maven.org/maven2/com/bytedance/btrace/rhea-trace-processor/3.0.0/rhea-trace-processor-3.0.0.jar" \
21+
-o tools/btrace/rhea-trace-shell.jar
22+
```
23+
3. **Device ABI**: Run `adb shell getprop ro.product.cpu.abi` — btrace only supports arm64-v8a and armeabi-v7a (no x86/x86_64)
24+
25+
## Step 1: Parse Arguments
26+
27+
| Argument | Default | Description |
28+
|----------|---------|-------------|
29+
| branch1 | current branch | First branch to trace |
30+
| branch2 | `main` | Second branch to compare against |
31+
| duration | `30` | Trace duration in seconds |
32+
| sql-query | see below | SQL query to prefill in Perfetto UI |
33+
34+
If no arguments are provided, ask the user what they want to trace and which branches to compare. If only one branch is given, capture only that branch (no comparison).
35+
36+
## Step 2: Integrate btrace into Sample App
37+
38+
The sample app is at `sentry-samples/sentry-samples-android/`.
39+
40+
### 2a: Add btrace dependency
41+
42+
In `sentry-samples/sentry-samples-android/build.gradle.kts`, add to the `dependencies` block:
43+
44+
```kotlin
45+
implementation("com.bytedance.btrace:rhea-inhouse:3.0.0")
46+
```
47+
48+
### 2b: Restrict ABI to device architecture
49+
50+
The btrace native library (shadowhook) does not support x86/x86_64. Replace the `ndk` abiFilters line in `defaultConfig` to match the connected device:
51+
52+
```kotlin
53+
ndk { abiFilters.addAll(listOf("arm64-v8a")) }
54+
```
55+
56+
Adjust if the device reports a different ABI.
57+
58+
### 2c: Initialize btrace in Application
59+
60+
In `MyApplication.java`, add `attachBaseContext`:
61+
62+
```java
63+
import android.content.Context;
64+
import com.bytedance.rheatrace.RheaTrace3;
65+
66+
// Add before onCreate:
67+
@Override
68+
protected void attachBaseContext(Context base) {
69+
super.attachBaseContext(base);
70+
RheaTrace3.init(base);
71+
}
72+
```
73+
74+
**Important**: The package is `com.bytedance.rheatrace`, not `com.bytedance.btrace`.
75+
76+
### 2d: Add ProGuard keep rule for release builds
77+
78+
In `sentry-samples/sentry-samples-android/proguard-rules.pro`, add:
79+
80+
```
81+
-keep class com.bytedance.rheatrace.** { *; }
82+
-keepnames class io.sentry.** { *; }
83+
```
84+
85+
The first rule prevents R8 from stripping btrace's HTTP server classes (fails with `SocketException` otherwise). The second preserves Sentry class and method names so they appear readable in the Perfetto trace instead of obfuscated single-letter names.
86+
87+
## Step 3: Build and Install
88+
89+
Always use release builds for tracing — debug builds have StrictMode, debuggable overhead, and no R8 optimizations, which skew results.
90+
91+
```bash
92+
./gradlew :sentry-samples:sentry-samples-android:installRelease
93+
```
94+
95+
## Step 4: Capture Trace
96+
97+
For each branch to trace:
98+
99+
### 4a: Set btrace properties and launch app
100+
101+
```bash
102+
adb shell setprop debug.rhea3.startWhenAppLaunch 1
103+
adb shell setprop debug.rhea3.waitTraceTimeout 60
104+
adb shell am force-stop io.sentry.samples.android
105+
sleep 1
106+
adb shell am start -n io.sentry.samples.android/.MainActivity
107+
```
108+
109+
The app must be started AFTER `debug.rhea3.startWhenAppLaunch` is set, otherwise the trace server won't initialize.
110+
111+
### 4b: Play a sound to signal the user, then capture
112+
113+
Play a sound when tracing actually starts so the user knows to begin interacting. Pipe btrace output through a loop that triggers the sound on the "start tracing" line:
114+
115+
```bash
116+
java -jar tools/btrace/rhea-trace-shell.jar ... 2>&1 | while IFS= read -r line; do
117+
echo "$line"
118+
if [[ "$line" == *"start tracing"* ]]; then
119+
afplay -v 1.5 /System/Library/Sounds/Ping.aiff &
120+
fi
121+
done
122+
```
123+
124+
This ensures the sound is synchronized with the actual trace start, not an estimated delay.
125+
126+
```bash
127+
java -jar tools/btrace/rhea-trace-shell.jar \
128+
-a io.sentry.samples.android \
129+
-t ${duration} \
130+
-waitTraceTimeout 60 \
131+
-o tools/btrace/traces/${branch_name}.pb \
132+
sched
133+
```
134+
135+
Do NOT use the `-r` flag — it fails to resolve the launcher activity because LeakCanary registers a second one. Launch the app manually in step 4a instead.
136+
137+
### 4c: Switch branches for comparison
138+
139+
When capturing a second branch:
140+
141+
1. Stash the btrace integration changes:
142+
```bash
143+
git stash push -m "btrace integration" -- \
144+
sentry-samples/sentry-samples-android/build.gradle.kts \
145+
sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/MyApplication.java \
146+
sentry-samples/sentry-samples-android/proguard-rules.pro
147+
```
148+
2. Checkout the other branch
149+
3. Pop the stash: `git stash pop`
150+
4. Rebuild and install: `./gradlew :sentry-samples:sentry-samples-android:installRelease`
151+
5. Repeat steps 4a and 4b with a different output filename
152+
6. Switch back to the original branch and restore files
153+
154+
## Step 5: Open in Perfetto UI
155+
156+
Generate a viewer HTML and serve it locally. Use the template at `assets/viewer-template.html` as a base — copy it to `tools/btrace/traces/viewer.html` and replace the placeholder values:
157+
158+
- `TRACE_FILES`: array of `{file, title}` objects for each captured trace
159+
- `SQL_QUERY`: the SQL query to prefill
160+
161+
The SQL query is passed via the URL hash parameter: `https://ui.perfetto.dev/#!/?query=...`
162+
163+
The trace data is sent via the postMessage API (required for local files — URL deep-linking does not work with `file://`).
164+
165+
Start a local HTTP server and open the viewer:
166+
167+
```bash
168+
cd tools/btrace/traces && python3 -m http.server 8008 &
169+
open http://localhost:8008/viewer.html
170+
```
171+
172+
### Default SQL Query
173+
174+
If no custom query is provided, use:
175+
176+
```sql
177+
SELECT
178+
s.name AS slice_name,
179+
s.dur / 1e6 AS dur_ms,
180+
s.ts,
181+
t.name AS track_name
182+
FROM slice s
183+
JOIN thread_track t ON s.track_id = t.id
184+
WHERE s.name GLOB '*SentryWindowCallback.dispatch*'
185+
ORDER BY s.ts
186+
```
187+
188+
## Cleanup
189+
190+
After tracing is complete, remind the user that the btrace integration changes to the sample app should NOT be committed. The `tools/btrace/` directory is gitignored.
191+
192+
## Troubleshooting
193+
194+
| Problem | Solution |
195+
|---------|----------|
196+
| `No compatible library found [shadowhook]` | Restrict `ndk.abiFilters` to arm64-v8a only |
197+
| `package com.bytedance.btrace does not exist` | Use `com.bytedance.rheatrace` (not `btrace`) |
198+
| `ResolverActivity does not exist` with `-r` flag | Don't use `-r`; launch the app manually before capturing |
199+
| `wait for trace ready timeout` on download | Set `debug.rhea3.startWhenAppLaunch=1` BEFORE launching the app, and use `-waitTraceTimeout 60` |
200+
| Empty jar file (0 bytes) | Download from Maven Central (`repo1.maven.org`), not `oss.sonatype.org` |
201+
| `FileNotFoundException` on sampling download | App was already running when properties were set; force-stop and relaunch |
202+
| `SocketException: Unexpected end of file` in release builds | R8 stripped btrace classes; add `-keep class com.bytedance.rheatrace.** { *; }` to proguard-rules.pro |
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<head><title>btrace Trace Viewer</title></head>
4+
<body>
5+
<h2>Perfetto Trace Viewer</h2>
6+
<div id="buttons"><!-- BUTTONS_PLACEHOLDER --></div>
7+
<p id="status"></p>
8+
<script>
9+
// Replace these values when copying the template
10+
var TRACE_FILES = [/* TRACE_FILES_PLACEHOLDER */];
11+
var SQL_QUERY = `SQL_QUERY_PLACEHOLDER`;
12+
13+
var container = document.getElementById('buttons');
14+
TRACE_FILES.forEach(function(t) {
15+
var btn = document.createElement('button');
16+
btn.textContent = 'Open ' + t.title;
17+
btn.style.marginRight = '8px';
18+
btn.onclick = function() { openTrace(t.file, t.title); };
19+
container.appendChild(btn);
20+
});
21+
22+
function openTrace(file, title) {
23+
var status = document.getElementById('status');
24+
status.textContent = 'Opening Perfetto UI...';
25+
var url = 'https://ui.perfetto.dev/#!/?query=' + encodeURIComponent(SQL_QUERY);
26+
var w = window.open(url);
27+
var ping = setInterval(function() { w.postMessage('PING', '*'); }, 100);
28+
window.onmessage = function(e) {
29+
if (e.data === 'PONG') {
30+
clearInterval(ping);
31+
status.textContent = 'Loading trace: ' + file;
32+
fetch(file)
33+
.then(function(r) { return r.arrayBuffer(); })
34+
.then(function(buf) {
35+
w.postMessage({
36+
perfetto: {
37+
buffer: buf,
38+
title: title,
39+
fileName: file
40+
}
41+
}, '*');
42+
status.textContent = 'Trace sent to Perfetto UI!';
43+
});
44+
}
45+
};
46+
}
47+
</script>
48+
</body>
49+
</html>

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
## Unreleased
44

5+
### Fixes
6+
7+
- Fix ANR caused by `GestureDetectorCompat` Handler/MessageQueue lock contention in `SentryWindowCallback` ([#5138](https://github.com/getsentry/sentry-java/pull/5138))
8+
59
### Internal
610

711
- Bump AGP version from v8.6.0 to v8.13.1 ([#5063](https://github.com/getsentry/sentry-java/pull/5063))

agents.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,3 +31,7 @@ source = "path:.agents/skills/create-java-pr"
3131
[[skills]]
3232
name = "test"
3333
source = "path:.agents/skills/test"
34+
35+
[[skills]]
36+
name = "btrace-perfetto"
37+
source = "path:.agents/skills/btrace-perfetto"

sentry-android-core/proguard-rules.pro

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
##---------------Begin: proguard configuration for android-core ----------
22

33
##---------------Begin: proguard configuration for androidx.core ----------
4-
-keep class androidx.core.view.GestureDetectorCompat { <init>(...); }
54
-keep class androidx.core.app.FrameMetricsAggregator { <init>(...); }
65
-keep interface androidx.core.view.ScrollingView { *; }
76
##---------------End: proguard configuration for androidx.core ----------

sentry-android-core/src/main/java/io/sentry/android/core/UserInteractionIntegration.java

Lines changed: 11 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -28,14 +28,11 @@ public final class UserInteractionIntegration
2828
private @Nullable IScopes scopes;
2929
private @Nullable SentryAndroidOptions options;
3030

31-
private final boolean isAndroidXAvailable;
3231
private final boolean isAndroidxLifecycleAvailable;
3332

3433
public UserInteractionIntegration(
3534
final @NotNull Application application, final @NotNull io.sentry.util.LoadClass classLoader) {
3635
this.application = Objects.requireNonNull(application, "Application is required");
37-
isAndroidXAvailable =
38-
classLoader.isClassAvailable("androidx.core.view.GestureDetectorCompat", options);
3936
isAndroidxLifecycleAvailable =
4037
classLoader.isClassAvailable("androidx.lifecycle.Lifecycle", options);
4138
}
@@ -128,27 +125,19 @@ public void register(@NotNull IScopes scopes, @NotNull SentryOptions options) {
128125
.log(SentryLevel.DEBUG, "UserInteractionIntegration enabled: %s", integrationEnabled);
129126

130127
if (integrationEnabled) {
131-
if (isAndroidXAvailable) {
132-
application.registerActivityLifecycleCallbacks(this);
133-
this.options.getLogger().log(SentryLevel.DEBUG, "UserInteractionIntegration installed.");
134-
addIntegrationToSdkVersion("UserInteraction");
135-
136-
// In case of a deferred init, we hook into any resumed activity
137-
if (isAndroidxLifecycleAvailable) {
138-
final @Nullable Activity activity = CurrentActivityHolder.getInstance().getActivity();
139-
if (activity instanceof LifecycleOwner) {
140-
if (((LifecycleOwner) activity).getLifecycle().getCurrentState()
141-
== Lifecycle.State.RESUMED) {
142-
startTracking(activity);
143-
}
128+
application.registerActivityLifecycleCallbacks(this);
129+
this.options.getLogger().log(SentryLevel.DEBUG, "UserInteractionIntegration installed.");
130+
addIntegrationToSdkVersion("UserInteraction");
131+
132+
// In case of a deferred init, we hook into any resumed activity
133+
if (isAndroidxLifecycleAvailable) {
134+
final @Nullable Activity activity = CurrentActivityHolder.getInstance().getActivity();
135+
if (activity instanceof LifecycleOwner) {
136+
if (((LifecycleOwner) activity).getLifecycle().getCurrentState()
137+
== Lifecycle.State.RESUMED) {
138+
startTracking(activity);
144139
}
145140
}
146-
} else {
147-
options
148-
.getLogger()
149-
.log(
150-
SentryLevel.INFO,
151-
"androidx.core is not available, UserInteractionIntegration won't be installed");
152141
}
153142
}
154143
}

0 commit comments

Comments
 (0)