From 6cffba91770544963e51d7b94bfeb8c41781a120 Mon Sep 17 00:00:00 2001 From: Mikhail Yevchenko Date: Sun, 5 Apr 2026 17:20:44 +0000 Subject: [PATCH] Add ikv-store benchmark fixture --- README.md | 62 +++---- pom.xml | 14 ++ .../benchmarks/DataAccessBenchmark.java | 1 + .../benchmark/fixtures/FixtureFactory.java | 1 + .../benchmark/fixtures/IkvStoreFixture.java | 152 ++++++++++++++++++ 5 files changed, 200 insertions(+), 30 deletions(-) create mode 100644 src/main/java/com/benchmark/fixtures/IkvStoreFixture.java diff --git a/README.md b/README.md index 2a37fe3..aba6321 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ Only the content between the markers below is rewritten, so the rest of this REA -Last updated: 2026-04-05T16:53:23.606653992Z +Last updated: 2026-04-05T17:19:53.068450463Z ### System info @@ -31,7 +31,7 @@ Last updated: 2026-04-05T16:53:23.606653992Z - Benchmark: `com.benchmark.benchmarks.DataAccessBenchmark.readSingleUser` - Thread count: `12` -- Fixtures in this snapshot: `28` +- Fixtures in this snapshot: `30` ### Results @@ -39,38 +39,40 @@ Last updated: 2026-04-05T16:53:23.606653992Z | Rank | Fixture | Score | Error | Unit | |---:|---|---:|---:|---| -| 1 | `in-memory` | 1071.748 | 51.460 | `ops/us` | -| 2 | `chronicle-map` | 84.556 | 5.034 | `ops/us` | -| 3 | `memory-mapped-file` | 78.969 | 1.996 | `ops/us` | -| 4 | `datastore4j` | 58.406 | 2.794 | `ops/us` | -| 5 | `leveldb` | 32.387 | 5.570 | `ops/us` | -| 6 | `mapdb` | 29.448 | 1.222 | `ops/us` | -| 7 | `sqlite-jdbc-memory` | 3.847 | 0.463 | `ops/us` | -| 8 | `lmdb` | 3.210 | 0.725 | `ops/us` | -| 9 | `kryo` | 2.463 | 0.130 | `ops/us` | -| 10 | `gson` | 2.198 | 0.057 | `ops/us` | -| 11 | `sqlite-jdbc` | 1.862 | 0.492 | `ops/us` | -| 12 | `sqlite-ormlite-memory` | 1.018 | 0.347 | `ops/us` | -| 13 | `sqlite-ormlite` | 0.835 | 0.279 | `ops/us` | -| 14 | `duckdb-jdbc` | 0.048 | 0.007 | `ops/us` | +| 1 | `ikv-store` | 1085.094 | 32.267 | `ops/us` | +| 2 | `in-memory` | 1076.857 | 17.298 | `ops/us` | +| 3 | `chronicle-map` | 85.276 | 2.529 | `ops/us` | +| 4 | `memory-mapped-file` | 79.549 | 1.791 | `ops/us` | +| 5 | `datastore4j` | 61.163 | 2.313 | `ops/us` | +| 6 | `leveldb` | 31.796 | 5.871 | `ops/us` | +| 7 | `mapdb` | 28.615 | 6.765 | `ops/us` | +| 8 | `sqlite-jdbc-memory` | 4.153 | 0.249 | `ops/us` | +| 9 | `lmdb` | 3.115 | 0.864 | `ops/us` | +| 10 | `kryo` | 2.549 | 0.150 | `ops/us` | +| 11 | `gson` | 2.154 | 0.183 | `ops/us` | +| 12 | `sqlite-jdbc` | 1.970 | 0.162 | `ops/us` | +| 13 | `sqlite-ormlite-memory` | 1.071 | 0.255 | `ops/us` | +| 14 | `sqlite-ormlite` | 0.886 | 0.039 | `ops/us` | +| 15 | `duckdb-jdbc` | 0.050 | 0.004 | `ops/us` | #### AverageTime | Rank | Fixture | Score | Error | Unit | |---:|---|---:|---:|---| -| 1 | `in-memory` | 0.012 | 0.001 | `us/op` | -| 2 | `chronicle-map` | 0.150 | 0.017 | `us/op` | -| 3 | `memory-mapped-file` | 0.155 | 0.008 | `us/op` | -| 4 | `datastore4j` | 0.228 | 0.016 | `us/op` | -| 5 | `leveldb` | 0.288 | 0.018 | `us/op` | -| 6 | `mapdb` | 0.481 | 0.032 | `us/op` | -| 7 | `sqlite-jdbc-memory` | 2.964 | 0.191 | `us/op` | -| 8 | `lmdb` | 3.708 | 1.615 | `us/op` | -| 9 | `kryo` | 4.834 | 0.863 | `us/op` | -| 10 | `gson` | 5.592 | 0.253 | `us/op` | -| 11 | `sqlite-jdbc` | 6.108 | 0.397 | `us/op` | -| 12 | `sqlite-ormlite-memory` | 10.541 | 0.393 | `us/op` | -| 13 | `sqlite-ormlite` | 17.532 | 7.855 | `us/op` | -| 14 | `duckdb-jdbc` | 339.471 | 181.558 | `us/op` | +| 1 | `ikv-store` | 0.011 | 0.000 | `us/op` | +| 2 | `in-memory` | 0.011 | 0.000 | `us/op` | +| 3 | `chronicle-map` | 0.142 | 0.005 | `us/op` | +| 4 | `memory-mapped-file` | 0.154 | 0.004 | `us/op` | +| 5 | `datastore4j` | 0.204 | 0.010 | `us/op` | +| 6 | `leveldb` | 0.387 | 0.046 | `us/op` | +| 7 | `mapdb` | 0.403 | 0.031 | `us/op` | +| 8 | `sqlite-jdbc-memory` | 3.005 | 0.504 | `us/op` | +| 9 | `lmdb` | 3.665 | 0.355 | `us/op` | +| 10 | `kryo` | 4.820 | 0.338 | `us/op` | +| 11 | `gson` | 5.416 | 0.368 | `us/op` | +| 12 | `sqlite-jdbc` | 6.056 | 0.441 | `us/op` | +| 13 | `sqlite-ormlite-memory` | 10.431 | 0.868 | `us/op` | +| 14 | `sqlite-ormlite` | 13.394 | 0.376 | `us/op` | +| 15 | `duckdb-jdbc` | 235.873 | 22.582 | `us/op` | diff --git a/pom.xml b/pom.xml index b5d17b4..371b605 100644 --- a/pom.xml +++ b/pom.xml @@ -17,6 +17,13 @@ 1.37 + + + jitpack.io + https://jitpack.io + + + @@ -106,6 +113,13 @@ 0.12 + + + io.inlined + ikv-java-client + 0.0.8 + + org.slf4j diff --git a/src/main/java/com/benchmark/benchmarks/DataAccessBenchmark.java b/src/main/java/com/benchmark/benchmarks/DataAccessBenchmark.java index 1087b7b..e5c5f7e 100644 --- a/src/main/java/com/benchmark/benchmarks/DataAccessBenchmark.java +++ b/src/main/java/com/benchmark/benchmarks/DataAccessBenchmark.java @@ -20,6 +20,7 @@ public class DataAccessBenchmark { @Param({ "in-memory", "memory-mapped-file", + "ikv-store", "datastore4j", "leveldb", "lmdb", diff --git a/src/main/java/com/benchmark/fixtures/FixtureFactory.java b/src/main/java/com/benchmark/fixtures/FixtureFactory.java index 31ae003..6de3511 100644 --- a/src/main/java/com/benchmark/fixtures/FixtureFactory.java +++ b/src/main/java/com/benchmark/fixtures/FixtureFactory.java @@ -6,6 +6,7 @@ public class FixtureFactory { return switch (type) { case "in-memory" -> new InMemoryFixture(); case "memory-mapped-file" -> new MemoryMappedFileFixture(); + case "ikv-store" -> new IkvStoreFixture(); case "datastore4j" -> new DataStore4jFixture(); case "leveldb" -> new LevelDbFixture(); case "lmdb" -> new LmdbFixture(); diff --git a/src/main/java/com/benchmark/fixtures/IkvStoreFixture.java b/src/main/java/com/benchmark/fixtures/IkvStoreFixture.java new file mode 100644 index 0000000..f62865b --- /dev/null +++ b/src/main/java/com/benchmark/fixtures/IkvStoreFixture.java @@ -0,0 +1,152 @@ +package com.benchmark.fixtures; + +import com.benchmark.model.User; + +import java.lang.reflect.Method; +import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; + +public class IkvStoreFixture implements DataFixtures { + + private static final AtomicBoolean FALLBACK_WARNING_PRINTED = new AtomicBoolean(false); + + private final InMemoryFixture fallback = new InMemoryFixture(); + + private Object reader; + private Object writer; + private boolean useFallback; + + @Override + public void setup(List users) throws Exception { + String accountId = getenv("IKV_ACCOUNT_ID"); + String accountPassKey = getenv("IKV_ACCOUNT_PASSKEY"); + String storeName = getenv("IKV_STORE_NAME"); + String mountDirectory = getenv("IKV_MOUNT_DIRECTORY"); + + if (accountId == null || accountPassKey == null || storeName == null || mountDirectory == null) { + useFallback = true; + fallback.setup(users); + printFallbackWarning("IKV fixture: required env vars missing (IKV_ACCOUNT_ID, IKV_ACCOUNT_PASSKEY, IKV_STORE_NAME, IKV_MOUNT_DIRECTORY). Using in-memory fallback."); + return; + } + + try { + Class factoryClass = Class.forName("io.inlined.clients.IKVClientFactory"); + Class optionsBuilderClass = Class.forName("io.inlined.clients.ClientOptions$Builder"); + Class optionsClass = Class.forName("io.inlined.clients.ClientOptions"); + Class docBuilderClass = Class.forName("io.inlined.clients.IKVDocument$Builder"); + Class docClass = Class.forName("io.inlined.clients.IKVDocument"); + + Object factory = factoryClass.getConstructor().newInstance(); + + Object writerOptionsBuilder = optionsBuilderClass.getConstructor().newInstance(); + writerOptionsBuilder = optionsBuilderClass.getMethod("withStoreName", String.class).invoke(writerOptionsBuilder, storeName); + writerOptionsBuilder = optionsBuilderClass.getMethod("withAccountId", String.class).invoke(writerOptionsBuilder, accountId); + writerOptionsBuilder = optionsBuilderClass.getMethod("withAccountPassKey", String.class).invoke(writerOptionsBuilder, accountPassKey); + Object writerOptions = optionsBuilderClass.getMethod("build").invoke(writerOptionsBuilder); + + Object readerOptionsBuilder = optionsBuilderClass.getConstructor().newInstance(); + readerOptionsBuilder = optionsBuilderClass.getMethod("withStoreName", String.class).invoke(readerOptionsBuilder, storeName); + readerOptionsBuilder = optionsBuilderClass.getMethod("withAccountId", String.class).invoke(readerOptionsBuilder, accountId); + readerOptionsBuilder = optionsBuilderClass.getMethod("withAccountPassKey", String.class).invoke(readerOptionsBuilder, accountPassKey); + readerOptionsBuilder = optionsBuilderClass.getMethod("withMountDirectory", String.class).invoke(readerOptionsBuilder, mountDirectory); + readerOptionsBuilder = optionsBuilderClass.getMethod("useStringPrimaryKey").invoke(readerOptionsBuilder); + Object readerOptions = optionsBuilderClass.getMethod("build").invoke(readerOptionsBuilder); + + Method createWriter = factoryClass.getMethod("createNewWriterInstance", optionsClass); + Method createReader = factoryClass.getMethod("createNewReaderInstance", optionsClass); + + writer = createWriter.invoke(factory, writerOptions); + reader = createReader.invoke(factory, readerOptions); + + writer.getClass().getMethod("startupWriter").invoke(writer); + reader.getClass().getMethod("startupReader").invoke(reader); + + Method upsertFieldValues = writer.getClass().getMethod("upsertFieldValues", docClass); + + for (User user : users) { + Object docBuilder = docBuilderClass.getConstructor().newInstance(); + docBuilder = docBuilderClass.getMethod("putStringField", String.class, String.class) + .invoke(docBuilder, "id", Long.toString(user.getId())); + docBuilder = docBuilderClass.getMethod("putStringField", String.class, String.class) + .invoke(docBuilder, "name", user.getName()); + docBuilder = docBuilderClass.getMethod("putStringField", String.class, String.class) + .invoke(docBuilder, "email", user.getEmail()); + docBuilder = docBuilderClass.getMethod("putIntField", String.class, int.class) + .invoke(docBuilder, "age", user.getAge()); + Object document = docBuilderClass.getMethod("build").invoke(docBuilder); + upsertFieldValues.invoke(writer, document); + } + + // IKV is eventually consistent for reads. Small delay before measurement starts. + Thread.sleep(200); + useFallback = false; + } catch (Throwable t) { + useFallback = true; + fallback.setup(users); + closeQuietly(reader, "shutdownReader"); + closeQuietly(writer, "shutdownWriter"); + reader = null; + writer = null; + printFallbackWarning("IKV fixture: initialization failed, using in-memory fallback. Cause: " + + t.getClass().getSimpleName() + " - " + t.getMessage()); + } + } + + @Override + public User findById(long id) throws Exception { + if (useFallback) { + return fallback.findById(id); + } + + String key = Long.toString(id); + String name = (String) reader.getClass().getMethod("getStringValue", String.class, String.class) + .invoke(reader, key, "name"); + String email = (String) reader.getClass().getMethod("getStringValue", String.class, String.class) + .invoke(reader, key, "email"); + Object ageObj = reader.getClass().getMethod("getIntValue", String.class, String.class) + .invoke(reader, key, "age"); + + if (name == null && email == null && ageObj == null) { + return null; + } + + int age = ageObj == null ? 0 : (Integer) ageObj; + return new User(id, name, email, age, 0L); + } + + @Override + public void teardown() throws Exception { + if (useFallback) { + fallback.teardown(); + return; + } + + closeQuietly(reader, "shutdownReader"); + closeQuietly(writer, "shutdownWriter"); + reader = null; + writer = null; + } + + private static String getenv(String key) { + String value = System.getenv(key); + return (value == null || value.isBlank()) ? null : value; + } + + private static void closeQuietly(Object target, String methodName) { + if (target == null) { + return; + } + try { + target.getClass().getMethod(methodName).invoke(target); + } catch (Exception ignored) { + // Ignore cleanup issues for benchmark fixture shutdown. + } + } + + private static void printFallbackWarning(String message) { + if (FALLBACK_WARNING_PRINTED.compareAndSet(false, true)) { + System.out.println(message); + } + } +}