diff --git a/CHANGELOG.md b/CHANGELOG.md index d826db437c5..91e1d94eba0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ ### Performance +- 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)) - 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)) - 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)) - Batch and coalesce scope-persistence disk writes to reduce startup cost ([#5791](https://github.com/getsentry/sentry-java/pull/5791)) diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/ScreenshotEventProcessor.java b/sentry-android-core/src/main/java/io/sentry/android/core/ScreenshotEventProcessor.java index bbef7846cd9..dd0e259f937 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/ScreenshotEventProcessor.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/ScreenshotEventProcessor.java @@ -216,7 +216,7 @@ private boolean isMaskingEnabled() { try (final MaskRenderer maskRenderer = new MaskRenderer()) { // Make bitmap mutable if needed if (!screenshot.isMutable()) { - mutableBitmap = screenshot.copy(Bitmap.Config.ARGB_8888, true); + mutableBitmap = screenshot.copy(Bitmap.Config.RGB_565, true); if (mutableBitmap == null) { screenshot.recycle(); return null; diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/internal/util/ScreenshotUtils.java b/sentry-android-core/src/main/java/io/sentry/android/core/internal/util/ScreenshotUtils.java index db2b12122a5..092caa040db 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/internal/util/ScreenshotUtils.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/internal/util/ScreenshotUtils.java @@ -18,7 +18,7 @@ import java.io.ByteArrayOutputStream; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; @@ -97,9 +97,8 @@ public class ScreenshotUtils { } try { - // ARGB_8888 -> This configuration is very flexible and offers the best quality final Bitmap bitmap = - Bitmap.createBitmap(view.getWidth(), view.getHeight(), Bitmap.Config.ARGB_8888); + Bitmap.createBitmap(view.getWidth(), view.getHeight(), Bitmap.Config.RGB_565); final @NotNull CountDownLatch latch = new CountDownLatch(1); @@ -110,21 +109,21 @@ public class ScreenshotUtils { thread.start(); boolean success = false; + final AtomicInteger copyResultCode = new AtomicInteger(-1); try { final Handler handler = new Handler(thread.getLooper()); - final AtomicBoolean copyResultSuccess = new AtomicBoolean(false); PixelCopy.request( window, bitmap, copyResult -> { - copyResultSuccess.set(copyResult == PixelCopy.SUCCESS); + copyResultCode.set(copyResult); latch.countDown(); }, handler); - success = - latch.await(CAPTURE_TIMEOUT_MS, TimeUnit.MILLISECONDS) && copyResultSuccess.get(); + final boolean completed = latch.await(CAPTURE_TIMEOUT_MS, TimeUnit.MILLISECONDS); + success = completed && copyResultCode.get() == PixelCopy.SUCCESS; } catch (Throwable e) { // ignored logger.log(SentryLevel.ERROR, "Taking screenshot using PixelCopy failed.", e); @@ -133,6 +132,10 @@ public class ScreenshotUtils { } if (!success) { + logger.log( + SentryLevel.WARNING, + "PixelCopy failed for screenshot capture (result=%d).", + copyResultCode.get()); return null; } } else { diff --git a/sentry-android-replay/src/main/java/io/sentry/android/replay/screenshot/CanvasStrategy.kt b/sentry-android-replay/src/main/java/io/sentry/android/replay/screenshot/CanvasStrategy.kt index 406b23a9e4e..c3deefbf71b 100644 --- a/sentry-android-replay/src/main/java/io/sentry/android/replay/screenshot/CanvasStrategy.kt +++ b/sentry-android-replay/src/main/java/io/sentry/android/replay/screenshot/CanvasStrategy.kt @@ -97,7 +97,7 @@ internal class CanvasStrategy( Bitmap.createBitmap( config.recordingWidth, config.recordingHeight, - Bitmap.Config.ARGB_8888, + Bitmap.Config.RGB_565, ) } } diff --git a/sentry-android-replay/src/main/java/io/sentry/android/replay/screenshot/PixelCopyStrategy.kt b/sentry-android-replay/src/main/java/io/sentry/android/replay/screenshot/PixelCopyStrategy.kt index 14871b45d6d..06be58e19de 100644 --- a/sentry-android-replay/src/main/java/io/sentry/android/replay/screenshot/PixelCopyStrategy.kt +++ b/sentry-android-replay/src/main/java/io/sentry/android/replay/screenshot/PixelCopyStrategy.kt @@ -51,7 +51,15 @@ internal class PixelCopyStrategy( private val executor = executorProvider.getExecutor() private val mainLooperHandler = executorProvider.getMainLooperHandler() private val screenshot = - Bitmap.createBitmap(config.recordingWidth, config.recordingHeight, Bitmap.Config.ARGB_8888) + Bitmap.createBitmap( + config.recordingWidth, + config.recordingHeight, + if (options.sessionReplay.isCaptureSurfaceViews) { + Bitmap.Config.ARGB_8888 + } else { + Bitmap.Config.RGB_565 + }, + ) private val prescaledMatrix by lazy(NONE) { Matrix().apply { preScale(config.scaleFactorX, config.scaleFactorY) } } private val lastCaptureSuccessful = AtomicBoolean(false) diff --git a/sentry-android-replay/src/test/java/io/sentry/android/replay/screenshot/PixelCopyStrategyTest.kt b/sentry-android-replay/src/test/java/io/sentry/android/replay/screenshot/PixelCopyStrategyTest.kt index 0098ab24fc6..587927e9793 100644 --- a/sentry-android-replay/src/test/java/io/sentry/android/replay/screenshot/PixelCopyStrategyTest.kt +++ b/sentry-android-replay/src/test/java/io/sentry/android/replay/screenshot/PixelCopyStrategyTest.kt @@ -40,6 +40,7 @@ import kotlin.test.assertFalse import kotlin.test.assertTrue import org.junit.runner.RunWith import org.mockito.kotlin.any +import org.mockito.kotlin.argumentCaptor import org.mockito.kotlin.doAnswer import org.mockito.kotlin.mock import org.mockito.kotlin.never @@ -470,7 +471,9 @@ class PixelCopyStrategyTest { assertFalse(fixture.contentChangedMarked.get()) assertTrue(strategy.lastCaptureSuccessful()) - verify(fixture.callback).onScreenshotRecorded(any()) + val screenshot = argumentCaptor() + verify(fixture.callback).onScreenshotRecorded(screenshot.capture()) + assertEquals(Bitmap.Config.RGB_565, screenshot.firstValue.config) } @Test @@ -502,7 +505,9 @@ class PixelCopyStrategyTest { shadowOf(Looper.getMainLooper()).idle() assertTrue(strategy.lastCaptureSuccessful()) - verify(fixture.callback).onScreenshotRecorded(any()) + val screenshot = argumentCaptor() + verify(fixture.callback).onScreenshotRecorded(screenshot.capture()) + assertEquals(Bitmap.Config.ARGB_8888, screenshot.firstValue.config) } @Test