From e345adb744291b6a0c43747a0d7060db9372e128 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20=E2=80=9CCLOVIS=E2=80=9D=20Canet?= Date: Fri, 6 Sep 2024 21:18:00 +0200 Subject: [PATCH 01/10] build(weak): Create the weak module --- settings.gradle.kts | 2 ++ weak/README.md | 7 +++++++ weak/build.gradle.kts | 31 +++++++++++++++++++++++++++++++ 3 files changed, 40 insertions(+) create mode 100644 weak/README.md create mode 100644 weak/build.gradle.kts diff --git a/settings.gradle.kts b/settings.gradle.kts index 36aaa59..dbeb7f9 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -66,6 +66,8 @@ include( "backbone", + "weak", + "spine", "spine-ktor", "spine-ktor:spine-ktor-server", diff --git a/weak/README.md b/weak/README.md new file mode 100644 index 0000000..74cbdfe --- /dev/null +++ b/weak/README.md @@ -0,0 +1,7 @@ +# Module Weak + +Weak references and maps for Kotlin Multiplatform. + + + + diff --git a/weak/build.gradle.kts b/weak/build.gradle.kts new file mode 100644 index 0000000..42a33f3 --- /dev/null +++ b/weak/build.gradle.kts @@ -0,0 +1,31 @@ +plugins { + alias(opensavvyConventions.plugins.base) + alias(opensavvyConventions.plugins.kotlin.library) +} + +kotlin { + jvm() + js(IR) { + browser() + nodejs() + } + iosSimulatorArm64() + iosArm64() + iosX64() + linuxX64() + + sourceSets.commonTest.dependencies { + implementation(libs.bundles.prepared) + } +} + +library { + name.set("Weak") + description.set("Weak references and maps for Kotlin Multiplatform") + homeUrl.set("https://opensavvy.gitlab.io/groundwork/pedestal/api-docs/weak/index.html") + + license.set { + name.set("Apache 2.0") + url.set("https://www.apache.org/licenses/LICENSE-2.0.txt") + } +} -- 2.51.2 From a0b38bfbdb9f8fd1c65d86baa6ba9ccefd4edd66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20=E2=80=9CCLOVIS=E2=80=9D=20Canet?= Date: Fri, 6 Sep 2024 21:42:02 +0200 Subject: [PATCH 02/10] feat(weak): Create the ExperimentalWeakApi --- weak/src/commonMain/kotlin/ExperimentalWeakApi.kt | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 weak/src/commonMain/kotlin/ExperimentalWeakApi.kt diff --git a/weak/src/commonMain/kotlin/ExperimentalWeakApi.kt b/weak/src/commonMain/kotlin/ExperimentalWeakApi.kt new file mode 100644 index 0000000..63a47aa --- /dev/null +++ b/weak/src/commonMain/kotlin/ExperimentalWeakApi.kt @@ -0,0 +1,5 @@ +package opensavvy.pedestal.weak + +@RequiresOptIn("This API is waiting for feedback before stabilization. Please send us feedback at https://gitlab.com/opensavvy/groundwork/pedestal/-/issues/150", RequiresOptIn.Level.ERROR) +@MustBeDocumented +annotation class ExperimentalWeakApi -- 2.51.2 From ee9aca1bd2d9e82326d1b51307dc653f27bfc82d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20=E2=80=9CCLOVIS=E2=80=9D=20Canet?= Date: Fri, 6 Sep 2024 21:45:55 +0200 Subject: [PATCH 03/10] feat(weak): Create WeakRef and SoftRef Closes #152 --- gradle/libs.versions.toml | 3 + weak/build.gradle.kts | 4 + weak/src/commonMain/kotlin/WeakRef.kt | 83 ++++++++++++++++++++ weak/src/commonTest/kotlin/WeakRefTest.kt | 1 + weak/src/jsMain/kotlin/WeakRef.js.kt | 34 ++++++++ weak/src/jvmMain/kotlin/WeakRef.jvm.kt | 37 +++++++++ weak/src/nativeMain/kotlin/WeakRef.native.kt | 37 +++++++++ 7 files changed, 199 insertions(+) create mode 100644 weak/src/commonMain/kotlin/WeakRef.kt create mode 100644 weak/src/commonTest/kotlin/WeakRefTest.kt create mode 100644 weak/src/jsMain/kotlin/WeakRef.js.kt create mode 100644 weak/src/jvmMain/kotlin/WeakRef.jvm.kt create mode 100644 weak/src/nativeMain/kotlin/WeakRef.native.kt diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e1c228b..c952213 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -9,6 +9,7 @@ kotlinx-datetime = "0.6.0" # https://github.com/Kotlin/kotlinx-dateti kotlinx-coroutines = "1.8.1" # https://github.com/Kotlin/kotlinx.coroutines/releases kotlinx-serialization = "1.7.1" # https://github.com/Kotlin/kotlinx.serialization/releases prepared = "1.3.0" # https://gitlab.com/opensavvy/prepared/-/releases +kotlin-js = "1.0.0-pre.801" # https://central.sonatype.com/artifact/org.jetbrains.kotlin-wrappers/kotlin-js [plugins] @@ -19,6 +20,8 @@ kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-t kotlinx-serialization-core = { module = "org.jetbrains.kotlinx:kotlinx-serialization-core", version.ref = "kotlinx-serialization" } kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization" } +kotlinJs = { module = "org.jetbrains.kotlin-wrappers:kotlin-js", version.ref = "kotlin-js" } + ktor-server-core = { module = "io.ktor:ktor-server-core", version.ref = "ktor" } ktor-server-testHost = { module = "io.ktor:ktor-server-test-host", version.ref = "ktor" } ktor-server-contentNegotiation = { module = "io.ktor:ktor-server-content-negotiation", version.ref = "ktor" } diff --git a/weak/build.gradle.kts b/weak/build.gradle.kts index 42a33f3..91fd4f7 100644 --- a/weak/build.gradle.kts +++ b/weak/build.gradle.kts @@ -14,6 +14,10 @@ kotlin { iosX64() linuxX64() + sourceSets.jsMain.dependencies { + implementation(libs.kotlinJs) + } + sourceSets.commonTest.dependencies { implementation(libs.bundles.prepared) } diff --git a/weak/src/commonMain/kotlin/WeakRef.kt b/weak/src/commonMain/kotlin/WeakRef.kt new file mode 100644 index 0000000..8782914 --- /dev/null +++ b/weak/src/commonMain/kotlin/WeakRef.kt @@ -0,0 +1,83 @@ +package opensavvy.pedestal.weak + +/** + * A weak reference holds a reference to another object, without preventing that object from being garbage-collected. + * + * The [value][read] referenced by this class is eligible for garbage-collection, meaning that it could disappear + * **at any time**. Garbage collectors are complex machinery. One should take care _not_ to write code that depends + * on the behavior of a specific garbage collector, as that behavior can change between future versions. + * + * ### Obtain instances + * + * The default implementations are available via the top-level [WeakRef] and [SoftRef] functions. + * Other implementations are available in the `algorithms` subpackage. + * + * ### Not stable for inheritance + * + * This interface is not stable for inheritance. We may add new methods at any time. + */ +interface WeakRef { + + /** + * Attempts to read the value held by this reference. + * + * If the value was garbage-collected, which may happen at any time, + * this accessor returns `null`. + * + * One should not assume any additional behavior. In particular, the value may continue + * to be returned long after it has become unreachable from anywhere else in the program, + * or disappear sooner than one expected. + * + * In particular, it is not possible for this class to provide a function to check whether + * the value is still available or not: the value could disappear between that check and + * the call to this function. + */ + fun read(): T? + + companion object +} + +/** + * Instantiates a weak reference: a reference to a [value] that doesn't stop + * the garbage collector from collecting it. + * + * The value may be accessed with [WeakRef.read]. + * However, it could disappear **at any time**. + * + * ### When should you use a weak reference? + * + * The runtime is encouraged to clear the value as soon as possible. + * + * This makes implementations ideal for writing mappers from an object to a more expensive one, + * when we know that we don't need the result of the mapping as soon as the initial object + * becomes unavailable. + * + * The exact behavior of the returned object is platform-specific. + * + * @see SoftRef For implementing caches + */ +@ExperimentalWeakApi +expect fun WeakRef(value: T): WeakRef + +/** + * Instantiates a soft reference: a reference to a [value] that doesn't stop + * the garbage collector from collecting it. + * + * The value may be accessed with [WeakRef.read]. + * However, it could disappear **at any time**. + * + * ### When should you use a soft reference? + * + * The runtime is encouraged to keep the value for as long as possible. + * For example, the runtime may decide to keep the value until memory pressure. + * + * This makes this implementation ideal for writing caches, since the value will be kept longer + * than strictly necessary. + * + * The exact behavior of the returned object is platform-specific. + * + * @see WeakRef For implementing mappers + */ +@ExperimentalWeakApi +@Suppress("FunctionName") +expect fun SoftRef(value: T): WeakRef diff --git a/weak/src/commonTest/kotlin/WeakRefTest.kt b/weak/src/commonTest/kotlin/WeakRefTest.kt new file mode 100644 index 0000000..c17fdb4 --- /dev/null +++ b/weak/src/commonTest/kotlin/WeakRefTest.kt @@ -0,0 +1 @@ +package opensavvy.pedestal.weak diff --git a/weak/src/jsMain/kotlin/WeakRef.js.kt b/weak/src/jsMain/kotlin/WeakRef.js.kt new file mode 100644 index 0000000..5a6e536 --- /dev/null +++ b/weak/src/jsMain/kotlin/WeakRef.js.kt @@ -0,0 +1,34 @@ +package opensavvy.pedestal.weak + +private class JsWeakRef( + value: T +) : WeakRef { + + private val reference = js.memory.WeakRef(value) + + override fun read(): T? = + reference.deref() + .takeIf { it != undefined } + + override fun toString(): String = + reference.toString() +} + +/** + * Implementation of [WeakRef] backed by a JS [WeakRef][js.memory.WeakRef]. + * + * JS doesn't make a difference between weak and soft references. + */ +@ExperimentalWeakApi +actual fun WeakRef(value: T): WeakRef = + JsWeakRef(value) + +/** + * Implementation of [WeakRef] backed by a JS [WeakRef][js.memory.WeakRef]. + * + * JS doesn't make a difference between weak and soft references. + */ +@ExperimentalWeakApi +@Suppress("FunctionName") +actual fun SoftRef(value: T): WeakRef = + JsWeakRef(value) diff --git a/weak/src/jvmMain/kotlin/WeakRef.jvm.kt b/weak/src/jvmMain/kotlin/WeakRef.jvm.kt new file mode 100644 index 0000000..910c6b3 --- /dev/null +++ b/weak/src/jvmMain/kotlin/WeakRef.jvm.kt @@ -0,0 +1,37 @@ +package opensavvy.pedestal.weak + +import java.lang.ref.Reference +import java.lang.ref.SoftReference +import java.lang.ref.WeakReference + +private class JavaReferenceHolder( + private val reference: Reference, +) : WeakRef { + + override fun read(): T? = + reference.get() + + override fun toString(): String = + reference.toString() +} + +/** + * Interprets a Java [reference] into a Kotlin [WeakRef]. + */ +fun WeakRef.Companion.fromJava(reference: Reference): WeakRef = + JavaReferenceHolder(reference) + +/** + * Implementation of [WeakRef] backed by a JVM [WeakReference]. + */ +@ExperimentalWeakApi +actual fun WeakRef(value: T): WeakRef = + JavaReferenceHolder(WeakReference(value)) + +/** + * Implementation of [WeakRef] backed by a JVM [SoftReference]. + */ +@ExperimentalWeakApi +@Suppress("FunctionName") +actual fun SoftRef(value: T): WeakRef = + JavaReferenceHolder(SoftReference(value)) diff --git a/weak/src/nativeMain/kotlin/WeakRef.native.kt b/weak/src/nativeMain/kotlin/WeakRef.native.kt new file mode 100644 index 0000000..7b452af --- /dev/null +++ b/weak/src/nativeMain/kotlin/WeakRef.native.kt @@ -0,0 +1,37 @@ +package opensavvy.pedestal.weak + +import kotlin.experimental.ExperimentalNativeApi +import kotlin.native.ref.WeakReference + +@ExperimentalNativeApi +private class NativeWeakRef( + value: T, +) : WeakRef { + + private val reference = WeakReference(value) + + override fun read(): T? = + reference.get() + +} + +/** + * Implementation of [WeakRef] backed by a native [WeakReference]. + * + * Kotlin Native doesn't make a difference between weak and soft references. + */ +@ExperimentalWeakApi +@ExperimentalNativeApi +actual fun WeakRef(value: T): WeakRef = + NativeWeakRef(value) + +/** + * Implementation of [WeakRef] backed by a native [WeakReference]. + * + * Kotlin Native doesn't make a difference between weak and soft references. + */ +@ExperimentalWeakApi +@ExperimentalNativeApi +@Suppress("FunctionName") +actual fun SoftRef(value: T): WeakRef = + NativeWeakRef(value) -- 2.51.2 From bfdaa6a675dc78ca5a1285072c8a298de08fee24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20=E2=80=9CCLOVIS=E2=80=9D=20Canet?= Date: Sun, 8 Sep 2024 18:48:44 +0200 Subject: [PATCH 04/10] feat(weak): Create WeakMap Closes #153 --- weak/src/commonMain/kotlin/WeakMap.kt | 240 +++++++++++++++++++ weak/src/jsMain/kotlin/WeakMap.js.kt | 72 ++++++ weak/src/jvmMain/kotlin/WeakMap.jvm.kt | 44 ++++ weak/src/nativeMain/kotlin/WeakMap.native.kt | 11 + 4 files changed, 367 insertions(+) create mode 100644 weak/src/commonMain/kotlin/WeakMap.kt create mode 100644 weak/src/jsMain/kotlin/WeakMap.js.kt create mode 100644 weak/src/jvmMain/kotlin/WeakMap.jvm.kt create mode 100644 weak/src/nativeMain/kotlin/WeakMap.native.kt diff --git a/weak/src/commonMain/kotlin/WeakMap.kt b/weak/src/commonMain/kotlin/WeakMap.kt new file mode 100644 index 0000000..0f79838 --- /dev/null +++ b/weak/src/commonMain/kotlin/WeakMap.kt @@ -0,0 +1,240 @@ +package opensavvy.pedestal.weak + +import kotlin.reflect.KProperty + +/** + * A weak map is a map in which elements can be garbage-collected at any moment. + * + * The exact timing in which elements can be garbage-collected is implementation-defined. + * Some implementations may allow garbage-collection of keys, some may allow garbage-collection of values, + * or some entirely different behavior. + * + * This class expresses that an association between two values exists, but we may be capable of regenerating it + * if it disappears, or we simply don't have an important enough need for it to keep memory. + * + * Unlike regular [Map], this interface doesn't allow iteration. + * It is not possible to observe the contents of this map, since they are ever mutable + * and elements may disappear at any time. + * Some implementations may provide such observability features. + * + * ### Obtain instances + * + * The default implementation is available via the top-level [WeakMap] function. + * Other implementations are available in the `algorithms` subpackage. + */ +interface WeakMap { + + /** + * Gets the value associated with [key] in this map. + * + * If no value is currently associated with [key], `null` is returned. + * + * @see Map.get Equivalent method for regular maps. + */ + operator fun get(key: K): V? + + /** + * Associates [value] to the [key]. + * + * An implementation may remove this association at any time. + * + * @see MutableMap.set Equivalent method for regular maps. + */ + operator fun set(key: K, value: V) + + /** + * Checks whether a value is currently associated with [key]. + * + * **Note.** Values may be removed at any-time. + * It is possible that a [get] call returns `null`, + * even if it is directly following a [contains] call that returned `true`. + * + * @see Map.contains Equivalent method for regular maps. + */ + @ExperimentalWeakApi + operator fun contains(key: K): Boolean + + /** + * Removes [key] from this map and returns the previously-stored value. + * + * If no value was previously stored, `null` is returned (same behavior as [get], but in a single operation). + * + * @see MutableMap.remove Equivalent method for regular maps. + */ + @ExperimentalWeakApi + fun remove(key: K): V? + + companion object +} + +// region Constructors + +/** + * Instantiates a new, empty [WeakMap]. + * + * The map returned by this function has weak keys: keys of the map + * may be freed if they are not referred to elsewhere in the program. + * When a key is freed, its value is freed as well. + * + * Values are strongly held: until a key is removed from the map, + * it may never be freed by the garbage-collector. + * + * The way the map decides whether two keys are identical is implementation-defined. + * For example, some implementations may use referential equality, some others may + * use the [Any.equals] function. + */ +@ExperimentalWeakApi +expect fun WeakMap(): WeakMap + +/** + * Instantiates a new [WeakMap] by copying [values]. + * + * The map returned by this function has weak keys: keys of the map + * may be freed if they are not referred to elsewhere in the program. + * When a key is freed, its value is freed as well. + * + * Values are strongly held: until a key is removed from the map, + * it may never be freed by the garbage-collector. + * + * The way the map decides whether two keys are identical is implementation-defined. + * For example, some implementations may use referential equality, some others may + * use the [Any.equals] function. + */ +@ExperimentalWeakApi +expect fun WeakMap(values: Map): WeakMap + +// endregion +// region getOrXXX helpers + +/** + * Attempts to find the value associated with [key], returning [defaultValue] if none is found. + */ +@ExperimentalWeakApi +inline fun WeakMap.getOrDefault(key: K, defaultValue: V): V = + get(key) ?: defaultValue + +/** + * Attempts to find the value associated with [key], returning the result of [defaultValue] if none is found. + * + * @see Map.getOrElse Equivalent method for regular maps. + */ +@ExperimentalWeakApi +inline fun WeakMap.getOrElse(key: K, defaultValue: () -> V): V = + get(key) ?: defaultValue() + +/** + * Attempts to find the value associated with [key]. + * + * If no value is associated with [key], calls [defaultValue], + * stores its result back into the map as well as returns it. + * + * In a way, this is a sort of simple cache, where a new value is recomputed + * if the previous one has been deleted. + * + * @see MutableMap.getOrPut Equivalent method for regular maps. + */ +@ExperimentalWeakApi +inline fun WeakMap.getOrPut(key: K, defaultValue: () -> V): V = + get(key) ?: run { + val new = defaultValue() + set(key, new) + new + } + +// endregion +// region Delegation syntax for string-based access + +/** + * Allows to use a weak map for data-oriented usage. + * + * ### Example + * + * ```kotlin + * val map = WeakMap() + * + * var score by map + * var age by map + * + * score = 5 + * println(age) + * ``` + * + * @see Map.getValue Equivalent method for regular maps. + */ +@ExperimentalWeakApi +inline operator fun WeakMap.getValue(thisRef: Any?, property: KProperty<*>): V? = + get(property.name) + +/** + * Allows to use a weak map for data-oriented usage. + * + * ### Example + * + * ```kotlin + * val map = WeakMap() + * + * var score by map + * var age by map + * + * score = 5 + * println(age) + * ``` + * + * @see MutableMap.setValue Equivalent method for regular maps. + */ +@ExperimentalWeakApi +inline operator fun WeakMap.setValue(thisRef: Any?, property: KProperty<*>, value: V) { + set(property.name, value) +} + +// endregion +// region Collection helpers + +/** + * Sets all the values from the provided map into the current one. + * + * @see MutableMap.putAll Equivalent method for regular maps. + */ +@ExperimentalWeakApi +fun WeakMap.setAll(from: Map) { + for ((k, v) in from) { + set(k, v) + } +} + +/** + * Removes the specified keys from this map. + */ +@ExperimentalWeakApi +fun WeakMap.removeAll(from: Iterable) { + for (k in from) { + remove(k) + } +} + +/** + * Removes the specified associations from this map. + */ +@ExperimentalWeakApi +fun WeakMap.removeAll(from: Map) { + for ((k, v) in from) { + remove(k, v) + } +} + +// endregion +// region Other extensions + +/** + * Removes the association of [key] to [value] from this map. + * + * If [key] is associated to another value than [value], nothing happens. + */ +@ExperimentalWeakApi +fun WeakMap.remove(key: K, value: V) { + if (get(key) == value) { + remove(key) + } +} + +// endregion diff --git a/weak/src/jsMain/kotlin/WeakMap.js.kt b/weak/src/jsMain/kotlin/WeakMap.js.kt new file mode 100644 index 0000000..b08ff70 --- /dev/null +++ b/weak/src/jsMain/kotlin/WeakMap.js.kt @@ -0,0 +1,72 @@ +package opensavvy.pedestal.weak + +import js.array.tupleOf + +private class JsWeakMap( + private val wrapped: js.collections.WeakMap +) : WeakMap { + override fun get(key: K): V? { + if (key == null) + return null + + return wrapped[key] + } + + override fun set(key: K, value: V) { + if (key == null) + return + + wrapped[key] = value + } + + @ExperimentalWeakApi + override fun contains(key: K): Boolean { + if (key == null) + return false + + return wrapped.has(key) + } + + @ExperimentalWeakApi + override fun remove(key: K): V? { + if (key == null) + return null + + val current = get(key) + wrapped.delete(key) + return current + } + + override fun toString(): String = + wrapped.toString() +} + +/** + * Instantiates a new, empty [WeakMap]. + * + * This implementation is backed by a JS [WeakMap][js.collections.WeakMap]. + */ +@ExperimentalWeakApi +actual fun WeakMap(): WeakMap = + JsWeakMap(js.collections.WeakMap()) + +/** + * Instantiates a new [WeakMap] by copying [values]. + * + * This implementation is backed by a JS [WeakMap][js.collections.WeakMap]. + */ +@ExperimentalWeakApi +actual fun WeakMap(values: Map): WeakMap { + val map = js.collections.WeakMap( + values + .mapNotNull { (k, v) -> + if (k == null) + null + else + tupleOf(k, v) + } + .toTypedArray() + ) + + return JsWeakMap(map) +} diff --git a/weak/src/jvmMain/kotlin/WeakMap.jvm.kt b/weak/src/jvmMain/kotlin/WeakMap.jvm.kt new file mode 100644 index 0000000..cb69d2e --- /dev/null +++ b/weak/src/jvmMain/kotlin/WeakMap.jvm.kt @@ -0,0 +1,44 @@ +package opensavvy.pedestal.weak + +import java.util.* + +private class JavaWeakMap( + private val wrapped: WeakHashMap +) : WeakMap { + + override fun get(key: K): V? = + wrapped[key] + + override fun set(key: K, value: V) { + wrapped[key] = value + } + + @ExperimentalWeakApi + override fun contains(key: K): Boolean = + wrapped.containsKey(key) + + @ExperimentalWeakApi + override fun remove(key: K): V? = + wrapped.remove(key) + + override fun toString(): String = + wrapped.toString() +} + +/** + * Instantiates a new, empty [WeakMap]. + * + * This implementation is backed by a Java [WeakHashMap]. + */ +@ExperimentalWeakApi +actual fun WeakMap(): WeakMap = + JavaWeakMap(WeakHashMap()) + +/** + * Instantiates a new [WeakMap] by copying [values]. + * + * This implementation is backed by a Java [WeakHashMap]. + */ +@ExperimentalWeakApi +actual fun WeakMap(values: Map): WeakMap = + JavaWeakMap(WeakHashMap(values)) diff --git a/weak/src/nativeMain/kotlin/WeakMap.native.kt b/weak/src/nativeMain/kotlin/WeakMap.native.kt new file mode 100644 index 0000000..3c1eca8 --- /dev/null +++ b/weak/src/nativeMain/kotlin/WeakMap.native.kt @@ -0,0 +1,11 @@ +package opensavvy.pedestal.weak + +@ExperimentalWeakApi +actual fun WeakMap(): WeakMap { + TODO("Not yet implemented") +} + +@ExperimentalWeakApi +actual fun WeakMap(values: Map): WeakMap { + TODO("Not yet implemented") +} -- 2.51.2 From 245640266ec90228c5589b5a93ce103a6bd7736d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20=E2=80=9CCLOVIS=E2=80=9D=20Canet?= Date: Sun, 8 Sep 2024 18:50:59 +0200 Subject: [PATCH 05/10] feat(weak): Create fake implementations of WeakRef and WeakMap --- .../kotlin/algorithms/EmptyWeakMap.kt | 32 ++++++++++++++++ .../kotlin/algorithms/EmptyWeakRef.kt | 28 ++++++++++++++ .../kotlin/algorithms/FakeWeakMap.kt | 38 +++++++++++++++++++ .../kotlin/algorithms/FakeWeakRef.kt | 26 +++++++++++++ 4 files changed, 124 insertions(+) create mode 100644 weak/src/commonMain/kotlin/algorithms/EmptyWeakMap.kt create mode 100644 weak/src/commonMain/kotlin/algorithms/EmptyWeakRef.kt create mode 100644 weak/src/commonMain/kotlin/algorithms/FakeWeakMap.kt create mode 100644 weak/src/commonMain/kotlin/algorithms/FakeWeakRef.kt diff --git a/weak/src/commonMain/kotlin/algorithms/EmptyWeakMap.kt b/weak/src/commonMain/kotlin/algorithms/EmptyWeakMap.kt new file mode 100644 index 0000000..ec28ae1 --- /dev/null +++ b/weak/src/commonMain/kotlin/algorithms/EmptyWeakMap.kt @@ -0,0 +1,32 @@ +package opensavvy.pedestal.weak.algorithms + +import opensavvy.pedestal.weak.ExperimentalWeakApi +import opensavvy.pedestal.weak.WeakMap + +private class EmptyWeakMapImpl : WeakMap { + override fun get(key: K): V? = null + override fun set(key: K, value: V) = Unit + + @ExperimentalWeakApi + override fun contains(key: K): Boolean = false + + @ExperimentalWeakApi + override fun remove(key: K): V? = null + + override fun toString(): String = "EmptyWeakMap" +} + +/** + * A [WeakMap] implementation that immediately frees its elements. + * + * Values passed to [set][WeakMap.set] are never stored, so the map is always empty. + * + * Use this implementation when testing algorithms that use a weak map, to ensure they don't rely on the value + * still existing. + * + * @see FakeWeakMap Opposite behavior: values are never freed. + */ +@Suppress("FunctionName") +@ExperimentalWeakApi +fun EmptyWeakMap(): WeakMap = + EmptyWeakMapImpl() diff --git a/weak/src/commonMain/kotlin/algorithms/EmptyWeakRef.kt b/weak/src/commonMain/kotlin/algorithms/EmptyWeakRef.kt new file mode 100644 index 0000000..1552eba --- /dev/null +++ b/weak/src/commonMain/kotlin/algorithms/EmptyWeakRef.kt @@ -0,0 +1,28 @@ +package opensavvy.pedestal.weak.algorithms + +import opensavvy.pedestal.weak.ExperimentalWeakApi +import opensavvy.pedestal.weak.WeakRef + +private object EmptyWeakRefImpl : WeakRef { + override fun read(): Nothing? = null + + override fun toString() = "EmptyWeakRef" +} + +/** + * A [WeakRef] implementation that doesn't store any value. + * + * It acts as if there was a value, but it was immediately freed. + */ +@Suppress("FunctionName") +@ExperimentalWeakApi +fun EmptyWeakRef(): WeakRef = + EmptyWeakRefImpl + +/** + * A [WeakRef] implementation that immediately frees its [value]. + */ +@Suppress("UNUSED_PARAMETER", "FunctionName") +@ExperimentalWeakApi +fun EmptyWeakRef(value: T): WeakRef = + EmptyWeakRefImpl diff --git a/weak/src/commonMain/kotlin/algorithms/FakeWeakMap.kt b/weak/src/commonMain/kotlin/algorithms/FakeWeakMap.kt new file mode 100644 index 0000000..b931078 --- /dev/null +++ b/weak/src/commonMain/kotlin/algorithms/FakeWeakMap.kt @@ -0,0 +1,38 @@ +package opensavvy.pedestal.weak.algorithms + +import opensavvy.pedestal.weak.ExperimentalWeakApi +import opensavvy.pedestal.weak.WeakMap + +private class FakeWeakMapImpl : WeakMap { + private val map = LinkedHashMap() + + override fun get(key: K): V? = + map[key] + + override fun set(key: K, value: V) { + map[key] = value + } + + @ExperimentalWeakApi + override fun contains(key: K): Boolean = + map.containsKey(key) + + @ExperimentalWeakApi + override fun remove(key: K): V? = + map.remove(key) + + override fun toString() = "FakeWeakMap" +} + +/** + * A [WeakMap] implementation that isn't weak. + * + * That is, all stored elements are strongly held and are never freed automatically. + * Elements are only removed when [WeakMap.remove] is called by the user. + * + * @see EmptyWeakMap Opposite behavior: values are immediately freed. + */ +@Suppress("FunctionName") +@ExperimentalWeakApi +fun FakeWeakMap(): WeakMap = + FakeWeakMapImpl() diff --git a/weak/src/commonMain/kotlin/algorithms/FakeWeakRef.kt b/weak/src/commonMain/kotlin/algorithms/FakeWeakRef.kt new file mode 100644 index 0000000..006c813 --- /dev/null +++ b/weak/src/commonMain/kotlin/algorithms/FakeWeakRef.kt @@ -0,0 +1,26 @@ +package opensavvy.pedestal.weak.algorithms + +import opensavvy.pedestal.weak.ExperimentalWeakApi +import opensavvy.pedestal.weak.WeakRef + +/** + * Fake implementation of [WeakRef]. + * + * Instead of being freed by the garbage-collector, this implementation is only + * freed when [clear] is called. + * + * Use this implementation to help trigger edge cases in algorithms that use weak references. + */ +@ExperimentalWeakApi +class FakeWeakRef( + value: T +) : WeakRef { + private var value: T? = value + + override fun read(): T? = + value + + fun clear() { + value = null + } +} -- 2.51.2 From 40672738e27f06ce7e9d358af3714a6700627cca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20=E2=80=9CCLOVIS=E2=80=9D=20Canet?= Date: Sun, 8 Sep 2024 19:16:48 +0200 Subject: [PATCH 06/10] feat(weak): Allow null values in WeakRef --- weak/src/commonMain/kotlin/WeakRef.kt | 11 ++++++++--- weak/src/commonMain/kotlin/algorithms/FakeWeakRef.kt | 2 +- weak/src/jsMain/kotlin/WeakRef.js.kt | 11 +++++++---- weak/src/jvmMain/kotlin/WeakRef.jvm.kt | 6 +++--- weak/src/nativeMain/kotlin/WeakRef.native.kt | 10 ++++++---- 5 files changed, 25 insertions(+), 15 deletions(-) diff --git a/weak/src/commonMain/kotlin/WeakRef.kt b/weak/src/commonMain/kotlin/WeakRef.kt index 8782914..bad16d5 100644 --- a/weak/src/commonMain/kotlin/WeakRef.kt +++ b/weak/src/commonMain/kotlin/WeakRef.kt @@ -7,6 +7,11 @@ package opensavvy.pedestal.weak * **at any time**. Garbage collectors are complex machinery. One should take care _not_ to write code that depends * on the behavior of a specific garbage collector, as that behavior can change between future versions. * + * Storing `null` is technically allowed for convenience reasons, but doesn't make much sense. + * It is not possible to distinguish between a weak reference that stores `null` and one that used store a value + * but has been cleared. + * Therefore, implementations are free to not store `null` values at all. + * * ### Obtain instances * * The default implementations are available via the top-level [WeakRef] and [SoftRef] functions. @@ -16,7 +21,7 @@ package opensavvy.pedestal.weak * * This interface is not stable for inheritance. We may add new methods at any time. */ -interface WeakRef { +interface WeakRef { /** * Attempts to read the value held by this reference. @@ -57,7 +62,7 @@ interface WeakRef { * @see SoftRef For implementing caches */ @ExperimentalWeakApi -expect fun WeakRef(value: T): WeakRef +expect fun WeakRef(value: T): WeakRef /** * Instantiates a soft reference: a reference to a [value] that doesn't stop @@ -80,4 +85,4 @@ expect fun WeakRef(value: T): WeakRef */ @ExperimentalWeakApi @Suppress("FunctionName") -expect fun SoftRef(value: T): WeakRef +expect fun SoftRef(value: T): WeakRef diff --git a/weak/src/commonMain/kotlin/algorithms/FakeWeakRef.kt b/weak/src/commonMain/kotlin/algorithms/FakeWeakRef.kt index 006c813..495e4d6 100644 --- a/weak/src/commonMain/kotlin/algorithms/FakeWeakRef.kt +++ b/weak/src/commonMain/kotlin/algorithms/FakeWeakRef.kt @@ -12,7 +12,7 @@ import opensavvy.pedestal.weak.WeakRef * Use this implementation to help trigger edge cases in algorithms that use weak references. */ @ExperimentalWeakApi -class FakeWeakRef( +class FakeWeakRef( value: T ) : WeakRef { private var value: T? = value diff --git a/weak/src/jsMain/kotlin/WeakRef.js.kt b/weak/src/jsMain/kotlin/WeakRef.js.kt index 5a6e536..873ab07 100644 --- a/weak/src/jsMain/kotlin/WeakRef.js.kt +++ b/weak/src/jsMain/kotlin/WeakRef.js.kt @@ -1,5 +1,7 @@ package opensavvy.pedestal.weak +import opensavvy.pedestal.weak.algorithms.EmptyWeakRef + private class JsWeakRef( value: T ) : WeakRef { @@ -20,8 +22,9 @@ private class JsWeakRef( * JS doesn't make a difference between weak and soft references. */ @ExperimentalWeakApi -actual fun WeakRef(value: T): WeakRef = - JsWeakRef(value) +actual fun WeakRef(value: T): WeakRef = + if (value == null) EmptyWeakRef(value) + else JsWeakRef(value) /** * Implementation of [WeakRef] backed by a JS [WeakRef][js.memory.WeakRef]. @@ -30,5 +33,5 @@ actual fun WeakRef(value: T): WeakRef = */ @ExperimentalWeakApi @Suppress("FunctionName") -actual fun SoftRef(value: T): WeakRef = - JsWeakRef(value) +actual fun SoftRef(value: T): WeakRef = + WeakRef(value) diff --git a/weak/src/jvmMain/kotlin/WeakRef.jvm.kt b/weak/src/jvmMain/kotlin/WeakRef.jvm.kt index 910c6b3..9c02cde 100644 --- a/weak/src/jvmMain/kotlin/WeakRef.jvm.kt +++ b/weak/src/jvmMain/kotlin/WeakRef.jvm.kt @@ -4,7 +4,7 @@ import java.lang.ref.Reference import java.lang.ref.SoftReference import java.lang.ref.WeakReference -private class JavaReferenceHolder( +private class JavaReferenceHolder( private val reference: Reference, ) : WeakRef { @@ -25,7 +25,7 @@ fun WeakRef.Companion.fromJava(reference: Reference): WeakRef = * Implementation of [WeakRef] backed by a JVM [WeakReference]. */ @ExperimentalWeakApi -actual fun WeakRef(value: T): WeakRef = +actual fun WeakRef(value: T): WeakRef = JavaReferenceHolder(WeakReference(value)) /** @@ -33,5 +33,5 @@ actual fun WeakRef(value: T): WeakRef = */ @ExperimentalWeakApi @Suppress("FunctionName") -actual fun SoftRef(value: T): WeakRef = +actual fun SoftRef(value: T): WeakRef = JavaReferenceHolder(SoftReference(value)) diff --git a/weak/src/nativeMain/kotlin/WeakRef.native.kt b/weak/src/nativeMain/kotlin/WeakRef.native.kt index 7b452af..5101dd5 100644 --- a/weak/src/nativeMain/kotlin/WeakRef.native.kt +++ b/weak/src/nativeMain/kotlin/WeakRef.native.kt @@ -1,5 +1,6 @@ package opensavvy.pedestal.weak +import opensavvy.pedestal.weak.algorithms.EmptyWeakRef import kotlin.experimental.ExperimentalNativeApi import kotlin.native.ref.WeakReference @@ -22,8 +23,9 @@ private class NativeWeakRef( */ @ExperimentalWeakApi @ExperimentalNativeApi -actual fun WeakRef(value: T): WeakRef = - NativeWeakRef(value) +actual fun WeakRef(value: T): WeakRef = + if (value == null) EmptyWeakRef(value) + else NativeWeakRef(value) /** * Implementation of [WeakRef] backed by a native [WeakReference]. @@ -33,5 +35,5 @@ actual fun WeakRef(value: T): WeakRef = @ExperimentalWeakApi @ExperimentalNativeApi @Suppress("FunctionName") -actual fun SoftRef(value: T): WeakRef = - NativeWeakRef(value) +actual fun SoftRef(value: T): WeakRef = + WeakRef(value) -- 2.51.2 From 282637ed20843a9f4e41ecae2ce68b3693ede200 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20=E2=80=9CCLOVIS=E2=80=9D=20Canet?= Date: Sun, 8 Sep 2024 19:23:52 +0200 Subject: [PATCH 07/10] feat(weak): Allow null values in WeakMap --- weak/src/commonMain/kotlin/WeakMap.kt | 24 ++++++++++---------- weak/src/jsMain/kotlin/WeakMap.js.kt | 6 ++--- weak/src/jvmMain/kotlin/WeakMap.jvm.kt | 6 ++--- weak/src/nativeMain/kotlin/WeakMap.native.kt | 4 ++-- 4 files changed, 20 insertions(+), 20 deletions(-) diff --git a/weak/src/commonMain/kotlin/WeakMap.kt b/weak/src/commonMain/kotlin/WeakMap.kt index 0f79838..43dff0e 100644 --- a/weak/src/commonMain/kotlin/WeakMap.kt +++ b/weak/src/commonMain/kotlin/WeakMap.kt @@ -22,7 +22,7 @@ import kotlin.reflect.KProperty * The default implementation is available via the top-level [WeakMap] function. * Other implementations are available in the `algorithms` subpackage. */ -interface WeakMap { +interface WeakMap { /** * Gets the value associated with [key] in this map. @@ -84,7 +84,7 @@ interface WeakMap { * use the [Any.equals] function. */ @ExperimentalWeakApi -expect fun WeakMap(): WeakMap +expect fun WeakMap(): WeakMap /** * Instantiates a new [WeakMap] by copying [values]. @@ -101,7 +101,7 @@ expect fun WeakMap(): WeakMap * use the [Any.equals] function. */ @ExperimentalWeakApi -expect fun WeakMap(values: Map): WeakMap +expect fun WeakMap(values: Map): WeakMap // endregion // region getOrXXX helpers @@ -110,7 +110,7 @@ expect fun WeakMap(values: Map): WeakMap * Attempts to find the value associated with [key], returning [defaultValue] if none is found. */ @ExperimentalWeakApi -inline fun WeakMap.getOrDefault(key: K, defaultValue: V): V = +inline fun WeakMap.getOrDefault(key: K, defaultValue: V): V = get(key) ?: defaultValue /** @@ -119,7 +119,7 @@ inline fun WeakMap.getOrDefault(key: K, defaultValue: V): V = * @see Map.getOrElse Equivalent method for regular maps. */ @ExperimentalWeakApi -inline fun WeakMap.getOrElse(key: K, defaultValue: () -> V): V = +inline fun WeakMap.getOrElse(key: K, defaultValue: () -> V): V = get(key) ?: defaultValue() /** @@ -134,7 +134,7 @@ inline fun WeakMap.getOrElse(key: K, defaultValue: () -> V): * @see MutableMap.getOrPut Equivalent method for regular maps. */ @ExperimentalWeakApi -inline fun WeakMap.getOrPut(key: K, defaultValue: () -> V): V = +inline fun WeakMap.getOrPut(key: K, defaultValue: () -> V): V = get(key) ?: run { val new = defaultValue() set(key, new) @@ -162,7 +162,7 @@ inline fun WeakMap.getOrPut(key: K, defaultValue: () -> V): V * @see Map.getValue Equivalent method for regular maps. */ @ExperimentalWeakApi -inline operator fun WeakMap.getValue(thisRef: Any?, property: KProperty<*>): V? = +inline operator fun WeakMap.getValue(thisRef: Any?, property: KProperty<*>): V? = get(property.name) /** @@ -183,7 +183,7 @@ inline operator fun WeakMap.getValue(thisRef: Any?, propert * @see MutableMap.setValue Equivalent method for regular maps. */ @ExperimentalWeakApi -inline operator fun WeakMap.setValue(thisRef: Any?, property: KProperty<*>, value: V) { +inline operator fun WeakMap.setValue(thisRef: Any?, property: KProperty<*>, value: V) { set(property.name, value) } @@ -196,7 +196,7 @@ inline operator fun WeakMap.setValue(thisRef: Any?, propert * @see MutableMap.putAll Equivalent method for regular maps. */ @ExperimentalWeakApi -fun WeakMap.setAll(from: Map) { +fun WeakMap.setAll(from: Map) { for ((k, v) in from) { set(k, v) } @@ -206,7 +206,7 @@ fun WeakMap.setAll(from: Map) { * Removes the specified keys from this map. */ @ExperimentalWeakApi -fun WeakMap.removeAll(from: Iterable) { +fun WeakMap.removeAll(from: Iterable) { for (k in from) { remove(k) } @@ -216,7 +216,7 @@ fun WeakMap.removeAll(from: Iterable) { * Removes the specified associations from this map. */ @ExperimentalWeakApi -fun WeakMap.removeAll(from: Map) { +fun WeakMap.removeAll(from: Map) { for ((k, v) in from) { remove(k, v) } @@ -231,7 +231,7 @@ fun WeakMap.removeAll(from: Map) { * If [key] is associated to another value than [value], nothing happens. */ @ExperimentalWeakApi -fun WeakMap.remove(key: K, value: V) { +fun WeakMap.remove(key: K, value: V) { if (get(key) == value) { remove(key) } diff --git a/weak/src/jsMain/kotlin/WeakMap.js.kt b/weak/src/jsMain/kotlin/WeakMap.js.kt index b08ff70..89bc5ea 100644 --- a/weak/src/jsMain/kotlin/WeakMap.js.kt +++ b/weak/src/jsMain/kotlin/WeakMap.js.kt @@ -2,7 +2,7 @@ package opensavvy.pedestal.weak import js.array.tupleOf -private class JsWeakMap( +private class JsWeakMap( private val wrapped: js.collections.WeakMap ) : WeakMap { override fun get(key: K): V? { @@ -47,7 +47,7 @@ private class JsWeakMap( * This implementation is backed by a JS [WeakMap][js.collections.WeakMap]. */ @ExperimentalWeakApi -actual fun WeakMap(): WeakMap = +actual fun WeakMap(): WeakMap = JsWeakMap(js.collections.WeakMap()) /** @@ -56,7 +56,7 @@ actual fun WeakMap(): WeakMap = * This implementation is backed by a JS [WeakMap][js.collections.WeakMap]. */ @ExperimentalWeakApi -actual fun WeakMap(values: Map): WeakMap { +actual fun WeakMap(values: Map): WeakMap { val map = js.collections.WeakMap( values .mapNotNull { (k, v) -> diff --git a/weak/src/jvmMain/kotlin/WeakMap.jvm.kt b/weak/src/jvmMain/kotlin/WeakMap.jvm.kt index cb69d2e..1f6387d 100644 --- a/weak/src/jvmMain/kotlin/WeakMap.jvm.kt +++ b/weak/src/jvmMain/kotlin/WeakMap.jvm.kt @@ -2,7 +2,7 @@ package opensavvy.pedestal.weak import java.util.* -private class JavaWeakMap( +private class JavaWeakMap( private val wrapped: WeakHashMap ) : WeakMap { @@ -31,7 +31,7 @@ private class JavaWeakMap( * This implementation is backed by a Java [WeakHashMap]. */ @ExperimentalWeakApi -actual fun WeakMap(): WeakMap = +actual fun WeakMap(): WeakMap = JavaWeakMap(WeakHashMap()) /** @@ -40,5 +40,5 @@ actual fun WeakMap(): WeakMap = * This implementation is backed by a Java [WeakHashMap]. */ @ExperimentalWeakApi -actual fun WeakMap(values: Map): WeakMap = +actual fun WeakMap(values: Map): WeakMap = JavaWeakMap(WeakHashMap(values)) diff --git a/weak/src/nativeMain/kotlin/WeakMap.native.kt b/weak/src/nativeMain/kotlin/WeakMap.native.kt index 3c1eca8..1046cbc 100644 --- a/weak/src/nativeMain/kotlin/WeakMap.native.kt +++ b/weak/src/nativeMain/kotlin/WeakMap.native.kt @@ -1,11 +1,11 @@ package opensavvy.pedestal.weak @ExperimentalWeakApi -actual fun WeakMap(): WeakMap { +actual fun WeakMap(): WeakMap { TODO("Not yet implemented") } @ExperimentalWeakApi -actual fun WeakMap(values: Map): WeakMap { +actual fun WeakMap(values: Map): WeakMap { TODO("Not yet implemented") } -- 2.51.2 From 363a2a5c860f57b763608fbf849139a868e4623a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20=E2=80=9CCLOVIS=E2=80=9D=20Canet?= Date: Sun, 8 Sep 2024 21:14:29 +0200 Subject: [PATCH 08/10] feat(weak): Create WeakKeyArrayMap --- .../kotlin/algorithms/WeakKeyArrayMap.kt | 195 ++++++++++++++++++ .../kotlin/algorithms/WeakKeyArrayMapTest.kt | 163 +++++++++++++++ weak/src/nativeMain/kotlin/WeakMap.native.kt | 23 ++- 3 files changed, 375 insertions(+), 6 deletions(-) create mode 100644 weak/src/commonMain/kotlin/algorithms/WeakKeyArrayMap.kt create mode 100644 weak/src/commonTest/kotlin/algorithms/WeakKeyArrayMapTest.kt diff --git a/weak/src/commonMain/kotlin/algorithms/WeakKeyArrayMap.kt b/weak/src/commonMain/kotlin/algorithms/WeakKeyArrayMap.kt new file mode 100644 index 0000000..af77ebd --- /dev/null +++ b/weak/src/commonMain/kotlin/algorithms/WeakKeyArrayMap.kt @@ -0,0 +1,195 @@ +package opensavvy.pedestal.weak.algorithms + +import opensavvy.pedestal.weak.ExperimentalWeakApi +import opensavvy.pedestal.weak.SoftRef +import opensavvy.pedestal.weak.WeakMap +import opensavvy.pedestal.weak.WeakRef + +@ExperimentalWeakApi +internal class WeakKeyMapImpl( + private val keyGenerator: (K) -> WeakRef, + private val keysAreTheSame: (K, K) -> Boolean +) : WeakMap { + + private val data = ArrayList>() + + override fun get(key: K): V? { + val iter = data.iterator() + + while (iter.hasNext()) { + val node = iter.next() + val storedKey = node.key.read() + + if (storedKey == null) { + // This is a weak reference that has been cleared by the GC, remove it from the list. + iter.remove() + } else if (keysAreTheSame(storedKey, key)) { + // This is the key we're searching for! + return node.value + } // else: not the key we're searching for, continue looping + } + + return null + } + + override fun set(key: K, value: V) { + remove(key) + data.add(WeakKeyMapNode(keyGenerator(key), value)) + } + + @ExperimentalWeakApi + override fun contains(key: K): Boolean = + get(key) != null + + @ExperimentalWeakApi + override fun remove(key: K): V? { + val iter = data.iterator() + + while (iter.hasNext()) { + val node = iter.next() + + if (node.key.read() == key) { + iter.remove() + return node.value + } + } + + return null + } + + override fun toString(): String = buildString { + append("WeakKeyArrayMap {") + + val iter = data.iterator() + + while (iter.hasNext()) { + val node = iter.next() + val key = node.key.read() + + if (key == null) { + iter.remove() + } else { + append(key) + append(" = ") + append(node.value) + append(", ") + } + } + + if (data.isNotEmpty()) + deleteRange(length - 2, length) + + append("}") + } +} + +private data class WeakKeyMapNode( + val key: WeakRef, + val value: V +) + +private val compareByEquality = { a: Any?, b: Any? -> a == b } +private val compareByIdentity = { a: Any?, b: Any? -> a === b } + +/** + * A pure-Kotlin common [WeakMap] implementation using [WeakRef]. + * + * Keys are compared using [equals][Any.equals]. + * + * This implementation is ideal when caching requests that are relatively cheap + * or when the keys have short lifetimes: we prefer to free memory early. + * + * ### Performance characteristics + * + * Elements are stored in an [ArrayList]. + * On each operation, elements are scanned linearly until the + * element on which the operation is requested is found. + * Expired elements are freed when they are visited. + * This implies that all operations are in `O(n)`. + * + * Additionally, older elements are visited more often, so they are more likely to be freed early. + */ +@ExperimentalWeakApi +fun WeakKeyArrayMap(): WeakMap = + WeakKeyMapImpl( + keyGenerator = { WeakRef(it) }, + keysAreTheSame = compareByEquality + ) + +/** + * A pure-Kotlin common [WeakMap] implementation using [SoftRef]. + * + * Keys are compared using [equals][Any.equals]. + * + * This implementation is ideal when caching requests that are relatively expensive + * or when the same keys are requested throughout the app's lifetime: + * we prefer to keep the cached value longer as long as there is memory available. + * + * ### Performance characteristics + * + * Elements are stored in an [ArrayList]. + * On each operation, elements are scanned linearly until the + * element on which the operation is requested is found. + * Expired elements are freed when they are visited. + * This implies that all operations are in `O(n)`. + * + * Additionally, older elements are visited more often, so they are more likely to be freed early. + */ +@ExperimentalWeakApi +fun SoftKeyArrayMap(): WeakMap = + WeakKeyMapImpl( + keyGenerator = { SoftRef(it) }, + keysAreTheSame = compareByEquality + ) + +/** + * A pure-Kotlin common [WeakMap] implementation using [WeakRef]. + * + * Keys are compared by identity. + * + * This implementation is ideal when storing mappings from large complex objects to other large objects. + * + * ### Performance characteristics + * + * Elements are stored in an [ArrayList]. + * On each operation, elements are scanned linearly until the + * element on which the operation is requested is found. + * Expired elements are freed when they are visited. + * This implies that all operations are in `O(n)`. + * + * Additionally, older elements are visited more often, so they are more likely to be freed early. + */ +@ExperimentalWeakApi +fun IdentityWeakKeyArrayMap(): WeakMap = + WeakKeyMapImpl( + keyGenerator = { WeakRef(it) }, + keysAreTheSame = compareByIdentity + ) + +/** + * A pure-Kotlin common [WeakMap] implementation using [SoftRef]. + * + * Keys are compared by identity. + * + * This implementation is ideal when storing mappings from specific long-lived objects. + * + * Do not use this implementation with short-lived objects! It will waste memory, as the cached + * values will be retained longer than necessary in case the same keys appears again, + * but this cannot happen since another key with the same identity cannot be created. + * + * ### Performance characteristics + * + * Elements are stored in an [ArrayList]. + * On each operation, elements are scanned linearly until the + * element on which the operation is requested is found. + * Expired elements are freed when they are visited. + * This implies that all operations are in `O(n)`. + * + * Additionally, older elements are visited more often, so they are more likely to be freed early. + */ +@ExperimentalWeakApi +fun IdentitySoftKeyArrayMap(): WeakMap = + WeakKeyMapImpl( + keyGenerator = { SoftRef(it) }, + keysAreTheSame = compareByIdentity + ) diff --git a/weak/src/commonTest/kotlin/algorithms/WeakKeyArrayMapTest.kt b/weak/src/commonTest/kotlin/algorithms/WeakKeyArrayMapTest.kt new file mode 100644 index 0000000..129c369 --- /dev/null +++ b/weak/src/commonTest/kotlin/algorithms/WeakKeyArrayMapTest.kt @@ -0,0 +1,163 @@ +package opensavvy.pedestal.weak.algorithms + +import opensavvy.pedestal.weak.ExperimentalWeakApi +import opensavvy.prepared.runner.kotest.PreparedSpec +import opensavvy.prepared.suite.* + +@Suppress("NAME_SHADOWING") +@OptIn(ExperimentalWeakApi::class) +class WeakKeyArrayMapTest : PreparedSpec({ + + // region Helpers to fake weak references used as keys during these tests + + val keys: Prepared>> by prepared { + HashMap() + } + + suspend fun TestDsl.key(key: Int): FakeWeakRef = + checkNotNull(keys()[key]) + + // endregion + + val map: Prepared> by prepared { + val keys = keys() + + WeakKeyMapImpl( + keyGenerator = { key -> + FakeWeakRef(key) + .also { keys[key] = it } + }, + keysAreTheSame = { a, b -> a == b } + ) + } + + suite("A single element is stored") { + test("Values should be stored: get") { + val map = map() + + map[5] = "5" + check(map[5] == "5") + } + + test("Values should be stored: contains") { + val map = map() + + map[5] = "5" + check(5 in map) + } + + test("Setting the value again overwrites it") { + val map = map() + + map[5] = "SHOULD BE OVERWRITTEN" + map[5] = "EXPECTED VALUE" + check(map[5] == "EXPECTED VALUE") + } + + test("Cannot get an element that isn't part of the map") { + val map = map() + + map[5] = "5" + check(map[6] == null) + } + + test("An element that isn't part of the map isn't contained") { + val map = map() + + map[5] = "5" + check(6 !in map) + } + } + + suite("A single element was stored") { + test("Getting a manually-deleted value should return null") { + val map = map() + + map[5] = "5" + map.remove(5) + check(map[5] == null) + } + + test("A manually-deleted value should not be contained") { + val map = map() + + map[5] = "5" + map.remove(5) + check(5 !in map) + } + + test("Getting a GC-deleted value should return null") { + val map = map() + + map[5] = "5" + key(5).clear() + check(map[5] == null) + } + + test("A GC-deleted value should not be contained") { + val map = map() + + map[5] = "5" + key(5).clear() + check(5 !in map) + } + } + + suite("Brute-forcing complex situations") { + // This is a test generator to create weird situations. + // If this fails, extract a single test that tests that specific situation. + + /** + * How large will the map-under-test be? + */ + val mapSize by randomInt(2, 100) + + /** + * The key that will be used for the assertion. + */ + val testKey by randomInt(0, 1_000) + + /** + * Other keys that will be added to the map. + */ + val generatedKeys by prepared { + List(mapSize()) { random.nextInt(0, 1_000) } + } + + /** + * Adds many keys to the map. + */ + val addKeys by prepared { + val map = map() + + for (key in generatedKeys()) { + map[key] = key.toString() + } + } + + /** + * Deletes ~50% of the added keys. + */ + val deleteSomeKeys by prepared { + for (key in generatedKeys()) { + if (key != testKey() && random.nextBoolean()) { + println("Deleting key $key") + key(key).clear() + } + } + } + + suite("Get a stored value") { + repeat(100) { + test("Test #$it") { + val map = map() + addKeys() + map[testKey()] = "TEST" + deleteSomeKeys() + check(map[testKey()] == "TEST") + } + } + } + } + +}) diff --git a/weak/src/nativeMain/kotlin/WeakMap.native.kt b/weak/src/nativeMain/kotlin/WeakMap.native.kt index 1046cbc..924ed21 100644 --- a/weak/src/nativeMain/kotlin/WeakMap.native.kt +++ b/weak/src/nativeMain/kotlin/WeakMap.native.kt @@ -1,11 +1,22 @@ package opensavvy.pedestal.weak +import opensavvy.pedestal.weak.algorithms.WeakKeyArrayMap + +/** + * Instantiates a new, empty [WeakMap]. + * + * This implementation uses [WeakKeyArrayMap]. + */ @ExperimentalWeakApi -actual fun WeakMap(): WeakMap { - TODO("Not yet implemented") -} +actual fun WeakMap(): WeakMap = + WeakKeyArrayMap() +/** + * Instantiates a new [WeakMap] by copying [values]. + * + * This implementation uses [WeakKeyArrayMap]. + */ @ExperimentalWeakApi -actual fun WeakMap(values: Map): WeakMap { - TODO("Not yet implemented") -} +actual fun WeakMap(values: Map): WeakMap = + WeakKeyArrayMap() + .also { it.setAll(values) } -- 2.51.2 From af5c9f317af6fe4705cb115bc45ce466576b1928 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20=E2=80=9CCLOVIS=E2=80=9D=20Canet?= Date: Sun, 8 Sep 2024 21:19:45 +0200 Subject: [PATCH 09/10] docs: Add all module labels to the CONTRIBUTING.md file --- CONTRIBUTING.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0b5fe06..31ec424 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -75,3 +75,17 @@ These labels should only be applied to issues, not merge requests. + +
+Library modules + +We do not always use these labels. When in doubt, write a comment asking for precision. +These labels should only be applied to issues, not merge requests. + +- ~progress: Issues for the `:progress` library and its compat modules. +- ~state: Issues for the `:state` library and its compat modules. +- ~cache: Issues for the `:cache` library and its compat modules. +- ~backbone: Issues for the `:backbone` library. +- ~weak: Issues for the `:weak` library and its compat modules. + +
-- 2.51.2 From 1a1b895c41a528d30527c51a06488272051238f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20=E2=80=9CCLOVIS=E2=80=9D=20Canet?= Date: Sun, 8 Sep 2024 21:45:40 +0200 Subject: [PATCH 10/10] docs(weak): Add a module header --- build.gradle.kts | 1 + weak/README.md | 37 +++++++++++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+) diff --git a/build.gradle.kts b/build.gradle.kts index 4f0324b..934129f 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -30,6 +30,7 @@ dependencies { dokkatoo(projects.state) dokkatoo(projects.stateArrow) dokkatoo(projects.stateCoroutines) + dokkatoo(projects.weak) kover(projects.backbone) kover(projects.cache) diff --git a/weak/README.md b/weak/README.md index 74cbdfe..c15e1c5 100644 --- a/weak/README.md +++ b/weak/README.md @@ -5,3 +5,40 @@ Weak references and maps for Kotlin Multiplatform. + +## Introduction + +When we declare a variable in Kotlin, we create a **strong reference** on an object: +```kotlin +class User(val email: String, val age: Int) +val george = User("george@mail.net", 18) +``` +The object referred to by the variable `george` must be kept in memory until the variable `george` goes out of scope. + +Sometimes, however, we can accept that an object disappears even before we stop referring to it. For example, this may be needed to avoid caches growing out to be larger than the available memory. + +Most platforms provide ways to hint to a runtime that an object may be freed even if it is still in use. +This library provides a unified way to give these hints to the runtime. + +## Weak references + +Weak references hint to the runtime that a value may be freed even if it is in use. +This library provides two main implementations: +- [`WeakRef`][opensavvy.pedestal.weak.WeakRef] hints that a value may be freed as soon as it isn't used elsewhere, +- [`SoftRef`][opensavvy.pedestal.weak.SoftRef] hints that a value will likely to re-used in the future even if it doesn't seem to be used at a specific instant, but may still be removed if the system lacks memory. + +## Weak maps + +Weak maps are used to create associations between two values that may be freed by the runtime. +Essentially, weak maps are maps that "forget" values. +Unlike regular maps, they are not iterable, to ensure algorithms cannot depend on their internal state. + +The main implementation is [`WeakMap`][opensavvy.pedestal.weak.WeakMap], which weakly holds its keys. + +# Package opensavvy.pedestal.weak + +Primitive weak data structures and their default (platform-specific) implementation. + +# Package opensavvy.pedestal.weak.algorithms + +Additional pure-Kotlin implementations that are useful when no platform-specific implementation is available, when the exact same behavior must be provided on all platforms, or to help with testing. -- 2.51.2