Skip to content

Commit 8ba5fb7

Browse files
authored
Merge 4d296ec into 84b57d0
2 parents 84b57d0 + 4d296ec commit 8ba5fb7

File tree

6 files changed

+85
-5
lines changed

6 files changed

+85
-5
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# Changelog
22

3+
## Unreleased
4+
5+
### Fixes
6+
7+
- Session Replay: Fix memory leak when masking Compose screens ([#3985](https://github.com/getsentry/sentry-java/pull/3985))
8+
39
## 7.19.0
410

511
### Fixes

buildSrc/src/main/java/Config.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,7 @@ object Config {
194194
val mockitoKotlin = "org.mockito.kotlin:mockito-kotlin:4.1.0"
195195
val mockitoInline = "org.mockito:mockito-inline:4.8.0"
196196
val awaitility = "org.awaitility:awaitility-kotlin:4.1.1"
197+
val awaitility3 = "org.awaitility:awaitility-kotlin:3.1.6" // need this due to a conflict of awaitility4+ and espresso on hamcrest
197198
val mockWebserver = "com.squareup.okhttp3:mockwebserver:${Libs.okHttpVersion}"
198199
val jsonUnit = "net.javacrumbs.json-unit:json-unit:2.32.0"
199200
val hsqldb = "org.hsqldb:hsqldb:2.6.1"

sentry-android-integration-tests/sentry-uitest-android/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@ dependencies {
125125
androidTestImplementation(Config.TestLibs.mockWebserver)
126126
androidTestImplementation(Config.TestLibs.androidxJunit)
127127
androidTestImplementation(Config.TestLibs.leakCanaryInstrumentation)
128+
androidTestImplementation(Config.TestLibs.awaitility3)
128129
androidTestUtil(Config.TestLibs.androidxTestOrchestrator)
129130
}
130131

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
package io.sentry.uitest.android
2+
3+
import androidx.lifecycle.Lifecycle
4+
import androidx.test.core.app.launchActivity
5+
import io.sentry.SentryOptions
6+
import leakcanary.LeakAssertions
7+
import leakcanary.LeakCanary
8+
import org.awaitility.kotlin.await
9+
import shark.AndroidReferenceMatchers
10+
import shark.IgnoredReferenceMatcher
11+
import shark.ReferencePattern
12+
import java.util.concurrent.atomic.AtomicBoolean
13+
import kotlin.test.Test
14+
15+
class ReplayTest : BaseUiTest() {
16+
@Test
17+
fun composeReplayDoesNotLeak() {
18+
val sent = AtomicBoolean(false)
19+
20+
LeakCanary.config = LeakCanary.config.copy(
21+
referenceMatchers = AndroidReferenceMatchers.appDefaults +
22+
listOf(
23+
IgnoredReferenceMatcher(
24+
ReferencePattern.InstanceFieldPattern(
25+
"com.saucelabs.rdcinjector.testfairy.TestFairyEventQueue",
26+
"context"
27+
)
28+
),
29+
// Seems like a false-positive returned by LeakCanary when curtains is used in
30+
// the host application (LeakCanary uses it itself internally). We use kind of
31+
// the same approach which possibly clashes with LeakCanary's internal state.
32+
// Only the case when replay is enabled.
33+
// TODO: check if it's actually a leak on our side, or a false-positive and report to LeakCanary's github issue tracker
34+
IgnoredReferenceMatcher(
35+
ReferencePattern.InstanceFieldPattern(
36+
"curtains.internal.RootViewsSpy",
37+
"delegatingViewList"
38+
)
39+
)
40+
) + ('a'..'z').map { char ->
41+
IgnoredReferenceMatcher(
42+
ReferencePattern.StaticFieldPattern(
43+
"com.testfairy.modules.capture.TouchListener",
44+
"$char"
45+
)
46+
)
47+
}
48+
)
49+
50+
val activityScenario = launchActivity<ComposeActivity>()
51+
activityScenario.moveToState(Lifecycle.State.RESUMED)
52+
53+
initSentry {
54+
it.experimental.sessionReplay.sessionSampleRate = 1.0
55+
56+
it.beforeSendReplay =
57+
SentryOptions.BeforeSendReplayCallback { event, _ ->
58+
sent.set(true)
59+
event
60+
}
61+
}
62+
63+
// wait until first segment is being sent
64+
await.untilTrue(sent)
65+
66+
activityScenario.moveToState(Lifecycle.State.DESTROYED)
67+
68+
LeakAssertions.assertNoLeaks()
69+
}
70+
}

sentry-android-replay/src/main/java/io/sentry/android/replay/util/Nodes.kt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import androidx.compose.ui.graphics.Color
88
import androidx.compose.ui.graphics.ColorProducer
99
import androidx.compose.ui.graphics.painter.Painter
1010
import androidx.compose.ui.layout.LayoutCoordinates
11+
import androidx.compose.ui.layout.findRootCoordinates
1112
import androidx.compose.ui.node.LayoutNode
1213
import androidx.compose.ui.text.TextLayoutResult
1314
import kotlin.math.roundToInt
@@ -165,8 +166,8 @@ private inline fun Float.fastCoerceAtMost(maximumValue: Float): Float {
165166
*
166167
* @return boundaries of this layout relative to the window's origin.
167168
*/
168-
internal fun LayoutCoordinates.boundsInWindow(root: LayoutCoordinates?): Rect {
169-
root ?: return Rect()
169+
internal fun LayoutCoordinates.boundsInWindow(rootCoordinates: LayoutCoordinates?): Rect {
170+
val root = rootCoordinates ?: findRootCoordinates()
170171

171172
val rootWidth = root.size.width.toFloat()
172173
val rootHeight = root.size.height.toFloat()

sentry-android-replay/src/main/java/io/sentry/android/replay/viewhierarchy/ComposeViewHierarchyNode.kt

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import io.sentry.android.replay.util.toOpaque
2727
import io.sentry.android.replay.viewhierarchy.ViewHierarchyNode.GenericViewHierarchyNode
2828
import io.sentry.android.replay.viewhierarchy.ViewHierarchyNode.ImageViewHierarchyNode
2929
import io.sentry.android.replay.viewhierarchy.ViewHierarchyNode.TextViewHierarchyNode
30+
import java.lang.ref.WeakReference
3031

3132
@TargetApi(26)
3233
internal object ComposeViewHierarchyNode {
@@ -62,7 +63,7 @@ internal object ComposeViewHierarchyNode {
6263
return options.experimental.sessionReplay.maskViewClasses.contains(className)
6364
}
6465

65-
private var _rootCoordinates: LayoutCoordinates? = null
66+
private var _rootCoordinates: WeakReference<LayoutCoordinates>? = null
6667

6768
private fun fromComposeNode(
6869
node: LayoutNode,
@@ -77,11 +78,11 @@ internal object ComposeViewHierarchyNode {
7778
}
7879

7980
if (isComposeRoot) {
80-
_rootCoordinates = node.coordinates.findRootCoordinates()
81+
_rootCoordinates = WeakReference(node.coordinates.findRootCoordinates())
8182
}
8283

8384
val semantics = node.collapsedSemantics
84-
val visibleRect = node.coordinates.boundsInWindow(_rootCoordinates)
85+
val visibleRect = node.coordinates.boundsInWindow(_rootCoordinates?.get())
8586
val isVisible = !node.outerCoordinator.isTransparent() &&
8687
(semantics == null || !semantics.contains(SemanticsProperties.InvisibleToUser)) &&
8788
visibleRect.height() > 0 && visibleRect.width() > 0

0 commit comments

Comments
 (0)