Created
February 6, 2020 12:08
-
-
Save nhachicha/7c3154842970b9dcecbe18e89e8acb22 to your computer and use it in GitHub Desktop.
Example of a GC detection mechanism in Kotlin/Native
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package sample | |
import kotlinx.cinterop.StableRef | |
import kotlinx.cinterop.toCPointer | |
import kotlinx.coroutines.* | |
import kotlinx.coroutines.channels.Channel | |
import kotlinx.coroutines.channels.ReceiveChannel | |
import kotlin.native.internal.GC | |
import kotlin.native.ref.WeakReference | |
// A reference is observed via a WeakRef, this coroutine keeps polling the observed references to detect if they were GC'ed by the Kotlin runtime. | |
fun CoroutineScope.collectorDaemon(referencesReceiver: ReceiveChannel<Pair<Long, Long>>) { | |
launch { | |
val queue = mutableListOf<Pair<Long, Long>>() | |
while (true) { | |
ensureActive() | |
GC.collect() // hint to the GC to run | |
// add all new references to the queue (if any) | |
while (!referencesReceiver.isEmpty) { | |
queue.add(referencesReceiver.poll()!!) | |
} | |
// scan the queue to discover any GC'ed reference | |
val iterator = queue.iterator() | |
while (iterator.hasNext()) { | |
val pair = iterator.next() | |
val stableWeakRef = StableRef.fromValue(pair.first.toCPointer()!!) | |
val weakRef: WeakReference<Any> = stableWeakRef.get() as WeakReference<Any> | |
if (weakRef.get() == null) { | |
// Object has been GC'ed, closing pointer and removing the pair from the reference queue. | |
stableWeakRef.dispose() | |
iterator.remove() | |
(::nativeDestructor)(pair.second) | |
} | |
} | |
println("ReferenceQueue size: ${queue.size}") | |
delay(500) | |
} | |
} | |
} | |
// method to be called internally whenever we want to observe the lifecycle (GC collection) of an allocated instance | |
internal fun observeInstance(instance: Person, destructorPointer: Long, senderChannel: Channel<Pair<Long, Long>>) { | |
GlobalScope.launch { | |
val stableRefRawPointer = StableRef.create(WeakReference(instance)).asCPointer().rawValue.toLong() | |
senderChannel.send(Pair(stableRefRawPointer, destructorPointer)) | |
} | |
} | |
// in a real scenario this will call the c-interop layer to free/call dtor of the underlying C++ object | |
internal fun nativeDestructor(nativePointer: Long) { | |
// TODO Call c-interop layer to free nativePointer allocated in C++ | |
println("Closing pointer: $nativePointer") | |
} | |
// channel through which we communicate the instances we want the observe via the 'collectorDaemon' | |
@SharedImmutable | |
val referencesChannel = Channel<Pair<Long, Long>>() | |
fun main() = runBlocking { | |
GlobalScope.launch { | |
collectorDaemon(referencesChannel) | |
} | |
allocate1() | |
allocate2() | |
// after the first allocation 'collectorDaemon' should clean the GC'ed p2 (p1 and p3 are still hold with a strong reference) | |
// after the second allocation 'collectorDaemon' should clean the GC'ed p4 and p6 (p6 is still hold with a strong reference) | |
// the above is undeterministic since we don't control exactly when the Kotlin runtime triggers the GC, and when the WeakReference 'referred' becomes null | |
// nullifying the strong references should allow 'collectorDaemon' eventually to clean the GC'ed p1,p3 and p6 | |
strongReference.clear() | |
allocateLargeObjects() // simulate a workload where the GC needs to kick in to start collect... | |
// Delay to observe in the console that the 'collectorDaemon' is detecting GC'ed object and freeing their respective native pointer | |
delay(10000) | |
} | |
// -- Helper methods and class to the example -- | |
data class Person(val name: String, internal val pointer: Long) | |
val strongReference = mutableListOf<Person>() | |
// simulate some allocations, some should be GC'ed right away and some will be kept alive using a strong reference | |
fun allocate1() { | |
val p1: Person = Person("p1", 0XCAFEBABE) | |
val p2: Person = Person("p2", 0x8BADF00D) | |
val p3: Person = Person("p3", 0x1BADB002) | |
observeInstance(p1, p1.pointer, referencesChannel) | |
observeInstance(p2, p2.pointer, referencesChannel) | |
observeInstance(p3, p3.pointer, referencesChannel) | |
strongReference.add(p1) | |
strongReference.add(p3) | |
} | |
// simulate some allocations, some should be GC'ed right away and some will be kept alive using a strong reference | |
fun allocate2() { | |
val p4: Person = Person("p4", 0xB105F00D) | |
val p5: Person = Person("p5", 0xBAAAAAAD) | |
val p6: Person = Person("p6", 0xBAADF00D) | |
observeInstance(p4, p4.pointer, referencesChannel) | |
observeInstance(p5, p5.pointer, referencesChannel) | |
observeInstance(p6, p6.pointer, referencesChannel) | |
strongReference.add(p6) | |
} | |
suspend fun allocateLargeObjects() { | |
repeat(100) { | |
ByteArray(100_000_000) | |
delay(100) | |
} | |
} | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment