Skip to content

Commit 4af06f2

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 4af06f2

2 files changed

Lines changed: 15 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: 14 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,7 @@ public class ReplayIntegration(
352353
}
353354
addFrame(bitmap, frameTimeStamp, screen)
354355
}
355-
checkCanRecord()
356+
postOnMainThread { checkCanRecord() }
356357
}
357358

358359
override fun onScreenshotRecorded(screenshot: File, frameTimestamp: Long) {
@@ -375,7 +376,7 @@ public class ReplayIntegration(
375376
}
376377
addFrame(screenshot, frameTimestamp, screen)
377378
}
378-
checkCanRecord()
379+
postOnMainThread { checkCanRecord() }
379380
}
380381

381382
override fun close() {
@@ -444,6 +445,17 @@ public class ReplayIntegration(
444445
captureStrategy?.onTouchEvent(event)
445446
}
446447

448+
// Runs [block] on the main thread. If already there, executes inline; otherwise posts via
449+
// the main looper handler. Prevents deadlocks when lifecycle-lock-acquiring code (e.g.
450+
// checkCanRecord -> pauseInternal) is called from the replay executor thread.
451+
private inline fun postOnMainThread(crossinline block: () -> Unit) {
452+
if (Looper.myLooper() == Looper.getMainLooper()) {
453+
block()
454+
} else {
455+
mainLooperHandler.post { block() }
456+
}
457+
}
458+
447459
/**
448460
* Check if we're offline or rate-limited and pause for session mode to not overflow the envelope
449461
* cache.

0 commit comments

Comments
 (0)