realm-kotlin icon indicating copy to clipboard operation
realm-kotlin copied to clipboard

RealmResults leak

Open santaevpavel opened this issue 1 year ago • 1 comments

How frequently does the bug occur?

Sometimes

Description

In specific cases notification callback is not released that leads to memory leaks. A notification callback holds a reference to RealmResults that I believe holds reference to native realm results.

The leak happens when a Flow that returned from ObjectQuery::asFlow() is cancelled before callbackFlow() (source) execution reaches awaitClose() (source). It that case notification token that was received in RealmResults::registerForNotification won't be released.

This leak can be verified by capturing memory heap dump. NativeObjectReference, RealmResultsImpl instances left after closing everything and calling GC. Only a reference to callback holds reference to RealmResultsImpl.

Screenshot 2024-10-22 at 16 52 35

I think it can be fixed by surrounding withContext(dispatcher) { ... } by try catch that catches CancellationException and releases a notification token if needed:

return callbackFlow {
   //... 
  try {
      withContext(dispatcher) {
           //...
       }
   } catch (err: CancellationException) {
       token.value.cancel()
       throw err
   }
   awaitClose { 
         // ... 
   }
}
 

Stacktrace & log output

No response

Can you reproduce the bug?

Sometimes

Reproduction Steps

This bug can be reproduced by creating many observers and cancelling them in a loop.


    private fun observe() {
        lifecycleScope.launch {
            (0..100).forEach {
                val jobs = observeMany()
                delay(100)
                jobs.cancel()
                delay(100)
            }
       }
    }

    private fun observeMany(): Job {
        return lifecycleScope.launch {
            val flows = (0..100).map { idx ->
                realm!!.query(Entity0::class)
                    .query("column1 >= $0", 4000 + idx * 100)
                    .asFlow()
                    .map { it.list }
            }
            combine(flows) { arr -> arr }
                .collect {
                    Log.d("!!!!!", "Realm observed many Entity0")
                }
            }
    }

Version

3.0.0

What Atlas App Services are you using?

Local Database only

Are you using encryption?

No

Platform OS and version(s)

Android 15

Build environment

Android Studio version: 2023.3.1 Patch 2 Android Build Tools version: 8.3.2 Gradle version: 8.4

santaevpavel avatar Oct 22 '24 14:10 santaevpavel