All Downloads are FREE. Search and download functionalities are using the official Maven repository.

godot.core.GarbageCollector.kt Maven / Gradle / Ivy

There is a newer version: 0.2.0-3.3.2
Show newest version
package godot.core

import godot.core.memory.GodotStatic
import godot.core.memory.NativeCoreWeakReference
import godot.core.memory.ReferenceWeakReference
import godot.util.VoidPtr
import godot.util.info
import godot.util.warning
import java.lang.ref.ReferenceQueue
import java.lang.ref.WeakReference
import java.time.Duration
import java.time.Instant
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.Executors
import java.util.concurrent.TimeUnit

object GarbageCollector {

    /** Minimum time before 2 iterations of the thread.*/
    private const val MIN_DELAY = 0L

    /** Maximum time before 2 iterations of the thread.*/
    private const val MAX_DELAY = 2000L

    /** The delay between iterations can be increased or decreased by this value.*/
    private const val INC_DELAY = 100L

    /** Current time between 2 iterations of the thread. */
    private var current_delay = (MIN_DELAY + MAX_DELAY) / 2L

    /** Number of references to check each loop.*/
    private const val CHECK_NUMBER = 256

    /** Percentage of objects to check each loop.*/
    private const val CHECK_PER_CENT = 0.2f

    /** Index of the last Object checked.*/
    private var current_index = 0

    /** Pointers to Objects.*/
    private val wrappedMap = ConcurrentHashMap(CHECK_NUMBER)

    /** Pointers to References.*/
    private val refWrappedList = ArrayList(CHECK_NUMBER)

    /** CurrentSize of the List, so we can grow it a lot chunk instead of one by one */
    private var current_size = CHECK_NUMBER

    /** Pointers to NativeCoreType.*/
    private val nativeCoreTypeMap = ConcurrentHashMap(CHECK_NUMBER)

    /** Queues so we are notified when the GC runs on References.*/
    private val refReferenceQueue = ReferenceQueue()

    /** Queues so we are notified when the GC runs on NativeCoreTypes.*/
    private val nativeReferenceQueue = ReferenceQueue()

    /** List of element to remove from RefWrapperList*/
    private val deleteQueue = ArrayList()

    /** List mirroring the content of the Object HashMap.*/
    private var wrapperList: List>? = null

    /** Holds the instances to clean up when the JVM stops.*/
    private val staticInstances = mutableListOf()

    private val executor = Executors.newSingleThreadScheduledExecutor()

    private var forceJvmGarbageCollector = false
    private var shouldDisplayLeakInstancesOnClose = true
    private var isCleanup = false

    private var gcState = GCState.NONE

    val isClosed: Boolean
        get() = gcState == GCState.CLOSED

    fun registerObject(instance: KtObject) {
        val rawPtr = instance.rawPtr
        wrappedMap[rawPtr] = instance
    }

    fun registerReference(instance: KtObject) {
        val index = instance.__id.toInt()
        synchronized(refWrappedList) {
            //It will throw an Exception if the size is too small so we have to grow the list.
            if (refWrappedList.size == index) {
                if (refWrappedList.size == current_size) {
                    current_size *= 2
                    refWrappedList.ensureCapacity(current_size)
                }
                //index is size of the list, this means we can add it at the end of it.
                refWrappedList.add(ReferenceWeakReference(instance, refReferenceQueue, index))
            } else {
                refWrappedList[index] = ReferenceWeakReference(instance, refReferenceQueue, index)
            }
        }
    }

    fun registerNativeCoreType(nativeCoreType: NativeCoreType, variantType: VariantType) {
        val rawPtr = nativeCoreType._handle
        nativeCoreTypeMap[rawPtr] = NativeCoreWeakReference(nativeCoreType, nativeReferenceQueue, variantType)
    }

    fun registerStatic(instance: GodotStatic) {
        staticInstances.add(instance)
    }

    fun getObjectInstance(ptr: VoidPtr, id: Long): KtObject? {
        val ktObject = wrappedMap[ptr]
        if (ktObject != null) {
            if (id == ktObject.__id) {
                return ktObject
            }
        }
        return null
    }

    fun getRefInstance(index: Int): KtObject? {
        synchronized(refWrappedList) {
            if (refWrappedList.size <= index) {
                //If the list is too small, it means the ref we want is not there yet.
                return null
            }
            val ref = refWrappedList[index]
            val instance = ref?.get()
            if(instance != null){
                ref.counter++
                return instance
            }
            return null
        }
    }

    fun getNativeCoreTypeInstance(ptr: VoidPtr): NativeCoreType? {
        return nativeCoreTypeMap[ptr]?.get()
    }

    fun isInstanceValid(ktObject: KtObject) = MemoryBridge.checkInstance(ktObject.rawPtr, ktObject.__id)

    fun start(forceJvmGarbageCollector: Boolean) {
        this.forceJvmGarbageCollector = forceJvmGarbageCollector
        gcState = GCState.STARTED
        info("Starting GC thread")
        executor.schedule(GarbageCollector::run, 0, TimeUnit.MILLISECONDS)
    }

    private fun run() {
        while (gcState == GCState.STARTED) {
            if (forceJvmGarbageCollector) {
                forceJvmGc()
            }
            val isActive = checkAndClean()

            if (isActive) {
                current_delay -= INC_DELAY
                current_delay = current_delay.coerceAtLeast(MIN_DELAY)
            } else {
                current_delay += INC_DELAY
                current_delay = current_delay.coerceAtMost(MAX_DELAY)
            }

            if (current_delay > 0L) {
                Thread.sleep(current_delay)
            }
        }
        gcState = GCState.CLOSED
    }

    /**
     * Remove the Object pointers that died since the last GC.
     * Decrease the counter of the References and NativeCoreType that are not reachable anymore.
     * @return True if something has been deleted.
     */
    private fun checkAndClean(): Boolean {
        var isActive = false
        var counter = 0

        // Check validity of cpp pointer for classic godot Object, if not valid, then remove jvm instance.
        // This binds jvm instance lifecycle to native object's one.
        if (wrapperList == null) {
            //We only create a single List in this block so we don't block the whole map for the duration of the checking
            wrapperList = wrappedMap.toList()
        }

        val size = wrapperList!!.size

        //Number of check is a % of the total list of object, which at least the regular number of check per iteration.
        val limit = (CHECK_PER_CENT * size)
            .toInt()
            .coerceAtLeast(CHECK_NUMBER)
            .coerceAtMost(size - current_index)

        while (counter < limit) {
            val entry = wrapperList!![current_index++]
            if (!MemoryBridge.checkInstance(entry.first, entry.second.__id)) {
                wrappedMap.remove(entry.first)
                isActive = true
            }
            counter++
        }
        if (current_index == size) {
            wrapperList = null
            current_index = 0
        }
        counter = 0

        // A native reference cannot die while a jvm instance exists (because counter > 0). When we don't need the
        // jvm instance anymore, we decrease the counter.
        while (counter < CHECK_NUMBER || isCleanup) {
            val ref = (refReferenceQueue.poll() ?: break) as ReferenceWeakReference
            //We first add the ref to the deleteQueue
            deleteQueue.add(ref)
            isActive = true
            counter++
        }
        synchronized(refWrappedList) {
            //we remove the element from the map
            for (ref in deleteQueue) {
                val otherRef = refWrappedList[ref.index]
                //Check if the ref in the hashmap hasn't been modified by another thread
                if(otherRef == ref){
                    refWrappedList[ref.index] = null
                }
            }
        }

        //Lastly, we call unref after removing elements from the map.
        // We don't want an index to be reused when there is cleaning to do.
        for (ref in deleteQueue) {
            MemoryBridge.unref(ref.ptr, ref.counter)
        }

        deleteQueue.clear()
        counter = 0

        // Same as before for NativeCoreTypes
        while (counter < CHECK_NUMBER || isCleanup) {
            val ref = (nativeReferenceQueue.poll() ?: break) as NativeCoreWeakReference
            if (MemoryBridge.unrefNativeCoreType(ref.ptr, ref.variantType.baseOrdinal)) {
                nativeCoreTypeMap.remove(ref.ptr)
                isActive = true
            }
            counter++
        }
        return isActive
    }

    fun close() {
        info("Closing GC thread")
        gcState = GCState.CLOSING
        executor.shutdown()
        executor.awaitTermination(5000, TimeUnit.MILLISECONDS)
    }

    fun cleanUp() {
        for (instance in staticInstances) {
            instance.collect()
        }

        isCleanup = true
        var begin = Instant.now()
        while (refWrappedList.any { it != null } || wrappedMap.isNotEmpty() || nativeCoreTypeMap.isNotEmpty()) {

            forceJvmGc()
            if (checkAndClean()) {
                begin = Instant.now()
            }

            val finish = Instant.now()
            if (Duration.between(begin, finish).toMillis() > 5000) {
                warning(
                    buildString {
                        appendLine("Some JVM godot instances are leaked.")
                        if (shouldDisplayLeakInstancesOnClose) {
                            appendLine("Leaked references:")
                            for (entry in refWrappedList) {
                                if (entry != null) {
                                    append("    ${entry.get()}")
                                    append(System.lineSeparator())
                                }
                            }
                            appendLine("Leaked objects:")
                            for (entry in wrappedMap) {
                                append("    ${entry.key}: ${entry.value}")
                                append(System.lineSeparator())
                            }
                            appendLine("Leaked native core types:")
                            for (entry in nativeCoreTypeMap) {
                                append("    ${entry.key}: ${entry.value}")
                                append(System.lineSeparator())
                            }
                        }
                    }
                )
                MemoryBridge.notifyLeak()
                break
            }
        }
    }

    /**
     * Force JVM garbage collector to run
     */
    private fun forceJvmGc() {
        var any: Any? = Any()
        val wkRef = WeakReference(any)
        any = null
        while (wkRef.get() != null) {
            System.gc()
        }
    }

    private object MemoryBridge {
        external fun checkInstance(ptr: VoidPtr, instanceId: Long): Boolean
        external fun unref(ptr: VoidPtr, counter: Int): Boolean
        external fun unrefNativeCoreType(ptr: VoidPtr, variantType: Int): Boolean
        external fun notifyLeak()
    }

    private enum class GCState {
        NONE,
        STARTED,
        CLOSING,
        CLOSED
    }

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy