diff --git a/buildSrc/src/main/kotlin/SelektExtensions.kt b/buildSrc/src/main/kotlin/SelektExtensions.kt index cc33f4b812..ecdf291aea 100644 --- a/buildSrc/src/main/kotlin/SelektExtensions.kt +++ b/buildSrc/src/main/kotlin/SelektExtensions.kt @@ -62,8 +62,8 @@ fun Project.disableKotlinCompilerAssertions() { kotlinOptions { freeCompilerArgs = listOf( "-Xno-call-assertions", - "-Xno-receiver-assertions", - "-Xno-param-assertions" + "-Xno-param-assertions", + "-Xno-receiver-assertions" ) } } diff --git a/selekt-java/src/jmh/kotlin/com/bloomberg/selekt/cache/benchmarks/CommonLruCacheBenchmark.kt b/selekt-java/src/jmh/kotlin/com/bloomberg/selekt/cache/benchmarks/CommonLruCacheBenchmark.kt new file mode 100644 index 0000000000..fd1f0e503c --- /dev/null +++ b/selekt-java/src/jmh/kotlin/com/bloomberg/selekt/cache/benchmarks/CommonLruCacheBenchmark.kt @@ -0,0 +1,85 @@ +/* + * Copyright 2024 Bloomberg Finance L.P. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.bloomberg.selekt.cache.benchmarks + +import com.bloomberg.selekt.cache.CommonLruCache +import org.openjdk.jmh.annotations.Benchmark +import org.openjdk.jmh.annotations.BenchmarkMode +import org.openjdk.jmh.annotations.Level +import org.openjdk.jmh.annotations.Mode +import org.openjdk.jmh.annotations.Scope +import org.openjdk.jmh.annotations.Setup +import org.openjdk.jmh.annotations.State + +@State(Scope.Thread) +open class CommonCacheInput { + internal lateinit var cache: CommonLruCache + internal lateinit var largeCache: CommonLruCache + + @Setup(Level.Iteration) + fun setUp() { + cache = CommonLruCache(1) {} + largeCache = CommonLruCache(64) {} + } +} + +open class CommonLruCacheBenchmark { + @Benchmark + @BenchmarkMode(Mode.Throughput) + fun getEntry(input: CommonCacheInput) = input.cache.run { + get("1") {} + } + + @Benchmark + @BenchmarkMode(Mode.Throughput) + fun getEntryWithEviction(input: CommonCacheInput) = input.cache.run { + get("1") {} + get("2") {} + } + + @Benchmark + @BenchmarkMode(Mode.Throughput) + fun getEntries(input: CommonCacheInput) = input.largeCache.run { + get("1") { "" } + get("2") { "" } + } + + @Benchmark + @BenchmarkMode(Mode.Throughput) + fun getManyEntries(input: CommonCacheInput) = input.largeCache.run { + get("0") { "" } + get("1") { "" } + get("2") { "" } + get("3") { "" } + get("4") { "" } + get("5") { "" } + get("6") { "" } + get("7") { "" } + get("8") { "" } + get("9") { "" } + get("2") { "" } + get("3") { "" } + get("9") { "" } + get("4") { "" } + get("5") { "" } + get("0") { "" } + get("8") { "" } + get("6") { "" } + get("1") { "" } + get("7") { "" } + } +} diff --git a/selekt-java/src/jmh/kotlin/com/bloomberg/selekt/cache/benchmarks/LinkedLruCacheBenchmark.kt b/selekt-java/src/jmh/kotlin/com/bloomberg/selekt/cache/benchmarks/LinkedLruCacheBenchmark.kt new file mode 100644 index 0000000000..12c75f3114 --- /dev/null +++ b/selekt-java/src/jmh/kotlin/com/bloomberg/selekt/cache/benchmarks/LinkedLruCacheBenchmark.kt @@ -0,0 +1,85 @@ +/* + * Copyright 2024 Bloomberg Finance L.P. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.bloomberg.selekt.cache.benchmarks + +import com.bloomberg.selekt.cache.LinkedLruCache +import org.openjdk.jmh.annotations.Benchmark +import org.openjdk.jmh.annotations.BenchmarkMode +import org.openjdk.jmh.annotations.Level +import org.openjdk.jmh.annotations.Mode +import org.openjdk.jmh.annotations.Scope +import org.openjdk.jmh.annotations.Setup +import org.openjdk.jmh.annotations.State + +@State(Scope.Thread) +open class LinkedCacheInput { + internal lateinit var cache: LinkedLruCache + internal lateinit var largeCache: LinkedLruCache + + @Setup(Level.Iteration) + fun setUp() { + cache = LinkedLruCache(1) {} + largeCache = LinkedLruCache(64) {} + } +} + +open class LinkedLruCacheBenchmark { + @Benchmark + @BenchmarkMode(Mode.Throughput) + fun getEntry(input: LinkedCacheInput) = input.cache.run { + get("1") {} + } + + @Benchmark + @BenchmarkMode(Mode.Throughput) + fun getEntryWithEviction(input: LinkedCacheInput) = input.cache.run { + get("1") {} + get("2") {} + } + + @Benchmark + @BenchmarkMode(Mode.Throughput) + fun getEntries(input: LinkedCacheInput) = input.largeCache.run { + get("1") { "" } + get("2") { "" } + } + + @Benchmark + @BenchmarkMode(Mode.Throughput) + fun getManyEntries(input: LinkedCacheInput) = input.largeCache.run { + get("0") { "" } + get("1") { "" } + get("2") { "" } + get("3") { "" } + get("4") { "" } + get("5") { "" } + get("6") { "" } + get("7") { "" } + get("8") { "" } + get("9") { "" } + get("2") { "" } + get("3") { "" } + get("9") { "" } + get("4") { "" } + get("5") { "" } + get("0") { "" } + get("8") { "" } + get("6") { "" } + get("1") { "" } + get("7") { "" } + } +} diff --git a/selekt-java/src/jmh/kotlin/com/bloomberg/selekt/cache/benchmarks/StampedCacheBenchmark.kt b/selekt-java/src/jmh/kotlin/com/bloomberg/selekt/cache/benchmarks/StampedCacheBenchmark.kt new file mode 100644 index 0000000000..6ac71c3ca6 --- /dev/null +++ b/selekt-java/src/jmh/kotlin/com/bloomberg/selekt/cache/benchmarks/StampedCacheBenchmark.kt @@ -0,0 +1,85 @@ +/* + * Copyright 2024 Bloomberg Finance L.P. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.bloomberg.selekt.cache.benchmarks + +import com.bloomberg.selekt.cache.StampedCache +import org.openjdk.jmh.annotations.Benchmark +import org.openjdk.jmh.annotations.BenchmarkMode +import org.openjdk.jmh.annotations.Level +import org.openjdk.jmh.annotations.Mode +import org.openjdk.jmh.annotations.Scope +import org.openjdk.jmh.annotations.Setup +import org.openjdk.jmh.annotations.State + +@State(Scope.Thread) +open class StampedCacheInput { + internal lateinit var cache: StampedCache + internal lateinit var largeCache: StampedCache + + @Setup(Level.Iteration) + fun setUp() { + cache = StampedCache(1) {} + largeCache = StampedCache(64) {} + } +} + +open class StampedCacheBenchmark { + @Benchmark + @BenchmarkMode(Mode.Throughput) + fun getEntry(input: StampedCacheInput) = input.cache.run { + get("1") {} + } + + @Benchmark + @BenchmarkMode(Mode.Throughput) + fun getEntryWithEviction(input: StampedCacheInput) = input.cache.run { + get("1") {} + get("2") {} + } + + @Benchmark + @BenchmarkMode(Mode.Throughput) + fun getEntries(input: StampedCacheInput) = input.largeCache.run { + get("1") { "" } + get("2") { "" } + } + + @Benchmark + @BenchmarkMode(Mode.Throughput) + fun getManyEntries(input: StampedCacheInput) = input.largeCache.run { + get("0") { "" } + get("1") { "" } + get("2") { "" } + get("3") { "" } + get("4") { "" } + get("5") { "" } + get("6") { "" } + get("7") { "" } + get("8") { "" } + get("9") { "" } + get("2") { "" } + get("3") { "" } + get("9") { "" } + get("4") { "" } + get("5") { "" } + get("0") { "" } + get("8") { "" } + get("6") { "" } + get("1") { "" } + get("7") { "" } + } +} diff --git a/selekt-java/src/jmh/kotlin/com/bloomberg/selekt/collections/map/benchmarks/FastLinkedStringMapBenchmark.kt b/selekt-java/src/jmh/kotlin/com/bloomberg/selekt/collections/map/benchmarks/FastLinkedStringMapBenchmark.kt new file mode 100644 index 0000000000..bbf52a22a3 --- /dev/null +++ b/selekt-java/src/jmh/kotlin/com/bloomberg/selekt/collections/map/benchmarks/FastLinkedStringMapBenchmark.kt @@ -0,0 +1,122 @@ +/* + * Copyright 2024 Bloomberg Finance L.P. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.bloomberg.selekt.collections.map.benchmarks + +import com.bloomberg.selekt.collections.map.FastLinkedStringMap +import org.openjdk.jmh.annotations.Benchmark +import org.openjdk.jmh.annotations.BenchmarkMode +import org.openjdk.jmh.annotations.Level +import org.openjdk.jmh.annotations.Mode +import org.openjdk.jmh.annotations.Scope +import org.openjdk.jmh.annotations.Setup +import org.openjdk.jmh.annotations.State + +@State(Scope.Thread) +open class LinkedMapInput { + internal lateinit var smallMap: FastLinkedStringMap + internal lateinit var largeMap: FastLinkedStringMap + internal lateinit var largeAccessMap: FastLinkedStringMap + + @Setup(Level.Iteration) + fun setUp() { + smallMap = FastLinkedStringMap(1, 1) {} + largeMap = FastLinkedStringMap(64, 64, false) {} + largeAccessMap = FastLinkedStringMap(64, 64, true) {} + } +} + +open class FastLinkedStringMapBenchmark { + @Benchmark + @BenchmarkMode(Mode.Throughput) + fun getEntry(input: LinkedMapInput) = input.smallMap.run { + getElsePut("1") { "" } + } + + @Benchmark + @BenchmarkMode(Mode.Throughput) + fun getEntries(input: LinkedMapInput) = input.largeMap.run { + getElsePut("1") { "" } + getElsePut("2") { "" } + } + + @Benchmark + @BenchmarkMode(Mode.Throughput) + fun getEntriesDifferentLengths(input: LinkedMapInput) = input.largeMap.run { + getElsePut("1") { "" } + getElsePut("23") { "" } + } + + @Benchmark + @BenchmarkMode(Mode.Throughput) + fun getEntriesAccessOrder(input: LinkedMapInput) = input.largeAccessMap.run { + getElsePut("1") { "" } + getElsePut("2") { "" } + } + + @Benchmark + @BenchmarkMode(Mode.Throughput) + fun getEntriesWithCollision(input: LinkedMapInput) = input.smallMap.run { + getElsePut("1") { "" } + getElsePut("2") { "" } + } + + @Benchmark + @BenchmarkMode(Mode.Throughput) + fun getEntriesDifferentLengthsWithCollision(input: LinkedMapInput) = input.smallMap.run { + getElsePut("1") { "" } + getElsePut("23") { "" } + } + + @Benchmark + @BenchmarkMode(Mode.Throughput) + fun getThenRemoveEntry(input: LinkedMapInput) = input.smallMap.run { + getElsePut("1") { "" } + removeEntry("1") + } + + @Benchmark + @BenchmarkMode(Mode.Throughput) + fun getThenRemoveKey(input: LinkedMapInput) = input.smallMap.run { + getElsePut("1") { "" } + removeKey("1") + } + + @Benchmark + @BenchmarkMode(Mode.Throughput) + fun getManyEntriesAccessOrder(input: LinkedMapInput) = input.largeAccessMap.run { + getEntryElsePut("0") { "" } + getEntryElsePut("1") { "" } + getEntryElsePut("2") { "" } + getEntryElsePut("3") { "" } + getEntryElsePut("4") { "" } + getEntryElsePut("5") { "" } + getEntryElsePut("6") { "" } + getEntryElsePut("7") { "" } + getEntryElsePut("8") { "" } + getEntryElsePut("9") { "" } + getEntryElsePut("2") { "" } + getEntryElsePut("3") { "" } + getEntryElsePut("9") { "" } + getEntryElsePut("4") { "" } + getEntryElsePut("5") { "" } + getEntryElsePut("0") { "" } + getEntryElsePut("8") { "" } + getEntryElsePut("6") { "" } + getEntryElsePut("1") { "" } + getEntryElsePut("7") { "" } + } +} diff --git a/selekt-java/src/jmh/kotlin/com/bloomberg/selekt/collections/map/benchmarks/FastStampedStringMapBenchmark.kt b/selekt-java/src/jmh/kotlin/com/bloomberg/selekt/collections/map/benchmarks/FastStampedStringMapBenchmark.kt new file mode 100644 index 0000000000..32b4e166e8 --- /dev/null +++ b/selekt-java/src/jmh/kotlin/com/bloomberg/selekt/collections/map/benchmarks/FastStampedStringMapBenchmark.kt @@ -0,0 +1,85 @@ +/* + * Copyright 2024 Bloomberg Finance L.P. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.bloomberg.selekt.collections.map.benchmarks + +import com.bloomberg.selekt.collections.map.FastStampedStringMap +import org.openjdk.jmh.annotations.Benchmark +import org.openjdk.jmh.annotations.BenchmarkMode +import org.openjdk.jmh.annotations.Level +import org.openjdk.jmh.annotations.Mode +import org.openjdk.jmh.annotations.Scope +import org.openjdk.jmh.annotations.Setup +import org.openjdk.jmh.annotations.State + +@State(Scope.Thread) +open class StampedMapInput { + internal lateinit var map: FastStampedStringMap + internal lateinit var largeMap: FastStampedStringMap + + @Setup(Level.Iteration) + fun setUp() { + map = FastStampedStringMap(1) {} + largeMap = FastStampedStringMap(64) {} + } +} + +open class FastStampedStringMapBenchmark { + @Benchmark + @BenchmarkMode(Mode.Throughput) + fun getEntry(input: StampedMapInput) = input.map.run { + getEntryElsePut("1") { "" } + } + + @Benchmark + @BenchmarkMode(Mode.Throughput) + fun getEntryWithCollision(input: StampedMapInput) = input.map.run { + getEntryElsePut("1") { "" } + getEntryElsePut("2") { "" } + } + + @Benchmark + @BenchmarkMode(Mode.Throughput) + fun getThenRemoveEntry(input: StampedMapInput) = input.map.run { + getEntryElsePut("1") { "" } + removeEntry("1") + } + + @Benchmark + @BenchmarkMode(Mode.Throughput) + fun getManyEntries(input: StampedMapInput) = input.largeMap.run { + getEntryElsePut("0") { "" } + getEntryElsePut("1") { "" } + getEntryElsePut("2") { "" } + getEntryElsePut("3") { "" } + getEntryElsePut("4") { "" } + getEntryElsePut("5") { "" } + getEntryElsePut("6") { "" } + getEntryElsePut("7") { "" } + getEntryElsePut("8") { "" } + getEntryElsePut("9") { "" } + getEntryElsePut("2") { "" } + getEntryElsePut("3") { "" } + getEntryElsePut("9") { "" } + getEntryElsePut("4") { "" } + getEntryElsePut("5") { "" } + getEntryElsePut("0") { "" } + getEntryElsePut("8") { "" } + getEntryElsePut("6") { "" } + getEntryElsePut("1") { "" } + getEntryElsePut("7") { "" } + } +} diff --git a/selekt-java/src/jmh/kotlin/com/bloomberg/selekt/collections/map/benchmarks/FastStringMapBenchmark.kt b/selekt-java/src/jmh/kotlin/com/bloomberg/selekt/collections/map/benchmarks/FastStringMapBenchmark.kt new file mode 100644 index 0000000000..0d3690cf25 --- /dev/null +++ b/selekt-java/src/jmh/kotlin/com/bloomberg/selekt/collections/map/benchmarks/FastStringMapBenchmark.kt @@ -0,0 +1,58 @@ +/* + * Copyright 2024 Bloomberg Finance L.P. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.bloomberg.selekt.collections.map.benchmarks + +import com.bloomberg.selekt.collections.map.FastStringMap +import org.openjdk.jmh.annotations.Benchmark +import org.openjdk.jmh.annotations.BenchmarkMode +import org.openjdk.jmh.annotations.Level +import org.openjdk.jmh.annotations.Mode +import org.openjdk.jmh.annotations.Scope +import org.openjdk.jmh.annotations.Setup +import org.openjdk.jmh.annotations.State + +@State(Scope.Thread) +open class MapInput { + internal lateinit var map: FastStringMap + + @Setup(Level.Iteration) + fun setUp() { + map = FastStringMap(1) + } +} + +open class FastStringMapBenchmark { + @Benchmark + @BenchmarkMode(Mode.Throughput) + fun getEntry(input: MapInput) = input.map.run { + getEntryElsePut("1") { "" } + } + + @Benchmark + @BenchmarkMode(Mode.Throughput) + fun getEntryWithCollision(input: MapInput) = input.map.run { + getEntryElsePut("1") { "" } + getEntryElsePut("2") { "" } + } + + @Benchmark + @BenchmarkMode(Mode.Throughput) + fun getThenRemoveEntry(input: MapInput) = input.map.run { + getEntryElsePut("1") { "" } + removeEntry("1") + } +} diff --git a/selekt-java/src/jmh/kotlin/com/bloomberg/selekt/cache/benchmarks/LruCacheBenchmark.kt b/selekt-java/src/jmh/kotlin/com/bloomberg/selekt/jdk/benchmarks/ArrayBenchmark.kt similarity index 72% rename from selekt-java/src/jmh/kotlin/com/bloomberg/selekt/cache/benchmarks/LruCacheBenchmark.kt rename to selekt-java/src/jmh/kotlin/com/bloomberg/selekt/jdk/benchmarks/ArrayBenchmark.kt index 9ae099b5e4..ce85ef01ae 100644 --- a/selekt-java/src/jmh/kotlin/com/bloomberg/selekt/cache/benchmarks/LruCacheBenchmark.kt +++ b/selekt-java/src/jmh/kotlin/com/bloomberg/selekt/jdk/benchmarks/ArrayBenchmark.kt @@ -14,9 +14,8 @@ * limitations under the License. */ -package com.bloomberg.selekt.cache.benchmarks +package com.bloomberg.selekt.jdk.benchmarks -import com.bloomberg.selekt.cache.LruCache import org.openjdk.jmh.annotations.Benchmark import org.openjdk.jmh.annotations.BenchmarkMode import org.openjdk.jmh.annotations.Level @@ -26,26 +25,26 @@ import org.openjdk.jmh.annotations.Setup import org.openjdk.jmh.annotations.State @State(Scope.Thread) -open class CacheInput { - internal lateinit var cache: LruCache +open class ArrayInput { + internal lateinit var array: Array @Setup(Level.Iteration) fun setUp() { - cache = LruCache(1) {} + array = Array(2) { Any() } } } -open class LruCacheBenchmark { +open class ArrayBenchmark { @Benchmark @BenchmarkMode(Mode.Throughput) - fun getEntry(input: CacheInput) = input.cache.run { - this["1", {}] + fun getFirst(input: ArrayInput) = input.array.run { + firstOrNull() } @Benchmark @BenchmarkMode(Mode.Throughput) - fun getEntryWithEviction(input: CacheInput) = input.cache.run { - this["1", {}] - this["2", {}] + fun getEntries(input: ArrayInput) = input.array.run { + firstOrNull() + this[1] } } diff --git a/selekt-java/src/jmh/kotlin/com/bloomberg/selekt/jdk/benchmarks/ArrayListBenchmark.kt b/selekt-java/src/jmh/kotlin/com/bloomberg/selekt/jdk/benchmarks/ArrayListBenchmark.kt new file mode 100644 index 0000000000..3d98bb9e35 --- /dev/null +++ b/selekt-java/src/jmh/kotlin/com/bloomberg/selekt/jdk/benchmarks/ArrayListBenchmark.kt @@ -0,0 +1,53 @@ +/* + * Copyright 2024 Bloomberg Finance L.P. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.bloomberg.selekt.jdk.benchmarks + +import org.openjdk.jmh.annotations.Benchmark +import org.openjdk.jmh.annotations.BenchmarkMode +import org.openjdk.jmh.annotations.Level +import org.openjdk.jmh.annotations.Mode +import org.openjdk.jmh.annotations.Scope +import org.openjdk.jmh.annotations.Setup +import org.openjdk.jmh.annotations.State + +@State(Scope.Thread) +open class ArrayListInput { + internal lateinit var list: ArrayList + + @Setup(Level.Iteration) + fun setUp() { + list = ArrayList(1).apply { + add(Any()) + add(Any()) + } + } +} + +open class ArrayListBenchmark { + @Benchmark + @BenchmarkMode(Mode.Throughput) + fun getFirst(input: ArrayListInput) = input.list.run { + firstOrNull() + } + + @Benchmark + @BenchmarkMode(Mode.Throughput) + fun getEntries(input: ArrayListInput) = input.list.run { + firstOrNull() + this[1] + } +} diff --git a/selekt-java/src/jmh/kotlin/com/bloomberg/selekt/jdk/benchmarks/HashMapBenchmark.kt b/selekt-java/src/jmh/kotlin/com/bloomberg/selekt/jdk/benchmarks/HashMapBenchmark.kt new file mode 100644 index 0000000000..fa385eabaa --- /dev/null +++ b/selekt-java/src/jmh/kotlin/com/bloomberg/selekt/jdk/benchmarks/HashMapBenchmark.kt @@ -0,0 +1,66 @@ +/* + * Copyright 2024 Bloomberg Finance L.P. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.bloomberg.selekt.jdk.benchmarks + +import org.openjdk.jmh.annotations.Benchmark +import org.openjdk.jmh.annotations.BenchmarkMode +import org.openjdk.jmh.annotations.Level +import org.openjdk.jmh.annotations.Mode +import org.openjdk.jmh.annotations.Scope +import org.openjdk.jmh.annotations.Setup +import org.openjdk.jmh.annotations.State + +@State(Scope.Thread) +open class HashMapInput { + internal lateinit var smallMap: HashMap + internal lateinit var largeMap: HashMap + + @Setup(Level.Iteration) + fun setUp() { + smallMap = HashMap(1) + largeMap = HashMap(64) + } +} + +open class FastStringMapBenchmark { + @Benchmark + @BenchmarkMode(Mode.Throughput) + fun getEntry(input: HashMapInput) = input.smallMap.run { + getOrPut("1") { "" } + } + + @Benchmark + @BenchmarkMode(Mode.Throughput) + fun getEntries(input: HashMapInput) = input.largeMap.run { + getOrPut("1") { "" } + getOrPut("2") { "" } + } + + @Benchmark + @BenchmarkMode(Mode.Throughput) + fun getEntryWithCollision(input: HashMapInput) = input.smallMap.run { + getOrPut("1") { "" } + getOrPut("2") { "" } + } + + @Benchmark + @BenchmarkMode(Mode.Throughput) + fun getThenRemoveEntry(input: HashMapInput) = input.smallMap.run { + getOrPut("1") { "" } + remove("1") + } +} diff --git a/selekt-java/src/jmh/kotlin/com/bloomberg/selekt/jdk/benchmarks/LinkedHashMapBenchmark.kt b/selekt-java/src/jmh/kotlin/com/bloomberg/selekt/jdk/benchmarks/LinkedHashMapBenchmark.kt new file mode 100644 index 0000000000..fd9b715f2d --- /dev/null +++ b/selekt-java/src/jmh/kotlin/com/bloomberg/selekt/jdk/benchmarks/LinkedHashMapBenchmark.kt @@ -0,0 +1,113 @@ +/* + * Copyright 2024 Bloomberg Finance L.P. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.bloomberg.selekt.jdk.benchmarks + +import org.openjdk.jmh.annotations.Benchmark +import org.openjdk.jmh.annotations.BenchmarkMode +import org.openjdk.jmh.annotations.Level +import org.openjdk.jmh.annotations.Mode +import org.openjdk.jmh.annotations.Scope +import org.openjdk.jmh.annotations.Setup +import org.openjdk.jmh.annotations.State + +@State(Scope.Thread) +open class LinkedHashMapInput { + internal lateinit var smallMap: LinkedHashMap + internal lateinit var largeMap: LinkedHashMap + internal lateinit var largeAccessOrderMap: LinkedHashMap + + @Setup(Level.Iteration) + fun setUp() { + smallMap = object : LinkedHashMap(1, 1.1f, false) { + override fun removeEldestEntry(eldest: MutableMap.MutableEntry) = size > 1 + } + largeMap = object : LinkedHashMap(64, 1.1f, false) { + override fun removeEldestEntry(eldest: MutableMap.MutableEntry) = size > 64 + } + largeAccessOrderMap = object : LinkedHashMap(64, 1.1f, true) { + override fun removeEldestEntry(eldest: MutableMap.MutableEntry) = size > 64 + } + } +} + +open class LinkedHashMapBenchmark { + @Benchmark + @BenchmarkMode(Mode.Throughput) + fun getEntry(input: LinkedHashMapInput) = input.smallMap.run { + getOrPut("1") { "" } + } + + @Benchmark + @BenchmarkMode(Mode.Throughput) + fun getEntries(input: LinkedHashMapInput) = input.largeMap.run { + getOrPut("1") { "" } + getOrPut("2") { "" } + } + + @Benchmark + @BenchmarkMode(Mode.Throughput) + fun getEntriesDifferentLengths(input: LinkedHashMapInput) = input.largeMap.run { + getOrPut("1") { "" } + getOrPut("23") { "" } + } + + @Benchmark + @BenchmarkMode(Mode.Throughput) + fun getEntriesAccessOrder(input: LinkedHashMapInput) = input.largeAccessOrderMap.run { + getOrPut("1") { "" } + getOrPut("2") { "" } + } + + @Benchmark + @BenchmarkMode(Mode.Throughput) + fun getManyEntriesAccessOrder(input: LinkedHashMapInput) = input.largeAccessOrderMap.run { + getOrPut("0") { "" } + getOrPut("1") { "" } + getOrPut("2") { "" } + getOrPut("3") { "" } + getOrPut("4") { "" } + getOrPut("5") { "" } + getOrPut("6") { "" } + getOrPut("7") { "" } + getOrPut("8") { "" } + getOrPut("9") { "" } + getOrPut("2") { "" } + getOrPut("3") { "" } + getOrPut("9") { "" } + getOrPut("4") { "" } + getOrPut("5") { "" } + getOrPut("0") { "" } + getOrPut("8") { "" } + getOrPut("6") { "" } + getOrPut("1") { "" } + getOrPut("7") { "" } + } + + @Benchmark + @BenchmarkMode(Mode.Throughput) + fun getEntryWithRemoval(input: LinkedHashMapInput) = input.smallMap.run { + getOrPut("1") { "" } + getOrPut("2") { "" } + } + + @Benchmark + @BenchmarkMode(Mode.Throughput) + fun getThenRemoveEntry(input: LinkedHashMapInput) = input.smallMap.run { + getOrPut("1") { "" } + remove("1") + } +} diff --git a/selekt-java/src/jmh/kotlin/com/bloomberg/selekt/jdk/benchmarks/LinkedListBenchmark.kt b/selekt-java/src/jmh/kotlin/com/bloomberg/selekt/jdk/benchmarks/LinkedListBenchmark.kt new file mode 100644 index 0000000000..5c2a11e1bf --- /dev/null +++ b/selekt-java/src/jmh/kotlin/com/bloomberg/selekt/jdk/benchmarks/LinkedListBenchmark.kt @@ -0,0 +1,60 @@ +/* + * Copyright 2024 Bloomberg Finance L.P. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.bloomberg.selekt.jdk.benchmarks + +import org.openjdk.jmh.annotations.Benchmark +import org.openjdk.jmh.annotations.BenchmarkMode +import org.openjdk.jmh.annotations.Level +import org.openjdk.jmh.annotations.Mode +import org.openjdk.jmh.annotations.Scope +import org.openjdk.jmh.annotations.Setup +import org.openjdk.jmh.annotations.State +import java.util.LinkedList + +@State(Scope.Thread) +open class LinkedListInput { + internal lateinit var list: LinkedList + + data class Node( + val value: Any, + val next: Any? + ) + + @Setup(Level.Iteration) + fun setUp() { + list = LinkedList().apply { + val next = Any() + add(Node(Any(), next)) + add(Node(Any(), null)) + } + } +} + +open class LinkedListBenchmark { + @Benchmark + @BenchmarkMode(Mode.Throughput) + fun getFirst(input: LinkedListInput) = input.list.run { + firstOrNull() + } + + @Benchmark + @BenchmarkMode(Mode.Throughput) + fun getEntries(input: LinkedListInput) = input.list.run { + val first = firstOrNull() + first?.next + } +} diff --git a/selekt-java/src/main/kotlin/com/bloomberg/selekt/SQLConnection.kt b/selekt-java/src/main/kotlin/com/bloomberg/selekt/SQLConnection.kt index 4e71010efd..08eb79bc30 100644 --- a/selekt-java/src/main/kotlin/com/bloomberg/selekt/SQLConnection.kt +++ b/selekt-java/src/main/kotlin/com/bloomberg/selekt/SQLConnection.kt @@ -16,7 +16,7 @@ package com.bloomberg.selekt -import com.bloomberg.selekt.cache.LruCache +import com.bloomberg.selekt.cache.CommonLruCache import com.bloomberg.selekt.commons.forEachByPosition import com.bloomberg.selekt.commons.forUntil import javax.annotation.concurrent.NotThreadSafe @@ -31,7 +31,7 @@ internal class SQLConnection( key: Key? ) : CloseableSQLExecutor { private val pointer = sqlite.open(path, flags) - private val preparedStatements = LruCache(configuration.maxSqlCacheSize) { + private val preparedStatements = CommonLruCache(configuration.maxSqlCacheSize) { it.close() pooledPreparedStatement = it } @@ -189,18 +189,16 @@ internal class SQLConnection( block() } - private fun acquirePreparedStatement(sql: String) = preparedStatements[ - sql, { - val pointer = sqlite.prepare(pointer, sql) - pooledPreparedStatement.let { - if (it != null) { - SQLPreparedStatement.recycle(it, pointer, sql).also { pooledPreparedStatement = null } - } else { - SQLPreparedStatement(pointer, sql, sqlite, random) - } + private fun acquirePreparedStatement(sql: String) = preparedStatements.get(sql) { + val pointer = sqlite.prepare(pointer, sql) + pooledPreparedStatement.let { + if (it != null) { + SQLPreparedStatement.recycle(it, pointer, sql).also { pooledPreparedStatement = null } + } else { + SQLPreparedStatement(pointer, sql, sqlite, random) } } - ] + } private fun releasePreparedStatement(preparedStatement: SQLPreparedStatement) { if (runCatching { preparedStatement.resetAndClearBindings() }.isFailure) { diff --git a/selekt-java/src/main/kotlin/com/bloomberg/selekt/cache/CommonLruCache.kt b/selekt-java/src/main/kotlin/com/bloomberg/selekt/cache/CommonLruCache.kt new file mode 100644 index 0000000000..ac15e06428 --- /dev/null +++ b/selekt-java/src/main/kotlin/com/bloomberg/selekt/cache/CommonLruCache.kt @@ -0,0 +1,79 @@ +/* + * Copyright 2024 Bloomberg Finance L.P. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.bloomberg.selekt.cache + +class CommonLruCache( + @PublishedApi + @JvmField + internal val maxSize: Int, + disposal: (T) -> Unit +) { + @PublishedApi + @JvmField + internal var cache: StampedCache? = StampedCache(maxSize, disposal) + @PublishedApi + @JvmField + internal var linkedCache: LinkedLruCache? = null + + fun evict(key: String) { + cache?.let { + it.evict(key) + return + } + linkedCache!!.evict(key) + } + + fun evictAll() { + cache?.let { + it.evictAll() + return + } + linkedCache!!.evictAll() + } + + inline fun get(key: String, supplier: () -> T): T { + return cache?.get(key) { + supplier().also { value -> + if (cache!!.shouldTransform()) { + // Adding another entry to the cache will necessitate the removal of the + // least recently used entry first to honour our maximum size constraint. + // For the implementation of the store currently assigned, this is an O(N) + // operation. We transform to an O(1) implementation. + transform() + linkedCache!!.store.put(key, value) + } + } + } ?: linkedCache!!.get(key, supplier) + } + + fun containsKey(key: String): Boolean { + cache?.let { + return it.containsKey(key) + } + return linkedCache!!.containsKey(key) + } + + @PublishedApi + internal fun StampedCache.shouldTransform() = (store.size >= maxSize) + + @PublishedApi + internal fun transform() { + linkedCache = cache!!.asLruCache().also { + cache = null + } + } +} diff --git a/selekt-java/src/main/kotlin/com/bloomberg/selekt/cache/LruCache.kt b/selekt-java/src/main/kotlin/com/bloomberg/selekt/cache/LinkedLruCache.kt similarity index 53% rename from selekt-java/src/main/kotlin/com/bloomberg/selekt/cache/LruCache.kt rename to selekt-java/src/main/kotlin/com/bloomberg/selekt/cache/LinkedLruCache.kt index 06a50e3a14..02146cd3eb 100644 --- a/selekt-java/src/main/kotlin/com/bloomberg/selekt/cache/LruCache.kt +++ b/selekt-java/src/main/kotlin/com/bloomberg/selekt/cache/LinkedLruCache.kt @@ -16,36 +16,36 @@ package com.bloomberg.selekt.cache +import com.bloomberg.selekt.collections.map.FastLinkedStringMap import javax.annotation.concurrent.NotThreadSafe -private const val NO_RESIZE_LOAD_FACTOR = 1.1f - @NotThreadSafe -class LruCache(private val maxSize: Int, private val disposal: (T) -> Unit) { +class LinkedLruCache( @PublishedApi @JvmField - @JvmSynthetic - internal val store = object : LinkedHashMap(maxSize, NO_RESIZE_LOAD_FACTOR, true) { - override fun removeEldestEntry(eldest: MutableMap.MutableEntry) = (size > maxSize).also { - if (it) { - disposal(eldest.value) - } - } - - override fun remove(key: String): T? = super.remove(key)?.also { disposal(it) } - } + internal val maxSize: Int, + @PublishedApi + @JvmField + internal val store: FastLinkedStringMap +) { + constructor( + maxSize: Int, + disposal: (T) -> Unit + ) : this(maxSize, FastLinkedStringMap( + maxSize = maxSize, + disposal = disposal, + accessOrder = true + )) fun evict(key: String) { - store.remove(key) + store.removeKey(key) } fun evictAll() { - store.values.toList() - .also { store.clear() } - .forEach { disposal(it) } + store.clear() } - inline operator fun get(key: String, supplier: () -> T): T = store.getOrPut(key, supplier) + inline fun get(key: String, supplier: () -> T): T = store.getElsePut(key, supplier) fun containsKey(key: String) = store.containsKey(key) } diff --git a/selekt-java/src/main/kotlin/com/bloomberg/selekt/cache/StampedCache.kt b/selekt-java/src/main/kotlin/com/bloomberg/selekt/cache/StampedCache.kt new file mode 100644 index 0000000000..c3dfb46144 --- /dev/null +++ b/selekt-java/src/main/kotlin/com/bloomberg/selekt/cache/StampedCache.kt @@ -0,0 +1,49 @@ +/* + * Copyright 2024 Bloomberg Finance L.P. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.bloomberg.selekt.cache + +import com.bloomberg.selekt.collections.map.FastStampedStringMap +import javax.annotation.concurrent.NotThreadSafe + +@NotThreadSafe +class StampedCache( + capacity: Int, + @PublishedApi + @JvmField + internal val disposal: (T) -> Unit +) { + @PublishedApi + @JvmField + internal val store = FastStampedStringMap(capacity = capacity, disposal = disposal) + + fun evict(key: String) { + store.removeKey(key) + } + + fun evictAll() { + store.clear() + } + + inline fun get(key: String, supplier: () -> T): T = store.getElsePut(key, supplier) + + fun containsKey(key: String) = store.containsKey(key) + + internal fun asLruCache() = LinkedLruCache( + maxSize = store.size, + store = store.asLinkedMap(store.size, disposal) + ) +} diff --git a/selekt-java/src/main/kotlin/com/bloomberg/selekt/collections/map/FastLinkedStringMap.kt b/selekt-java/src/main/kotlin/com/bloomberg/selekt/collections/map/FastLinkedStringMap.kt new file mode 100644 index 0000000000..a3d3b69b79 --- /dev/null +++ b/selekt-java/src/main/kotlin/com/bloomberg/selekt/collections/map/FastLinkedStringMap.kt @@ -0,0 +1,183 @@ +/* + * Copyright 2024 Bloomberg Finance L.P. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.bloomberg.selekt.collections.map + +import javax.annotation.concurrent.NotThreadSafe + +@NotThreadSafe +class FastLinkedStringMap( + @PublishedApi + @JvmField + internal val maxSize: Int, + capacity: Int = maxSize, + @PublishedApi + @JvmField + internal val accessOrder: Boolean = false, + private val disposal: (T) -> Unit +) : FastStringMap(capacity) { + private var head: LinkedEntry? = null + private var tail: LinkedEntry? = null + + @PublishedApi + @JvmField + internal var spare: LinkedEntry? = null + + inline fun getElsePut( + key: String, + supplier: () -> T + ): T { + val hashCode = hash(key) + val index = hashIndex(hashCode) + entryMatching(index, hashCode, key)?.let { + if (accessOrder) { + putFirst(it as LinkedEntry) + } + return it.value!! + } + return addAssociation(index, hashCode, key, supplier()).value!! + } + + @PublishedApi + internal fun put( + key: String, + value: T + ): T { + val hashCode = hash(key) + val index = hashIndex(hashCode) + return addAssociation(index, hashCode, key, value).value!! + } + + fun removeKey(key: String) { + disposal((super.removeEntry(key) as LinkedEntry).unlink().value!!) + } + + override fun clear() { + super.clear() + spare = null + var entry = tail + while (entry != null) { + val previous = entry.previous + disposal(entry.unlink().value!!) + entry.key = "" + entry.value = null + entry = previous + } + } + + private fun LinkedEntry.unlink(): Entry = apply { + previous?.let { it.next = next } + next?.let { it.previous = previous } + if (this === head) { + head = next + } + if (this === tail) { + tail = previous + } + previous = null + next = null + } + + @PublishedApi + @JvmSynthetic + internal fun putFirst(node: LinkedEntry): Unit = node.run { + if (this === head) { + return + } + previous?.let { it.next = next } + next?.let { it.previous = previous } + if (this === tail) { + tail = previous + } + next = head + previous = null + head?.let { it.previous = this } + head = this + if (tail == null) { + tail = this + } + } + + @PublishedApi + override fun addAssociation( + index: Int, + hashCode: Int, + key: String, + value: T + ): Entry { + if (size >= maxSize) { + spare = removeLastEntry() + } + return (super.addAssociation(index, hashCode, key, value) as LinkedEntry).also { + putFirst(it) + } + } + + override fun createEntry( + index: Int, + hashCode: Int, + key: String, + value: T + ): Entry { + spare?.let { + spare = null + return it.update(index, hashCode, key, value, store[index]) + } + return LinkedEntry(index, hashCode, key, value, store[index]) + } + + @PublishedApi + @JvmSynthetic + internal fun removeLastEntry(): LinkedEntry = tail!!.apply { + previous?.let { it.next = null } ?: run { head = null } + tail = previous + previous = null + super.removeEntry(key) + key = "" + disposal(value!!) + value = null + } + + @PublishedApi + internal class LinkedEntry( + index: Int, + hashCode: Int, + key: String, + value: T, + after: Entry? + ) : Entry(index, hashCode, key, value, after) { + @JvmField + var previous: LinkedEntry? = null + + @JvmField + var next: LinkedEntry? = null + + @Suppress("NOTHING_TO_INLINE") + inline fun update( + index: Int, + hashCode: Int, + key: String, + value: T, + after: Entry? + ) = apply { + this.index = index + this.hashCode = hashCode + this.key = key + this.value = value + this.after = after + } + } +} diff --git a/selekt-java/src/main/kotlin/com/bloomberg/selekt/collections/map/FastStampedStringMap.kt b/selekt-java/src/main/kotlin/com/bloomberg/selekt/collections/map/FastStampedStringMap.kt new file mode 100644 index 0000000000..5568c3075b --- /dev/null +++ b/selekt-java/src/main/kotlin/com/bloomberg/selekt/collections/map/FastStampedStringMap.kt @@ -0,0 +1,135 @@ +/* + * Copyright 2024 Bloomberg Finance L.P. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.bloomberg.selekt.collections.map + +import javax.annotation.concurrent.NotThreadSafe + +@NotThreadSafe +class FastStampedStringMap( + capacity: Int, + private val disposal: (T) -> Unit +) : FastStringMap(capacity) { + private var currentStamp = Int.MIN_VALUE + private var spare: StampedEntry? = null + + inline fun getElsePut( + key: String, + supplier: () -> T + ): T { + val hashCode = hash(key) + val index = hashIndex(hashCode) + entryMatching(index, hashCode, key)?.let { + (it as StampedEntry).stamp = nextStamp() + return it.value!! + } + return addAssociation(index, hashCode, key, supplier()).value!! + } + + override fun createEntry( + index: Int, + hashCode: Int, + key: String, + value: T + ): Entry { + spare?.let { + spare = null + return it.update(index, hashCode, key, value, nextStamp(), store[index]) + } + return StampedEntry(index, hashCode, key, value, nextStamp(), store[index]) + } + + fun removeKey(key: String) { + disposal(super.removeEntry(key).value!!) + } + + override fun clear() { + entries().forEach { + disposal(it.value!!) + } + spare = null + super.clear() + } + + @PublishedApi + internal fun nextStamp(): Int { + if (Int.MAX_VALUE == currentStamp) { + resetAllStamps() + } + currentStamp += 1 + return currentStamp + } + + internal fun asLinkedMap( + maxSize: Int = size, + disposal: (T) -> Unit + ) = FastLinkedStringMap( + maxSize = maxSize, + capacity = maxSize, + accessOrder = true, + disposal = disposal + ).apply { + this@FastStampedStringMap.entries().sortedBy { + (it as StampedEntry).stamp + }.forEach { + addAssociation(it.index, it.hashCode, it.key, it.value!!) + } + } + + private fun resetAllStamps() { + @Suppress("UNCHECKED_CAST") + (entries() as Iterable>).sortedBy(StampedEntry::stamp).run { + currentStamp = Int.MIN_VALUE + maxOf(0, size - 1) + forEachIndexed { index, it -> + it.stamp = Int.MIN_VALUE + index + } + } + } + + @Suppress("UNCHECKED_CAST") + @PublishedApi + internal fun removeLastEntry(): StampedEntry = (entries() as Iterable>) + .minBy(StampedEntry::stamp).let { + (removeEntry(it.key) as StampedEntry).apply { disposal(value!!) } + } + + @PublishedApi + internal class StampedEntry( + index: Int, + hashCode: Int, + key: String, + value: T, + var stamp: Int, + after: Entry? + ) : Entry(index, hashCode, key, value, after) { + @Suppress("NOTHING_TO_INLINE") + inline fun update( + index: Int, + hashCode: Int, + key: String, + value: T, + stamp: Int, + after: Entry? + ) = apply { + this.index = index + this.hashCode = hashCode + this.key = key + this.value = value + this.stamp = stamp + this.after = after + } + } +} diff --git a/selekt-java/src/main/kotlin/com/bloomberg/selekt/collections/map/FastStringMap.kt b/selekt-java/src/main/kotlin/com/bloomberg/selekt/collections/map/FastStringMap.kt new file mode 100644 index 0000000000..667d50909e --- /dev/null +++ b/selekt-java/src/main/kotlin/com/bloomberg/selekt/collections/map/FastStringMap.kt @@ -0,0 +1,163 @@ +/* + * Copyright 2024 Bloomberg Finance L.P. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.bloomberg.selekt.collections.map + +import javax.annotation.concurrent.NotThreadSafe + +/** + * @param capacity a power of two. + */ +@NotThreadSafe +open class FastStringMap(capacity: Int) { + @JvmField + var size: Int = 0 + + @JvmField + @PublishedApi + internal val store = arrayOfNulls>(capacity) + private val hashLimit = capacity - 1 + + fun isEmpty() = 0 == size + + fun containsKey(key: String): Boolean { + val hashCode = hash(key) + val index = hashIndex(hashCode) + var entry = store[index] + while (entry != null) { + if (entry.hashCode == hashCode && entry.key == key) { + return true + } + entry = entry.after + } + return false + } + + inline fun getEntryElsePut( + key: String, + supplier: () -> T + ): Entry { + val hashCode = hash(key) + val index = hashIndex(hashCode) + var entry = store[index] + while (entry != null) { + if (entry.hashCode == hashCode && entry.key == key) { + return entry + } + entry = entry.after + } + return addAssociation(index, hashCode, key, supplier()) + } + + fun removeEntry(key: String): Entry { + val hashCode = hash(key) + val index = hashIndex(hashCode) + var entry = store[index] + var previous: Entry? = null + while (entry != null) { + if (entry.hashCode == hashCode && entry.key == key) { + return removeAssociation(entry, previous) + } + previous = entry + entry = entry.after + } + throw NoSuchElementException() + } + + @PublishedApi + internal open fun addAssociation( + index: Int, + hashCode: Int, + key: String, + value: T + ): Entry = createEntry(index, hashCode, key, value).also { + store[index] = it + size += 1 + } + + protected open fun createEntry( + index: Int, + hashCode: Int, + key: String, + value: T + ): Entry = Entry(index, hashCode, key, value, store[index]) + + internal fun entries(): Iterable> = store.flatMap { + sequence { + var current = it + while (current != null) { + yield(current) + current = current.after + } + } + } + + open fun clear() { + store.fill(null) + size = 0 + } + + @Suppress("NOTHING_TO_INLINE") + @PublishedApi + internal inline fun entryMatching(index: Int, hashCode: Int, key: String): Entry? { + var entry = store[index] + while (entry != null) { + if (entry.hashCode == hashCode && entry.key == key) { + return entry + } + entry = entry.after + } + return null + } + + private fun removeAssociation( + entry: Entry, + previousEntry: Entry? + ): Entry { + if (previousEntry == null) { + store[entry.index] = entry.after + } else { + previousEntry.after = entry.after + } + size -= 1 + return entry + } + + @PublishedApi + internal fun hash(key: String): Int = key.hashCode() + + @PublishedApi + internal fun hashIndex(hashCode: Int): Int = hashCode and hashLimit + + open class Entry( + @JvmField + var index: Int, + @JvmField + var hashCode: Int, + @JvmField + var key: String, + @JvmField + var value: T?, + @JvmField + var after: Entry? + ) { + internal fun reset(): T? = value.also { _ -> + key = "" + value = null + after = null + } + } +} diff --git a/selekt-java/src/test/kotlin/com/bloomberg/selekt/cache/CommonLruCacheTest.kt b/selekt-java/src/test/kotlin/com/bloomberg/selekt/cache/CommonLruCacheTest.kt new file mode 100644 index 0000000000..c4792c1eb9 --- /dev/null +++ b/selekt-java/src/test/kotlin/com/bloomberg/selekt/cache/CommonLruCacheTest.kt @@ -0,0 +1,143 @@ +/* + * Copyright 2020 Bloomberg Finance L.P. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.bloomberg.selekt.cache + +import org.junit.jupiter.api.Assertions.assertNull +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import org.mockito.kotlin.anyOrNull +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.inOrder +import org.mockito.kotlin.mock +import org.mockito.kotlin.never +import org.mockito.kotlin.same +import org.mockito.kotlin.times +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever +import kotlin.test.assertFalse +import kotlin.test.assertNotNull +import kotlin.test.assertSame +import kotlin.test.assertTrue +import kotlin.test.fail + +internal class CommonLruCacheTest { + private val first = Any() + private val second = Any() + private val supplier = mock<() -> Any>() + private val disposal: (Any) -> Unit = mock { onGeneric { invoke(it) } doReturn Unit } + + @Test + fun get() { + val first = Any() + val cache = CommonLruCache(1, disposal) + cache.get("1") { first } + assertSame(first, cache.get("1") { fail() }) + assertNotNull(cache.cache) + } + + @Test + fun getTwo() { + val cache = CommonLruCache(2, disposal) + cache.get("1") { first } + cache.get("2") { second } + assertSame(first, cache.get("1") { fail() }) + assertSame(second, cache.get("2") { fail() }) + assertNotNull(cache.cache) + } + + @Test + fun getAfterEvict() { + val cache = CommonLruCache(1, disposal) + cache.get("1") { first } + cache.get("2") { second } + assertFalse(cache.containsKey("1")) + assertSame(second, cache.get("2") { fail() }) + assertNull(cache.cache) + } + + @Test + fun evict() { + val cache = CommonLruCache(2, disposal) + cache.get("1") { first } + cache.get("2") { second } + cache.evict("1") + inOrder(disposal) { + verify(disposal, times(1)).invoke(same(first)) + } + assertSame(second, cache.get("2") { fail() }) + } + + @Test + fun evictAll() { + val cache = CommonLruCache(2, disposal) + cache.get("1") { first } + cache.get("2") { second } + cache.evictAll() + inOrder(disposal) { + verify(disposal, times(1)).invoke(same(second)) + verify(disposal, times(1)).invoke(same(first)) + } + } + + @Test + fun evictWhenEmpty() { + val cache = CommonLruCache(1, disposal) + assertThrows { + cache.evict("1") + } + verify(disposal, never()).invoke(anyOrNull()) + } + + @Test + fun evictLeastRecentlyUsed() { + val cache = CommonLruCache(2, disposal) + val third = Any() + cache.get("1") { first } + cache.get("2") { second } + cache.get("1") { fail() } + cache.get("3") { third } + assertFalse(cache.containsKey("2")) + inOrder(disposal) { + verify(disposal, times(1)).invoke(same(second)) + } + } + + @Test + fun getWhenAbsent() { + whenever(supplier.invoke()) doReturn Any() + val cache = CommonLruCache(1, disposal) + val item = cache.get("1", supplier) + verify(supplier, times(1)).invoke() + assertSame(item, cache.get("1", supplier)) + } + + @Test + fun containsFalse() { + whenever(supplier.invoke()) doReturn Any() + val cache = CommonLruCache(1, disposal) + cache.get("1", supplier) + assertFalse(cache.containsKey("2")) + } + + @Test + fun containsTrue() { + whenever(supplier.invoke()) doReturn Any() + val cache = CommonLruCache(1, disposal) + cache.get("1", supplier) + assertTrue(cache.containsKey("1")) + } +} diff --git a/selekt-java/src/test/kotlin/com/bloomberg/selekt/cache/LinkedLruCacheTest.kt b/selekt-java/src/test/kotlin/com/bloomberg/selekt/cache/LinkedLruCacheTest.kt new file mode 100644 index 0000000000..0102172bba --- /dev/null +++ b/selekt-java/src/test/kotlin/com/bloomberg/selekt/cache/LinkedLruCacheTest.kt @@ -0,0 +1,137 @@ +/* + * Copyright 2020 Bloomberg Finance L.P. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.bloomberg.selekt.cache + +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import org.mockito.kotlin.anyOrNull +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.inOrder +import org.mockito.kotlin.mock +import org.mockito.kotlin.never +import org.mockito.kotlin.same +import org.mockito.kotlin.times +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever +import kotlin.test.assertFalse +import kotlin.test.assertSame +import kotlin.test.assertTrue +import kotlin.test.fail + +internal class LinkedLruCacheTest { + private val first = Any() + private val second = Any() + private val supplier = mock<() -> Any>() + private val disposal: (Any) -> Unit = mock { onGeneric { invoke(it) } doReturn Unit } + + @Test + fun get() { + val first = Any() + val cache = LinkedLruCache(1, disposal) + cache.get("1") { first } + assertSame(first, cache.get("1") { fail() }) + } + + @Test + fun getTwo() { + val cache = LinkedLruCache(2, disposal) + cache.get("1") { first } + cache.get("2") { second } + assertSame(first, cache.get("1") { fail() }) + assertSame(second, cache.get("2") { fail() }) + } + + @Test + fun getAfterEvict() { + val cache = LinkedLruCache(1, disposal) + cache.get("1") { first } + cache.get("2") { second } + assertFalse(cache.containsKey("1")) + assertSame(second, cache.get("2") { fail() }) + } + + @Test + fun evict() { + val cache = LinkedLruCache(2, disposal) + cache.get("1") { first } + cache.get("2") { second } + cache.evict("1") + inOrder(disposal) { + verify(disposal, times(1)).invoke(same(first)) + } + assertSame(second, cache.get("2") { fail() }) + } + + @Test + fun evictAll() { + val cache = LinkedLruCache(2, disposal) + cache.get("1") { first } + cache.get("2") { second } + cache.evictAll() + inOrder(disposal) { + verify(disposal, times(1)).invoke(same(first)) + verify(disposal, times(1)).invoke(same(second)) + } + } + + @Test + fun evictWhenEmpty() { + val cache = LinkedLruCache(1, disposal) + assertThrows { + cache.evict("1") + } + verify(disposal, never()).invoke(anyOrNull()) + } + + @Test + fun evictLeastRecentlyUsed() { + val cache = LinkedLruCache(2, disposal) + val third = Any() + cache.get("1") { first } + cache.get("2") { second } + cache.get("1") { fail() } + cache.get("3") { third } + inOrder(disposal) { + verify(disposal, times(1)).invoke(same(second)) + } + } + + @Test + fun getWhenAbsent() { + whenever(supplier.invoke()) doReturn Any() + val cache = LinkedLruCache(1, disposal) + val item = cache.get("1", supplier) + verify(supplier, times(1)).invoke() + assertSame(item, cache.get("1", supplier)) + } + + @Test + fun containsFalse() { + whenever(supplier.invoke()) doReturn Any() + val cache = LinkedLruCache(1, disposal) + cache.get("1", supplier) + assertFalse(cache.containsKey("2")) + } + + @Test + fun containsTrue() { + whenever(supplier.invoke()) doReturn Any() + val cache = LinkedLruCache(1, disposal) + cache.get("1", supplier) + assertTrue(cache.containsKey("1")) + } +} diff --git a/selekt-java/src/test/kotlin/com/bloomberg/selekt/cache/LruCacheTest.kt b/selekt-java/src/test/kotlin/com/bloomberg/selekt/cache/LruCacheTest.kt deleted file mode 100644 index 7679080144..0000000000 --- a/selekt-java/src/test/kotlin/com/bloomberg/selekt/cache/LruCacheTest.kt +++ /dev/null @@ -1,149 +0,0 @@ -/* - * Copyright 2020 Bloomberg Finance L.P. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.bloomberg.selekt.cache - -import org.junit.jupiter.api.Test -import org.mockito.kotlin.anyOrNull -import org.mockito.kotlin.doReturn -import org.mockito.kotlin.inOrder -import org.mockito.kotlin.mock -import org.mockito.kotlin.never -import org.mockito.kotlin.same -import org.mockito.kotlin.times -import org.mockito.kotlin.verify -import org.mockito.kotlin.whenever -import kotlin.test.assertFalse -import kotlin.test.assertSame -import kotlin.test.assertTrue -import kotlin.test.fail - -internal class LruCacheTest { - @Test - fun get() { - val first = Any() - val disposal: (Any) -> Unit = mock { onGeneric { invoke(it) } doReturn Unit } - val cache = LruCache(1, disposal) - cache["1", { first }] - assertSame(first, cache["1", { fail() }]) - } - - @Test - fun getTwo() { - val first = Any() - val second = Any() - val disposal: (Any) -> Unit = mock { onGeneric { invoke(it) } doReturn Unit } - val cache = LruCache(2, disposal) - cache["1", { first }] - cache["2", { second }] - assertSame(first, cache["1", { fail() }]) - assertSame(second, cache["2", { fail() }]) - } - - @Test - fun getAfterEvict() { - val first = Any() - val second = Any() - val disposal: (Any) -> Unit = mock { onGeneric { invoke(it) } doReturn Unit } - val cache = LruCache(1, disposal) - cache["1", { first }] - cache["2", { second }] - assertFalse(cache.containsKey("1")) - assertSame(second, cache["2", { fail() }]) - } - - @Test - fun evict() { - val first = Any() - val second = Any() - val disposal: (Any) -> Unit = mock { onGeneric { invoke(it) } doReturn Unit } - val cache = LruCache(2, disposal) - cache["1", { first }] - cache["2", { second }] - cache.evict("1") - inOrder(disposal) { - verify(disposal, times(1)).invoke(same(first)) - } - assertSame(second, cache["2", { fail() }]) - } - - @Test - fun evictAll() { - val first = Any() - val second = Any() - val disposal: (Any) -> Unit = mock { onGeneric { invoke(it) } doReturn Unit } - val cache = LruCache(2, disposal) - cache["1", { first }] - cache["2", { second }] - cache.evictAll() - inOrder(disposal) { - verify(disposal, times(1)).invoke(same(first)) - verify(disposal, times(1)).invoke(same(second)) - } - } - - @Test - fun evictWhenEmpty() { - val disposal: (Any) -> Unit = mock { onGeneric { invoke(it) } doReturn Unit } - val cache = LruCache(1, disposal) - cache.evict("1") - verify(disposal, never()).invoke(anyOrNull()) - } - - @Test - fun evictLeastRecentlyUsed() { - val first = Any() - val second = Any() - val disposal: (Any) -> Unit = mock { onGeneric { invoke(it) } doReturn Unit } - val cache = LruCache(1, disposal) - cache["1", { first }] - cache["2", { second }] - inOrder(disposal) { - verify(disposal, times(1)).invoke(same(first)) - } - } - - @Test - fun getWhenAbsent() { - val disposal: (Any) -> Unit = mock { onGeneric { invoke(it) } doReturn Unit } - val supplier = mock<() -> Any>() - whenever(supplier.invoke()) doReturn Any() - val cache = LruCache(1, disposal) - val item = cache["1", supplier] - verify(supplier, times(1)).invoke() - assertSame(item, cache["1", supplier]) - } - - @Test - fun containsFalse() { - val disposal: (Any) -> Unit = mock { onGeneric { invoke(it) } doReturn Unit } - val supplier = mock<() -> Any>() - whenever(supplier.invoke()) doReturn Any() - val cache = LruCache(1, disposal) - cache["1", supplier] - assertFalse(cache.containsKey("2")) - } - - @Test - fun containsTrue() { - val disposal: (Any) -> Unit = mock { onGeneric { invoke(it) } doReturn Unit } - val supplier = mock<() -> Any>() - whenever(supplier.invoke()) doReturn Any() - val cache = LruCache(1, disposal) - cache["1", supplier] - assertTrue(cache.containsKey("1")) - } -} diff --git a/selekt-java/src/test/kotlin/com/bloomberg/selekt/cache/StampedCacheTest.kt b/selekt-java/src/test/kotlin/com/bloomberg/selekt/cache/StampedCacheTest.kt new file mode 100644 index 0000000000..64ec827d91 --- /dev/null +++ b/selekt-java/src/test/kotlin/com/bloomberg/selekt/cache/StampedCacheTest.kt @@ -0,0 +1,118 @@ +/* + * Copyright 2020 Bloomberg Finance L.P. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.bloomberg.selekt.cache + +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import org.mockito.kotlin.anyOrNull +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.inOrder +import org.mockito.kotlin.mock +import org.mockito.kotlin.never +import org.mockito.kotlin.same +import org.mockito.kotlin.times +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever +import kotlin.test.assertFalse +import kotlin.test.assertSame +import kotlin.test.assertTrue +import kotlin.test.fail + +internal class StampedCacheTest { + private val first = Any() + private val second = Any() + private val supplier = mock<() -> Any>() + private val disposal: (Any) -> Unit = mock { onGeneric { invoke(it) } doReturn Unit } + + @Test + fun get() { + val first = Any() + val cache = StampedCache(1, disposal) + cache.get("1") { first } + assertSame(first, cache.get("1") { fail() }) + } + + @Test + fun getTwo() { + val cache = StampedCache(2, disposal) + cache.get("1") { first } + cache.get("2") { second } + assertSame(first, cache.get("1") { fail() }) + assertSame(second, cache.get("2") { fail() }) + } + + @Test + fun evict() { + val cache = StampedCache(2, disposal) + cache.get("1") { first } + cache.get("2") { second } + cache.evict("1") + assertFalse(cache.containsKey("1")) + inOrder(disposal) { + verify(disposal, times(1)).invoke(same(first)) + } + assertSame(second, cache.get("2") { fail() }) + } + + @Test + fun evictAll() { + val cache = StampedCache(2, disposal) + cache.get("1") { first } + cache.get("2") { second } + cache.evictAll() + assertFalse(cache.containsKey("1")) + assertFalse(cache.containsKey("2")) + inOrder(disposal) { + verify(disposal, times(1)).invoke(same(second)) + verify(disposal, times(1)).invoke(same(first)) + } + } + + @Test + fun evictWhenEmpty() { + val cache = StampedCache(1, disposal) + assertThrows { + cache.evict("1") + } + verify(disposal, never()).invoke(anyOrNull()) + } + + @Test + fun getWhenAbsent() { + whenever(supplier.invoke()) doReturn Any() + val cache = StampedCache(1, disposal) + val item = cache.get("1", supplier) + verify(supplier, times(1)).invoke() + assertSame(item, cache.get("1", supplier)) + } + + @Test + fun containsFalse() { + whenever(supplier.invoke()) doReturn Any() + val cache = StampedCache(1, disposal) + cache.get("1", supplier) + assertFalse(cache.containsKey("2")) + } + + @Test + fun containsTrue() { + whenever(supplier.invoke()) doReturn Any() + val cache = StampedCache(1, disposal) + cache.get("1", supplier) + assertTrue(cache.containsKey("1")) + } +} diff --git a/selekt-java/src/test/kotlin/com/bloomberg/selekt/collections/map/FastLinkedStringMapTest.kt b/selekt-java/src/test/kotlin/com/bloomberg/selekt/collections/map/FastLinkedStringMapTest.kt new file mode 100644 index 0000000000..efd8e89869 --- /dev/null +++ b/selekt-java/src/test/kotlin/com/bloomberg/selekt/collections/map/FastLinkedStringMapTest.kt @@ -0,0 +1,157 @@ +/* + * Copyright 2020 Bloomberg Finance L.P. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.bloomberg.selekt.collections.map + +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import org.mockito.kotlin.anyOrNull +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.inOrder +import org.mockito.kotlin.mock +import org.mockito.kotlin.never +import org.mockito.kotlin.same +import org.mockito.kotlin.times +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertSame +import kotlin.test.assertTrue +import kotlin.test.fail + +internal class FastLinkedStringMapTest { + private val first = Any() + private val second = Any() + private val supplier = mock<() -> Any>() + private val disposal: (Any) -> Unit = mock { onGeneric { invoke(it) } doReturn Unit } + + @Test + fun get() { + val map = FastLinkedStringMap(1, 1, false, disposal) + assertSame(first, map.getElsePut("1") { first }) + } + + @Test + fun getTwo() { + val map = FastLinkedStringMap(2, 64, false, disposal) + map.getElsePut("1") { first } + map.getElsePut("2") { second } + assertSame(first, map.getElsePut("1") { fail() }) + assertSame(second, map.getElsePut("2") { fail() }) + } + + @Test + fun getAfterEvict() { + val map = FastLinkedStringMap(1, 1, false, disposal) + map.getElsePut("1") { first } + map.getElsePut("2") { second } + assertFalse(map.containsKey("1")) + assertSame(second, map.getElsePut("2") { fail() }) + } + + @Test + fun remove() { + val map = FastLinkedStringMap(2, 64, false, disposal) + map.getElsePut("1") { first } + map.getElsePut("2") { second } + map.removeKey("1") + inOrder(disposal) { + verify(disposal, times(1)).invoke(same(first)) + } + assertSame(second, map.getElsePut("2") { fail() }) + } + + @Test + fun clear() { + val map = FastLinkedStringMap(2, 64, false, disposal) + map.getElsePut("1") { first } + map.getElsePut("2") { second } + map.clear() + inOrder(disposal) { + verify(disposal, times(1)).invoke(same(first)) + verify(disposal, times(1)).invoke(same(second)) + } + } + + @Test + fun removeWhenEmpty() { + val map = FastLinkedStringMap(1, 1, false, disposal) + assertThrows { + map.removeKey("1") + } + verify(disposal, never()).invoke(anyOrNull()) + } + + @Test + fun removeLastEntryAccessed() { + val map = FastLinkedStringMap(2, 2, true, disposal) + map.getElsePut("1") { first } + map.getElsePut("2") { second } + map.getElsePut("1") { first } + map.removeLastEntry() + assertEquals(1, map.size) + assertTrue(map.containsKey("1")) + assertFalse(map.containsKey("2")) + } + + @Test + fun removeLastEntryInserted() { + val map = FastLinkedStringMap(2, 2, false, disposal) + map.getElsePut("1") { first } + map.getElsePut("2") { second } + map.getElsePut("1") { first } + map.removeLastEntry() + assertEquals(1, map.size) + assertFalse(map.containsKey("1")) + assertTrue(map.containsKey("2")) + } + + @Test + fun evictLeastRecentlyUsed() { + val map = FastLinkedStringMap(1, 1, false, disposal) + map.getElsePut("1") { first } + map.getElsePut("2") { second } + inOrder(disposal) { + verify(disposal, times(1)).invoke(same(first)) + } + } + + @Test + fun getWhenAbsent() { + whenever(supplier.invoke()) doReturn Any() + val map = FastLinkedStringMap(1, 1, false, disposal) + val item = map.getElsePut("1", supplier) + verify(supplier, times(1)).invoke() + assertSame(item, map.getElsePut("1", supplier)) + } + + @Test + fun containsFalse() { + whenever(supplier.invoke()) doReturn Any() + val map = FastLinkedStringMap(1, 1, false, disposal) + map.getElsePut("1", supplier) + assertFalse(map.containsKey("2")) + } + + @Test + fun containsTrue() { + whenever(supplier.invoke()) doReturn Any() + val map = FastLinkedStringMap(1, 1, false, disposal) + map.getElsePut("1", supplier) + assertTrue(map.containsKey("1")) + } +} diff --git a/selekt-java/src/test/kotlin/com/bloomberg/selekt/collections/map/FastStampedStringMapTest.kt b/selekt-java/src/test/kotlin/com/bloomberg/selekt/collections/map/FastStampedStringMapTest.kt new file mode 100644 index 0000000000..398332d533 --- /dev/null +++ b/selekt-java/src/test/kotlin/com/bloomberg/selekt/collections/map/FastStampedStringMapTest.kt @@ -0,0 +1,168 @@ +/* + * Copyright 2024 Bloomberg Finance L.P. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.bloomberg.selekt.collections.map + +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.mock +import org.mockito.kotlin.times +import org.mockito.kotlin.verify +import org.mockito.kotlin.verifyNoMoreInteractions +import org.mockito.kotlin.whenever +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertSame +import kotlin.test.assertTrue +import kotlin.test.fail + +internal class FastStampedStringMapTest { + private val first = Any() + private val second = Any() + private val supplier = mock<() -> Any>() + private val disposal: (Any) -> Unit = mock { onGeneric { invoke(it) } doReturn Unit } + + @Test + fun get() { + val map = FastStampedStringMap(1, disposal) + assertSame(first, map.getElsePut("1") { first }) + } + + @Test + fun sizeOne() { + val map = FastStampedStringMap(1, disposal) + map.getElsePut("1") { first } + assertEquals(1, map.size) + } + + @Test + fun getTwice() { + val map = FastStampedStringMap(1, disposal) + map.getElsePut("1") { first } + assertSame(first, map.getElsePut("1") { fail() }) + } + + @Test + fun getWhenAbsent() { + whenever(supplier.invoke()) doReturn Any() + val map = FastStampedStringMap(1, disposal) + val item = map.getElsePut("1", supplier) + verify(supplier, times(1)).invoke() + assertSame(item, map.getElsePut("1", supplier)) + verifyNoMoreInteractions(supplier) + } + + @Test + fun getTwo() { + val map = FastStampedStringMap(64, disposal) + map.getElsePut("1") { first } + map.getElsePut("2") { second } + assertEquals(2, map.size) + } + + @Test + fun getTwoWithCollisions() { + val map = FastStampedStringMap(1, disposal) + map.getElsePut("1") { first } + map.getElsePut("2") { second } + assertSame(first, map.getElsePut("1") { fail() }) + assertSame(second, map.getElsePut("2") { fail() }) + } + + @Test + fun sizeTwo() { + val map = FastStampedStringMap(1, disposal) + map.getElsePut("1") { first } + map.getElsePut("2") { second } + assertSame(first, map.getElsePut("1") { fail() }) + assertSame(second, map.getElsePut("2") { fail() }) + } + + @Test + fun removeOne() { + val map = FastStampedStringMap(1, disposal) + map.getElsePut("1") { first } + assertSame(first, map.removeEntry("1").value) + } + + @Test + fun removeTwo() { + val map = FastStampedStringMap(2, disposal) + map.getElsePut("1") { first } + map.getElsePut("2") { second } + assertSame(first, map.removeEntry("1").value) + assertSame(second, map.getElsePut("2") { fail() }) + } + + @Test + fun removeTwoWithCollisions() { + val map = FastStampedStringMap(1, disposal) + map.getElsePut("1") { first } + map.getElsePut("2") { second } + assertSame(first, map.removeEntry("1").value) + assertSame(second, map.getElsePut("2") { fail() }) + } + + @Test + fun removeThenSize() { + val map = FastStampedStringMap(1, disposal) + map.getElsePut("1") { first } + map.removeEntry("1") + assertEquals(0, map.size) + } + + @Test + fun removeWhenEmpty() { + val map = FastStampedStringMap(1, disposal) + assertThrows { + map.removeEntry("1") + } + assertEquals(0, map.size) + } + + @Test + fun clear() { + val map = FastStampedStringMap(1, disposal) + map.getElsePut("1") { Any() } + assertEquals(1, map.size) + map.clear() + assertTrue(map.isEmpty()) + } + + @Test + fun clearWhenEmpty() { + val map = FastStampedStringMap(1, disposal) + map.clear() + assertTrue(map.isEmpty()) + } + + @Test + fun containsFalse() { + whenever(supplier.invoke()) doReturn Any() + val map = FastStampedStringMap(1, disposal) + map.getElsePut("1", supplier) + assertFalse(map.containsKey("2")) + } + + @Test + fun containsTrue() { + whenever(supplier.invoke()) doReturn Any() + val map = FastStampedStringMap(1, disposal) + map.getElsePut("1", supplier) + assertTrue(map.containsKey("1")) + } +} diff --git a/selekt-java/src/test/kotlin/com/bloomberg/selekt/collections/map/FastStringMapTest.kt b/selekt-java/src/test/kotlin/com/bloomberg/selekt/collections/map/FastStringMapTest.kt new file mode 100644 index 0000000000..7db457cb34 --- /dev/null +++ b/selekt-java/src/test/kotlin/com/bloomberg/selekt/collections/map/FastStringMapTest.kt @@ -0,0 +1,181 @@ +/* + * Copyright 2024 Bloomberg Finance L.P. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.bloomberg.selekt.collections.map + +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.mock +import org.mockito.kotlin.times +import org.mockito.kotlin.verify +import org.mockito.kotlin.verifyNoMoreInteractions +import org.mockito.kotlin.whenever +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertSame +import kotlin.test.assertTrue +import kotlin.test.fail + +internal class FastStringMapTest { + @Test + fun get() { + val first = Any() + val map = FastStringMap(1) + assertSame(first, map.getEntryElsePut("1") { first }.value) + } + + @Test + fun sizeOne() { + val first = Any() + val map = FastStringMap(1) + map.getEntryElsePut("1") { first } + assertEquals(1, map.size) + } + + @Test + fun getTwice() { + val first = Any() + val map = FastStringMap(1) + map.getEntryElsePut("1") { first } + assertSame(first, map.getEntryElsePut("1") { fail() }.value) + } + + @Test + fun getWhenAbsent() { + val supplier = mock<() -> Any>() + whenever(supplier.invoke()) doReturn Any() + val map = FastStringMap(1) + val item = map.getEntryElsePut("1", supplier) + verify(supplier, times(1)).invoke() + assertSame(item, map.getEntryElsePut("1", supplier)) + verifyNoMoreInteractions(supplier) + } + + @Test + fun getTwo() { + val first = Any() + val second = Any() + val map = FastStringMap(64) + map.getEntryElsePut("1") { first } + map.getEntryElsePut("2") { second } + assertEquals(2, map.size) + } + + @Test + fun getTwoWithCollisions() { + val first = Any() + val second = Any() + val map = FastStringMap(1) + map.getEntryElsePut("1") { first } + map.getEntryElsePut("2") { second } + assertSame(first, map.getEntryElsePut("1") { fail() }.value) + assertSame(second, map.getEntryElsePut("2") { fail() }.value) + } + + @Test + fun sizeTwo() { + val first = Any() + val second = Any() + val map = FastStringMap(1) + map.getEntryElsePut("1") { first } + map.getEntryElsePut("2") { second } + assertSame(first, map.getEntryElsePut("1") { fail() }.value) + assertSame(second, map.getEntryElsePut("2") { fail() }.value) + } + + @Test + fun removeOne() { + val first = Any() + val map = FastStringMap(1) + map.getEntryElsePut("1") { first } + assertSame(first, map.removeEntry("1").value) + } + + @Test + fun removeTwo() { + val first = Any() + val second = Any() + val map = FastStringMap(2) + map.getEntryElsePut("1") { first } + map.getEntryElsePut("2") { second } + assertSame(first, map.removeEntry("1").value) + assertSame(second, map.getEntryElsePut("2") { fail() }.value) + } + + @Test + fun removeTwoWithCollisions() { + val first = Any() + val second = Any() + val map = FastStringMap(1) + map.getEntryElsePut("1") { first } + map.getEntryElsePut("2") { second } + assertSame(first, map.removeEntry("1").value) + assertSame(second, map.getEntryElsePut("2") { fail() }.value) + } + + @Test + fun removeThenSize() { + val first = Any() + val map = FastStringMap(1) + map.getEntryElsePut("1") { first } + map.removeEntry("1") + assertEquals(0, map.size) + } + + @Test + fun removeWhenEmpty() { + val map = FastStringMap(1) + assertThrows { + map.removeEntry("1") + } + assertEquals(0, map.size) + } + + @Test + fun clear() { + val map = FastStringMap(1) + map.getEntryElsePut("1") { Any() } + assertEquals(1, map.size) + map.clear() + assertTrue(map.isEmpty()) + } + + @Test + fun clearWhenEmpty() { + val map = FastStringMap(1) + map.clear() + assertTrue(map.isEmpty()) + } + + @Test + fun containsFalse() { + val supplier = mock<() -> Any>() + whenever(supplier.invoke()) doReturn Any() + val map = FastStringMap(1) + map.getEntryElsePut("1", supplier) + assertFalse(map.containsKey("2")) + } + + @Test + fun containsTrue() { + val supplier = mock<() -> Any>() + whenever(supplier.invoke()) doReturn Any() + val map = FastStringMap(1) + map.getEntryElsePut("1", supplier) + assertTrue(map.containsKey("1")) + } +}