Skip to content

Instantly share code, notes, and snippets.

@nhachicha
Created February 6, 2020 12:08
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save nhachicha/7c3154842970b9dcecbe18e89e8acb22 to your computer and use it in GitHub Desktop.
Save nhachicha/7c3154842970b9dcecbe18e89e8acb22 to your computer and use it in GitHub Desktop.
Example of a GC detection mechanism in Kotlin/Native
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