Skip to content

ref(replay): Use main thread to schedule capture #4542

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Changelog

## Unreleased

### Improvements

- Session Replay: Use main thread looper to schedule replay capture ([#4542](https://github.com/getsentry/sentry-java/pull/4542))

## 8.16.1-alpha.2

### Fixes
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,13 +60,25 @@ internal class ScreenshotRecorder(
private val debugOverlayDrawable = DebugOverlayDrawable()

fun capture() {
if (options.sessionReplay.isDebug) {
options.logger.log(DEBUG, "Capturing screenshot, isCapturing: %s", isCapturing.get())
}
if (!isCapturing.get()) {
if (options.sessionReplay.isDebug) {
options.logger.log(DEBUG, "ScreenshotRecorder is paused, not capturing screenshot")
}
return
}

if (options.sessionReplay.isDebug) {
options.logger.log(
DEBUG,
"Capturing screenshot, contentChanged: %s, lastCaptureSuccessful: %s",
contentChanged.get(),
lastCaptureSuccessful.get(),
)
}

if (!contentChanged.get() && lastCaptureSuccessful.get()) {
screenshotRecorderCallback?.onScreenshotRecorded(screenshot)
return
Expand All @@ -84,99 +96,95 @@ internal class ScreenshotRecorder(
return
}

// postAtFrontOfQueue to ensure the view hierarchy and bitmap are ase close in-sync as possible
mainLooperHandler.post {
try {
contentChanged.set(false)
PixelCopy.request(
window,
screenshot,
{ copyResult: Int ->
if (copyResult != PixelCopy.SUCCESS) {
options.logger.log(INFO, "Failed to capture replay recording: %d", copyResult)
lastCaptureSuccessful.set(false)
return@request
}

// TODO: handle animations with heuristics (e.g. if we fall under this condition 2 times
// in a row, we should capture)
if (contentChanged.get()) {
options.logger.log(INFO, "Failed to determine view hierarchy, not capturing")
lastCaptureSuccessful.set(false)
return@request
}
try {
contentChanged.set(false)
PixelCopy.request(
window,
screenshot,
{ copyResult: Int ->
if (copyResult != PixelCopy.SUCCESS) {
options.logger.log(INFO, "Failed to capture replay recording: %d", copyResult)
lastCaptureSuccessful.set(false)
return@request
}

// TODO: handle animations with heuristics (e.g. if we fall under this condition 2 times
// in a row, we should capture)
if (contentChanged.get()) {
options.logger.log(INFO, "Failed to determine view hierarchy, not capturing")
lastCaptureSuccessful.set(false)
return@request
}

// TODO: disableAllMasking here and dont traverse?
val viewHierarchy = ViewHierarchyNode.fromView(root, null, 0, options)
root.traverse(viewHierarchy, options)

recorder.submitSafely(options, "screenshot_recorder.mask") {
val debugMasks = mutableListOf<Rect>()

val canvas = Canvas(screenshot)
canvas.setMatrix(prescaledMatrix)
viewHierarchy.traverse { node ->
if (node.shouldMask && (node.width > 0 && node.height > 0)) {
node.visibleRect ?: return@traverse false

// TODO: investigate why it returns true on RN when it shouldn't
// if (viewHierarchy.isObscured(node)) {
// return@traverse true
// }

val (visibleRects, color) =
when (node) {
is ImageViewHierarchyNode -> {
listOf(node.visibleRect) to screenshot.dominantColorForRect(node.visibleRect)
}

// TODO: disableAllMasking here and dont traverse?
val viewHierarchy = ViewHierarchyNode.fromView(root, null, 0, options)
root.traverse(viewHierarchy, options)

recorder.submitSafely(options, "screenshot_recorder.mask") {
val debugMasks = mutableListOf<Rect>()

val canvas = Canvas(screenshot)
canvas.setMatrix(prescaledMatrix)
viewHierarchy.traverse { node ->
if (node.shouldMask && (node.width > 0 && node.height > 0)) {
node.visibleRect ?: return@traverse false

// TODO: investigate why it returns true on RN when it shouldn't
// if (viewHierarchy.isObscured(node)) {
// return@traverse true
// }

val (visibleRects, color) =
when (node) {
is ImageViewHierarchyNode -> {
listOf(node.visibleRect) to
screenshot.dominantColorForRect(node.visibleRect)
}

is TextViewHierarchyNode -> {
val textColor =
node.layout?.dominantTextColor ?: node.dominantColor ?: Color.BLACK
node.layout.getVisibleRects(
node.visibleRect,
node.paddingLeft,
node.paddingTop,
) to textColor
}

else -> {
listOf(node.visibleRect) to Color.BLACK
}
is TextViewHierarchyNode -> {
val textColor =
node.layout?.dominantTextColor ?: node.dominantColor ?: Color.BLACK
node.layout.getVisibleRects(
node.visibleRect,
node.paddingLeft,
node.paddingTop,
) to textColor
}

maskingPaint.setColor(color)
visibleRects.forEach { rect ->
canvas.drawRoundRect(RectF(rect), 10f, 10f, maskingPaint)
}
if (options.replayController.isDebugMaskingOverlayEnabled()) {
debugMasks.addAll(visibleRects)
else -> {
listOf(node.visibleRect) to Color.BLACK
}
}

maskingPaint.setColor(color)
visibleRects.forEach { rect ->
canvas.drawRoundRect(RectF(rect), 10f, 10f, maskingPaint)
}
if (options.replayController.isDebugMaskingOverlayEnabled()) {
debugMasks.addAll(visibleRects)
}
return@traverse true
}
return@traverse true
}

if (options.replayController.isDebugMaskingOverlayEnabled()) {
mainLooperHandler.post {
if (debugOverlayDrawable.callback == null) {
root.overlay.add(debugOverlayDrawable)
}
debugOverlayDrawable.updateMasks(debugMasks)
root.postInvalidate()
if (options.replayController.isDebugMaskingOverlayEnabled()) {
mainLooperHandler.post {
if (debugOverlayDrawable.callback == null) {
root.overlay.add(debugOverlayDrawable)
}
debugOverlayDrawable.updateMasks(debugMasks)
root.postInvalidate()
}
screenshotRecorderCallback?.onScreenshotRecorded(screenshot)
lastCaptureSuccessful.set(true)
contentChanged.set(false)
}
},
mainLooperHandler.handler,
)
} catch (e: Throwable) {
options.logger.log(WARNING, "Failed to capture replay recording", e)
lastCaptureSuccessful.set(false)
}
screenshotRecorderCallback?.onScreenshotRecorded(screenshot)
lastCaptureSuccessful.set(true)
contentChanged.set(false)
}
},
mainLooperHandler.handler,
)
} catch (e: Throwable) {
options.logger.log(WARNING, "Failed to capture replay recording", e)
lastCaptureSuccessful.set(false)
}
}

Expand Down
Loading
Loading