diff --git a/CHANGELOG.md b/CHANGELOG.md index ff7e55338a8..fb1ae41ad65 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ - Send `otel.kind` to Sentry ([#3907](https://github.com/getsentry/sentry-java/pull/3907)) - Allow passing `environment` to `CheckinUtils.withCheckIn` ([3889](https://github.com/getsentry/sentry-java/pull/3889)) +- Changes up to `7.18.0` have been merged and are now included as well ### Fixes @@ -59,6 +60,7 @@ - Uses faster Random implementation to generate UUIDs - Android 15: Add support for 16KB page sizes ([#3851](https://github.com/getsentry/sentry-java/pull/3851)) - See https://developer.android.com/guide/practices/page-sizes for more details +- Changes up to `7.17.0` have been merged and are now included as well ### Fixes @@ -326,6 +328,29 @@ You may also use `LifecycleHelper.close(token)`, e.g. in case you need to pass t - Report exceptions returned by Throwable.getSuppressed() to Sentry as exception groups ([#3396] https://github.com/getsentry/sentry-java/pull/3396) +## 7.18.0 + +### Features + +- Android 15: Add support for 16KB page sizes ([#3620](https://github.com/getsentry/sentry-java/pull/3620)) + - See https://developer.android.com/guide/practices/page-sizes for more details +- Session Replay: Add `beforeSendReplay` callback ([#3855](https://github.com/getsentry/sentry-java/pull/3855)) +- Session Replay: Add support for masking/unmasking view containers ([#3881](https://github.com/getsentry/sentry-java/pull/3881)) + +### Fixes + +- Avoid collecting normal frames ([#3782](https://github.com/getsentry/sentry-java/pull/3782)) +- Ensure android initialization process continues even if options configuration block throws an exception ([#3887](https://github.com/getsentry/sentry-java/pull/3887)) +- Do not report parsing ANR error when there are no threads ([#3888](https://github.com/getsentry/sentry-java/pull/3888)) + - This should significantly reduce the number of events with message "Sentry Android SDK failed to parse system thread dump..." reported +- Session Replay: Disable replay in session mode when rate limit is active ([#3854](https://github.com/getsentry/sentry-java/pull/3854)) + +### Dependencies + +- Bump Native SDK from v0.7.2 to v0.7.8 ([#3620](https://github.com/getsentry/sentry-java/pull/3620)) + - [changelog](https://github.com/getsentry/sentry-native/blob/master/CHANGELOG.md#078) + - [diff](https://github.com/getsentry/sentry-native/compare/0.7.2...0.7.8) + ## 7.17.0 ### Features diff --git a/build.gradle.kts b/build.gradle.kts index 7c1a10181b7..e7d828e61f4 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -227,7 +227,7 @@ spotless { target("**/*.java") removeUnusedImports() googleJavaFormat() - targetExclude("**/generated/**", "**/vendor/**") + targetExclude("**/generated/**", "**/vendor/**", "**/sentry-native/**") } kotlin { target("**/*.kt") diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/AnrV2Integration.java b/sentry-android-core/src/main/java/io/sentry/android/core/AnrV2Integration.java index 9efb1ad13b8..6b66106d3f4 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/AnrV2Integration.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/AnrV2Integration.java @@ -313,8 +313,11 @@ private void reportAsSentryEvent( final ThreadDumpParser threadDumpParser = new ThreadDumpParser(options, isBackground); final List threads = threadDumpParser.parse(lines); if (threads.isEmpty()) { - // if the list is empty this means our regex matching is garbage and this is still error - return new ParseResult(ParseResult.Type.ERROR, dump); + // if the list is empty this means the system failed to capture a proper thread dump of + // the android threads, and only contains kernel-level threads and statuses, those ANRs + // are not actionable and neither they are reported by Google Play Console, so we just + // fall back to not reporting them + return new ParseResult(ParseResult.Type.NO_DUMP); } return new ParseResult(ParseResult.Type.DUMP, dump, threads); } catch (Throwable e) { diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/SentryAndroid.java b/sentry-android-core/src/main/java/io/sentry/android/core/SentryAndroid.java index 6a7745104d3..ea1f8ae875c 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/SentryAndroid.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/SentryAndroid.java @@ -133,7 +133,17 @@ public static void init( isTimberAvailable, isReplayAvailable); - configuration.configure(options); + try { + configuration.configure(options); + } catch (Throwable t) { + // let it slip, but log it + options + .getLogger() + .log( + SentryLevel.ERROR, + "Error in the 'OptionsConfiguration.configure' callback.", + t); + } // if SentryPerformanceProvider was disabled or removed, // we set the app start / sdk init time here instead diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/SentryFrameMetrics.java b/sentry-android-core/src/main/java/io/sentry/android/core/SentryFrameMetrics.java index 23409eadeab..cf2241757ca 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/SentryFrameMetrics.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/SentryFrameMetrics.java @@ -6,7 +6,6 @@ @ApiStatus.Internal final class SentryFrameMetrics { - private int normalFrameCount; private int slowFrameCount; private int frozenFrameCount; @@ -18,15 +17,11 @@ final class SentryFrameMetrics { public SentryFrameMetrics() {} public SentryFrameMetrics( - final int normalFrameCount, final int slowFrameCount, final long slowFrameDelayNanos, final int frozenFrameCount, final long frozenFrameDelayNanos, final long totalDurationNanos) { - - this.normalFrameCount = normalFrameCount; - this.slowFrameCount = slowFrameCount; this.slowFrameDelayNanos = slowFrameDelayNanos; @@ -47,15 +42,9 @@ public void addFrame( } else if (isSlow) { slowFrameDelayNanos += delayNanos; slowFrameCount += 1; - } else { - normalFrameCount += 1; } } - public int getNormalFrameCount() { - return normalFrameCount; - } - public int getSlowFrameCount() { return slowFrameCount; } @@ -72,8 +61,9 @@ public long getFrozenFrameDelayNanos() { return frozenFrameDelayNanos; } - public int getTotalFrameCount() { - return normalFrameCount + slowFrameCount + frozenFrameCount; + /** Returns the sum of the slow and frozen frames. */ + public int getSlowFrozenFrameCount() { + return slowFrameCount + frozenFrameCount; } public long getTotalDurationNanos() { @@ -81,8 +71,6 @@ public long getTotalDurationNanos() { } public void clear() { - normalFrameCount = 0; - slowFrameCount = 0; slowFrameDelayNanos = 0; @@ -95,7 +83,6 @@ public void clear() { @NotNull public SentryFrameMetrics duplicate() { return new SentryFrameMetrics( - normalFrameCount, slowFrameCount, slowFrameDelayNanos, frozenFrameCount, @@ -110,7 +97,6 @@ public SentryFrameMetrics duplicate() { @NotNull public SentryFrameMetrics diffTo(final @NotNull SentryFrameMetrics other) { return new SentryFrameMetrics( - normalFrameCount - other.normalFrameCount, slowFrameCount - other.slowFrameCount, slowFrameDelayNanos - other.slowFrameDelayNanos, frozenFrameCount - other.frozenFrameCount, @@ -123,8 +109,7 @@ public SentryFrameMetrics diffTo(final @NotNull SentryFrameMetrics other) { * to 0 */ public boolean containsValidData() { - return normalFrameCount >= 0 - && slowFrameCount >= 0 + return slowFrameCount >= 0 && slowFrameDelayNanos >= 0 && frozenFrameCount >= 0 && frozenFrameDelayNanos >= 0 diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/SpanFrameMetricsCollector.java b/sentry-android-core/src/main/java/io/sentry/android/core/SpanFrameMetricsCollector.java index d4e47ddc806..a83454d29b7 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/SpanFrameMetricsCollector.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/SpanFrameMetricsCollector.java @@ -196,7 +196,7 @@ private void captureFrameMetrics(@NotNull final ISpan span) { } } - int totalFrameCount = frameMetrics.getTotalFrameCount(); + int totalFrameCount = frameMetrics.getSlowFrozenFrameCount(); final long nextScheduledFrameNanos = frameMetricsCollector.getLastKnownFrameStartTimeNanos(); // nextScheduledFrameNanos might be -1 if no frames have been scheduled for drawing yet @@ -258,15 +258,17 @@ public void onFrameMetricCollected( (long) ((double) ONE_SECOND_NANOS / (double) refreshRate); lastKnownFrameDurationNanos = expectedFrameDurationNanos; - frames.add( - new Frame( - frameStartNanos, - frameEndNanos, - durationNanos, - delayNanos, - isSlow, - isFrozen, - expectedFrameDurationNanos)); + if (isSlow || isFrozen) { + frames.add( + new Frame( + frameStartNanos, + frameEndNanos, + durationNanos, + delayNanos, + isSlow, + isFrozen, + expectedFrameDurationNanos)); + } } private static int interpolateFrameCount( @@ -281,7 +283,7 @@ private static int interpolateFrameCount( final long frameMetricsDurationNanos = frameMetrics.getTotalDurationNanos(); final long nonRenderedDuration = spanDurationNanos - frameMetricsDurationNanos; if (nonRenderedDuration > 0) { - return (int) (nonRenderedDuration / frameDurationNanos); + return (int) Math.ceil((double) nonRenderedDuration / frameDurationNanos); } return 0; } diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/AnrV2IntegrationTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/AnrV2IntegrationTest.kt index 1abcd43719b..68339a4b797 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/AnrV2IntegrationTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/AnrV2IntegrationTest.kt @@ -101,7 +101,8 @@ class AnrV2IntegrationTest { reason: Int? = ApplicationExitInfo.REASON_ANR, timestamp: Long? = null, importance: Int? = null, - addTrace: Boolean = true + addTrace: Boolean = true, + addBadTrace: Boolean = false ) { val builder = ApplicationExitInfoBuilder.newBuilder() if (reason != null) { @@ -117,8 +118,36 @@ class AnrV2IntegrationTest { if (!addTrace) { return } - whenever(mock.traceInputStream).thenReturn( - """ + if (addBadTrace) { + whenever(mock.traceInputStream).thenReturn( + """ + Subject: Input dispatching timed out (7985007 com.example.app/com.example.app.ui.MainActivity (server) is not responding. Waited 5000ms for FocusEvent(hasFocus=false)) + Here are no Binder-related exception messages available. + Pid(12233) have D state thread(tid:12236 name:Signal Catcher) + + + RssHwmKb: 823716 + RssKb: 548348 + RssAnonKb: 382156 + RssShmemKb: 13304 + VmSwapKb: 82484 + + + --- CriticalEventLog --- + capacity: 20 + timestamp_ms: 1731507490032 + window_ms: 300000 + + ----- dumping pid: 12233 at 313446151 + libdebuggerd_client: unexpected registration response: 0 + + ----- Waiting Channels: pid 12233 at 2024-11-13 19:48:09.980104540+0530 ----- + Cmd line: com.example.app:mainProcess + """.trimIndent().byteInputStream() + ) + } else { + whenever(mock.traceInputStream).thenReturn( + """ "main" prio=5 tid=1 Blocked | group="main" sCount=1 ucsCount=0 flags=1 obj=0x72a985e0 self=0xb400007cabc57380 | sysTid=28941 nice=-10 cgrp=top-app sched=0/0 handle=0x7deceb74f8 @@ -147,8 +176,9 @@ class AnrV2IntegrationTest { native: #02 pc 00000000000b63b0 /apex/com.android.runtime/lib64/bionic/libc.so (__pthread_start(void*)+208) (BuildId: 01331f74b0bb2cb958bdc15282b8ec7b) native: #03 pc 00000000000530b8 /apex/com.android.runtime/lib64/bionic/libc.so (__start_thread+64) (BuildId: 01331f74b0bb2cb958bdc15282b8ec7b) (no managed stack frames) - """.trimIndent().byteInputStream() - ) + """.trimIndent().byteInputStream() + ) + } } shadowActivityManager.addApplicationExitInfo(exitInfo) } @@ -551,4 +581,14 @@ class AnrV2IntegrationTest { verify(fixture.scopes, never()).captureEvent(any(), anyOrNull()) } + + @Test + fun `when traceInputStream has bad data, does not report ANR`() { + val integration = fixture.getSut(tmpDir, lastReportedAnrTimestamp = oldTimestamp) + fixture.addAppExitInfo(timestamp = newTimestamp, addBadTrace = true) + + integration.register(fixture.scopes, fixture.options) + + verify(fixture.scopes, never()).captureEvent(any(), anyOrNull()) + } } diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/SentryAndroidTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/SentryAndroidTest.kt index 68c564da4eb..6fe39bb9d8b 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/SentryAndroidTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/SentryAndroidTest.kt @@ -518,6 +518,19 @@ class SentryAndroidTest { assertEquals(99, AppStartMetrics.getInstance().appStartTimeSpan.startUptimeMs) } + @Test + fun `if the config options block throws still intializes android event processors`() { + lateinit var optionsRef: SentryOptions + fixture.initSut(context = mock()) { options -> + optionsRef = options + options.dsn = "https://key@sentry.io/123" + throw RuntimeException("Boom!") + } + + assertTrue(optionsRef.eventProcessors.any { it is DefaultAndroidEventProcessor }) + assertTrue(optionsRef.eventProcessors.any { it is AnrV2EventProcessor }) + } + private fun prefillScopeCache(cacheDir: String) { val scopeDir = File(cacheDir, SCOPE_CACHE).also { it.mkdirs() } File(scopeDir, BREADCRUMBS_FILENAME).writeText( diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/SentryFrameMetricsTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/SentryFrameMetricsTest.kt index 1e992041b0e..a8138b61ffe 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/SentryFrameMetricsTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/SentryFrameMetricsTest.kt @@ -6,15 +6,6 @@ import kotlin.test.assertFalse import kotlin.test.assertTrue class SentryFrameMetricsTest { - @Test - fun addFastFrame() { - val frameMetrics = SentryFrameMetrics() - frameMetrics.addFrame(10, 0, false, false) - assertEquals(1, frameMetrics.normalFrameCount) - - frameMetrics.addFrame(10, 0, false, false) - assertEquals(2, frameMetrics.normalFrameCount) - } @Test fun addSlowFrame() { @@ -43,10 +34,12 @@ class SentryFrameMetricsTest { @Test fun totalFrameCount() { val frameMetrics = SentryFrameMetrics() + // Normal frames are ignored frameMetrics.addFrame(10, 0, false, false) + // Slow and frozen frames are considered frameMetrics.addFrame(116, 100, true, false) frameMetrics.addFrame(1016, 1000, true, true) - assertEquals(3, frameMetrics.totalFrameCount) + assertEquals(2, frameMetrics.slowFrozenFrameCount) } @Test @@ -57,12 +50,11 @@ class SentryFrameMetricsTest { frameMetrics.addFrame(1016, 1000, true, true) val dup = frameMetrics.duplicate() - assertEquals(1, dup.normalFrameCount) assertEquals(1, dup.slowFrameCount) assertEquals(100, dup.slowFrameDelayNanos) assertEquals(1, dup.frozenFrameCount) assertEquals(1000, dup.frozenFrameDelayNanos) - assertEquals(3, dup.totalFrameCount) + assertEquals(2, dup.slowFrozenFrameCount) } @Test @@ -89,7 +81,7 @@ class SentryFrameMetricsTest { assertEquals(1, diff.frozenFrameCount) assertEquals(1000, diff.frozenFrameDelayNanos) - assertEquals(2, diff.totalFrameCount) + assertEquals(2, diff.slowFrozenFrameCount) } @Test @@ -102,12 +94,11 @@ class SentryFrameMetricsTest { frameMetrics.clear() - assertEquals(0, frameMetrics.normalFrameCount) assertEquals(0, frameMetrics.slowFrameCount) assertEquals(0, frameMetrics.slowFrameDelayNanos) assertEquals(0, frameMetrics.frozenFrameCount) assertEquals(0, frameMetrics.frozenFrameDelayNanos) - assertEquals(0, frameMetrics.totalFrameCount) + assertEquals(0, frameMetrics.slowFrozenFrameCount) } @Test diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/SpanFrameMetricsCollectorTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/SpanFrameMetricsCollectorTest.kt index d8ff8fde2e6..0527baf284a 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/SpanFrameMetricsCollectorTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/SpanFrameMetricsCollectorTest.kt @@ -192,7 +192,7 @@ class SpanFrameMetricsCollectorTest { sut.onFrameMetricCollected(0, 10, 10, 0, false, false, 60.0f) sut.onFrameMetricCollected(16, 48, 32, 16, true, false, 60.0f) sut.onFrameMetricCollected(60, 92, 32, 16, true, false, 60.0f) - sut.onFrameMetricCollected(100, 800, 800, 784, true, true, 60.0f) + sut.onFrameMetricCollected(100, 800, 700, 784, true, true, 60.0f) // then a second span starts fixture.timeNanos = 800 @@ -337,10 +337,11 @@ class SpanFrameMetricsCollectorTest { fixture.timeNanos = TimeUnit.SECONDS.toNanos(2) sut.onSpanFinished(span) - // then still 60 frames should be reported (1 second at 60fps) - verify(span).setData("frames.total", 60) + // then still 61 frames should be reported (1 second at 60fps with approximation) + verify(span).setData("frames.total", 61) verify(span).setData("frames.slow", 0) verify(span).setData("frames.frozen", 0) + verify(span).setData("frames.delay", 0.0) } @Test @@ -364,9 +365,9 @@ class SpanFrameMetricsCollectorTest { sut.onSpanFinished(span) // then - // still 60 fps should be reported for 1 seconds + // still 61 fps should be reported for 1 seconds (with approximation) // and one frame with frame delay should be reported (1s - 16ms) - verify(span).setData("frames.total", 61) + verify(span).setData("frames.total", 62) verify(span).setData("frames.slow", 0) verify(span).setData("frames.frozen", 1) verify(span).setData(eq("frames.delay"), AdditionalMatchers.eq(0.983333334, 0.01)) diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/internal/threaddump/ThreadDumpParserTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/internal/threaddump/ThreadDumpParserTest.kt index 9ef22d6a138..19de2e4935d 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/internal/threaddump/ThreadDumpParserTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/internal/threaddump/ThreadDumpParserTest.kt @@ -7,6 +7,7 @@ import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertNotNull import kotlin.test.assertNull +import kotlin.test.assertTrue class ThreadDumpParserTest { @@ -95,4 +96,15 @@ class ThreadDumpParserTest { assertEquals(28, lastFrame.lineno) assertNull(lastFrame.isInApp) } + + @Test + fun `thread dump garbage`() { + val lines = Lines.readLines(File("src/test/resources/thread_dump_bad_data.txt")) + val parser = ThreadDumpParser( + SentryOptions().apply { addInAppInclude("io.sentry.samples") }, + false + ) + val threads = parser.parse(lines) + assertTrue(threads.isEmpty()) + } } diff --git a/sentry-android-core/src/test/resources/thread_dump_bad_data.txt b/sentry-android-core/src/test/resources/thread_dump_bad_data.txt new file mode 100644 index 00000000000..abbe042fcee --- /dev/null +++ b/sentry-android-core/src/test/resources/thread_dump_bad_data.txt @@ -0,0 +1,1029 @@ +Subject: Input dispatching timed out (7985007 com.example.app/com.example.app.ui.MainActivity (server) is not responding. Waited 5000ms for FocusEvent(hasFocus=false)) +Here are no Binder-related exception messages available. +Pid(12233) have D state thread(tid:12236 name:Signal Catcher) + + +RssHwmKb: 823716 +RssKb: 548348 +RssAnonKb: 382156 +RssShmemKb: 13304 +VmSwapKb: 82484 + + +--- CriticalEventLog --- +capacity: 20 +timestamp_ms: 1731507490032 +window_ms: 300000 + +----- dumping pid: 12233 at 313446151 +libdebuggerd_client: unexpected registration response: 0 + +----- Waiting Channels: pid 12233 at 2024-11-13 19:48:09.980104540+0530 ----- +Cmd line: com.example.app:mainProcess + +sysTid=12233 state=R 0 +sysTid=12236 state=S do_sigtimedwait +sysTid=12237 state=S futex_wait_queue_me +sysTid=12238 state=S futex_wait_queue_me +sysTid=12239 state=S futex_wait_queue_me +sysTid=12240 state=S futex_wait_queue_me +sysTid=12241 state=S futex_wait_queue_me +sysTid=12242 state=S binder_wait_for_work +sysTid=12243 state=S binder_wait_for_work +sysTid=12245 state=S binder_wait_for_work +sysTid=12252 state=S futex_wait_queue_me +sysTid=12254 state=S inotify_read +sysTid=12257 state=S __arm64_sys_nanosleep +sysTid=12259 state=R 0 +sysTid=12260 state=S __arm64_sys_nanosleep +sysTid=12268 state=R 0 +sysTid=12269 state=S __arm64_sys_nanosleep +sysTid=12270 state=S __arm64_sys_nanosleep +sysTid=12278 state=S futex_wait_queue_me +sysTid=12279 state=S futex_wait_queue_me +sysTid=12280 state=S futex_wait_queue_me +sysTid=12283 state=S futex_wait_queue_me +sysTid=12287 state=S futex_wait_queue_me +sysTid=12290 state=S futex_wait_queue_me +sysTid=12291 state=S futex_wait_queue_me +sysTid=12292 state=S futex_wait_queue_me +sysTid=12295 state=S futex_wait_queue_me +sysTid=12296 state=S futex_wait_queue_me +sysTid=12297 state=S futex_wait_queue_me +sysTid=12298 state=S futex_wait_queue_me +sysTid=12304 state=S futex_wait_queue_me +sysTid=12310 state=S futex_wait_queue_me +sysTid=12311 state=S futex_wait_queue_me +sysTid=12312 state=S do_epoll_wait +sysTid=12314 state=S futex_wait_queue_me +sysTid=12315 state=S futex_wait_queue_me +sysTid=12316 state=S futex_wait_queue_me +sysTid=12317 state=S futex_wait_queue_me +sysTid=12319 state=S futex_wait_queue_me +sysTid=12320 state=S do_epoll_wait +sysTid=12321 state=S futex_wait_queue_me +sysTid=12323 state=S futex_wait_queue_me +sysTid=12324 state=S futex_wait_queue_me +sysTid=12325 state=S futex_wait_queue_me +sysTid=12328 state=S do_epoll_wait +sysTid=12341 state=S futex_wait_queue_me +sysTid=12343 state=S futex_wait_queue_me +sysTid=12344 state=S futex_wait_queue_me +sysTid=12349 state=S futex_wait_queue_me +sysTid=12350 state=S futex_wait_queue_me +sysTid=12351 state=S futex_wait_queue_me +sysTid=12352 state=S futex_wait_queue_me +sysTid=12353 state=S binder_wait_for_work +sysTid=12354 state=S futex_wait_queue_me +sysTid=12355 state=S futex_wait_queue_me +sysTid=12358 state=R lock_page_maybe_drop_mmap +sysTid=12362 state=S futex_wait_queue_me +sysTid=12363 state=S futex_wait_queue_me +sysTid=12365 state=S do_epoll_wait +sysTid=12366 state=S futex_wait_queue_me +sysTid=12367 state=S futex_wait_queue_me +sysTid=12368 state=S futex_wait_queue_me +sysTid=12370 state=S futex_wait_queue_me +sysTid=12371 state=S futex_wait_queue_me +sysTid=12373 state=S futex_wait_queue_me +sysTid=12384 state=S binder_wait_for_work +sysTid=12391 state=S futex_wait_queue_me +sysTid=12399 state=S do_epoll_wait +sysTid=12401 state=S futex_wait_queue_me +sysTid=12402 state=S futex_wait_queue_me +sysTid=12404 state=S futex_wait_queue_me +sysTid=12405 state=S do_epoll_wait +sysTid=12407 state=S futex_wait_queue_me +sysTid=12408 state=S futex_wait_queue_me +sysTid=12409 state=S do_wait +sysTid=12410 state=S futex_wait_queue_me +sysTid=12412 state=S do_epoll_wait +sysTid=12435 state=S do_epoll_wait +sysTid=12468 state=S do_epoll_wait +sysTid=12514 state=S futex_wait_queue_me +sysTid=12550 state=S futex_wait_queue_me +sysTid=12561 state=S binder_wait_for_work +sysTid=12567 state=S binder_wait_for_work +sysTid=12580 state=S futex_wait_queue_me +sysTid=12619 state=S futex_wait_queue_me +sysTid=12627 state=S futex_wait_queue_me +sysTid=12644 state=S futex_wait_queue_me +sysTid=12887 state=S futex_wait_queue_me +sysTid=13430 state=S futex_wait_queue_me +sysTid=13438 state=S futex_wait_queue_me +sysTid=13443 state=S futex_wait_queue_me +sysTid=13454 state=S futex_wait_queue_me +sysTid=13455 state=S futex_wait_queue_me +sysTid=13564 state=S binder_wait_for_work +sysTid=13576 state=S binder_wait_for_work +sysTid=13579 state=S binder_wait_for_work +sysTid=13616 state=S binder_wait_for_work +sysTid=13624 state=S futex_wait_queue_me +sysTid=13706 state=S futex_wait_queue_me +sysTid=13722 state=S futex_wait_queue_me +sysTid=13724 state=S futex_wait_queue_me +sysTid=13730 state=S futex_wait_queue_me +sysTid=13740 state=S futex_wait_queue_me +sysTid=13744 state=S futex_wait_queue_me +sysTid=13745 state=S futex_wait_queue_me +sysTid=13748 state=S futex_wait_queue_me +sysTid=13754 state=S futex_wait_queue_me +sysTid=13756 state=S futex_wait_queue_me +sysTid=13757 state=S futex_wait_queue_me +sysTid=13758 state=S futex_wait_queue_me +sysTid=13759 state=S futex_wait_queue_me +sysTid=13763 state=S futex_wait_queue_me +sysTid=13767 state=S futex_wait_queue_me +sysTid=13768 state=S futex_wait_queue_me +sysTid=13769 state=S futex_wait_queue_me +sysTid=13773 state=S futex_wait_queue_me +sysTid=13776 state=S futex_wait_queue_me +sysTid=13781 state=S futex_wait_queue_me +sysTid=13782 state=S futex_wait_queue_me +sysTid=13783 state=S futex_wait_queue_me +sysTid=13784 state=S futex_wait_queue_me +sysTid=13786 state=S futex_wait_queue_me +sysTid=13791 state=S futex_wait_queue_me +sysTid=13792 state=S futex_wait_queue_me +sysTid=13793 state=S futex_wait_queue_me +sysTid=13794 state=S futex_wait_queue_me +sysTid=13795 state=S futex_wait_queue_me +sysTid=13796 state=S futex_wait_queue_me +sysTid=13797 state=S futex_wait_queue_me +sysTid=13798 state=S futex_wait_queue_me +sysTid=13799 state=S futex_wait_queue_me +sysTid=13800 state=S futex_wait_queue_me +sysTid=13806 state=S futex_wait_queue_me +sysTid=13809 state=S futex_wait_queue_me +sysTid=13814 state=S futex_wait_queue_me +sysTid=13815 state=S futex_wait_queue_me +sysTid=13816 state=S futex_wait_queue_me +sysTid=13817 state=S futex_wait_queue_me +sysTid=13818 state=S futex_wait_queue_me +sysTid=13820 state=S futex_wait_queue_me +sysTid=13825 state=S futex_wait_queue_me +sysTid=13830 state=S futex_wait_queue_me +sysTid=13831 state=S futex_wait_queue_me +sysTid=13832 state=S futex_wait_queue_me +sysTid=13833 state=S futex_wait_queue_me +sysTid=13834 state=S futex_wait_queue_me +sysTid=13835 state=S futex_wait_queue_me +sysTid=13836 state=S futex_wait_queue_me +sysTid=13841 state=S futex_wait_queue_me +sysTid=13847 state=S futex_wait_queue_me +sysTid=13848 state=S futex_wait_queue_me +sysTid=13849 state=S futex_wait_queue_me +sysTid=13850 state=S futex_wait_queue_me +sysTid=13851 state=S futex_wait_queue_me +sysTid=13852 state=S futex_wait_queue_me +sysTid=13853 state=S futex_wait_queue_me +sysTid=13854 state=S futex_wait_queue_me +sysTid=13857 state=S futex_wait_queue_me +sysTid=13863 state=S futex_wait_queue_me +sysTid=13867 state=S futex_wait_queue_me +sysTid=13880 state=S futex_wait_queue_me +sysTid=13920 state=S futex_wait_queue_me +sysTid=13949 state=S futex_wait_queue_me +sysTid=13953 state=S futex_wait_queue_me +sysTid=13954 state=S futex_wait_queue_me +sysTid=13955 state=S futex_wait_queue_me +sysTid=13958 state=S futex_wait_queue_me +sysTid=13959 state=S futex_wait_queue_me +sysTid=13967 state=S futex_wait_queue_me +sysTid=13980 state=S futex_wait_queue_me +sysTid=13981 state=S futex_wait_queue_me +sysTid=13982 state=S futex_wait_queue_me +sysTid=13983 state=S futex_wait_queue_me +sysTid=13984 state=S futex_wait_queue_me +sysTid=13986 state=S futex_wait_queue_me +sysTid=13987 state=S futex_wait_queue_me +sysTid=13991 state=S futex_wait_queue_me +sysTid=13998 state=S futex_wait_queue_me +sysTid=13999 state=S futex_wait_queue_me +sysTid=14000 state=S futex_wait_queue_me +sysTid=14001 state=S futex_wait_queue_me +sysTid=14002 state=S futex_wait_queue_me +sysTid=14003 state=S futex_wait_queue_me +sysTid=14004 state=S futex_wait_queue_me +sysTid=14005 state=S futex_wait_queue_me +sysTid=14006 state=S futex_wait_queue_me +sysTid=14007 state=S futex_wait_queue_me +sysTid=14026 state=S futex_wait_queue_me +sysTid=14052 state=S futex_wait_queue_me +sysTid=14057 state=S futex_wait_queue_me +sysTid=14060 state=S futex_wait_queue_me +sysTid=14063 state=S futex_wait_queue_me +sysTid=14069 state=S futex_wait_queue_me +sysTid=14072 state=S futex_wait_queue_me +sysTid=14075 state=S futex_wait_queue_me +sysTid=14081 state=S futex_wait_queue_me +sysTid=14084 state=S futex_wait_queue_me +sysTid=14089 state=S futex_wait_queue_me +sysTid=14090 state=S futex_wait_queue_me +sysTid=14091 state=S futex_wait_queue_me +sysTid=14092 state=S futex_wait_queue_me +sysTid=14093 state=S futex_wait_queue_me +sysTid=14094 state=S futex_wait_queue_me +sysTid=14095 state=S futex_wait_queue_me +sysTid=14096 state=S futex_wait_queue_me +sysTid=14097 state=S futex_wait_queue_me +sysTid=14098 state=S futex_wait_queue_me +sysTid=14099 state=S futex_wait_queue_me +sysTid=14100 state=S futex_wait_queue_me +sysTid=14101 state=S futex_wait_queue_me +sysTid=14102 state=S futex_wait_queue_me +sysTid=14103 state=S futex_wait_queue_me +sysTid=14104 state=S futex_wait_queue_me +sysTid=14106 state=S futex_wait_queue_me +sysTid=14111 state=S futex_wait_queue_me +sysTid=14117 state=S futex_wait_queue_me +sysTid=14120 state=S futex_wait_queue_me +sysTid=14121 state=S futex_wait_queue_me +sysTid=14122 state=S futex_wait_queue_me +sysTid=14123 state=S futex_wait_queue_me +sysTid=14124 state=S futex_wait_queue_me +sysTid=14129 state=S futex_wait_queue_me +sysTid=14130 state=S futex_wait_queue_me +sysTid=14131 state=S futex_wait_queue_me +sysTid=14132 state=S futex_wait_queue_me +sysTid=14136 state=S futex_wait_queue_me +sysTid=14144 state=S futex_wait_queue_me +sysTid=14148 state=S futex_wait_queue_me +sysTid=14154 state=S futex_wait_queue_me +sysTid=14158 state=S futex_wait_queue_me +sysTid=14164 state=S futex_wait_queue_me +sysTid=14167 state=S futex_wait_queue_me +sysTid=14168 state=S futex_wait_queue_me +sysTid=14169 state=S futex_wait_queue_me +sysTid=14170 state=S futex_wait_queue_me +sysTid=14171 state=S futex_wait_queue_me +sysTid=14172 state=S futex_wait_queue_me +sysTid=14173 state=S futex_wait_queue_me +sysTid=14174 state=S futex_wait_queue_me +sysTid=14175 state=S futex_wait_queue_me +sysTid=14176 state=S futex_wait_queue_me +sysTid=14177 state=S futex_wait_queue_me +sysTid=14178 state=S futex_wait_queue_me +sysTid=14179 state=S futex_wait_queue_me +sysTid=14180 state=S futex_wait_queue_me +sysTid=14181 state=S futex_wait_queue_me +sysTid=14182 state=S futex_wait_queue_me +sysTid=14190 state=S futex_wait_queue_me +sysTid=14195 state=S futex_wait_queue_me +sysTid=14198 state=S futex_wait_queue_me +sysTid=14207 state=S futex_wait_queue_me +sysTid=14209 state=S futex_wait_queue_me +sysTid=14210 state=S futex_wait_queue_me +sysTid=14214 state=S futex_wait_queue_me +sysTid=14220 state=S futex_wait_queue_me +sysTid=14223 state=S futex_wait_queue_me +sysTid=14227 state=S futex_wait_queue_me +sysTid=14235 state=S futex_wait_queue_me +sysTid=14242 state=S futex_wait_queue_me +sysTid=14243 state=S futex_wait_queue_me +sysTid=14244 state=S futex_wait_queue_me +sysTid=14245 state=S futex_wait_queue_me +sysTid=14246 state=S futex_wait_queue_me +sysTid=14247 state=S futex_wait_queue_me +sysTid=14248 state=S futex_wait_queue_me +sysTid=14249 state=S futex_wait_queue_me +sysTid=14250 state=S futex_wait_queue_me +sysTid=14251 state=S futex_wait_queue_me +sysTid=14253 state=S futex_wait_queue_me +sysTid=14259 state=S futex_wait_queue_me +sysTid=14264 state=S futex_wait_queue_me +sysTid=14269 state=S futex_wait_queue_me +sysTid=14272 state=S futex_wait_queue_me +sysTid=14277 state=S futex_wait_queue_me +sysTid=14282 state=S futex_wait_queue_me +sysTid=14296 state=S futex_wait_queue_me +sysTid=14302 state=S futex_wait_queue_me +sysTid=14309 state=S futex_wait_queue_me +sysTid=14314 state=S futex_wait_queue_me +sysTid=14319 state=S futex_wait_queue_me +sysTid=14324 state=S futex_wait_queue_me +sysTid=14325 state=S futex_wait_queue_me +sysTid=14327 state=S futex_wait_queue_me +sysTid=14328 state=S futex_wait_queue_me +sysTid=14329 state=S futex_wait_queue_me +sysTid=14331 state=S futex_wait_queue_me +sysTid=14348 state=S futex_wait_queue_me +sysTid=14349 state=S futex_wait_queue_me +sysTid=14350 state=S futex_wait_queue_me +sysTid=14351 state=S futex_wait_queue_me +sysTid=14352 state=S futex_wait_queue_me +sysTid=14353 state=S futex_wait_queue_me +sysTid=14357 state=S futex_wait_queue_me +sysTid=14358 state=S futex_wait_queue_me +sysTid=14359 state=S futex_wait_queue_me +sysTid=14360 state=S futex_wait_queue_me +sysTid=14361 state=S futex_wait_queue_me +sysTid=14363 state=S futex_wait_queue_me +sysTid=14364 state=S futex_wait_queue_me +sysTid=14365 state=S futex_wait_queue_me +sysTid=14366 state=S futex_wait_queue_me +sysTid=14367 state=S futex_wait_queue_me +sysTid=14368 state=S futex_wait_queue_me +sysTid=14369 state=S futex_wait_queue_me +sysTid=14380 state=S futex_wait_queue_me +sysTid=14400 state=S futex_wait_queue_me +sysTid=14414 state=S futex_wait_queue_me +sysTid=14423 state=S futex_wait_queue_me +sysTid=14431 state=S futex_wait_queue_me +sysTid=14439 state=S futex_wait_queue_me +sysTid=14442 state=S futex_wait_queue_me +sysTid=14451 state=S futex_wait_queue_me +sysTid=14453 state=S futex_wait_queue_me +sysTid=14454 state=S futex_wait_queue_me +sysTid=14456 state=S futex_wait_queue_me +sysTid=14457 state=S futex_wait_queue_me +sysTid=14459 state=S futex_wait_queue_me +sysTid=14460 state=S futex_wait_queue_me +sysTid=14461 state=S futex_wait_queue_me +sysTid=14462 state=S futex_wait_queue_me +sysTid=14465 state=S futex_wait_queue_me +sysTid=14466 state=S futex_wait_queue_me +sysTid=14467 state=S futex_wait_queue_me +sysTid=14473 state=S futex_wait_queue_me +sysTid=14485 state=S futex_wait_queue_me +sysTid=14491 state=S futex_wait_queue_me +sysTid=14493 state=S futex_wait_queue_me +sysTid=14500 state=S futex_wait_queue_me +sysTid=14514 state=S futex_wait_queue_me +sysTid=14522 state=S futex_wait_queue_me +sysTid=14529 state=S futex_wait_queue_me +sysTid=14531 state=S futex_wait_queue_me +sysTid=14538 state=S futex_wait_queue_me +sysTid=14542 state=S futex_wait_queue_me +sysTid=14550 state=S futex_wait_queue_me +sysTid=14551 state=S futex_wait_queue_me +sysTid=14552 state=S futex_wait_queue_me +sysTid=14554 state=S futex_wait_queue_me +sysTid=14555 state=S futex_wait_queue_me +sysTid=14556 state=S futex_wait_queue_me +sysTid=14557 state=S futex_wait_queue_me +sysTid=14558 state=S futex_wait_queue_me +sysTid=14559 state=S futex_wait_queue_me +sysTid=14560 state=S futex_wait_queue_me +sysTid=14561 state=S futex_wait_queue_me +sysTid=14562 state=S futex_wait_queue_me +sysTid=14563 state=S futex_wait_queue_me +sysTid=14564 state=S futex_wait_queue_me +sysTid=14565 state=S futex_wait_queue_me +sysTid=14566 state=S futex_wait_queue_me +sysTid=14567 state=S futex_wait_queue_me +sysTid=14568 state=S futex_wait_queue_me +sysTid=14570 state=S futex_wait_queue_me +sysTid=14573 state=S futex_wait_queue_me +sysTid=14580 state=S futex_wait_queue_me +sysTid=14585 state=S futex_wait_queue_me +sysTid=14594 state=S futex_wait_queue_me +sysTid=14606 state=S futex_wait_queue_me +sysTid=14608 state=S futex_wait_queue_me +sysTid=14622 state=S futex_wait_queue_me +sysTid=14646 state=S futex_wait_queue_me +sysTid=14660 state=S futex_wait_queue_me +sysTid=14664 state=S futex_wait_queue_me +sysTid=14673 state=S futex_wait_queue_me +sysTid=14676 state=S futex_wait_queue_me +sysTid=14691 state=S futex_wait_queue_me +sysTid=14694 state=S futex_wait_queue_me +sysTid=14695 state=S futex_wait_queue_me +sysTid=14696 state=S futex_wait_queue_me +sysTid=14697 state=S futex_wait_queue_me +sysTid=14698 state=S futex_wait_queue_me +sysTid=14699 state=S futex_wait_queue_me +sysTid=14700 state=S futex_wait_queue_me +sysTid=14701 state=S futex_wait_queue_me +sysTid=14702 state=S futex_wait_queue_me +sysTid=14703 state=S futex_wait_queue_me +sysTid=14704 state=S futex_wait_queue_me +sysTid=14705 state=S futex_wait_queue_me +sysTid=14706 state=S futex_wait_queue_me +sysTid=14707 state=S futex_wait_queue_me +sysTid=14708 state=S futex_wait_queue_me +sysTid=14709 state=S futex_wait_queue_me +sysTid=14710 state=S futex_wait_queue_me +sysTid=14711 state=S futex_wait_queue_me +sysTid=14712 state=S futex_wait_queue_me +sysTid=14713 state=S futex_wait_queue_me +sysTid=14714 state=S futex_wait_queue_me +sysTid=14715 state=S futex_wait_queue_me +sysTid=14716 state=S futex_wait_queue_me +sysTid=14717 state=S futex_wait_queue_me +sysTid=14718 state=S futex_wait_queue_me +sysTid=14719 state=S futex_wait_queue_me +sysTid=14720 state=S futex_wait_queue_me +sysTid=14721 state=S futex_wait_queue_me +sysTid=14722 state=S futex_wait_queue_me +sysTid=14723 state=S futex_wait_queue_me +sysTid=14724 state=S futex_wait_queue_me +sysTid=14725 state=S futex_wait_queue_me +sysTid=14726 state=S futex_wait_queue_me +sysTid=14727 state=S futex_wait_queue_me +sysTid=14728 state=S futex_wait_queue_me +sysTid=14731 state=S futex_wait_queue_me +sysTid=14737 state=S futex_wait_queue_me +sysTid=14744 state=S futex_wait_queue_me +sysTid=14749 state=S futex_wait_queue_me +sysTid=14756 state=S futex_wait_queue_me +sysTid=14764 state=S futex_wait_queue_me +sysTid=14766 state=S futex_wait_queue_me +sysTid=14770 state=S futex_wait_queue_me +sysTid=14780 state=S futex_wait_queue_me +sysTid=14783 state=S futex_wait_queue_me +sysTid=14787 state=S futex_wait_queue_me +sysTid=14794 state=S futex_wait_queue_me +sysTid=14799 state=S futex_wait_queue_me +sysTid=14807 state=S futex_wait_queue_me +sysTid=14813 state=S futex_wait_queue_me +sysTid=14817 state=S futex_wait_queue_me +sysTid=14818 state=S futex_wait_queue_me +sysTid=14819 state=S futex_wait_queue_me +sysTid=14820 state=S futex_wait_queue_me +sysTid=14824 state=S futex_wait_queue_me +sysTid=14825 state=S futex_wait_queue_me +sysTid=14826 state=S futex_wait_queue_me +sysTid=14827 state=S futex_wait_queue_me +sysTid=14828 state=S futex_wait_queue_me +sysTid=14829 state=S futex_wait_queue_me +sysTid=14830 state=S futex_wait_queue_me +sysTid=14835 state=S futex_wait_queue_me +sysTid=14842 state=S futex_wait_queue_me +sysTid=14852 state=S futex_wait_queue_me +sysTid=14854 state=S futex_wait_queue_me +sysTid=14862 state=S futex_wait_queue_me +sysTid=14868 state=S futex_wait_queue_me +sysTid=14869 state=S futex_wait_queue_me +sysTid=14870 state=S futex_wait_queue_me +sysTid=14871 state=S futex_wait_queue_me +sysTid=14872 state=S futex_wait_queue_me +sysTid=14873 state=S futex_wait_queue_me +sysTid=14874 state=S futex_wait_queue_me +sysTid=14875 state=S futex_wait_queue_me +sysTid=14876 state=S futex_wait_queue_me +sysTid=14877 state=S futex_wait_queue_me +sysTid=14878 state=S futex_wait_queue_me +sysTid=14879 state=S futex_wait_queue_me +sysTid=14880 state=S futex_wait_queue_me +sysTid=14881 state=S futex_wait_queue_me +sysTid=14882 state=S futex_wait_queue_me +sysTid=14883 state=S futex_wait_queue_me +sysTid=14884 state=S futex_wait_queue_me +sysTid=14885 state=S futex_wait_queue_me +sysTid=14887 state=S futex_wait_queue_me +sysTid=14888 state=S futex_wait_queue_me +sysTid=14889 state=S futex_wait_queue_me +sysTid=14890 state=S futex_wait_queue_me +sysTid=14891 state=S futex_wait_queue_me +sysTid=14892 state=S futex_wait_queue_me +sysTid=14893 state=S futex_wait_queue_me +sysTid=14897 state=S futex_wait_queue_me +sysTid=14903 state=S futex_wait_queue_me +sysTid=14911 state=S futex_wait_queue_me +sysTid=14915 state=S futex_wait_queue_me +sysTid=14920 state=S futex_wait_queue_me +sysTid=14924 state=S futex_wait_queue_me +sysTid=14932 state=S futex_wait_queue_me +sysTid=14972 state=S futex_wait_queue_me +sysTid=14974 state=S futex_wait_queue_me +sysTid=15011 state=S futex_wait_queue_me +sysTid=15019 state=S futex_wait_queue_me +sysTid=15032 state=S futex_wait_queue_me +sysTid=15054 state=S futex_wait_queue_me +sysTid=15124 state=S futex_wait_queue_me +sysTid=15177 state=S futex_wait_queue_me +sysTid=15217 state=S futex_wait_queue_me +sysTid=15228 state=S futex_wait_queue_me +sysTid=15236 state=S futex_wait_queue_me +sysTid=15248 state=S futex_wait_queue_me +sysTid=15265 state=S futex_wait_queue_me +sysTid=15272 state=S futex_wait_queue_me +sysTid=15276 state=S futex_wait_queue_me +sysTid=15344 state=S sk_wait_data +sysTid=15400 state=S sk_wait_data +sysTid=15415 state=S sk_wait_data +sysTid=15421 state=S sk_wait_data +sysTid=15449 state=S sk_wait_data +sysTid=15463 state=S sk_wait_data +sysTid=15471 state=S sk_wait_data +sysTid=15479 state=S sk_wait_data +sysTid=15486 state=S sk_wait_data +sysTid=15509 state=S sk_wait_data +sysTid=15515 state=S sk_wait_data +sysTid=15525 state=S sk_wait_data +sysTid=15530 state=S sk_wait_data +sysTid=15536 state=S sk_wait_data +sysTid=15541 state=S sk_wait_data +sysTid=15578 state=S futex_wait_queue_me +sysTid=16256 state=S futex_wait_queue_me +sysTid=16261 state=S futex_wait_queue_me +sysTid=16262 state=S futex_wait_queue_me + +----- end 12233 ----- + +libdebuggerd_client: unexpected registration response: 0 + +----- Waiting Channels: pid 12233 at 2024-11-13 19:48:10.010218499+0530 ----- +Cmd line: com.example.app:gameProcess + +sysTid=12233 state=R 0 +sysTid=12236 state=D swap_readpage +sysTid=12237 state=S futex_wait_queue_me +sysTid=12238 state=S futex_wait_queue_me +sysTid=12239 state=S futex_wait_queue_me +sysTid=12240 state=S futex_wait_queue_me +sysTid=12241 state=S futex_wait_queue_me +sysTid=12242 state=S binder_wait_for_work +sysTid=12243 state=S binder_wait_for_work +sysTid=12245 state=S binder_wait_for_work +sysTid=12252 state=S futex_wait_queue_me +sysTid=12254 state=S inotify_read +sysTid=12257 state=S __arm64_sys_nanosleep +sysTid=12259 state=R 0 +sysTid=12260 state=S __arm64_sys_nanosleep +sysTid=12268 state=S __arm64_sys_nanosleep +sysTid=12269 state=S __arm64_sys_nanosleep +sysTid=12270 state=S __arm64_sys_nanosleep +sysTid=12278 state=S futex_wait_queue_me +sysTid=12279 state=S futex_wait_queue_me +sysTid=12280 state=S futex_wait_queue_me +sysTid=12283 state=S futex_wait_queue_me +sysTid=12287 state=S futex_wait_queue_me +sysTid=12290 state=S futex_wait_queue_me +sysTid=12291 state=S futex_wait_queue_me +sysTid=12292 state=S futex_wait_queue_me +sysTid=12295 state=S futex_wait_queue_me +sysTid=12296 state=S futex_wait_queue_me +sysTid=12297 state=S futex_wait_queue_me +sysTid=12298 state=S futex_wait_queue_me +sysTid=12304 state=S futex_wait_queue_me +sysTid=12310 state=S futex_wait_queue_me +sysTid=12311 state=S futex_wait_queue_me +sysTid=12312 state=S do_epoll_wait +sysTid=12314 state=S futex_wait_queue_me +sysTid=12315 state=S futex_wait_queue_me +sysTid=12316 state=S futex_wait_queue_me +sysTid=12317 state=S futex_wait_queue_me +sysTid=12319 state=S futex_wait_queue_me +sysTid=12320 state=S do_epoll_wait +sysTid=12321 state=S futex_wait_queue_me +sysTid=12323 state=S futex_wait_queue_me +sysTid=12324 state=S futex_wait_queue_me +sysTid=12325 state=S futex_wait_queue_me +sysTid=12328 state=S do_epoll_wait +sysTid=12341 state=S futex_wait_queue_me +sysTid=12343 state=S futex_wait_queue_me +sysTid=12344 state=S futex_wait_queue_me +sysTid=12349 state=S futex_wait_queue_me +sysTid=12350 state=S futex_wait_queue_me +sysTid=12351 state=S futex_wait_queue_me +sysTid=12352 state=S futex_wait_queue_me +sysTid=12353 state=S binder_wait_for_work +sysTid=12354 state=S futex_wait_queue_me +sysTid=12355 state=S futex_wait_queue_me +sysTid=12358 state=R 0 +sysTid=12362 state=S futex_wait_queue_me +sysTid=12363 state=S futex_wait_queue_me +sysTid=12365 state=S do_epoll_wait +sysTid=12366 state=S futex_wait_queue_me +sysTid=12367 state=S futex_wait_queue_me +sysTid=12368 state=S futex_wait_queue_me +sysTid=12370 state=S futex_wait_queue_me +sysTid=12371 state=S futex_wait_queue_me +sysTid=12373 state=S futex_wait_queue_me +sysTid=12384 state=S binder_wait_for_work +sysTid=12391 state=S futex_wait_queue_me +sysTid=12399 state=S do_epoll_wait +sysTid=12401 state=S futex_wait_queue_me +sysTid=12402 state=S futex_wait_queue_me +sysTid=12404 state=S futex_wait_queue_me +sysTid=12405 state=S do_epoll_wait +sysTid=12407 state=S futex_wait_queue_me +sysTid=12408 state=S futex_wait_queue_me +sysTid=12409 state=S do_wait +sysTid=12410 state=S futex_wait_queue_me +sysTid=12412 state=S do_epoll_wait +sysTid=12435 state=S do_epoll_wait +sysTid=12468 state=S futex_wait_queue_me +sysTid=12514 state=S futex_wait_queue_me +sysTid=12550 state=S futex_wait_queue_me +sysTid=12561 state=S binder_wait_for_work +sysTid=12567 state=S binder_wait_for_work +sysTid=12580 state=S futex_wait_queue_me +sysTid=12619 state=S futex_wait_queue_me +sysTid=12627 state=S futex_wait_queue_me +sysTid=12644 state=S futex_wait_queue_me +sysTid=12887 state=S futex_wait_queue_me +sysTid=13430 state=S futex_wait_queue_me +sysTid=13438 state=S futex_wait_queue_me +sysTid=13443 state=S futex_wait_queue_me +sysTid=13454 state=S futex_wait_queue_me +sysTid=13455 state=S futex_wait_queue_me +sysTid=13564 state=S binder_wait_for_work +sysTid=13576 state=S binder_wait_for_work +sysTid=13579 state=S binder_wait_for_work +sysTid=13616 state=S binder_wait_for_work +sysTid=13624 state=S futex_wait_queue_me +sysTid=13706 state=S futex_wait_queue_me +sysTid=13722 state=S futex_wait_queue_me +sysTid=13724 state=S futex_wait_queue_me +sysTid=13730 state=S futex_wait_queue_me +sysTid=13740 state=S futex_wait_queue_me +sysTid=13744 state=S futex_wait_queue_me +sysTid=13745 state=S futex_wait_queue_me +sysTid=13748 state=S futex_wait_queue_me +sysTid=13754 state=S futex_wait_queue_me +sysTid=13756 state=S futex_wait_queue_me +sysTid=13757 state=S futex_wait_queue_me +sysTid=13758 state=S futex_wait_queue_me +sysTid=13759 state=S futex_wait_queue_me +sysTid=13763 state=S futex_wait_queue_me +sysTid=13767 state=S futex_wait_queue_me +sysTid=13768 state=S futex_wait_queue_me +sysTid=13769 state=S futex_wait_queue_me +sysTid=13773 state=S futex_wait_queue_me +sysTid=13776 state=S futex_wait_queue_me +sysTid=13781 state=S futex_wait_queue_me +sysTid=13782 state=S futex_wait_queue_me +sysTid=13783 state=S futex_wait_queue_me +sysTid=13784 state=S futex_wait_queue_me +sysTid=13786 state=S futex_wait_queue_me +sysTid=13791 state=S futex_wait_queue_me +sysTid=13792 state=S futex_wait_queue_me +sysTid=13793 state=S futex_wait_queue_me +sysTid=13794 state=S futex_wait_queue_me +sysTid=13795 state=S futex_wait_queue_me +sysTid=13796 state=S futex_wait_queue_me +sysTid=13797 state=S futex_wait_queue_me +sysTid=13798 state=S futex_wait_queue_me +sysTid=13799 state=S futex_wait_queue_me +sysTid=13800 state=S futex_wait_queue_me +sysTid=13806 state=S futex_wait_queue_me +sysTid=13809 state=S futex_wait_queue_me +sysTid=13814 state=S futex_wait_queue_me +sysTid=13815 state=S futex_wait_queue_me +sysTid=13816 state=S futex_wait_queue_me +sysTid=13817 state=S futex_wait_queue_me +sysTid=13818 state=S futex_wait_queue_me +sysTid=13820 state=S futex_wait_queue_me +sysTid=13825 state=S futex_wait_queue_me +sysTid=13830 state=S futex_wait_queue_me +sysTid=13831 state=S futex_wait_queue_me +sysTid=13832 state=S futex_wait_queue_me +sysTid=13833 state=S futex_wait_queue_me +sysTid=13834 state=S futex_wait_queue_me +sysTid=13835 state=S futex_wait_queue_me +sysTid=13836 state=S futex_wait_queue_me +sysTid=13841 state=S futex_wait_queue_me +sysTid=13847 state=S futex_wait_queue_me +sysTid=13848 state=S futex_wait_queue_me +sysTid=13849 state=S futex_wait_queue_me +sysTid=13850 state=S futex_wait_queue_me +sysTid=13851 state=S futex_wait_queue_me +sysTid=13852 state=S futex_wait_queue_me +sysTid=13853 state=S futex_wait_queue_me +sysTid=13854 state=S futex_wait_queue_me +sysTid=13857 state=S futex_wait_queue_me +sysTid=13863 state=S futex_wait_queue_me +sysTid=13867 state=S futex_wait_queue_me +sysTid=13880 state=S futex_wait_queue_me +sysTid=13920 state=S futex_wait_queue_me +sysTid=13949 state=S futex_wait_queue_me +sysTid=13953 state=S futex_wait_queue_me +sysTid=13954 state=S futex_wait_queue_me +sysTid=13955 state=S futex_wait_queue_me +sysTid=13958 state=S futex_wait_queue_me +sysTid=13959 state=S futex_wait_queue_me +sysTid=13967 state=S futex_wait_queue_me +sysTid=13980 state=S futex_wait_queue_me +sysTid=13981 state=S futex_wait_queue_me +sysTid=13982 state=S futex_wait_queue_me +sysTid=13983 state=S futex_wait_queue_me +sysTid=13984 state=S futex_wait_queue_me +sysTid=13986 state=S futex_wait_queue_me +sysTid=13987 state=S futex_wait_queue_me +sysTid=13991 state=S futex_wait_queue_me +sysTid=13998 state=S futex_wait_queue_me +sysTid=13999 state=S futex_wait_queue_me +sysTid=14000 state=S futex_wait_queue_me +sysTid=14001 state=S futex_wait_queue_me +sysTid=14002 state=S futex_wait_queue_me +sysTid=14003 state=S futex_wait_queue_me +sysTid=14004 state=S futex_wait_queue_me +sysTid=14005 state=S futex_wait_queue_me +sysTid=14006 state=S futex_wait_queue_me +sysTid=14007 state=S futex_wait_queue_me +sysTid=14026 state=S futex_wait_queue_me +sysTid=14052 state=S futex_wait_queue_me +sysTid=14057 state=S futex_wait_queue_me +sysTid=14060 state=S futex_wait_queue_me +sysTid=14063 state=S futex_wait_queue_me +sysTid=14069 state=S futex_wait_queue_me +sysTid=14072 state=S futex_wait_queue_me +sysTid=14075 state=S futex_wait_queue_me +sysTid=14081 state=S futex_wait_queue_me +sysTid=14084 state=S futex_wait_queue_me +sysTid=14089 state=S futex_wait_queue_me +sysTid=14090 state=S futex_wait_queue_me +sysTid=14091 state=S futex_wait_queue_me +sysTid=14092 state=S futex_wait_queue_me +sysTid=14093 state=S futex_wait_queue_me +sysTid=14094 state=S futex_wait_queue_me +sysTid=14095 state=S futex_wait_queue_me +sysTid=14096 state=S futex_wait_queue_me +sysTid=14097 state=S futex_wait_queue_me +sysTid=14098 state=S futex_wait_queue_me +sysTid=14099 state=S futex_wait_queue_me +sysTid=14100 state=S futex_wait_queue_me +sysTid=14101 state=S futex_wait_queue_me +sysTid=14102 state=S futex_wait_queue_me +sysTid=14103 state=S futex_wait_queue_me +sysTid=14104 state=S futex_wait_queue_me +sysTid=14106 state=S futex_wait_queue_me +sysTid=14111 state=S futex_wait_queue_me +sysTid=14117 state=S futex_wait_queue_me +sysTid=14120 state=S futex_wait_queue_me +sysTid=14121 state=S futex_wait_queue_me +sysTid=14122 state=S futex_wait_queue_me +sysTid=14123 state=S futex_wait_queue_me +sysTid=14124 state=S futex_wait_queue_me +sysTid=14129 state=S futex_wait_queue_me +sysTid=14130 state=S futex_wait_queue_me +sysTid=14131 state=S futex_wait_queue_me +sysTid=14132 state=S futex_wait_queue_me +sysTid=14136 state=S futex_wait_queue_me +sysTid=14144 state=S futex_wait_queue_me +sysTid=14148 state=S futex_wait_queue_me +sysTid=14154 state=S futex_wait_queue_me +sysTid=14158 state=S futex_wait_queue_me +sysTid=14164 state=S futex_wait_queue_me +sysTid=14167 state=S futex_wait_queue_me +sysTid=14168 state=S futex_wait_queue_me +sysTid=14169 state=S futex_wait_queue_me +sysTid=14170 state=S futex_wait_queue_me +sysTid=14171 state=S futex_wait_queue_me +sysTid=14172 state=S futex_wait_queue_me +sysTid=14173 state=S futex_wait_queue_me +sysTid=14174 state=S futex_wait_queue_me +sysTid=14175 state=S futex_wait_queue_me +sysTid=14176 state=S futex_wait_queue_me +sysTid=14177 state=S futex_wait_queue_me +sysTid=14178 state=S futex_wait_queue_me +sysTid=14179 state=S futex_wait_queue_me +sysTid=14180 state=S futex_wait_queue_me +sysTid=14181 state=S futex_wait_queue_me +sysTid=14182 state=S futex_wait_queue_me +sysTid=14190 state=S futex_wait_queue_me +sysTid=14195 state=S futex_wait_queue_me +sysTid=14198 state=S futex_wait_queue_me +sysTid=14207 state=S futex_wait_queue_me +sysTid=14209 state=S futex_wait_queue_me +sysTid=14210 state=S futex_wait_queue_me +sysTid=14214 state=S futex_wait_queue_me +sysTid=14220 state=S futex_wait_queue_me +sysTid=14223 state=S futex_wait_queue_me +sysTid=14227 state=S futex_wait_queue_me +sysTid=14235 state=S futex_wait_queue_me +sysTid=14242 state=S futex_wait_queue_me +sysTid=14243 state=S futex_wait_queue_me +sysTid=14244 state=S futex_wait_queue_me +sysTid=14245 state=S futex_wait_queue_me +sysTid=14246 state=S futex_wait_queue_me +sysTid=14247 state=S futex_wait_queue_me +sysTid=14248 state=S futex_wait_queue_me +sysTid=14249 state=S futex_wait_queue_me +sysTid=14250 state=S futex_wait_queue_me +sysTid=14251 state=S futex_wait_queue_me +sysTid=14253 state=S futex_wait_queue_me +sysTid=14259 state=S futex_wait_queue_me +sysTid=14264 state=S futex_wait_queue_me +sysTid=14269 state=S futex_wait_queue_me +sysTid=14272 state=S futex_wait_queue_me +sysTid=14277 state=S futex_wait_queue_me +sysTid=14282 state=S futex_wait_queue_me +sysTid=14296 state=S futex_wait_queue_me +sysTid=14302 state=S futex_wait_queue_me +sysTid=14309 state=S futex_wait_queue_me +sysTid=14314 state=S futex_wait_queue_me +sysTid=14319 state=S futex_wait_queue_me +sysTid=14324 state=S futex_wait_queue_me +sysTid=14325 state=S futex_wait_queue_me +sysTid=14327 state=S futex_wait_queue_me +sysTid=14328 state=S futex_wait_queue_me +sysTid=14329 state=S futex_wait_queue_me +sysTid=14331 state=S futex_wait_queue_me +sysTid=14348 state=S futex_wait_queue_me +sysTid=14349 state=S futex_wait_queue_me +sysTid=14350 state=S futex_wait_queue_me +sysTid=14351 state=S futex_wait_queue_me +sysTid=14352 state=S futex_wait_queue_me +sysTid=14353 state=S futex_wait_queue_me +sysTid=14357 state=S futex_wait_queue_me +sysTid=14358 state=S futex_wait_queue_me +sysTid=14359 state=S futex_wait_queue_me +sysTid=14360 state=S futex_wait_queue_me +sysTid=14361 state=S futex_wait_queue_me +sysTid=14363 state=S futex_wait_queue_me +sysTid=14364 state=S futex_wait_queue_me +sysTid=14365 state=S futex_wait_queue_me +sysTid=14366 state=S futex_wait_queue_me +sysTid=14367 state=S futex_wait_queue_me +sysTid=14368 state=S futex_wait_queue_me +sysTid=14369 state=S futex_wait_queue_me +sysTid=14380 state=S futex_wait_queue_me +sysTid=14400 state=S futex_wait_queue_me +sysTid=14414 state=S futex_wait_queue_me +sysTid=14423 state=S futex_wait_queue_me +sysTid=14431 state=S futex_wait_queue_me +sysTid=14439 state=S futex_wait_queue_me +sysTid=14442 state=S futex_wait_queue_me +sysTid=14451 state=S futex_wait_queue_me +sysTid=14453 state=S futex_wait_queue_me +sysTid=14454 state=S futex_wait_queue_me +sysTid=14456 state=S futex_wait_queue_me +sysTid=14457 state=S futex_wait_queue_me +sysTid=14459 state=S futex_wait_queue_me +sysTid=14460 state=S futex_wait_queue_me +sysTid=14461 state=S futex_wait_queue_me +sysTid=14462 state=S futex_wait_queue_me +sysTid=14465 state=S futex_wait_queue_me +sysTid=14466 state=S futex_wait_queue_me +sysTid=14467 state=S futex_wait_queue_me +sysTid=14473 state=S futex_wait_queue_me +sysTid=14485 state=S futex_wait_queue_me +sysTid=14491 state=S futex_wait_queue_me +sysTid=14493 state=S futex_wait_queue_me +sysTid=14500 state=S futex_wait_queue_me +sysTid=14514 state=S futex_wait_queue_me +sysTid=14522 state=S futex_wait_queue_me +sysTid=14529 state=S futex_wait_queue_me +sysTid=14531 state=S futex_wait_queue_me +sysTid=14538 state=S futex_wait_queue_me +sysTid=14542 state=S futex_wait_queue_me +sysTid=14550 state=S futex_wait_queue_me +sysTid=14551 state=S futex_wait_queue_me +sysTid=14552 state=S futex_wait_queue_me +sysTid=14554 state=S futex_wait_queue_me +sysTid=14555 state=S futex_wait_queue_me +sysTid=14556 state=S futex_wait_queue_me +sysTid=14557 state=S futex_wait_queue_me +sysTid=14558 state=S futex_wait_queue_me +sysTid=14559 state=S futex_wait_queue_me +sysTid=14560 state=S futex_wait_queue_me +sysTid=14561 state=S futex_wait_queue_me +sysTid=14562 state=S futex_wait_queue_me +sysTid=14563 state=S futex_wait_queue_me +sysTid=14564 state=S futex_wait_queue_me +sysTid=14565 state=S futex_wait_queue_me +sysTid=14566 state=S futex_wait_queue_me +sysTid=14567 state=S futex_wait_queue_me +sysTid=14568 state=S futex_wait_queue_me +sysTid=14570 state=S futex_wait_queue_me +sysTid=14573 state=S futex_wait_queue_me +sysTid=14580 state=S futex_wait_queue_me +sysTid=14585 state=S futex_wait_queue_me +sysTid=14594 state=S futex_wait_queue_me +sysTid=14606 state=S futex_wait_queue_me +sysTid=14608 state=S futex_wait_queue_me +sysTid=14622 state=S futex_wait_queue_me +sysTid=14646 state=S futex_wait_queue_me +sysTid=14660 state=S futex_wait_queue_me +sysTid=14664 state=S futex_wait_queue_me +sysTid=14673 state=S futex_wait_queue_me +sysTid=14676 state=S futex_wait_queue_me +sysTid=14691 state=S futex_wait_queue_me +sysTid=14694 state=S futex_wait_queue_me +sysTid=14695 state=S futex_wait_queue_me +sysTid=14696 state=S futex_wait_queue_me +sysTid=14697 state=S futex_wait_queue_me +sysTid=14698 state=S futex_wait_queue_me +sysTid=14699 state=S futex_wait_queue_me +sysTid=14700 state=S futex_wait_queue_me +sysTid=14701 state=S futex_wait_queue_me +sysTid=14702 state=S futex_wait_queue_me +sysTid=14703 state=S futex_wait_queue_me +sysTid=14704 state=S futex_wait_queue_me +sysTid=14705 state=S futex_wait_queue_me +sysTid=14706 state=S futex_wait_queue_me +sysTid=14707 state=S futex_wait_queue_me +sysTid=14708 state=S futex_wait_queue_me +sysTid=14709 state=S futex_wait_queue_me +sysTid=14710 state=S futex_wait_queue_me +sysTid=14711 state=S futex_wait_queue_me +sysTid=14712 state=S futex_wait_queue_me +sysTid=14713 state=S futex_wait_queue_me +sysTid=14714 state=S futex_wait_queue_me +sysTid=14715 state=S futex_wait_queue_me +sysTid=14716 state=S futex_wait_queue_me +sysTid=14717 state=S futex_wait_queue_me +sysTid=14718 state=S futex_wait_queue_me +sysTid=14719 state=S futex_wait_queue_me +sysTid=14720 state=S futex_wait_queue_me +sysTid=14721 state=S futex_wait_queue_me +sysTid=14722 state=S futex_wait_queue_me +sysTid=14723 state=S futex_wait_queue_me +sysTid=14724 state=S futex_wait_queue_me +sysTid=14725 state=S futex_wait_queue_me +sysTid=14726 state=S futex_wait_queue_me +sysTid=14727 state=S futex_wait_queue_me +sysTid=14728 state=S futex_wait_queue_me +sysTid=14731 state=S futex_wait_queue_me +sysTid=14737 state=S futex_wait_queue_me +sysTid=14744 state=S futex_wait_queue_me +sysTid=14749 state=S futex_wait_queue_me +sysTid=14756 state=S futex_wait_queue_me +sysTid=14764 state=S futex_wait_queue_me +sysTid=14766 state=S futex_wait_queue_me +sysTid=14770 state=S futex_wait_queue_me +sysTid=14780 state=S futex_wait_queue_me +sysTid=14783 state=S futex_wait_queue_me +sysTid=14787 state=S futex_wait_queue_me +sysTid=14794 state=S futex_wait_queue_me +sysTid=14799 state=S futex_wait_queue_me +sysTid=14807 state=S futex_wait_queue_me +sysTid=14813 state=S futex_wait_queue_me +sysTid=14817 state=S futex_wait_queue_me +sysTid=14818 state=S futex_wait_queue_me +sysTid=14819 state=S futex_wait_queue_me +sysTid=14820 state=S futex_wait_queue_me +sysTid=14824 state=S futex_wait_queue_me +sysTid=14825 state=S futex_wait_queue_me +sysTid=14826 state=S futex_wait_queue_me +sysTid=14827 state=S futex_wait_queue_me +sysTid=14828 state=S futex_wait_queue_me +sysTid=14829 state=S futex_wait_queue_me +sysTid=14830 state=S futex_wait_queue_me +sysTid=14835 state=S futex_wait_queue_me +sysTid=14842 state=S futex_wait_queue_me +sysTid=14852 state=S futex_wait_queue_me +sysTid=14854 state=S futex_wait_queue_me +sysTid=14862 state=S futex_wait_queue_me +sysTid=14868 state=S futex_wait_queue_me +sysTid=14869 state=S futex_wait_queue_me +sysTid=14870 state=S futex_wait_queue_me +sysTid=14871 state=S futex_wait_queue_me +sysTid=14872 state=S futex_wait_queue_me +sysTid=14873 state=S futex_wait_queue_me +sysTid=14874 state=S futex_wait_queue_me +sysTid=14875 state=S futex_wait_queue_me +sysTid=14876 state=S futex_wait_queue_me +sysTid=14877 state=S futex_wait_queue_me +sysTid=14878 state=S futex_wait_queue_me +sysTid=14879 state=S futex_wait_queue_me +sysTid=14880 state=S futex_wait_queue_me +sysTid=14881 state=S futex_wait_queue_me +sysTid=14882 state=S futex_wait_queue_me +sysTid=14883 state=S futex_wait_queue_me +sysTid=14884 state=S futex_wait_queue_me +sysTid=14885 state=S futex_wait_queue_me +sysTid=14887 state=S futex_wait_queue_me +sysTid=14888 state=S futex_wait_queue_me +sysTid=14889 state=S futex_wait_queue_me +sysTid=14890 state=S futex_wait_queue_me +sysTid=14891 state=S futex_wait_queue_me +sysTid=14892 state=S futex_wait_queue_me +sysTid=14893 state=S futex_wait_queue_me +sysTid=14897 state=S futex_wait_queue_me +sysTid=14903 state=S futex_wait_queue_me +sysTid=14911 state=S futex_wait_queue_me +sysTid=14915 state=S futex_wait_queue_me +sysTid=14920 state=S futex_wait_queue_me +sysTid=14924 state=S futex_wait_queue_me +sysTid=14932 state=S futex_wait_queue_me +sysTid=14972 state=S futex_wait_queue_me +sysTid=14974 state=S futex_wait_queue_me +sysTid=15011 state=S futex_wait_queue_me +sysTid=15019 state=S futex_wait_queue_me +sysTid=15032 state=S futex_wait_queue_me +sysTid=15054 state=S futex_wait_queue_me +sysTid=15124 state=S futex_wait_queue_me +sysTid=15177 state=S futex_wait_queue_me +sysTid=15217 state=S futex_wait_queue_me +sysTid=15228 state=S futex_wait_queue_me +sysTid=15236 state=S futex_wait_queue_me +sysTid=15248 state=S futex_wait_queue_me +sysTid=15265 state=S futex_wait_queue_me +sysTid=15272 state=S futex_wait_queue_me +sysTid=15276 state=S futex_wait_queue_me +sysTid=15344 state=S sk_wait_data +sysTid=15400 state=S sk_wait_data +sysTid=15415 state=S sk_wait_data +sysTid=15421 state=S sk_wait_data +sysTid=15449 state=S sk_wait_data +sysTid=15463 state=S sk_wait_data +sysTid=15471 state=S sk_wait_data +sysTid=15479 state=S sk_wait_data +sysTid=15486 state=S sk_wait_data +sysTid=15509 state=S sk_wait_data +sysTid=15515 state=S sk_wait_data +sysTid=15525 state=S sk_wait_data +sysTid=15530 state=S sk_wait_data +sysTid=15536 state=S sk_wait_data +sysTid=15541 state=S sk_wait_data +sysTid=15578 state=S futex_wait_queue_me +sysTid=16256 state=S futex_wait_queue_me +sysTid=16261 state=S futex_wait_queue_me +sysTid=16262 state=S futex_wait_queue_me + +----- end 12233 ----- diff --git a/sentry-android-ndk/build.gradle.kts b/sentry-android-ndk/build.gradle.kts index 41d6bbab136..339c1682146 100644 --- a/sentry-android-ndk/build.gradle.kts +++ b/sentry-android-ndk/build.gradle.kts @@ -63,6 +63,13 @@ android { ignore = true } } + + @Suppress("UnstableApiUsage") + packagingOptions { + jniLibs { + useLegacyPackaging = true + } + } } dependencies { diff --git a/sentry-android-replay/api/sentry-android-replay.api b/sentry-android-replay/api/sentry-android-replay.api index 888cf6a172e..906e848bb5d 100644 --- a/sentry-android-replay/api/sentry-android-replay.api +++ b/sentry-android-replay/api/sentry-android-replay.api @@ -57,7 +57,7 @@ public final class io/sentry/android/replay/ReplayCache$Companion { public final fun makeReplayCacheDir (Lio/sentry/SentryOptions;Lio/sentry/protocol/SentryId;)Ljava/io/File; } -public final class io/sentry/android/replay/ReplayIntegration : android/content/ComponentCallbacks, io/sentry/Integration, io/sentry/ReplayController, io/sentry/android/replay/ScreenshotRecorderCallback, io/sentry/android/replay/gestures/TouchRecorderCallback, java/io/Closeable { +public final class io/sentry/android/replay/ReplayIntegration : android/content/ComponentCallbacks, io/sentry/IConnectionStatusProvider$IConnectionStatusObserver, io/sentry/Integration, io/sentry/ReplayController, io/sentry/android/replay/ScreenshotRecorderCallback, io/sentry/android/replay/gestures/TouchRecorderCallback, io/sentry/transport/RateLimiter$IRateLimitObserver, java/io/Closeable { public static final field $stable I public fun (Landroid/content/Context;Lio/sentry/transport/ICurrentDateProvider;)V public fun (Landroid/content/Context;Lio/sentry/transport/ICurrentDateProvider;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;)V @@ -69,7 +69,9 @@ public final class io/sentry/android/replay/ReplayIntegration : android/content/ public fun getReplayId ()Lio/sentry/protocol/SentryId; public fun isRecording ()Z public fun onConfigurationChanged (Landroid/content/res/Configuration;)V + public fun onConnectionStatusChanged (Lio/sentry/IConnectionStatusProvider$ConnectionStatus;)V public fun onLowMemory ()V + public fun onRateLimitChanged (Lio/sentry/transport/RateLimiter;)V public fun onScreenshotRecorded (Landroid/graphics/Bitmap;)V public fun onScreenshotRecorded (Ljava/io/File;J)V public fun onTouchEvent (Landroid/view/MotionEvent;)V diff --git a/sentry-android-replay/src/main/java/io/sentry/android/replay/ReplayIntegration.kt b/sentry-android-replay/src/main/java/io/sentry/android/replay/ReplayIntegration.kt index 38483e3aac7..5663b80f630 100644 --- a/sentry-android-replay/src/main/java/io/sentry/android/replay/ReplayIntegration.kt +++ b/sentry-android-replay/src/main/java/io/sentry/android/replay/ReplayIntegration.kt @@ -7,6 +7,11 @@ import android.graphics.Bitmap import android.os.Build import android.view.MotionEvent import io.sentry.Breadcrumb +import io.sentry.DataCategory.All +import io.sentry.DataCategory.Replay +import io.sentry.IConnectionStatusProvider.ConnectionStatus +import io.sentry.IConnectionStatusProvider.ConnectionStatus.DISCONNECTED +import io.sentry.IConnectionStatusProvider.IConnectionStatusObserver import io.sentry.IScopes import io.sentry.Integration import io.sentry.NoOpReplayBreadcrumbConverter @@ -32,6 +37,8 @@ import io.sentry.cache.PersistingScopeObserver.REPLAY_FILENAME import io.sentry.hints.Backfillable import io.sentry.protocol.SentryId import io.sentry.transport.ICurrentDateProvider +import io.sentry.transport.RateLimiter +import io.sentry.transport.RateLimiter.IRateLimitObserver import io.sentry.util.FileUtils import io.sentry.util.HintUtils import io.sentry.util.IntegrationUtils.addIntegrationToSdkVersion @@ -48,7 +55,14 @@ public class ReplayIntegration( private val recorderProvider: (() -> Recorder)? = null, private val recorderConfigProvider: ((configChanged: Boolean) -> ScreenshotRecorderConfig)? = null, private val replayCacheProvider: ((replayId: SentryId, recorderConfig: ScreenshotRecorderConfig) -> ReplayCache)? = null -) : Integration, Closeable, ScreenshotRecorderCallback, TouchRecorderCallback, ReplayController, ComponentCallbacks { +) : Integration, + Closeable, + ScreenshotRecorderCallback, + TouchRecorderCallback, + ReplayController, + ComponentCallbacks, + IConnectionStatusObserver, + IRateLimitObserver { // needed for the Java's call site constructor(context: Context, dateProvider: ICurrentDateProvider) : this( @@ -113,6 +127,8 @@ public class ReplayIntegration( gestureRecorder = gestureRecorderProvider?.invoke() ?: GestureRecorder(options, this) isEnabled.set(true) + options.connectionStatusProvider.addConnectionStatusObserver(this) + scopes.rateLimiter?.addRateLimitObserver(this) try { context.registerComponentCallbacks(this) } catch (e: Throwable) { @@ -222,12 +238,14 @@ public class ReplayIntegration( scopes?.configureScope { screen = it.screen?.substringAfterLast('.') } captureStrategy?.onScreenshotRecorded(bitmap) { frameTimeStamp -> addFrame(bitmap, frameTimeStamp, screen) + checkCanRecord() } } override fun onScreenshotRecorded(screenshot: File, frameTimestamp: Long) { captureStrategy?.onScreenshotRecorded { _ -> addFrame(screenshot, frameTimestamp) + checkCanRecord() } } @@ -236,6 +254,8 @@ public class ReplayIntegration( return } + options.connectionStatusProvider.removeConnectionStatusObserver(this) + scopes?.rateLimiter?.removeRateLimitObserver(this) try { context.unregisterComponentCallbacks(this) } catch (ignored: Throwable) { @@ -259,12 +279,55 @@ public class ReplayIntegration( recorder?.start(recorderConfig) } + override fun onConnectionStatusChanged(status: ConnectionStatus) { + if (captureStrategy !is SessionCaptureStrategy) { + // we only want to stop recording when offline for session mode + return + } + + if (status == DISCONNECTED) { + pause() + } else { + // being positive for other states, even if it's NO_PERMISSION + resume() + } + } + + override fun onRateLimitChanged(rateLimiter: RateLimiter) { + if (captureStrategy !is SessionCaptureStrategy) { + // we only want to stop recording when rate-limited for session mode + return + } + + if (rateLimiter.isActiveForCategory(All) || rateLimiter.isActiveForCategory(Replay)) { + pause() + } else { + resume() + } + } + override fun onLowMemory() = Unit override fun onTouchEvent(event: MotionEvent) { captureStrategy?.onTouchEvent(event) } + /** + * Check if we're offline or rate-limited and pause for session mode to not overflow the + * envelope cache. + */ + private fun checkCanRecord() { + if (captureStrategy is SessionCaptureStrategy && + ( + options.connectionStatusProvider.connectionStatus == DISCONNECTED || + scopes?.rateLimiter?.isActiveForCategory(All) == true || + scopes?.rateLimiter?.isActiveForCategory(Replay) == true + ) + ) { + pause() + } + } + private fun registerRootViewListeners() { if (recorder is OnRootViewsChangedListener) { rootViewsSpy.listeners += (recorder as OnRootViewsChangedListener) diff --git a/sentry-android-replay/src/main/java/io/sentry/android/replay/ScreenshotRecorder.kt b/sentry-android-replay/src/main/java/io/sentry/android/replay/ScreenshotRecorder.kt index c7aded105a7..734be2c06a6 100644 --- a/sentry-android-replay/src/main/java/io/sentry/android/replay/ScreenshotRecorder.kt +++ b/sentry-android-replay/src/main/java/io/sentry/android/replay/ScreenshotRecorder.kt @@ -36,7 +36,6 @@ import java.lang.ref.WeakReference import java.util.concurrent.Executors import java.util.concurrent.ThreadFactory import java.util.concurrent.atomic.AtomicBoolean -import java.util.concurrent.atomic.AtomicReference import kotlin.LazyThreadSafetyMode.NONE import kotlin.math.roundToInt @@ -52,7 +51,6 @@ internal class ScreenshotRecorder( Executors.newSingleThreadScheduledExecutor(RecorderExecutorServiceThreadFactory()) } private var rootView: WeakReference? = null - private val pendingViewHierarchy = AtomicReference() private val maskingPaint by lazy(NONE) { Paint() } private val singlePixelBitmap: Bitmap by lazy(NONE) { Bitmap.createBitmap( @@ -231,7 +229,6 @@ internal class ScreenshotRecorder( unbind(rootView?.get()) rootView?.clear() lastScreenshot?.recycle() - pendingViewHierarchy.set(null) isCapturing.set(false) recorder.gracefullyShutdown(options) } diff --git a/sentry-android-replay/src/main/java/io/sentry/android/replay/capture/CaptureStrategy.kt b/sentry-android-replay/src/main/java/io/sentry/android/replay/capture/CaptureStrategy.kt index 4ad9f03386a..29169259539 100644 --- a/sentry-android-replay/src/main/java/io/sentry/android/replay/capture/CaptureStrategy.kt +++ b/sentry-android-replay/src/main/java/io/sentry/android/replay/capture/CaptureStrategy.kt @@ -57,6 +57,7 @@ internal interface CaptureStrategy { fun close() companion object { + private const val BREADCRUMB_START_OFFSET = 100L internal val currentEventsLock = AutoClosableReentrantLock() fun createSegment( @@ -162,7 +163,10 @@ internal interface CaptureStrategy { val urls = LinkedList() breadcrumbs.forEach { breadcrumb -> - if (breadcrumb.timestamp.time >= segmentTimestamp.time && + // we add some fixed breadcrumb offset to make sure we don't miss any + // breadcrumbs that might be relevant for the current segment, but just happened + // earlier than the current segment (e.g. network connectivity changed) + if ((breadcrumb.timestamp.time + BREADCRUMB_START_OFFSET) >= segmentTimestamp.time && breadcrumb.timestamp.time < endTimestamp.time ) { val rrwebEvent = options diff --git a/sentry-android-replay/src/main/java/io/sentry/android/replay/capture/SessionCaptureStrategy.kt b/sentry-android-replay/src/main/java/io/sentry/android/replay/capture/SessionCaptureStrategy.kt index 3109c55c5a6..16a4fd2249b 100644 --- a/sentry-android-replay/src/main/java/io/sentry/android/replay/capture/SessionCaptureStrategy.kt +++ b/sentry-android-replay/src/main/java/io/sentry/android/replay/capture/SessionCaptureStrategy.kt @@ -1,7 +1,6 @@ package io.sentry.android.replay.capture import android.graphics.Bitmap -import io.sentry.IConnectionStatusProvider.ConnectionStatus.DISCONNECTED import io.sentry.IScopes import io.sentry.SentryLevel.DEBUG import io.sentry.SentryLevel.INFO @@ -73,11 +72,6 @@ internal class SessionCaptureStrategy( } override fun onScreenshotRecorded(bitmap: Bitmap?, store: ReplayCache.(frameTimestamp: Long) -> Unit) { - if (options.connectionStatusProvider.connectionStatus == DISCONNECTED) { - options.logger.log(DEBUG, "Skipping screenshot recording, no internet connection") - bitmap?.recycle() - return - } // have to do it before submitting, otherwise if the queue is busy, the timestamp won't be // reflecting the exact time of when it was captured val frameTimestamp = dateProvider.currentTimeMillis diff --git a/sentry-android-replay/src/main/java/io/sentry/android/replay/viewhierarchy/ViewHierarchyNode.kt b/sentry-android-replay/src/main/java/io/sentry/android/replay/viewhierarchy/ViewHierarchyNode.kt index 03cb37ad3e6..03bda7cfc65 100644 --- a/sentry-android-replay/src/main/java/io/sentry/android/replay/viewhierarchy/ViewHierarchyNode.kt +++ b/sentry-android-replay/src/main/java/io/sentry/android/replay/viewhierarchy/ViewHierarchyNode.kt @@ -3,6 +3,7 @@ package io.sentry.android.replay.viewhierarchy import android.annotation.TargetApi import android.graphics.Rect import android.view.View +import android.view.ViewParent import android.widget.ImageView import android.widget.TextView import io.sentry.SentryOptions @@ -261,6 +262,13 @@ sealed class ViewHierarchyNode( return true } + if (!this.isMaskContainer(options) && + this.parent != null && + this.parent.isUnmaskContainer(options) + ) { + return false + } + if (this.javaClass.isAssignableFrom(options.experimental.sessionReplay.unmaskViewClasses)) { return false } @@ -268,6 +276,18 @@ sealed class ViewHierarchyNode( return this.javaClass.isAssignableFrom(options.experimental.sessionReplay.maskViewClasses) } + private fun ViewParent.isUnmaskContainer(options: SentryOptions): Boolean { + val unmaskContainer = + options.experimental.sessionReplay.unmaskViewContainerClass ?: return false + return this.javaClass.name == unmaskContainer + } + + private fun View.isMaskContainer(options: SentryOptions): Boolean { + val maskContainer = + options.experimental.sessionReplay.maskViewContainerClass ?: return false + return this.javaClass.name == maskContainer + } + fun fromView(view: View, parent: ViewHierarchyNode?, distance: Int, options: SentryOptions): ViewHierarchyNode { val (isVisible, visibleRect) = view.isVisibleToUser() val shouldMask = isVisible && view.shouldMask(options) diff --git a/sentry-android-replay/src/test/java/io/sentry/android/replay/ReplayIntegrationTest.kt b/sentry-android-replay/src/test/java/io/sentry/android/replay/ReplayIntegrationTest.kt index 4b10043e725..9b319b9c0ee 100644 --- a/sentry-android-replay/src/test/java/io/sentry/android/replay/ReplayIntegrationTest.kt +++ b/sentry-android-replay/src/test/java/io/sentry/android/replay/ReplayIntegrationTest.kt @@ -9,6 +9,8 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import io.sentry.Breadcrumb import io.sentry.DateUtils import io.sentry.Hint +import io.sentry.IConnectionStatusProvider.ConnectionStatus.CONNECTED +import io.sentry.IConnectionStatusProvider.ConnectionStatus.DISCONNECTED import io.sentry.IScopes import io.sentry.Scope import io.sentry.ScopeCallback @@ -26,6 +28,7 @@ import io.sentry.android.replay.ReplayCache.Companion.SEGMENT_KEY_REPLAY_TYPE import io.sentry.android.replay.ReplayCache.Companion.SEGMENT_KEY_TIMESTAMP import io.sentry.android.replay.ReplayCache.Companion.SEGMENT_KEY_WIDTH import io.sentry.android.replay.capture.CaptureStrategy +import io.sentry.android.replay.capture.SessionCaptureStrategy import io.sentry.android.replay.capture.SessionCaptureStrategyTest.Fixture.Companion.VIDEO_DURATION import io.sentry.android.replay.gestures.GestureRecorder import io.sentry.cache.PersistingScopeObserver @@ -38,6 +41,7 @@ import io.sentry.rrweb.RRWebMetaEvent import io.sentry.rrweb.RRWebVideoEvent import io.sentry.transport.CurrentDateProvider import io.sentry.transport.ICurrentDateProvider +import io.sentry.transport.RateLimiter import org.junit.Rule import org.junit.rules.TemporaryFolder import org.junit.runner.RunWith @@ -82,10 +86,12 @@ class ReplayIntegrationTest { } } val scope = Scope(options) + val rateLimiter = mock() val scopes = mock { doAnswer { ((it.arguments[0]) as ScopeCallback).run(scope) }.whenever(mock).configureScope(any()) + on { rateLimiter }.thenReturn(rateLimiter) } val replayCache = mock { @@ -98,6 +104,8 @@ class ReplayIntegrationTest { context: Context, sessionSampleRate: Double = 1.0, onErrorSampleRate: Double = 1.0, + isOffline: Boolean = false, + isRateLimited: Boolean = false, recorderProvider: (() -> Recorder)? = null, replayCaptureStrategyProvider: ((isFullSession: Boolean) -> CaptureStrategy)? = null, recorderConfigProvider: ((configChanged: Boolean) -> ScreenshotRecorderConfig)? = null, @@ -107,6 +115,12 @@ class ReplayIntegrationTest { options.run { experimental.sessionReplay.onErrorSampleRate = onErrorSampleRate experimental.sessionReplay.sessionSampleRate = sessionSampleRate + connectionStatusProvider = mock { + on { connectionStatus }.thenReturn(if (isOffline) DISCONNECTED else CONNECTED) + } + } + if (isRateLimited) { + whenever(rateLimiter.isActiveForCategory(any())).thenReturn(true) } return ReplayIntegration( context, @@ -567,4 +581,123 @@ class ReplayIntegrationTest { verify(fixture.replayCache).addFrame(any(), any(), eq("MainActivity")) } + + @Test + fun `onScreenshotRecorded pauses replay when offline for sessions`() { + val captureStrategy = getSessionCaptureStrategy(fixture.options) + val recorder = mock() + val replay = fixture.getSut( + context, + recorderProvider = { recorder }, + replayCaptureStrategyProvider = { captureStrategy }, + isOffline = true + ) + + replay.register(fixture.scopes, fixture.options) + replay.start() + replay.onScreenshotRecorded(mock()) + + verify(recorder).pause() + } + + @Test + fun `onScreenshotRecorded pauses replay when rate-limited for sessions`() { + val captureStrategy = getSessionCaptureStrategy(fixture.options) + val recorder = mock() + val replay = fixture.getSut( + context, + recorderProvider = { recorder }, + replayCaptureStrategyProvider = { captureStrategy }, + isRateLimited = true + ) + + replay.register(fixture.scopes, fixture.options) + replay.start() + replay.onScreenshotRecorded(mock()) + + verify(recorder).pause() + } + + @Test + fun `onConnectionStatusChanged pauses replay when offline for sessions`() { + val captureStrategy = getSessionCaptureStrategy(fixture.options) + val recorder = mock() + val replay = fixture.getSut( + context, + recorderProvider = { recorder }, + replayCaptureStrategyProvider = { captureStrategy } + ) + + replay.register(fixture.scopes, fixture.options) + replay.start() + replay.onConnectionStatusChanged(DISCONNECTED) + + verify(recorder).pause() + } + + @Test + fun `onConnectionStatusChanged resumes replay when back-online for sessions`() { + val captureStrategy = getSessionCaptureStrategy(fixture.options) + val recorder = mock() + val replay = fixture.getSut( + context, + recorderProvider = { recorder }, + replayCaptureStrategyProvider = { captureStrategy } + ) + + replay.register(fixture.scopes, fixture.options) + replay.start() + replay.onConnectionStatusChanged(CONNECTED) + + verify(recorder).resume() + } + + @Test + fun `onRateLimitChanged pauses replay when rate-limited for sessions`() { + val captureStrategy = getSessionCaptureStrategy(fixture.options) + val recorder = mock() + val replay = fixture.getSut( + context, + recorderProvider = { recorder }, + replayCaptureStrategyProvider = { captureStrategy }, + isRateLimited = true + ) + + replay.register(fixture.scopes, fixture.options) + replay.start() + replay.onRateLimitChanged(fixture.rateLimiter) + + verify(recorder).pause() + } + + @Test + fun `onRateLimitChanged resumes replay when rate-limit lifted for sessions`() { + val captureStrategy = getSessionCaptureStrategy(fixture.options) + val recorder = mock() + val replay = fixture.getSut( + context, + recorderProvider = { recorder }, + replayCaptureStrategyProvider = { captureStrategy }, + isRateLimited = false + ) + + replay.register(fixture.scopes, fixture.options) + replay.start() + replay.onRateLimitChanged(fixture.rateLimiter) + + verify(recorder).resume() + } + + private fun getSessionCaptureStrategy(options: SentryOptions): SessionCaptureStrategy { + return SessionCaptureStrategy( + options, + null, + CurrentDateProvider.getInstance(), + executor = mock { + doAnswer { + (it.arguments[0] as Runnable).run() + }.whenever(mock).submit(any()) + } + ) + } } diff --git a/sentry-android-replay/src/test/java/io/sentry/android/replay/viewhierarchy/ContainerMaskingOptionsTest.kt b/sentry-android-replay/src/test/java/io/sentry/android/replay/viewhierarchy/ContainerMaskingOptionsTest.kt new file mode 100644 index 00000000000..ff9a125d955 --- /dev/null +++ b/sentry-android-replay/src/test/java/io/sentry/android/replay/viewhierarchy/ContainerMaskingOptionsTest.kt @@ -0,0 +1,231 @@ +package io.sentry.android.replay.viewhierarchy + +import android.app.Activity +import android.content.Context +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.drawable.Drawable +import android.os.Bundle +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import android.widget.LinearLayout +import android.widget.LinearLayout.LayoutParams +import android.widget.TextView +import androidx.test.ext.junit.runners.AndroidJUnit4 +import io.sentry.SentryOptions +import io.sentry.android.replay.maskAllImages +import io.sentry.android.replay.maskAllText +import org.junit.runner.RunWith +import org.robolectric.Robolectric.buildActivity +import org.robolectric.annotation.Config +import kotlin.test.BeforeTest +import kotlin.test.Test +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +@RunWith(AndroidJUnit4::class) +@Config(sdk = [30]) +class ContainerMaskingOptionsTest { + + @BeforeTest + fun setup() { + System.setProperty("robolectric.areWindowsMarkedVisible", "true") + } + + @Test + fun `when maskAllText is set TextView in Unmask container is unmasked`() { + buildActivity(MaskingOptionsActivity::class.java).setup() + + val options = SentryOptions().apply { + experimental.sessionReplay.maskAllText = true + experimental.sessionReplay.setUnmaskViewContainerClass(CustomUnmask::class.java.name) + } + + val textNode = ViewHierarchyNode.fromView(MaskingOptionsActivity.textViewInUnmask!!, null, 0, options) + assertFalse(textNode.shouldMask) + } + + @Test + fun `when maskAllImages is set ImageView in Unmask container is unmasked`() { + buildActivity(MaskingOptionsActivity::class.java).setup() + + val options = SentryOptions().apply { + experimental.sessionReplay.maskAllImages = true + experimental.sessionReplay.setUnmaskViewContainerClass(CustomUnmask::class.java.name) + } + + val imageNode = ViewHierarchyNode.fromView(MaskingOptionsActivity.imageViewInUnmask!!, null, 0, options) + assertFalse(imageNode.shouldMask) + } + + @Test + fun `MaskContainer is always masked`() { + buildActivity(MaskingOptionsActivity::class.java).setup() + + val options = SentryOptions().apply { + experimental.sessionReplay.setMaskViewContainerClass(CustomMask::class.java.name) + } + + val maskContainer = ViewHierarchyNode.fromView(MaskingOptionsActivity.maskWithChildren!!, null, 0, options) + + assertTrue(maskContainer.shouldMask) + } + + @Test + fun `when Views are in UnmaskContainer only direct children are unmasked`() { + buildActivity(MaskingOptionsActivity::class.java).setup() + + val options = SentryOptions().apply { + experimental.sessionReplay.addMaskViewClass(CustomView::class.java.name) + experimental.sessionReplay.setUnmaskViewContainerClass(CustomUnmask::class.java.name) + } + + val maskContainer = ViewHierarchyNode.fromView(MaskingOptionsActivity.unmaskWithChildren!!, null, 0, options) + val firstChild = ViewHierarchyNode.fromView(MaskingOptionsActivity.customViewInUnmask!!, maskContainer, 0, options) + val secondLevelChild = ViewHierarchyNode.fromView(MaskingOptionsActivity.secondLayerChildInUnmask!!, firstChild, 0, options) + + assertFalse(maskContainer.shouldMask) + assertFalse(firstChild.shouldMask) + assertTrue(secondLevelChild.shouldMask) + } + + @Test + fun `when MaskContainer is direct child of UnmaskContainer all children od Mask are masked`() { + buildActivity(MaskingOptionsActivity::class.java).setup() + + val options = SentryOptions().apply { + experimental.sessionReplay.setMaskViewContainerClass(CustomMask::class.java.name) + experimental.sessionReplay.setUnmaskViewContainerClass(CustomUnmask::class.java.name) + } + + val unmaskNode = ViewHierarchyNode.fromView(MaskingOptionsActivity.unmaskWithMaskChild!!, null, 0, options) + val maskNode = ViewHierarchyNode.fromView(MaskingOptionsActivity.maskAsDirectChildOfUnmask!!, unmaskNode, 0, options) + + assertFalse(unmaskNode.shouldMask) + assertTrue(maskNode.shouldMask) + } + + private class CustomView(context: Context) : View(context) { + override fun onDraw(canvas: Canvas) { + super.onDraw(canvas) + canvas.drawColor(Color.BLACK) + } + } + + private open class CustomGroup(context: Context) : LinearLayout(context) { + init { + setBackgroundColor(android.R.color.white) + orientation = VERTICAL + layoutParams = LayoutParams(100, 100) + } + + override fun onDraw(canvas: Canvas) { + super.onDraw(canvas) + canvas.drawColor(Color.BLACK) + } + } + + private class CustomMask(context: Context) : CustomGroup(context) + private class CustomUnmask(context: Context) : CustomGroup(context) + + private class MaskingOptionsActivity : Activity() { + + companion object { + var unmaskWithTextView: ViewGroup? = null + var textViewInUnmask: TextView? = null + + var unmaskWithImageView: ViewGroup? = null + var imageViewInUnmask: ImageView? = null + + var unmaskWithChildren: ViewGroup? = null + var customViewInUnmask: ViewGroup? = null + var secondLayerChildInUnmask: View? = null + + var maskWithChildren: ViewGroup? = null + + var unmaskWithMaskChild: ViewGroup? = null + var maskAsDirectChildOfUnmask: ViewGroup? = null + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + val linearLayout = LinearLayout(this).apply { + setBackgroundColor(android.R.color.white) + orientation = LinearLayout.VERTICAL + layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT) + } + + val context = this + + linearLayout.addView( + CustomUnmask(context).apply { + unmaskWithTextView = this + this.addView( + TextView(context).apply { + textViewInUnmask = this + text = "Hello, World!" + layoutParams = LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT) + } + ) + } + ) + + linearLayout.addView( + CustomUnmask(context).apply { + unmaskWithImageView = this + this.addView( + ImageView(context).apply { + imageViewInUnmask = this + val image = this::class.java.classLoader.getResource("Tongariro.jpg")!! + setImageDrawable(Drawable.createFromPath(image.path)) + layoutParams = LayoutParams(50, 50).apply { + setMargins(0, 16, 0, 0) + } + } + ) + } + ) + + linearLayout.addView( + CustomUnmask(context).apply { + unmaskWithChildren = this + this.addView( + CustomGroup(context).apply { + customViewInUnmask = this + this.addView( + CustomView(context).apply { + secondLayerChildInUnmask = this + } + ) + } + ) + } + ) + + linearLayout.addView( + CustomMask(context).apply { + maskWithChildren = this + this.addView( + CustomGroup(context).apply { + this.addView(CustomView(context)) + } + ) + } + ) + + linearLayout.addView( + CustomUnmask(context).apply { + unmaskWithMaskChild = this + this.addView( + CustomMask(context).apply { + maskAsDirectChildOfUnmask = this + } + ) + } + ) + + setContentView(linearLayout) + } + } +} diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index ba1fee1b032..7cf213a03cf 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -2800,6 +2800,7 @@ public class io/sentry/SentryOptions { public fun getBeforeBreadcrumb ()Lio/sentry/SentryOptions$BeforeBreadcrumbCallback; public fun getBeforeEnvelopeCallback ()Lio/sentry/SentryOptions$BeforeEnvelopeCallback; public fun getBeforeSend ()Lio/sentry/SentryOptions$BeforeSendCallback; + public fun getBeforeSendReplay ()Lio/sentry/SentryOptions$BeforeSendReplayCallback; public fun getBeforeSendTransaction ()Lio/sentry/SentryOptions$BeforeSendTransactionCallback; public fun getBundleIds ()Ljava/util/Set; public fun getCacheDirPath ()Ljava/lang/String; @@ -2916,6 +2917,7 @@ public class io/sentry/SentryOptions { public fun setBeforeBreadcrumb (Lio/sentry/SentryOptions$BeforeBreadcrumbCallback;)V public fun setBeforeEnvelopeCallback (Lio/sentry/SentryOptions$BeforeEnvelopeCallback;)V public fun setBeforeSend (Lio/sentry/SentryOptions$BeforeSendCallback;)V + public fun setBeforeSendReplay (Lio/sentry/SentryOptions$BeforeSendReplayCallback;)V public fun setBeforeSendTransaction (Lio/sentry/SentryOptions$BeforeSendTransactionCallback;)V public fun setCacheDirPath (Ljava/lang/String;)V public fun setConnectionStatusProvider (Lio/sentry/IConnectionStatusProvider;)V @@ -3022,6 +3024,10 @@ public abstract interface class io/sentry/SentryOptions$BeforeSendCallback { public abstract fun execute (Lio/sentry/SentryEvent;Lio/sentry/Hint;)Lio/sentry/SentryEvent; } +public abstract interface class io/sentry/SentryOptions$BeforeSendReplayCallback { + public abstract fun execute (Lio/sentry/SentryReplayEvent;Lio/sentry/Hint;)Lio/sentry/SentryReplayEvent; +} + public abstract interface class io/sentry/SentryOptions$BeforeSendTransactionCallback { public abstract fun execute (Lio/sentry/protocol/SentryTransaction;Lio/sentry/Hint;)Lio/sentry/protocol/SentryTransaction; } @@ -3154,19 +3160,23 @@ public final class io/sentry/SentryReplayOptions { public fun getErrorReplayDuration ()J public fun getFrameRate ()I public fun getMaskViewClasses ()Ljava/util/Set; + public fun getMaskViewContainerClass ()Ljava/lang/String; public fun getOnErrorSampleRate ()Ljava/lang/Double; public fun getQuality ()Lio/sentry/SentryReplayOptions$SentryReplayQuality; public fun getSessionDuration ()J public fun getSessionSampleRate ()Ljava/lang/Double; public fun getSessionSegmentDuration ()J public fun getUnmaskViewClasses ()Ljava/util/Set; + public fun getUnmaskViewContainerClass ()Ljava/lang/String; public fun isSessionReplayEnabled ()Z public fun isSessionReplayForErrorsEnabled ()Z public fun setMaskAllImages (Z)V public fun setMaskAllText (Z)V + public fun setMaskViewContainerClass (Ljava/lang/String;)V public fun setOnErrorSampleRate (Ljava/lang/Double;)V public fun setQuality (Lio/sentry/SentryReplayOptions$SentryReplayQuality;)V public fun setSessionSampleRate (Ljava/lang/Double;)V + public fun setUnmaskViewContainerClass (Ljava/lang/String;)V } public final class io/sentry/SentryReplayOptions$SentryReplayQuality : java/lang/Enum { @@ -5874,15 +5884,22 @@ public final class io/sentry/transport/NoOpTransportGate : io/sentry/transport/I public fun isConnected ()Z } -public final class io/sentry/transport/RateLimiter { +public final class io/sentry/transport/RateLimiter : java/io/Closeable { public fun (Lio/sentry/SentryOptions;)V public fun (Lio/sentry/transport/ICurrentDateProvider;Lio/sentry/SentryOptions;)V + public fun addRateLimitObserver (Lio/sentry/transport/RateLimiter$IRateLimitObserver;)V + public fun close ()V public fun filter (Lio/sentry/SentryEnvelope;Lio/sentry/Hint;)Lio/sentry/SentryEnvelope; public fun isActiveForCategory (Lio/sentry/DataCategory;)Z public fun isAnyRateLimitActive ()Z + public fun removeRateLimitObserver (Lio/sentry/transport/RateLimiter$IRateLimitObserver;)V public fun updateRetryAfterLimits (Ljava/lang/String;Ljava/lang/String;I)V } +public abstract interface class io/sentry/transport/RateLimiter$IRateLimitObserver { + public abstract fun onRateLimitChanged (Lio/sentry/transport/RateLimiter;)V +} + public final class io/sentry/transport/ReusableCountLatch { public fun ()V public fun (I)V diff --git a/sentry/src/main/java/io/sentry/SentryClient.java b/sentry/src/main/java/io/sentry/SentryClient.java index dcf64d0806e..b9824cb0081 100644 --- a/sentry/src/main/java/io/sentry/SentryClient.java +++ b/sentry/src/main/java/io/sentry/SentryClient.java @@ -276,6 +276,17 @@ private void finalizeTransaction(final @NotNull IScope scope, final @NotNull Hin event = processReplayEvent(event, hint, options.getEventProcessors()); + if (event != null) { + event = executeBeforeSendReplay(event, hint); + + if (event == null) { + options.getLogger().log(SentryLevel.DEBUG, "Event was dropped by beforeSendReplay"); + options + .getClientReportRecorder() + .recordLostEvent(DiscardReason.BEFORE_SEND, DataCategory.Replay); + } + } + if (event == null) { options.getLogger().log(SentryLevel.DEBUG, "Replay was dropped by Event processors."); return SentryId.EMPTY_ID; @@ -1133,6 +1144,27 @@ private void sortBreadcrumbsByDate( return transaction; } + private @Nullable SentryReplayEvent executeBeforeSendReplay( + @NotNull SentryReplayEvent event, final @NotNull Hint hint) { + final SentryOptions.BeforeSendReplayCallback beforeSendReplay = options.getBeforeSendReplay(); + if (beforeSendReplay != null) { + try { + event = beforeSendReplay.execute(event, hint); + } catch (Throwable e) { + options + .getLogger() + .log( + SentryLevel.ERROR, + "The BeforeSendReplay callback threw an exception. It will be added as breadcrumb and continue.", + e); + + // drop event in case of an error in beforeSend due to PII concerns + event = null; + } + } + return event; + } + @Override public void close() { close(false); diff --git a/sentry/src/main/java/io/sentry/SentryOptions.java b/sentry/src/main/java/io/sentry/SentryOptions.java index 8e427251e69..2ea882b3dc2 100644 --- a/sentry/src/main/java/io/sentry/SentryOptions.java +++ b/sentry/src/main/java/io/sentry/SentryOptions.java @@ -152,6 +152,12 @@ public class SentryOptions { */ private @Nullable BeforeSendTransactionCallback beforeSendTransaction; + /** + * This function is called with an SDK specific replay object and can return a modified replay + * object or nothing to skip reporting the replay + */ + private @Nullable BeforeSendReplayCallback beforeSendReplay; + /** * This function is called with an SDK specific breadcrumb object before the breadcrumb is added * to the scope. When nothing is returned from the function, the breadcrumb is dropped @@ -749,6 +755,24 @@ public void setBeforeSendTransaction( this.beforeSendTransaction = beforeSendTransaction; } + /** + * Returns the BeforeSendReplay callback + * + * @return the beforeSend callback or null if not set + */ + public @Nullable BeforeSendReplayCallback getBeforeSendReplay() { + return beforeSendReplay; + } + + /** + * Sets the beforeSendReplay callback + * + * @param beforeSendReplay the beforeSend callback + */ + public void setBeforeSendReplay(@Nullable BeforeSendReplayCallback beforeSendReplay) { + this.beforeSendReplay = beforeSendReplay; + } + /** * Returns the beforeBreadcrumb callback * @@ -2474,6 +2498,23 @@ public interface BeforeSendTransactionCallback { SentryTransaction execute(@NotNull SentryTransaction transaction, @NotNull Hint hint); } + /** The BeforeSendReplay callback */ + public interface BeforeSendReplayCallback { + + /** + * Mutate or drop a replay event before being sent. Note that there might be many replay events + * for a single replay (i.e. segments), you can check {@link SentryReplayEvent#getReplayId()} to + * identify that the segments belong to the same replay. + * + * @param event the event + * @param hint the hint, contains {@link ReplayRecording}, can be accessed via {@link + * Hint#getReplayRecording()} + * @return the original event or the mutated event or null if event was dropped + */ + @Nullable + SentryReplayEvent execute(@NotNull SentryReplayEvent event, @NotNull Hint hint); + } + /** The BeforeBreadcrumb callback */ public interface BeforeBreadcrumbCallback { diff --git a/sentry/src/main/java/io/sentry/SentryReplayOptions.java b/sentry/src/main/java/io/sentry/SentryReplayOptions.java index 0c99085726a..fd492213ac1 100644 --- a/sentry/src/main/java/io/sentry/SentryReplayOptions.java +++ b/sentry/src/main/java/io/sentry/SentryReplayOptions.java @@ -81,6 +81,12 @@ public enum SentryReplayQuality { */ private Set unmaskViewClasses = new CopyOnWriteArraySet<>(); + /** The class name of the view container that masks all of its children. */ + private @Nullable String maskViewContainerClass = null; + + /** The class name of the view container that unmasks its direct children. */ + private @Nullable String unmaskViewContainerClass = null; + /** * Defines the quality of the session replay. The higher the quality, the more accurate the replay * will be, but also more data to transfer and more CPU load, defaults to MEDIUM. @@ -239,4 +245,25 @@ public long getSessionSegmentDuration() { public long getSessionDuration() { return sessionDuration; } + + @ApiStatus.Internal + public void setMaskViewContainerClass(@NotNull String containerClass) { + addMaskViewClass(containerClass); + maskViewContainerClass = containerClass; + } + + @ApiStatus.Internal + public void setUnmaskViewContainerClass(@NotNull String containerClass) { + unmaskViewContainerClass = containerClass; + } + + @ApiStatus.Internal + public @Nullable String getMaskViewContainerClass() { + return maskViewContainerClass; + } + + @ApiStatus.Internal + public @Nullable String getUnmaskViewContainerClass() { + return unmaskViewContainerClass; + } } diff --git a/sentry/src/main/java/io/sentry/clientreport/ClientReportRecorder.java b/sentry/src/main/java/io/sentry/clientreport/ClientReportRecorder.java index 0428980995c..b4d4574abae 100644 --- a/sentry/src/main/java/io/sentry/clientreport/ClientReportRecorder.java +++ b/sentry/src/main/java/io/sentry/clientreport/ClientReportRecorder.java @@ -171,6 +171,9 @@ private DataCategory categoryFromItemType(SentryItemType itemType) { if (SentryItemType.CheckIn.equals(itemType)) { return DataCategory.Monitor; } + if (SentryItemType.ReplayVideo.equals(itemType)) { + return DataCategory.Replay; + } return DataCategory.Default; } diff --git a/sentry/src/main/java/io/sentry/transport/AsyncHttpTransport.java b/sentry/src/main/java/io/sentry/transport/AsyncHttpTransport.java index 985bbcccbbe..24f954c0c10 100644 --- a/sentry/src/main/java/io/sentry/transport/AsyncHttpTransport.java +++ b/sentry/src/main/java/io/sentry/transport/AsyncHttpTransport.java @@ -170,6 +170,7 @@ public void close() throws IOException { @Override public void close(final boolean isRestarting) throws IOException { + rateLimiter.close(); executor.shutdown(); options.getLogger().log(SentryLevel.DEBUG, "Shutting down"); try { diff --git a/sentry/src/main/java/io/sentry/transport/RateLimiter.java b/sentry/src/main/java/io/sentry/transport/RateLimiter.java index 34e74c85ef9..cb560aff99c 100644 --- a/sentry/src/main/java/io/sentry/transport/RateLimiter.java +++ b/sentry/src/main/java/io/sentry/transport/RateLimiter.java @@ -15,16 +15,21 @@ import io.sentry.hints.SubmissionResult; import io.sentry.util.HintUtils; import io.sentry.util.StringUtils; +import java.io.Closeable; +import java.io.IOException; import java.util.ArrayList; import java.util.Date; import java.util.List; import java.util.Map; +import java.util.Timer; +import java.util.TimerTask; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CopyOnWriteArrayList; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; /** Controls retry limits on different category types sent to Sentry. */ -public final class RateLimiter { +public final class RateLimiter implements Closeable { private static final int HTTP_RETRY_AFTER_DEFAULT_DELAY_MILLIS = 60000; @@ -32,6 +37,9 @@ public final class RateLimiter { private final @NotNull SentryOptions options; private final @NotNull Map sentryRetryAfterLimit = new ConcurrentHashMap<>(); + private final @NotNull List rateLimitObservers = new CopyOnWriteArrayList<>(); + private @Nullable Timer timer = null; + private final @NotNull Object timerLock = new Object(); public RateLimiter( final @NotNull ICurrentDateProvider currentDateProvider, @@ -180,6 +188,8 @@ private boolean isRetryAfter(final @NotNull String itemType) { return DataCategory.Transaction; case "check_in": return DataCategory.Monitor; + case "replay_video": + return DataCategory.Replay; default: return DataCategory.Unknown; } @@ -272,6 +282,23 @@ private void applyRetryAfterOnlyIfLonger( // only overwrite its previous date if the limit is even longer if (oldDate == null || date.after(oldDate)) { sentryRetryAfterLimit.put(dataCategory, date); + + notifyRateLimitObservers(); + + synchronized (timerLock) { + if (timer == null) { + timer = new Timer(true); + } + + timer.schedule( + new TimerTask() { + @Override + public void run() { + notifyRateLimitObservers(); + } + }, + date); + } } } @@ -293,4 +320,41 @@ private long parseRetryAfterOrDefault(final @Nullable String retryAfterHeader) { } return retryAfterMillis; } + + private void notifyRateLimitObservers() { + for (IRateLimitObserver observer : rateLimitObservers) { + observer.onRateLimitChanged(this); + } + } + + public void addRateLimitObserver(@NotNull final IRateLimitObserver observer) { + rateLimitObservers.add(observer); + } + + public void removeRateLimitObserver(@NotNull final IRateLimitObserver observer) { + rateLimitObservers.remove(observer); + } + + @Override + public void close() throws IOException { + synchronized (timerLock) { + if (timer != null) { + timer.cancel(); + timer = null; + } + } + rateLimitObservers.clear(); + } + + public interface IRateLimitObserver { + /** + * Invoked whenever the rate limit changed. You should use {@link + * RateLimiter#isActiveForCategory(DataCategory)} to check whether the category you're + * interested in has changed. + * + * @param rateLimiter this {@link RateLimiter} instance which you can use to check if the rate + * limit is active for a specific category + */ + void onRateLimitChanged(@NotNull RateLimiter rateLimiter); + } } diff --git a/sentry/src/test/java/io/sentry/SentryClientTest.kt b/sentry/src/test/java/io/sentry/SentryClientTest.kt index 738538a0064..e1b13a87734 100644 --- a/sentry/src/test/java/io/sentry/SentryClientTest.kt +++ b/sentry/src/test/java/io/sentry/SentryClientTest.kt @@ -2826,6 +2826,68 @@ class SentryClientTest { assertFalse(called) } + @Test + fun `when beforeSendReplay is set, callback is invoked`() { + var invoked = false + fixture.sentryOptions.setBeforeSendReplay { replay: SentryReplayEvent, _: Hint -> invoked = true; replay } + + fixture.getSut().captureReplayEvent(SentryReplayEvent(), Scope(fixture.sentryOptions), Hint()) + + assertTrue(invoked) + } + + @Test + fun `when beforeSendReplay returns null, event is dropped`() { + fixture.sentryOptions.setBeforeSendReplay { replay: SentryReplayEvent, _: Hint -> null } + + fixture.getSut().captureReplayEvent(SentryReplayEvent(), Scope(fixture.sentryOptions), Hint()) + + verify(fixture.transport, never()).send(any(), anyOrNull()) + + assertClientReport( + fixture.sentryOptions.clientReportRecorder, + listOf( + DiscardedEvent(DiscardReason.BEFORE_SEND.reason, DataCategory.Replay.category, 1) + ) + ) + } + + @Test + fun `when beforeSendReplay returns new instance, new instance is sent`() { + val expected = SentryReplayEvent().apply { tags = mapOf("test" to "test") } + fixture.sentryOptions.setBeforeSendReplay { _, _ -> expected } + + fixture.getSut().captureReplayEvent(SentryReplayEvent(), Scope(fixture.sentryOptions), Hint()) + + verify(fixture.transport).send( + check { + val replay = getReplayFromData(it.items.first().data) + assertEquals("test", replay!!.tags!!["test"]) + }, + anyOrNull() + ) + verifyNoMoreInteractions(fixture.transport) + } + + @Test + fun `when beforeSendReplay throws an exception, replay is dropped`() { + val exception = Exception("test") + + exception.stackTrace.toString() + fixture.sentryOptions.setBeforeSendReplay { _, _ -> throw exception } + + val id = fixture.getSut().captureReplayEvent(SentryReplayEvent(), Scope(fixture.sentryOptions), Hint()) + + assertEquals(SentryId.EMPTY_ID, id) + + assertClientReport( + fixture.sentryOptions.clientReportRecorder, + listOf( + DiscardedEvent(DiscardReason.BEFORE_SEND.reason, DataCategory.Replay.category, 1) + ) + ) + } + private fun givenScopeWithStartedSession(errored: Boolean = false, crashed: Boolean = false): IScope { val scope = createScope(fixture.sentryOptions) scope.startSession() @@ -3003,6 +3065,25 @@ class SentryClientTest { )!! } + private fun getReplayFromData(data: ByteArray): SentryReplayEvent? { + val unpacker = MessagePack.newDefaultUnpacker(data) + val mapSize = unpacker.unpackMapHeader() + for (i in 0 until mapSize) { + val key = unpacker.unpackString() + when (key) { + SentryItemType.ReplayEvent.itemType -> { + val replayEventLength = unpacker.unpackBinaryHeader() + val replayEventBytes = unpacker.readPayload(replayEventLength) + return fixture.sentryOptions.serializer.deserialize( + InputStreamReader(replayEventBytes.inputStream()), + SentryReplayEvent::class.java + )!! + } + } + } + return null + } + private fun verifyAttachmentsInEnvelope(eventId: SentryId?) { verify(fixture.transport).send( check { actual -> diff --git a/sentry/src/test/java/io/sentry/clientreport/ClientReportTest.kt b/sentry/src/test/java/io/sentry/clientreport/ClientReportTest.kt index 6950142072e..138ea12d312 100644 --- a/sentry/src/test/java/io/sentry/clientreport/ClientReportTest.kt +++ b/sentry/src/test/java/io/sentry/clientreport/ClientReportTest.kt @@ -10,12 +10,14 @@ import io.sentry.Hint import io.sentry.IScopes import io.sentry.NoOpLogger import io.sentry.ProfilingTraceData +import io.sentry.ReplayRecording import io.sentry.Sentry import io.sentry.SentryEnvelope import io.sentry.SentryEnvelopeHeader import io.sentry.SentryEnvelopeItem import io.sentry.SentryEvent import io.sentry.SentryOptions +import io.sentry.SentryReplayEvent import io.sentry.SentryTracer import io.sentry.Session import io.sentry.TracesSamplingDecision @@ -69,13 +71,14 @@ class ClientReportTest { SentryEnvelopeItem.fromUserFeedback(opts.serializer, UserFeedback(SentryId(UUID.randomUUID()))), SentryEnvelopeItem.fromAttachment(opts.serializer, NoOpLogger.getInstance(), Attachment("{ \"number\": 10 }".toByteArray(), "log.json"), 1000), SentryEnvelopeItem.fromProfilingTrace(ProfilingTraceData(File(""), transaction), 1000, opts.serializer), - SentryEnvelopeItem.fromCheckIn(opts.serializer, CheckIn("monitor-slug-1", CheckInStatus.ERROR)) + SentryEnvelopeItem.fromCheckIn(opts.serializer, CheckIn("monitor-slug-1", CheckInStatus.ERROR)), + SentryEnvelopeItem.fromReplay(opts.serializer, opts.logger, SentryReplayEvent(), ReplayRecording(), false) ) clientReportRecorder.recordLostEnvelope(DiscardReason.NETWORK_ERROR, envelope) val clientReportAtEnd = clientReportRecorder.resetCountsAndGenerateClientReport() - testHelper.assertTotalCount(14, clientReportAtEnd) + testHelper.assertTotalCount(15, clientReportAtEnd) testHelper.assertCountFor(DiscardReason.SAMPLE_RATE, DataCategory.Error, 3, clientReportAtEnd) testHelper.assertCountFor(DiscardReason.BEFORE_SEND, DataCategory.Error, 2, clientReportAtEnd) testHelper.assertCountFor(DiscardReason.QUEUE_OVERFLOW, DataCategory.Transaction, 1, clientReportAtEnd) @@ -87,6 +90,7 @@ class ClientReportTest { testHelper.assertCountFor(DiscardReason.NETWORK_ERROR, DataCategory.Attachment, 1, clientReportAtEnd) testHelper.assertCountFor(DiscardReason.NETWORK_ERROR, DataCategory.Profile, 1, clientReportAtEnd) testHelper.assertCountFor(DiscardReason.NETWORK_ERROR, DataCategory.Monitor, 1, clientReportAtEnd) + testHelper.assertCountFor(DiscardReason.NETWORK_ERROR, DataCategory.Replay, 1, clientReportAtEnd) } @Test diff --git a/sentry/src/test/java/io/sentry/transport/AsyncHttpTransportTest.kt b/sentry/src/test/java/io/sentry/transport/AsyncHttpTransportTest.kt index abaa965175c..ae479ed6cb8 100644 --- a/sentry/src/test/java/io/sentry/transport/AsyncHttpTransportTest.kt +++ b/sentry/src/test/java/io/sentry/transport/AsyncHttpTransportTest.kt @@ -329,6 +329,14 @@ class AsyncHttpTransportTest { ) } + @Test + fun `close closes the rate limiter`() { + val sut = fixture.getSUT() + sut.close() + + verify(fixture.rateLimiter).close() + } + @Test fun `close uses flushTimeoutMillis option to schedule termination`() { fixture.sentryOptions.flushTimeoutMillis = 123 diff --git a/sentry/src/test/java/io/sentry/transport/RateLimiterTest.kt b/sentry/src/test/java/io/sentry/transport/RateLimiterTest.kt index 8c198654a39..1b7ae7fe614 100644 --- a/sentry/src/test/java/io/sentry/transport/RateLimiterTest.kt +++ b/sentry/src/test/java/io/sentry/transport/RateLimiterTest.kt @@ -3,17 +3,22 @@ package io.sentry.transport import io.sentry.Attachment import io.sentry.CheckIn import io.sentry.CheckInStatus +import io.sentry.DataCategory.Replay import io.sentry.Hint +import io.sentry.IHub +import io.sentry.ILogger import io.sentry.IScopes import io.sentry.ISerializer import io.sentry.NoOpLogger import io.sentry.ProfilingTraceData +import io.sentry.ReplayRecording import io.sentry.SentryEnvelope import io.sentry.SentryEnvelopeHeader import io.sentry.SentryEnvelopeItem import io.sentry.SentryEvent import io.sentry.SentryOptions import io.sentry.SentryOptionsManipulator +import io.sentry.SentryReplayEvent import io.sentry.SentryTracer import io.sentry.Session import io.sentry.TransactionContext @@ -25,6 +30,7 @@ import io.sentry.protocol.SentryId import io.sentry.protocol.SentryTransaction import io.sentry.protocol.User import io.sentry.util.HintUtils +import org.awaitility.kotlin.await import org.mockito.kotlin.eq import org.mockito.kotlin.mock import org.mockito.kotlin.same @@ -34,6 +40,7 @@ import org.mockito.kotlin.verifyNoMoreInteractions import org.mockito.kotlin.whenever import java.io.File import java.util.UUID +import java.util.concurrent.atomic.AtomicBoolean import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertFalse @@ -303,4 +310,70 @@ class RateLimiterTest { verify(hint).markFlushed() } + + @Test + fun `drop replay items as lost`() { + val rateLimiter = fixture.getSUT() + val hub = mock() + whenever(hub.options).thenReturn(SentryOptions()) + + val replayItem = SentryEnvelopeItem.fromReplay(fixture.serializer, mock(), SentryReplayEvent(), ReplayRecording(), false) + val attachmentItem = SentryEnvelopeItem.fromAttachment(fixture.serializer, NoOpLogger.getInstance(), Attachment("{ \"number\": 10 }".toByteArray(), "log.json"), 1000) + val envelope = SentryEnvelope(SentryEnvelopeHeader(), arrayListOf(replayItem, attachmentItem)) + + rateLimiter.updateRetryAfterLimits("60:replay:key", null, 1) + val result = rateLimiter.filter(envelope, Hint()) + + assertNotNull(result) + assertEquals(1, result.items.toList().size) + + verify(fixture.clientReportRecorder, times(1)).recordLostEnvelopeItem(eq(DiscardReason.RATELIMIT_BACKOFF), same(replayItem)) + verifyNoMoreInteractions(fixture.clientReportRecorder) + } + + @Test + fun `apply rate limits notifies observers`() { + val rateLimiter = fixture.getSUT() + + var applied = false + rateLimiter.addRateLimitObserver { + applied = rateLimiter.isActiveForCategory(Replay) + } + rateLimiter.updateRetryAfterLimits("60:replay:key", null, 1) + + assertTrue(applied) + } + + @Test + fun `apply rate limits schedules a timer to notify observers of lifted limits`() { + val rateLimiter = fixture.getSUT() + whenever(fixture.currentDateProvider.currentTimeMillis).thenReturn(0, 1, 2001) + + val applied = AtomicBoolean(true) + rateLimiter.addRateLimitObserver { + applied.set(rateLimiter.isActiveForCategory(Replay)) + } + rateLimiter.updateRetryAfterLimits("1:replay:key", null, 1) + + await.untilFalse(applied) + assertFalse(applied.get()) + } + + @Test + fun `close cancels the timer`() { + val rateLimiter = fixture.getSUT() + whenever(fixture.currentDateProvider.currentTimeMillis).thenReturn(0, 1, 2001) + + val applied = AtomicBoolean(true) + rateLimiter.addRateLimitObserver { + applied.set(rateLimiter.isActiveForCategory(Replay)) + } + + rateLimiter.updateRetryAfterLimits("1:replay:key", null, 1) + rateLimiter.close() + + // wait for 1.5s to ensure the timer has run after 1s + await.untilTrue(applied) + assertTrue(applied.get()) + } }