From 420f2a61e1cc64d68bf03825e8fd70cf49ac6a01 Mon Sep 17 00:00:00 2001 From: Linnea Gräf Date: Tue, 24 Sep 2024 11:40:15 +0200 Subject: Use weak caches for custom textures --- src/main/kotlin/util/MutableMapWithMaxSize.kt | 38 ------- src/main/kotlin/util/Optionalutil.kt | 5 + src/main/kotlin/util/collections/InstanceList.kt | 57 +++++++++++ .../util/collections/MutableMapWithMaxSize.kt | 40 ++++++++ src/main/kotlin/util/collections/WeakCache.kt | 110 +++++++++++++++++++++ src/main/kotlin/util/collections/listutil.kt | 9 ++ src/main/kotlin/util/listutil.kt | 9 -- 7 files changed, 221 insertions(+), 47 deletions(-) delete mode 100644 src/main/kotlin/util/MutableMapWithMaxSize.kt create mode 100644 src/main/kotlin/util/Optionalutil.kt create mode 100644 src/main/kotlin/util/collections/InstanceList.kt create mode 100644 src/main/kotlin/util/collections/MutableMapWithMaxSize.kt create mode 100644 src/main/kotlin/util/collections/WeakCache.kt create mode 100644 src/main/kotlin/util/collections/listutil.kt delete mode 100644 src/main/kotlin/util/listutil.kt (limited to 'src/main/kotlin/util') diff --git a/src/main/kotlin/util/MutableMapWithMaxSize.kt b/src/main/kotlin/util/MutableMapWithMaxSize.kt deleted file mode 100644 index 067e652..0000000 --- a/src/main/kotlin/util/MutableMapWithMaxSize.kt +++ /dev/null @@ -1,38 +0,0 @@ - -package moe.nea.firmament.util - -fun mutableMapWithMaxSize(maxSize: Int): MutableMap = object : LinkedHashMap() { - override fun removeEldestEntry(eldest: MutableMap.MutableEntry): Boolean { - return size > maxSize - } -} - -fun ((T) -> R).memoizeIdentity(maxCacheSize: Int): (T) -> R { - val memoized = { it: IdentityCharacteristics -> - this(it.value) - }.memoize(maxCacheSize) - return { memoized(IdentityCharacteristics(it)) } -} - -@PublishedApi -internal val SENTINEL_NULL = java.lang.Object() - -/** - * Requires the map to only contain values of type [R] or [SENTINEL_NULL]. This is ensured if the map is only ever - * accessed via this function. - */ -inline fun MutableMap.computeNullableFunction(key: T, crossinline func: () -> R): R { - val value = this.getOrPut(key) { - func() ?: SENTINEL_NULL - } - @Suppress("UNCHECKED_CAST") - return if (value === SENTINEL_NULL) null as R - else value as R -} - -fun ((T) -> R).memoize(maxCacheSize: Int): (T) -> R { - val map = mutableMapWithMaxSize(maxCacheSize) - return { - map.computeNullableFunction(it) { this@memoize(it) } - } -} diff --git a/src/main/kotlin/util/Optionalutil.kt b/src/main/kotlin/util/Optionalutil.kt new file mode 100644 index 0000000..1cef2fe --- /dev/null +++ b/src/main/kotlin/util/Optionalutil.kt @@ -0,0 +1,5 @@ +package moe.nea.firmament.util + +import java.util.Optional + +fun T?.intoOptional(): Optional = Optional.ofNullable(this) diff --git a/src/main/kotlin/util/collections/InstanceList.kt b/src/main/kotlin/util/collections/InstanceList.kt new file mode 100644 index 0000000..fd8c786 --- /dev/null +++ b/src/main/kotlin/util/collections/InstanceList.kt @@ -0,0 +1,57 @@ +package moe.nea.firmament.util.collections + +import java.lang.ref.ReferenceQueue +import java.lang.ref.WeakReference + +class InstanceList(val name: String) { + val queue = object : ReferenceQueue() {} + val set = mutableSetOf() + + val size: Int + get() { + clearOldReferences() + return set.size + } + + fun clearOldReferences() { + while (true) { + val reference = queue.poll() ?: break + set.remove(reference) + } + } + + fun getAll(): List { + clearOldReferences() + return set.mapNotNull { it.get() } + } + + fun add(t: T) { + set.add(Ref(t)) + } + + init { + if (init) + allInstances.add(this) + } + + inner class Ref(referent: T) : WeakReference(referent) { + val hashCode = System.identityHashCode(referent) + override fun equals(other: Any?): Boolean { + return other is InstanceList<*>.Ref && hashCode == other.hashCode && get() === other.get() + } + + override fun hashCode(): Int { + return hashCode + } + } + + companion object { + private var init = false + val allInstances = InstanceList>("InstanceLists") + + init { + init = true + allInstances.add(allInstances) + } + } +} diff --git a/src/main/kotlin/util/collections/MutableMapWithMaxSize.kt b/src/main/kotlin/util/collections/MutableMapWithMaxSize.kt new file mode 100644 index 0000000..218bc55 --- /dev/null +++ b/src/main/kotlin/util/collections/MutableMapWithMaxSize.kt @@ -0,0 +1,40 @@ + +package moe.nea.firmament.util.collections + +import moe.nea.firmament.util.IdentityCharacteristics + +fun mutableMapWithMaxSize(maxSize: Int): MutableMap = object : LinkedHashMap() { + override fun removeEldestEntry(eldest: MutableMap.MutableEntry): Boolean { + return size > maxSize + } +} + +fun ((T) -> R).memoizeIdentity(maxCacheSize: Int): (T) -> R { + val memoized = { it: IdentityCharacteristics -> + this(it.value) + }.memoize(maxCacheSize) + return { memoized(IdentityCharacteristics(it)) } +} + +@PublishedApi +internal val SENTINEL_NULL = java.lang.Object() + +/** + * Requires the map to only contain values of type [R] or [SENTINEL_NULL]. This is ensured if the map is only ever + * accessed via this function. + */ +inline fun MutableMap.computeNullableFunction(key: T, crossinline func: () -> R): R { + val value = this.getOrPut(key) { + func() ?: SENTINEL_NULL + } + @Suppress("UNCHECKED_CAST") + return if (value === SENTINEL_NULL) null as R + else value as R +} + +fun ((T) -> R).memoize(maxCacheSize: Int): (T) -> R { + val map = mutableMapWithMaxSize(maxCacheSize) + return { + map.computeNullableFunction(it) { this@memoize(it) } + } +} diff --git a/src/main/kotlin/util/collections/WeakCache.kt b/src/main/kotlin/util/collections/WeakCache.kt new file mode 100644 index 0000000..38f9886 --- /dev/null +++ b/src/main/kotlin/util/collections/WeakCache.kt @@ -0,0 +1,110 @@ +package moe.nea.firmament.util.collections + +import java.lang.ref.ReferenceQueue +import java.lang.ref.WeakReference +import moe.nea.firmament.features.debug.DebugLogger + +/** + * Cache class that uses [WeakReferences][WeakReference] to only cache values while there is still a life reference to + * the key. Each key can have additional extra data that is used to look up values. That extra data is not required to + * be a life reference. The main Key is compared using strict reference equality. This map is not synchronized. + */ +class WeakCache(val name: String) { + private val queue = object : ReferenceQueue() {} + private val map = mutableMapOf() + + val size: Int + get() { + clearOldReferences() + return map.size + } + + fun clearOldReferences() { + var successCount = 0 + var totalCount = 0 + while (true) { + val reference = queue.poll() ?: break + totalCount++ + if (map.remove(reference) != null) + successCount++ + } + if (totalCount > 0) + logger.log { "Cleared $successCount/$totalCount references from queue" } + } + + fun get(key: Key, extraData: ExtraKey): Value? { + clearOldReferences() + return map[Ref(key, extraData)] + } + + fun put(key: Key, extraData: ExtraKey, value: Value) { + clearOldReferences() + map[Ref(key, extraData)] = value + } + + fun getOrPut(key: Key, extraData: ExtraKey, value: (Key, ExtraKey) -> Value): Value { + clearOldReferences() + return map.getOrPut(Ref(key, extraData)) { value(key, extraData) } + } + + fun clear() { + map.clear() + } + + init { + allInstances.add(this) + } + + companion object { + val allInstances = InstanceList>("WeakCaches") + private val logger = DebugLogger("WeakCache") + fun memoize(name: String, function: (Key) -> Value): + CacheFunction.NoExtraData { + return CacheFunction.NoExtraData(WeakCache(name), function) + } + + fun memoize(name: String, function: (Key, ExtraKey) -> Value): + CacheFunction.WithExtraData { + return CacheFunction.WithExtraData(WeakCache(name), function) + } + } + + inner class Ref( + weakInstance: Key, + val extraData: ExtraKey, + ) : WeakReference(weakInstance, queue) { + val hashCode = System.identityHashCode(weakInstance) * 31 + extraData.hashCode() + override fun equals(other: Any?): Boolean { + if (other !is WeakCache<*, *, *>.Ref) return false + return other.hashCode == this.hashCode + && other.get() === this.get() + && other.extraData == this.extraData + } + + override fun hashCode(): Int { + return hashCode + } + } + + interface CacheFunction { + val cache: WeakCache<*, *, *> + + data class NoExtraData( + override val cache: WeakCache, + val wrapped: (Key) -> Value, + ) : CacheFunction, (Key) -> Value { + override fun invoke(p1: Key): Value { + return cache.getOrPut(p1, Unit, { a, _ -> wrapped(a) }) + } + } + + data class WithExtraData( + override val cache: WeakCache, + val wrapped: (Key, ExtraKey) -> Value, + ) : CacheFunction, (Key, ExtraKey) -> Value { + override fun invoke(p1: Key, p2: ExtraKey): Value { + return cache.getOrPut(p1, p2, wrapped) + } + } + } +} diff --git a/src/main/kotlin/util/collections/listutil.kt b/src/main/kotlin/util/collections/listutil.kt new file mode 100644 index 0000000..7f85a30 --- /dev/null +++ b/src/main/kotlin/util/collections/listutil.kt @@ -0,0 +1,9 @@ + +package moe.nea.firmament.util.collections + +fun List.lastNotNullOfOrNull(func: (T) -> R?): R? { + for (i in indices.reversed()) { + return func(this[i]) ?: continue + } + return null +} diff --git a/src/main/kotlin/util/listutil.kt b/src/main/kotlin/util/listutil.kt deleted file mode 100644 index 73cb23e..0000000 --- a/src/main/kotlin/util/listutil.kt +++ /dev/null @@ -1,9 +0,0 @@ - -package moe.nea.firmament.util - -fun List.lastNotNullOfOrNull(func: (T) -> R?): R? { - for (i in indices.reversed()) { - return func(this[i]) ?: continue - } - return null -} -- cgit