diff --git a/README.md b/README.md index ec2708b..69ce5a5 100644 --- a/README.md +++ b/README.md @@ -139,6 +139,125 @@ 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 `SerialMap.apply` 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`. A typical release process is as follows: 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..acf08a4 --- /dev/null +++ b/benchmark/src/main/scala/com/evolution/scache/bench/CacheBenchmark.scala @@ -0,0 +1,242 @@ +package com.evolution.scache.bench + +import cats.effect.IO +import cats.effect.unsafe.implicits.global +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() + } + + /** + * 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 90a29d1..af192f3 100644 --- a/build.sbt +++ b/build.sbt @@ -70,7 +70,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) @@ -96,6 +96,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 da6b5db..9a667c2 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -9,3 +9,5 @@ addSbtPlugin("com.evolution" % "sbt-scalac-opts-plugin" % "0.1.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..a0cf3bf 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) } } } + } + + /** + * 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]]) = { + 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,634 @@ 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] + .asRight[Unit] + .asRight[Unit] } - // 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] - } - } - // 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)).map { + // 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. + (a, none[F[Unit]]) + .asRight[Unit] + .asRight[Unit] + // 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]])]] - .pure[F] + ().asLeft[Either[Unit, (A, Option[F[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 +909,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 +932,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 +1020,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 +1039,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 +1065,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 +1079,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 +1116,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 +1141,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 +1155,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 +1223,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 +1256,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..053d94d 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} @@ -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..9fd6b39 --- /dev/null +++ b/scache/src/test/scala/com/evolution/scache/CacheDefectsSpec.scala @@ -0,0 +1,316 @@ +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) + } + + 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/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