Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[DO NOT MERGE] Sharing a repro scenario for a leak related to Recomposer.snapshotInvalidations #1409

Closed
wants to merge 1 commit into from

Conversation

pyricau
Copy link

@pyricau pyricau commented Jun 2, 2024

I'm opening this just as a demo, I'll close the PR immediately after, this isn't meant to be merged.

The core of the issue is that large object graphs can be retained by Recomposer.snapshotInvalidations until the next recomposition. When an activity gets destroyed, there's no more recomposition there, so Recomposer.snapshotInvalidations isn't cleared until something else needs recomposing. This is a problem in Compose tests where the Recomposer is a shared instance and a singleton.

This issue has been impacting our UI tests more and more as we migrated to Compose, it's been 2 years now, and we've had to progressively disable leak detection in more and more tests as this kept firing and making tests fail. I only finally figured it out.

While there seems to be a workaround (composeTestRule.waitForIdle()), ideally we wouldn't need to do this.

I'll check in with folks if this is worth filing.

Here's what the failure looks like:

leakcanary.NoLeakAssertionFailedError: Application memory leaks were detected:
====================================
HEAP ANALYSIS RESULT
====================================
1 APPLICATION LEAKS

References underlined with "~~~" are likely causes.
Learn more at https://squ.re/leaks.

86965 bytes retained by leaking objects
Signature: d116c5db7719b516032df23a9f15ca63ca60ffe1
┬───
│ GC Root: System class
│
├─ androidx.compose.runtime.snapshots.SnapshotKt class
│    Leaking: NO (a class is never leaking)
│    ↓ static SnapshotKt.applyObservers
│                        ~~~~~~~~~~~~~~
├─ java.util.ArrayList instance
│    Leaking: UNKNOWN
│    Retaining 28 B in 2 objects
│    ↓ ArrayList[0]
│               ~~~
├─ androidx.compose.runtime.Recomposer$recompositionRunner$2$unregisterApplyObserver$1 instance
│    Leaking: UNKNOWN
│    Retaining 16 B in 1 objects
│    Anonymous subclass of kotlin.jvm.internal.Lambda
│    ↓ Recomposer$recompositionRunner$2$unregisterApplyObserver$1.this$0
│                                                                 ~~~~~~
├─ androidx.compose.runtime.Recomposer instance
│    Leaking: UNKNOWN
│    Retaining 88.3 kB in 1758 objects
│    ↓ Recomposer.snapshotInvalidations
│                 ~~~~~~~~~~~~~~~~~~~~~
├─ androidx.compose.runtime.collection.IdentityArraySet instance
│    Leaking: UNKNOWN
│    Retaining 87.4 kB in 1720 objects
│    ↓ IdentityArraySet.values
│                       ~~~~~~
├─ java.lang.Object[] array
│    Leaking: UNKNOWN
│    Retaining 87.4 kB in 1719 objects
│    ↓ Object[2]
│            ~~~
├─ androidx.compose.runtime.ParcelableSnapshotMutableState instance
│    Leaking: UNKNOWN
│    Retaining 87.1 kB in 1706 objects
│    ↓ SnapshotMutableStateImpl.next
│                               ~~~~
├─ androidx.compose.runtime.SnapshotMutableStateImpl$StateStateRecord instance
│    Leaking: UNKNOWN
│    Retaining 87.0 kB in 1704 objects
│    ↓ SnapshotMutableStateImpl$StateStateRecord.value
│                                                ~~~~~
├─ androidx.compose.runtime.internal.ComposableLambdaImpl instance
│    Leaking: UNKNOWN
│    Retaining 87.0 kB in 1702 objects
│    ↓ ComposableLambdaImpl._block
│                           ~~~~~~
├─ com.example.compose.jetchat.LeakingActivity$resetContent$1 instance
│    Leaking: UNKNOWN
│    Retaining 87.0 kB in 1701 objects
│    Anonymous class implementing kotlin.jvm.functions.Function2
│    this$0 instance of com.example.compose.jetchat.LeakingActivity with mDestroyed = true
│    ↓ LeakingActivity$resetContent$1.this$0
│                                     ~~~~~~
╰→ com.example.compose.jetchat.LeakingActivity instance
​     Leaking: YES (ObjectWatcher was watching this because com.example.compose.jetchat.LeakingActivity received Activity#onDestroy() callback and Activity#mDestroyed is true)
​     Retaining 87.0 kB in 1700 objects
​     key = 6e7baf8d-18bc-4d55-8c0d-a7bd3bc674de
​     watchDurationMillis = 5130
​     retainedDurationMillis = 129
​     mApplication instance of android.app.Application
​     mBase instance of androidx.appcompat.view.ContextThemeWrapper
====================================
0 LIBRARY LEAKS

A Library Leak is a leak caused by a known bug in 3rd party code that you do not have control over.
See https://square.github.io/leakcanary/fundamentals-how-leakcanary-works/#4-categorizing-leaks
====================================
0 UNREACHABLE OBJECTS

An unreachable object is still in memory but LeakCanary could not find a strong reference path
from GC roots.
====================================
METADATA

Please include this in bug reports and Stack Overflow questions.

Build.VERSION.SDK_INT: 26
Build.MANUFACTURER: unknown
LeakCanary version: 3.0-alpha-7
App process name: com.example.compose.jetchat
Class count: 9794
Instance count: 75040
Primitive array count: 70212
Object array count: 8425
Thread count: 21
Heap total bytes: 6846740
Bitmap count: 98
Bitmap total bytes: 3418920
Large bitmap count: 0
Large bitmap total bytes: 0
Stats: LruCache[maxSize=3000,hits=17476,misses=24185,hitRate=41%] RandomAccess[bytes=1154589,reads=24185,travel=5637779180,range=9201471,size=11883130]
assertionTag:
waitForRetainedDurationMillis: 5128
totalDurationMillis: 7956
Analysis duration: 538 ms
Heap dump file path: /data/user/0/com.example.compose.jetchat/files/instrumentation_tests/2024-06-01_23-45-22_057_RecomposerLeakTest-app_launches.hprof
Heap dump timestamp: 1717310724884
Heap dump duration: 150 ms
====================================
at leakcanary.NoLeakAssertionFailedError$Companion.throwOnApplicationLeaks$lambda$3(NoLeakAssertionFailedError.kt:25)
at leakcanary.NoLeakAssertionFailedError$Companion.$r8$lambda$YWXscVJi44IEBq_-uq5c_WHdK1k(Unknown Source:0)
at leakcanary.NoLeakAssertionFailedError$Companion$$ExternalSyntheticLambda0.reportHeapAnalysis(D8$$SyntheticClass:0)
at leakcanary.AndroidDetectLeaksAssert.runLeakChecks(AndroidDetectLeaksAssert.kt:112)
at leakcanary.AndroidDetectLeaksAssert.assertNoLeaks(AndroidDetectLeaksAssert.kt:34)
at leakcanary.LeakAssertions.assertNoLeaks(LeakAssertions.kt:21)
at leakcanary.LeakAssertions.assertNoLeaks$default(LeakAssertions.kt:20)
at com.example.compose.jetchat.RecomposerLeakTest.app_launches(RecomposerLeakTest.kt:44)

@pyricau pyricau requested a review from a team as a code owner June 2, 2024 06:55
@pyricau pyricau requested a review from jdkoren June 2, 2024 06:55
@pyricau pyricau closed this Jun 2, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

1 participant