From cbd73b73bbd00712943a5223dd1684c7be275add Mon Sep 17 00:00:00 2001 From: CharlieTap Date: Sun, 17 Dec 2023 20:04:47 +0000 Subject: [PATCH] Native support (#11) Support for macos, ios, linux targets across x86 and ARM --- README.md | 8 + benchmark/build.gradle.kts | 3 + .../cachemap/benchmark/BenchmarkConfig.kt | 0 .../CacheMapMultiThreadedBenchmark.kt | 4 + .../CacheMapSingleThreadBenchmark.kt | 88 ++++++++ .../RWHashMapMultiThreadedBenchmark.kt | 4 + .../RWHashMapSingleThreadedBenchmark.kt | 108 ++++++++++ cachemap-suspend/build.gradle.kts | 96 +-------- .../cachemap/InternalSuspendCacheMap.kt | 12 +- cachemap/build.gradle.kts | 96 +-------- .../charlietap/cachemap/InternalCacheMap.kt | 12 +- gradle.properties | 1 + gradle/libs.versions.toml | 14 +- .../plugins/kmp-conventions/build.gradle.kts | 21 ++ .../kmp-conventions/settings.gradle.kts | 9 + .../main/kotlin/kmp-conventions.gradle.kts | 29 +++ .../publishing-conventions/build.gradle.kts | 31 +++ .../settings.gradle.kts | 9 + .../kotlin/PublishingConventionsExtension.kt | 6 + .../kotlin/PublishingConventionsPlugin.kt | 118 +++++++++++ leftright-shared/build.gradle.kts | 114 ++-------- .../leftright/CacheAlignedCounter.kt | 10 + .../charlietap/leftright/ReadEpochIndex.kt | 7 + .../charlietap/leftright/ThreadLocal.kt | 7 - .../src/ffi/cinterop/libcounter.def | 31 +++ .../leftright/CacheAlignedCounter.kt} | 12 +- .../charlietap/leftright/ReadEpochIndex.kt | 8 + .../charlietap/leftright/ThreadLocal.kt | 12 -- .../leftright/CacheAlignedCounter.kt | 33 +++ .../charlietap/leftright/CoreProvider.kt | 8 + .../charlietap/leftright/ReadEpochIndex.kt | 22 ++ .../io/github/charlietap/leftright/Yield.kt | 7 + leftright-suspend/build.gradle.kts | 96 +-------- .../charlietap/leftright/SuspendLeftRight.kt | 16 +- .../leftright/SuspendLeftRightTest.kt | 20 +- leftright/build.gradle.kts | 97 +-------- .../github/charlietap/leftright/LeftRight.kt | 21 +- .../charlietap/leftright/LeftRightTest.kt | 16 +- .../charlietap/leftright/LeftRightTest.kt | 10 +- .../charlietap/leftright/LeftRightTest.kt | 194 ++++++++++++++++++ settings.gradle.kts | 3 + 41 files changed, 876 insertions(+), 537 deletions(-) rename benchmark/src/{jvmMain => commonMain}/kotlin/io/github/charlietap/cachemap/benchmark/BenchmarkConfig.kt (100%) create mode 100644 benchmark/src/nativeMain/kotlin/io/github/charlietap/cachemap/benchmark/CacheMapMultiThreadedBenchmark.kt create mode 100644 benchmark/src/nativeMain/kotlin/io/github/charlietap/cachemap/benchmark/CacheMapSingleThreadBenchmark.kt create mode 100644 benchmark/src/nativeMain/kotlin/io/github/charlietap/cachemap/benchmark/RWHashMapMultiThreadedBenchmark.kt create mode 100644 benchmark/src/nativeMain/kotlin/io/github/charlietap/cachemap/benchmark/RWHashMapSingleThreadedBenchmark.kt create mode 100644 gradle/plugins/kmp-conventions/build.gradle.kts create mode 100644 gradle/plugins/kmp-conventions/settings.gradle.kts create mode 100644 gradle/plugins/kmp-conventions/src/main/kotlin/kmp-conventions.gradle.kts create mode 100644 gradle/plugins/publishing-conventions/build.gradle.kts create mode 100644 gradle/plugins/publishing-conventions/settings.gradle.kts create mode 100644 gradle/plugins/publishing-conventions/src/main/kotlin/PublishingConventionsExtension.kt create mode 100644 gradle/plugins/publishing-conventions/src/main/kotlin/PublishingConventionsPlugin.kt create mode 100644 leftright-shared/src/commonMain/kotlin/io/github/charlietap/leftright/CacheAlignedCounter.kt create mode 100644 leftright-shared/src/commonMain/kotlin/io/github/charlietap/leftright/ReadEpochIndex.kt delete mode 100644 leftright-shared/src/commonMain/kotlin/io/github/charlietap/leftright/ThreadLocal.kt create mode 100644 leftright-shared/src/ffi/cinterop/libcounter.def rename leftright-shared/src/{commonMain/kotlin/io/github/charlietap/leftright/PaddedVolatileInt.kt => jvmMain/kotlin/io/github/charlietap/leftright/CacheAlignedCounter.kt} (76%) create mode 100644 leftright-shared/src/jvmMain/kotlin/io/github/charlietap/leftright/ReadEpochIndex.kt delete mode 100644 leftright-shared/src/jvmMain/kotlin/io/github/charlietap/leftright/ThreadLocal.kt create mode 100644 leftright-shared/src/nativeMain/kotlin/io/github/charlietap/leftright/CacheAlignedCounter.kt create mode 100644 leftright-shared/src/nativeMain/kotlin/io/github/charlietap/leftright/CoreProvider.kt create mode 100644 leftright-shared/src/nativeMain/kotlin/io/github/charlietap/leftright/ReadEpochIndex.kt create mode 100644 leftright-shared/src/nativeMain/kotlin/io/github/charlietap/leftright/Yield.kt create mode 100644 leftright/src/nativeTest/kotlin/io/github/charlietap/leftright/LeftRightTest.kt diff --git a/README.md b/README.md index 4f0a684..295d6b1 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,10 @@ # cachemap +![badge][badge-android] ![badge][badge-jvm] +![badge][badge-ios] +![badge][badge-linux] +![badge][badge-mac] --- @@ -146,4 +150,8 @@ This project is dual-licensed under both the MIT and Apache 2.0 licenses. You ca - For details on the MIT license, please see the [LICENSE-MIT](LICENSE-MIT) file. - For details on the Apache 2.0 license, please see the [LICENSE-APACHE](LICENSE-APACHE) file. +[badge-android]: http://img.shields.io/badge/-android-6EDB8D.svg?style=flat [badge-jvm]: http://img.shields.io/badge/-jvm-DB413D.svg?style=flat +[badge-linux]: http://img.shields.io/badge/-linux-2D3F6C.svg?style=flat +[badge-ios]: http://img.shields.io/badge/-ios-CDCDCD.svg?style=flat +[badge-mac]: http://img.shields.io/badge/-macos-111111.svg?style=flat diff --git a/benchmark/build.gradle.kts b/benchmark/build.gradle.kts index 4c1d355..29ed3ab 100644 --- a/benchmark/build.gradle.kts +++ b/benchmark/build.gradle.kts @@ -11,17 +11,20 @@ plugins { allOpen { annotation("org.openjdk.jmh.annotations.State") + annotation("kotlinx.benchmark.State") } benchmark { targets { register("jvm") +// register("macosArm64") } } kotlin { jvm() + macosArm64() jvmToolchain { languageVersion.set(JavaLanguageVersion.of(libs.versions.java.compiler.version.get().toInt())) diff --git a/benchmark/src/jvmMain/kotlin/io/github/charlietap/cachemap/benchmark/BenchmarkConfig.kt b/benchmark/src/commonMain/kotlin/io/github/charlietap/cachemap/benchmark/BenchmarkConfig.kt similarity index 100% rename from benchmark/src/jvmMain/kotlin/io/github/charlietap/cachemap/benchmark/BenchmarkConfig.kt rename to benchmark/src/commonMain/kotlin/io/github/charlietap/cachemap/benchmark/BenchmarkConfig.kt diff --git a/benchmark/src/nativeMain/kotlin/io/github/charlietap/cachemap/benchmark/CacheMapMultiThreadedBenchmark.kt b/benchmark/src/nativeMain/kotlin/io/github/charlietap/cachemap/benchmark/CacheMapMultiThreadedBenchmark.kt new file mode 100644 index 0000000..44b3705 --- /dev/null +++ b/benchmark/src/nativeMain/kotlin/io/github/charlietap/cachemap/benchmark/CacheMapMultiThreadedBenchmark.kt @@ -0,0 +1,4 @@ +package io.github.charlietap.cachemap.benchmark + +// Todo enable threading +class CacheMapMultiThreadedBenchmark : CacheMapSingleThreadBenchmark() diff --git a/benchmark/src/nativeMain/kotlin/io/github/charlietap/cachemap/benchmark/CacheMapSingleThreadBenchmark.kt b/benchmark/src/nativeMain/kotlin/io/github/charlietap/cachemap/benchmark/CacheMapSingleThreadBenchmark.kt new file mode 100644 index 0000000..68445c8 --- /dev/null +++ b/benchmark/src/nativeMain/kotlin/io/github/charlietap/cachemap/benchmark/CacheMapSingleThreadBenchmark.kt @@ -0,0 +1,88 @@ +package io.github.charlietap.cachemap.benchmark + +import io.github.charlietap.cachemap.cacheMapOf +import kotlinx.benchmark.Benchmark +import kotlinx.benchmark.BenchmarkMode +import kotlinx.benchmark.BenchmarkTimeUnit +import kotlinx.benchmark.Blackhole +import kotlinx.benchmark.Measurement +import kotlinx.benchmark.Mode +import kotlinx.benchmark.OutputTimeUnit +import kotlinx.benchmark.Scope +import kotlinx.benchmark.Setup +import kotlinx.benchmark.State +import kotlinx.benchmark.TearDown +import kotlinx.benchmark.Warmup + +@State(Scope.Benchmark) +@BenchmarkMode(Mode.AverageTime, Mode.Throughput) +@OutputTimeUnit(BenchmarkTimeUnit.NANOSECONDS) +@Warmup(iterations = BenchmarkConfig.WARMUP_ITERATIONS) +@Measurement(iterations = BenchmarkConfig.MEASUREMENT_ITERATIONS, time = 1, timeUnit = BenchmarkTimeUnit.SECONDS) +class CacheMapSingleThreadBenchmark { + + private val cacheMap = cacheMapOf() + + @Setup() + fun setup() { + for (i in 1..1000) { + cacheMap.put("key$i", "value$i") + } + } + + @Benchmark + fun put(blackhole: Blackhole) { + val result = cacheMap.put("Hello", "World") + blackhole.consume(result) + } + + @Benchmark + fun overwrite(blackhole: Blackhole) { + val result = cacheMap.put("key1", "value2") + blackhole.consume(result) + } + + @Benchmark + fun putAll(blackhole: Blackhole) { + val anotherMap = mapOf("Hello" to "World", "SecondKey" to "SecondValue") + val result = cacheMap.putAll(anotherMap) + blackhole.consume(result) + } + + @Benchmark + fun get(blackhole: Blackhole) { + val result: String? = cacheMap["key1"] + blackhole.consume(result) + } + + @Benchmark + fun getMiss(blackhole: Blackhole) { + val result: String? = cacheMap["Hello"] + blackhole.consume(result) + } + + @Benchmark + fun remove(blackhole: Blackhole) { + val result = cacheMap.remove("key1") + blackhole.consume(result) + } + + @Benchmark + fun stressTest(blackhole: Blackhole) { + for (i in 1..1000) { + val putResult = cacheMap.put("newKey$i", "newValue$i") + blackhole.consume(putResult) + + val getResult: String? = cacheMap["key$i"] + blackhole.consume(getResult) + + val removeResult = cacheMap.remove("newKey$i") + blackhole.consume(removeResult) + } + } + + @TearDown() + fun tearDown() { + cacheMap.clear() + } +} diff --git a/benchmark/src/nativeMain/kotlin/io/github/charlietap/cachemap/benchmark/RWHashMapMultiThreadedBenchmark.kt b/benchmark/src/nativeMain/kotlin/io/github/charlietap/cachemap/benchmark/RWHashMapMultiThreadedBenchmark.kt new file mode 100644 index 0000000..543e5b1 --- /dev/null +++ b/benchmark/src/nativeMain/kotlin/io/github/charlietap/cachemap/benchmark/RWHashMapMultiThreadedBenchmark.kt @@ -0,0 +1,4 @@ +package io.github.charlietap.cachemap.benchmark + +// todo add threads +class RWHashMapMultiThreadedBenchmark : RWHashMapSingleThreadedBenchmark() diff --git a/benchmark/src/nativeMain/kotlin/io/github/charlietap/cachemap/benchmark/RWHashMapSingleThreadedBenchmark.kt b/benchmark/src/nativeMain/kotlin/io/github/charlietap/cachemap/benchmark/RWHashMapSingleThreadedBenchmark.kt new file mode 100644 index 0000000..9b01a1e --- /dev/null +++ b/benchmark/src/nativeMain/kotlin/io/github/charlietap/cachemap/benchmark/RWHashMapSingleThreadedBenchmark.kt @@ -0,0 +1,108 @@ +package io.github.charlietap.cachemap.benchmark + +import kotlinx.atomicfu.locks.ReentrantLock +import kotlinx.atomicfu.locks.withLock +import kotlinx.benchmark.Benchmark +import kotlinx.benchmark.BenchmarkMode +import kotlinx.benchmark.BenchmarkTimeUnit +import kotlinx.benchmark.Blackhole +import kotlinx.benchmark.Measurement +import kotlinx.benchmark.Mode +import kotlinx.benchmark.OutputTimeUnit +import kotlinx.benchmark.Scope +import kotlinx.benchmark.Setup +import kotlinx.benchmark.State +import kotlinx.benchmark.TearDown +import kotlinx.benchmark.Warmup + +@State(Scope.Benchmark) +@BenchmarkMode(Mode.AverageTime, Mode.Throughput) +@OutputTimeUnit(BenchmarkTimeUnit.NANOSECONDS) +@Warmup(iterations = BenchmarkConfig.WARMUP_ITERATIONS) +@Measurement(iterations = BenchmarkConfig.MEASUREMENT_ITERATIONS, time = 1, timeUnit = BenchmarkTimeUnit.SECONDS) +class RWHashMapSingleThreadedBenchmark { + + private val map = HashMap() + private val lock = ReentrantLock() + + @Setup() + fun setup() { + for (i in 1..1000) { + map["key$i"] = "value$i" + } + } + + @Benchmark + fun put(blackhole: Blackhole) { + val result = lock.withLock { + map.put("Hello", "World") + } + blackhole.consume(result) + } + + @Benchmark + fun overwrite(blackhole: Blackhole) { + val result = lock.withLock { + map.put("key1", "value2") + } + blackhole.consume(result) + } + + @Benchmark + fun putAll(blackhole: Blackhole) { + val anotherMap = mapOf("Hello" to "World", "SecondKey" to "SecondValue") + lock.withLock { + map.putAll(anotherMap) + } + blackhole.consume(anotherMap) + } + + @Benchmark + fun get(blackhole: Blackhole) { + val result: String? = lock.withLock { + map["key1"] + } + blackhole.consume(result) + } + + @Benchmark + fun getMiss(blackhole: Blackhole) { + val result: String? = lock.withLock { + map["Hello"] + } + blackhole.consume(result) + } + + @Benchmark + fun remove(blackhole: Blackhole) { + val result = lock.withLock { + map.remove("key1") + } + blackhole.consume(result) + } + + @Benchmark + fun stressTest(blackhole: Blackhole) { + for (i in 1..1000) { + val putResult = lock.withLock { + map.put("newKey$i", "newValue$i") + } + blackhole.consume(putResult) + + val getResult: String? = lock.withLock { + map["key$i"] + } + blackhole.consume(getResult) + + val removeResult = lock.withLock { + map.remove("newKey$i") + } + blackhole.consume(removeResult) + } + } + + @TearDown() + fun tearDown() { + map.clear() + } +} diff --git a/cachemap-suspend/build.gradle.kts b/cachemap-suspend/build.gradle.kts index fb1e1dc..043bf70 100644 --- a/cachemap-suspend/build.gradle.kts +++ b/cachemap-suspend/build.gradle.kts @@ -6,28 +6,12 @@ plugins { alias(libs.plugins.kotlin.serialization) alias(libs.plugins.kotlin.atomic.fu) alias(libs.plugins.kotlinter) - alias(libs.plugins.dokka) - id("maven-publish") - id("signing") + id("kmp-conventions") + id("publishing-conventions") } kotlin { - jvm() - - jvmToolchain { - languageVersion.set(JavaLanguageVersion.of(libs.versions.java.compiler.version.get().toInt())) - vendor.set(JvmVendorSpec.matching(libs.versions.java.vendor.get())) - } - - targets.configureEach { - compilations.configureEach { - kotlinOptions { - - } - } - } - sourceSets { commonMain { @@ -51,79 +35,9 @@ kotlin { } } -val dokkaHtml by tasks.getting(org.jetbrains.dokka.gradle.DokkaTask::class) - -val javadocJar: TaskProvider by tasks.registering(Jar::class) { - dependsOn(dokkaHtml) - archiveClassifier.set("javadoc") - from(dokkaHtml.outputDirectory) -} - -tasks.withType().configureEach { - val signingTasks = tasks.withType() - mustRunAfter(signingTasks) -} - -tasks.withType().configureEach { - notCompatibleWithConfigurationCache("https://github.com/Kotlin/dokka/issues/2231") -} - -group = "io.github.charlietap" -version = libs.versions.version.name.get() - -publishing { - - val manualFileRepo = uri("file://${rootProject.layout.buildDirectory.get()}/manual") - - repositories { - maven { - name = "manual" - url = manualFileRepo - } - } - - publications.withType().configureEach { - - artifact(javadocJar) - - pom { - name.set(project.name) - description.set("A read optimised suspending concurrent map for Kotlin Multiplatform") - url.set("https://github.com/CharlieTap/cachemap") - licenses { - license { - name.set("Apache-2.0") - url.set("https://www.apache.org/licenses/LICENSE-2.0") - } - license { - name.set("MIT") - url.set("https://opensource.org/licenses/MIT") - } - } - developers { - developer { - id.set("CharlieTap") - name.set("Charlie Tapping") - } - } - scm { - connection.set("scm:git:https://github.com/CharlieTap/cachemap.git") - developerConnection.set("scm:git:ssh://github.com/CharlieTap/cachemap.git") - url.set("https://github.com/CharlieTap/cachemap") - } - } - } -} - -signing { - val signingKey: String? by project - val signingPassword: String? by project - - if(signingKey != null) { - useInMemoryPgpKeys(signingKey, signingPassword) - } - - sign(project.extensions.getByType().publications) +configure { + name = "cachemap-suspend" + description = "A read optimised suspending concurrent map for Kotlin Multiplatform" } tasks.withType().configureEach { diff --git a/cachemap-suspend/src/commonMain/kotlin/io/github/charlietap/cachemap/InternalSuspendCacheMap.kt b/cachemap-suspend/src/commonMain/kotlin/io/github/charlietap/cachemap/InternalSuspendCacheMap.kt index a06f6f4..7bb5ab4 100644 --- a/cachemap-suspend/src/commonMain/kotlin/io/github/charlietap/cachemap/InternalSuspendCacheMap.kt +++ b/cachemap-suspend/src/commonMain/kotlin/io/github/charlietap/cachemap/InternalSuspendCacheMap.kt @@ -68,7 +68,13 @@ internal class InternalSuspendCacheMap( override suspend fun remove(key: K, value: V): Boolean { return inner.mutate { map -> - map.remove(key, value) + val mapValue = map[key] + if (value == mapValue) { + map.remove(key) + true + } else { + false + } } } @@ -87,8 +93,10 @@ internal class InternalSuspendCacheMap( val constructor = { if (capacity != null) { HashMap(capacity) - } else { + } else if (population != null) { HashMap(population) + } else { + HashMap() } } if (readerParallelism != null) { diff --git a/cachemap/build.gradle.kts b/cachemap/build.gradle.kts index deab742..d1b7a27 100644 --- a/cachemap/build.gradle.kts +++ b/cachemap/build.gradle.kts @@ -6,28 +6,12 @@ plugins { alias(libs.plugins.kotlin.serialization) alias(libs.plugins.kotlin.atomic.fu) alias(libs.plugins.kotlinter) - alias(libs.plugins.dokka) - id("maven-publish") - id("signing") + id("kmp-conventions") + id("publishing-conventions") } kotlin { - jvm() - - jvmToolchain { - languageVersion.set(JavaLanguageVersion.of(libs.versions.java.compiler.version.get().toInt())) - vendor.set(JvmVendorSpec.matching(libs.versions.java.vendor.get())) - } - - targets.configureEach { - compilations.configureEach { - kotlinOptions { - - } - } - } - sourceSets { commonMain { @@ -50,79 +34,9 @@ kotlin { } } -val dokkaHtml by tasks.getting(org.jetbrains.dokka.gradle.DokkaTask::class) - -val javadocJar: TaskProvider by tasks.registering(Jar::class) { - dependsOn(dokkaHtml) - archiveClassifier.set("javadoc") - from(dokkaHtml.outputDirectory) -} - -tasks.withType().configureEach { - val signingTasks = tasks.withType() - mustRunAfter(signingTasks) -} - -tasks.withType().configureEach { - notCompatibleWithConfigurationCache("https://github.com/Kotlin/dokka/issues/2231") -} - -publishing { - - val manualFileRepo = uri("file://${rootProject.layout.buildDirectory.get()}/manual") - - repositories { - maven { - name = "manual" - url = manualFileRepo - } - } - - publications.withType().configureEach { - - groupId = "io.github.charlietap" - version = libs.versions.version.name.get() - - artifact(javadocJar) - - pom { - name.set(project.name) - description.set("A read optimised concurrent map for Kotlin Multiplatform") - url.set("https://github.com/CharlieTap/cachemap") - licenses { - license { - name.set("Apache-2.0") - url.set("https://www.apache.org/licenses/LICENSE-2.0") - } - license { - name.set("MIT") - url.set("https://opensource.org/licenses/MIT") - } - } - developers { - developer { - id.set("CharlieTap") - name.set("Charlie Tapping") - } - } - scm { - connection.set("scm:git:https://github.com/CharlieTap/cachemap.git") - developerConnection.set("scm:git:ssh://github.com/CharlieTap/cachemap.git") - url.set("https://github.com/CharlieTap/cachemap") - } - } - } -} - -signing { - val signingKey: String? by project - val signingPassword: String? by project - - if(signingKey != null) { - useInMemoryPgpKeys(signingKey, signingPassword) - } - - sign(project.extensions.getByType().publications) +configure { + name = "cachemap" + description = "A read optimised concurrent map for Kotlin Multiplatform" } tasks.withType().configureEach { diff --git a/cachemap/src/commonMain/kotlin/io/github/charlietap/cachemap/InternalCacheMap.kt b/cachemap/src/commonMain/kotlin/io/github/charlietap/cachemap/InternalCacheMap.kt index 93c179e..d0c5a5f 100644 --- a/cachemap/src/commonMain/kotlin/io/github/charlietap/cachemap/InternalCacheMap.kt +++ b/cachemap/src/commonMain/kotlin/io/github/charlietap/cachemap/InternalCacheMap.kt @@ -68,7 +68,13 @@ internal class InternalCacheMap( override fun remove(key: K, value: V): Boolean { return inner.mutate { map -> - map.remove(key, value) + val mapValue = map[key] + if (value == mapValue) { + map.remove(key) + true + } else { + false + } } } @@ -93,8 +99,10 @@ internal class InternalCacheMap( val constructor = { if (capacity != null) { HashMap(capacity) - } else { + } else if (population != null) { HashMap(population) + } else { + HashMap() } } if (readerParallelism != null) { diff --git a/gradle.properties b/gradle.properties index e7ad5a8..2222849 100644 --- a/gradle.properties +++ b/gradle.properties @@ -5,3 +5,4 @@ org.gradle.jvmargs=-Xmx4096M -Dfile.encoding=UTF-8 kotlin.code.style=official kotlin.mpp.stability.nowarn=true +kotlin.mpp.enableCInteropCommonization=true diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 3d26b3a..4323ebe 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,6 +1,6 @@ [versions] -version-name = "0.2.2" +version-name = "0.2.4" java-compiler-version = "17" java-bytecode-version = "11" @@ -23,14 +23,13 @@ hlc = "1.1.0" junit = "4.13.2" -kotlin = "1.9.20" +kotlin = "1.9.21" kpoet = "1.14.2" -ksp = "1.9.20-1.0.13" kotlinter = "3.15.0" kot-compile-testing = "1.5.0" -kotlinx-atomic-fu = "0.21.0" -kotlinx-benchmark = "0.4.9" +kotlinx-atomic-fu = "0.23.1" +kotlinx-benchmark = "0.4.10" kotlinx-coroutines = "1.7.3" kotlinx-datetime = "0.4.0" kotlinx-serialization = "1.5.1" @@ -62,7 +61,6 @@ kotlin-kapt = { id = "org.jetbrains.kotlin.kapt", version.ref = "kotlin" } kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } kotlin-atomic-fu = { id = "org.jetbrains.kotlin.plugin.atomicfu", version.ref = "kotlin" } -kotlin-symbol-processing = { id = "com.google.devtools.ksp", version.ref = "ksp" } kotlinter = { id = "org.jmailen.kotlinter", version.ref = "kotlinter" } sqldelight = { id = "app.cash.sqldelight", version.ref = "sqldelight" } @@ -86,7 +84,6 @@ kotlin-junit = { module = "org.jetbrains.kotlin:kotlin-test-junit", version.ref kotlin-poet-core = { module = "com.squareup:kotlinpoet", version.ref = "kpoet"} kotlin-poet-ksp = { module = "com.squareup:kotlinpoet-ksp", version.ref = "kpoet"} kotlin-reflection = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref = "kotlin"} -kotlin-symbol-processing-api = { module = "com.google.devtools.ksp:symbol-processing-api", version.ref = "ksp"} kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin"} kotlinx-atomic-fu = { module = "org.jetbrains.kotlinx:atomicfu", version.ref = "kotlinx-atomic-fu"} @@ -115,9 +112,10 @@ sqldelight-jvm-driver = { module = "app.cash.sqldelight:sqlite-driver", version. sqldelight-native-driver = { module = "app.cash.sqldelight:native-driver", version.ref = "sqldelight" } uuid = { module = "com.benasher44:uuid", version.ref = "uuid"} +dokka-gradle-plugin = { module = "org.jetbrains.dokka:dokka-gradle-plugin", version.ref = "dokka" } gradle-versions-plugin = { module = "com.github.ben-manes:gradle-versions-plugin", version.ref = "versions-plugin" } kotlinter-plugin = { module = "org.jmailen.gradle:kotlinter-gradle", version.ref = "kotlinter" } - +kotlin-gradle-plugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" } [bundles] diff --git a/gradle/plugins/kmp-conventions/build.gradle.kts b/gradle/plugins/kmp-conventions/build.gradle.kts new file mode 100644 index 0000000..170af46 --- /dev/null +++ b/gradle/plugins/kmp-conventions/build.gradle.kts @@ -0,0 +1,21 @@ +plugins { + `kotlin-dsl` +} + +repositories { + mavenCentral() + gradlePluginPortal() + google() +} + +dependencies { + implementation(files(libs.javaClass.superclass.protectionDomain.codeSource.location)) + implementation(libs.kotlin.gradle.plugin) +} + +kotlin { + jvmToolchain { + languageVersion.set(JavaLanguageVersion.of(libs.versions.java.compiler.version.get().toInt())) + vendor.set(JvmVendorSpec.matching(libs.versions.java.vendor.get())) + } +} diff --git a/gradle/plugins/kmp-conventions/settings.gradle.kts b/gradle/plugins/kmp-conventions/settings.gradle.kts new file mode 100644 index 0000000..af9628a --- /dev/null +++ b/gradle/plugins/kmp-conventions/settings.gradle.kts @@ -0,0 +1,9 @@ +dependencyResolutionManagement { + versionCatalogs { + create("libs") { + from(files("../../libs.versions.toml")) + } + } +} + +rootProject.name = "kmp-conventions" diff --git a/gradle/plugins/kmp-conventions/src/main/kotlin/kmp-conventions.gradle.kts b/gradle/plugins/kmp-conventions/src/main/kotlin/kmp-conventions.gradle.kts new file mode 100644 index 0000000..475bd1f --- /dev/null +++ b/gradle/plugins/kmp-conventions/src/main/kotlin/kmp-conventions.gradle.kts @@ -0,0 +1,29 @@ +import org.gradle.accessors.dm.LibrariesForLibs +import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension + +plugins { + id("org.jetbrains.kotlin.multiplatform") +} + +val libs = the() + +fun KotlinMultiplatformExtension.nativeTargets() = setOf( + macosArm64(), + macosX64(), + iosArm64(), + iosSimulatorArm64(), + iosX64(), + linuxArm64(), + linuxX64(), +) + +kotlin { + + jvm() + nativeTargets() + + jvmToolchain { + languageVersion.set(JavaLanguageVersion.of(libs.versions.java.compiler.version.get().toInt())) + vendor.set(JvmVendorSpec.matching(libs.versions.java.vendor.get())) + } +} diff --git a/gradle/plugins/publishing-conventions/build.gradle.kts b/gradle/plugins/publishing-conventions/build.gradle.kts new file mode 100644 index 0000000..354c276 --- /dev/null +++ b/gradle/plugins/publishing-conventions/build.gradle.kts @@ -0,0 +1,31 @@ +plugins { + `kotlin-dsl` + `java-gradle-plugin` +} + +repositories { + mavenCentral() + gradlePluginPortal() + google() +} + +dependencies { + implementation(gradleApi()) + implementation(libs.dokka.gradle.plugin) +} + +kotlin { + jvmToolchain { + languageVersion.set(JavaLanguageVersion.of(libs.versions.java.compiler.version.get().toInt())) + vendor.set(JvmVendorSpec.matching(libs.versions.java.vendor.get())) + } +} + +gradlePlugin { + plugins { + create("publishingConventions") { + id = "publishing-conventions" + implementationClass = "PublishingConventionsPlugin" + } + } +} diff --git a/gradle/plugins/publishing-conventions/settings.gradle.kts b/gradle/plugins/publishing-conventions/settings.gradle.kts new file mode 100644 index 0000000..5979700 --- /dev/null +++ b/gradle/plugins/publishing-conventions/settings.gradle.kts @@ -0,0 +1,9 @@ +dependencyResolutionManagement { + versionCatalogs { + create("libs") { + from(files("../../libs.versions.toml")) + } + } +} + +rootProject.name = "publishing-conventions" diff --git a/gradle/plugins/publishing-conventions/src/main/kotlin/PublishingConventionsExtension.kt b/gradle/plugins/publishing-conventions/src/main/kotlin/PublishingConventionsExtension.kt new file mode 100644 index 0000000..5d32f27 --- /dev/null +++ b/gradle/plugins/publishing-conventions/src/main/kotlin/PublishingConventionsExtension.kt @@ -0,0 +1,6 @@ +import org.gradle.api.provider.Property + +interface PublishingConventionsExtension { + val name: Property + val description: Property +} diff --git a/gradle/plugins/publishing-conventions/src/main/kotlin/PublishingConventionsPlugin.kt b/gradle/plugins/publishing-conventions/src/main/kotlin/PublishingConventionsPlugin.kt new file mode 100644 index 0000000..3d0849c --- /dev/null +++ b/gradle/plugins/publishing-conventions/src/main/kotlin/PublishingConventionsPlugin.kt @@ -0,0 +1,118 @@ +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.api.artifacts.VersionCatalogsExtension +import org.gradle.api.publish.PublicationContainer +import org.gradle.api.publish.PublishingExtension +import org.gradle.api.publish.maven.MavenPublication +import org.gradle.api.publish.maven.plugins.MavenPublishPlugin +import org.gradle.api.publish.maven.tasks.AbstractPublishToMaven +import org.gradle.api.tasks.TaskProvider +import org.jetbrains.dokka.gradle.DokkaTask +import org.gradle.api.tasks.bundling.Jar +import org.gradle.kotlin.dsl.create +import org.gradle.kotlin.dsl.getByType +import org.gradle.kotlin.dsl.getValue +import org.gradle.kotlin.dsl.getting +import org.gradle.kotlin.dsl.provideDelegate +import org.gradle.kotlin.dsl.registering +import org.gradle.kotlin.dsl.withType +import org.gradle.plugins.signing.Sign +import org.gradle.plugins.signing.SigningExtension +import org.gradle.plugins.signing.SigningPlugin +import org.jetbrains.dokka.gradle.DokkaPlugin + +class PublishingConventionsPlugin : Plugin { + override fun apply(project: Project) { + + project.pluginManager.apply(DokkaPlugin::class.java) + project.pluginManager.apply(MavenPublishPlugin::class.java) + project.pluginManager.apply(SigningPlugin::class.java) + + val extension = project.extensions.create("publishing-convention-extension") + + project.group = "io.github.charlietap" + project.version = project.extensions.getByType(VersionCatalogsExtension::class.java).find("libs").get().findVersion("version-name").get().requiredVersion + + val dokkaHtml by project.tasks.getting(DokkaTask::class) + val javadocTask by project.tasks.registering(Jar::class) { + dependsOn(dokkaHtml) + archiveClassifier.set("javadoc") + from(dokkaHtml.outputDirectory) + } + + project.tasks.withType().configureEach { + val signingTasks = project.tasks.withType() + mustRunAfter(signingTasks) + } + + project.tasks.withType().configureEach { + notCompatibleWithConfigurationCache("https://github.com/Kotlin/dokka/issues/2231") + } + + project.afterEvaluate { + + project.extensions.configure(PublishingExtension::class.java) { + configurePublishing(project, javadocTask, extension) + } + + project.extensions.configure(SigningExtension::class.java) { + configureSigning(project, project.extensions.getByType().publications) + } + } + } + + private fun PublishingExtension.configurePublishing(project: Project, javadocJar: TaskProvider, extension: PublishingConventionsExtension) { + + val manualFileRepo = project.uri("file://${project.rootProject.layout.buildDirectory.get()}/manual") + + repositories { + maven { + name = "manual" + url = manualFileRepo + } + } + + publications.withType().configureEach { + + artifact(javadocJar) + + pom { + name.set(extension.name) + description.set(extension.description) + url.set("https://github.com/CharlieTap/cachemap") + licenses { + license { + name.set("Apache-2.0") + url.set("https://www.apache.org/licenses/LICENSE-2.0") + } + license { + name.set("MIT") + url.set("https://opensource.org/licenses/MIT") + } + } + developers { + developer { + id.set("CharlieTap") + name.set("Charlie Tapping") + } + } + scm { + connection.set("scm:git:https://github.com/CharlieTap/cachemap.git") + developerConnection.set("scm:git:ssh://github.com/CharlieTap/cachemap.git") + url.set("https://github.com/CharlieTap/cachemap") + } + } + } + } + + private fun SigningExtension.configureSigning(project: Project, publications: PublicationContainer) { + val signingKey: String? by project + val signingPassword: String? by project + + if(signingKey != null) { + useInMemoryPgpKeys(signingKey, signingPassword) + } + + sign(publications) + } +} diff --git a/leftright-shared/build.gradle.kts b/leftright-shared/build.gradle.kts index eb2ec5f..e851f1c 100644 --- a/leftright-shared/build.gradle.kts +++ b/leftright-shared/build.gradle.kts @@ -6,30 +6,31 @@ plugins { alias(libs.plugins.kotlin.serialization) alias(libs.plugins.kotlin.atomic.fu) alias(libs.plugins.kotlinter) - alias(libs.plugins.dokka) - id("maven-publish") - id("signing") + id("kmp-conventions") + id("publishing-conventions") } kotlin { - jvm() - - jvmToolchain { - languageVersion.set(JavaLanguageVersion.of(libs.versions.java.compiler.version.get().toInt())) - vendor.set(JvmVendorSpec.matching(libs.versions.java.vendor.get())) - } - - targets.configureEach { - compilations.configureEach { - kotlinOptions { - + setOf( + macosArm64(), + macosX64(), + iosArm64(), + iosSimulatorArm64(), + iosX64(), + linuxArm64(), + linuxX64(), + ).forEach { + it.compilations.getByName("main") { + cinterops { + val libcounter by creating { + defFile(project.file("src/ffi/cinterop/libcounter.def")) + } } } } sourceSets { - commonMain { dependencies {} } @@ -39,89 +40,12 @@ kotlin { implementation(libs.kotlin.test) } } - - jvmMain { - dependencies { - - } - } - } -} - - -val dokkaHtml by tasks.getting(org.jetbrains.dokka.gradle.DokkaTask::class) - -val javadocJar: TaskProvider by tasks.registering(Jar::class) { - dependsOn(dokkaHtml) - archiveClassifier.set("javadoc") - from(dokkaHtml.outputDirectory) -} - -tasks.withType().configureEach { - val signingTasks = tasks.withType() - mustRunAfter(signingTasks) -} - -tasks.withType().configureEach { - notCompatibleWithConfigurationCache("https://github.com/Kotlin/dokka/issues/2231") -} - -group = "io.github.charlietap" -version = libs.versions.version.name.get() - -publishing { - - val manualFileRepo = uri("file://${rootProject.layout.buildDirectory.get()}/manual") - - repositories { - maven { - name = "manual" - url = manualFileRepo - } - } - - publications.withType().configureEach { - - artifact(javadocJar) - - pom { - name.set(project.name) - description.set("A shared runtime library exposing a read optimised concurrency primitive for Kotlin Multiplatform") - url.set("https://github.com/CharlieTap/cachemap") - licenses { - license { - name.set("Apache-2.0") - url.set("https://www.apache.org/licenses/LICENSE-2.0") - } - license { - name.set("MIT") - url.set("https://opensource.org/licenses/MIT") - } - } - developers { - developer { - id.set("CharlieTap") - name.set("Charlie Tapping") - } - } - scm { - connection.set("scm:git:https://github.com/CharlieTap/cachemap.git") - developerConnection.set("scm:git:ssh://github.com/CharlieTap/cachemap.git") - url.set("https://github.com/CharlieTap/cachemap") - } - } } } -signing { - val signingKey: String? by project - val signingPassword: String? by project - - if(signingKey != null) { - useInMemoryPgpKeys(signingKey, signingPassword) - } - - sign(project.extensions.getByType().publications) +configure { + name = "leftright-shared" + description = "A shared runtime library exposing a read optimised concurrency primitive for Kotlin Multiplatform" } tasks.withType().configureEach { diff --git a/leftright-shared/src/commonMain/kotlin/io/github/charlietap/leftright/CacheAlignedCounter.kt b/leftright-shared/src/commonMain/kotlin/io/github/charlietap/leftright/CacheAlignedCounter.kt new file mode 100644 index 0000000..aa8fefb --- /dev/null +++ b/leftright-shared/src/commonMain/kotlin/io/github/charlietap/leftright/CacheAlignedCounter.kt @@ -0,0 +1,10 @@ +package io.github.charlietap.leftright + +interface CacheAlignedCounter { + + fun increment(): Int + + fun value(): Int +} + +expect fun counter(initial: Int): CacheAlignedCounter diff --git a/leftright-shared/src/commonMain/kotlin/io/github/charlietap/leftright/ReadEpochIndex.kt b/leftright-shared/src/commonMain/kotlin/io/github/charlietap/leftright/ReadEpochIndex.kt new file mode 100644 index 0000000..f55f6ff --- /dev/null +++ b/leftright-shared/src/commonMain/kotlin/io/github/charlietap/leftright/ReadEpochIndex.kt @@ -0,0 +1,7 @@ +package io.github.charlietap.leftright + +interface ReadEpochIndex { + fun value(): Int +} + +expect fun readEpochIndex(initializer: () -> Int): ReadEpochIndex diff --git a/leftright-shared/src/commonMain/kotlin/io/github/charlietap/leftright/ThreadLocal.kt b/leftright-shared/src/commonMain/kotlin/io/github/charlietap/leftright/ThreadLocal.kt deleted file mode 100644 index 4acadfd..0000000 --- a/leftright-shared/src/commonMain/kotlin/io/github/charlietap/leftright/ThreadLocal.kt +++ /dev/null @@ -1,7 +0,0 @@ -package io.github.charlietap.leftright - -interface ThreadLocal { - var value: T -} - -expect fun threadLocal(initializer: () -> T): ThreadLocal diff --git a/leftright-shared/src/ffi/cinterop/libcounter.def b/leftright-shared/src/ffi/cinterop/libcounter.def new file mode 100644 index 0000000..d620c63 --- /dev/null +++ b/leftright-shared/src/ffi/cinterop/libcounter.def @@ -0,0 +1,31 @@ +language = C +package = libcounter +--- +#include + +#if defined(__x86_64__) || defined(__aarch64__) || defined(_ARCH_PPC64) +#define CACHE_LINE_SIZE 128 +#elif defined(__arm__) || defined(__mips__) || defined(__mips64) || \ + defined(__riscv) || defined(__sparc__) || defined(__hexagon__) +#define CACHE_LINE_SIZE 32 +#elif defined(__m68k__) +#define CACHE_LINE_SIZE 16 +#elif defined(__s390x__) +#define CACHE_LINE_SIZE 256 +#else +#define CACHE_LINE_SIZE 64 +#endif + +typedef struct { + alignas(CACHE_LINE_SIZE) volatile int value; + char padding[CACHE_LINE_SIZE - (sizeof(int) % CACHE_LINE_SIZE)]; +} CacheAlignedInt; + +int increment_counter(CacheAlignedInt* counter) { + counter->value += 1; + return counter->value; +} + +int get_counter_value(CacheAlignedInt* counter) { + return counter->value; +} diff --git a/leftright-shared/src/commonMain/kotlin/io/github/charlietap/leftright/PaddedVolatileInt.kt b/leftright-shared/src/jvmMain/kotlin/io/github/charlietap/leftright/CacheAlignedCounter.kt similarity index 76% rename from leftright-shared/src/commonMain/kotlin/io/github/charlietap/leftright/PaddedVolatileInt.kt rename to leftright-shared/src/jvmMain/kotlin/io/github/charlietap/leftright/CacheAlignedCounter.kt index 3116512..ac224b6 100644 --- a/leftright-shared/src/commonMain/kotlin/io/github/charlietap/leftright/PaddedVolatileInt.kt +++ b/leftright-shared/src/jvmMain/kotlin/io/github/charlietap/leftright/CacheAlignedCounter.kt @@ -1,6 +1,6 @@ package io.github.charlietap.leftright -class PaddedVolatileInt(initialValue: Int) { +class JvmCacheAlignedCounter(initialValue: Int) : CacheAlignedCounter { @Volatile private var p1: Long = 0 @@ -32,16 +32,14 @@ class PaddedVolatileInt(initialValue: Int) { @Volatile var value: Int = initialValue - fun get(): Int { + override fun value(): Int { return value } - fun set(newValue: Int) { - value = newValue - } - - fun incrementAndGet(): Int { + override fun increment(): Int { value += 1 return value } } + +actual fun counter(initial: Int): CacheAlignedCounter = JvmCacheAlignedCounter(initial) diff --git a/leftright-shared/src/jvmMain/kotlin/io/github/charlietap/leftright/ReadEpochIndex.kt b/leftright-shared/src/jvmMain/kotlin/io/github/charlietap/leftright/ReadEpochIndex.kt new file mode 100644 index 0000000..d96177e --- /dev/null +++ b/leftright-shared/src/jvmMain/kotlin/io/github/charlietap/leftright/ReadEpochIndex.kt @@ -0,0 +1,8 @@ +package io.github.charlietap.leftright + +class JvmReadEpochIndex(initializer: () -> Int) : ReadEpochIndex { + private val threadLocal = ThreadLocal.withInitial(initializer) + override fun value(): Int = threadLocal.get() +} + +actual fun readEpochIndex(initializer: () -> Int): ReadEpochIndex = JvmReadEpochIndex(initializer) diff --git a/leftright-shared/src/jvmMain/kotlin/io/github/charlietap/leftright/ThreadLocal.kt b/leftright-shared/src/jvmMain/kotlin/io/github/charlietap/leftright/ThreadLocal.kt deleted file mode 100644 index d818f5f..0000000 --- a/leftright-shared/src/jvmMain/kotlin/io/github/charlietap/leftright/ThreadLocal.kt +++ /dev/null @@ -1,12 +0,0 @@ -package io.github.charlietap.leftright - -class JvmThreadLocal(initializer: () -> T) : ThreadLocal { - private val threadLocal = java.lang.ThreadLocal.withInitial(initializer) - override var value: T - get() = threadLocal.get() - set(value) = threadLocal.set(value) -} - -actual fun threadLocal(initializer: () -> T): ThreadLocal { - return JvmThreadLocal(initializer) -} diff --git a/leftright-shared/src/nativeMain/kotlin/io/github/charlietap/leftright/CacheAlignedCounter.kt b/leftright-shared/src/nativeMain/kotlin/io/github/charlietap/leftright/CacheAlignedCounter.kt new file mode 100644 index 0000000..87efac5 --- /dev/null +++ b/leftright-shared/src/nativeMain/kotlin/io/github/charlietap/leftright/CacheAlignedCounter.kt @@ -0,0 +1,33 @@ +package io.github.charlietap.leftright + +import kotlinx.cinterop.ExperimentalForeignApi +import kotlinx.cinterop.alloc +import kotlinx.cinterop.nativeHeap +import kotlinx.cinterop.ptr +import libcounter.CacheAlignedInt +import libcounter.get_counter_value +import libcounter.increment_counter + +@OptIn(ExperimentalForeignApi::class) +class NativeCacheAlignedCounter(initialValue: Int) : CacheAlignedCounter { + + private val counter = nativeHeap.alloc() + + init { + counter.value = initialValue + } + + override fun increment(): Int { + return increment_counter(counter.ptr) + } + + override fun value(): Int { + return get_counter_value(counter.ptr) + } + + fun free() { + nativeHeap.free(counter.rawPtr) + } +} + +actual fun counter(initial: Int): CacheAlignedCounter = NativeCacheAlignedCounter(initial) diff --git a/leftright-shared/src/nativeMain/kotlin/io/github/charlietap/leftright/CoreProvider.kt b/leftright-shared/src/nativeMain/kotlin/io/github/charlietap/leftright/CoreProvider.kt new file mode 100644 index 0000000..5229147 --- /dev/null +++ b/leftright-shared/src/nativeMain/kotlin/io/github/charlietap/leftright/CoreProvider.kt @@ -0,0 +1,8 @@ +package io.github.charlietap.leftright + +import platform.posix._SC_NPROCESSORS_ONLN +import platform.posix.sysconf + +actual fun coreProvider() = CoreProvider { + sysconf(_SC_NPROCESSORS_ONLN).toInt() +} diff --git a/leftright-shared/src/nativeMain/kotlin/io/github/charlietap/leftright/ReadEpochIndex.kt b/leftright-shared/src/nativeMain/kotlin/io/github/charlietap/leftright/ReadEpochIndex.kt new file mode 100644 index 0000000..3767bb4 --- /dev/null +++ b/leftright-shared/src/nativeMain/kotlin/io/github/charlietap/leftright/ReadEpochIndex.kt @@ -0,0 +1,22 @@ +package io.github.charlietap.leftright + +import kotlin.native.concurrent.ThreadLocal + +@ThreadLocal +private var idx: Int? = null + +private class NativeReadEpochIndex(val initializer: () -> Int) : ReadEpochIndex { + init { + idx = null + } + override fun value(): Int { + if (idx == null) { + idx = initializer() + } + return idx!! + } +} + +actual fun readEpochIndex(initializer: () -> Int): ReadEpochIndex { + return NativeReadEpochIndex(initializer) +} diff --git a/leftright-shared/src/nativeMain/kotlin/io/github/charlietap/leftright/Yield.kt b/leftright-shared/src/nativeMain/kotlin/io/github/charlietap/leftright/Yield.kt new file mode 100644 index 0000000..785de87 --- /dev/null +++ b/leftright-shared/src/nativeMain/kotlin/io/github/charlietap/leftright/Yield.kt @@ -0,0 +1,7 @@ +package io.github.charlietap.leftright + +import platform.posix.sched_yield + +actual fun yield() { + sched_yield() +} diff --git a/leftright-suspend/build.gradle.kts b/leftright-suspend/build.gradle.kts index 03cc411..bf3afa4 100644 --- a/leftright-suspend/build.gradle.kts +++ b/leftright-suspend/build.gradle.kts @@ -6,28 +6,12 @@ plugins { alias(libs.plugins.kotlin.serialization) alias(libs.plugins.kotlin.atomic.fu) alias(libs.plugins.kotlinter) - alias(libs.plugins.dokka) - id("maven-publish") - id("signing") + id("kmp-conventions") + id("publishing-conventions") } kotlin { - jvm() - - jvmToolchain { - languageVersion.set(JavaLanguageVersion.of(libs.versions.java.compiler.version.get().toInt())) - vendor.set(JvmVendorSpec.matching(libs.versions.java.vendor.get())) - } - - targets.configureEach { - compilations.configureEach { - kotlinOptions { - - } - } - } - sourceSets { commonMain { @@ -53,79 +37,9 @@ kotlin { } } -val dokkaHtml by tasks.getting(org.jetbrains.dokka.gradle.DokkaTask::class) - -val javadocJar: TaskProvider by tasks.registering(Jar::class) { - dependsOn(dokkaHtml) - archiveClassifier.set("javadoc") - from(dokkaHtml.outputDirectory) -} - -tasks.withType().configureEach { - val signingTasks = tasks.withType() - mustRunAfter(signingTasks) -} - -tasks.withType().configureEach { - notCompatibleWithConfigurationCache("https://github.com/Kotlin/dokka/issues/2231") -} - -group = "io.github.charlietap" -version = libs.versions.version.name.get() - -publishing { - - val manualFileRepo = uri("file://${rootProject.layout.buildDirectory.get()}/manual") - - repositories { - maven { - name = "manual" - url = manualFileRepo - } - } - - publications.withType().configureEach { - - artifact(javadocJar) - - pom { - name.set(project.name) - description.set("A read optimised suspending concurrency primitive for Kotlin Multiplatform") - url.set("https://github.com/CharlieTap/cachemap") - licenses { - license { - name.set("Apache-2.0") - url.set("https://www.apache.org/licenses/LICENSE-2.0") - } - license { - name.set("MIT") - url.set("https://opensource.org/licenses/MIT") - } - } - developers { - developer { - id.set("CharlieTap") - name.set("Charlie Tapping") - } - } - scm { - connection.set("scm:git:https://github.com/CharlieTap/cachemap.git") - developerConnection.set("scm:git:ssh://github.com/CharlieTap/cachemap.git") - url.set("https://github.com/CharlieTap/cachemap") - } - } - } -} - -signing { - val signingKey: String? by project - val signingPassword: String? by project - - if(signingKey != null) { - useInMemoryPgpKeys(signingKey, signingPassword) - } - - sign(project.extensions.getByType().publications) +configure { + name = "leftright-suspend" + description = "A read optimised suspending concurrency primitive for Kotlin Multiplatform" } tasks.withType().configureEach { diff --git a/leftright-suspend/src/commonMain/kotlin/io/github/charlietap/leftright/SuspendLeftRight.kt b/leftright-suspend/src/commonMain/kotlin/io/github/charlietap/leftright/SuspendLeftRight.kt index 6bea454..21ed41f 100644 --- a/leftright-suspend/src/commonMain/kotlin/io/github/charlietap/leftright/SuspendLeftRight.kt +++ b/leftright-suspend/src/commonMain/kotlin/io/github/charlietap/leftright/SuspendLeftRight.kt @@ -12,16 +12,16 @@ class SuspendLeftRight( constructor: () -> T, readerParallelism: Int = readerParallelism(), @PublishedApi internal val switch: AtomicBoolean = atomic(LEFT), - internal val allEpochs: Array = Array(readerParallelism) { PaddedVolatileInt(0) }, + internal val allEpochs: Array = Array(readerParallelism) { counter(0) }, internal val readEpochCount: AtomicInt = atomic(0), - internal val readEpochIdx: ThreadLocal = threadLocal { readEpochCount.getAndIncrement() }, + internal val readEpochIdx: ReadEpochIndex = readEpochIndex { readEpochCount.getAndIncrement() }, internal val left: T = constructor(), internal val right: T = constructor(), @PublishedApi internal val writeMutex: Mutex = Mutex(), ) { @PublishedApi - internal val readEpoch get() = allEpochs[readEpochIdx.value] + internal val readEpoch get() = allEpochs[readEpochIdx.value()] @PublishedApi internal val readSide get() = if (switch.value == LEFT) left else right @@ -42,10 +42,10 @@ class SuspendLeftRight( } inline fun read(action: (T) -> V): V { - readEpoch.incrementAndGet() + readEpoch.increment() return action(readSide).also { - readEpoch.incrementAndGet() + readEpoch.increment() } } @@ -57,7 +57,7 @@ class SuspendLeftRight( // this is a little inefficient as it could also filter readers that are on the correct side but mid-read // Its unclear whether the computational effort of filtering these out would have any meaningful impact val activeIndices = (0 until activeThreads).fold(mutableListOf>()) { acc, idx -> - val epoch = allEpochs[idx].value + val epoch = allEpochs[idx].value() acc.apply { if (epoch % 2 != 0) { acc.add(idx to epoch) @@ -73,8 +73,8 @@ class SuspendLeftRight( iterations = 0 yield() } else { - activeIndices.removeIf { (allEpochsIdx, epoch) -> - epoch != allEpochs[allEpochsIdx].value + activeIndices.removeAll { (allEpochsIdx, epoch) -> + epoch != allEpochs[allEpochsIdx].value() } } diff --git a/leftright-suspend/src/commonTest/kotlin/io/github/charlietap/leftright/SuspendLeftRightTest.kt b/leftright-suspend/src/commonTest/kotlin/io/github/charlietap/leftright/SuspendLeftRightTest.kt index 5efdeac..7296880 100644 --- a/leftright-suspend/src/commonTest/kotlin/io/github/charlietap/leftright/SuspendLeftRightTest.kt +++ b/leftright-suspend/src/commonTest/kotlin/io/github/charlietap/leftright/SuspendLeftRightTest.kt @@ -15,7 +15,7 @@ class SuspendLeftRightTest { @Test fun `ensure thread local index local increments the total epoch count`() { - val allEpochs = Array(1) { PaddedVolatileInt(0) } + val allEpochs = Array(1) { counter(0) } val totalEpochCount = atomic(0) @@ -25,17 +25,17 @@ class SuspendLeftRightTest { readEpochCount = totalEpochCount, ) - assertEquals(0, allEpochs[0].value) + assertEquals(0, allEpochs[0].value()) assertEquals(0, totalEpochCount.value) // before idx access is zero - assertEquals(0, suspendLeftRight.readEpochIdx.value) // idx access + assertEquals(0, suspendLeftRight.readEpochIdx.value()) // idx access assertEquals(1, totalEpochCount.value) // after idx access is incremented - assertEquals(0, suspendLeftRight.readEpoch.value) // ensure read epoch count is at zero + assertEquals(0, suspendLeftRight.readEpoch.value()) // ensure read epoch count is at zero } @Test fun `ensure a read increments the relevant epoch counter by 2`() { val expectedResult = 117 - val allEpochs = Array(1) { PaddedVolatileInt(0) } + val allEpochs = Array(1) { counter(0) } val suspendLeftRight = SuspendLeftRight( constructor = { expectedResult }, @@ -44,7 +44,7 @@ class SuspendLeftRightTest { val result = suspendLeftRight.read { it } - assertEquals(2, allEpochs[0].value) + assertEquals(2, allEpochs[0].value()) assertEquals(expectedResult, result) } @@ -52,7 +52,7 @@ class SuspendLeftRightTest { fun `ensure a write updates the switch`() = runTest { val expectedResult = mutableSetOf(1, 2) val switch = atomic(LEFT) - val allEpochs = Array(1) { PaddedVolatileInt(0) } + val allEpochs = Array(1) { counter(0) } val suspendLeftRight = SuspendLeftRight( constructor = { mutableSetOf(1) }, @@ -66,7 +66,7 @@ class SuspendLeftRightTest { val result = suspendLeftRight.mutate { it.add(2) } assertEquals(true, result) - assertEquals(0, allEpochs[0].value) + assertEquals(0, allEpochs[0].value()) assertEquals(RIGHT, switch.value) // assert the switch changed assertSame(readSide, suspendLeftRight.writeSide) // assert the pointers have switched sides assertSame(writeSide, suspendLeftRight.readSide) // assert the pointers have switched sides @@ -107,14 +107,14 @@ class SuspendLeftRightTest { writeMutex = writeMutex, ) - assertEquals(0, suspendLeftRight.readEpoch.value) + assertEquals(0, suspendLeftRight.readEpoch.value()) writeMutex.lock() val routine = async { suspendLeftRight.read { it } } val result = routine.await() assertEquals(mutableSetOf(1), result) - assertEquals(2, suspendLeftRight.readEpoch.value) // Note coroutines lets us run on the same thread + assertEquals(2, suspendLeftRight.readEpoch.value()) // Note coroutines lets us run on the same thread writeMutex.unlock() } } diff --git a/leftright/build.gradle.kts b/leftright/build.gradle.kts index 2edfc3b..e6180a7 100644 --- a/leftright/build.gradle.kts +++ b/leftright/build.gradle.kts @@ -6,28 +6,12 @@ plugins { alias(libs.plugins.kotlin.serialization) alias(libs.plugins.kotlin.atomic.fu) alias(libs.plugins.kotlinter) - alias(libs.plugins.dokka) - id("maven-publish") - id("signing") + id("kmp-conventions") + id("publishing-conventions") } kotlin { - jvm() - - jvmToolchain { - languageVersion.set(JavaLanguageVersion.of(libs.versions.java.compiler.version.get().toInt())) - vendor.set(JvmVendorSpec.matching(libs.versions.java.vendor.get())) - } - - targets.configureEach { - compilations.configureEach { - kotlinOptions { - - } - } - } - sourceSets { commonMain { @@ -48,83 +32,16 @@ kotlin { } } - } -} - -val dokkaHtml by tasks.getting(org.jetbrains.dokka.gradle.DokkaTask::class) - -val javadocJar: TaskProvider by tasks.registering(Jar::class) { - dependsOn(dokkaHtml) - archiveClassifier.set("javadoc") - from(dokkaHtml.outputDirectory) -} - -tasks.withType().configureEach { - val signingTasks = tasks.withType() - mustRunAfter(signingTasks) -} - -tasks.withType().configureEach { - notCompatibleWithConfigurationCache("https://github.com/Kotlin/dokka/issues/2231") -} - -group = "io.github.charlietap" -version = libs.versions.version.name.get() - -publishing { - - val manualFileRepo = uri("file://${rootProject.layout.buildDirectory.get()}/manual") - - repositories { - maven { - name = "manual" - url = manualFileRepo - } - } - - publications.withType().configureEach { - artifact(javadocJar) - - pom { - name.set(project.name) - description.set("A read optimised concurrency primitive for Kotlin Multiplatform") - url.set("https://github.com/CharlieTap/cachemap") - licenses { - license { - name.set("Apache-2.0") - url.set("https://www.apache.org/licenses/LICENSE-2.0") - } - license { - name.set("MIT") - url.set("https://opensource.org/licenses/MIT") - } - } - developers { - developer { - id.set("CharlieTap") - name.set("Charlie Tapping") - } - } - scm { - connection.set("scm:git:https://github.com/CharlieTap/cachemap.git") - developerConnection.set("scm:git:ssh://github.com/CharlieTap/cachemap.git") - url.set("https://github.com/CharlieTap/cachemap") - } + nativeTest { + languageSettings.optIn("kotlinx.cinterop.ExperimentalForeignApi") } } } - -signing { - val signingKey: String? by project - val signingPassword: String? by project - - if(signingKey != null) { - useInMemoryPgpKeys(signingKey, signingPassword) - } - - sign(project.extensions.getByType().publications) +configure { + name = "leftright" + description = "A read optimised concurrency primitive for Kotlin Multiplatform" } tasks.withType().configureEach { diff --git a/leftright/src/commonMain/kotlin/io/github/charlietap/leftright/LeftRight.kt b/leftright/src/commonMain/kotlin/io/github/charlietap/leftright/LeftRight.kt index 36c2836..4fc74c6 100644 --- a/leftright/src/commonMain/kotlin/io/github/charlietap/leftright/LeftRight.kt +++ b/leftright/src/commonMain/kotlin/io/github/charlietap/leftright/LeftRight.kt @@ -4,23 +4,24 @@ import kotlinx.atomicfu.AtomicBoolean import kotlinx.atomicfu.AtomicInt import kotlinx.atomicfu.atomic import kotlinx.atomicfu.locks.ReentrantLock +import kotlinx.atomicfu.locks.reentrantLock +import kotlinx.atomicfu.locks.withLock import kotlinx.atomicfu.update -import kotlin.concurrent.withLock class LeftRight( constructor: () -> T, readerParallelism: Int = readerParallelism(), @PublishedApi internal val switch: AtomicBoolean = atomic(LEFT), - internal val allEpochs: Array = Array(readerParallelism) { PaddedVolatileInt(0) }, + internal val allEpochs: Array = Array(readerParallelism) { counter(0) }, internal val readEpochCount: AtomicInt = atomic(0), - internal val readEpochIdx: ThreadLocal = threadLocal { readEpochCount.getAndIncrement() }, + internal val readEpochIdx: ReadEpochIndex = readEpochIndex { readEpochCount.getAndIncrement() }, internal val left: T = constructor(), internal val right: T = constructor(), - @PublishedApi internal val writeMutex: ReentrantLock = ReentrantLock(), + @PublishedApi internal val writeMutex: ReentrantLock = reentrantLock(), ) { @PublishedApi - internal val readEpoch get() = allEpochs[readEpochIdx.value] + internal val readEpoch get() = allEpochs[readEpochIdx.value()] @PublishedApi internal val readSide get() = if (switch.value == LEFT) left else right @@ -41,10 +42,10 @@ class LeftRight( } inline fun read(action: (T) -> V): V { - readEpoch.incrementAndGet() + readEpoch.increment() return action(readSide).also { - readEpoch.incrementAndGet() + readEpoch.increment() } } @@ -56,7 +57,7 @@ class LeftRight( // this is a little inefficient as it could also filter readers that are on the correct side but mid-read // Its unclear whether the computational effort of filtering these out would have any meaningful impact val activeIndices = (0 until activeThreads).fold(mutableListOf>()) { acc, idx -> - val epoch = allEpochs[idx].value + val epoch = allEpochs[idx].value() acc.apply { if (epoch % 2 != 0) { acc.add(idx to epoch) @@ -72,8 +73,8 @@ class LeftRight( iterations = 0 yield() } else { - activeIndices.removeIf { (allEpochsIdx, epoch) -> - epoch != allEpochs[allEpochsIdx].value + activeIndices.removeAll { (allEpochsIdx, epoch) -> + epoch != allEpochs[allEpochsIdx].value() } } diff --git a/leftright/src/commonTest/kotlin/io/github/charlietap/leftright/LeftRightTest.kt b/leftright/src/commonTest/kotlin/io/github/charlietap/leftright/LeftRightTest.kt index 69f3728..6c9b279 100644 --- a/leftright/src/commonTest/kotlin/io/github/charlietap/leftright/LeftRightTest.kt +++ b/leftright/src/commonTest/kotlin/io/github/charlietap/leftright/LeftRightTest.kt @@ -11,7 +11,7 @@ class LeftRightTest { @Test fun `ensure thread local index local increments the total epoch count`() { - val allEpochs = Array(1) { PaddedVolatileInt(0) } + val allEpochs = Array(1) { counter(0) } val totalEpochCount = atomic(0) @@ -21,17 +21,17 @@ class LeftRightTest { readEpochCount = totalEpochCount, ) - assertEquals(0, allEpochs[0].value) + assertEquals(0, allEpochs[0].value()) assertEquals(0, totalEpochCount.value) // before idx access is zero - assertEquals(0, leftRight.readEpochIdx.value) // idx access + assertEquals(0, leftRight.readEpochIdx.value()) // idx access assertEquals(1, totalEpochCount.value) // after idx access is incremented - assertEquals(0, leftRight.readEpoch.value) // ensure read epoch count is at zero + assertEquals(0, leftRight.readEpoch.value()) // ensure read epoch count is at zero } @Test fun `ensure a read increments the relevant epoch counter by 2`() { val expectedResult = 117 - val allEpochs = Array(1) { PaddedVolatileInt(0) } + val allEpochs = Array(1) { counter(0) } val leftRight = LeftRight( constructor = { expectedResult }, @@ -40,7 +40,7 @@ class LeftRightTest { val result = leftRight.read { it } - assertEquals(2, allEpochs[0].value) + assertEquals(2, allEpochs[0].value()) assertEquals(expectedResult, result) } @@ -48,7 +48,7 @@ class LeftRightTest { fun `ensure a write updates the switch`() { val expectedResult = mutableSetOf(1, 2) val switch = atomic(LEFT) - val allEpochs = Array(1) { PaddedVolatileInt(0) } + val allEpochs = Array(1) { counter(0) } val leftRight = LeftRight( constructor = { mutableSetOf(1) }, @@ -62,7 +62,7 @@ class LeftRightTest { val result = leftRight.mutate { it.add(2) } assertEquals(true, result) - assertEquals(0, allEpochs[0].value) + assertEquals(0, allEpochs[0].value()) assertEquals(RIGHT, switch.value) // assert the switch changed assertSame(readSide, leftRight.writeSide) // assert the pointers have switched sides assertSame(writeSide, leftRight.readSide) // assert the pointers have switched sides diff --git a/leftright/src/jvmTest/kotlin/io/github/charlietap/leftright/LeftRightTest.kt b/leftright/src/jvmTest/kotlin/io/github/charlietap/leftright/LeftRightTest.kt index fb7a5dd..13aeafc 100644 --- a/leftright/src/jvmTest/kotlin/io/github/charlietap/leftright/LeftRightTest.kt +++ b/leftright/src/jvmTest/kotlin/io/github/charlietap/leftright/LeftRightTest.kt @@ -42,21 +42,21 @@ class LeftRightJVMTest { writeMutex = writeMutex, ) - assertEquals(0, leftRight.readEpoch.value) + assertEquals(0, leftRight.readEpoch.value()) writeMutex.lock() var result: MutableSet? = null var epoch = 0 val routine = Thread { result = leftRight.read { it } - epoch = leftRight.readEpoch.value + epoch = leftRight.readEpoch.value() } routine.run { start() join() } assertEquals(mutableSetOf(1), result) - assertEquals(0, leftRight.readEpoch.value) // first threads epoch remains + assertEquals(0, leftRight.readEpoch.value()) // first threads epoch remains assertEquals(2, epoch) // spawned threads epoch increments writeMutex.unlock() } @@ -88,7 +88,7 @@ class LeftRightJVMTest { routine1.join() routine2.join() - assertEquals(2, leftRight.allEpochs[0].value) - assertEquals(2, leftRight.allEpochs[1].value) + assertEquals(2, leftRight.allEpochs[0].value()) + assertEquals(2, leftRight.allEpochs[1].value()) } } diff --git a/leftright/src/nativeTest/kotlin/io/github/charlietap/leftright/LeftRightTest.kt b/leftright/src/nativeTest/kotlin/io/github/charlietap/leftright/LeftRightTest.kt new file mode 100644 index 0000000..e36eab5 --- /dev/null +++ b/leftright/src/nativeTest/kotlin/io/github/charlietap/leftright/LeftRightTest.kt @@ -0,0 +1,194 @@ +@file:OptIn(ExperimentalForeignApi::class) + +package io.github.charlietap.leftright + +import kotlinx.atomicfu.locks.reentrantLock +import kotlinx.cinterop.ExperimentalForeignApi +import kotlinx.cinterop.StableRef +import kotlinx.cinterop.alloc +import kotlinx.cinterop.asStableRef +import kotlinx.cinterop.memScoped +import kotlinx.cinterop.ptr +import kotlinx.cinterop.staticCFunction +import kotlinx.cinterop.toLong +import kotlinx.cinterop.value +import platform.posix.PTHREAD_CREATE_JOINABLE +import platform.posix.pthread_attr_destroy +import platform.posix.pthread_attr_init +import platform.posix.pthread_attr_setdetachstate +import platform.posix.pthread_attr_t +import platform.posix.pthread_create +import platform.posix.pthread_join +import platform.posix.pthread_self +import platform.posix.pthread_tVar +import platform.posix.sleep +import kotlin.test.Test +import kotlin.test.assertEquals + +class LeftRightNativeTest { + + private data class ThreadData( + val leftRight: LeftRight>, + var result: MutableSet? = null, + var epoch: Int = 0, + ) + + @Test + fun `ensure only a single writer can access the write side`() { + val writeMutex = reentrantLock() + + val leftRight = LeftRight( + constructor = { mutableSetOf(1) }, + writeMutex = writeMutex, + ) + + writeMutex.lock() + + memScoped { + val thread = alloc() + val attr = alloc() + pthread_attr_init(attr.ptr) + pthread_attr_setdetachstate(attr.ptr, PTHREAD_CREATE_JOINABLE) + + pthread_create( + thread.ptr, + attr.ptr, + staticCFunction { lp -> + val lr = lp?.asStableRef>>()?.get() + lr?.mutate { + it.add(2) + } + null + }, + StableRef.create(leftRight).asCPointer(), + ) + + sleep(1u) + + assertEquals(mutableSetOf(1), leftRight.readSide) + assertEquals(mutableSetOf(1), leftRight.writeSide) + + writeMutex.unlock() + + pthread_join(thread.value, null) + + assertEquals(mutableSetOf(1, 2), leftRight.readSide) + assertEquals(mutableSetOf(1, 2), leftRight.writeSide) + + pthread_attr_destroy(attr.ptr) + } + } + + @Test + fun `ensure reads proceeds whilst writes are taking place`() { + val writeMutex = reentrantLock() + + val leftRight = LeftRight( + constructor = { mutableSetOf(1) }, + writeMutex = writeMutex, + ) + + val threadData = ThreadData( + leftRight, + ) + + assertEquals(0, leftRight.readEpoch.value()) + writeMutex.lock() + + memScoped { + val thread = alloc() + val attr = alloc() + pthread_attr_init(attr.ptr) + pthread_attr_setdetachstate(attr.ptr, PTHREAD_CREATE_JOINABLE) + + pthread_create( + thread.ptr, + attr.ptr, + staticCFunction { tdp -> + val td = tdp?.asStableRef()?.get() + + println("pthread id" + pthread_self().toLong()) + println("pthread idx: " + td?.leftRight?.readEpochIdx?.value()) + println("pthread value: " + td?.leftRight?.readEpoch?.value()) + + td?.result = td?.leftRight?.read { it } + td?.epoch = td?.leftRight?.readEpoch?.value() ?: 0 + null + }, + StableRef.create(threadData).asCPointer(), + ) + + pthread_join(thread.value, null) + pthread_attr_destroy(attr.ptr) + } + + assertEquals(mutableSetOf(1), threadData.result) + println("test thread id" + pthread_self().toLong()) + println("test thread idx: " + leftRight.readEpochIdx.value()) + println("test thread value: " + leftRight.readEpoch.value()) + println(leftRight.allEpochs.size) + leftRight.allEpochs.forEach { + println(it.value()) + } + assertEquals(0, leftRight.readEpoch.value()) // first threads epoch remains + assertEquals(2, threadData.epoch) // spawned threads epoch increments + writeMutex.unlock() + } + + @Test + fun `ensure reads increment separate counters`() { + val writeMutex = reentrantLock() + + val leftRight = LeftRight( + constructor = { mutableSetOf(1) }, + writeMutex = writeMutex, + ) + + memScoped { + val thread = alloc() + val attr = alloc() + pthread_attr_init(attr.ptr) + pthread_attr_setdetachstate(attr.ptr, PTHREAD_CREATE_JOINABLE) + + pthread_create( + thread.ptr, + attr.ptr, + staticCFunction { lp -> + val lr = lp?.asStableRef>>()?.get() + lr?.read { + it.first() + } + null + }, + StableRef.create(leftRight).asCPointer(), + ) + pthread_join(thread.value, null) + pthread_attr_destroy(attr.ptr) + } + + memScoped { + val thread = alloc() + val attr = alloc() + pthread_attr_init(attr.ptr) + pthread_attr_setdetachstate(attr.ptr, PTHREAD_CREATE_JOINABLE) + + pthread_create( + thread.ptr, + attr.ptr, + staticCFunction { lp -> + val lr = lp?.asStableRef>>()?.get() + lr?.read { + it.first() + } + null + }, + StableRef.create(leftRight).asCPointer(), + ) + pthread_join(thread.value, null) + pthread_attr_destroy(attr.ptr) + } + + assertEquals(2, leftRight.allEpochs[0].value()) + assertEquals(2, leftRight.allEpochs[1].value()) + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index da5a54d..fe42140 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -4,6 +4,9 @@ pluginManagement { google() mavenCentral() } + + includeBuild("gradle/plugins/kmp-conventions") + includeBuild("gradle/plugins/publishing-conventions") } plugins {