Skip to content

Commit dabe4b2

Browse files
romtsnclaude
andcommitted
fix(replay): Post checkCanRecord to main thread to prevent deadlock
onScreenshotRecorded can run on the replay executor thread (PixelCopy masked-capture and emit paths). checkCanRecord -> pauseInternal acquires lifecycleLock — if another thread holds that lock and submits to the same single-threaded executor, we deadlock. When not already on the main thread, post checkCanRecord to the main looper so it never runs on the executor. On main thread, call directly to preserve existing synchronous behavior. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 6335e35 commit dabe4b2

2 files changed

Lines changed: 16 additions & 2 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
### Fixes
1010

11+
- Fix potential ANR/deadlock in Session Replay when `checkCanRecord` runs on the replay executor thread ([#5837](https://github.com/getsentry/sentry-java/pull/5837))
1112
- Release `MediaMuxer` when the replay video encoder fails to start to avoid a resource leak ([#5607](https://github.com/getsentry/sentry-java/pull/5607))
1213

1314
### Performance

sentry-android-replay/src/main/java/io/sentry/android/replay/ReplayIntegration.kt

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import android.content.Context
44
import android.graphics.Bitmap
55
import android.graphics.BitmapFactory
66
import android.os.Build
7+
import android.os.Looper
78
import android.view.MotionEvent
89
import io.sentry.Breadcrumb
910
import io.sentry.DataCategory.All
@@ -352,7 +353,15 @@ public class ReplayIntegration(
352353
}
353354
addFrame(bitmap, frameTimeStamp, screen)
354355
}
355-
checkCanRecord()
356+
// When called from the replay executor (PixelCopy masked-capture and emit paths),
357+
// checkCanRecord -> pauseInternal acquires lifecycleLock. If another thread holds
358+
// that lock while submitting to the same executor, we deadlock. Post to main in
359+
// that case; on main thread call directly to keep the existing synchronous behavior.
360+
if (Looper.myLooper() == Looper.getMainLooper()) {
361+
checkCanRecord()
362+
} else {
363+
mainLooperHandler.post { checkCanRecord() }
364+
}
356365
}
357366

358367
override fun onScreenshotRecorded(screenshot: File, frameTimestamp: Long) {
@@ -375,7 +384,11 @@ public class ReplayIntegration(
375384
}
376385
addFrame(screenshot, frameTimestamp, screen)
377386
}
378-
checkCanRecord()
387+
if (Looper.myLooper() == Looper.getMainLooper()) {
388+
checkCanRecord()
389+
} else {
390+
mainLooperHandler.post { checkCanRecord() }
391+
}
379392
}
380393

381394
override fun close() {

0 commit comments

Comments
 (0)