Skip to content

Commit de6a178

Browse files
romtsnclaude
andauthored
fix(gestures): Replace GestureDetectorCompat with lightweight detector to fix ANR (#5138)
* fix(android): Replace GestureDetectorCompat with lightweight SentryGestureDetector to fix ANR GestureDetectorCompat internally uses Handler.sendMessage/removeMessages which acquires a synchronized lock on the main thread MessageQueue, plus recordGestureClassification triggers IPC calls. This caused ANRs under load (SDK-CRASHES-JAVA-596, 175K+ occurrences). Replace with a minimal custom detector that only detects click, scroll, and fling without any Handler scheduling, MessageQueue contention, or IPC overhead. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * test(android): Add unit tests for SentryGestureDetector Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * changelog Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(gestures): Clear VelocityTracker on ACTION_DOWN to prevent stale velocity data Matches GestureDetector behavior: if consecutive ACTION_DOWN events arrive without an intervening ACTION_UP/ACTION_CANCEL, stale motion data could bleed into fling detection. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(gestures): Remove stale GestureDetectorCompat class availability check UserInteractionIntegration gated itself on GestureDetectorCompat being available via classloader check, but SentryGestureDetector only uses Android SDK classes. Remove the check so the integration works without androidx.core. Also remove the stale proguard -keep rule. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Move changelog entry * ref(gestures): Simplify SentryGestureDetector resource lifecycle Keep VelocityTracker alive across gesture cycles instead of obtain/recycle churn on every gesture. Add release() method called from SentryWindowCallback.stopTracking() to prevent native resource leaks when activity is destroyed mid-gesture. Also fix broken Javadoc @link to removed dependency, change onTouchEvent return to void, and remove redundant null check. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(gestures): Handle multi-touch to prevent spurious tap detection When a second finger touches the screen (ACTION_POINTER_DOWN), cancel tap detection by setting isInTapRegion to false. Previously, pinch-to-zoom gestures where the first finger stayed still would incorrectly trigger onSingleTapUp, producing false click breadcrumbs. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * chore: Register btrace-perfetto skill in agents.toml Add local btrace-perfetto skill for capturing and comparing Perfetto traces on Android devices using btrace 3.0. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * chore: Add btrace-perfetto skill for Perfetto trace capture Workflow skill that automates capturing and comparing Perfetto traces using btrace 3.0 on Android devices. Includes a Perfetto UI viewer template with SQL query deep-linking via postMessage API. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * chore: Update btrace skill with release builds and sound cues Use release builds for accurate profiling, add ProGuard keep rules for btrace and Sentry class names, increase trace duration to 30s, and play a Ping sound synced to the actual trace start. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * chore: Update btrace-perfetto skill with debug builds and trace comparison Prefer debug builds for richer tracing instrumentation (Handler, MessageQueue, Lock slices). Add trace_processor prerequisite for local querying. Add Step 6 with SQL queries and comparison table generation. Include sampling rate reference and additional troubleshooting entries. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(gestures): Suppress fling after multi-touch to prevent misclassified swipes The ACTION_POINTER_DOWN handler suppressed taps but not flings. When the last finger lifts quickly after a pinch-to-zoom, the velocity check in ACTION_UP could fire onFling, causing SentryGestureListener to misclassify the gesture as a swipe breadcrumb. Add ignoreUpEvent flag mirroring GestureDetector's mIgnoreNextUpEvent to skip the entire UP handler after multi-touch. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent ce4b2c1 commit de6a178

File tree

12 files changed

+892
-48
lines changed

12 files changed

+892
-48
lines changed

.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: 284 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,284 @@
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. After capture, query traces locally with `trace_processor` to compute comparison stats.
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. **Perfetto trace_processor**: Check if `/tmp/trace_processor` exists. If not, download it:
24+
```bash
25+
curl -sL "https://get.perfetto.dev/trace_processor" -o /tmp/trace_processor && chmod +x /tmp/trace_processor
26+
```
27+
4. **Device ABI**: Run `adb shell getprop ro.product.cpu.abi` — btrace only supports arm64-v8a and armeabi-v7a (no x86/x86_64)
28+
29+
## Step 1: Parse Arguments
30+
31+
| Argument | Default | Description |
32+
|----------|---------|-------------|
33+
| branch1 | current branch | First branch to trace |
34+
| branch2 | `main` | Second branch to compare against |
35+
| duration | `30` | Trace duration in seconds |
36+
| sql-query | see below | SQL query to prefill in Perfetto UI |
37+
38+
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).
39+
40+
## Step 2: Integrate btrace into Sample App
41+
42+
The sample app is at `sentry-samples/sentry-samples-android/`.
43+
44+
### 2a: Add btrace dependency
45+
46+
In `sentry-samples/sentry-samples-android/build.gradle.kts`, add to the `dependencies` block:
47+
48+
```kotlin
49+
implementation("com.bytedance.btrace:rhea-inhouse:3.0.0")
50+
```
51+
52+
### 2b: Restrict ABI to device architecture
53+
54+
The btrace native library (shadowhook) does not support x86/x86_64. Replace the `ndk` abiFilters line in `defaultConfig` to match the connected device:
55+
56+
```kotlin
57+
ndk { abiFilters.addAll(listOf("arm64-v8a")) }
58+
```
59+
60+
Adjust if the device reports a different ABI.
61+
62+
### 2c: Initialize btrace in Application
63+
64+
In `MyApplication.java`, add `attachBaseContext`:
65+
66+
```java
67+
import android.content.Context;
68+
import com.bytedance.rheatrace.RheaTrace3;
69+
70+
// Add before onCreate:
71+
@Override
72+
protected void attachBaseContext(Context base) {
73+
super.attachBaseContext(base);
74+
RheaTrace3.init(base);
75+
}
76+
```
77+
78+
**Important**: The package is `com.bytedance.rheatrace`, not `com.bytedance.btrace`.
79+
80+
### 2d: Add ProGuard keep rules (release builds only)
81+
82+
Only needed when building release. In `sentry-samples/sentry-samples-android/proguard-rules.pro`, add:
83+
84+
```
85+
-keep class com.bytedance.rheatrace.** { *; }
86+
-keepnames class io.sentry.** { *; }
87+
```
88+
89+
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.
90+
91+
## Step 3: Build and Install
92+
93+
Prefer **debug builds** — they provide richer tracing instrumentation (Handler, MessageQueue, Monitor:Lock slices visible) which is essential for comparing internal SDK behavior. Use the default 1kHz btrace sampling rate for debug builds.
94+
95+
```bash
96+
./gradlew :sentry-samples:sentry-samples-android:installDebug
97+
```
98+
99+
**Release builds** are useful when you need to measure real-world performance without StrictMode/debuggable overhead or with R8 optimizations. Require the ProGuard keep rules from step 2d. Use `-sampleInterval 333000` (333μs / 3kHz) for finer granularity since release code runs faster.
100+
101+
```bash
102+
./gradlew :sentry-samples:sentry-samples-android:installRelease
103+
```
104+
105+
## Step 4: Capture Trace
106+
107+
For each branch to trace:
108+
109+
### 4a: Set btrace properties and launch app
110+
111+
Clear any stale port files, set properties, and launch:
112+
113+
```bash
114+
adb shell "rm -rf /storage/emulated/0/Android/data/io.sentry.samples.android/files/rhea-port"
115+
adb shell setprop debug.rhea3.startWhenAppLaunch 1
116+
adb shell setprop debug.rhea3.waitTraceTimeout 60
117+
adb shell am force-stop io.sentry.samples.android
118+
sleep 2
119+
adb shell am start -n io.sentry.samples.android/.MainActivity
120+
sleep 5
121+
```
122+
123+
The app must be started AFTER `debug.rhea3.startWhenAppLaunch` is set, otherwise the trace server won't initialize. The 5s sleep after launch gives the btrace HTTP server time to start.
124+
125+
### 4b: Play a sound to signal the user, then capture
126+
127+
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:
128+
129+
```bash
130+
java -jar tools/btrace/rhea-trace-shell.jar \
131+
-a io.sentry.samples.android \
132+
-t ${duration} \
133+
-waitTraceTimeout 60 \
134+
-o tools/btrace/traces/${branch_name}.pb \
135+
sched 2>&1 | while IFS= read -r line; do
136+
echo "$line"
137+
if [[ "$line" == *"start tracing"* ]]; then
138+
afplay -v 1.5 /System/Library/Sounds/Ping.aiff &
139+
fi
140+
done
141+
```
142+
143+
For release builds with finer sampling, add `-sampleInterval 333000`.
144+
145+
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.
146+
147+
### 4c: Switch branches for comparison
148+
149+
When capturing a second branch:
150+
151+
1. Stash the btrace integration changes:
152+
```bash
153+
git stash push -m "btrace integration" -- \
154+
sentry-samples/sentry-samples-android/build.gradle.kts \
155+
sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/MyApplication.java \
156+
sentry-samples/sentry-samples-android/proguard-rules.pro
157+
```
158+
2. Checkout the other branch
159+
3. Pop the stash: `git stash pop`
160+
4. Rebuild and install (same variant — debug or release — as the first branch)
161+
5. Repeat steps 4a and 4b with a different output filename
162+
6. Switch back to the original branch and restore files
163+
164+
## Step 5: Open in Perfetto UI
165+
166+
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:
167+
168+
- `TRACE_FILES`: array of `{file, title}` objects for each captured trace
169+
- `SQL_QUERY`: the SQL query to prefill
170+
171+
The SQL query is passed via the URL hash parameter: `https://ui.perfetto.dev/#!/?query=...`
172+
173+
The trace data is sent via the postMessage API (required for local files — URL deep-linking does not work with `file://`).
174+
175+
Start a local HTTP server and open the viewer:
176+
177+
```bash
178+
cd tools/btrace/traces && python3 -m http.server 8008 &
179+
open http://localhost:8008/viewer.html
180+
```
181+
182+
### Default SQL Query
183+
184+
If no custom query is provided, use:
185+
186+
```sql
187+
SELECT
188+
s.name AS slice_name,
189+
s.dur / 1e6 AS dur_ms,
190+
s.ts,
191+
t.name AS track_name
192+
FROM slice s
193+
JOIN thread_track t ON s.track_id = t.id
194+
WHERE s.name GLOB '*SentryWindowCallback.dispatch*'
195+
ORDER BY s.ts
196+
```
197+
198+
## Step 6: Query and Compare Traces
199+
200+
After capturing both branches, use `trace_processor` to compute comparison stats locally.
201+
202+
### Basic stats query
203+
204+
For each trace file, run:
205+
206+
```bash
207+
/tmp/trace_processor -Q "
208+
WITH events AS (
209+
SELECT s.dur / 1e6 as dur_ms FROM slice s
210+
WHERE s.name GLOB '*${METHOD_GLOB}*' AND s.dur > 0
211+
ORDER BY s.dur
212+
)
213+
SELECT COUNT(*) as count,
214+
ROUND(AVG(dur_ms), 4) as avg_ms,
215+
ROUND((SELECT dur_ms FROM events LIMIT 1 OFFSET (SELECT COUNT(*)/2 FROM events)), 4) as median_ms,
216+
ROUND(MIN(dur_ms), 4) as min_ms,
217+
ROUND(MAX(dur_ms), 4) as max_ms
218+
FROM events
219+
" tools/btrace/traces/${trace_file}.pb
220+
```
221+
222+
Replace `${METHOD_GLOB}` with the method pattern to compare (e.g. `SentryGestureDetector.onTouchEvent`, `SentryWindowCallback.dispatchTouchEvent`).
223+
224+
### Finding child calls (debug builds)
225+
226+
To find what happens inside a method (e.g. Handler calls, lock acquisitions):
227+
228+
```bash
229+
/tmp/trace_processor -Q "
230+
WITH RECURSIVE descendants(id, depth) AS (
231+
SELECT s.id, 0 FROM slice s WHERE s.name GLOB '*${PARENT_METHOD}*'
232+
UNION ALL
233+
SELECT s.id, d.depth + 1 FROM slice s JOIN descendants d ON s.parent_id = d.id WHERE d.depth < 10
234+
)
235+
SELECT s.name, COUNT(*) as count, ROUND(AVG(s.dur / 1e6), 3) as avg_ms
236+
FROM slice s JOIN descendants d ON s.id = d.id
237+
WHERE d.depth > 0
238+
GROUP BY s.name ORDER BY count DESC
239+
LIMIT 20
240+
" tools/btrace/traces/${trace_file}.pb
241+
```
242+
243+
### Build the comparison table
244+
245+
Run the stats query on both trace files, then present a markdown table:
246+
247+
```
248+
| Metric | Branch A | Branch B | Delta |
249+
|--------|----------|----------|-------|
250+
| Count | ... | ... | |
251+
| Average| ... | ... | -X% |
252+
| Median | ... | ... | -X% |
253+
| Max | ... | ... | -X% |
254+
```
255+
256+
Compute delta as `(branchA - branchB) / branchB * 100`. Negative means branch A is faster.
257+
258+
### Sampling rate reference
259+
260+
| Rate | Interval | `-sampleInterval` | Use case |
261+
|------|----------|-------------------|----------|
262+
| 1 kHz | 1ms | `1000000` (default) | Debug builds, general profiling |
263+
| 3 kHz | 333μs | `333000` | Release builds, finer granularity |
264+
| 10 kHz | 100μs | `100000` | Maximum detail, higher overhead |
265+
266+
Higher sampling rates capture shorter method calls but add CPU overhead which can skew results. For most comparisons, the default 1kHz is sufficient.
267+
268+
## Cleanup
269+
270+
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.
271+
272+
## Troubleshooting
273+
274+
| Problem | Solution |
275+
|---------|----------|
276+
| `No compatible library found [shadowhook]` | Restrict `ndk.abiFilters` to arm64-v8a only |
277+
| `package com.bytedance.btrace does not exist` | Use `com.bytedance.rheatrace` (not `btrace`) |
278+
| `ResolverActivity does not exist` with `-r` flag | Don't use `-r`; launch the app manually before capturing |
279+
| `wait for trace ready timeout` on download | Set `debug.rhea3.startWhenAppLaunch=1` BEFORE launching the app, and use `-waitTraceTimeout 60` |
280+
| Empty jar file (0 bytes) | Download from Maven Central (`repo1.maven.org`), not `oss.sonatype.org` |
281+
| `FileNotFoundException` on sampling download | App was already running when properties were set; force-stop and relaunch |
282+
| `SocketException: Unexpected end of file` in release builds | R8 stripped btrace classes; add `-keep class com.bytedance.rheatrace.** { *; }` to proguard-rules.pro |
283+
| Stale port from previous session | Run `adb shell "rm -rf /storage/emulated/0/Android/data/io.sentry.samples.android/files/rhea-port"` before launching |
284+
| Most `onTouchEvent` durations are 0ms | Increase sampling rate with `-sampleInterval 333000` (3kHz) |
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 ----------

0 commit comments

Comments
 (0)