From 4f2c8a1a3c46de56058785e0675a0790bd7e5401 Mon Sep 17 00:00:00 2001 From: Roman Zavarnitsyn Date: Tue, 21 Jul 2026 18:23:24 +0200 Subject: [PATCH 1/9] 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 --- .../screenshot/PixelCopyStrategyTest.kt | 92 +++++++++++++++++++ 1 file changed, 92 insertions(+) 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 0098ab24fc..b242c58450 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 @@ -396,6 +396,98 @@ class PixelCopyStrategyTest { verify(executor).submit(any()) } + @Test + @Config(shadows = [DeferredWindowPixelCopyShadow::class]) + fun `capture drops frame while PixelCopy is in flight`() { + val activity = buildActivity(SimpleActivity::class.java).setup() + shadowOf(Looper.getMainLooper()).idle() + val root = activity.get().findViewById(android.R.id.content) + val strategy = fixture.getSut(executor = fixture.inlineExecutor()) + + strategy.capture(root) + strategy.capture(root) + DeferredWindowPixelCopyShadow.flush() + shadowOf(Looper.getMainLooper()).idle() + + verify(fixture.callback).onScreenshotRecorded(any()) + + strategy.capture(root) + DeferredWindowPixelCopyShadow.flush() + shadowOf(Looper.getMainLooper()).idle() + + verify(fixture.callback, times(2)).onScreenshotRecorded(any()) + } + + @Test + @Config(shadows = [DeferredWindowPixelCopyShadow::class]) + fun `capture drops frame while masking is in flight`() { + val activity = buildActivity(SimpleActivity::class.java).setup() + shadowOf(Looper.getMainLooper()).idle() + val root = activity.get().findViewById(android.R.id.content) + val tasks = mutableListOf() + val executor = mock() + whenever(executor.submit(any())).doAnswer { + tasks += it.getArgument(0) + mock>() + } + val strategy = fixture.getSut(executor) + + strategy.capture(root) + DeferredWindowPixelCopyShadow.flush() + shadowOf(Looper.getMainLooper()).idle() + strategy.capture(root) + DeferredWindowPixelCopyShadow.flush() + shadowOf(Looper.getMainLooper()).idle() + + assertEquals(1, tasks.size) + tasks.removeAt(0).run() + + strategy.capture(root) + DeferredWindowPixelCopyShadow.flush() + shadowOf(Looper.getMainLooper()).idle() + + assertEquals(1, tasks.size) + } + + @Test + @Config(shadows = [DeferredWindowPixelCopyShadow::class]) + fun `emitLastScreenshot skips while frame is in flight`() { + val activity = buildActivity(SimpleActivity::class.java).setup() + shadowOf(Looper.getMainLooper()).idle() + val root = activity.get().findViewById(android.R.id.content) + val strategy = fixture.getSut(executor = fixture.inlineExecutor()) + captureStableFrame(strategy, root) + + strategy.capture(root) + strategy.emitLastScreenshot() + + verify(fixture.callback).onScreenshotRecorded(any()) + + DeferredWindowPixelCopyShadow.flush() + shadowOf(Looper.getMainLooper()).idle() + verify(fixture.callback, times(2)).onScreenshotRecorded(any()) + } + + @Test + @Config(shadows = [DeferredWindowPixelCopyShadow::class]) + fun `close defers cleanup until PixelCopy completes`() { + val activity = buildActivity(SimpleActivity::class.java).setup() + shadowOf(Looper.getMainLooper()).idle() + val root = activity.get().findViewById(android.R.id.content) + val executor = mock() + val strategy = fixture.getSut(executor) + + strategy.capture(root) + strategy.close() + + verify(executor, never()).submit(any()) + + DeferredWindowPixelCopyShadow.flush() + shadowOf(Looper.getMainLooper()).idle() + + verify(executor).submit(any()) + } + @Test @Config(shadows = [DeferredWindowPixelCopyShadow::class]) fun `capture skips the first unstable PixelCopy result`() { From d63d92f6d6fa452c4f1fb6f6458e50bc7301c2f5 Mon Sep 17 00:00:00 2001 From: Roman Zavarnitsyn Date: Wed, 22 Jul 2026 13:57:05 +0200 Subject: [PATCH 2/9] 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 --- .../sentry/android/replay/screenshot/PixelCopyStrategyTest.kt | 3 +++ 1 file changed, 3 insertions(+) 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 b242c58450..2dbbb6e6cd 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 @@ -406,6 +406,9 @@ class PixelCopyStrategyTest { strategy.capture(root) strategy.capture(root) + + assertTrue(fixture.contentChangedMarked.get()) + DeferredWindowPixelCopyShadow.flush() shadowOf(Looper.getMainLooper()).idle() From 26848005a2d1d4d5bff2a5d95c618f0b5b4ecfb6 Mon Sep 17 00:00:00 2001 From: Roman Zavarnitsyn Date: Wed, 22 Jul 2026 17:45:28 +0200 Subject: [PATCH 3/9] 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 --- .../sentry/android/replay/screenshot/CanvasStrategy.kt | 2 +- .../android/replay/screenshot/PixelCopyStrategy.kt | 10 +++++++++- .../android/replay/screenshot/PixelCopyStrategyTest.kt | 9 +++++++-- 3 files changed, 17 insertions(+), 4 deletions(-) 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 406b23a9e4..c3deefbf71 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 14871b45d6..06be58e19d 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 2dbbb6e6cd..44d1cb2fb8 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 @@ -565,7 +566,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 @@ -597,7 +600,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 From 953f2c670f469c3591a8736348d262deffc64a00 Mon Sep 17 00:00:00 2001 From: Roman Zavarnitsyn Date: Wed, 29 Jul 2026 21:23:46 +0200 Subject: [PATCH 4/9] 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) --- .../java/io/sentry/android/core/ScreenshotEventProcessor.java | 2 +- .../io/sentry/android/core/internal/util/ScreenshotUtils.java | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) 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 bbef7846cd..dd0e259f93 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 db2b12122a..ec9235b285 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 @@ -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); From 3dfd8f9fc094393baf22198e4a70d61a3678d299 Mon Sep 17 00:00:00 2001 From: Roman Zavarnitsyn Date: Thu, 30 Jul 2026 15:22:07 +0200 Subject: [PATCH 5/9] fix(replay): Remove duplicate test methods from rebase Co-Authored-By: Claude Opus 4.6 (1M context) --- .../screenshot/PixelCopyStrategyTest.kt | 95 ------------------- 1 file changed, 95 deletions(-) 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 44d1cb2fb8..587927e979 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 @@ -397,101 +397,6 @@ class PixelCopyStrategyTest { verify(executor).submit(any()) } - @Test - @Config(shadows = [DeferredWindowPixelCopyShadow::class]) - fun `capture drops frame while PixelCopy is in flight`() { - val activity = buildActivity(SimpleActivity::class.java).setup() - shadowOf(Looper.getMainLooper()).idle() - val root = activity.get().findViewById(android.R.id.content) - val strategy = fixture.getSut(executor = fixture.inlineExecutor()) - - strategy.capture(root) - strategy.capture(root) - - assertTrue(fixture.contentChangedMarked.get()) - - DeferredWindowPixelCopyShadow.flush() - shadowOf(Looper.getMainLooper()).idle() - - verify(fixture.callback).onScreenshotRecorded(any()) - - strategy.capture(root) - DeferredWindowPixelCopyShadow.flush() - shadowOf(Looper.getMainLooper()).idle() - - verify(fixture.callback, times(2)).onScreenshotRecorded(any()) - } - - @Test - @Config(shadows = [DeferredWindowPixelCopyShadow::class]) - fun `capture drops frame while masking is in flight`() { - val activity = buildActivity(SimpleActivity::class.java).setup() - shadowOf(Looper.getMainLooper()).idle() - val root = activity.get().findViewById(android.R.id.content) - val tasks = mutableListOf() - val executor = mock() - whenever(executor.submit(any())).doAnswer { - tasks += it.getArgument(0) - mock>() - } - val strategy = fixture.getSut(executor) - - strategy.capture(root) - DeferredWindowPixelCopyShadow.flush() - shadowOf(Looper.getMainLooper()).idle() - strategy.capture(root) - DeferredWindowPixelCopyShadow.flush() - shadowOf(Looper.getMainLooper()).idle() - - assertEquals(1, tasks.size) - tasks.removeAt(0).run() - - strategy.capture(root) - DeferredWindowPixelCopyShadow.flush() - shadowOf(Looper.getMainLooper()).idle() - - assertEquals(1, tasks.size) - } - - @Test - @Config(shadows = [DeferredWindowPixelCopyShadow::class]) - fun `emitLastScreenshot skips while frame is in flight`() { - val activity = buildActivity(SimpleActivity::class.java).setup() - shadowOf(Looper.getMainLooper()).idle() - val root = activity.get().findViewById(android.R.id.content) - val strategy = fixture.getSut(executor = fixture.inlineExecutor()) - captureStableFrame(strategy, root) - - strategy.capture(root) - strategy.emitLastScreenshot() - - verify(fixture.callback).onScreenshotRecorded(any()) - - DeferredWindowPixelCopyShadow.flush() - shadowOf(Looper.getMainLooper()).idle() - verify(fixture.callback, times(2)).onScreenshotRecorded(any()) - } - - @Test - @Config(shadows = [DeferredWindowPixelCopyShadow::class]) - fun `close defers cleanup until PixelCopy completes`() { - val activity = buildActivity(SimpleActivity::class.java).setup() - shadowOf(Looper.getMainLooper()).idle() - val root = activity.get().findViewById(android.R.id.content) - val executor = mock() - val strategy = fixture.getSut(executor) - - strategy.capture(root) - strategy.close() - - verify(executor, never()).submit(any()) - - DeferredWindowPixelCopyShadow.flush() - shadowOf(Looper.getMainLooper()).idle() - - verify(executor).submit(any()) - } - @Test @Config(shadows = [DeferredWindowPixelCopyShadow::class]) fun `capture skips the first unstable PixelCopy result`() { From 3a9e542908d7a0b27852d9082aece84e3cc2af92 Mon Sep 17 00:00:00 2001 From: Roman Zavarnitsyn Date: Thu, 30 Jul 2026 16:07:18 +0200 Subject: [PATCH 6/9] changelog Co-Authored-By: Claude Opus 4.6 (1M context) --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d826db437c..c69453ac50 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -49,6 +49,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)) - Create the outbox and cache directories lazily in their consumers instead of during SDK init, moving the `mkdirs()` calls off the init (main) thread ([#5792](https://github.com/getsentry/sentry-java/pull/5792)) - Reduce the number of SDK threads: `LifecycleWatcher` now schedules the session-end task on the shared timer executor instead of creating a dedicated `java.util.Timer` thread ([#5819](https://github.com/getsentry/sentry-java/pull/5819)) - Reduce the number of SDK threads: `RateLimiter` now schedules its rate-limit-lifted notifications on the shared timer executor instead of creating a dedicated `java.util.Timer` thread ([#5814](https://github.com/getsentry/sentry-java/pull/5814)) From a11aa9b77566184ee7520fd44961fc674c7332d2 Mon Sep 17 00:00:00 2001 From: Roman Zavarnitsyn Date: Thu, 30 Jul 2026 16:09:17 +0200 Subject: [PATCH 7/9] fix: Move changelog entry to Unreleased section Co-Authored-By: Claude Opus 4.6 (1M context) --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c69453ac50..91e1d94eba 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)) @@ -49,7 +50,6 @@ ### 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)) - Create the outbox and cache directories lazily in their consumers instead of during SDK init, moving the `mkdirs()` calls off the init (main) thread ([#5792](https://github.com/getsentry/sentry-java/pull/5792)) - Reduce the number of SDK threads: `LifecycleWatcher` now schedules the session-end task on the shared timer executor instead of creating a dedicated `java.util.Timer` thread ([#5819](https://github.com/getsentry/sentry-java/pull/5819)) - Reduce the number of SDK threads: `RateLimiter` now schedules its rate-limit-lifted notifications on the shared timer executor instead of creating a dedicated `java.util.Timer` thread ([#5814](https://github.com/getsentry/sentry-java/pull/5814)) From 7e0ae5ce70f5f41e03750a85faf8b760c516ed59 Mon Sep 17 00:00:00 2001 From: Roman Zavarnitsyn Date: Thu, 30 Jul 2026 16:44:12 +0200 Subject: [PATCH 8/9] fix(screenshot): Log PixelCopy result code on failure Co-Authored-By: Claude Opus 4.6 (1M context) --- .../android/core/internal/util/ScreenshotUtils.java | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) 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 ec9235b285..b0b8f430b4 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 @@ -19,6 +19,7 @@ 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; @@ -109,21 +110,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); @@ -132,6 +133,10 @@ public class ScreenshotUtils { } if (!success) { + logger.log( + SentryLevel.WARNING, + "PixelCopy failed for screenshot capture (result=%d).", + copyResultCode.get()); return null; } } else { From 685bc37105186e7fb53d9d62c867cf0389ecc548 Mon Sep 17 00:00:00 2001 From: Sentry Github Bot Date: Thu, 30 Jul 2026 14:47:06 +0000 Subject: [PATCH 9/9] Format code --- .../io/sentry/android/core/internal/util/ScreenshotUtils.java | 1 - 1 file changed, 1 deletion(-) 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 b0b8f430b4..092caa040d 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,6 @@ 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;