diff --git a/README.md b/README.md index ec2708b..de09c0a 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ # SCache + [![Build Status](https://github.com/evolution-gaming/scache/workflows/CI/badge.svg)](https://github.com/evolution-gaming/scache/actions?query=workflow%3ACI) [![Coverage Status](https://coveralls.io/repos/evolution-gaming/scache/badge.svg)](https://coveralls.io/r/evolution-gaming/scache) [![Codacy Badge](https://app.codacy.com/project/badge/Grade/c44790f3e44a495488141d9eed4aa757)](https://www.codacy.com/gh/evolution-gaming/scache/dashboard?utm_source=github.com&utm_medium=referral&utm_content=evolution-gaming/scache&utm_campaign=Badge_Grade) @@ -7,23 +8,23 @@ ## Key features -* Available for: Scala 2.13.x, 3.3.x and later -* Auto loading of missing values +* Available for: Scala 2.13.x and 3.3.x +* Autoloading of missing values * Expiry of not used records -* Deleting oldest values in case of exceeding max size +* Deleting the oldest values in case of exceeding max size * Tagless Final -* Partition entries by `hashCode` into multiple caches in order to avoid thread contention for some corner cases +* Partition entries by `hashCode` into multiple caches in order to avoid thread contention for some + corner cases ## Introduction -`Cache` is a main entry point towards `scache` library. Most users may want to -call `Cache#expiring` method to get the instance of the trait. The -documentation could be found in source code of +`Cache` is a main entry point towards `scache` library. Most users may want to call `Cache#expiring` +method to get the instance of the trait. The documentation could be found in source code of [Cache.scala](src/main/scala/com/evolution/scache/Cache.scala) and also at [javadoc.io](https://javadoc.io/doc/com.evolution/scache_2.13/latest/com/evolution/scache/Cache$.html). -See [Setup](https://github.com/evolution-gaming/scache#setup) for more details -on how to add the library itself. +See [Setup](https://github.com/evolution-gaming/scache#setup) for more details on how to add the +library itself. ## Cache.scala @@ -35,19 +36,19 @@ trait Cache[F[_], K, V] { def getOrElse(key: K, default: => F[V]): F[V] /** - * Does not run `value` concurrently for the same key - */ + * Does not run `value` concurrently for the same key + */ def getOrUpdate(key: K)(value: => F[V]): F[V] /** - * Does not run `value` concurrently for the same key - * Releasable.release will be called upon key removal from the cache - */ + * Does not run `value` concurrently for the same key + * Releasable.release will be called upon key removal from the cache + */ def getOrUpdateReleasable(key: K)(value: => F[Releasable[F, V]]): F[V] /** - * @return previous value if any, possibly not yet loaded - */ + * @return previous value if any, possibly not yet loaded + */ def put(key: K, value: V): F[F[Option[V]]] @@ -60,19 +61,19 @@ trait Cache[F[_], K, V] { def keys: F[Set[K]] /** - * Might be an expensive call - */ + * Might be an expensive call + */ def values: F[Map[K, F[V]]] /** - * @return previous value if any, possibly not yet loaded - */ + * @return previous value if any, possibly not yet loaded + */ def remove(key: K): F[F[Option[V]]] /** - * Removes loading values from the cache, however does not cancel them - */ + * Removes loading values from the cache, however does not cancel them + */ def clear: F[F[Unit]] } ``` @@ -87,20 +88,20 @@ trait SerialMap[F[_], K, V] { def getOrElse(key: K, default: => F[V]): F[V] /** - * Does not run `value` concurrently for the same key - */ + * Does not run `value` concurrently for the same key + */ def getOrUpdate(key: K, value: => F[V]): F[V] def put(key: K, value: V): F[Option[V]] /** - * `f` will be run serially for the same key, entry will be removed in case of `f` returns `none` - */ + * `f` will be run serially for the same key, entry will be removed in case of `f` returns `None` + */ def modify[A](key: K)(f: Option[V] => F[(Option[V], A)]): F[A] /** - * `f` will be run serially for the same key, entry will be removed in case of `f` returns `none` - */ + * `f` will be run serially for the same key, entry will be removed in case of `f` returns `None` + */ def update[A](key: K)(f: Option[V] => F[Option[V]]): F[Unit] def size: F[Int] @@ -108,8 +109,8 @@ trait SerialMap[F[_], K, V] { def keys: F[Set[K]] /** - * Might be an expensive call - */ + * Might be an expensive call + */ def values: F[Map[K, V]] def remove(key: K): F[Option[V]] @@ -120,8 +121,8 @@ trait SerialMap[F[_], K, V] { ## Setup -`scache`, along with its dependencies, is available on Evolution's JFrog Artifactory. That is why one needs to include -a dependency on https://github.com/evolution-gaming/sbt-artifactory-plugin. +`scache`, along with its dependencies, is available on Evolution's JFrog Artifactory. That is why +one needs to include a dependency on https://github.com/evolution-gaming/sbt-artifactory-plugin. ```scala addSbtPlugin("com.evolution" % "sbt-artifactory-plugin" % "0.0.2") @@ -135,13 +136,151 @@ libraryDependencies += "com.evolution" %% "scache" % " +cp -r benchmark /tmp/scache-old/ +cp build.sbt /tmp/scache-old/build.sbt +cp project/plugins.sbt /tmp/scache-old/project/plugins.sbt + +cd /tmp/scache-old && sbt "benchmark/Jmh/run -rf json -rff /tmp/old.json" +cd - && sbt "benchmark/Jmh/run -rf json -rff /tmp/new.json" +``` + +The `benchmark` project and the JMH plugin come from `build.sbt` and `project/plugins.sbt`, which is +why those two are copied over as well. If the older revision has a different internal API, the +benchmark will not compile there until the affected lines are adjusted. Going back past the `MapRef` +rewrite, for instance, only the `single` flavor needs it: + +```scala +case "single" => LoadingCache.of(LoadingCache.EntryRefs.empty[IO, Int, Int]) +``` + +Finally, `git worktree remove /tmp/scache-old` when done. + +## Migrating to 7.0 + +The cache state moved from a single `Ref[F, Map[K, EntryRef]]` to a per-key `MapRef` over a +`ConcurrentHashMap`. What that means for the users: + +**Type classes.** `Cache.loading`, `Cache.expiring`, `SerialMap.of`, and the factory-style +`SerialMap.apply[F]` now ask for `Async[F]` instead of `Concurrent[F]` / `Temporal[F]`, because +the new state needs `Sync` for +the `ConcurrentHashMap` next to `Concurrent` for the fibers. Nothing to do for `IO` or for any stack +that already has an `Async` instance, otherwise the call sites have to provide one. + +**Removed.** `LoadingCache.EntryRefs`, and with it the overload +`LoadingCache.of(map: EntryRefs[F, K, V])`. Both were `private[scache]`, so this only affects code +inside this library. `LoadingCache.of[F, K, V]` replaces them. + +**No more contention failures.** `getOrUpdate` used to give up with +`IllegalStateException("extreme contention")` after 10000 lost CAS attempts on the shared state. +Operations on distinct keys no longer contend at all, so the limit is gone along with that failure +mode. + +**Cancelling a load cleans up.** Cancelling `getOrUpdate` now removes the entry it installed and +fails everyone waiting for that entry with `CancelledError`, instead of leaving the key unusable and +its waiters blocked forever. Note that the load is shared, so this reaches callers that were not +cancelled themselves: if two requests ask for the same key, the first one runs the load and the +second one waits for it, then a timeout cancelling the first fails the second with `CancelledError` +as well. It gets to retry, where before it would have hung. + +**Loads can expire.** `ExpiringCache` evicts entries that have been loading longer than +`Config.loadingTimeout`, failing their waiters with `ExpiredError`. The load itself is not +cancelled, only detached from the cache. `loadingTimeout` defaults to the smaller of +`expireAfterRead` and `expireAfterWrite`, so set it explicitly if the loads are legitimately slower +than the expiration. + +**Enumeration is weakly consistent.** `keys`, `values`, `values1`, `size`, `foldMap` and +`foldMapPar` are served by the `ConcurrentHashMap` and no longer observe an atomic snapshot of the +map: an entry added or removed concurrently may or may not be included. + ## Release process -The release process is based on Git tags and makes use of [evolution-gaming/scala-github-actions](https://github.com/evolution-gaming/scala-github-actions) which uses [sbt-dynver](https://github.com/sbt/sbt-dynver) to automatically obtain the version from the latest Git tag. The flow is defined in `.github/workflows/release.yml`. + +The release process is based on Git tags and makes use +of [evolution-gaming/scala-github-actions](https://github.com/evolution-gaming/scala-github-actions) +which uses [sbt-dynver](https://github.com/sbt/sbt-dynver) to automatically obtain the version from +the latest Git tag. The flow is defined in `.github/workflows/release.yml`. A typical release process is as follows: -1. Create and push a new Git tag. The version should be in the format `vX.Y.Z` (example: `v4.1.0`). Example: `git tag v4.1.0 && git push origin v4.1.0` -2. On success, a new GitHub release is automatically created with a calculated diff and auto-generated release notes. You can see it on `Releases` page, change the description if needed -3. On failure, the tag is deleted from the remote repository. Please note that your local tag isn't deleted, so if the failure is recoverable then you can delete the local tag and try again (an example of *unrecoverable* failure is successfully publishing only a few of the artifacts to Artifactory which means a new attempt would fail since Artifactory doesn't allow overwriting its contents) + +1. Create and push a new Git tag. The version should be in the format `vX.Y.Z` (example: `v4.1.0`). + Example: `git tag v4.1.0 && git push origin v4.1.0` +2. On success, a new GitHub release is automatically created with a calculated diff and + auto-generated release notes. You can see it on `Releases` page, change the description if needed +3. On failure, the tag is deleted from the remote repository. Please note that your local tag isn't + deleted, so if the failure is recoverable then you can delete the local tag and try again (an + example of *unrecoverable* failure is successfully publishing only a few of the artifacts to + Artifactory which means a new attempt would fail since Artifactory doesn't allow overwriting its + contents) diff --git a/benchmark/results/mapref.json b/benchmark/results/mapref.json new file mode 100644 index 0000000..fa3e94f --- /dev/null +++ b/benchmark/results/mapref.json @@ -0,0 +1,2110 @@ +[ + { + "jmhVersion" : "1.37", + "benchmark" : "com.evolution.scache.bench.CacheBenchmark.containsRandomKeys", + "mode" : "thrpt", + "threads" : 1, + "forks" : 1, + "jvm" : "/usr/lib/jvm/temurin-25-jdk-arm64/bin/java", + "jvmArgs" : [ + ], + "jdkVersion" : "25.0.4", + "vmName" : "OpenJDK 64-Bit Server VM", + "vmVersion" : "25.0.4+7-LTS", + "warmupIterations" : 1, + "warmupTime" : "3 s", + "warmupBatchSize" : 1, + "measurementIterations" : 5, + "measurementTime" : "2 s", + "measurementBatchSize" : 1, + "params" : { + "flavor" : "single" + }, + "primaryMetric" : { + "score" : 3.1969619388670303E7, + "scoreError" : 3.369091011404188E7, + "scoreConfidence" : [ + -1721290.7253715768, + 6.566052950271218E7 + ], + "scorePercentiles" : { + "0.0" : 2.0373560202060994E7, + "50.0" : 3.717077135867445E7, + "90.0" : 3.890113688088267E7, + "95.0" : 3.890113688088267E7, + "99.0" : 3.890113688088267E7, + "99.9" : 3.890113688088267E7, + "99.99" : 3.890113688088267E7, + "99.999" : 3.890113688088267E7, + "99.9999" : 3.890113688088267E7, + "100.0" : 3.890113688088267E7 + }, + "scoreUnit" : "ops/s", + "rawData" : [ + [ + 3.890113688088267E7, + 3.717077135867445E7, + 2.475512513453481E7, + 2.0373560202060994E7, + 3.8647503367198594E7 + ] + ] + }, + "secondaryMetrics" : { + } + }, + { + "jmhVersion" : "1.37", + "benchmark" : "com.evolution.scache.bench.CacheBenchmark.containsRandomKeys", + "mode" : "thrpt", + "threads" : 1, + "forks" : 1, + "jvm" : "/usr/lib/jvm/temurin-25-jdk-arm64/bin/java", + "jvmArgs" : [ + ], + "jdkVersion" : "25.0.4", + "vmName" : "OpenJDK 64-Bit Server VM", + "vmVersion" : "25.0.4+7-LTS", + "warmupIterations" : 1, + "warmupTime" : "3 s", + "warmupBatchSize" : 1, + "measurementIterations" : 5, + "measurementTime" : "2 s", + "measurementBatchSize" : 1, + "params" : { + "flavor" : "partitioned" + }, + "primaryMetric" : { + "score" : 3.369634244912512E7, + "scoreError" : 1462710.1392786421, + "scoreConfidence" : [ + 3.2233632309846476E7, + 3.515905258840376E7 + ], + "scorePercentiles" : { + "0.0" : 3.316293488205917E7, + "50.0" : 3.3857893332021646E7, + "90.0" : 3.409063406529033E7, + "95.0" : 3.409063406529033E7, + "99.0" : 3.409063406529033E7, + "99.9" : 3.409063406529033E7, + "99.99" : 3.409063406529033E7, + "99.999" : 3.409063406529033E7, + "99.9999" : 3.409063406529033E7, + "100.0" : 3.409063406529033E7 + }, + "scoreUnit" : "ops/s", + "rawData" : [ + [ + 3.316293488205917E7, + 3.392053759598523E7, + 3.3449712370269198E7, + 3.3857893332021646E7, + 3.409063406529033E7 + ] + ] + }, + "secondaryMetrics" : { + } + }, + { + "jmhVersion" : "1.37", + "benchmark" : "com.evolution.scache.bench.CacheBenchmark.containsRandomKeys", + "mode" : "thrpt", + "threads" : 1, + "forks" : 1, + "jvm" : "/usr/lib/jvm/temurin-25-jdk-arm64/bin/java", + "jvmArgs" : [ + ], + "jdkVersion" : "25.0.4", + "vmName" : "OpenJDK 64-Bit Server VM", + "vmVersion" : "25.0.4+7-LTS", + "warmupIterations" : 1, + "warmupTime" : "3 s", + "warmupBatchSize" : 1, + "measurementIterations" : 5, + "measurementTime" : "2 s", + "measurementBatchSize" : 1, + "params" : { + "flavor" : "expiring" + }, + "primaryMetric" : { + "score" : 3.367826930484824E7, + "scoreError" : 964179.5461959628, + "scoreConfidence" : [ + 3.2714089758652277E7, + 3.46424488510442E7 + ], + "scorePercentiles" : { + "0.0" : 3.3411977775183685E7, + "50.0" : 3.375408739218799E7, + "90.0" : 3.392657087366003E7, + "95.0" : 3.392657087366003E7, + "99.0" : 3.392657087366003E7, + "99.9" : 3.392657087366003E7, + "99.99" : 3.392657087366003E7, + "99.999" : 3.392657087366003E7, + "99.9999" : 3.392657087366003E7, + "100.0" : 3.392657087366003E7 + }, + "scoreUnit" : "ops/s", + "rawData" : [ + [ + 3.3413978863959692E7, + 3.388473161924981E7, + 3.3411977775183685E7, + 3.392657087366003E7, + 3.375408739218799E7 + ] + ] + }, + "secondaryMetrics" : { + } + }, + { + "jmhVersion" : "1.37", + "benchmark" : "com.evolution.scache.bench.CacheBenchmark.foldMapWholeCache", + "mode" : "thrpt", + "threads" : 1, + "forks" : 1, + "jvm" : "/usr/lib/jvm/temurin-25-jdk-arm64/bin/java", + "jvmArgs" : [ + ], + "jdkVersion" : "25.0.4", + "vmName" : "OpenJDK 64-Bit Server VM", + "vmVersion" : "25.0.4+7-LTS", + "warmupIterations" : 1, + "warmupTime" : "3 s", + "warmupBatchSize" : 1, + "measurementIterations" : 5, + "measurementTime" : "2 s", + "measurementBatchSize" : 1, + "params" : { + "flavor" : "single" + }, + "primaryMetric" : { + "score" : 1146.354938255863, + "scoreError" : 54.39132606319258, + "scoreConfidence" : [ + 1091.9636121926706, + 1200.7462643190556 + ], + "scorePercentiles" : { + "0.0" : 1128.6117569912892, + "50.0" : 1149.4846693874074, + "90.0" : 1165.0450493257158, + "95.0" : 1165.0450493257158, + "99.0" : 1165.0450493257158, + "99.9" : 1165.0450493257158, + "99.99" : 1165.0450493257158, + "99.999" : 1165.0450493257158, + "99.9999" : 1165.0450493257158, + "100.0" : 1165.0450493257158 + }, + "scoreUnit" : "ops/s", + "rawData" : [ + [ + 1128.6117569912892, + 1151.9273154924313, + 1165.0450493257158, + 1149.4846693874074, + 1136.7059000824718 + ] + ] + }, + "secondaryMetrics" : { + } + }, + { + "jmhVersion" : "1.37", + "benchmark" : "com.evolution.scache.bench.CacheBenchmark.foldMapWholeCache", + "mode" : "thrpt", + "threads" : 1, + "forks" : 1, + "jvm" : "/usr/lib/jvm/temurin-25-jdk-arm64/bin/java", + "jvmArgs" : [ + ], + "jdkVersion" : "25.0.4", + "vmName" : "OpenJDK 64-Bit Server VM", + "vmVersion" : "25.0.4+7-LTS", + "warmupIterations" : 1, + "warmupTime" : "3 s", + "warmupBatchSize" : 1, + "measurementIterations" : 5, + "measurementTime" : "2 s", + "measurementBatchSize" : 1, + "params" : { + "flavor" : "partitioned" + }, + "primaryMetric" : { + "score" : 1104.8148849227277, + "scoreError" : 72.82069539939145, + "scoreConfidence" : [ + 1031.9941895233362, + 1177.6355803221193 + ], + "scorePercentiles" : { + "0.0" : 1087.1582899807274, + "50.0" : 1095.1061264648129, + "90.0" : 1128.0064030101485, + "95.0" : 1128.0064030101485, + "99.0" : 1128.0064030101485, + "99.9" : 1128.0064030101485, + "99.99" : 1128.0064030101485, + "99.999" : 1128.0064030101485, + "99.9999" : 1128.0064030101485, + "100.0" : 1128.0064030101485 + }, + "scoreUnit" : "ops/s", + "rawData" : [ + [ + 1087.1582899807274, + 1128.0064030101485, + 1122.3610599836188, + 1091.442545174331, + 1095.1061264648129 + ] + ] + }, + "secondaryMetrics" : { + } + }, + { + "jmhVersion" : "1.37", + "benchmark" : "com.evolution.scache.bench.CacheBenchmark.foldMapWholeCache", + "mode" : "thrpt", + "threads" : 1, + "forks" : 1, + "jvm" : "/usr/lib/jvm/temurin-25-jdk-arm64/bin/java", + "jvmArgs" : [ + ], + "jdkVersion" : "25.0.4", + "vmName" : "OpenJDK 64-Bit Server VM", + "vmVersion" : "25.0.4+7-LTS", + "warmupIterations" : 1, + "warmupTime" : "3 s", + "warmupBatchSize" : 1, + "measurementIterations" : 5, + "measurementTime" : "2 s", + "measurementBatchSize" : 1, + "params" : { + "flavor" : "expiring" + }, + "primaryMetric" : { + "score" : 996.2937519226587, + "scoreError" : 55.13808937840677, + "scoreConfidence" : [ + 941.1556625442519, + 1051.4318413010653 + ], + "scorePercentiles" : { + "0.0" : 976.8675998491138, + "50.0" : 1000.1594280106416, + "90.0" : 1010.6124720573836, + "95.0" : 1010.6124720573836, + "99.0" : 1010.6124720573836, + "99.9" : 1010.6124720573836, + "99.99" : 1010.6124720573836, + "99.999" : 1010.6124720573836, + "99.9999" : 1010.6124720573836, + "100.0" : 1010.6124720573836 + }, + "scoreUnit" : "ops/s", + "rawData" : [ + [ + 976.8675998491138, + 986.3780063044616, + 1000.1594280106416, + 1007.4512533916926, + 1010.6124720573836 + ] + ] + }, + "secondaryMetrics" : { + } + }, + { + "jmhVersion" : "1.37", + "benchmark" : "com.evolution.scache.bench.CacheBenchmark.get1HitRandomKeys", + "mode" : "thrpt", + "threads" : 1, + "forks" : 1, + "jvm" : "/usr/lib/jvm/temurin-25-jdk-arm64/bin/java", + "jvmArgs" : [ + ], + "jdkVersion" : "25.0.4", + "vmName" : "OpenJDK 64-Bit Server VM", + "vmVersion" : "25.0.4+7-LTS", + "warmupIterations" : 1, + "warmupTime" : "3 s", + "warmupBatchSize" : 1, + "measurementIterations" : 5, + "measurementTime" : "2 s", + "measurementBatchSize" : 1, + "params" : { + "flavor" : "single" + }, + "primaryMetric" : { + "score" : 2.3656329352353036E7, + "scoreError" : 240852.02929078127, + "scoreConfidence" : [ + 2.3415477323062256E7, + 2.3897181381643817E7 + ], + "scorePercentiles" : { + "0.0" : 2.357634653523525E7, + "50.0" : 2.3638906703739434E7, + "90.0" : 2.3727581681384802E7, + "95.0" : 2.3727581681384802E7, + "99.0" : 2.3727581681384802E7, + "99.9" : 2.3727581681384802E7, + "99.99" : 2.3727581681384802E7, + "99.999" : 2.3727581681384802E7, + "99.9999" : 2.3727581681384802E7, + "100.0" : 2.3727581681384802E7 + }, + "scoreUnit" : "ops/s", + "rawData" : [ + [ + 2.357634653523525E7, + 2.362739654320742E7, + 2.3727581681384802E7, + 2.3638906703739434E7, + 2.3711415298198283E7 + ] + ] + }, + "secondaryMetrics" : { + } + }, + { + "jmhVersion" : "1.37", + "benchmark" : "com.evolution.scache.bench.CacheBenchmark.get1HitRandomKeys", + "mode" : "thrpt", + "threads" : 1, + "forks" : 1, + "jvm" : "/usr/lib/jvm/temurin-25-jdk-arm64/bin/java", + "jvmArgs" : [ + ], + "jdkVersion" : "25.0.4", + "vmName" : "OpenJDK 64-Bit Server VM", + "vmVersion" : "25.0.4+7-LTS", + "warmupIterations" : 1, + "warmupTime" : "3 s", + "warmupBatchSize" : 1, + "measurementIterations" : 5, + "measurementTime" : "2 s", + "measurementBatchSize" : 1, + "params" : { + "flavor" : "partitioned" + }, + "primaryMetric" : { + "score" : 2.214371938171286E7, + "scoreError" : 224303.0131105092, + "scoreConfidence" : [ + 2.191941636860235E7, + 2.2368022394823372E7 + ], + "scorePercentiles" : { + "0.0" : 2.2055403163757384E7, + "50.0" : 2.214594285170191E7, + "90.0" : 2.221896973403725E7, + "95.0" : 2.221896973403725E7, + "99.0" : 2.221896973403725E7, + "99.9" : 2.221896973403725E7, + "99.99" : 2.221896973403725E7, + "99.999" : 2.221896973403725E7, + "99.9999" : 2.221896973403725E7, + "100.0" : 2.221896973403725E7 + }, + "scoreUnit" : "ops/s", + "rawData" : [ + [ + 2.2055403163757384E7, + 2.2144317659965023E7, + 2.221896973403725E7, + 2.2153963499102745E7, + 2.214594285170191E7 + ] + ] + }, + "secondaryMetrics" : { + } + }, + { + "jmhVersion" : "1.37", + "benchmark" : "com.evolution.scache.bench.CacheBenchmark.get1HitRandomKeys", + "mode" : "thrpt", + "threads" : 1, + "forks" : 1, + "jvm" : "/usr/lib/jvm/temurin-25-jdk-arm64/bin/java", + "jvmArgs" : [ + ], + "jdkVersion" : "25.0.4", + "vmName" : "OpenJDK 64-Bit Server VM", + "vmVersion" : "25.0.4+7-LTS", + "warmupIterations" : 1, + "warmupTime" : "3 s", + "warmupBatchSize" : 1, + "measurementIterations" : 5, + "measurementTime" : "2 s", + "measurementBatchSize" : 1, + "params" : { + "flavor" : "expiring" + }, + "primaryMetric" : { + "score" : 1.2918849083181381E7, + "scoreError" : 1122776.0073354242, + "scoreConfidence" : [ + 1.1796073075845957E7, + 1.4041625090516806E7 + ], + "scorePercentiles" : { + "0.0" : 1.2502198162162138E7, + "50.0" : 1.2902247919082075E7, + "90.0" : 1.3275992024473783E7, + "95.0" : 1.3275992024473783E7, + "99.0" : 1.3275992024473783E7, + "99.9" : 1.3275992024473783E7, + "99.99" : 1.3275992024473783E7, + "99.999" : 1.3275992024473783E7, + "99.9999" : 1.3275992024473783E7, + "100.0" : 1.3275992024473783E7 + }, + "scoreUnit" : "ops/s", + "rawData" : [ + [ + 1.2502198162162138E7, + 1.2823191658055855E7, + 1.2902247919082075E7, + 1.3090615652133044E7, + 1.3275992024473783E7 + ] + ] + }, + "secondaryMetrics" : { + } + }, + { + "jmhVersion" : "1.37", + "benchmark" : "com.evolution.scache.bench.CacheBenchmark.getHitRandomKeys", + "mode" : "thrpt", + "threads" : 1, + "forks" : 1, + "jvm" : "/usr/lib/jvm/temurin-25-jdk-arm64/bin/java", + "jvmArgs" : [ + ], + "jdkVersion" : "25.0.4", + "vmName" : "OpenJDK 64-Bit Server VM", + "vmVersion" : "25.0.4+7-LTS", + "warmupIterations" : 1, + "warmupTime" : "3 s", + "warmupBatchSize" : 1, + "measurementIterations" : 5, + "measurementTime" : "2 s", + "measurementBatchSize" : 1, + "params" : { + "flavor" : "single" + }, + "primaryMetric" : { + "score" : 2.610205362810062E7, + "scoreError" : 300192.6968273039, + "scoreConfidence" : [ + 2.5801860931273315E7, + 2.6402246324927922E7 + ], + "scorePercentiles" : { + "0.0" : 2.5975453183753625E7, + "50.0" : 2.611428963881623E7, + "90.0" : 2.618940271968549E7, + "95.0" : 2.618940271968549E7, + "99.0" : 2.618940271968549E7, + "99.9" : 2.618940271968549E7, + "99.99" : 2.618940271968549E7, + "99.999" : 2.618940271968549E7, + "99.9999" : 2.618940271968549E7, + "100.0" : 2.618940271968549E7 + }, + "scoreUnit" : "ops/s", + "rawData" : [ + [ + 2.6123877195745643E7, + 2.618940271968549E7, + 2.611428963881623E7, + 2.610724540250211E7, + 2.5975453183753625E7 + ] + ] + }, + "secondaryMetrics" : { + } + }, + { + "jmhVersion" : "1.37", + "benchmark" : "com.evolution.scache.bench.CacheBenchmark.getHitRandomKeys", + "mode" : "thrpt", + "threads" : 1, + "forks" : 1, + "jvm" : "/usr/lib/jvm/temurin-25-jdk-arm64/bin/java", + "jvmArgs" : [ + ], + "jdkVersion" : "25.0.4", + "vmName" : "OpenJDK 64-Bit Server VM", + "vmVersion" : "25.0.4+7-LTS", + "warmupIterations" : 1, + "warmupTime" : "3 s", + "warmupBatchSize" : 1, + "measurementIterations" : 5, + "measurementTime" : "2 s", + "measurementBatchSize" : 1, + "params" : { + "flavor" : "partitioned" + }, + "primaryMetric" : { + "score" : 2.3022334327981513E7, + "scoreError" : 2837115.7378140083, + "scoreConfidence" : [ + 2.0185218590167504E7, + 2.5859450065795522E7 + ], + "scorePercentiles" : { + "0.0" : 2.1835290389886387E7, + "50.0" : 2.3179949818595886E7, + "90.0" : 2.386566532613996E7, + "95.0" : 2.386566532613996E7, + "99.0" : 2.386566532613996E7, + "99.9" : 2.386566532613996E7, + "99.99" : 2.386566532613996E7, + "99.999" : 2.386566532613996E7, + "99.9999" : 2.386566532613996E7, + "100.0" : 2.386566532613996E7 + }, + "scoreUnit" : "ops/s", + "rawData" : [ + [ + 2.1835290389886387E7, + 2.386566532613996E7, + 2.304830678968735E7, + 2.3179949818595886E7, + 2.3182459315597977E7 + ] + ] + }, + "secondaryMetrics" : { + } + }, + { + "jmhVersion" : "1.37", + "benchmark" : "com.evolution.scache.bench.CacheBenchmark.getHitRandomKeys", + "mode" : "thrpt", + "threads" : 1, + "forks" : 1, + "jvm" : "/usr/lib/jvm/temurin-25-jdk-arm64/bin/java", + "jvmArgs" : [ + ], + "jdkVersion" : "25.0.4", + "vmName" : "OpenJDK 64-Bit Server VM", + "vmVersion" : "25.0.4+7-LTS", + "warmupIterations" : 1, + "warmupTime" : "3 s", + "warmupBatchSize" : 1, + "measurementIterations" : 5, + "measurementTime" : "2 s", + "measurementBatchSize" : 1, + "params" : { + "flavor" : "expiring" + }, + "primaryMetric" : { + "score" : 1.322145857456372E7, + "scoreError" : 804864.2979845649, + "scoreConfidence" : [ + 1.2416594276579155E7, + 1.4026322872548284E7 + ], + "scorePercentiles" : { + "0.0" : 1.2980734554712847E7, + "50.0" : 1.3247878855106996E7, + "90.0" : 1.3531393857558675E7, + "95.0" : 1.3531393857558675E7, + "99.0" : 1.3531393857558675E7, + "99.9" : 1.3531393857558675E7, + "99.99" : 1.3531393857558675E7, + "99.999" : 1.3531393857558675E7, + "99.9999" : 1.3531393857558675E7, + "100.0" : 1.3531393857558675E7 + }, + "scoreUnit" : "ops/s", + "rawData" : [ + [ + 1.2980734554712847E7, + 1.3247878855106996E7, + 1.3261619945113301E7, + 1.308566566032678E7, + 1.3531393857558675E7 + ] + ] + }, + "secondaryMetrics" : { + } + }, + { + "jmhVersion" : "1.37", + "benchmark" : "com.evolution.scache.bench.CacheBenchmark.getOrUpdateHitRandomKeys", + "mode" : "thrpt", + "threads" : 1, + "forks" : 1, + "jvm" : "/usr/lib/jvm/temurin-25-jdk-arm64/bin/java", + "jvmArgs" : [ + ], + "jdkVersion" : "25.0.4", + "vmName" : "OpenJDK 64-Bit Server VM", + "vmVersion" : "25.0.4+7-LTS", + "warmupIterations" : 1, + "warmupTime" : "3 s", + "warmupBatchSize" : 1, + "measurementIterations" : 5, + "measurementTime" : "2 s", + "measurementBatchSize" : 1, + "params" : { + "flavor" : "single" + }, + "primaryMetric" : { + "score" : 1.2600264369109768E7, + "scoreError" : 1955554.316665008, + "scoreConfidence" : [ + 1.064471005244476E7, + 1.4555818685774777E7 + ], + "scorePercentiles" : { + "0.0" : 1.2257446670466183E7, + "50.0" : 1.2444519889789457E7, + "90.0" : 1.3486929475731004E7, + "95.0" : 1.3486929475731004E7, + "99.0" : 1.3486929475731004E7, + "99.9" : 1.3486929475731004E7, + "99.99" : 1.3486929475731004E7, + "99.999" : 1.3486929475731004E7, + "99.9999" : 1.3486929475731004E7, + "100.0" : 1.3486929475731004E7 + }, + "scoreUnit" : "ops/s", + "rawData" : [ + [ + 1.2257446670466183E7, + 1.228708213287984E7, + 1.2525343676682362E7, + 1.3486929475731004E7, + 1.2444519889789457E7 + ] + ] + }, + "secondaryMetrics" : { + } + }, + { + "jmhVersion" : "1.37", + "benchmark" : "com.evolution.scache.bench.CacheBenchmark.getOrUpdateHitRandomKeys", + "mode" : "thrpt", + "threads" : 1, + "forks" : 1, + "jvm" : "/usr/lib/jvm/temurin-25-jdk-arm64/bin/java", + "jvmArgs" : [ + ], + "jdkVersion" : "25.0.4", + "vmName" : "OpenJDK 64-Bit Server VM", + "vmVersion" : "25.0.4+7-LTS", + "warmupIterations" : 1, + "warmupTime" : "3 s", + "warmupBatchSize" : 1, + "measurementIterations" : 5, + "measurementTime" : "2 s", + "measurementBatchSize" : 1, + "params" : { + "flavor" : "partitioned" + }, + "primaryMetric" : { + "score" : 1.1455778008713856E7, + "scoreError" : 1604837.5158296826, + "scoreConfidence" : [ + 9850940.492884174, + 1.3060615524543539E7 + ], + "scorePercentiles" : { + "0.0" : 1.0712238977504678E7, + "50.0" : 1.1632535370421866E7, + "90.0" : 1.1678866180872906E7, + "95.0" : 1.1678866180872906E7, + "99.0" : 1.1678866180872906E7, + "99.9" : 1.1678866180872906E7, + "99.99" : 1.1678866180872906E7, + "99.999" : 1.1678866180872906E7, + "99.9999" : 1.1678866180872906E7, + "100.0" : 1.1678866180872906E7 + }, + "scoreUnit" : "ops/s", + "rawData" : [ + [ + 1.0712238977504678E7, + 1.1597058798032353E7, + 1.1632535370421866E7, + 1.1678866180872906E7, + 1.165819071673748E7 + ] + ] + }, + "secondaryMetrics" : { + } + }, + { + "jmhVersion" : "1.37", + "benchmark" : "com.evolution.scache.bench.CacheBenchmark.getOrUpdateHitRandomKeys", + "mode" : "thrpt", + "threads" : 1, + "forks" : 1, + "jvm" : "/usr/lib/jvm/temurin-25-jdk-arm64/bin/java", + "jvmArgs" : [ + ], + "jdkVersion" : "25.0.4", + "vmName" : "OpenJDK 64-Bit Server VM", + "vmVersion" : "25.0.4+7-LTS", + "warmupIterations" : 1, + "warmupTime" : "3 s", + "warmupBatchSize" : 1, + "measurementIterations" : 5, + "measurementTime" : "2 s", + "measurementBatchSize" : 1, + "params" : { + "flavor" : "expiring" + }, + "primaryMetric" : { + "score" : 9117919.948766232, + "scoreError" : 2958044.5409369254, + "scoreConfidence" : [ + 6159875.407829306, + 1.2075964489703156E7 + ], + "scorePercentiles" : { + "0.0" : 7912061.891600653, + "50.0" : 9587634.745973224, + "90.0" : 9675586.113721954, + "95.0" : 9675586.113721954, + "99.0" : 9675586.113721954, + "99.9" : 9675586.113721954, + "99.99" : 9675586.113721954, + "99.999" : 9675586.113721954, + "99.9999" : 9675586.113721954, + "100.0" : 9675586.113721954 + }, + "scoreUnit" : "ops/s", + "rawData" : [ + [ + 7912061.891600653, + 8783573.435274316, + 9587634.745973224, + 9675586.113721954, + 9630743.557261016 + ] + ] + }, + "secondaryMetrics" : { + } + }, + { + "jmhVersion" : "1.37", + "benchmark" : "com.evolution.scache.bench.CacheBenchmark.getOrUpdateHitSingleHotKey", + "mode" : "thrpt", + "threads" : 1, + "forks" : 1, + "jvm" : "/usr/lib/jvm/temurin-25-jdk-arm64/bin/java", + "jvmArgs" : [ + ], + "jdkVersion" : "25.0.4", + "vmName" : "OpenJDK 64-Bit Server VM", + "vmVersion" : "25.0.4+7-LTS", + "warmupIterations" : 1, + "warmupTime" : "3 s", + "warmupBatchSize" : 1, + "measurementIterations" : 5, + "measurementTime" : "2 s", + "measurementBatchSize" : 1, + "params" : { + "flavor" : "single" + }, + "primaryMetric" : { + "score" : 1.3816754329865208E7, + "scoreError" : 1215345.5910112457, + "scoreConfidence" : [ + 1.2601408738853961E7, + 1.5032099920876455E7 + ], + "scorePercentiles" : { + "0.0" : 1.3265795371024773E7, + "50.0" : 1.38960232733952E7, + "90.0" : 1.4059458464590153E7, + "95.0" : 1.4059458464590153E7, + "99.0" : 1.4059458464590153E7, + "99.9" : 1.4059458464590153E7, + "99.99" : 1.4059458464590153E7, + "99.999" : 1.4059458464590153E7, + "99.9999" : 1.4059458464590153E7, + "100.0" : 1.4059458464590153E7 + }, + "scoreUnit" : "ops/s", + "rawData" : [ + [ + 1.3889381144000364E7, + 1.4059458464590153E7, + 1.3973113396315552E7, + 1.3265795371024773E7, + 1.38960232733952E7 + ] + ] + }, + "secondaryMetrics" : { + } + }, + { + "jmhVersion" : "1.37", + "benchmark" : "com.evolution.scache.bench.CacheBenchmark.getOrUpdateHitSingleHotKey", + "mode" : "thrpt", + "threads" : 1, + "forks" : 1, + "jvm" : "/usr/lib/jvm/temurin-25-jdk-arm64/bin/java", + "jvmArgs" : [ + ], + "jdkVersion" : "25.0.4", + "vmName" : "OpenJDK 64-Bit Server VM", + "vmVersion" : "25.0.4+7-LTS", + "warmupIterations" : 1, + "warmupTime" : "3 s", + "warmupBatchSize" : 1, + "measurementIterations" : 5, + "measurementTime" : "2 s", + "measurementBatchSize" : 1, + "params" : { + "flavor" : "partitioned" + }, + "primaryMetric" : { + "score" : 1.258033162435461E7, + "scoreError" : 1669391.8295807883, + "scoreConfidence" : [ + 1.0910939794773823E7, + 1.4249723453935398E7 + ], + "scorePercentiles" : { + "0.0" : 1.1912368882887961E7, + "50.0" : 1.2547707945330203E7, + "90.0" : 1.2987591005178122E7, + "95.0" : 1.2987591005178122E7, + "99.0" : 1.2987591005178122E7, + "99.9" : 1.2987591005178122E7, + "99.99" : 1.2987591005178122E7, + "99.999" : 1.2987591005178122E7, + "99.9999" : 1.2987591005178122E7, + "100.0" : 1.2987591005178122E7 + }, + "scoreUnit" : "ops/s", + "rawData" : [ + [ + 1.2508255685273835E7, + 1.2547707945330203E7, + 1.1912368882887961E7, + 1.2945734603102926E7, + 1.2987591005178122E7 + ] + ] + }, + "secondaryMetrics" : { + } + }, + { + "jmhVersion" : "1.37", + "benchmark" : "com.evolution.scache.bench.CacheBenchmark.getOrUpdateHitSingleHotKey", + "mode" : "thrpt", + "threads" : 1, + "forks" : 1, + "jvm" : "/usr/lib/jvm/temurin-25-jdk-arm64/bin/java", + "jvmArgs" : [ + ], + "jdkVersion" : "25.0.4", + "vmName" : "OpenJDK 64-Bit Server VM", + "vmVersion" : "25.0.4+7-LTS", + "warmupIterations" : 1, + "warmupTime" : "3 s", + "warmupBatchSize" : 1, + "measurementIterations" : 5, + "measurementTime" : "2 s", + "measurementBatchSize" : 1, + "params" : { + "flavor" : "expiring" + }, + "primaryMetric" : { + "score" : 1.0402956935076972E7, + "scoreError" : 1844031.4084078763, + "scoreConfidence" : [ + 8558925.526669096, + 1.2246988343484849E7 + ], + "scorePercentiles" : { + "0.0" : 9623459.366173595, + "50.0" : 1.0576976635197667E7, + "90.0" : 1.0816858836448867E7, + "95.0" : 1.0816858836448867E7, + "99.0" : 1.0816858836448867E7, + "99.9" : 1.0816858836448867E7, + "99.99" : 1.0816858836448867E7, + "99.999" : 1.0816858836448867E7, + "99.9999" : 1.0816858836448867E7, + "100.0" : 1.0816858836448867E7 + }, + "scoreUnit" : "ops/s", + "rawData" : [ + [ + 9623459.366173595, + 1.0576976635197667E7, + 1.0286879032117361E7, + 1.0816858836448867E7, + 1.071061080544737E7 + ] + ] + }, + "secondaryMetrics" : { + } + }, + { + "jmhVersion" : "1.37", + "benchmark" : "com.evolution.scache.bench.CacheBenchmark.getOrUpdateInsertDistinctKeys", + "mode" : "thrpt", + "threads" : 1, + "forks" : 1, + "jvm" : "/usr/lib/jvm/temurin-25-jdk-arm64/bin/java", + "jvmArgs" : [ + ], + "jdkVersion" : "25.0.4", + "vmName" : "OpenJDK 64-Bit Server VM", + "vmVersion" : "25.0.4+7-LTS", + "warmupIterations" : 1, + "warmupTime" : "3 s", + "warmupBatchSize" : 1, + "measurementIterations" : 5, + "measurementTime" : "2 s", + "measurementBatchSize" : 1, + "params" : { + "flavor" : "single" + }, + "primaryMetric" : { + "score" : 2064391.2397355475, + "scoreError" : 495114.50418752234, + "scoreConfidence" : [ + 1569276.735548025, + 2559505.74392307 + ], + "scorePercentiles" : { + "0.0" : 1921090.5339368568, + "50.0" : 2032096.8985423783, + "90.0" : 2251906.465421067, + "95.0" : 2251906.465421067, + "99.0" : 2251906.465421067, + "99.9" : 2251906.465421067, + "99.99" : 2251906.465421067, + "99.999" : 2251906.465421067, + "99.9999" : 2251906.465421067, + "100.0" : 2251906.465421067 + }, + "scoreUnit" : "ops/s", + "rawData" : [ + [ + 2251906.465421067, + 2126694.971239933, + 1921090.5339368568, + 1990167.3295375037, + 2032096.8985423783 + ] + ] + }, + "secondaryMetrics" : { + } + }, + { + "jmhVersion" : "1.37", + "benchmark" : "com.evolution.scache.bench.CacheBenchmark.getOrUpdateInsertDistinctKeys", + "mode" : "thrpt", + "threads" : 1, + "forks" : 1, + "jvm" : "/usr/lib/jvm/temurin-25-jdk-arm64/bin/java", + "jvmArgs" : [ + ], + "jdkVersion" : "25.0.4", + "vmName" : "OpenJDK 64-Bit Server VM", + "vmVersion" : "25.0.4+7-LTS", + "warmupIterations" : 1, + "warmupTime" : "3 s", + "warmupBatchSize" : 1, + "measurementIterations" : 5, + "measurementTime" : "2 s", + "measurementBatchSize" : 1, + "params" : { + "flavor" : "partitioned" + }, + "primaryMetric" : { + "score" : 2408100.7966738828, + "scoreError" : 688277.3099934626, + "scoreConfidence" : [ + 1719823.48668042, + 3096378.1066673454 + ], + "scorePercentiles" : { + "0.0" : 2214902.446455962, + "50.0" : 2397090.830640664, + "90.0" : 2591226.0007935627, + "95.0" : 2591226.0007935627, + "99.0" : 2591226.0007935627, + "99.9" : 2591226.0007935627, + "99.99" : 2591226.0007935627, + "99.999" : 2591226.0007935627, + "99.9999" : 2591226.0007935627, + "100.0" : 2591226.0007935627 + }, + "scoreUnit" : "ops/s", + "rawData" : [ + [ + 2214902.446455962, + 2397090.830640664, + 2586857.90852089, + 2250426.796958335, + 2591226.0007935627 + ] + ] + }, + "secondaryMetrics" : { + } + }, + { + "jmhVersion" : "1.37", + "benchmark" : "com.evolution.scache.bench.CacheBenchmark.getOrUpdateInsertDistinctKeys", + "mode" : "thrpt", + "threads" : 1, + "forks" : 1, + "jvm" : "/usr/lib/jvm/temurin-25-jdk-arm64/bin/java", + "jvmArgs" : [ + ], + "jdkVersion" : "25.0.4", + "vmName" : "OpenJDK 64-Bit Server VM", + "vmVersion" : "25.0.4+7-LTS", + "warmupIterations" : 1, + "warmupTime" : "3 s", + "warmupBatchSize" : 1, + "measurementIterations" : 5, + "measurementTime" : "2 s", + "measurementBatchSize" : 1, + "params" : { + "flavor" : "expiring" + }, + "primaryMetric" : { + "score" : 1986562.3204064355, + "scoreError" : 554031.9622058993, + "scoreConfidence" : [ + 1432530.358200536, + 2540594.282612335 + ], + "scorePercentiles" : { + "0.0" : 1802236.711151572, + "50.0" : 2013762.1033552743, + "90.0" : 2143329.708784185, + "95.0" : 2143329.708784185, + "99.0" : 2143329.708784185, + "99.9" : 2143329.708784185, + "99.99" : 2143329.708784185, + "99.999" : 2143329.708784185, + "99.9999" : 2143329.708784185, + "100.0" : 2143329.708784185 + }, + "scoreUnit" : "ops/s", + "rawData" : [ + [ + 1802236.711151572, + 1878310.977267158, + 2013762.1033552743, + 2143329.708784185, + 2095172.1014739864 + ] + ] + }, + "secondaryMetrics" : { + } + }, + { + "jmhVersion" : "1.37", + "benchmark" : "com.evolution.scache.bench.CacheBenchmark.mixedRandomKeys", + "mode" : "thrpt", + "threads" : 1, + "forks" : 1, + "jvm" : "/usr/lib/jvm/temurin-25-jdk-arm64/bin/java", + "jvmArgs" : [ + ], + "jdkVersion" : "25.0.4", + "vmName" : "OpenJDK 64-Bit Server VM", + "vmVersion" : "25.0.4+7-LTS", + "warmupIterations" : 1, + "warmupTime" : "3 s", + "warmupBatchSize" : 1, + "measurementIterations" : 5, + "measurementTime" : "2 s", + "measurementBatchSize" : 1, + "params" : { + "flavor" : "single" + }, + "primaryMetric" : { + "score" : 7446103.188974698, + "scoreError" : 217271.73676120647, + "scoreConfidence" : [ + 7228831.452213491, + 7663374.925735905 + ], + "scorePercentiles" : { + "0.0" : 7368027.720041424, + "50.0" : 7458117.974019589, + "90.0" : 7518687.673216142, + "95.0" : 7518687.673216142, + "99.0" : 7518687.673216142, + "99.9" : 7518687.673216142, + "99.99" : 7518687.673216142, + "99.999" : 7518687.673216142, + "99.9999" : 7518687.673216142, + "100.0" : 7518687.673216142 + }, + "scoreUnit" : "ops/s", + "rawData" : [ + [ + 7368027.720041424, + 7518687.673216142, + 7458117.974019589, + 7418294.374809377, + 7467388.20278696 + ] + ] + }, + "secondaryMetrics" : { + } + }, + { + "jmhVersion" : "1.37", + "benchmark" : "com.evolution.scache.bench.CacheBenchmark.mixedRandomKeys", + "mode" : "thrpt", + "threads" : 1, + "forks" : 1, + "jvm" : "/usr/lib/jvm/temurin-25-jdk-arm64/bin/java", + "jvmArgs" : [ + ], + "jdkVersion" : "25.0.4", + "vmName" : "OpenJDK 64-Bit Server VM", + "vmVersion" : "25.0.4+7-LTS", + "warmupIterations" : 1, + "warmupTime" : "3 s", + "warmupBatchSize" : 1, + "measurementIterations" : 5, + "measurementTime" : "2 s", + "measurementBatchSize" : 1, + "params" : { + "flavor" : "partitioned" + }, + "primaryMetric" : { + "score" : 7395581.1615020335, + "scoreError" : 330065.4723422714, + "scoreConfidence" : [ + 7065515.689159762, + 7725646.633844305 + ], + "scorePercentiles" : { + "0.0" : 7308351.271318262, + "50.0" : 7377370.118848213, + "90.0" : 7532547.912811478, + "95.0" : 7532547.912811478, + "99.0" : 7532547.912811478, + "99.9" : 7532547.912811478, + "99.99" : 7532547.912811478, + "99.999" : 7532547.912811478, + "99.9999" : 7532547.912811478, + "100.0" : 7532547.912811478 + }, + "scoreUnit" : "ops/s", + "rawData" : [ + [ + 7377370.118848213, + 7308351.271318262, + 7346711.907692262, + 7532547.912811478, + 7412924.59683995 + ] + ] + }, + "secondaryMetrics" : { + } + }, + { + "jmhVersion" : "1.37", + "benchmark" : "com.evolution.scache.bench.CacheBenchmark.mixedRandomKeys", + "mode" : "thrpt", + "threads" : 1, + "forks" : 1, + "jvm" : "/usr/lib/jvm/temurin-25-jdk-arm64/bin/java", + "jvmArgs" : [ + ], + "jdkVersion" : "25.0.4", + "vmName" : "OpenJDK 64-Bit Server VM", + "vmVersion" : "25.0.4+7-LTS", + "warmupIterations" : 1, + "warmupTime" : "3 s", + "warmupBatchSize" : 1, + "measurementIterations" : 5, + "measurementTime" : "2 s", + "measurementBatchSize" : 1, + "params" : { + "flavor" : "expiring" + }, + "primaryMetric" : { + "score" : 5557347.54920658, + "scoreError" : 179023.68039684725, + "scoreConfidence" : [ + 5378323.868809733, + 5736371.229603427 + ], + "scorePercentiles" : { + "0.0" : 5487301.510511556, + "50.0" : 5564301.986902345, + "90.0" : 5614419.715007943, + "95.0" : 5614419.715007943, + "99.0" : 5614419.715007943, + "99.9" : 5614419.715007943, + "99.99" : 5614419.715007943, + "99.999" : 5614419.715007943, + "99.9999" : 5614419.715007943, + "100.0" : 5614419.715007943 + }, + "scoreUnit" : "ops/s", + "rawData" : [ + [ + 5574776.34187705, + 5487301.510511556, + 5545938.191734008, + 5564301.986902345, + 5614419.715007943 + ] + ] + }, + "secondaryMetrics" : { + } + }, + { + "jmhVersion" : "1.37", + "benchmark" : "com.evolution.scache.bench.CacheBenchmark.modifyInsertDistinctKeys", + "mode" : "thrpt", + "threads" : 1, + "forks" : 1, + "jvm" : "/usr/lib/jvm/temurin-25-jdk-arm64/bin/java", + "jvmArgs" : [ + ], + "jdkVersion" : "25.0.4", + "vmName" : "OpenJDK 64-Bit Server VM", + "vmVersion" : "25.0.4+7-LTS", + "warmupIterations" : 1, + "warmupTime" : "3 s", + "warmupBatchSize" : 1, + "measurementIterations" : 5, + "measurementTime" : "2 s", + "measurementBatchSize" : 1, + "params" : { + "flavor" : "single" + }, + "primaryMetric" : { + "score" : 1.1440416365124347E7, + "scoreError" : 3644668.2121006115, + "scoreConfidence" : [ + 7795748.153023735, + 1.5085084577224959E7 + ], + "scorePercentiles" : { + "0.0" : 1.0820762378391398E7, + "50.0" : 1.0856903795876203E7, + "90.0" : 1.3008989591236407E7, + "95.0" : 1.3008989591236407E7, + "99.0" : 1.3008989591236407E7, + "99.9" : 1.3008989591236407E7, + "99.99" : 1.3008989591236407E7, + "99.999" : 1.3008989591236407E7, + "99.9999" : 1.3008989591236407E7, + "100.0" : 1.3008989591236407E7 + }, + "scoreUnit" : "ops/s", + "rawData" : [ + [ + 1.0856903795876203E7, + 1.1665066912099674E7, + 1.0820762378391398E7, + 1.3008989591236407E7, + 1.0850359148018045E7 + ] + ] + }, + "secondaryMetrics" : { + } + }, + { + "jmhVersion" : "1.37", + "benchmark" : "com.evolution.scache.bench.CacheBenchmark.modifyInsertDistinctKeys", + "mode" : "thrpt", + "threads" : 1, + "forks" : 1, + "jvm" : "/usr/lib/jvm/temurin-25-jdk-arm64/bin/java", + "jvmArgs" : [ + ], + "jdkVersion" : "25.0.4", + "vmName" : "OpenJDK 64-Bit Server VM", + "vmVersion" : "25.0.4+7-LTS", + "warmupIterations" : 1, + "warmupTime" : "3 s", + "warmupBatchSize" : 1, + "measurementIterations" : 5, + "measurementTime" : "2 s", + "measurementBatchSize" : 1, + "params" : { + "flavor" : "partitioned" + }, + "primaryMetric" : { + "score" : 1.065320648434866E7, + "scoreError" : 3018056.3397372253, + "scoreConfidence" : [ + 7635150.144611435, + 1.3671262824085886E7 + ], + "scorePercentiles" : { + "0.0" : 9391118.019878384, + "50.0" : 1.08953151604612E7, + "90.0" : 1.1295289274193797E7, + "95.0" : 1.1295289274193797E7, + "99.0" : 1.1295289274193797E7, + "99.9" : 1.1295289274193797E7, + "99.99" : 1.1295289274193797E7, + "99.999" : 1.1295289274193797E7, + "99.9999" : 1.1295289274193797E7, + "100.0" : 1.1295289274193797E7 + }, + "scoreUnit" : "ops/s", + "rawData" : [ + [ + 1.1243455904601825E7, + 9391118.019878384, + 1.1295289274193797E7, + 1.08953151604612E7, + 1.0440854062608097E7 + ] + ] + }, + "secondaryMetrics" : { + } + }, + { + "jmhVersion" : "1.37", + "benchmark" : "com.evolution.scache.bench.CacheBenchmark.modifyInsertDistinctKeys", + "mode" : "thrpt", + "threads" : 1, + "forks" : 1, + "jvm" : "/usr/lib/jvm/temurin-25-jdk-arm64/bin/java", + "jvmArgs" : [ + ], + "jdkVersion" : "25.0.4", + "vmName" : "OpenJDK 64-Bit Server VM", + "vmVersion" : "25.0.4+7-LTS", + "warmupIterations" : 1, + "warmupTime" : "3 s", + "warmupBatchSize" : 1, + "measurementIterations" : 5, + "measurementTime" : "2 s", + "measurementBatchSize" : 1, + "params" : { + "flavor" : "expiring" + }, + "primaryMetric" : { + "score" : 1.1764340847162846E7, + "scoreError" : 1109593.3872117454, + "scoreConfidence" : [ + 1.06547474599511E7, + 1.2873934234374592E7 + ], + "scorePercentiles" : { + "0.0" : 1.1458791699921632E7, + "50.0" : 1.1695132171683468E7, + "90.0" : 1.2124258432993853E7, + "95.0" : 1.2124258432993853E7, + "99.0" : 1.2124258432993853E7, + "99.9" : 1.2124258432993853E7, + "99.99" : 1.2124258432993853E7, + "99.999" : 1.2124258432993853E7, + "99.9999" : 1.2124258432993853E7, + "100.0" : 1.2124258432993853E7 + }, + "scoreUnit" : "ops/s", + "rawData" : [ + [ + 1.2000167342333589E7, + 1.1458791699921632E7, + 1.1543354588881696E7, + 1.2124258432993853E7, + 1.1695132171683468E7 + ] + ] + }, + "secondaryMetrics" : { + } + }, + { + "jmhVersion" : "1.37", + "benchmark" : "com.evolution.scache.bench.CacheBenchmark.modifyUpdateRandomKeys", + "mode" : "thrpt", + "threads" : 1, + "forks" : 1, + "jvm" : "/usr/lib/jvm/temurin-25-jdk-arm64/bin/java", + "jvmArgs" : [ + ], + "jdkVersion" : "25.0.4", + "vmName" : "OpenJDK 64-Bit Server VM", + "vmVersion" : "25.0.4+7-LTS", + "warmupIterations" : 1, + "warmupTime" : "3 s", + "warmupBatchSize" : 1, + "measurementIterations" : 5, + "measurementTime" : "2 s", + "measurementBatchSize" : 1, + "params" : { + "flavor" : "single" + }, + "primaryMetric" : { + "score" : 1.1133317544516152E7, + "scoreError" : 1593637.5576875797, + "scoreConfidence" : [ + 9539679.986828573, + 1.272695510220373E7 + ], + "scorePercentiles" : { + "0.0" : 1.0465849078364821E7, + "50.0" : 1.131587585815989E7, + "90.0" : 1.1512799935215317E7, + "95.0" : 1.1512799935215317E7, + "99.0" : 1.1512799935215317E7, + "99.9" : 1.1512799935215317E7, + "99.99" : 1.1512799935215317E7, + "99.999" : 1.1512799935215317E7, + "99.9999" : 1.1512799935215317E7, + "100.0" : 1.1512799935215317E7 + }, + "scoreUnit" : "ops/s", + "rawData" : [ + [ + 1.0465849078364821E7, + 1.1017622745463807E7, + 1.131587585815989E7, + 1.135444010537692E7, + 1.1512799935215317E7 + ] + ] + }, + "secondaryMetrics" : { + } + }, + { + "jmhVersion" : "1.37", + "benchmark" : "com.evolution.scache.bench.CacheBenchmark.modifyUpdateRandomKeys", + "mode" : "thrpt", + "threads" : 1, + "forks" : 1, + "jvm" : "/usr/lib/jvm/temurin-25-jdk-arm64/bin/java", + "jvmArgs" : [ + ], + "jdkVersion" : "25.0.4", + "vmName" : "OpenJDK 64-Bit Server VM", + "vmVersion" : "25.0.4+7-LTS", + "warmupIterations" : 1, + "warmupTime" : "3 s", + "warmupBatchSize" : 1, + "measurementIterations" : 5, + "measurementTime" : "2 s", + "measurementBatchSize" : 1, + "params" : { + "flavor" : "partitioned" + }, + "primaryMetric" : { + "score" : 9271847.264499156, + "scoreError" : 3252568.9460835992, + "scoreConfidence" : [ + 6019278.318415556, + 1.2524416210582756E7 + ], + "scorePercentiles" : { + "0.0" : 7783762.7963893805, + "50.0" : 9648900.18115227, + "90.0" : 9778672.439737821, + "95.0" : 9778672.439737821, + "99.0" : 9778672.439737821, + "99.9" : 9778672.439737821, + "99.99" : 9778672.439737821, + "99.999" : 9778672.439737821, + "99.9999" : 9778672.439737821, + "100.0" : 9778672.439737821 + }, + "scoreUnit" : "ops/s", + "rawData" : [ + [ + 7783762.7963893805, + 9403684.72806924, + 9648900.18115227, + 9744216.17714707, + 9778672.439737821 + ] + ] + }, + "secondaryMetrics" : { + } + }, + { + "jmhVersion" : "1.37", + "benchmark" : "com.evolution.scache.bench.CacheBenchmark.modifyUpdateRandomKeys", + "mode" : "thrpt", + "threads" : 1, + "forks" : 1, + "jvm" : "/usr/lib/jvm/temurin-25-jdk-arm64/bin/java", + "jvmArgs" : [ + ], + "jdkVersion" : "25.0.4", + "vmName" : "OpenJDK 64-Bit Server VM", + "vmVersion" : "25.0.4+7-LTS", + "warmupIterations" : 1, + "warmupTime" : "3 s", + "warmupBatchSize" : 1, + "measurementIterations" : 5, + "measurementTime" : "2 s", + "measurementBatchSize" : 1, + "params" : { + "flavor" : "expiring" + }, + "primaryMetric" : { + "score" : 1.0532905817183655E7, + "scoreError" : 2267783.9046359947, + "scoreConfidence" : [ + 8265121.91254766, + 1.280068972181965E7 + ], + "scorePercentiles" : { + "0.0" : 9493733.99943698, + "50.0" : 1.0705230566117764E7, + "90.0" : 1.0929416977530964E7, + "95.0" : 1.0929416977530964E7, + "99.0" : 1.0929416977530964E7, + "99.9" : 1.0929416977530964E7, + "99.99" : 1.0929416977530964E7, + "99.999" : 1.0929416977530964E7, + "99.9999" : 1.0929416977530964E7, + "100.0" : 1.0929416977530964E7 + }, + "scoreUnit" : "ops/s", + "rawData" : [ + [ + 9493733.99943698, + 1.0697430865237301E7, + 1.0838716677595265E7, + 1.0929416977530964E7, + 1.0705230566117764E7 + ] + ] + }, + "secondaryMetrics" : { + } + }, + { + "jmhVersion" : "1.37", + "benchmark" : "com.evolution.scache.bench.CacheBenchmark.putInsertDistinctKeys", + "mode" : "thrpt", + "threads" : 1, + "forks" : 1, + "jvm" : "/usr/lib/jvm/temurin-25-jdk-arm64/bin/java", + "jvmArgs" : [ + ], + "jdkVersion" : "25.0.4", + "vmName" : "OpenJDK 64-Bit Server VM", + "vmVersion" : "25.0.4+7-LTS", + "warmupIterations" : 1, + "warmupTime" : "3 s", + "warmupBatchSize" : 1, + "measurementIterations" : 5, + "measurementTime" : "2 s", + "measurementBatchSize" : 1, + "params" : { + "flavor" : "single" + }, + "primaryMetric" : { + "score" : 9243132.037909547, + "scoreError" : 5764175.06062249, + "scoreConfidence" : [ + 3478956.977287057, + 1.5007307098532036E7 + ], + "scorePercentiles" : { + "0.0" : 6983322.719652706, + "50.0" : 1.0129765781771481E7, + "90.0" : 1.044484917267873E7, + "95.0" : 1.044484917267873E7, + "99.0" : 1.044484917267873E7, + "99.9" : 1.044484917267873E7, + "99.99" : 1.044484917267873E7, + "99.999" : 1.044484917267873E7, + "99.9999" : 1.044484917267873E7, + "100.0" : 1.044484917267873E7 + }, + "scoreUnit" : "ops/s", + "rawData" : [ + [ + 1.0226528340599027E7, + 6983322.719652706, + 1.0129765781771481E7, + 1.044484917267873E7, + 8431194.174845789 + ] + ] + }, + "secondaryMetrics" : { + } + }, + { + "jmhVersion" : "1.37", + "benchmark" : "com.evolution.scache.bench.CacheBenchmark.putInsertDistinctKeys", + "mode" : "thrpt", + "threads" : 1, + "forks" : 1, + "jvm" : "/usr/lib/jvm/temurin-25-jdk-arm64/bin/java", + "jvmArgs" : [ + ], + "jdkVersion" : "25.0.4", + "vmName" : "OpenJDK 64-Bit Server VM", + "vmVersion" : "25.0.4+7-LTS", + "warmupIterations" : 1, + "warmupTime" : "3 s", + "warmupBatchSize" : 1, + "measurementIterations" : 5, + "measurementTime" : "2 s", + "measurementBatchSize" : 1, + "params" : { + "flavor" : "partitioned" + }, + "primaryMetric" : { + "score" : 1.0303951827211116E7, + "scoreError" : 644225.2057348174, + "scoreConfidence" : [ + 9659726.621476298, + 1.0948177032945933E7 + ], + "scorePercentiles" : { + "0.0" : 1.0061987798179567E7, + "50.0" : 1.0401208243354078E7, + "90.0" : 1.0451064344557744E7, + "95.0" : 1.0451064344557744E7, + "99.0" : 1.0451064344557744E7, + "99.9" : 1.0451064344557744E7, + "99.99" : 1.0451064344557744E7, + "99.999" : 1.0451064344557744E7, + "99.9999" : 1.0451064344557744E7, + "100.0" : 1.0451064344557744E7 + }, + "scoreUnit" : "ops/s", + "rawData" : [ + [ + 1.0408369236345714E7, + 1.0061987798179567E7, + 1.0197129513618471E7, + 1.0401208243354078E7, + 1.0451064344557744E7 + ] + ] + }, + "secondaryMetrics" : { + } + }, + { + "jmhVersion" : "1.37", + "benchmark" : "com.evolution.scache.bench.CacheBenchmark.putInsertDistinctKeys", + "mode" : "thrpt", + "threads" : 1, + "forks" : 1, + "jvm" : "/usr/lib/jvm/temurin-25-jdk-arm64/bin/java", + "jvmArgs" : [ + ], + "jdkVersion" : "25.0.4", + "vmName" : "OpenJDK 64-Bit Server VM", + "vmVersion" : "25.0.4+7-LTS", + "warmupIterations" : 1, + "warmupTime" : "3 s", + "warmupBatchSize" : 1, + "measurementIterations" : 5, + "measurementTime" : "2 s", + "measurementBatchSize" : 1, + "params" : { + "flavor" : "expiring" + }, + "primaryMetric" : { + "score" : 8858241.84881128, + "scoreError" : 800409.5549905874, + "scoreConfidence" : [ + 8057832.293820692, + 9658651.403801868 + ], + "scorePercentiles" : { + "0.0" : 8645918.501259424, + "50.0" : 8804002.231917279, + "90.0" : 9197328.698727867, + "95.0" : 9197328.698727867, + "99.0" : 9197328.698727867, + "99.9" : 9197328.698727867, + "99.99" : 9197328.698727867, + "99.999" : 9197328.698727867, + "99.9999" : 9197328.698727867, + "100.0" : 9197328.698727867 + }, + "scoreUnit" : "ops/s", + "rawData" : [ + [ + 8804002.231917279, + 8761994.152325422, + 9197328.698727867, + 8881965.659826407, + 8645918.501259424 + ] + ] + }, + "secondaryMetrics" : { + } + }, + { + "jmhVersion" : "1.37", + "benchmark" : "com.evolution.scache.bench.CacheBenchmark.putReplaceRandomKeys", + "mode" : "thrpt", + "threads" : 1, + "forks" : 1, + "jvm" : "/usr/lib/jvm/temurin-25-jdk-arm64/bin/java", + "jvmArgs" : [ + ], + "jdkVersion" : "25.0.4", + "vmName" : "OpenJDK 64-Bit Server VM", + "vmVersion" : "25.0.4+7-LTS", + "warmupIterations" : 1, + "warmupTime" : "3 s", + "warmupBatchSize" : 1, + "measurementIterations" : 5, + "measurementTime" : "2 s", + "measurementBatchSize" : 1, + "params" : { + "flavor" : "single" + }, + "primaryMetric" : { + "score" : 9534665.240798771, + "scoreError" : 532363.9869741199, + "scoreConfidence" : [ + 9002301.253824651, + 1.0067029227772892E7 + ], + "scorePercentiles" : { + "0.0" : 9400367.26667304, + "50.0" : 9536747.12415566, + "90.0" : 9717542.231094409, + "95.0" : 9717542.231094409, + "99.0" : 9717542.231094409, + "99.9" : 9717542.231094409, + "99.99" : 9717542.231094409, + "99.999" : 9717542.231094409, + "99.9999" : 9717542.231094409, + "100.0" : 9717542.231094409 + }, + "scoreUnit" : "ops/s", + "rawData" : [ + [ + 9400503.975258283, + 9400367.26667304, + 9536747.12415566, + 9717542.231094409, + 9618165.606812466 + ] + ] + }, + "secondaryMetrics" : { + } + }, + { + "jmhVersion" : "1.37", + "benchmark" : "com.evolution.scache.bench.CacheBenchmark.putReplaceRandomKeys", + "mode" : "thrpt", + "threads" : 1, + "forks" : 1, + "jvm" : "/usr/lib/jvm/temurin-25-jdk-arm64/bin/java", + "jvmArgs" : [ + ], + "jdkVersion" : "25.0.4", + "vmName" : "OpenJDK 64-Bit Server VM", + "vmVersion" : "25.0.4+7-LTS", + "warmupIterations" : 1, + "warmupTime" : "3 s", + "warmupBatchSize" : 1, + "measurementIterations" : 5, + "measurementTime" : "2 s", + "measurementBatchSize" : 1, + "params" : { + "flavor" : "partitioned" + }, + "primaryMetric" : { + "score" : 8832068.910531137, + "scoreError" : 511003.8484566339, + "scoreConfidence" : [ + 8321065.062074503, + 9343072.758987771 + ], + "scorePercentiles" : { + "0.0" : 8646777.46285042, + "50.0" : 8835152.944772968, + "90.0" : 9019671.536448525, + "95.0" : 9019671.536448525, + "99.0" : 9019671.536448525, + "99.9" : 9019671.536448525, + "99.99" : 9019671.536448525, + "99.999" : 9019671.536448525, + "99.9999" : 9019671.536448525, + "100.0" : 9019671.536448525 + }, + "scoreUnit" : "ops/s", + "rawData" : [ + [ + 8646777.46285042, + 8835152.944772968, + 8850488.594511893, + 8808254.014071878, + 9019671.536448525 + ] + ] + }, + "secondaryMetrics" : { + } + }, + { + "jmhVersion" : "1.37", + "benchmark" : "com.evolution.scache.bench.CacheBenchmark.putReplaceRandomKeys", + "mode" : "thrpt", + "threads" : 1, + "forks" : 1, + "jvm" : "/usr/lib/jvm/temurin-25-jdk-arm64/bin/java", + "jvmArgs" : [ + ], + "jdkVersion" : "25.0.4", + "vmName" : "OpenJDK 64-Bit Server VM", + "vmVersion" : "25.0.4+7-LTS", + "warmupIterations" : 1, + "warmupTime" : "3 s", + "warmupBatchSize" : 1, + "measurementIterations" : 5, + "measurementTime" : "2 s", + "measurementBatchSize" : 1, + "params" : { + "flavor" : "expiring" + }, + "primaryMetric" : { + "score" : 8000773.064419588, + "scoreError" : 526530.2671306859, + "scoreConfidence" : [ + 7474242.797288902, + 8527303.331550274 + ], + "scorePercentiles" : { + "0.0" : 7788425.204067172, + "50.0" : 8008375.253989635, + "90.0" : 8169200.763814323, + "95.0" : 8169200.763814323, + "99.0" : 8169200.763814323, + "99.9" : 8169200.763814323, + "99.99" : 8169200.763814323, + "99.999" : 8169200.763814323, + "99.9999" : 8169200.763814323, + "100.0" : 8169200.763814323 + }, + "scoreUnit" : "ops/s", + "rawData" : [ + [ + 7788425.204067172, + 8036433.317647551, + 8001430.782579265, + 8169200.763814323, + 8008375.253989635 + ] + ] + }, + "secondaryMetrics" : { + } + }, + { + "jmhVersion" : "1.37", + "benchmark" : "com.evolution.scache.bench.CacheBenchmark.removeAndPutRandomKeys", + "mode" : "thrpt", + "threads" : 1, + "forks" : 1, + "jvm" : "/usr/lib/jvm/temurin-25-jdk-arm64/bin/java", + "jvmArgs" : [ + ], + "jdkVersion" : "25.0.4", + "vmName" : "OpenJDK 64-Bit Server VM", + "vmVersion" : "25.0.4+7-LTS", + "warmupIterations" : 1, + "warmupTime" : "3 s", + "warmupBatchSize" : 1, + "measurementIterations" : 5, + "measurementTime" : "2 s", + "measurementBatchSize" : 1, + "params" : { + "flavor" : "single" + }, + "primaryMetric" : { + "score" : 3868942.3678093804, + "scoreError" : 188930.75680681516, + "scoreConfidence" : [ + 3680011.6110025654, + 4057873.1246161954 + ], + "scorePercentiles" : { + "0.0" : 3806388.7140883896, + "50.0" : 3878788.995648829, + "90.0" : 3917681.1382461563, + "95.0" : 3917681.1382461563, + "99.0" : 3917681.1382461563, + "99.9" : 3917681.1382461563, + "99.99" : 3917681.1382461563, + "99.999" : 3917681.1382461563, + "99.9999" : 3917681.1382461563, + "100.0" : 3917681.1382461563 + }, + "scoreUnit" : "ops/s", + "rawData" : [ + [ + 3806388.7140883896, + 3830701.322462561, + 3878788.995648829, + 3911151.668600967, + 3917681.1382461563 + ] + ] + }, + "secondaryMetrics" : { + } + }, + { + "jmhVersion" : "1.37", + "benchmark" : "com.evolution.scache.bench.CacheBenchmark.removeAndPutRandomKeys", + "mode" : "thrpt", + "threads" : 1, + "forks" : 1, + "jvm" : "/usr/lib/jvm/temurin-25-jdk-arm64/bin/java", + "jvmArgs" : [ + ], + "jdkVersion" : "25.0.4", + "vmName" : "OpenJDK 64-Bit Server VM", + "vmVersion" : "25.0.4+7-LTS", + "warmupIterations" : 1, + "warmupTime" : "3 s", + "warmupBatchSize" : 1, + "measurementIterations" : 5, + "measurementTime" : "2 s", + "measurementBatchSize" : 1, + "params" : { + "flavor" : "partitioned" + }, + "primaryMetric" : { + "score" : 4048120.4013739116, + "scoreError" : 185157.69285698567, + "scoreConfidence" : [ + 3862962.708516926, + 4233278.094230898 + ], + "scorePercentiles" : { + "0.0" : 3972960.4424302257, + "50.0" : 4047395.9469632925, + "90.0" : 4095390.184556391, + "95.0" : 4095390.184556391, + "99.0" : 4095390.184556391, + "99.9" : 4095390.184556391, + "99.99" : 4095390.184556391, + "99.999" : 4095390.184556391, + "99.9999" : 4095390.184556391, + "100.0" : 4095390.184556391 + }, + "scoreUnit" : "ops/s", + "rawData" : [ + [ + 3972960.4424302257, + 4047395.9469632925, + 4040573.177884957, + 4095390.184556391, + 4084282.2550346935 + ] + ] + }, + "secondaryMetrics" : { + } + }, + { + "jmhVersion" : "1.37", + "benchmark" : "com.evolution.scache.bench.CacheBenchmark.removeAndPutRandomKeys", + "mode" : "thrpt", + "threads" : 1, + "forks" : 1, + "jvm" : "/usr/lib/jvm/temurin-25-jdk-arm64/bin/java", + "jvmArgs" : [ + ], + "jdkVersion" : "25.0.4", + "vmName" : "OpenJDK 64-Bit Server VM", + "vmVersion" : "25.0.4+7-LTS", + "warmupIterations" : 1, + "warmupTime" : "3 s", + "warmupBatchSize" : 1, + "measurementIterations" : 5, + "measurementTime" : "2 s", + "measurementBatchSize" : 1, + "params" : { + "flavor" : "expiring" + }, + "primaryMetric" : { + "score" : 2968570.210475764, + "scoreError" : 249160.43915868978, + "scoreConfidence" : [ + 2719409.771317074, + 3217730.6496344535 + ], + "scorePercentiles" : { + "0.0" : 2872091.694084293, + "50.0" : 2983536.690458085, + "90.0" : 3034509.685901376, + "95.0" : 3034509.685901376, + "99.0" : 3034509.685901376, + "99.9" : 3034509.685901376, + "99.99" : 3034509.685901376, + "99.999" : 3034509.685901376, + "99.9999" : 3034509.685901376, + "100.0" : 3034509.685901376 + }, + "scoreUnit" : "ops/s", + "rawData" : [ + [ + 3013411.786385771, + 3034509.685901376, + 2939301.195549292, + 2872091.694084293, + 2983536.690458085 + ] + ] + }, + "secondaryMetrics" : { + } + } +] + + diff --git a/benchmark/results/master-7c9fa9f.json b/benchmark/results/master-7c9fa9f.json new file mode 100644 index 0000000..b7c5c5f --- /dev/null +++ b/benchmark/results/master-7c9fa9f.json @@ -0,0 +1,2110 @@ +[ + { + "jmhVersion" : "1.37", + "benchmark" : "com.evolution.scache.bench.CacheBenchmark.containsRandomKeys", + "mode" : "thrpt", + "threads" : 1, + "forks" : 1, + "jvm" : "/usr/lib/jvm/temurin-25-jdk-arm64/bin/java", + "jvmArgs" : [ + ], + "jdkVersion" : "25.0.4", + "vmName" : "OpenJDK 64-Bit Server VM", + "vmVersion" : "25.0.4+7-LTS", + "warmupIterations" : 1, + "warmupTime" : "3 s", + "warmupBatchSize" : 1, + "measurementIterations" : 5, + "measurementTime" : "2 s", + "measurementBatchSize" : 1, + "params" : { + "flavor" : "single" + }, + "primaryMetric" : { + "score" : 2.668982695605997E7, + "scoreError" : 3908765.7953641405, + "scoreConfidence" : [ + 2.278106116069583E7, + 3.059859275142411E7 + ], + "scorePercentiles" : { + "0.0" : 2.4936202534090832E7, + "50.0" : 2.710991340428906E7, + "90.0" : 2.7464093391158577E7, + "95.0" : 2.7464093391158577E7, + "99.0" : 2.7464093391158577E7, + "99.9" : 2.7464093391158577E7, + "99.99" : 2.7464093391158577E7, + "99.999" : 2.7464093391158577E7, + "99.9999" : 2.7464093391158577E7, + "100.0" : 2.7464093391158577E7 + }, + "scoreUnit" : "ops/s", + "rawData" : [ + [ + 2.4936202534090832E7, + 2.6730598998322867E7, + 2.710991340428906E7, + 2.72083264524385E7, + 2.7464093391158577E7 + ] + ] + }, + "secondaryMetrics" : { + } + }, + { + "jmhVersion" : "1.37", + "benchmark" : "com.evolution.scache.bench.CacheBenchmark.containsRandomKeys", + "mode" : "thrpt", + "threads" : 1, + "forks" : 1, + "jvm" : "/usr/lib/jvm/temurin-25-jdk-arm64/bin/java", + "jvmArgs" : [ + ], + "jdkVersion" : "25.0.4", + "vmName" : "OpenJDK 64-Bit Server VM", + "vmVersion" : "25.0.4+7-LTS", + "warmupIterations" : 1, + "warmupTime" : "3 s", + "warmupBatchSize" : 1, + "measurementIterations" : 5, + "measurementTime" : "2 s", + "measurementBatchSize" : 1, + "params" : { + "flavor" : "partitioned" + }, + "primaryMetric" : { + "score" : 2.359470895910461E7, + "scoreError" : 3374028.1439309292, + "scoreConfidence" : [ + 2.0220680815173678E7, + 2.696873710303554E7 + ], + "scorePercentiles" : { + "0.0" : 2.206558013235072E7, + "50.0" : 2.399476163160392E7, + "90.0" : 2.415304742453361E7, + "95.0" : 2.415304742453361E7, + "99.0" : 2.415304742453361E7, + "99.9" : 2.415304742453361E7, + "99.99" : 2.415304742453361E7, + "99.999" : 2.415304742453361E7, + "99.9999" : 2.415304742453361E7, + "100.0" : 2.415304742453361E7 + }, + "scoreUnit" : "ops/s", + "rawData" : [ + [ + 2.206558013235072E7, + 2.3658507216192886E7, + 2.399476163160392E7, + 2.415304742453361E7, + 2.4101648390841924E7 + ] + ] + }, + "secondaryMetrics" : { + } + }, + { + "jmhVersion" : "1.37", + "benchmark" : "com.evolution.scache.bench.CacheBenchmark.containsRandomKeys", + "mode" : "thrpt", + "threads" : 1, + "forks" : 1, + "jvm" : "/usr/lib/jvm/temurin-25-jdk-arm64/bin/java", + "jvmArgs" : [ + ], + "jdkVersion" : "25.0.4", + "vmName" : "OpenJDK 64-Bit Server VM", + "vmVersion" : "25.0.4+7-LTS", + "warmupIterations" : 1, + "warmupTime" : "3 s", + "warmupBatchSize" : 1, + "measurementIterations" : 5, + "measurementTime" : "2 s", + "measurementBatchSize" : 1, + "params" : { + "flavor" : "expiring" + }, + "primaryMetric" : { + "score" : 2.321925890862229E7, + "scoreError" : 1274521.9146968399, + "scoreConfidence" : [ + 2.1944736993925452E7, + 2.449378082331913E7 + ], + "scorePercentiles" : { + "0.0" : 2.274430770816594E7, + "50.0" : 2.3410046342388917E7, + "90.0" : 2.348376014315221E7, + "95.0" : 2.348376014315221E7, + "99.0" : 2.348376014315221E7, + "99.9" : 2.348376014315221E7, + "99.99" : 2.348376014315221E7, + "99.999" : 2.348376014315221E7, + "99.9999" : 2.348376014315221E7, + "100.0" : 2.348376014315221E7 + }, + "scoreUnit" : "ops/s", + "rawData" : [ + [ + 2.274430770816594E7, + 2.299878282335825E7, + 2.3410046342388917E7, + 2.348376014315221E7, + 2.3459397526046146E7 + ] + ] + }, + "secondaryMetrics" : { + } + }, + { + "jmhVersion" : "1.37", + "benchmark" : "com.evolution.scache.bench.CacheBenchmark.foldMapWholeCache", + "mode" : "thrpt", + "threads" : 1, + "forks" : 1, + "jvm" : "/usr/lib/jvm/temurin-25-jdk-arm64/bin/java", + "jvmArgs" : [ + ], + "jdkVersion" : "25.0.4", + "vmName" : "OpenJDK 64-Bit Server VM", + "vmVersion" : "25.0.4+7-LTS", + "warmupIterations" : 1, + "warmupTime" : "3 s", + "warmupBatchSize" : 1, + "measurementIterations" : 5, + "measurementTime" : "2 s", + "measurementBatchSize" : 1, + "params" : { + "flavor" : "single" + }, + "primaryMetric" : { + "score" : 1175.7306999297216, + "scoreError" : 57.255009410261, + "scoreConfidence" : [ + 1118.4756905194606, + 1232.9857093399826 + ], + "scorePercentiles" : { + "0.0" : 1161.7614996582122, + "50.0" : 1168.9664344937914, + "90.0" : 1195.3207088037525, + "95.0" : 1195.3207088037525, + "99.0" : 1195.3207088037525, + "99.9" : 1195.3207088037525, + "99.99" : 1195.3207088037525, + "99.999" : 1195.3207088037525, + "99.9999" : 1195.3207088037525, + "100.0" : 1195.3207088037525 + }, + "scoreUnit" : "ops/s", + "rawData" : [ + [ + 1168.9664344937914, + 1187.6828053116567, + 1195.3207088037525, + 1161.7614996582122, + 1164.9220513811954 + ] + ] + }, + "secondaryMetrics" : { + } + }, + { + "jmhVersion" : "1.37", + "benchmark" : "com.evolution.scache.bench.CacheBenchmark.foldMapWholeCache", + "mode" : "thrpt", + "threads" : 1, + "forks" : 1, + "jvm" : "/usr/lib/jvm/temurin-25-jdk-arm64/bin/java", + "jvmArgs" : [ + ], + "jdkVersion" : "25.0.4", + "vmName" : "OpenJDK 64-Bit Server VM", + "vmVersion" : "25.0.4+7-LTS", + "warmupIterations" : 1, + "warmupTime" : "3 s", + "warmupBatchSize" : 1, + "measurementIterations" : 5, + "measurementTime" : "2 s", + "measurementBatchSize" : 1, + "params" : { + "flavor" : "partitioned" + }, + "primaryMetric" : { + "score" : 1142.3657111775376, + "scoreError" : 107.43051564370381, + "scoreConfidence" : [ + 1034.9351955338338, + 1249.7962268212414 + ], + "scorePercentiles" : { + "0.0" : 1112.3730498575105, + "50.0" : 1130.314022236891, + "90.0" : 1173.078057402639, + "95.0" : 1173.078057402639, + "99.0" : 1173.078057402639, + "99.9" : 1173.078057402639, + "99.99" : 1173.078057402639, + "99.999" : 1173.078057402639, + "99.9999" : 1173.078057402639, + "100.0" : 1173.078057402639 + }, + "scoreUnit" : "ops/s", + "rawData" : [ + [ + 1124.996526161093, + 1173.078057402639, + 1171.066900229554, + 1130.314022236891, + 1112.3730498575105 + ] + ] + }, + "secondaryMetrics" : { + } + }, + { + "jmhVersion" : "1.37", + "benchmark" : "com.evolution.scache.bench.CacheBenchmark.foldMapWholeCache", + "mode" : "thrpt", + "threads" : 1, + "forks" : 1, + "jvm" : "/usr/lib/jvm/temurin-25-jdk-arm64/bin/java", + "jvmArgs" : [ + ], + "jdkVersion" : "25.0.4", + "vmName" : "OpenJDK 64-Bit Server VM", + "vmVersion" : "25.0.4+7-LTS", + "warmupIterations" : 1, + "warmupTime" : "3 s", + "warmupBatchSize" : 1, + "measurementIterations" : 5, + "measurementTime" : "2 s", + "measurementBatchSize" : 1, + "params" : { + "flavor" : "expiring" + }, + "primaryMetric" : { + "score" : 1008.5905754913513, + "scoreError" : 129.6834833238242, + "scoreConfidence" : [ + 878.907092167527, + 1138.2740588151755 + ], + "scorePercentiles" : { + "0.0" : 972.3203475908738, + "50.0" : 1017.4770798645286, + "90.0" : 1050.2159596417712, + "95.0" : 1050.2159596417712, + "99.0" : 1050.2159596417712, + "99.9" : 1050.2159596417712, + "99.99" : 1050.2159596417712, + "99.999" : 1050.2159596417712, + "99.9999" : 1050.2159596417712, + "100.0" : 1050.2159596417712 + }, + "scoreUnit" : "ops/s", + "rawData" : [ + [ + 1017.4770798645286, + 1050.2159596417712, + 1027.0466003437505, + 972.3203475908738, + 975.892890015832 + ] + ] + }, + "secondaryMetrics" : { + } + }, + { + "jmhVersion" : "1.37", + "benchmark" : "com.evolution.scache.bench.CacheBenchmark.get1HitRandomKeys", + "mode" : "thrpt", + "threads" : 1, + "forks" : 1, + "jvm" : "/usr/lib/jvm/temurin-25-jdk-arm64/bin/java", + "jvmArgs" : [ + ], + "jdkVersion" : "25.0.4", + "vmName" : "OpenJDK 64-Bit Server VM", + "vmVersion" : "25.0.4+7-LTS", + "warmupIterations" : 1, + "warmupTime" : "3 s", + "warmupBatchSize" : 1, + "measurementIterations" : 5, + "measurementTime" : "2 s", + "measurementBatchSize" : 1, + "params" : { + "flavor" : "single" + }, + "primaryMetric" : { + "score" : 1.947357814889176E7, + "scoreError" : 1897224.727346683, + "scoreConfidence" : [ + 1.7576353421545073E7, + 2.1370802876238443E7 + ], + "scorePercentiles" : { + "0.0" : 1.8655865406924173E7, + "50.0" : 1.960507592829235E7, + "90.0" : 1.990721002565632E7, + "95.0" : 1.990721002565632E7, + "99.0" : 1.990721002565632E7, + "99.9" : 1.990721002565632E7, + "99.99" : 1.990721002565632E7, + "99.999" : 1.990721002565632E7, + "99.9999" : 1.990721002565632E7, + "100.0" : 1.990721002565632E7 + }, + "scoreUnit" : "ops/s", + "rawData" : [ + [ + 1.8655865406924173E7, + 1.960507592829235E7, + 1.9419391976170056E7, + 1.978034740741591E7, + 1.990721002565632E7 + ] + ] + }, + "secondaryMetrics" : { + } + }, + { + "jmhVersion" : "1.37", + "benchmark" : "com.evolution.scache.bench.CacheBenchmark.get1HitRandomKeys", + "mode" : "thrpt", + "threads" : 1, + "forks" : 1, + "jvm" : "/usr/lib/jvm/temurin-25-jdk-arm64/bin/java", + "jvmArgs" : [ + ], + "jdkVersion" : "25.0.4", + "vmName" : "OpenJDK 64-Bit Server VM", + "vmVersion" : "25.0.4+7-LTS", + "warmupIterations" : 1, + "warmupTime" : "3 s", + "warmupBatchSize" : 1, + "measurementIterations" : 5, + "measurementTime" : "2 s", + "measurementBatchSize" : 1, + "params" : { + "flavor" : "partitioned" + }, + "primaryMetric" : { + "score" : 1.877709693039935E7, + "scoreError" : 1778281.4034962654, + "scoreConfidence" : [ + 1.6998815526903085E7, + 2.0555378333895616E7 + ], + "scorePercentiles" : { + "0.0" : 1.79658669613779E7, + "50.0" : 1.894370524042261E7, + "90.0" : 1.9071809504318964E7, + "95.0" : 1.9071809504318964E7, + "99.0" : 1.9071809504318964E7, + "99.9" : 1.9071809504318964E7, + "99.99" : 1.9071809504318964E7, + "99.999" : 1.9071809504318964E7, + "99.9999" : 1.9071809504318964E7, + "100.0" : 1.9071809504318964E7 + }, + "scoreUnit" : "ops/s", + "rawData" : [ + [ + 1.79658669613779E7, + 1.894370524042261E7, + 1.8854167029034752E7, + 1.9071809504318964E7, + 1.9049935916842535E7 + ] + ] + }, + "secondaryMetrics" : { + } + }, + { + "jmhVersion" : "1.37", + "benchmark" : "com.evolution.scache.bench.CacheBenchmark.get1HitRandomKeys", + "mode" : "thrpt", + "threads" : 1, + "forks" : 1, + "jvm" : "/usr/lib/jvm/temurin-25-jdk-arm64/bin/java", + "jvmArgs" : [ + ], + "jdkVersion" : "25.0.4", + "vmName" : "OpenJDK 64-Bit Server VM", + "vmVersion" : "25.0.4+7-LTS", + "warmupIterations" : 1, + "warmupTime" : "3 s", + "warmupBatchSize" : 1, + "measurementIterations" : 5, + "measurementTime" : "2 s", + "measurementBatchSize" : 1, + "params" : { + "flavor" : "expiring" + }, + "primaryMetric" : { + "score" : 1.2585153418621961E7, + "scoreError" : 596201.1953998889, + "scoreConfidence" : [ + 1.1988952223222071E7, + 1.318135461402185E7 + ], + "scorePercentiles" : { + "0.0" : 1.2314784165527573E7, + "50.0" : 1.2636106513270374E7, + "90.0" : 1.2697950010560602E7, + "95.0" : 1.2697950010560602E7, + "99.0" : 1.2697950010560602E7, + "99.9" : 1.2697950010560602E7, + "99.99" : 1.2697950010560602E7, + "99.999" : 1.2697950010560602E7, + "99.9999" : 1.2697950010560602E7, + "100.0" : 1.2697950010560602E7 + }, + "scoreUnit" : "ops/s", + "rawData" : [ + [ + 1.2314784165527573E7, + 1.2668367685843222E7, + 1.2697950010560602E7, + 1.2636106513270374E7, + 1.2608558717908025E7 + ] + ] + }, + "secondaryMetrics" : { + } + }, + { + "jmhVersion" : "1.37", + "benchmark" : "com.evolution.scache.bench.CacheBenchmark.getHitRandomKeys", + "mode" : "thrpt", + "threads" : 1, + "forks" : 1, + "jvm" : "/usr/lib/jvm/temurin-25-jdk-arm64/bin/java", + "jvmArgs" : [ + ], + "jdkVersion" : "25.0.4", + "vmName" : "OpenJDK 64-Bit Server VM", + "vmVersion" : "25.0.4+7-LTS", + "warmupIterations" : 1, + "warmupTime" : "3 s", + "warmupBatchSize" : 1, + "measurementIterations" : 5, + "measurementTime" : "2 s", + "measurementBatchSize" : 1, + "params" : { + "flavor" : "single" + }, + "primaryMetric" : { + "score" : 2.173830758601103E7, + "scoreError" : 4045725.465183449, + "scoreConfidence" : [ + 1.769258212082758E7, + 2.5784033051194478E7 + ], + "scorePercentiles" : { + "0.0" : 2.0081695594979215E7, + "50.0" : 2.2310687984531026E7, + "90.0" : 2.2605461206244823E7, + "95.0" : 2.2605461206244823E7, + "99.0" : 2.2605461206244823E7, + "99.9" : 2.2605461206244823E7, + "99.99" : 2.2605461206244823E7, + "99.999" : 2.2605461206244823E7, + "99.9999" : 2.2605461206244823E7, + "100.0" : 2.2605461206244823E7 + }, + "scoreUnit" : "ops/s", + "rawData" : [ + [ + 2.0081695594979215E7, + 2.131390023977005E7, + 2.2310687984531026E7, + 2.2379792904530022E7, + 2.2605461206244823E7 + ] + ] + }, + "secondaryMetrics" : { + } + }, + { + "jmhVersion" : "1.37", + "benchmark" : "com.evolution.scache.bench.CacheBenchmark.getHitRandomKeys", + "mode" : "thrpt", + "threads" : 1, + "forks" : 1, + "jvm" : "/usr/lib/jvm/temurin-25-jdk-arm64/bin/java", + "jvmArgs" : [ + ], + "jdkVersion" : "25.0.4", + "vmName" : "OpenJDK 64-Bit Server VM", + "vmVersion" : "25.0.4+7-LTS", + "warmupIterations" : 1, + "warmupTime" : "3 s", + "warmupBatchSize" : 1, + "measurementIterations" : 5, + "measurementTime" : "2 s", + "measurementBatchSize" : 1, + "params" : { + "flavor" : "partitioned" + }, + "primaryMetric" : { + "score" : 2.136679595243842E7, + "scoreError" : 1910198.4313375792, + "scoreConfidence" : [ + 1.945659752110084E7, + 2.3276994383776E7 + ], + "scorePercentiles" : { + "0.0" : 2.052901742214534E7, + "50.0" : 2.1458461146165032E7, + "90.0" : 2.1769067301032174E7, + "95.0" : 2.1769067301032174E7, + "99.0" : 2.1769067301032174E7, + "99.9" : 2.1769067301032174E7, + "99.99" : 2.1769067301032174E7, + "99.999" : 2.1769067301032174E7, + "99.9999" : 2.1769067301032174E7, + "100.0" : 2.1769067301032174E7 + }, + "scoreUnit" : "ops/s", + "rawData" : [ + [ + 2.052901742214534E7, + 2.1375713418298718E7, + 2.1458461146165032E7, + 2.1701720474550862E7, + 2.1769067301032174E7 + ] + ] + }, + "secondaryMetrics" : { + } + }, + { + "jmhVersion" : "1.37", + "benchmark" : "com.evolution.scache.bench.CacheBenchmark.getHitRandomKeys", + "mode" : "thrpt", + "threads" : 1, + "forks" : 1, + "jvm" : "/usr/lib/jvm/temurin-25-jdk-arm64/bin/java", + "jvmArgs" : [ + ], + "jdkVersion" : "25.0.4", + "vmName" : "OpenJDK 64-Bit Server VM", + "vmVersion" : "25.0.4+7-LTS", + "warmupIterations" : 1, + "warmupTime" : "3 s", + "warmupBatchSize" : 1, + "measurementIterations" : 5, + "measurementTime" : "2 s", + "measurementBatchSize" : 1, + "params" : { + "flavor" : "expiring" + }, + "primaryMetric" : { + "score" : 1.2675177745194595E7, + "scoreError" : 432636.6995276609, + "scoreConfidence" : [ + 1.2242541045666935E7, + 1.3107814444722256E7 + ], + "scorePercentiles" : { + "0.0" : 1.2533921340431755E7, + "50.0" : 1.2689733918666331E7, + "90.0" : 1.2801837574175106E7, + "95.0" : 1.2801837574175106E7, + "99.0" : 1.2801837574175106E7, + "99.9" : 1.2801837574175106E7, + "99.99" : 1.2801837574175106E7, + "99.999" : 1.2801837574175106E7, + "99.9999" : 1.2801837574175106E7, + "100.0" : 1.2801837574175106E7 + }, + "scoreUnit" : "ops/s", + "rawData" : [ + [ + 1.2590681507322881E7, + 1.2689733918666331E7, + 1.2533921340431755E7, + 1.2801837574175106E7, + 1.2759714385376912E7 + ] + ] + }, + "secondaryMetrics" : { + } + }, + { + "jmhVersion" : "1.37", + "benchmark" : "com.evolution.scache.bench.CacheBenchmark.getOrUpdateHitRandomKeys", + "mode" : "thrpt", + "threads" : 1, + "forks" : 1, + "jvm" : "/usr/lib/jvm/temurin-25-jdk-arm64/bin/java", + "jvmArgs" : [ + ], + "jdkVersion" : "25.0.4", + "vmName" : "OpenJDK 64-Bit Server VM", + "vmVersion" : "25.0.4+7-LTS", + "warmupIterations" : 1, + "warmupTime" : "3 s", + "warmupBatchSize" : 1, + "measurementIterations" : 5, + "measurementTime" : "2 s", + "measurementBatchSize" : 1, + "params" : { + "flavor" : "single" + }, + "primaryMetric" : { + "score" : 9556350.80244932, + "scoreError" : 2933656.516220063, + "scoreConfidence" : [ + 6622694.286229257, + 1.2490007318669382E7 + ], + "scorePercentiles" : { + "0.0" : 8232155.611846094, + "50.0" : 9913316.977013214, + "90.0" : 1.0064043243061196E7, + "95.0" : 1.0064043243061196E7, + "99.0" : 1.0064043243061196E7, + "99.9" : 1.0064043243061196E7, + "99.99" : 1.0064043243061196E7, + "99.999" : 1.0064043243061196E7, + "99.9999" : 1.0064043243061196E7, + "100.0" : 1.0064043243061196E7 + }, + "scoreUnit" : "ops/s", + "rawData" : [ + [ + 8232155.611846094, + 9589301.729088498, + 9913316.977013214, + 9982936.45123759, + 1.0064043243061196E7 + ] + ] + }, + "secondaryMetrics" : { + } + }, + { + "jmhVersion" : "1.37", + "benchmark" : "com.evolution.scache.bench.CacheBenchmark.getOrUpdateHitRandomKeys", + "mode" : "thrpt", + "threads" : 1, + "forks" : 1, + "jvm" : "/usr/lib/jvm/temurin-25-jdk-arm64/bin/java", + "jvmArgs" : [ + ], + "jdkVersion" : "25.0.4", + "vmName" : "OpenJDK 64-Bit Server VM", + "vmVersion" : "25.0.4+7-LTS", + "warmupIterations" : 1, + "warmupTime" : "3 s", + "warmupBatchSize" : 1, + "measurementIterations" : 5, + "measurementTime" : "2 s", + "measurementBatchSize" : 1, + "params" : { + "flavor" : "partitioned" + }, + "primaryMetric" : { + "score" : 1.0678442144623997E7, + "scoreError" : 865953.3582824023, + "scoreConfidence" : [ + 9812488.786341595, + 1.1544395502906399E7 + ], + "scorePercentiles" : { + "0.0" : 1.0283514302068304E7, + "50.0" : 1.0742798620523794E7, + "90.0" : 1.0838678211317401E7, + "95.0" : 1.0838678211317401E7, + "99.0" : 1.0838678211317401E7, + "99.9" : 1.0838678211317401E7, + "99.99" : 1.0838678211317401E7, + "99.999" : 1.0838678211317401E7, + "99.9999" : 1.0838678211317401E7, + "100.0" : 1.0838678211317401E7 + }, + "scoreUnit" : "ops/s", + "rawData" : [ + [ + 1.0283514302068304E7, + 1.0742798620523794E7, + 1.0731977149710754E7, + 1.0838678211317401E7, + 1.079524243949973E7 + ] + ] + }, + "secondaryMetrics" : { + } + }, + { + "jmhVersion" : "1.37", + "benchmark" : "com.evolution.scache.bench.CacheBenchmark.getOrUpdateHitRandomKeys", + "mode" : "thrpt", + "threads" : 1, + "forks" : 1, + "jvm" : "/usr/lib/jvm/temurin-25-jdk-arm64/bin/java", + "jvmArgs" : [ + ], + "jdkVersion" : "25.0.4", + "vmName" : "OpenJDK 64-Bit Server VM", + "vmVersion" : "25.0.4+7-LTS", + "warmupIterations" : 1, + "warmupTime" : "3 s", + "warmupBatchSize" : 1, + "measurementIterations" : 5, + "measurementTime" : "2 s", + "measurementBatchSize" : 1, + "params" : { + "flavor" : "expiring" + }, + "primaryMetric" : { + "score" : 7890637.145902408, + "scoreError" : 3207007.3351330687, + "scoreConfidence" : [ + 4683629.81076934, + 1.1097644481035477E7 + ], + "scorePercentiles" : { + "0.0" : 6637084.796563348, + "50.0" : 8442102.51538766, + "90.0" : 8502972.86107493, + "95.0" : 8502972.86107493, + "99.0" : 8502972.86107493, + "99.9" : 8502972.86107493, + "99.99" : 8502972.86107493, + "99.999" : 8502972.86107493, + "99.9999" : 8502972.86107493, + "100.0" : 8502972.86107493 + }, + "scoreUnit" : "ops/s", + "rawData" : [ + [ + 6637084.796563348, + 7425579.575101286, + 8442102.51538766, + 8445445.981384816, + 8502972.86107493 + ] + ] + }, + "secondaryMetrics" : { + } + }, + { + "jmhVersion" : "1.37", + "benchmark" : "com.evolution.scache.bench.CacheBenchmark.getOrUpdateHitSingleHotKey", + "mode" : "thrpt", + "threads" : 1, + "forks" : 1, + "jvm" : "/usr/lib/jvm/temurin-25-jdk-arm64/bin/java", + "jvmArgs" : [ + ], + "jdkVersion" : "25.0.4", + "vmName" : "OpenJDK 64-Bit Server VM", + "vmVersion" : "25.0.4+7-LTS", + "warmupIterations" : 1, + "warmupTime" : "3 s", + "warmupBatchSize" : 1, + "measurementIterations" : 5, + "measurementTime" : "2 s", + "measurementBatchSize" : 1, + "params" : { + "flavor" : "single" + }, + "primaryMetric" : { + "score" : 1.0647052535059381E7, + "scoreError" : 908172.9386971546, + "scoreConfidence" : [ + 9738879.596362226, + 1.1555225473756537E7 + ], + "scorePercentiles" : { + "0.0" : 1.0312061852126697E7, + "50.0" : 1.0761203837061074E7, + "90.0" : 1.0843945010610003E7, + "95.0" : 1.0843945010610003E7, + "99.0" : 1.0843945010610003E7, + "99.9" : 1.0843945010610003E7, + "99.99" : 1.0843945010610003E7, + "99.999" : 1.0843945010610003E7, + "99.9999" : 1.0843945010610003E7, + "100.0" : 1.0843945010610003E7 + }, + "scoreUnit" : "ops/s", + "rawData" : [ + [ + 1.0312061852126697E7, + 1.0761203837061074E7, + 1.0488442376385598E7, + 1.0843945010610003E7, + 1.0829609599113524E7 + ] + ] + }, + "secondaryMetrics" : { + } + }, + { + "jmhVersion" : "1.37", + "benchmark" : "com.evolution.scache.bench.CacheBenchmark.getOrUpdateHitSingleHotKey", + "mode" : "thrpt", + "threads" : 1, + "forks" : 1, + "jvm" : "/usr/lib/jvm/temurin-25-jdk-arm64/bin/java", + "jvmArgs" : [ + ], + "jdkVersion" : "25.0.4", + "vmName" : "OpenJDK 64-Bit Server VM", + "vmVersion" : "25.0.4+7-LTS", + "warmupIterations" : 1, + "warmupTime" : "3 s", + "warmupBatchSize" : 1, + "measurementIterations" : 5, + "measurementTime" : "2 s", + "measurementBatchSize" : 1, + "params" : { + "flavor" : "partitioned" + }, + "primaryMetric" : { + "score" : 1.2143347636833161E7, + "scoreError" : 2532473.185762147, + "scoreConfidence" : [ + 9610874.451071015, + 1.4675820822595308E7 + ], + "scorePercentiles" : { + "0.0" : 1.097553632337268E7, + "50.0" : 1.2404860398847016E7, + "90.0" : 1.2568646480506606E7, + "95.0" : 1.2568646480506606E7, + "99.0" : 1.2568646480506606E7, + "99.9" : 1.2568646480506606E7, + "99.99" : 1.2568646480506606E7, + "99.999" : 1.2568646480506606E7, + "99.9999" : 1.2568646480506606E7, + "100.0" : 1.2568646480506606E7 + }, + "scoreUnit" : "ops/s", + "rawData" : [ + [ + 1.097553632337268E7, + 1.2357246244976053E7, + 1.2568646480506606E7, + 1.2404860398847016E7, + 1.241044873646345E7 + ] + ] + }, + "secondaryMetrics" : { + } + }, + { + "jmhVersion" : "1.37", + "benchmark" : "com.evolution.scache.bench.CacheBenchmark.getOrUpdateHitSingleHotKey", + "mode" : "thrpt", + "threads" : 1, + "forks" : 1, + "jvm" : "/usr/lib/jvm/temurin-25-jdk-arm64/bin/java", + "jvmArgs" : [ + ], + "jdkVersion" : "25.0.4", + "vmName" : "OpenJDK 64-Bit Server VM", + "vmVersion" : "25.0.4+7-LTS", + "warmupIterations" : 1, + "warmupTime" : "3 s", + "warmupBatchSize" : 1, + "measurementIterations" : 5, + "measurementTime" : "2 s", + "measurementBatchSize" : 1, + "params" : { + "flavor" : "expiring" + }, + "primaryMetric" : { + "score" : 9378680.328936517, + "scoreError" : 982420.6031170777, + "scoreConfidence" : [ + 8396259.725819439, + 1.0361100932053596E7 + ], + "scorePercentiles" : { + "0.0" : 8974443.743600614, + "50.0" : 9479807.389392955, + "90.0" : 9592566.154525455, + "95.0" : 9592566.154525455, + "99.0" : 9592566.154525455, + "99.9" : 9592566.154525455, + "99.99" : 9592566.154525455, + "99.999" : 9592566.154525455, + "99.9999" : 9592566.154525455, + "100.0" : 9592566.154525455 + }, + "scoreUnit" : "ops/s", + "rawData" : [ + [ + 8974443.743600614, + 9287264.844333252, + 9479807.389392955, + 9559319.512830311, + 9592566.154525455 + ] + ] + }, + "secondaryMetrics" : { + } + }, + { + "jmhVersion" : "1.37", + "benchmark" : "com.evolution.scache.bench.CacheBenchmark.getOrUpdateInsertDistinctKeys", + "mode" : "thrpt", + "threads" : 1, + "forks" : 1, + "jvm" : "/usr/lib/jvm/temurin-25-jdk-arm64/bin/java", + "jvmArgs" : [ + ], + "jdkVersion" : "25.0.4", + "vmName" : "OpenJDK 64-Bit Server VM", + "vmVersion" : "25.0.4+7-LTS", + "warmupIterations" : 1, + "warmupTime" : "3 s", + "warmupBatchSize" : 1, + "measurementIterations" : 5, + "measurementTime" : "2 s", + "measurementBatchSize" : 1, + "params" : { + "flavor" : "single" + }, + "primaryMetric" : { + "score" : 1253353.2148731076, + "scoreError" : 144163.4089606328, + "scoreConfidence" : [ + 1109189.8059124749, + 1397516.6238337404 + ], + "scorePercentiles" : { + "0.0" : 1207483.8870237072, + "50.0" : 1254419.5979144573, + "90.0" : 1304434.668470544, + "95.0" : 1304434.668470544, + "99.0" : 1304434.668470544, + "99.9" : 1304434.668470544, + "99.99" : 1304434.668470544, + "99.999" : 1304434.668470544, + "99.9999" : 1304434.668470544, + "100.0" : 1304434.668470544 + }, + "scoreUnit" : "ops/s", + "rawData" : [ + [ + 1207483.8870237072, + 1229327.461338026, + 1304434.668470544, + 1271100.459618804, + 1254419.5979144573 + ] + ] + }, + "secondaryMetrics" : { + } + }, + { + "jmhVersion" : "1.37", + "benchmark" : "com.evolution.scache.bench.CacheBenchmark.getOrUpdateInsertDistinctKeys", + "mode" : "thrpt", + "threads" : 1, + "forks" : 1, + "jvm" : "/usr/lib/jvm/temurin-25-jdk-arm64/bin/java", + "jvmArgs" : [ + ], + "jdkVersion" : "25.0.4", + "vmName" : "OpenJDK 64-Bit Server VM", + "vmVersion" : "25.0.4+7-LTS", + "warmupIterations" : 1, + "warmupTime" : "3 s", + "warmupBatchSize" : 1, + "measurementIterations" : 5, + "measurementTime" : "2 s", + "measurementBatchSize" : 1, + "params" : { + "flavor" : "partitioned" + }, + "primaryMetric" : { + "score" : 2107224.7218189174, + "scoreError" : 73260.93856119034, + "scoreConfidence" : [ + 2033963.783257727, + 2180485.660380108 + ], + "scorePercentiles" : { + "0.0" : 2087234.443357716, + "50.0" : 2098368.9134500367, + "90.0" : 2134047.996604957, + "95.0" : 2134047.996604957, + "99.0" : 2134047.996604957, + "99.9" : 2134047.996604957, + "99.99" : 2134047.996604957, + "99.999" : 2134047.996604957, + "99.9999" : 2134047.996604957, + "100.0" : 2134047.996604957 + }, + "scoreUnit" : "ops/s", + "rawData" : [ + [ + 2097093.2303452946, + 2119379.0253365827, + 2098368.9134500367, + 2134047.996604957, + 2087234.443357716 + ] + ] + }, + "secondaryMetrics" : { + } + }, + { + "jmhVersion" : "1.37", + "benchmark" : "com.evolution.scache.bench.CacheBenchmark.getOrUpdateInsertDistinctKeys", + "mode" : "thrpt", + "threads" : 1, + "forks" : 1, + "jvm" : "/usr/lib/jvm/temurin-25-jdk-arm64/bin/java", + "jvmArgs" : [ + ], + "jdkVersion" : "25.0.4", + "vmName" : "OpenJDK 64-Bit Server VM", + "vmVersion" : "25.0.4+7-LTS", + "warmupIterations" : 1, + "warmupTime" : "3 s", + "warmupBatchSize" : 1, + "measurementIterations" : 5, + "measurementTime" : "2 s", + "measurementBatchSize" : 1, + "params" : { + "flavor" : "expiring" + }, + "primaryMetric" : { + "score" : 1914230.6612272218, + "scoreError" : 128210.15459434713, + "scoreConfidence" : [ + 1786020.5066328747, + 2042440.815821569 + ], + "scorePercentiles" : { + "0.0" : 1888847.3830820452, + "50.0" : 1894839.0163127994, + "90.0" : 1964813.3213276572, + "95.0" : 1964813.3213276572, + "99.0" : 1964813.3213276572, + "99.9" : 1964813.3213276572, + "99.99" : 1964813.3213276572, + "99.999" : 1964813.3213276572, + "99.9999" : 1964813.3213276572, + "100.0" : 1964813.3213276572 + }, + "scoreUnit" : "ops/s", + "rawData" : [ + [ + 1894839.0163127994, + 1890849.780108352, + 1931803.8053052544, + 1888847.3830820452, + 1964813.3213276572 + ] + ] + }, + "secondaryMetrics" : { + } + }, + { + "jmhVersion" : "1.37", + "benchmark" : "com.evolution.scache.bench.CacheBenchmark.mixedRandomKeys", + "mode" : "thrpt", + "threads" : 1, + "forks" : 1, + "jvm" : "/usr/lib/jvm/temurin-25-jdk-arm64/bin/java", + "jvmArgs" : [ + ], + "jdkVersion" : "25.0.4", + "vmName" : "OpenJDK 64-Bit Server VM", + "vmVersion" : "25.0.4+7-LTS", + "warmupIterations" : 1, + "warmupTime" : "3 s", + "warmupBatchSize" : 1, + "measurementIterations" : 5, + "measurementTime" : "2 s", + "measurementBatchSize" : 1, + "params" : { + "flavor" : "single" + }, + "primaryMetric" : { + "score" : 5396895.228224106, + "scoreError" : 297135.3572683813, + "scoreConfidence" : [ + 5099759.870955725, + 5694030.585492487 + ], + "scorePercentiles" : { + "0.0" : 5303767.247966121, + "50.0" : 5425071.556631504, + "90.0" : 5488523.089563385, + "95.0" : 5488523.089563385, + "99.0" : 5488523.089563385, + "99.9" : 5488523.089563385, + "99.99" : 5488523.089563385, + "99.999" : 5488523.089563385, + "99.9999" : 5488523.089563385, + "100.0" : 5488523.089563385 + }, + "scoreUnit" : "ops/s", + "rawData" : [ + [ + 5436470.3295649765, + 5330643.917394542, + 5303767.247966121, + 5425071.556631504, + 5488523.089563385 + ] + ] + }, + "secondaryMetrics" : { + } + }, + { + "jmhVersion" : "1.37", + "benchmark" : "com.evolution.scache.bench.CacheBenchmark.mixedRandomKeys", + "mode" : "thrpt", + "threads" : 1, + "forks" : 1, + "jvm" : "/usr/lib/jvm/temurin-25-jdk-arm64/bin/java", + "jvmArgs" : [ + ], + "jdkVersion" : "25.0.4", + "vmName" : "OpenJDK 64-Bit Server VM", + "vmVersion" : "25.0.4+7-LTS", + "warmupIterations" : 1, + "warmupTime" : "3 s", + "warmupBatchSize" : 1, + "measurementIterations" : 5, + "measurementTime" : "2 s", + "measurementBatchSize" : 1, + "params" : { + "flavor" : "partitioned" + }, + "primaryMetric" : { + "score" : 6319172.36619421, + "scoreError" : 341152.9393909181, + "scoreConfidence" : [ + 5978019.426803292, + 6660325.305585128 + ], + "scorePercentiles" : { + "0.0" : 6247116.580698031, + "50.0" : 6309656.365791242, + "90.0" : 6466804.885572892, + "95.0" : 6466804.885572892, + "99.0" : 6466804.885572892, + "99.9" : 6466804.885572892, + "99.99" : 6466804.885572892, + "99.999" : 6466804.885572892, + "99.9999" : 6466804.885572892, + "100.0" : 6466804.885572892 + }, + "scoreUnit" : "ops/s", + "rawData" : [ + [ + 6247116.580698031, + 6253451.933914791, + 6309656.365791242, + 6318832.064994099, + 6466804.885572892 + ] + ] + }, + "secondaryMetrics" : { + } + }, + { + "jmhVersion" : "1.37", + "benchmark" : "com.evolution.scache.bench.CacheBenchmark.mixedRandomKeys", + "mode" : "thrpt", + "threads" : 1, + "forks" : 1, + "jvm" : "/usr/lib/jvm/temurin-25-jdk-arm64/bin/java", + "jvmArgs" : [ + ], + "jdkVersion" : "25.0.4", + "vmName" : "OpenJDK 64-Bit Server VM", + "vmVersion" : "25.0.4+7-LTS", + "warmupIterations" : 1, + "warmupTime" : "3 s", + "warmupBatchSize" : 1, + "measurementIterations" : 5, + "measurementTime" : "2 s", + "measurementBatchSize" : 1, + "params" : { + "flavor" : "expiring" + }, + "primaryMetric" : { + "score" : 5079640.769863769, + "scoreError" : 196585.28113545506, + "scoreConfidence" : [ + 4883055.488728315, + 5276226.050999224 + ], + "scorePercentiles" : { + "0.0" : 5022015.116776671, + "50.0" : 5064039.304473805, + "90.0" : 5154656.62996337, + "95.0" : 5154656.62996337, + "99.0" : 5154656.62996337, + "99.9" : 5154656.62996337, + "99.99" : 5154656.62996337, + "99.999" : 5154656.62996337, + "99.9999" : 5154656.62996337, + "100.0" : 5154656.62996337 + }, + "scoreUnit" : "ops/s", + "rawData" : [ + [ + 5064039.304473805, + 5053923.423285929, + 5022015.116776671, + 5103569.374819071, + 5154656.62996337 + ] + ] + }, + "secondaryMetrics" : { + } + }, + { + "jmhVersion" : "1.37", + "benchmark" : "com.evolution.scache.bench.CacheBenchmark.modifyInsertDistinctKeys", + "mode" : "thrpt", + "threads" : 1, + "forks" : 1, + "jvm" : "/usr/lib/jvm/temurin-25-jdk-arm64/bin/java", + "jvmArgs" : [ + ], + "jdkVersion" : "25.0.4", + "vmName" : "OpenJDK 64-Bit Server VM", + "vmVersion" : "25.0.4+7-LTS", + "warmupIterations" : 1, + "warmupTime" : "3 s", + "warmupBatchSize" : 1, + "measurementIterations" : 5, + "measurementTime" : "2 s", + "measurementBatchSize" : 1, + "params" : { + "flavor" : "single" + }, + "primaryMetric" : { + "score" : 1878912.4210543842, + "scoreError" : 710903.9727294672, + "scoreConfidence" : [ + 1168008.448324917, + 2589816.3937838515 + ], + "scorePercentiles" : { + "0.0" : 1600679.0614138204, + "50.0" : 1904158.1997164863, + "90.0" : 2050392.6750582848, + "95.0" : 2050392.6750582848, + "99.0" : 2050392.6750582848, + "99.9" : 2050392.6750582848, + "99.99" : 2050392.6750582848, + "99.999" : 2050392.6750582848, + "99.9999" : 2050392.6750582848, + "100.0" : 2050392.6750582848 + }, + "scoreUnit" : "ops/s", + "rawData" : [ + [ + 1600679.0614138204, + 1806619.426479329, + 1904158.1997164863, + 2032712.7426040012, + 2050392.6750582848 + ] + ] + }, + "secondaryMetrics" : { + } + }, + { + "jmhVersion" : "1.37", + "benchmark" : "com.evolution.scache.bench.CacheBenchmark.modifyInsertDistinctKeys", + "mode" : "thrpt", + "threads" : 1, + "forks" : 1, + "jvm" : "/usr/lib/jvm/temurin-25-jdk-arm64/bin/java", + "jvmArgs" : [ + ], + "jdkVersion" : "25.0.4", + "vmName" : "OpenJDK 64-Bit Server VM", + "vmVersion" : "25.0.4+7-LTS", + "warmupIterations" : 1, + "warmupTime" : "3 s", + "warmupBatchSize" : 1, + "measurementIterations" : 5, + "measurementTime" : "2 s", + "measurementBatchSize" : 1, + "params" : { + "flavor" : "partitioned" + }, + "primaryMetric" : { + "score" : 6129375.557986379, + "scoreError" : 1038058.0956069265, + "scoreConfidence" : [ + 5091317.462379452, + 7167433.6535933055 + ], + "scorePercentiles" : { + "0.0" : 5737430.714697042, + "50.0" : 6223255.650005074, + "90.0" : 6430690.723111775, + "95.0" : 6430690.723111775, + "99.0" : 6430690.723111775, + "99.9" : 6430690.723111775, + "99.99" : 6430690.723111775, + "99.999" : 6430690.723111775, + "99.9999" : 6430690.723111775, + "100.0" : 6430690.723111775 + }, + "scoreUnit" : "ops/s", + "rawData" : [ + [ + 5737430.714697042, + 6430690.723111775, + 6264615.258024062, + 6223255.650005074, + 5990885.444093944 + ] + ] + }, + "secondaryMetrics" : { + } + }, + { + "jmhVersion" : "1.37", + "benchmark" : "com.evolution.scache.bench.CacheBenchmark.modifyInsertDistinctKeys", + "mode" : "thrpt", + "threads" : 1, + "forks" : 1, + "jvm" : "/usr/lib/jvm/temurin-25-jdk-arm64/bin/java", + "jvmArgs" : [ + ], + "jdkVersion" : "25.0.4", + "vmName" : "OpenJDK 64-Bit Server VM", + "vmVersion" : "25.0.4+7-LTS", + "warmupIterations" : 1, + "warmupTime" : "3 s", + "warmupBatchSize" : 1, + "measurementIterations" : 5, + "measurementTime" : "2 s", + "measurementBatchSize" : 1, + "params" : { + "flavor" : "expiring" + }, + "primaryMetric" : { + "score" : 7146225.916640465, + "scoreError" : 1320913.3081067128, + "scoreConfidence" : [ + 5825312.608533752, + 8467139.224747177 + ], + "scorePercentiles" : { + "0.0" : 6681314.535673143, + "50.0" : 7371364.66202603, + "90.0" : 7423390.68066954, + "95.0" : 7423390.68066954, + "99.0" : 7423390.68066954, + "99.9" : 7423390.68066954, + "99.99" : 7423390.68066954, + "99.999" : 7423390.68066954, + "99.9999" : 7423390.68066954, + "100.0" : 7423390.68066954 + }, + "scoreUnit" : "ops/s", + "rawData" : [ + [ + 6876185.396613163, + 6681314.535673143, + 7423390.68066954, + 7378874.308220445, + 7371364.66202603 + ] + ] + }, + "secondaryMetrics" : { + } + }, + { + "jmhVersion" : "1.37", + "benchmark" : "com.evolution.scache.bench.CacheBenchmark.modifyUpdateRandomKeys", + "mode" : "thrpt", + "threads" : 1, + "forks" : 1, + "jvm" : "/usr/lib/jvm/temurin-25-jdk-arm64/bin/java", + "jvmArgs" : [ + ], + "jdkVersion" : "25.0.4", + "vmName" : "OpenJDK 64-Bit Server VM", + "vmVersion" : "25.0.4+7-LTS", + "warmupIterations" : 1, + "warmupTime" : "3 s", + "warmupBatchSize" : 1, + "measurementIterations" : 5, + "measurementTime" : "2 s", + "measurementBatchSize" : 1, + "params" : { + "flavor" : "single" + }, + "primaryMetric" : { + "score" : 7235161.054910319, + "scoreError" : 868101.8851271113, + "scoreConfidence" : [ + 6367059.169783208, + 8103262.94003743 + ], + "scorePercentiles" : { + "0.0" : 6838042.764532054, + "50.0" : 7333214.81689126, + "90.0" : 7380274.508596127, + "95.0" : 7380274.508596127, + "99.0" : 7380274.508596127, + "99.9" : 7380274.508596127, + "99.99" : 7380274.508596127, + "99.999" : 7380274.508596127, + "99.9999" : 7380274.508596127, + "100.0" : 7380274.508596127 + }, + "scoreUnit" : "ops/s", + "rawData" : [ + [ + 6838042.764532054, + 7272939.186993804, + 7333214.81689126, + 7380274.508596127, + 7351333.997538346 + ] + ] + }, + "secondaryMetrics" : { + } + }, + { + "jmhVersion" : "1.37", + "benchmark" : "com.evolution.scache.bench.CacheBenchmark.modifyUpdateRandomKeys", + "mode" : "thrpt", + "threads" : 1, + "forks" : 1, + "jvm" : "/usr/lib/jvm/temurin-25-jdk-arm64/bin/java", + "jvmArgs" : [ + ], + "jdkVersion" : "25.0.4", + "vmName" : "OpenJDK 64-Bit Server VM", + "vmVersion" : "25.0.4+7-LTS", + "warmupIterations" : 1, + "warmupTime" : "3 s", + "warmupBatchSize" : 1, + "measurementIterations" : 5, + "measurementTime" : "2 s", + "measurementBatchSize" : 1, + "params" : { + "flavor" : "partitioned" + }, + "primaryMetric" : { + "score" : 8030418.014427399, + "scoreError" : 1135749.166291302, + "scoreConfidence" : [ + 6894668.848136097, + 9166167.180718701 + ], + "scorePercentiles" : { + "0.0" : 7542307.161561721, + "50.0" : 8099953.18405735, + "90.0" : 8270018.419383614, + "95.0" : 8270018.419383614, + "99.0" : 8270018.419383614, + "99.9" : 8270018.419383614, + "99.99" : 8270018.419383614, + "99.999" : 8270018.419383614, + "99.9999" : 8270018.419383614, + "100.0" : 8270018.419383614 + }, + "scoreUnit" : "ops/s", + "rawData" : [ + [ + 7542307.161561721, + 7994463.829800631, + 8099953.18405735, + 8245347.477333684, + 8270018.419383614 + ] + ] + }, + "secondaryMetrics" : { + } + }, + { + "jmhVersion" : "1.37", + "benchmark" : "com.evolution.scache.bench.CacheBenchmark.modifyUpdateRandomKeys", + "mode" : "thrpt", + "threads" : 1, + "forks" : 1, + "jvm" : "/usr/lib/jvm/temurin-25-jdk-arm64/bin/java", + "jvmArgs" : [ + ], + "jdkVersion" : "25.0.4", + "vmName" : "OpenJDK 64-Bit Server VM", + "vmVersion" : "25.0.4+7-LTS", + "warmupIterations" : 1, + "warmupTime" : "3 s", + "warmupBatchSize" : 1, + "measurementIterations" : 5, + "measurementTime" : "2 s", + "measurementBatchSize" : 1, + "params" : { + "flavor" : "expiring" + }, + "primaryMetric" : { + "score" : 9630572.550058808, + "scoreError" : 2720816.029386905, + "scoreConfidence" : [ + 6909756.520671903, + 1.2351388579445712E7 + ], + "scorePercentiles" : { + "0.0" : 8388855.925012687, + "50.0" : 9987812.57645374, + "90.0" : 1.0064960216799403E7, + "95.0" : 1.0064960216799403E7, + "99.0" : 1.0064960216799403E7, + "99.9" : 1.0064960216799403E7, + "99.99" : 1.0064960216799403E7, + "99.999" : 1.0064960216799403E7, + "99.9999" : 1.0064960216799403E7, + "100.0" : 1.0064960216799403E7 + }, + "scoreUnit" : "ops/s", + "rawData" : [ + [ + 8388855.925012687, + 9718505.620580476, + 9992728.41144773, + 9987812.57645374, + 1.0064960216799403E7 + ] + ] + }, + "secondaryMetrics" : { + } + }, + { + "jmhVersion" : "1.37", + "benchmark" : "com.evolution.scache.bench.CacheBenchmark.putInsertDistinctKeys", + "mode" : "thrpt", + "threads" : 1, + "forks" : 1, + "jvm" : "/usr/lib/jvm/temurin-25-jdk-arm64/bin/java", + "jvmArgs" : [ + ], + "jdkVersion" : "25.0.4", + "vmName" : "OpenJDK 64-Bit Server VM", + "vmVersion" : "25.0.4+7-LTS", + "warmupIterations" : 1, + "warmupTime" : "3 s", + "warmupBatchSize" : 1, + "measurementIterations" : 5, + "measurementTime" : "2 s", + "measurementBatchSize" : 1, + "params" : { + "flavor" : "single" + }, + "primaryMetric" : { + "score" : 1663899.5832672145, + "scoreError" : 659633.0505148813, + "scoreConfidence" : [ + 1004266.5327523332, + 2323532.6337820957 + ], + "scorePercentiles" : { + "0.0" : 1372915.4396342183, + "50.0" : 1763971.7069522035, + "90.0" : 1772858.9117825238, + "95.0" : 1772858.9117825238, + "99.0" : 1772858.9117825238, + "99.9" : 1772858.9117825238, + "99.99" : 1772858.9117825238, + "99.999" : 1772858.9117825238, + "99.9999" : 1772858.9117825238, + "100.0" : 1772858.9117825238 + }, + "scoreUnit" : "ops/s", + "rawData" : [ + [ + 1372915.4396342183, + 1643783.3081010312, + 1763971.7069522035, + 1765968.5498660954, + 1772858.9117825238 + ] + ] + }, + "secondaryMetrics" : { + } + }, + { + "jmhVersion" : "1.37", + "benchmark" : "com.evolution.scache.bench.CacheBenchmark.putInsertDistinctKeys", + "mode" : "thrpt", + "threads" : 1, + "forks" : 1, + "jvm" : "/usr/lib/jvm/temurin-25-jdk-arm64/bin/java", + "jvmArgs" : [ + ], + "jdkVersion" : "25.0.4", + "vmName" : "OpenJDK 64-Bit Server VM", + "vmVersion" : "25.0.4+7-LTS", + "warmupIterations" : 1, + "warmupTime" : "3 s", + "warmupBatchSize" : 1, + "measurementIterations" : 5, + "measurementTime" : "2 s", + "measurementBatchSize" : 1, + "params" : { + "flavor" : "partitioned" + }, + "primaryMetric" : { + "score" : 5398596.1357309045, + "scoreError" : 925411.849705046, + "scoreConfidence" : [ + 4473184.2860258585, + 6324007.985435951 + ], + "scorePercentiles" : { + "0.0" : 5195910.895499133, + "50.0" : 5340762.000445858, + "90.0" : 5814079.125259639, + "95.0" : 5814079.125259639, + "99.0" : 5814079.125259639, + "99.9" : 5814079.125259639, + "99.99" : 5814079.125259639, + "99.999" : 5814079.125259639, + "99.9999" : 5814079.125259639, + "100.0" : 5814079.125259639 + }, + "scoreUnit" : "ops/s", + "rawData" : [ + [ + 5195910.895499133, + 5340762.000445858, + 5352485.278109238, + 5814079.125259639, + 5289743.379340656 + ] + ] + }, + "secondaryMetrics" : { + } + }, + { + "jmhVersion" : "1.37", + "benchmark" : "com.evolution.scache.bench.CacheBenchmark.putInsertDistinctKeys", + "mode" : "thrpt", + "threads" : 1, + "forks" : 1, + "jvm" : "/usr/lib/jvm/temurin-25-jdk-arm64/bin/java", + "jvmArgs" : [ + ], + "jdkVersion" : "25.0.4", + "vmName" : "OpenJDK 64-Bit Server VM", + "vmVersion" : "25.0.4+7-LTS", + "warmupIterations" : 1, + "warmupTime" : "3 s", + "warmupBatchSize" : 1, + "measurementIterations" : 5, + "measurementTime" : "2 s", + "measurementBatchSize" : 1, + "params" : { + "flavor" : "expiring" + }, + "primaryMetric" : { + "score" : 5468882.5744210975, + "scoreError" : 757216.8948121608, + "scoreConfidence" : [ + 4711665.679608936, + 6226099.469233259 + ], + "scorePercentiles" : { + "0.0" : 5199349.300253403, + "50.0" : 5537402.313959748, + "90.0" : 5668745.757221077, + "95.0" : 5668745.757221077, + "99.0" : 5668745.757221077, + "99.9" : 5668745.757221077, + "99.99" : 5668745.757221077, + "99.999" : 5668745.757221077, + "99.9999" : 5668745.757221077, + "100.0" : 5668745.757221077 + }, + "scoreUnit" : "ops/s", + "rawData" : [ + [ + 5537402.313959748, + 5332726.631343062, + 5199349.300253403, + 5668745.757221077, + 5606188.869328199 + ] + ] + }, + "secondaryMetrics" : { + } + }, + { + "jmhVersion" : "1.37", + "benchmark" : "com.evolution.scache.bench.CacheBenchmark.putReplaceRandomKeys", + "mode" : "thrpt", + "threads" : 1, + "forks" : 1, + "jvm" : "/usr/lib/jvm/temurin-25-jdk-arm64/bin/java", + "jvmArgs" : [ + ], + "jdkVersion" : "25.0.4", + "vmName" : "OpenJDK 64-Bit Server VM", + "vmVersion" : "25.0.4+7-LTS", + "warmupIterations" : 1, + "warmupTime" : "3 s", + "warmupBatchSize" : 1, + "measurementIterations" : 5, + "measurementTime" : "2 s", + "measurementBatchSize" : 1, + "params" : { + "flavor" : "single" + }, + "primaryMetric" : { + "score" : 8133525.524794443, + "scoreError" : 172710.0605249215, + "scoreConfidence" : [ + 7960815.464269521, + 8306235.585319364 + ], + "scorePercentiles" : { + "0.0" : 8076726.563299344, + "50.0" : 8137610.041392742, + "90.0" : 8188105.828193315, + "95.0" : 8188105.828193315, + "99.0" : 8188105.828193315, + "99.9" : 8188105.828193315, + "99.99" : 8188105.828193315, + "99.999" : 8188105.828193315, + "99.9999" : 8188105.828193315, + "100.0" : 8188105.828193315 + }, + "scoreUnit" : "ops/s", + "rawData" : [ + [ + 8076726.563299344, + 8162786.496507161, + 8102398.694579652, + 8137610.041392742, + 8188105.828193315 + ] + ] + }, + "secondaryMetrics" : { + } + }, + { + "jmhVersion" : "1.37", + "benchmark" : "com.evolution.scache.bench.CacheBenchmark.putReplaceRandomKeys", + "mode" : "thrpt", + "threads" : 1, + "forks" : 1, + "jvm" : "/usr/lib/jvm/temurin-25-jdk-arm64/bin/java", + "jvmArgs" : [ + ], + "jdkVersion" : "25.0.4", + "vmName" : "OpenJDK 64-Bit Server VM", + "vmVersion" : "25.0.4+7-LTS", + "warmupIterations" : 1, + "warmupTime" : "3 s", + "warmupBatchSize" : 1, + "measurementIterations" : 5, + "measurementTime" : "2 s", + "measurementBatchSize" : 1, + "params" : { + "flavor" : "partitioned" + }, + "primaryMetric" : { + "score" : 7371216.039769781, + "scoreError" : 561587.2248889989, + "scoreConfidence" : [ + 6809628.814880782, + 7932803.26465878 + ], + "scorePercentiles" : { + "0.0" : 7224682.2587843845, + "50.0" : 7310793.784925936, + "90.0" : 7541122.523761597, + "95.0" : 7541122.523761597, + "99.0" : 7541122.523761597, + "99.9" : 7541122.523761597, + "99.99" : 7541122.523761597, + "99.999" : 7541122.523761597, + "99.9999" : 7541122.523761597, + "100.0" : 7541122.523761597 + }, + "scoreUnit" : "ops/s", + "rawData" : [ + [ + 7224682.2587843845, + 7310793.784925936, + 7266447.468722469, + 7541122.523761597, + 7513034.162654514 + ] + ] + }, + "secondaryMetrics" : { + } + }, + { + "jmhVersion" : "1.37", + "benchmark" : "com.evolution.scache.bench.CacheBenchmark.putReplaceRandomKeys", + "mode" : "thrpt", + "threads" : 1, + "forks" : 1, + "jvm" : "/usr/lib/jvm/temurin-25-jdk-arm64/bin/java", + "jvmArgs" : [ + ], + "jdkVersion" : "25.0.4", + "vmName" : "OpenJDK 64-Bit Server VM", + "vmVersion" : "25.0.4+7-LTS", + "warmupIterations" : 1, + "warmupTime" : "3 s", + "warmupBatchSize" : 1, + "measurementIterations" : 5, + "measurementTime" : "2 s", + "measurementBatchSize" : 1, + "params" : { + "flavor" : "expiring" + }, + "primaryMetric" : { + "score" : 7883961.811926027, + "scoreError" : 614479.7651965666, + "scoreConfidence" : [ + 7269482.04672946, + 8498441.577122593 + ], + "scorePercentiles" : { + "0.0" : 7741411.415805305, + "50.0" : 7809309.410113599, + "90.0" : 8065680.21956806, + "95.0" : 8065680.21956806, + "99.0" : 8065680.21956806, + "99.9" : 8065680.21956806, + "99.99" : 8065680.21956806, + "99.999" : 8065680.21956806, + "99.9999" : 8065680.21956806, + "100.0" : 8065680.21956806 + }, + "scoreUnit" : "ops/s", + "rawData" : [ + [ + 7741411.415805305, + 7756245.470820097, + 7809309.410113599, + 8065680.21956806, + 8047162.543323071 + ] + ] + }, + "secondaryMetrics" : { + } + }, + { + "jmhVersion" : "1.37", + "benchmark" : "com.evolution.scache.bench.CacheBenchmark.removeAndPutRandomKeys", + "mode" : "thrpt", + "threads" : 1, + "forks" : 1, + "jvm" : "/usr/lib/jvm/temurin-25-jdk-arm64/bin/java", + "jvmArgs" : [ + ], + "jdkVersion" : "25.0.4", + "vmName" : "OpenJDK 64-Bit Server VM", + "vmVersion" : "25.0.4+7-LTS", + "warmupIterations" : 1, + "warmupTime" : "3 s", + "warmupBatchSize" : 1, + "measurementIterations" : 5, + "measurementTime" : "2 s", + "measurementBatchSize" : 1, + "params" : { + "flavor" : "single" + }, + "primaryMetric" : { + "score" : 843648.4191691332, + "scoreError" : 96569.37046297507, + "scoreConfidence" : [ + 747079.0487061581, + 940217.7896321083 + ], + "scorePercentiles" : { + "0.0" : 808168.6869634853, + "50.0" : 852732.3205652174, + "90.0" : 873546.9671121296, + "95.0" : 873546.9671121296, + "99.0" : 873546.9671121296, + "99.9" : 873546.9671121296, + "99.99" : 873546.9671121296, + "99.999" : 873546.9671121296, + "99.9999" : 873546.9671121296, + "100.0" : 873546.9671121296 + }, + "scoreUnit" : "ops/s", + "rawData" : [ + [ + 853610.0925564962, + 852732.3205652174, + 808168.6869634853, + 830184.0286483376, + 873546.9671121296 + ] + ] + }, + "secondaryMetrics" : { + } + }, + { + "jmhVersion" : "1.37", + "benchmark" : "com.evolution.scache.bench.CacheBenchmark.removeAndPutRandomKeys", + "mode" : "thrpt", + "threads" : 1, + "forks" : 1, + "jvm" : "/usr/lib/jvm/temurin-25-jdk-arm64/bin/java", + "jvmArgs" : [ + ], + "jdkVersion" : "25.0.4", + "vmName" : "OpenJDK 64-Bit Server VM", + "vmVersion" : "25.0.4+7-LTS", + "warmupIterations" : 1, + "warmupTime" : "3 s", + "warmupBatchSize" : 1, + "measurementIterations" : 5, + "measurementTime" : "2 s", + "measurementBatchSize" : 1, + "params" : { + "flavor" : "partitioned" + }, + "primaryMetric" : { + "score" : 2453317.648717503, + "scoreError" : 157696.61327866913, + "scoreConfidence" : [ + 2295621.0354388338, + 2611014.2619961724 + ], + "scorePercentiles" : { + "0.0" : 2398732.007820915, + "50.0" : 2467664.707986163, + "90.0" : 2492391.9965532986, + "95.0" : 2492391.9965532986, + "99.0" : 2492391.9965532986, + "99.9" : 2492391.9965532986, + "99.99" : 2492391.9965532986, + "99.999" : 2492391.9965532986, + "99.9999" : 2492391.9965532986, + "100.0" : 2492391.9965532986 + }, + "scoreUnit" : "ops/s", + "rawData" : [ + [ + 2398732.007820915, + 2467664.707986163, + 2492391.9965532986, + 2485489.1753480323, + 2422310.355879108 + ] + ] + }, + "secondaryMetrics" : { + } + }, + { + "jmhVersion" : "1.37", + "benchmark" : "com.evolution.scache.bench.CacheBenchmark.removeAndPutRandomKeys", + "mode" : "thrpt", + "threads" : 1, + "forks" : 1, + "jvm" : "/usr/lib/jvm/temurin-25-jdk-arm64/bin/java", + "jvmArgs" : [ + ], + "jdkVersion" : "25.0.4", + "vmName" : "OpenJDK 64-Bit Server VM", + "vmVersion" : "25.0.4+7-LTS", + "warmupIterations" : 1, + "warmupTime" : "3 s", + "warmupBatchSize" : 1, + "measurementIterations" : 5, + "measurementTime" : "2 s", + "measurementBatchSize" : 1, + "params" : { + "flavor" : "expiring" + }, + "primaryMetric" : { + "score" : 2415054.3817994045, + "scoreError" : 68398.59839741555, + "scoreConfidence" : [ + 2346655.783401989, + 2483452.98019682 + ], + "scorePercentiles" : { + "0.0" : 2385992.235659156, + "50.0" : 2426136.6321967044, + "90.0" : 2426753.735994349, + "95.0" : 2426753.735994349, + "99.0" : 2426753.735994349, + "99.9" : 2426753.735994349, + "99.99" : 2426753.735994349, + "99.999" : 2426753.735994349, + "99.9999" : 2426753.735994349, + "100.0" : 2426753.735994349 + }, + "scoreUnit" : "ops/s", + "rawData" : [ + [ + 2385992.235659156, + 2409886.2173242806, + 2426136.6321967044, + 2426753.735994349, + 2426503.0878225327 + ] + ] + }, + "secondaryMetrics" : { + } + } +] + + diff --git a/benchmark/src/main/scala/com/evolution/scache/bench/CacheBenchmark.scala b/benchmark/src/main/scala/com/evolution/scache/bench/CacheBenchmark.scala new file mode 100644 index 0000000..f858999 --- /dev/null +++ b/benchmark/src/main/scala/com/evolution/scache/bench/CacheBenchmark.scala @@ -0,0 +1,285 @@ +package com.evolution.scache.bench + +import cats.effect.implicits.* +import cats.effect.unsafe.implicits.global +import cats.effect.{Deferred, IO} +import cats.syntax.all.* +import com.evolution.scache.{Cache, ExpiringCache, LoadingCache} +import org.openjdk.jmh.annotations.* + +import java.util.concurrent.TimeUnit +import scala.concurrent.duration.* + +/** + * Cache operations under contention. + * + * One invocation is the whole workload, `Fibers` fibers running `OpsPerFiber` cache operations + * each, so the reported number is cache operations per second, contention included. + * + * A full run takes under ten minutes, hence the modest iteration counts: the numbers are good + * enough to compare implementations or spot a regression, not to split hairs over a few percent. + * + * {{{ + * sbt "benchmark/Jmh/run" + * sbt "benchmark/Jmh/run -p flavor=partitioned .*getOrUpdateHitRandom.*" + * }}} + */ +object CacheBenchmark { + + final val Fibers = 8 + final val OpsPerFiber = 20000 + final val Ops = 160000 + final val KeySpace = 10000 + + private val fiberIndices = (0 until Fibers).toList + private val opIndices = (0 until OpsPerFiber).toList + + /** + * Key of the `n`-th operation of a scenario walking the key space pseudo-randomly. + * + * A hash of the operation index rather than a random number, so that every implementation and + * every iteration sees the very same key sequence, which is what makes the numbers comparable, + * and so that no shared random generator sits between the fibers and the cache. + */ + def key(fiber: Int, i: Int): Int = { + val n = fiber * OpsPerFiber + i + val h = n * 0x9e3775cd + ((h ^ (h >>> 16)) & Int.MaxValue) % KeySpace + } + + def parRun(op: (Int, Int) => IO[Unit]): IO[Unit] = { + fiberIndices.parTraverse_ { fiber => + opIndices.traverse_ { i => op(fiber, i) } + } + } +} + +/** + * Cache under benchmark, allocated once per trial. + * + * `flavor` selects how it is put together: a single unpartitioned `LoadingCache`, the partitioned + * `Cache.loading`, or the partitioned `Cache.expiring` with expiration far enough away not to + * interfere. + */ +@State(Scope.Benchmark) +abstract class CacheState { + + @Param(Array("single", "partitioned", "expiring")) + var flavor: String = "partitioned" + + var cache: Cache[IO, Int, Int] = null + + private var release: IO[Unit] = IO.unit + + private def resource = { + val expireAfterRead = 1.hour + flavor match { + case "single" => LoadingCache.of[IO, Int, Int] + case "partitioned" => Cache.loading[IO, Int, Int] + case "expiring" => Cache.expiring[IO, Int, Int](ExpiringCache.Config[IO, Int, Int](expireAfterRead)) + case flavor => sys.error(s"unknown flavor=$flavor") + } + } + + @Setup(Level.Trial) + def allocate(): Unit = { + val (cache, release) = resource.allocated.unsafeRunSync() + this.cache = cache + this.release = release + } + + @TearDown(Level.Trial) + def free(): Unit = release.unsafeRunSync() +} + +/** + * Cache emptied before every invocation, so that the scenarios adding keys always take the path of + * a missing key. + */ +@State(Scope.Benchmark) +class EmptyCacheState extends CacheState { + + @Setup(Level.Invocation) + def empty(): Unit = cache.clear.flatten.unsafeRunSync() +} + +/** + * Cache holding the whole key space, refilled between the iterations, so that the scenarios reading + * or replacing keys always take the path of a present key. + */ +@State(Scope.Benchmark) +class PopulatedCacheState extends CacheState { + + @Setup(Level.Iteration) + def populate(): Unit = { + (0 until CacheBenchmark.KeySpace) + .toList + .traverse_ { key => cache.put(key, key).flatten } + .unsafeRunSync() + } +} + +@BenchmarkMode(Array(Mode.Throughput)) +@OutputTimeUnit(TimeUnit.SECONDS) +@OperationsPerInvocation(160000) +@Warmup(iterations = 1, time = 3, timeUnit = TimeUnit.SECONDS) +@Measurement(iterations = 5, time = 2, timeUnit = TimeUnit.SECONDS) +@Fork(1) +@Threads(1) +class CacheBenchmark { + + import CacheBenchmark.* + + @Benchmark + def getOrUpdateInsertDistinctKeys(state: EmptyCacheState): Unit = { + parRun { (fiber, i) => + val key = fiber * OpsPerFiber + i + state.cache.getOrUpdate(key)(key.pure[IO]).void + }.unsafeRunSync() + } + + @Benchmark + def putInsertDistinctKeys(state: EmptyCacheState): Unit = { + parRun { (fiber, i) => + val key = fiber * OpsPerFiber + i + state.cache.put(key, i).flatten.void + }.unsafeRunSync() + } + + @Benchmark + def modifyInsertDistinctKeys(state: EmptyCacheState): Unit = { + parRun { (fiber, i) => + val key = fiber * OpsPerFiber + i + state.cache.modify(key) { _ => ((), Cache.Directive.Put(i, none)) }.void + }.unsafeRunSync() + } + + /** + * Not the same as [[getHitRandomKeys]]: `getOrUpdate` of a key that is already there still has to + * decide between a hit and a miss, which is where the old implementation touched the shared `Ref` + * even though it ended up returning a cached value. + */ + @Benchmark + def getOrUpdateHitRandomKeys(state: PopulatedCacheState): Unit = { + parRun { (fiber, i) => + val k = key(fiber, i) + state.cache.getOrUpdate(k)(k.pure[IO]).void + }.unsafeRunSync() + } + + @Benchmark + def getOrUpdateHitSingleHotKey(state: PopulatedCacheState): Unit = { + parRun { (_, _) => state.cache.getOrUpdate(0)(0.pure[IO]).void }.unsafeRunSync() + } + + @Benchmark + def getHitRandomKeys(state: PopulatedCacheState): Unit = { + parRun { (fiber, i) => state.cache.get(key(fiber, i)).void }.unsafeRunSync() + } + + @Benchmark + def get1HitRandomKeys(state: PopulatedCacheState): Unit = { + parRun { (fiber, i) => state.cache.get1(key(fiber, i)).void }.unsafeRunSync() + } + + @Benchmark + def containsRandomKeys(state: PopulatedCacheState): Unit = { + parRun { (fiber, i) => state.cache.contains(key(fiber, i)).void }.unsafeRunSync() + } + + @Benchmark + def putReplaceRandomKeys(state: PopulatedCacheState): Unit = { + parRun { (fiber, i) => state.cache.put(key(fiber, i), i).flatten.void }.unsafeRunSync() + } + + @Benchmark + def modifyUpdateRandomKeys(state: PopulatedCacheState): Unit = { + parRun { (fiber, i) => + state + .cache + .modify(key(fiber, i)) { + case Some(value) => ((), Cache.Directive.Put(value + 1, none)) + case None => ((), Cache.Directive.Ignore) + } + .void + }.unsafeRunSync() + } + + @Benchmark + def removeAndPutRandomKeys(state: PopulatedCacheState): Unit = { + parRun { (fiber, i) => + val k = key(fiber, i) + state.cache.remove(k).flatten *> state.cache.put(k, i).flatten.void + }.unsafeRunSync() + } + + @Benchmark + def mixedRandomKeys(state: PopulatedCacheState): Unit = { + parRun { (fiber, i) => + val k = key(fiber, i) + (i % 10) match { + case 0 => state.cache.put(k, i).flatten.void + case 1 => state.cache.remove(k).flatten.void + case 2 => state.cache.modify(k) { _ => ((), Cache.Directive.Put(i, none)) }.void + case 3 | 4 => state.cache.get(k).void + case _ => state.cache.getOrUpdate(k)(i.pure[IO]).void + } + }.unsafeRunSync() + } + + /** + * Cancellation of an in-flight load: install a `Loading` entry, wait until the load has actually + * started, cancel it, which unlinks the key and completes the entry's `Deferred`. + * + * One operation is the whole start-load-cancel cycle. No number to compare against before the + * `MapRef` rewrite: loads were not cancelable there, so this scenario would hang. + */ + @Benchmark + def getOrUpdateCancelDistinctKeys(state: EmptyCacheState): Unit = { + parRun { (fiber, i) => + val key = fiber * OpsPerFiber + i + Deferred[IO, Unit].flatMap { started => + state + .cache + .getOrUpdate(key) { started.complete(()) *> IO.never } + .start + .flatMap { loading => started.get *> loading.cancel } + } + }.unsafeRunSync() + } + + /** + * Same as [[getOrUpdateCancelDistinctKeys]], but with another fiber blocked on the loading entry + * when the load is canceled, so the cycle also covers unblocking the waiter, which either fails + * with the cancellation error or repeats the lookup and installs its own value. + */ + @Benchmark + def getOrUpdateCancelWithWaiter(state: EmptyCacheState): Unit = { + parRun { (fiber, i) => + val key = fiber * OpsPerFiber + i + Deferred[IO, Unit].flatMap { started => + for { + loading <- state.cache.getOrUpdate(key) { started.complete(()) *> IO.never }.start + _ <- started.get + waiter <- state.cache.getOrUpdate(key)(i.pure[IO]).attempt.start + _ <- loading.cancel + _ <- waiter.joinWithNever + } yield () + } + }.unsafeRunSync() + } + + /** + * Enumeration of the whole cache, one full traversal of `KeySpace` entries per operation, hence + * measured per traversal rather than per key. + */ + @Benchmark + @OperationsPerInvocation(1) + def foldMapWholeCache(state: PopulatedCacheState): Unit = { + state + .cache + .foldMap { case (_, value) => value.fold(identity, _.pure[IO]) } + .void + .unsafeRunSync() + } +} diff --git a/build.sbt b/build.sbt index 0750b8a..6e1591e 100644 --- a/build.sbt +++ b/build.sbt @@ -72,7 +72,7 @@ lazy val root = (project in file(".")) publish / skip := true, publishArtifact := false, ) - .aggregate(`cache-adt`, scache) + .aggregate(`cache-adt`, scache, benchmark) lazy val `cache-adt` = (project in file("cache-adt")) .settings(commonSettings) @@ -86,6 +86,7 @@ lazy val scache = (project in file("scache")) .settings( name := "scache", description := "Cache in Scala with cats-effect", + versionPolicyIntention := Compatibility.None, libraryDependencies ++= Seq( Cats.core, Cats.effect, @@ -96,6 +97,20 @@ lazy val scache = (project in file("scache")) ) .dependsOn(`cache-adt`) +lazy val benchmark = (project in file("benchmark")) + .enablePlugins(JmhPlugin) + .settings(commonSettings) + .settings( + name := "scache-benchmark", + description := "JMH benchmarks for scache", + publish / skip := true, + publishArtifact := false, + versionPolicyCheck / skip := true, + versionPolicyReportDependencyIssues / skip := true, + coverageEnabled := false, + ) + .dependsOn(scache) + addCommandAlias("fmt", "+scalafmtRepo") addCommandAlias("check", "+all versionPolicyCheck Compile/doc scalafmtCheckRepo") addCommandAlias("build", "all test package") diff --git a/project/plugins.sbt b/project/plugins.sbt index c4bd3e1..2d3383c 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -9,3 +9,5 @@ addSbtPlugin("com.evolution" % "sbt-scalac-opts-plugin" % "0.2.0") addSbtPlugin("com.evolution" % "sbt-artifactory-plugin" % "0.1.2") addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.6.2") + +addSbtPlugin("pl.project13.scala" % "sbt-jmh" % "0.4.8") diff --git a/scache/src/main/scala/com/evolution/scache/Cache.scala b/scache/src/main/scala/com/evolution/scache/Cache.scala index 14e5726..cf4a488 100644 --- a/scache/src/main/scala/com/evolution/scache/Cache.scala +++ b/scache/src/main/scala/com/evolution/scache/Cache.scala @@ -2,7 +2,7 @@ package com.evolution.scache import cats.effect.kernel.MonadCancel import cats.effect.syntax.all.* -import cats.effect.{Concurrent, Resource, Temporal} +import cats.effect.{Async, Concurrent, Resource, Temporal} import cats.kernel.CommutativeMonoid import cats.syntax.all.* import cats.{Applicative, Functor, Hash, Monad, MonadThrow, Monoid, Parallel, ~>} @@ -441,7 +441,7 @@ object Cache { * method will be called on underlying cache when resource is released to make sure all * resources stored in a cache are also released. */ - def loading[F[_]: Concurrent: Parallel: Runtime, K, V]: Resource[F, Cache[F, K, V]] = { + def loading[F[_]: Async: Parallel: Runtime, K, V]: Resource[F, Cache[F, K, V]] = { loading(none) } @@ -460,7 +460,7 @@ object Cache { * method will be called on underlying cache when resource is released to make sure all * resources stored in a cache are also released. */ - def loading[F[_]: Concurrent: Parallel: Runtime, K, V](partitions: Int): Resource[F, Cache[F, K, V]] = { + def loading[F[_]: Async: Parallel: Runtime, K, V](partitions: Int): Resource[F, Cache[F, K, V]] = { loading(partitions.some) } @@ -508,7 +508,7 @@ object Cache { * method will be called on underlying cache when resource is released to make sure all * resources stored in a cache are also released. */ - def loading[F[_]: Concurrent: Parallel: Runtime, K, V](partitions: Option[Int] = None) + def loading[F[_]: Async: Parallel: Runtime, K, V](partitions: Option[Int] = None) : Resource[F, Cache[F, K, V]] = { implicit val hash: Hash[K] = Hash.fromUniversalHashCode[K] @@ -518,7 +518,7 @@ object Cache { .map { _.pure[F] } .getOrElse { NrOfPartitions[F]() } .toResource - cache = LoadingCache.of(LoadingCache.EntryRefs.empty[F, K, V]) + cache = LoadingCache.of[F, K, V] partitions <- Partitions.of[Resource[F, _], K, Cache[F, K, V]](nrOfPartitions, _ => cache) } yield { fromPartitions(partitions) @@ -570,7 +570,7 @@ object Cache { * method will be called on underlying cache when resource is released to make sure all * resources stored in a cache are also released. */ - def expiring[F[_]: Temporal: Runtime: Parallel, K, V]( + def expiring[F[_]: Async: Runtime: Parallel, K, V]( config: ExpiringCache.Config[F, K, V], partitions: Option[Int] = None, ): Resource[F, Cache[F, K, V]] = { diff --git a/scache/src/main/scala/com/evolution/scache/CancelledError.scala b/scache/src/main/scala/com/evolution/scache/CancelledError.scala index 14f9ac2..10ffc08 100644 --- a/scache/src/main/scala/com/evolution/scache/CancelledError.scala +++ b/scache/src/main/scala/com/evolution/scache/CancelledError.scala @@ -2,4 +2,8 @@ package com.evolution.scache import scala.util.control.NoStackTrace +/** + * Failure of a value computation that got cancelled, reported to the callers that were waiting for + * its result rather than running it themselves. + */ case object CancelledError extends RuntimeException with NoStackTrace diff --git a/scache/src/main/scala/com/evolution/scache/ExpiredError.scala b/scache/src/main/scala/com/evolution/scache/ExpiredError.scala new file mode 100644 index 0000000..11e4d19 --- /dev/null +++ b/scache/src/main/scala/com/evolution/scache/ExpiredError.scala @@ -0,0 +1,9 @@ +package com.evolution.scache + +import scala.util.control.NoStackTrace + +/** + * Failure of a value computation that was taking so long the entry got expired while still loading, + * see [[ExpiringCache]]. + */ +case object ExpiredError extends RuntimeException with NoStackTrace diff --git a/scache/src/main/scala/com/evolution/scache/ExpiringCache.scala b/scache/src/main/scala/com/evolution/scache/ExpiringCache.scala index 03c8eaf..2a841dc 100644 --- a/scache/src/main/scala/com/evolution/scache/ExpiringCache.scala +++ b/scache/src/main/scala/com/evolution/scache/ExpiringCache.scala @@ -1,10 +1,10 @@ package com.evolution.scache import cats.effect.syntax.all.* -import cats.effect.{Clock, Ref, Resource, Temporal} +import cats.effect.{Async, Clock, Ref, Resource} import cats.kernel.CommutativeMonoid import cats.syntax.all.* -import cats.{Applicative, Monad, MonadThrow, Monoid} +import cats.{Applicative, MonadThrow, Monoid} import com.evolution.scache.Cache.Directive import com.evolution.scache.LoadingCache.EntryState import com.evolutiongaming.catshelper.ClockHelper.* @@ -16,36 +16,74 @@ object ExpiringCache { type Timestamp = Long + /** + * Shortest delay the cleanup routine is ever scheduled with, in milliseconds. + */ + private val MinExpireInterval = 10L + private[scache] def of[F[_], K, V]( config: Config[F, K, V], )(implicit - G: Temporal[F], + G: Async[F], ): Resource[F, Cache[F, K, V]] = { - type E = Entry[V] + type TimestampedValue = Entry[V] + + type LoadingDeferred = LoadingCache.DeferredThrow[F, LoadingCache.Entry[F, TimestampedValue]] val cooldown = math.max(config.expireAfterRead.toMillis / 5, 10L) val expireAfterReadMs = config.expireAfterRead.toMillis + cooldown / 2 val expireAfterWriteMs = config.expireAfterWrite.map { _.toMillis } + val expireAfterMs = expireAfterWriteMs.fold(expireAfterReadMs) { _ min expireAfterReadMs } + val loadingTimeoutMs = config + .loadingTimeout + .fold(expireAfterMs) { _.toMillis } + /* One cleanup run walks every entry, so the interval is what the cost of the routine is traded + * against. Values are sampled ten times per expiration, as before, while loads are sampled only + * twice per `loadingTimeout`, because a load overstaying its welcome by half the timeout is + * harmless and a short `loadingTimeout` next to a long expiration would otherwise turn the + * routine into a busy scan of the whole cache. The floor keeps a tiny configured duration from + * scheduling the routine with no delay at all. + */ val expireInterval = { - val expireInterval = expireAfterWriteMs.fold(expireAfterReadMs) { _ min expireAfterReadMs } - (expireInterval / 10).millis + val interval = (expireAfterMs / 10) min (loadingTimeoutMs / 2) + (interval max MinExpireInterval).millis } - def removeExpiredAndCheckSize(ref: Ref[F, LoadingCache.EntryRefs[F, K, E]], cache: Cache[F, K, E]) = { - - def remove(key: K) = { + /* One run of the expiration routine: drops the values that are too old, evicts the loads that + * are taking too long, and enforces `maxSize`. + * + * Loads are expired as well, because a load that never completes would otherwise stay in the + * map forever, holding the key hostage: nothing can be stored under it, everyone asking for it + * waits on a `Deferred` that will never complete, and so does the release of the cache itself. + * + * The three pieces of state are one and the same map seen from three angles, and are not kept + * in sync by hand: `entryMap` is the raw per-key state, needed here because the [[Cache]] + * interface exposes neither the entry states nor the `Deferred` of a load; `cache` is the very + * same map behind that interface, used for the removals, so that they go through the regular + * release logic; `loadingSince` is bookkeeping private to this routine, holding the moment each + * of the currently loading keys was first seen loading, carried over between the runs, as this + * is the only way to tell how long a load is running. Anything stale in `loadingSince` is + * ignored and dropped on the next run. + */ + def removeExpiredAndCheckSize( + entryMap: LoadingCache.EntryMap[F, K, TimestampedValue], + cache: Cache[F, K, TimestampedValue], + loadingSince: Ref[F, Map[K, (LoadingDeferred, Timestamp)]], + ): F[Unit] = { + + def remove(key: K): F[Unit] = { cache .remove(key) .flatten .void } - def removeExpired(key: K, entryRef: LoadingCache.EntryRef[F, Entry[V]]) = { + def removeExpired(key: K, entryRef: LoadingCache.EntryRef[F, TimestampedValue]): F[Unit] = { entryRef .get .flatMap { - case state: EntryState.Value[F, Entry[V]] => + case state: EntryState.Value[F, TimestampedValue] => for { now <- Clock[F].millis expiredAfterRead = expireAfterReadMs + state.entry.value.touched < now @@ -53,26 +91,93 @@ object ExpiringCache { expired = expiredAfterRead || expiredAfterWrite() result <- if (expired) remove(key) else ().pure[F] } yield result - case _: EntryState.Loading[F, Entry[V]] => ().pure[F] + case _: EntryState.Loading[F, TimestampedValue] => ().pure[F] case EntryState.Removed => ().pure[F] } } - def notExceedMaxSize(maxSize: Int) = { + /* Drops an entry that is still loading, failing everyone waiting for it with `ExpiredError`. + * + * Does nothing unless the entry is still loading the very same `deferred`, so that a load + * that has completed, or has been replaced by a newer one, in the meantime is left alone. + */ + def evictLoading( + key: K, + entryRef: LoadingCache.EntryRef[F, TimestampedValue], + deferred: LoadingDeferred, + ): F[Unit] = { + entryRef + .modify { + case state: EntryState.Loading[F, TimestampedValue] if state.deferred == deferred => + (EntryState.Removed, true) + case state => + (state, false) + } + .flatMap { + case true => + entryMap + .ref(key) + .update { + case Some(`entryRef`) => none + case other => other + } + .productR { deferred.complete(ExpiredError.asLeft).void } + case false => + ().pure[F] + } + .uncancelable + } - def drop(entryRefs: LoadingCache.EntryRefs[F, K, E]) = { + /* Evicts the loads that have been running longer than `Config.loadingTimeout`. + * + * A load has no timestamp of its own, so its age is counted from the first run of the routine + * that has seen it, which may be up to one run interval later than the load actually started. + * The bookkeeping is keyed by the `Deferred` of the load rather than by the key alone, so + * that a new load of the same key starts its own countdown instead of inheriting the one of + * its predecessor. + */ + def removeExpiredLoading( + loading: List[(K, LoadingCache.EntryRef[F, TimestampedValue], LoadingDeferred)], + ): F[Unit] = { + val threshold = loadingTimeoutMs + for { + now <- Clock[F].millis + expired <- loadingSince.modify { seen => + val seen1 = loading + .map { case (key, _, deferred) => + val since = seen + .get(key) + .collect { case (`deferred`, since) => since } + .getOrElse(now) + (key, (deferred, since)) + } + .toMap + val expired = loading.filter { case (key, _, deferred) => + seen1.get(key).exists { case (deferred1, since) => + (deferred1 == deferred) && (since + threshold < now) + } + } + (seen1 -- expired.map { case (key, _, _) => key }, expired) + } + result <- expired.foldMapM { case (key, entryRef, deferred) => evictLoading(key, entryRef, deferred) } + } yield result + } + + def notExceedMaxSize(maxSize: Int): F[Unit] = { + + def drop(entries: List[(K, LoadingCache.EntryRef[F, TimestampedValue])]): F[Unit] = { final case class Elem(key: K, timestamp: Timestamp) val zero = List.empty[Elem] - entryRefs + entries .foldLeft(zero.pure[F]) { case (result, (key, entryRef)) => result.flatMap { result => entryRef .get .map { - case state: EntryState.Value[F, Entry[V]] => Elem(key, state.entry.value.touched) :: result - case _: EntryState.Loading[F, Entry[V]] => result + case state: EntryState.Value[F, TimestampedValue] => Elem(key, state.entry.value.touched) :: result + case _: EntryState.Loading[F, TimestampedValue] => result case EntryState.Removed => result } } @@ -86,14 +191,22 @@ object ExpiringCache { } for { - entryRefs <- ref.get - result <- if (entryRefs.size > maxSize) drop(entryRefs) else ().pure[F] + size <- entryMap.size + result <- Async[F].whenA(size > maxSize) { entryMap.entries.flatMap(drop) } } yield result } for { - entryRefs <- ref.get - result <- entryRefs.foldMapM { case (key, entryRef) => removeExpired(key, entryRef) } + entries <- entryMap.entries + result <- entries.foldMapM { case (key, entryRef) => removeExpired(key, entryRef) } + loading <- entries.foldLeftM(List.empty[(K, LoadingCache.EntryRef[F, TimestampedValue], LoadingDeferred)]) { + case (acc, (key, entryRef)) => + entryRef.get.map { + case state: EntryState.Loading[F, TimestampedValue] => (key, entryRef, state.deferred) :: acc + case _ => acc + } + } + _ <- removeExpiredLoading(loading) _ <- config .maxSize .foldMapM { maxSize => notExceedMaxSize(maxSize) } @@ -102,17 +215,17 @@ object ExpiringCache { def refreshEntries( refresh: Refresh[K, F[Option[V]]], - ref: Ref[F, LoadingCache.EntryRefs[F, K, E]], - cache: Cache[F, K, E], - ) = { - ref - .get - .flatMap { entryRefs => - entryRefs.foldMapM { case (key, entryRef) => + entryMap: LoadingCache.EntryMap[F, K, TimestampedValue], + cache: Cache[F, K, TimestampedValue], + ): F[Unit] = { + entryMap + .entries + .flatMap { entries => + entries.foldMapM { case (key, entryRef) => entryRef .get .flatMap { - case _: EntryState.Value[F, Entry[V]] => + case _: EntryState.Value[F, TimestampedValue] => refresh .value(key) .flatMap { @@ -120,39 +233,39 @@ object ExpiringCache { case None => cache.remove(key).void } .handleError { _ => () } - case _: EntryState.Loading[F, Entry[V]] => ().pure[F] + case _: EntryState.Loading[F, TimestampedValue] => ().pure[F] case EntryState.Removed => ().pure[F] } } } } - def schedule(interval: FiniteDuration)(fa: F[Unit]) = Schedule(interval, interval)(fa) + def schedule(interval: FiniteDuration)(fa: F[Unit]): Resource[F, Unit] = Schedule(interval, interval)(fa) - val entryRefs = LoadingCache.EntryRefs.empty[F, K, E] for { - ref <- Ref[F].of(entryRefs).toResource - cache <- LoadingCache.of(ref) - _ <- schedule(expireInterval) { removeExpiredAndCheckSize(ref, cache) } + entryMap <- LoadingCache.EntryMap.of[F, K, TimestampedValue].toResource + loadingSince <- Ref[F].of(Map.empty[K, (LoadingDeferred, Timestamp)]).toResource + cache <- LoadingCache.of(entryMap) + _ <- schedule(expireInterval) { removeExpiredAndCheckSize(entryMap, cache, loadingSince) } _ <- config .refresh .foldMapM { refresh => - schedule(refresh.interval) { refreshEntries(refresh, ref, cache) } + schedule(refresh.interval) { refreshEntries(refresh, entryMap, cache) } } } yield { - apply(ref, cache, cooldown) + apply(entryMap, cache, cooldown) } } def apply[F[_]: MonadThrow: Clock, K, V]( - ref: Ref[F, LoadingCache.EntryRefs[F, K, Entry[V]]], + entryMap: LoadingCache.EntryMap[F, K, Entry[V]], cache: Cache[F, K, Entry[V]], cooldown: Long, ): Cache[F, K, V] = { - type E = Entry[V] + type TimestampedValue = Entry[V] - def entryOf(value: V) = { + def entryOf(value: V): F[TimestampedValue] = { Clock[F] .millis .map { timestamp => @@ -162,17 +275,13 @@ object ExpiringCache { implicit def monoidUnit: Monoid[F[Unit]] = Applicative.monoid[F, Unit] - def touch(key: K, entry: E) = { + def touch(key: K, entry: TimestampedValue): F[Unit] = { for { now <- Clock[F].millis result <- if ((entry.touched + cooldown) <= now) { - ref - .get - .flatMap { entries => - entries - .get(key) - .foldMap { _.update1 { _.touch(now) } } - } + entryMap + .lookup(key) + .flatMap { _.foldMap { _.update1 { _.touch(now) } } } } else { ().pure[F] } @@ -182,7 +291,7 @@ object ExpiringCache { abstract class ExpiringCache extends Cache.Abstract1[F, K, V] new ExpiringCache { self => - def get(key: K) = { + def get(key: K): F[Option[V]] = { cache .get1(key) .flatMap { @@ -201,7 +310,7 @@ object ExpiringCache { } } - def get1(key: K) = { + def get1(key: K): F[Option[Either[F[V], V]]] = { cache .get1(key) .flatMap { @@ -223,7 +332,7 @@ object ExpiringCache { } } - def getOrUpdate(key: K)(value: => F[V]) = { + def getOrUpdate(key: K)(value: => F[V]): F[V] = { getOrUpdate1(key) { value.map { a => (a, a, none[Release]) } } .flatMap { case Right(Right(a)) => a.pure[F] @@ -232,7 +341,7 @@ object ExpiringCache { } } - def getOrUpdate1[A](key: K)(value: => F[(A, V, Option[Release])]) = { + def getOrUpdate1[A](key: K)(value: => F[(A, V, Option[Release])]): F[Either[A, Either[F[V], V]]] = { cache .getOrUpdate1(key) { value.flatMap { case (a, value, release) => @@ -261,7 +370,7 @@ object ExpiringCache { } } - def put(key: K, value: V, release: Option[Release]) = { + def put(key: K, value: V, release: Option[Release]): F[F[Option[V]]] = { entryOf(value) .flatMap { entry => cache @@ -285,13 +394,13 @@ object ExpiringCache { cache.modify(key)(adaptedF) } - def contains(key: K) = cache.contains(key) + def contains(key: K): F[Boolean] = cache.contains(key) - def size = cache.size + def size: F[Int] = cache.size - def keys = cache.keys + def keys: F[Set[K]] = cache.keys - def values = { + def values: F[Map[K, F[V]]] = { cache .values .map { values => @@ -301,7 +410,7 @@ object ExpiringCache { } } - def values1 = { + def values1: F[Map[K, Either[F[V], V]]] = { cache .values1 .map { entries => @@ -315,22 +424,22 @@ object ExpiringCache { } } - def remove(key: K) = { + def remove(key: K): F[F[Option[V]]] = { cache .remove(key) .map { _.map { _.map { _.value } } } } - def clear = cache.clear + def clear: F[F[Unit]] = cache.clear - def foldMap[A: CommutativeMonoid](f: (K, Either[F[V], V]) => F[A]) = { + def foldMap[A: CommutativeMonoid](f: (K, Either[F[V], V]) => F[A]): F[A] = { cache.foldMap { case (k, Right(v)) => f(k, v.value.asRight) case (k, Left(v)) => f(k, v.map { _.value }.asLeft) } } - def foldMapPar[A: CommutativeMonoid](f: (K, Either[F[V], V]) => F[A]) = { + def foldMapPar[A: CommutativeMonoid](f: (K, Either[F[V], V]) => F[A]): F[A] = { cache.foldMap { case (k, Right(v)) => f(k, v.value.asRight) case (k, Left(v)) => f(k, v.map { _.value }.asLeft) @@ -421,24 +530,20 @@ object ExpiringCache { * If set to [[scala.Some]], the cache will schedule a background job, which will refresh or * remove the _existing_ values regularly. The keys not already present in a cache will not be * affected anyhow. See [[Refresh]] documentation for more details. + * @param loadingTimeout + * How long a value computation started by [[Cache#getOrUpdate]] is allowed to run before the + * entry is evicted and everyone waiting for it fails with [[ExpiredError]]. Without it a + * computation that never completes would hold the key forever. If set to [[scala.None]], the + * smaller of `expireAfterRead` and `expireAfterWrite` is used. Note, that the load is not + * cancelled, only detached from the cache, and that this, too, is best effort: the eviction + * only happens on a cleanup run, so a load may outlive the timeout by up to one run interval. */ final case class Config[F[_], -K, V]( expireAfterRead: FiniteDuration, expireAfterWrite: Option[FiniteDuration] = None, maxSize: Option[Int] = None, refresh: Option[Refresh[K, F[Option[V]]]] = None, + loadingTimeout: Option[FiniteDuration] = None, ) - private implicit class MapOps[K, V](val self: Map[K, V]) extends AnyVal { - def foldMapM[F[_]: Monad, A: Monoid](f: (K, V) => F[A]): F[A] = { - self.foldLeft(Monoid[A].empty.pure[F]) { case (a, (k, v)) => - for { - a <- a - b <- f(k, v) - } yield { - a.combine(b) - } - } - } - } } diff --git a/scache/src/main/scala/com/evolution/scache/LoadingCache.scala b/scache/src/main/scala/com/evolution/scache/LoadingCache.scala index c011fd2..8d86495 100644 --- a/scache/src/main/scala/com/evolution/scache/LoadingCache.scala +++ b/scache/src/main/scala/com/evolution/scache/LoadingCache.scala @@ -1,63 +1,212 @@ package com.evolution.scache +import cats.effect.* import cats.effect.implicits.* -import cats.effect.{Concurrent, Deferred, Fiber, GenConcurrent, Outcome, Ref, Resource} +import cats.effect.std.MapRef import cats.kernel.CommutativeMonoid import cats.syntax.all.* import cats.{Applicative, Functor, Monad, MonadThrow, Parallel} import com.evolution.scache.Cache.Directive import com.evolutiongaming.catshelper.ParallelHelper.* +import java.util.concurrent.ConcurrentHashMap +import scala.jdk.CollectionConverters.* + +/** + * Cache able to load values, i.e. to deduplicate concurrent computations of the same key. + * + * =State= + * + * The state is kept on two levels: + * - the outer level, [[LoadingCache.EntryMap]], answers "is there an entry for this key?" and is + * backed by a [[java.util.concurrent.ConcurrentHashMap]] exposed as a per-key + * [[cats.effect.std.MapRef]]; + * - the inner level, [[LoadingCache.EntryRef]], answers "what happened to the value of this + * entry?" and is a `Ref` holding an [[LoadingCache.EntryState]]. + * + * A key is in the cache if, and only if, the outer level holds an `EntryRef` for it and that + * `EntryRef` is not in [[LoadingCache.EntryState.Removed]] state. + * + * =Why a ConcurrentHashMap= + * + * The outer level used to be a single `Ref[F, Map[K, EntryRef]]`, so every insertion or removal of + * any key had to CAS one and the same `Ref`. That had two consequences: + * - operations on unrelated keys invalidated each other, so a `getOrUpdate` of one key could be + * starved by a steady stream of writes of other keys, and the retry limit guarding that loop + * turned such contention into a failure; + * - every write copied the entire map. + * + * With `MapRef.fromConcurrentHashMap` a CAS is scoped to a single key: operations on distinct keys + * never contend, and no retry limit is needed, because the loops below only spin on a real race + * over the same key, and every such race is won by a fiber that makes progress. Enumeration + * (`keys`, `entries`, `size`) is served by the `ConcurrentHashMap` itself, i.e. it is a weakly + * consistent view rather than an atomic snapshot. + * + * =Entry lifecycle= + * + * `getOrUpdate` installs an entry in `Loading` state, holding a `Deferred` that every other fiber + * asking for the same key awaits, and only then computes the value. The load then either + * - stores the computed value, moving the entry to `Value` state, or + * - drops the entry from the map and propagates the error to the caller and to the waiters, if + * the computation failed, or + * - discards and releases its own result, if `put` or `modify` stored another value under the key + * meanwhile, in which case the value of the winner is returned to the caller, or + * - discards and releases its own result, if the load was cancelled. + * + * Neither `remove` nor `clear` cancels a load in flight, so a load that outlives one of them still + * has a value on its hands. After a `remove` it stores that value under the key again, putting the + * key back into the cache; after a `clear` it stores it into the entry the `clear` has already + * unlinked, and the `clear` is the one that awaits and releases it. + * + * `Removed` is a tombstone meaning "this `EntryRef` is no longer in the map, look the key up + * again". It is needed because the two levels cannot be updated atomically together, so a fiber + * that looked an `EntryRef` up earlier needs a way to notice that its reference went stale. All + * retry loops here are driven by it: seeing `Removed` means re-reading the key, and the fiber that + * installed the tombstone is already committed to unlinking that key, hence the loops terminate. + * + * =Releasing values= + * + * A value is released exactly once, by the fiber that took it out of the entry, i.e. replaced or + * removed it. A fiber whose computed value did not make it into the map releases it itself. To + * avoid making unrelated callers wait for a foreign `release`, releases of values the caller did + * not ask about are started in the background. + * + * =Cancellation= + * + * Only the user-supplied computation is cancelable, all state transitions are masked. Cancelling a + * load flips its own `Loading` state to `Removed`, unlinks the key, always completes the `Deferred` + * with [[CancelledError]] so that waiters fail instead of hanging, even if the entry had already + * been taken away by `remove`, and releases the value if the computation did manage to produce one. + * Without that cleanup a cancelled load would leave behind a `Loading` entry with a `Deferred` + * nobody is going to complete, which makes the key unusable forever and blocks the waiters, + * `clear`, and therefore the release of the cache itself. + */ private[scache] object LoadingCache { - /** - * Maximum number of CAS retry attempts before giving up. This is a safety net against infinite - * spinning under extreme contention. - */ - /** - * Maximum number of CAS retry attempts on the outer map before giving up. Inner entry-level CAS - * loops are unbounded as they always make progress. - */ - private val MaxRetries: Int = 10000 - - def of[F[_]: Concurrent, K, V]( - map: EntryRefs[F, K, V], - ): Resource[F, Cache[F, K, V]] = { + def of[F[_]: Async, K, V]: Resource[F, Cache[F, K, V]] = { for { - ref <- Ref[F].of(map).toResource - cache <- of(ref) + entryMap <- EntryMap.of[F, K, V].toResource + cache <- of(entryMap) } yield cache } - def of[F[_]: Concurrent, K, V]( - ref: Ref[F, EntryRefs[F, K, V]], + /** + * Cache over an existing [[EntryMap]], clearing it, and thus releasing all the values, when the + * resource is released. + */ + def of[F[_]: Async, K, V]( + entryMap: EntryMap[F, K, V], ): Resource[F, Cache[F, K, V]] = { Resource.make { - apply(ref).pure[F] + apply(entryMap).pure[F] } { cache => cache.clear.flatten } } - def apply[F[_]: Concurrent, K, V]( - ref: Ref[F, EntryRefs[F, K, V]], - ): Cache[F, K, V] = { + /** + * Per-key view over the cache state: mutations go through [[cats.effect.std.MapRef]], so + * operations on distinct keys never contend, while enumeration is served by the backing + * [[java.util.concurrent.ConcurrentHashMap]]. + * + * Exposed as a trait rather than used directly, because [[ExpiringCache]] needs to walk and evict + * entries behind the back of the [[Cache]] interface, and because it makes the contention + * behaviour testable. + */ + trait EntryMap[F[_], K, V] { + + /** + * Atomic per-key handle on the map, where `None` stands for "no entry for this key": setting it + * to `None` removes the key, setting it to `Some` inserts or replaces the entry. This is the + * only way the mapping itself is modified, and the CAS it performs is scoped to `key`. + */ + def ref(key: K): Ref[F, Option[EntryRef[F, V]]] + + /** + * Non-atomic read of the entry, used when the mapping is not going to be modified. + */ + def lookup(key: K): F[Option[EntryRef[F, V]]] + + /** + * Weakly consistent view of the keys, i.e. concurrent modifications may or may not be seen. + */ + def keys: F[Set[K]] + + /** + * Weakly consistent view of the entries, see [[keys]]. + */ + def entries: F[List[(K, EntryRef[F, V])]] + + /** + * Number of entries, including the ones being loaded or removed, hence an upper bound of the + * number of values available. + */ + def size: F[Int] + + def contains(key: K): F[Boolean] + } - val handleReleaseError = (e: Throwable) => { - System.err.println(s"scache: failed to release cache entry: $e") + object EntryMap { + + def of[F[_]: Sync, K, V]: F[EntryMap[F, K, V]] = { + Sync[F] + .delay { new ConcurrentHashMap[K, EntryRef[F, V]]() } + .map { chm => apply(chm) } } - def checkRetries(counter: Int): F[Unit] = { - if (counter >= MaxRetries) { - new IllegalStateException( - s"Cache CAS retry limit ($MaxRetries) exceeded. This indicates extreme contention.", - ).raiseError[F, Unit] - } else { - ().pure[F] + /** + * Built over an explicitly passed [[java.util.concurrent.ConcurrentHashMap]] rather than via + * `MapRef.ofConcurrentHashMap`, because the latter only hands out the per-key `Ref`s, while + * [[EntryMap.keys]], [[EntryMap.entries]], [[EntryMap.size]] and [[EntryMap.contains]] need the + * map itself. + */ + def apply[F[_]: Sync, K, V](chm: ConcurrentHashMap[K, EntryRef[F, V]]): EntryMap[F, K, V] = { + val mapRef = MapRef.fromConcurrentHashMap[F, K, EntryRef[F, V]](chm) + new EntryMap[F, K, V] { + + def ref(key: K): Ref[F, Option[EntryRef[F, V]]] = + mapRef(key) + + def lookup(key: K): F[Option[EntryRef[F, V]]] = + Sync[F].delay { Option(chm.get(key)) } + + def keys: F[Set[K]] = + Sync[F].delay { chm.keySet().asScala.toSet } + + def entries: F[List[(K, EntryRef[F, V])]] = + Sync[F].delay { + chm + .entrySet() + .iterator() + .asScala + .map { entry => (entry.getKey, entry.getValue) } + .toList + } + + def size: F[Int] = + Sync[F].delay { chm.mappingCount().toInt } + + def contains(key: K): F[Boolean] = + Sync[F].delay { chm.containsKey(key) } } } + } - def entryOf(value: V, release: Option[F[Unit]]) = { + /** + * Cache over an existing [[EntryMap]], which never releases the values it still holds, hence + * meant to be wrapped into a resource by [[of]] rather than used directly. + */ + def apply[F[_]: Async, K, V]( + entryMap: EntryMap[F, K, V], + ): Cache[F, K, V] = { + + val F = Async[F] + + val handleReleaseError = (e: Throwable) => { + System.err.println(s"scache: failed to release cache entry: $e") + } + + def entryOf(value: V, release: Option[F[Unit]]): Entry[F, V] = { Entry( value = value, release = release.map { _.handleError(handleReleaseError) }, @@ -68,51 +217,45 @@ private[scache] object LoadingCache { new LoadingCache { - def get(key: K) = { - ref - .get - .flatMap { entryRefs => - entryRefs - .get(key) - .fold { - none[V].pure[F] - } { entry => - entry - .get - .flatMap { - case state: EntryState.Value[F, V] => - state - .entry - .value - .some - .pure[F] - case state: EntryState.Loading[F, V] => - state - .deferred - .get - .map { entry => - entry - .toOption - .map { _.value } - } - case EntryState.Removed => - none[V].pure[F] - } - } + def get(key: K): F[Option[V]] = { + entryMap + .lookup(key) + .flatMap { + _.fold { + none[V].pure[F] + } { entryRef => + entryRef + .get + .flatMap { + case state: EntryState.Value[F, V] => + state + .entry + .value + .some + .pure[F] + case state: EntryState.Loading[F, V] => + state + .deferred + .get + .map { entry => + entry + .toOption + .map { _.value } + } + case EntryState.Removed => + none[V].pure[F] + } + } } } - def get1(key: K) = { - ref - .get - .flatMap { entryRefs => - entryRefs - .get(key) - .flatTraverse { _.optEither } - } + def get1(key: K): F[Option[Either[F[V], V]]] = { + entryMap + .lookup(key) + .flatMap { _.flatTraverse { _.optEither } } } - def getOrUpdate(key: K)(value: => F[V]) = { + def getOrUpdate(key: K)(value: => F[V]): F[V] = { getOrUpdate1(key) { value.map { a => (a, a, none[Release]) } }.flatMap { case Right(Right(a)) => a.pure[F] case Right(Left(a)) => a @@ -120,581 +263,645 @@ private[scache] object LoadingCache { } } + /** + * Returns the value of the key, computing it if the key is not in the cache yet. + * + * The result is `Left` if this call did compute the value, and `Right` if the value came from + * the cache, either already computed (`Right`) or still being computed by another fiber + * (`Left`), so that the callers can tell a cache hit from a miss without waiting. + * + * The flow is: look the key up and return what is there, or, if there is nothing, install a + * `Loading` entry and compute the value. Installing the entry is masked and done with a + * single per-key CAS, so of the fibers racing to install one exactly one wins and the losers + * simply await its `Deferred`. A `Removed` entry is a stale reference, and means the lookup + * has to be repeated. + */ def getOrUpdate1[A](key: K)(value: => F[(A, V, Option[Release])]): F[Either[A, Either[F[V], V]]] = { - 0.tailRecM { counter => - checkRetries(counter) *> - ref - .access - .flatMap { case (entryRefs, set) => - entryRefs - .get(key) - .fold { - for { - deferred <- Deferred[F, Either[Throwable, Entry[F, V]]] - entryRef <- Ref[F].of[EntryState[F, V]](EntryState.Loading(deferred)) - result <- set(entryRefs.updated(key, entryRef)) - .flatMap { - case true => - value - .map { case (a, value, release) => - val entry = entryOf(value, release) - (a, entry) - } - .attempt - .race1 { deferred.get } - .flatMap { - // `value` got computed, and deferred was not (yet) completed by any other fiber in `put` - case Left(Right((a, entry))) => - deferred - .complete(entry.asRight) - .flatMap { - // Successfully completed our deferred, - // now trying to place the new value in the entry. - case true => - - def releaseAndReturnValue(state: EntryState.Value[F, V]) - : F[Either[A, Either[F[V], V]]] = - entry - .release1 - .start - .as { - state - .entry - .value - .asRight[F[V]] - .asRight[A] - } - - def releaseAndReturnLoading(state: EntryState.Loading[F, V]) - : F[Either[A, Either[F[V], V]]] = - entry - .release1 - .start - .as { - state - .deferred - .getOrError - .map(_.value) - .asLeft[V] - .asRight[A] - } - - // Try putting computed value in the map, if there is no entry with our key. - // If the map already contains an entry with our key, - // return its value (or value computation). - def tryPutNewValue: F[Either[A, Either[F[V], V]]] = - 0.tailRecM { counter => - ref - .access - .flatMap { case (entryRefs, set) => - entryRefs - .get(key) - .fold { - // No entry present in the map, so we try to add a new one - Ref[F] - .of[EntryState[F, V]](EntryState.Value(entry)) - .flatMap { entryRef => - set(entryRefs.updated(key, entryRef)).map { - case true => - a - .asLeft[Either[F[V], V]] - .asRight[Int] - case false => - (counter + 1) - .asLeft[Either[A, Either[F[V], V]]] - } - } - } { entryRef => - entryRef - .get - .flatMap { - case state: EntryState.Value[F, V] => - releaseAndReturnValue(state).map(_.asRight[Int]) - - case state: EntryState.Loading[F, V] => - releaseAndReturnLoading(state).map(_.asRight[Int]) - - // `Removed` means that this entry won't be present in the map - // next time we look the key up (see `remove` flow), - // so we just retry. - case EntryState.Removed => - (counter + 1) - .asLeft[Either[A, Either[F[V], V]]] - .pure[F] - } - .uncancelable - } - } - } - - entryRef - .access - .flatMap { - // Entry is still in loading state, containing the same deferred we just completed. - // Now we can try to put the computed value in the same entryRef. - case (state: EntryState.Loading[F, V], set) if state.deferred == deferred => - set(EntryState.Value(entry)) - .flatMap { - // Happy path: successfully placed our computed value - case true => - a - .asLeft[Either[F[V], V]] - .pure[F] - // Failed to set our value, meaning the entry was either: - // - Updated: in that case we release our computed value, and return - // the value (or its computation), giving it the priority - // - Removed: in that case we try to put our value back in the map - case false => - entryRef - .get - .flatMap { - case state: EntryState.Value[F, V] => - releaseAndReturnValue(state) - - case state: EntryState.Loading[F, V] => - releaseAndReturnLoading(state) - - case EntryState.Removed => - tryPutNewValue - } - } - - case (state: EntryState.Value[F, V], _) => - releaseAndReturnValue(state) - - case (state: EntryState.Loading[F, V], _) => - releaseAndReturnLoading(state) - - case (EntryState.Removed, _) => - tryPutNewValue - } - - // Deferred got completed by another fiber, so we return what they put there, - // and release the value we just computed. - case false => - entry - .release1 - .start - .productR( - deferred - .getOrError - .map { entry => - entry - .value - .asRight[F[V]] - .asRight[A] - }, - ) - } - // `value` computation completed with error, - // and deferred was not completed in another fiber in `put`. - case Left(Left(error)) => - deferred - .complete(error.asLeft) - .flatMap { - // Successfully completed our deferred with error, - // now trying to remove the entry from the map, if it is still there. - case true => - 0.tailRecM { counter1 => - ref - .access - .flatMap { case (entryRefs, set) => - entryRefs - .get(key) - .fold { - // Key was removed while we were loading, - // so we are just propagating the error - error.raiseError[F, Either[Int, Either[F[V], V]]] - } { - // The entry we added to the map is still there and unmodified, - // so we can safely remove it and propagate the error - case `entryRef` => - set(entryRefs - key).flatMap { - // Happy path: successfully removed our entry - case true => - error.raiseError[F, Either[Int, Either[F[V], V]]] - // Retrying (different keys could've been modified in the map) - case false => - (counter1 + 1) - .asLeft[Either[F[V], V]] - .pure[F] - } - // Another fiber replaced the `ref` we added to the map, - // so we return their value (computed or ongoing), - // or propagate our error if our entry got removed. - case entryRef => - entryRef - .optEither - .flatMap(_.liftTo[F](error)) - .map(_.asRight[Int]) - } - } - } + /* Runs the value computation for the `Loading` entry this fiber installed, and publishes + * its result. + * + * The computation is the only cancelable part of `getOrUpdate1`, hence it runs under + * `poll`, and `cleanupOnCancel` has to undo the installed entry: unlink the key, complete + * the `deferred` with `CancelledError` to unblock the waiters, and release the value if + * the computation completed before the cancellation was observed. + * + * The computation is raced against `deferred` to also handle being overtaken by a `put` of + * the same key: whoever completes the `deferred` first defines the value of the entry, and + * the loser releases the value it produced. + */ + def load( + poll: Poll[F], + entryRef: EntryRef[F, V], + deferred: DeferredThrow[F, Entry[F, V]], + ): F[Either[A, Either[F[V], V]]] = { + Ref[F].of(none[Entry[F, V]]).flatMap { computed => + val cleanupOnCancel = + entryRef + .modify { + case state: EntryState.Loading[F, V] if state.deferred == deferred => + (EntryState.Removed, true) + case state => + (state, false) + } + .flatMap { + case true => + entryMap + .ref(key) + .update { + case Some(`entryRef`) => none + case other => other + } + case false => + ().pure[F] + } + // Completed regardless of whether the entry was still ours: the waiters hold this + // very `deferred`, and if the entry was taken away without completing it, as + // `remove` does, we are the only one left to unblock them. A `deferred` already + // completed by `put` ignores this. + .productR { deferred.complete(CancelledError.asLeft).void } + .productR { + computed + .get + .flatMap { _.foldMapM { _.release1 } } + } - // Someone else completed the deferred before us, so they must've take care of - // updating the `ref`, and we return their result. - case false => - deferred - .getOrError - .map { _.value } - .asLeft[V] - .pure[F] - } - .map { _.asRight[A] } - - // Deferred was completed by `put` in another fiber before `value` computation completed. - // We return their value, and schedule release of our value that is still being computed. - case Right((fiber, entry)) => - fiber - .joinWithNever - .flatMap { - case Right((_, entry)) => entry.release1 - case _ => ().pure[F] - } - .start - .productR { - entry - .liftTo[F] - .map { entry => - entry - .value - .asRight[F[V]] - .asRight[A] + poll { + F.uncancelable { + _ { + value.map { case (a, value, release) => + val entry = entryOf(value, release) + (a, entry) + } + } + .flatTap { case (_, entry) => computed.set(entry.some) } + } + .attempt + .race1 { deferred.get } + } + .onCancel { cleanupOnCancel } + .flatMap { + // `value` got computed, and deferred was not (yet) completed by any other fiber in `put` + case Left(Right((a, entry))) => + deferred + .complete(entry.asRight) + .flatMap { + // Successfully completed our deferred, + // now trying to place the new value in the entry. + case true => + + def releaseAndReturnValue(state: EntryState.Value[F, V]): F[Either[A, Either[F[V], V]]] = + entry + .release1 + .start + .as { + state + .entry + .value + .asRight[F[V]] + .asRight[A] + } + + def releaseAndReturnLoading(state: EntryState.Loading[F, V]): F[Either[A, Either[F[V], V]]] = + entry + .release1 + .start + .as { + state + .deferred + .getOrError + .map(_.value) + .asLeft[V] + .asRight[A] + } + + // Try putting computed value in the map, if there is no entry with our key. + // If the map already contains an entry with our key, + // return its value (or value computation). + def tryPutNewValue: F[Either[A, Either[F[V], V]]] = + Ref[F] + .of[EntryState[F, V]](EntryState.Value(entry)) + .flatMap { newEntryRef => + ().tailRecM { _ => + entryMap + .ref(key) + .modify { + case None => (newEntryRef.some, none[EntryRef[F, V]]) + case some => (some, some) + } + .flatMap { + case None => + a + .asLeft[Either[F[V], V]] + .asRight[Unit] + .pure[F] + case Some(existingRef) => + existingRef + .get + .flatMap { + case state: EntryState.Value[F, V] => + releaseAndReturnValue(state).map(_.asRight[Unit]) + + case state: EntryState.Loading[F, V] => + releaseAndReturnLoading(state).map(_.asRight[Unit]) + + // `Removed` means that this entry won't be present in the map + // next time we look the key up (see `remove` flow), + // so we just retry. + case EntryState.Removed => + () + .asLeft[Either[A, Either[F[V], V]]] + .pure[F] } - } + } } - .map { _.asRight[Int] } + } - case false => - (counter + 1) - .asLeft[Either[A, Either[F[V], V]]] - .pure[F] - } - .uncancelable - } yield result - } { entryRef => - // Map already contained an entry under our key, so we return that value (or its ongoing computation) - entryRef - .optEither - .map { - case Some(either) => - either + entryRef + .access + .flatMap { + // Entry is still in loading state, containing the same deferred we just completed. + // Now we can try to put the computed value in the same entryRef. + case (state: EntryState.Loading[F, V], set) if state.deferred == deferred => + set(EntryState.Value(entry)) + .flatMap { + // Happy path: successfully placed our computed value + case true => + a + .asLeft[Either[F[V], V]] + .pure[F] + + // Failed to set our value: while we were loading, `put`, `modify`, + // `remove` or `clear` got to the same entry, so it was either: + // - Updated: in that case we release our computed value, and return + // the value (or its computation), giving it the priority + // - Removed: in that case we try to put our value back in the map + case false => + entryRef + .get + .flatMap { + case state: EntryState.Value[F, V] => + releaseAndReturnValue(state) + + case state: EntryState.Loading[F, V] => + releaseAndReturnLoading(state) + + case EntryState.Removed => + tryPutNewValue + } + } + + case (state: EntryState.Value[F, V], _) => + releaseAndReturnValue(state) + + case (state: EntryState.Loading[F, V], _) => + releaseAndReturnLoading(state) + + case (EntryState.Removed, _) => + tryPutNewValue + } + + // Deferred got completed by another fiber, so we return what they put there, + // and release the value we just computed. + case false => + entry + .release1 + .start + .productR( + deferred + .getOrError + .map { entry => + entry + .value + .asRight[F[V]] + .asRight[A] + }, + ) + } + + // `value` computation completed with error, + // and deferred was not completed in another fiber in `put`. + case Left(Left(error)) => + deferred + .complete(error.asLeft) + .flatMap { + // Successfully completed our deferred with error, + // now trying to remove the entry from the map, if it is still there. + case true => + entryMap + .ref(key) + .modify { + // The entry we added to the map is still there and unmodified, + // so we can safely remove it and propagate the error + case Some(`entryRef`) => (none[EntryRef[F, V]], none[EntryRef[F, V]]) + case other => (other, other) + } + .flatMap { + // Key was removed (or removed and replaced by us) while we were loading, + // so we are just propagating the error + case None => + error.raiseError[F, Either[F[V], V]] + // Another fiber replaced the entry we added to the map, + // so we return their value (computed or ongoing), + // or propagate our error if their entry got removed. + case Some(otherRef) => + otherRef + .optEither + .flatMap(_.liftTo[F](error)) + } + + // Someone else completed the deferred before us, so they must've take care of + // updating the entry, and we return their result. + case false => + deferred + .getOrError + .map { _.value } + .asLeft[V] + .pure[F] + } + .map { _.asRight[A] } + + // Deferred was completed by `put` in another fiber before `value` computation completed. + // We return their value, and schedule release of our value that is still being computed. + case Right((fiber, entry)) => + fiber + .joinWithNever + .flatMap { + case Right((_, entry)) => entry.release1 + case _ => ().pure[F] + } + .start + .productR { + entry + .liftTo[F] + .map { entry => + entry + .value + .asRight[F[V]] .asRight[A] - .asRight[Int] - // Entry got removed (see `remove` flow), so we retry expecting to get something else with our key. - case None => - (counter + 1) - .asLeft[Either[A, Either[F[V], V]]] - } - } + } + } } + } + } + + ().tailRecM { _ => + entryMap + .lookup(key) + .flatMap { + case Some(entryRef) => + entryRef + .optEither + .map { + case Some(either) => + either + .asRight[A] + .asRight[Unit] + // Entry got removed (see `remove` flow), so we retry expecting to get something else with our key. + case None => + ().asLeft[Either[A, Either[F[V], V]]] + } + case None => + F.uncancelable { poll => + for { + deferred <- Deferred[F, Either[Throwable, Entry[F, V]]] + entryRef <- Ref[F].of[EntryState[F, V]](EntryState.Loading(deferred)) + existing <- entryMap + .ref(key) + .modify { + case None => (entryRef.some, none[EntryRef[F, V]]) + case some => (some, some) + } + result <- existing match { + case Some(existingRef) => + existingRef + .optEither + .map { + case Some(either) => + either + .asRight[A] + .asRight[Unit] + case None => + ().asLeft[Either[A, Either[F[V], V]]] + } + case None => + load(poll, entryRef, deferred).map { _.asRight[Unit] } + } + } yield result + } + } } } + /** + * Stores the value under the key, returning the replaced value, if any. + * + * The outer effect performs the replacement, the inner one awaits the release of the replaced + * value, so that the caller can decide whether to wait for it. + * + * A `Loading` entry is not waited for: its `deferred` is completed with the new value, which + * both unblocks the waiters immediately and tells the loading fiber that it lost the race and + * has to release the value it computes. + */ def put(key: K, value: V, release: Option[Release]): F[F[Option[V]]] = { val entry = entryOf(value, release) - 0.tailRecM { counter => - checkRetries(counter) *> - ref - .access - .flatMap { case (entryRefs, set) => - entryRefs - .get(key) - .fold { - // No entry present in the map, so we add a new one - Ref[F] - .of[EntryState[F, V]](EntryState.Value(entry)) - .flatMap { entryRef => - set(entryRefs.updated(key, entryRef)).map { + + // Our value did not make it into the map, so nothing was replaced and we own its release, + // which we start and forget, as no caller is waiting for it. + def releaseAndExit: F[Either[Unit, F[Option[V]]]] = { + entry + .release + .traverse { _.start } + .as { + none[V] + .pure[F] + .asRight[Unit] + } + } + + ().tailRecM { _ => + entryMap + .lookup(key) + .flatMap { + case None => + // No entry present in the map, so we add a new one + Ref[F] + .of[EntryState[F, V]](EntryState.Value(entry)) + .flatMap { entryRef => + entryMap + .ref(key) + .modify { + case None => (entryRef.some, true) + case some => (some, false) + } + .map { + case true => + none[V] + .pure[F] + .asRight[Unit] + case false => + ().asLeft[F[Option[V]]] + } + } + case Some(entryRef) => + entryRef + .access + .flatMap { + // A computed value is already present in the map, so we are replacing it with our value. + case (state: EntryState.Value[F, V], set) => + set(EntryState.Value(entry)) + .flatMap { + // Successfully replaced the entryRef with our value, + // now we are responsible for releasing the old value. case true => - none[V] - .pure[F] - .asRight[Int] + state + .entry + .release + .traverse { _.start } + .map { fiber => + fiber + .foldMapM { _.joinWithNever } + .as { state.entry.value.some } + .asRight[Unit] + } + + // Failed to set the entryRef to our value + // so we just release our value and exit. case false => - (counter + 1) - .asLeft[F[Option[V]]] + releaseAndExit } - } - } { entryRef => - entryRef - .access - .flatMap { - // A computed value is already present in the map, so we are replacing it with our value. - case (state: EntryState.Value[F, V], set) => - set(EntryState.Value(entry)) - .flatMap { - // Successfully replaced the entryRef with our value, - // now we are responsible for releasing the old value. - case true => - state - .entry - .release - .traverse { _.start } - .map { fiber => - fiber - .foldMapM { _.joinWithNever } - .as { state.entry.value.some } - .asRight[Int] - } - // Failed to set the entryRef to our value - // so we just release our value and exit. - case false => - entry - .release - .traverse { _.start } // Start releasing and forget - .as { - none[V] - .pure[F] - .asRight[Int] - } - } - // The value is still loading, so we first try to complete the deferred with it, - // and then replace it with our value. - case (state: EntryState.Loading[F, V], set) => - state - .deferred - .complete(entry.asRight) - .flatMap { - // We successfully completed the deferred, now trying to set the value. + // The value is still loading, so we first try to complete the deferred with it, + // and then replace it with our value. + case (state: EntryState.Loading[F, V], set) => + state + .deferred + .complete(entry.asRight) + .flatMap { + // We successfully completed the deferred, now trying to set the value. + case true => + set(EntryState.Value(entry)).flatMap { + // We successfully replaced the entry with our value, so we are done. case true => - set(EntryState.Value(entry)).flatMap { - // We successfully replaced the entry with our value, so we are done. - case true => - none[V] - .pure[F] - .asRight[Int] - .pure[F] - // Another fiber placed their new value before us - // so we just release our value and exit. - case false => - entry - .release - .traverse { _.start } // Start releasing and forget - .as { - none[V] - .pure[F] - .asRight[Int] - } - } - // Someone just completed the deferred we saw + none[V] + .pure[F] + .asRight[Unit] + .pure[F] + + // Another fiber placed their new value before us // so we just release our value and exit. case false => - entry - .release - .traverse { _.start } // Start releasing and forget - .as { - none[V] - .pure[F] - .asRight[Int] - } + releaseAndExit } - // The key was just removed from the map, so just release the value and exit. - case (EntryState.Removed, _) => - entry - .release - .traverse { _.start } // Start releasing and forget - .as { - none[V] - .pure[F] - .asRight[Int] - } - } - .uncancelable + // Someone just completed the deferred we saw + // so we just release our value and exit. + case false => + releaseAndExit + } + + // The key was just removed from the map, so just release the value and exit. + case (EntryState.Removed, _) => + releaseAndExit } - } + .uncancelable + } } } + /** + * Applies the decision of `f` to the current value of the key, atomically. + * + * `f` is called with the value of the key, or `None` if there is none, and may be called more + * than once, because a lost CAS means the decision was made on a stale value and has to be + * taken again. A `Loading` entry is presented to `f` as `None`, as there is no value to + * decide upon yet, and is only overwritten if `f` decides to put one. + */ override def modify[A](key: K)(f: Option[V] => (A, Directive[F, V])): F[(A, Option[F[Unit]])] = { - 0.tailRecM { counter => - checkRetries(counter) *> - ref - .access - .flatMap { case (entryRefs, setMap) => - entryRefs - .get(key) - .fold { - f(None) match { - // No entry present in the map, and we want to add a new one - case (a, put: Directive.Put[F, V]) => - Ref[F] - .of[EntryState[F, V]](EntryState.Value(entryOf(put.value, put.release))) - .flatMap { entryRef => - setMap(entryRefs.updated(key, entryRef)).map { - case true => - (a, none[F[Unit]]) - .asRight[Int] - // Failed adding new entry to the map, retrying accessing the map - case false => - (counter + 1) - .asLeft[(A, Option[F[Unit]])] - } + ().tailRecM { _ => + entryMap + .lookup(key) + .flatMap { + case None => + f(None) match { + // No entry present in the map, and we want to add a new one + case (a, put: Directive.Put[F, V]) => + Ref[F] + .of[EntryState[F, V]](EntryState.Value(entryOf(put.value, put.release))) + .flatMap { entryRef => + entryMap + .ref(key) + .modify { + case None => (entryRef.some, true) + case some => (some, false) } - // No entry present in the map, and we don't want to have any, so exiting - case (a, Directive.Ignore | Directive.Remove) => - (a, none[F[Unit]]) - .asRight[Int] - .pure[F] - } - } { entryRef => - 0.tailRecM { counter1 => - entryRef - .access - .flatMap { - // A value is already present in the map - case (state: EntryState.Value[F, V], setRef) => - f(state.entry.value.some) match { - case (a, put: Directive.Put[F, V]) => - setRef(EntryState.Value(entryOf(put.value, put.release))) - .flatMap { - // Successfully replaced the entryRef with our value, - // now we are responsible for releasing the old value. - case true => + .map { + case true => + (a, none[F[Unit]]) + .asRight[Unit] + // Failed adding new entry to the map, retrying accessing the map + case false => + ().asLeft[(A, Option[F[Unit]])] + } + } + // No entry present in the map, and we don't want to have any, so exiting + case (a, Directive.Ignore | Directive.Remove) => + (a, none[F[Unit]]) + .asRight[Unit] + .pure[F] + } + case Some(entryRef) => + ().tailRecM { _ => + entryRef + .access + .flatMap { + // A value is already present in the map + case (state: EntryState.Value[F, V], setRef) => + f(state.entry.value.some) match { + case (a, put: Directive.Put[F, V]) => + setRef(EntryState.Value(entryOf(put.value, put.release))) + .flatMap { + // Successfully replaced the entryRef with our value, + // now we are responsible for releasing the old value. + case true => + state + .entry + .release + .traverse { _.start } + .map { release => + (a, release.map(_.joinWithNever)) + .asRight[Unit] + .asRight[Unit] + } + // Failed updating entryRef, retrying + case false => + () + .asLeft[Either[Unit, (A, Option[F[Unit]])]] + .pure[F] + } + // Keeping the value intact and exiting + case (a, Directive.Ignore) => + (a, none[F[Unit]]) + .asRight[Unit] + .asRight[Unit] + .pure[F] + // Removing the value + case (a, Directive.Remove) => + setRef(EntryState.Removed) + .flatMap { + // Successfully set the entryRef to `Removed` state, now removing it from the map. + // Only removing the key if it still contains this entry, otherwise noop. + case true => + entryMap + .ref(key) + .update { + case Some(`entryRef`) => none + case other => other + } + .flatMap { _ => + // Releasing the value regardless of the map update result. state .entry .release .traverse { _.start } .map { release => (a, release.map(_.joinWithNever)) - .asRight[Int] - .asRight[Int] - } - // Failed updating entryRef, retrying - case false => - (counter1 + 1) - .asLeft[Either[Int, (A, Option[F[Unit]])]] - .pure[F] - } - // Keeping the value intact and exiting - case (a, Directive.Ignore) => - (a, none[F[Unit]]) - .asRight[Int] - .asRight[Int] - .pure[F] - // Removing the value - case (a, Directive.Remove) => - setRef(EntryState.Removed) - .flatMap { - // Successfully set the entryRef to `Removed` state, now removing it from the map. - // Only removing the key if it still contains this entry, otherwise noop. - case true => - ref - .update { entryRefs => - entryRefs.get(key) match { - case Some(`entryRef`) => entryRefs - key - case _ => entryRefs - } - } - .flatMap { _ => - // Releasing the value regardless of the map update result. - state - .entry - .release - .traverse { _.start } - .map { release => - (a, release.map(_.joinWithNever)) - .asRight[Int] - .asRight[Int] - } + .asRight[Unit] + .asRight[Unit] } - // Failed updating entryRef, retrying - case false => - (counter1 + 1) - .asLeft[Either[Int, (A, Option[F[Unit]])]] - .pure[F] - } - } + } + // Failed updating entryRef, retrying + case false => + () + .asLeft[Either[Unit, (A, Option[F[Unit]])]] + .pure[F] + } + } - // Entry in the map is still loading - case (state: EntryState.Loading[F, V], setRef) => - f(None) match { - // Trying to replace it with our value - case (a, put: Directive.Put[F, V]) => - val entry = entryOf(put.value, put.release) - state - .deferred - .complete(entry.asRight) - .flatMap { - // We successfully completed the deferred, now trying to set the value. + // Entry in the map is still loading + case (state: EntryState.Loading[F, V], setRef) => + f(None) match { + // Trying to replace it with our value + case (a, put: Directive.Put[F, V]) => + val entry = entryOf(put.value, put.release) + state + .deferred + .complete(entry.asRight) + .flatMap { + // We successfully completed the deferred, now trying to set the value. + case true => + setRef(EntryState.Value(entry)).flatMap { + // We successfully replaced the entry with our value, so we are done. case true => - setRef(EntryState.Value(entry)).map { - // We successfully replaced the entry with our value, so we are done. - case true => - (a, none[F[Unit]]) - .asRight[Int] - .asRight[Int] - // Another fiber placed their new value (only Removed should be possible) - // before us so we retry accessing the entry. - case false => - (counter1 + 1) - .asLeft[Either[Int, (A, Option[F[Unit]])]] - } - // Failed to complete the deferred, meaning someone else completed it, and will - // now set the new value in the entryRef. Retrying the lookup. - case false => - (counter1 + 1) - .asLeft[Either[Int, (A, Option[F[Unit]])]] + (a, none[F[Unit]]) + .asRight[Unit] + .asRight[Unit] .pure[F] + // The entry was removed (only Removed is possible here) before our + // value made it in. Completing the deferred made us the owner of the + // release, and published the decision to the waiters, so instead of + // deciding again we release the value and exit, the same way `put` + // does when its value loses this very race. + case false => + entry + .release + .traverse { _.start } + .as { + (a, none[F[Unit]]) + .asRight[Unit] + .asRight[Unit] + } } - // Noop decision, exiting - case (a, Directive.Ignore | Directive.Remove) => - (a, none[F[Unit]]) - .asRight[Int] - .asRight[Int] - .pure[F] - } + // Failed to complete the deferred, meaning someone else completed it, and will + // now set the new value in the entryRef. Retrying the lookup. + case false => + () + .asLeft[Either[Unit, (A, Option[F[Unit]])]] + .pure[F] + } + // Noop decision, exiting + case (a, Directive.Ignore | Directive.Remove) => + (a, none[F[Unit]]) + .asRight[Unit] + .asRight[Unit] + .pure[F] + } - // Entry was just removed, it soon will be gone from the map. - case (EntryState.Removed, _) => - f(None) match { - // We want to place the new value; - // Retrying the map lookup, expecting a different result for our key. - case (_, _: Directive.Put[F, V]) => - (counter + 1) - .asLeft[(A, Option[F[Unit]])] - .asRight[Int] - .pure[F] - // Noop decision, exiting - case (a, Directive.Ignore | Directive.Remove) => - (a, none[F[Unit]]) - .asRight[Int] - .asRight[Int] - .pure[F] - } + // Entry was just removed, it soon will be gone from the map. + case (EntryState.Removed, _) => + f(None) match { + // We want to place the new value; + // Retrying the map lookup, expecting a different result for our key. + case (_, _: Directive.Put[F, V]) => + () + .asLeft[(A, Option[F[Unit]])] + .asRight[Unit] + .pure[F] + // Noop decision, exiting + case (a, Directive.Ignore | Directive.Remove) => + (a, none[F[Unit]]) + .asRight[Unit] + .asRight[Unit] + .pure[F] } - .uncancelable } - } - } + .uncancelable + } + } } } - def contains(key: K) = { - ref - .get - .map { _.contains(key) } - } + def contains(key: K): F[Boolean] = entryMap.contains(key) - def size = { - ref - .get - .map { _.size } - } + def size: F[Int] = entryMap.size - def keys = { - ref - .get - .map { _.keySet } - } + def keys: F[Set[K]] = entryMap.keys - def values = { - ref - .get - .flatMap { entryRefs => - entryRefs + def values: F[Map[K, F[V]]] = { + entryMap + .entries + .flatMap { entries => + entries .foldLeft { List .empty[(K, F[V])] @@ -713,11 +920,11 @@ private[scache] object LoadingCache { .map { _.toMap } } - def values1 = { - ref - .get - .flatMap { entryRefs => - entryRefs + def values1: F[Map[K, Either[F[V], V]]] = { + entryMap + .entries + .flatMap { entries => + entries .foldLeft { List .empty[(K, Either[F[V], V])] @@ -736,73 +943,83 @@ private[scache] object LoadingCache { .map { _.toMap } } + /** + * Removes the key from the cache, returning the removed value, if any. + * + * Unlinking the key and marking the entry `Removed` happen in that order and uncancelably: + * the mark is what makes this fiber the one responsible for the release, and what tells the + * fibers holding this `EntryRef` that they are looking at a stale reference. + * + * A `Loading` entry has no value to return, and removing it does not cancel the load: the + * loading fiber finds the `Removed` mark, sees that the key is now free, and stores its value + * under it, so a load that outlives the `remove` puts the key back into the cache. + */ def remove(key: K): F[F[Option[V]]] = { - 0.tailRecM { counter => - checkRetries(counter) *> - ref - .access - .flatMap { case (entryRefs, set) => - entryRefs - .get(key) - .fold { + entryMap + .ref(key) + .getAndSet(none) + .flatMap { + case Some(entryRef) => + // We just removed the entry from the map, now we need to release it. + // Replacing the value of the ref with `Removed` means that we are getting responsible for the release. + entryRef + .getAndSet(EntryState.Removed) + .flatMap { + // We removed a loaded value, so we are responsible for releasing it. + case state: EntryState.Value[F, V] => + state + .entry + .release1 + .as { state.entry.value.some } + .start + .map { fiber => + fiber.joinWithNever + } + + // We removed a loading value, and the fiber that will complete it will also + // release that value, so there is nothing for us to return. + case _: EntryState.Loading[F, V] => none[V] .pure[F] - .asRight[Int] .pure[F] - } { entryRef => - set(entryRefs - key) - .flatMap { - case true => - // We just removed the entry for the map, now we need to release it. - // Replacing the value of the ref with `Removed` means that we are getting responsible for the release. - entryRef - .getAndSet(EntryState.Removed) - .flatMap { - // We removed a loaded value, so we are responsible for releasing it. - case state: EntryState.Value[F, V] => - state - .entry - .release1 - .as { state.entry.value.some } - .start - .map { fiber => - fiber - .joinWithNever - .asRight[Int] - } - // We removed a loading value, and the fiber that will complete it will also - // release that value, so there is nothing for us to return. - case _: EntryState.Loading[F, V] => - none[V] - .pure[F] - .asRight[Int] - .pure[F] - - // We removed an entry that was already being removed by another fiber, so we are done. - case EntryState.Removed => - none[V] - .pure[F] - .asRight[Int] - .pure[F] - } - case false => - (counter + 1) - .asLeft[F[Option[V]]] - .pure[F] - } - .uncancelable - } - } - } + // We removed an entry that was already being removed by another fiber, so we are done. + case EntryState.Removed => + none[V] + .pure[F] + .pure[F] + } + case None => + none[V] + .pure[F] + .pure[F] + } + .uncancelable } - def clear = { - ref - .getAndSet(EntryRefs.empty) + /** + * Removes all the entries, returning an effect awaiting the release of all their values. + * + * The keys are unlinked one by one, as there is no atomic bulk operation on a per-key `Ref`, + * so entries added concurrently may survive the clearing. As this also runs on the release of + * the cache resource, an entry added while a large cache is being cleared can outlive the + * cache itself, with its value never released. + * + * Values of entries that are still loading are awaited before being released, which is why a + * load that never completes would make this, and the release of the cache resource, hang. + */ + def clear: F[F[Unit]] = { + entryMap + .keys + .flatMap { keys => + keys + .toList + .traverse { key => entryMap.ref(key).getAndSet(none) } + .map { _.flatten } + } .flatMap { entryRefs => entryRefs - .parFoldMap1 { case (_, entryRef) => + .parFoldMap1 { entryRef => entryRef .getOption .flatMap { _.foldMapM { _.release1 } } @@ -814,14 +1031,14 @@ private[scache] object LoadingCache { .map { _.joinWithNever } } - def foldMap[A: CommutativeMonoid](f: (K, Either[F[V], V]) => F[A]) = { - ref - .get - .flatMap { entryRefs => + def foldMap[A: CommutativeMonoid](f: (K, Either[F[V], V]) => F[A]): F[A] = { + entryMap + .entries + .flatMap { entries => val zero = CommutativeMonoid[A] .empty .pure[F] - entryRefs.foldLeft(zero) { case (a, (key, entryRef)) => + entries.foldLeft(zero) { case (a, (key, entryRef)) => for { a <- a v <- entryRef.optEither @@ -833,15 +1050,15 @@ private[scache] object LoadingCache { } } - def foldMapPar[A: CommutativeMonoid](f: (K, Either[F[V], V]) => F[A]) = { - ref - .get - .flatMap { entryRefs => + def foldMapPar[A: CommutativeMonoid](f: (K, Either[F[V], V]) => F[A]): F[A] = { + entryMap + .entries + .flatMap { entries => Parallel[F].sequential { val zero = Parallel[F] .applicative .pure(CommutativeMonoid[A].empty) - entryRefs + entries .foldLeft(zero) { case (a, (key, entryRef)) => val b = Parallel[F].parallel { for { @@ -859,6 +1076,9 @@ private[scache] object LoadingCache { } } + /** + * Cached value together with the effect releasing it, if it needs releasing. + */ final case class Entry[+F[_], +A](value: A, release: Option[F[Unit]]) object Entry { @@ -870,10 +1090,36 @@ private[scache] object LoadingCache { } } + /** + * State of a cache entry. + * + * The possible transitions are `Loading -> Value`, `Loading -> Removed`, `Value -> Value` and + * `Value -> Removed`, with `Removed` being terminal, so that a stale reference stays recognizable + * as such. + */ sealed trait EntryState[+F[_], +A] object EntryState { + + /** + * The value is being computed, and `deferred` will hold it, or the reason it will never be + * available: [[CancelledError]] if the load got cancelled, [[ExpiredError]] if it was evicted + * for taking too long, or the error the computation failed with. + * + * The `deferred` doubles as the identity of the load: a fiber may only act on the entry as long + * as it still holds the very same `deferred` it installed, which is what keeps a fiber from + * interfering with a load started after its own one ended. + */ final case class Loading[F[_], A](deferred: Deferred[F, Either[Throwable, Entry[F, A]]]) extends EntryState[F, A] + + /** + * The value is computed and available. + */ final case class Value[F[_], A](entry: Entry[F, A]) extends EntryState[F, A] + + /** + * The entry is gone, and this reference to it is stale: the key it used to be mapped to is + * either unlinked already, or is about to be, and has to be looked up anew. + */ case object Removed extends EntryState[Nothing, Nothing] } @@ -881,12 +1127,6 @@ private[scache] object LoadingCache { type EntryRef[F[_], A] = Ref[F, EntryState[F, A]] - type EntryRefs[F[_], K, V] = Map[K, EntryRef[F, V]] - - object EntryRefs { - def empty[F[_], K, V]: EntryRefs[F, K, V] = Map.empty - } - implicit class DeferredThrowOps[F[_], A](val self: DeferredThrow[F, A]) extends AnyVal { def getOrError( implicit @@ -912,6 +1152,9 @@ private[scache] object LoadingCache { implicit class EntryStateOps[F[_], A](val self: EntryState[F, A]) extends AnyVal { + /** + * Value of the entry, awaiting it if it is still loading, and `None` if it will never arrive. + */ def getOption( implicit F: Applicative[F], @@ -923,6 +1166,10 @@ private[scache] object LoadingCache { } } + /** + * The entry as the cache API sees it: `None` for an entry that is gone, `Left` for a value that + * is still being computed, and `Right` for a value that is already there. + */ def optEither( implicit F: MonadThrow[F], @@ -987,6 +1234,10 @@ private[scache] object LoadingCache { } } + /** + * Updates the value of the entry, if there is one, retrying on a lost CAS, and doing nothing at + * all if the entry is still loading or is gone. + */ def update1( f: A => A, )(implicit @@ -1016,6 +1267,15 @@ private[scache] object LoadingCache { } implicit class Ops[F[_], A, E](val fa: F[A]) extends AnyVal { + + /** + * Races `fa` against `fb`, returning `Left` if `fa` won, and `Right` with the still running + * `fa` if `fb` did. + * + * Unlike `race`, a losing `fa` is not cancelled, but handed over to the caller instead, because + * `fa` computes a value that will have to be released once it is there. A cancelled `fa` + * cancels the race, while a cancelled `fb` leaves the race waiting for `fa`. + */ def race1[B]( fb: F[B], )(implicit diff --git a/scache/src/main/scala/com/evolution/scache/SerialMap.scala b/scache/src/main/scala/com/evolution/scache/SerialMap.scala index 9003b4c..58d09de 100644 --- a/scache/src/main/scala/com/evolution/scache/SerialMap.scala +++ b/scache/src/main/scala/com/evolution/scache/SerialMap.scala @@ -2,7 +2,7 @@ package com.evolution.scache import cats.Applicative import cats.effect.implicits.* -import cats.effect.{Concurrent, Ref} +import cats.effect.{Async, Concurrent, Ref} import cats.syntax.all.* import com.evolutiongaming.catshelper.{Runtime, SerialRef} @@ -20,12 +20,12 @@ trait SerialMap[F[_], K, V] { def put(key: K, value: V): F[Option[V]] /** - * `f` will be run serially for the same key, entry will be removed in case of `f` returns `none` + * `f` will be run serially for the same key, entry will be removed in case of `f` returns `None` */ def modify[A](key: K)(f: Option[V] => F[(Option[V], A)]): F[A] /** - * `f` will be run serially for the same key, entry will be removed in case of `f` returns `none` + * `f` will be run serially for the same key, entry will be removed in case of `f` returns `None` */ def update[A](key: K)(f: Option[V] => F[Option[V]]): F[Unit] @@ -72,14 +72,14 @@ object SerialMap { self => def apply[F[_]]( implicit - F: Concurrent[F], + F: Async[F], ): Apply[F] = new Apply(F) - def of[F[_]: Concurrent: Runtime, K, V]: F[SerialMap[F, K, V]] = of(None) + def of[F[_]: Async: Runtime, K, V]: F[SerialMap[F, K, V]] = of(None) - def of[F[_]: Concurrent: Runtime, K, V](partitions: Int): F[SerialMap[F, K, V]] = of(Some(partitions)) + def of[F[_]: Async: Runtime, K, V](partitions: Int): F[SerialMap[F, K, V]] = of(Some(partitions)) - def of[F[_]: Concurrent: Runtime, K, V](partitions: Option[Int] = None): F[SerialMap[F, K, V]] = { + def of[F[_]: Async: Runtime, K, V](partitions: Option[Int] = None): F[SerialMap[F, K, V]] = { Cache .loading[F, K, SerialRef[F, State[V]]](partitions) .allocated @@ -224,13 +224,13 @@ object SerialMap { self => } } - class Apply[F[_]](val F: Concurrent[F]) extends AnyVal { + class Apply[F[_]](val F: Async[F]) extends AnyVal { def of[K, V]( implicit runtime: Runtime[F], ): F[SerialMap[F, K, V]] = { - implicit val concurrent: Concurrent[F] = F + implicit val async: Async[F] = F self.of[F, K, V](None) } } diff --git a/scache/src/test/scala/com/evolution/scache/CacheDefectsSpec.scala b/scache/src/test/scala/com/evolution/scache/CacheDefectsSpec.scala new file mode 100644 index 0000000..7467cd8 --- /dev/null +++ b/scache/src/test/scala/com/evolution/scache/CacheDefectsSpec.scala @@ -0,0 +1,403 @@ +package com.evolution.scache + +import cats.effect.* +import cats.syntax.all.* +import com.evolution.scache.IOSuite.* +import com.evolution.scache.LoadingCache.{EntryMap, EntryRef} +import org.scalatest.funsuite.AsyncFunSuite +import org.scalatest.matchers.should.Matchers + +import scala.concurrent.duration.* + +/** + * Asserts the expected behavior for four defects originally present in LoadingCache / + * ExpiringCache, fixed by rebuilding the cache on [[cats.effect.std.MapRef]]: + * - claim 1: loads are cancelable and cancellation cleans up the `Loading` entry; + * - claim 2: entries stuck in `Loading` state are evicted by the expiration routine; + * - claim 3: waiters on a `Loading` entry are unblocked when the load is cancelled; + * - claim 4: operations on distinct keys are independent, no shared-state CAS retries. + * + * Every stuck load is modeled with a `gate` Deferred instead of `IO.never` and released in a + * `guarantee`, so a failed assertion produces a clean test failure instead of hanging resource + * finalizers (`clear` waits on Loading entries). + */ +class CacheDefectsSpec extends AsyncFunSuite with Matchers { + + test("claim 1: cancelled load must not block the key for subsequent calls") { + val io = for { + entryMap <- EntryMap.of[IO, Int, Int] + cache = LoadingCache(entryMap) + started <- Deferred[IO, Unit] + gate <- Deferred[IO, Unit] + loader <- cache.getOrUpdate(0) { started.complete(()) *> gate.get.as(1) }.start + _ <- started.get + cancelling <- loader.cancel.start + result <- { + for { + cancelled <- cancelling.join.timeout(500.millis) + _ = cancelled should matchPattern { case Outcome.Succeeded(_) => } + present <- cache.get(0) + _ = present shouldEqual none + second <- cache.getOrUpdate(0)(2.pure[IO]).timeout(500.millis) + _ = second shouldEqual 2 + } yield () + }.guarantee { gate.complete(()) *> cancelling.join.void } + } yield result + io.run() + } + + test("claim 2: expiration cleanup must evict entries stuck in Loading state") { + val config = ExpiringCache.Config[IO, Int, Int]( + expireAfterRead = 100.millis, + loadingTimeout = 100.millis.some, + ) + val io = ExpiringCache.of[IO, Int, Int](config).use { cache => + for { + started <- Deferred[IO, Unit] + gate <- Deferred[IO, Unit] + // Attempted, see the test evicting a stuck Loading entry below. + loader <- cache.getOrUpdate(0) { started.complete(()) *> gate.get.as(1) }.attempt.start + _ <- started.get + result <- { + for { + _ <- cache.put(1, 1).flatten + _ <- IO.sleep(500.millis) + // Control: an ordinary value of the same age is gone, so the cleanup did run. + control <- cache.contains(1) + _ = control shouldEqual false + poisoned <- cache.contains(0) + _ = poisoned shouldEqual false + second <- cache.getOrUpdate(0)(2.pure[IO]).timeout(500.millis) + _ = second shouldEqual 2 + } yield () + }.guarantee { gate.complete(()) *> loader.join.void } + } yield result + } + io.run() + } + + test("claim 3: cancelling a load must unblock the fibers waiting on it") { + val io = for { + entryMap <- EntryMap.of[IO, Int, Int] + cache = LoadingCache(entryMap) + started <- Deferred[IO, Unit] + gate <- Deferred[IO, Unit] + loader <- cache.getOrUpdate(0) { started.complete(()) *> gate.get.as(1) }.start + _ <- started.get + // Attempted, so that the failure this test is after is observed as a value: a fiber left to + // end in `Errored` reports the error to the runtime as unhandled the moment it finishes, + // which here races with the `join` below. + waiter <- cache.getOrUpdate(0)(99.pure[IO]).attempt.start + _ <- IO.sleep(100.millis) + cancelling <- loader.cancel.start + result <- { + for { + outcome <- waiter.joinWithNever.timeout(500.millis) + _ = outcome should matchPattern { case Left(CancelledError) => } + present <- cache.get(0) + _ = present shouldEqual none + } yield () + }.guarantee { gate.complete(()) *> cancelling.join.void } + } yield result + io.run() + } + + test("claim 3: cancelling a load removed while loading must unblock the fibers waiting on it") { + val io = for { + entryMap <- EntryMap.of[IO, Int, Int] + cache = LoadingCache(entryMap) + started <- Deferred[IO, Unit] + gate <- Deferred[IO, Unit] + loader <- cache.getOrUpdate(0) { started.complete(()) *> gate.get.as(1) }.start + _ <- started.get + // Attempted, see the test above. + waiter <- cache.getOrUpdate(0)(99.pure[IO]).attempt.start + _ <- IO.sleep(100.millis) + // The entry stops being the loader's, so only the loader itself can still unblock the waiter. + _ <- cache.remove(0).flatten + cancelling <- loader.cancel.start + result <- { + for { + outcome <- waiter.joinWithNever.timeout(500.millis) + _ = outcome should matchPattern { case Left(CancelledError) => } + } yield () + }.guarantee { gate.complete(()) *> cancelling.join.void } + } yield result + io.run() + } + + test("claim 4: getOrUpdate must complete under sustained writes of unrelated keys") { + val io = LoadingCache.of[IO, Int, Int].use { cache => + for { + writers <- (1 to 8) + .toList + .traverse { key => + (cache.put(key, key).flatten *> cache.remove(key).flatten) + .foreverM + .start + } + _ <- IO.sleep(100.millis) + result <- { + for { + value <- cache.getOrUpdate(0)(1.pure[IO]).timeout(5.seconds) + _ = value shouldEqual 1 + } yield () + }.guarantee { writers.parTraverse_ { _.cancel } } + } yield result + } + io.run(timeout = 30.seconds) + } + + test("claim 4 mechanism: insert of an unrelated key must not force a retry of getOrUpdate") { + val io = for { + underlying <- EntryMap.of[IO, Int, Int] + attempts <- Ref[IO].of(0) + noise = insertUnrelated(underlying, 1) + cache = LoadingCache(intercepted(underlying, noise, attempts.some)) + value <- cache.getOrUpdate(0)(1.pure[IO]) + _ = value shouldEqual 1 + attempts <- attempts.get + _ = attempts shouldEqual 1 + keys <- cache.keys + _ = keys shouldEqual Set(0, 1) + } yield () + io.run() + } + + test("claim 4 mechanism: parallel getOrUpdate of distinct keys causes no insert retries") { + val io = for { + underlying <- EntryMap.of[IO, Int, Int] + attempts <- Ref[IO].of(0) + cache = LoadingCache(intercepted(underlying, IO.unit, attempts.some)) + _ <- (0 until 10000).toList.parTraverse { key => cache.getOrUpdate(key)(key.pure[IO]) } + size <- cache.size + _ = size shouldEqual 10000 + attempts <- attempts.get + _ = attempts shouldEqual 10000 + } yield () + io.run(timeout = 30.seconds) + } + + test("evicting a stuck Loading entry unblocks fibers waiting on it") { + val config = ExpiringCache.Config[IO, Int, Int]( + expireAfterRead = 1.minute, + loadingTimeout = 100.millis.some, + ) + val io = ExpiringCache.of[IO, Int, Int](config).use { cache => + for { + started <- Deferred[IO, Unit] + gate <- Deferred[IO, Unit] + // Attempted, because the eviction makes this load fail too, and a fiber left to end in + // `Errored` reports the error to the runtime as unhandled the moment it finishes, before + // the `join` below gets to observe it. + loader <- cache.getOrUpdate(0) { started.complete(()) *> gate.get.as(1) }.attempt.start + _ <- started.get + waiter <- cache.getOrUpdate(0)(99.pure[IO]).attempt.start + result <- { + for { + outcome <- waiter.joinWithNever.timeout(2.seconds) + _ = outcome should matchPattern { case Left(ExpiredError) => } + _ <- gate.complete(()) + // The fiber whose load was evicted learns about it as well. + evicted <- loader.joinWithNever.timeout(2.seconds) + _ = evicted should matchPattern { case Left(ExpiredError) => } + } yield () + }.guarantee { gate.complete(()).attempt *> loader.join.void } + } yield result + } + io.run() + } + + test("a new load generation does not inherit the previous generation's stuck-timer") { + val config = ExpiringCache.Config[IO, Int, Int]( + expireAfterRead = 1.minute, + loadingTimeout = 200.millis.some, + ) + val io = ExpiringCache.of[IO, Int, Int](config).use { cache => + for { + started1 <- Deferred[IO, Unit] + gate1 <- Deferred[IO, Unit] + loader1 <- cache.getOrUpdate(0) { started1.complete(()) *> gate1.get.as(1) }.start + _ <- started1.get + _ <- IO.sleep(150.millis) + _ <- gate1.complete(()) + _ <- loader1.join + _ <- cache.remove(0).flatten + started2 <- Deferred[IO, Unit] + gate2 <- Deferred[IO, Unit] + loader2 <- cache.getOrUpdate(0) { started2.complete(()) *> gate2.get.as(2) }.start + _ <- started2.get + result <- { + for { + _ <- IO.sleep(150.millis) + present <- cache.contains(0) + _ = present shouldEqual true + } yield () + }.guarantee { gate2.complete(()) *> loader2.join.void } + } yield result + } + io.run() + } + + test("cancellation races neither poison the key nor leak releases") { + val io = for { + entryMap <- EntryMap.of[IO, Int, Int] + cache = LoadingCache(entryMap) + balance <- Ref[IO].of(0) + _ <- (1 to 500).toList.traverse_ { i => + for { + fiber <- cache.getOrUpdate1(0) { balance.update { _ + 1 }.as((i, i, balance.update { _ - 1 }.some)) }.start + _ <- fiber.cancel.start + _ <- fiber.join + // The key must be usable right away, holding either the value of the load that made it + // in before the cancellation, or the one we compute here. + value <- cache.getOrUpdate(0)((-1).pure[IO]).timeout(1.second) + _ = value should (equal(i) or equal(-1)) + _ <- cache.remove(0).flatten + } yield () + } + // Releases of values nobody asked about are started in the background, so the balance is + // settled shortly after the last removal rather than at the moment of it. + _ <- (IO.sleep(10.millis) *> balance.get).iterateUntil { _ == 0 }.timeout(3.seconds) + } yield () + io.run(timeout = 60.seconds) + } + + test("modify racing remove over a loading key neither leaks nor double-releases") { + val io = for { + entryMap <- EntryMap.of[IO, Int, Int] + cache = LoadingCache(entryMap) + balance <- Ref[IO].of(0) + _ <- (1 to 500).toList.traverse_ { i => + for { + started <- Deferred[IO, Unit] + gate <- Deferred[IO, Unit] + loader <- cache.getOrUpdate(0) { started.complete(()) *> gate.get.as(-i) }.start + _ <- started.get + // The waiters widen the window between modify completing the deferred and committing the + // entry, which is exactly where the concurrent remove has to land. + waiters <- (1 to 8).toList.traverse { _ => cache.getOrUpdate(0)((-i).pure[IO]).start } + _ <- balance.update { _ + 1 } + _ <- ( + cache.modify(0) { _ => ((), Cache.Directive.Put(i, balance.update { _ - 1 }.some)) }, + cache.remove(0).flatten, + ).parTupled + _ <- gate.complete(()) + _ <- loader.join + _ <- waiters.traverse_ { _.join } + _ <- cache.remove(0).flatten + } yield () + } + // Exactly one release per iteration must run: the balance ends below zero on a double + // release and above zero on a leak, and either keeps this from ever reaching zero. + _ <- (IO.sleep(10.millis) *> balance.get).iterateUntil { _ == 0 }.timeout(3.seconds) + } yield () + io.run(timeout = 60.seconds) + } + + test("modify must release the value it published when its commit loses to remove") { + val io = for { + entryMap <- EntryMap.of[IO, Int, Int] + cache = LoadingCache(entryMap) + balance <- Ref[IO].of(0) + deferred <- Deferred[IO, Either[Throwable, LoadingCache.Entry[IO, Int]]] + inner <- Ref[IO].of[LoadingCache.EntryState[IO, Int]](LoadingCache.EntryState.Loading(deferred)) + armed <- Ref[IO].of(true) + // Fired by the commit attempt of `modify`, i.e. between it completing the deferred and + // writing the entry: the remove steals the entry, the put takes the key over. + noise = armed.getAndSet(false).flatMap { + case true => cache.remove(0).flatten *> cache.put(0, 99).flatten.void + case false => IO.unit + } + _ <- entryMap.ref(0).set(interceptedCommit(inner, noise).some) + _ <- balance.update { _ + 1 } + _ <- cache.modify(0) { + case None => ((), Cache.Directive.Put(1, balance.update { _ - 1 }.some)) + case Some(_) => ((), Cache.Directive.Ignore) + } + published <- deferred.get.timeout(1.second) + _ = published.map { _.value } shouldEqual 1.asRight + // The key must be held by the value the injected put installed, which is what proves the + // remove-put pair did land between the deferred completion and the commit. + taken <- cache.get(0) + _ = taken shouldEqual 99.some + _ <- cache.remove(0).flatten + _ <- (IO.sleep(10.millis) *> balance.get).iterateUntil { _ == 0 }.timeout(3.seconds) + } yield () + io.run() + } + + /** + * An entry `Ref` whose `access` setter runs `noise` before committing, so that a test can inject + * a concurrent state transition exactly between an operation reading the entry state and writing + * it back. Every other method, `getAndSet` of `remove` included, goes to `inner` untouched. + */ + private def interceptedCommit( + inner: Ref[IO, LoadingCache.EntryState[IO, Int]], + noise: IO[Unit], + ): EntryRef[IO, Int] = { + type A = LoadingCache.EntryState[IO, Int] + new Ref[IO, A] { + def get: IO[A] = inner.get + def set(a: A): IO[Unit] = inner.set(a) + def access: IO[(A, A => IO[Boolean])] = inner.access.map { case (a, set) => (a, (a1: A) => noise *> set(a1)) } + def tryUpdate(f: A => A): IO[Boolean] = inner.tryUpdate(f) + def tryModify[B](f: A => (A, B)): IO[Option[B]] = inner.tryModify(f) + def update(f: A => A): IO[Unit] = inner.update(f) + def modify[B](f: A => (A, B)): IO[B] = inner.modify(f) + def tryModifyState[B](state: cats.data.State[A, B]): IO[Option[B]] = inner.tryModifyState(state) + def modifyState[B](state: cats.data.State[A, B]): IO[B] = inner.modifyState(state) + } + } + + private def insertUnrelated(underlying: EntryMap[IO, Int, Int], key: Int): IO[Unit] = { + for { + entryRef <- Ref[IO].of[LoadingCache.EntryState[IO, Int]]( + LoadingCache.EntryState.Value(LoadingCache.Entry(key, none)), + ) + _ <- underlying.ref(key).set(entryRef.some) + } yield () + } + + /** + * `underlying` with every per-key `Ref` wrapped, so that each attempt of the cache to modify the + * mapping first runs `noise`, a write of some other key, and then is counted in `attempts`. + * + * That gives the deterministic version of what the `claim 4` test does with background fibers: an + * unrelated write is guaranteed to land between reading and writing the mapping, i.e. exactly + * where the shared `Ref[F, Map[K, EntryRef]]` used to lose its CAS. With a per-key `Ref` the + * attempt still succeeds, so the count stays at one attempt per insert. + * + * `noise` writes through `underlying` directly and is not counted. + */ + private def intercepted( + underlying: EntryMap[IO, Int, Int], + noise: IO[Unit], + attempts: Option[Ref[IO, Int]], + ): EntryMap[IO, Int, Int] = { + def wrap(ref: Ref[IO, Option[EntryRef[IO, Int]]]): Ref[IO, Option[EntryRef[IO, Int]]] = { + type A = Option[EntryRef[IO, Int]] + val observe = noise *> attempts.foldMapM { _.update { _ + 1 } } + new Ref[IO, A] { + def get: IO[A] = ref.get + def set(a: A): IO[Unit] = observe *> ref.set(a) + def access: IO[(A, A => IO[Boolean])] = ref.access.map { case (a, set) => (a, (a1: A) => observe *> set(a1)) } + def tryUpdate(f: A => A): IO[Boolean] = observe *> ref.tryUpdate(f) + def tryModify[B](f: A => (A, B)): IO[Option[B]] = observe *> ref.tryModify(f) + def update(f: A => A): IO[Unit] = observe *> ref.update(f) + def modify[B](f: A => (A, B)): IO[B] = observe *> ref.modify(f) + def tryModifyState[B](state: cats.data.State[A, B]): IO[Option[B]] = observe *> ref.tryModifyState(state) + def modifyState[B](state: cats.data.State[A, B]): IO[B] = observe *> ref.modifyState(state) + } + } + + new EntryMap[IO, Int, Int] { + def ref(key: Int): Ref[IO, Option[EntryRef[IO, Int]]] = wrap(underlying.ref(key)) + def lookup(key: Int): IO[Option[EntryRef[IO, Int]]] = underlying.lookup(key) + def keys: IO[Set[Int]] = underlying.keys + def entries: IO[List[(Int, EntryRef[IO, Int])]] = underlying.entries + def size: IO[Int] = underlying.size + def contains(key: Int): IO[Boolean] = underlying.contains(key) + } + } +} diff --git a/scache/src/test/scala/com/evolution/scache/CacheSpec.scala b/scache/src/test/scala/com/evolution/scache/CacheSpec.scala index be2fe03..a435f7c 100644 --- a/scache/src/test/scala/com/evolution/scache/CacheSpec.scala +++ b/scache/src/test/scala/com/evolution/scache/CacheSpec.scala @@ -26,7 +26,7 @@ class CacheSpec extends AsyncFunSuite with Matchers { for { (name, cache0) <- List( ("default", Cache.loading[IO, Int, Int]), - ("no partitions", LoadingCache.of(LoadingCache.EntryRefs.empty[IO, Int, Int])), + ("no partitions", LoadingCache.of[IO, Int, Int]), ("expiring", expiringCache), ( "expiring no partitions", @@ -615,21 +615,17 @@ class CacheSpec extends AsyncFunSuite with Matchers { for { deferred0 <- Deferred[IO, (Int, Option[IO[Unit]])] fiber0 <- cache.getOrUpdate1Ensure(0) { deferred0.get } - fiber1 <- cache.getOrUpdate2(0) { IO.never }.startEnsure - release <- Deferred[IO, Unit] - _ <- fiber0.cancel.start - _ <- deferred0.complete((0, release.complete(()).void.some)) - value <- fiber0.join - _ <- IO { value shouldEqual Outcome.canceled } - value <- fiber1.joinWithNever - _ <- IO { value shouldEqual 0.asRight } - _ <- cache.remove(0) - _ <- release.get + _ <- fiber0.cancel + outcome <- fiber0.join + _ <- IO { outcome shouldEqual Outcome.canceled } + value <- cache.getOrUpdate2(0) { (0, 0, none[IO[Unit]]).pure[IO] } + _ <- IO { value shouldEqual 0.asLeft } + value <- cache.get(0) + _ <- IO { value shouldEqual 0.some } _ <- metrics.expect( - metrics.expectedGet(hit = false) -> 1, + metrics.expectedGet(hit = false) -> 2, metrics.expectedGet(hit = true) -> 1, metrics.expectedLoad(success = true) -> 1, - metrics.expectedLife -> 1, ) } yield {} } @@ -1076,25 +1072,7 @@ class CacheSpec extends AsyncFunSuite with Matchers { } yield {} } - check(s"cancellation: $name") { (cache, metrics) => - for { - deferred <- Deferred[IO, Int] - fiber <- cache.getOrUpdateEnsure(0) { deferred.get } - _ <- fiber.cancel.start - _ <- deferred.complete(0) - cancelOutcome <- fiber.join - _ <- IO { cancelOutcome shouldEqual Outcome.canceled } - value <- cache.get(0) - _ <- IO { value shouldEqual 0.some } - _ <- metrics.expect( - metrics.expectedGet(hit = false) -> 1, - metrics.expectedLoad(success = true) -> 1, - metrics.expectedGet(hit = true) -> 1, - ) - } yield {} - } - - ignore(s"cancellation proper: $name") { + test(s"cancellation proper: $name") { cacheAndMetrics .use { case (cache, metrics) => for { diff --git a/scache/src/test/scala/com/evolution/scache/ExpiringCacheSpec.scala b/scache/src/test/scala/com/evolution/scache/ExpiringCacheSpec.scala index d77b74b..724ce38 100644 --- a/scache/src/test/scala/com/evolution/scache/ExpiringCacheSpec.scala +++ b/scache/src/test/scala/com/evolution/scache/ExpiringCacheSpec.scala @@ -90,14 +90,19 @@ class ExpiringCacheSpec extends AsyncFunSuite with Matchers { value <- cache.put(0, 0, release.set(true)) value <- value _ <- Sync[F].delay { value shouldEqual none } - value <- cache.put(1, 1) + released <- Deferred[F, Unit] + value <- cache.put(1, 1, released.complete(()).void) value <- value _ <- Sync[F].delay { value shouldEqual none } _ <- List.fill(6)(touch).foldMapM(identity) - value <- cache.get(0) - _ <- Sync[F].delay { value shouldEqual 0.some } + // The cleanup routine owns the moment of the eviction, and key 1 must not be read while it + // is being waited out, as a read would refresh it. Its release callback signals the + // eviction instead, with key 0 kept in use by the very same waiting. + _ <- Temporal[F].timeout((touch *> released.tryGet).iterateUntil { _.isDefined }, 5.seconds) value <- cache.get(1) _ <- Sync[F].delay { value shouldEqual none } + value <- cache.get(0) + _ <- Sync[F].delay { value shouldEqual 0.some } release <- release.get _ <- Sync[F].delay { release shouldEqual false } } yield {} diff --git a/scache/src/test/scala/com/evolution/scache/SerialMapSpec.scala b/scache/src/test/scala/com/evolution/scache/SerialMapSpec.scala index 0040a68..5fe1d3f 100644 --- a/scache/src/test/scala/com/evolution/scache/SerialMapSpec.scala +++ b/scache/src/test/scala/com/evolution/scache/SerialMapSpec.scala @@ -1,6 +1,6 @@ package com.evolution.scache -import cats.effect.{Async, Concurrent, Deferred, IO, Outcome} +import cats.effect.{Async, Deferred, IO, Outcome} import cats.syntax.all.* import com.evolution.scache.IOSuite.* import com.evolutiongaming.catshelper.CatsHelper.* @@ -228,9 +228,9 @@ class SerialMapSpec extends AsyncFunSuite with Matchers { } } - private def remove[F[_]: Concurrent] = { + private def remove[F[_]: Async] = { val key = "key" - val cache = LoadingCache.of(LoadingCache.EntryRefs.empty[F, String, SerialRef[F, SerialMap.State[Int]]]) + val cache = LoadingCache.of[F, String, SerialRef[F, SerialMap.State[Int]]] cache.use { cache => val serialMap = SerialMap(cache) for { @@ -262,9 +262,9 @@ class SerialMapSpec extends AsyncFunSuite with Matchers { } } - private def `not leak on failures`[F[_]: Concurrent] = { + private def `not leak on failures`[F[_]: Async] = { val key = "key" - val cache = LoadingCache.of(LoadingCache.EntryRefs.empty[F, String, SerialRef[F, SerialMap.State[Int]]]) + val cache = LoadingCache.of[F, String, SerialRef[F, SerialMap.State[Int]]] cache.use { cache => val serialMap = SerialMap(cache) val modifyError = serialMap.modify(key) { _ => TestError.raiseError[F, (Option[Int], Unit)] }.attempt