Skip to content

Commit 5cb2c36

Browse files
romtsncodexclaudegetsentry-bot
authored
perf(replay): Use RGB_565 when alpha is unnecessary (#5821)
* fix(replay): Prevent concurrent PixelCopy frame access Keep PixelCopy, masking, compositing, and cleanup from accessing the shared bitmap concurrently. Fixes GH-5340 Co-Authored-By: Codex <noreply@openai.com> * fix(android-replay): Preserve pending PixelCopy capture Re-arm the recorder's content-change gate when a capture is skipped because another frame is still in flight. This ensures the latest UI state is retried on the next capture interval. Refs GH-5340 Co-Authored-By: OpenAI Codex <noreply@openai.com> * perf(android-replay): Use RGB_565 when alpha is unnecessary Reduce replay screenshot memory while retaining ARGB_8888 for SurfaceView compositing. Refs GH-5340 Co-Authored-By: Codex <noreply@openai.com> * perf(screenshot): Use RGB_565 for screenshot capture bitmaps Screen content is always opaque so the alpha channel in ARGB_8888 is wasted. RGB_565 halves the per-screenshot bitmap memory (~2B/px vs 4B/px). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(replay): Remove duplicate test methods from rebase Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * changelog Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: Move changelog entry to Unreleased section Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(screenshot): Log PixelCopy result code on failure Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Format code --------- Co-authored-by: Codex <noreply@openai.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: Sentry Github Bot <bot+github-bot@sentry.io>
1 parent 1e904b2 commit 5cb2c36

6 files changed

Lines changed: 29 additions & 12 deletions

File tree

‎CHANGELOG.md‎

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

1313
### Performance
1414

15+
- Use `RGB_565` instead of `ARGB_8888` for screenshot and replay capture bitmaps, halving per-frame memory usage ([#5821](https://github.com/getsentry/sentry-java/pull/5821))
1516
- Remove an unused lock from `SentryPerformanceProvider`, which was allocated on every cold start in `ContentProvider.onCreate` without ever being acquired ([#5871](https://github.com/getsentry/sentry-java/pull/5871))
1617
- Parse the app start profiling config with only the deserializer it needs instead of building a full `JsonSerializer` and `SentryOptions`, cutting 188 of 221 allocations on the main thread before `Application.onCreate` ([#5867](https://github.com/getsentry/sentry-java/pull/5867))
1718
- Batch and coalesce scope-persistence disk writes to reduce startup cost ([#5791](https://github.com/getsentry/sentry-java/pull/5791))

‎sentry-android-core/src/main/java/io/sentry/android/core/ScreenshotEventProcessor.java‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -216,7 +216,7 @@ private boolean isMaskingEnabled() {
216216
try (final MaskRenderer maskRenderer = new MaskRenderer()) {
217217
// Make bitmap mutable if needed
218218
if (!screenshot.isMutable()) {
219-
mutableBitmap = screenshot.copy(Bitmap.Config.ARGB_8888, true);
219+
mutableBitmap = screenshot.copy(Bitmap.Config.RGB_565, true);
220220
if (mutableBitmap == null) {
221221
screenshot.recycle();
222222
return null;

‎sentry-android-core/src/main/java/io/sentry/android/core/internal/util/ScreenshotUtils.java‎

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
import java.io.ByteArrayOutputStream;
1919
import java.util.concurrent.CountDownLatch;
2020
import java.util.concurrent.TimeUnit;
21-
import java.util.concurrent.atomic.AtomicBoolean;
21+
import java.util.concurrent.atomic.AtomicInteger;
2222
import org.jetbrains.annotations.ApiStatus;
2323
import org.jetbrains.annotations.NotNull;
2424

@@ -97,9 +97,8 @@ public class ScreenshotUtils {
9797
}
9898

9999
try {
100-
// ARGB_8888 -> This configuration is very flexible and offers the best quality
101100
final Bitmap bitmap =
102-
Bitmap.createBitmap(view.getWidth(), view.getHeight(), Bitmap.Config.ARGB_8888);
101+
Bitmap.createBitmap(view.getWidth(), view.getHeight(), Bitmap.Config.RGB_565);
103102

104103
final @NotNull CountDownLatch latch = new CountDownLatch(1);
105104

@@ -110,21 +109,21 @@ public class ScreenshotUtils {
110109
thread.start();
111110

112111
boolean success = false;
112+
final AtomicInteger copyResultCode = new AtomicInteger(-1);
113113
try {
114114
final Handler handler = new Handler(thread.getLooper());
115-
final AtomicBoolean copyResultSuccess = new AtomicBoolean(false);
116115

117116
PixelCopy.request(
118117
window,
119118
bitmap,
120119
copyResult -> {
121-
copyResultSuccess.set(copyResult == PixelCopy.SUCCESS);
120+
copyResultCode.set(copyResult);
122121
latch.countDown();
123122
},
124123
handler);
125124

126-
success =
127-
latch.await(CAPTURE_TIMEOUT_MS, TimeUnit.MILLISECONDS) && copyResultSuccess.get();
125+
final boolean completed = latch.await(CAPTURE_TIMEOUT_MS, TimeUnit.MILLISECONDS);
126+
success = completed && copyResultCode.get() == PixelCopy.SUCCESS;
128127
} catch (Throwable e) {
129128
// ignored
130129
logger.log(SentryLevel.ERROR, "Taking screenshot using PixelCopy failed.", e);
@@ -133,6 +132,10 @@ public class ScreenshotUtils {
133132
}
134133

135134
if (!success) {
135+
logger.log(
136+
SentryLevel.WARNING,
137+
"PixelCopy failed for screenshot capture (result=%d).",
138+
copyResultCode.get());
136139
return null;
137140
}
138141
} else {

‎sentry-android-replay/src/main/java/io/sentry/android/replay/screenshot/CanvasStrategy.kt‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ internal class CanvasStrategy(
9797
Bitmap.createBitmap(
9898
config.recordingWidth,
9999
config.recordingHeight,
100-
Bitmap.Config.ARGB_8888,
100+
Bitmap.Config.RGB_565,
101101
)
102102
}
103103
}

‎sentry-android-replay/src/main/java/io/sentry/android/replay/screenshot/PixelCopyStrategy.kt‎

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,15 @@ internal class PixelCopyStrategy(
5151
private val executor = executorProvider.getExecutor()
5252
private val mainLooperHandler = executorProvider.getMainLooperHandler()
5353
private val screenshot =
54-
Bitmap.createBitmap(config.recordingWidth, config.recordingHeight, Bitmap.Config.ARGB_8888)
54+
Bitmap.createBitmap(
55+
config.recordingWidth,
56+
config.recordingHeight,
57+
if (options.sessionReplay.isCaptureSurfaceViews) {
58+
Bitmap.Config.ARGB_8888
59+
} else {
60+
Bitmap.Config.RGB_565
61+
},
62+
)
5563
private val prescaledMatrix by
5664
lazy(NONE) { Matrix().apply { preScale(config.scaleFactorX, config.scaleFactorY) } }
5765
private val lastCaptureSuccessful = AtomicBoolean(false)

‎sentry-android-replay/src/test/java/io/sentry/android/replay/screenshot/PixelCopyStrategyTest.kt‎

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ import kotlin.test.assertFalse
4040
import kotlin.test.assertTrue
4141
import org.junit.runner.RunWith
4242
import org.mockito.kotlin.any
43+
import org.mockito.kotlin.argumentCaptor
4344
import org.mockito.kotlin.doAnswer
4445
import org.mockito.kotlin.mock
4546
import org.mockito.kotlin.never
@@ -470,7 +471,9 @@ class PixelCopyStrategyTest {
470471

471472
assertFalse(fixture.contentChangedMarked.get())
472473
assertTrue(strategy.lastCaptureSuccessful())
473-
verify(fixture.callback).onScreenshotRecorded(any<Bitmap>())
474+
val screenshot = argumentCaptor<Bitmap>()
475+
verify(fixture.callback).onScreenshotRecorded(screenshot.capture())
476+
assertEquals(Bitmap.Config.RGB_565, screenshot.firstValue.config)
474477
}
475478

476479
@Test
@@ -502,7 +505,9 @@ class PixelCopyStrategyTest {
502505
shadowOf(Looper.getMainLooper()).idle()
503506

504507
assertTrue(strategy.lastCaptureSuccessful())
505-
verify(fixture.callback).onScreenshotRecorded(any<Bitmap>())
508+
val screenshot = argumentCaptor<Bitmap>()
509+
verify(fixture.callback).onScreenshotRecorded(screenshot.capture())
510+
assertEquals(Bitmap.Config.ARGB_8888, screenshot.firstValue.config)
506511
}
507512

508513
@Test

0 commit comments

Comments
 (0)