diff --git a/.gitignore b/.gitignore index eb5a316..aa2d97a 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ target +dependency-reduced-pom.xml diff --git a/README.md b/README.md index 23038f8..3f763a5 100644 --- a/README.md +++ b/README.md @@ -2,5 +2,8 @@ ```bash mvn package -q -java --enable-native-access=ALL-UNNAMED --sun-misc-unsafe-memory-access=allow -jar target/benchmarks.jar +java -jar target/benchmarks.jar + +# optional: suppress JDK warning from the launcher JVM +java --sun-misc-unsafe-memory-access=allow -jar target/benchmarks.jar ``` diff --git a/pom.xml b/pom.xml index 20146cd..47f709c 100644 --- a/pom.xml +++ b/pom.xml @@ -35,21 +35,21 @@ com.google.code.gson gson - 2.10.1 + 2.13.2 com.esotericsoftware kryo - 5.6.0 + 5.6.2 org.xerial sqlite-jdbc - 3.45.1.0 + 3.51.3.0 @@ -66,11 +66,32 @@ 2026.1 + + + org.lmdbjava + lmdbjava + 0.9.3 + + + + + org.mapdb + mapdb + 3.1.0 + + + + + org.duckdb + duckdb_jdbc + 1.5.1.0 + + org.slf4j slf4j-nop - 2.0.13 + 2.0.17 runtime diff --git a/src/main/java/com/benchmark/benchmarks/DataAccessBenchmark.java b/src/main/java/com/benchmark/benchmarks/DataAccessBenchmark.java index d8a8299..f740dca 100644 --- a/src/main/java/com/benchmark/benchmarks/DataAccessBenchmark.java +++ b/src/main/java/com/benchmark/benchmarks/DataAccessBenchmark.java @@ -20,6 +20,9 @@ public class DataAccessBenchmark { @Param({ "in-memory", "memory-mapped-file", + "lmdb", + "mapdb", + "duckdb-jdbc", "gson", "kryo", "sqlite-jdbc-memory", diff --git a/src/main/java/com/benchmark/fixtures/DuckDbJdbcFixture.java b/src/main/java/com/benchmark/fixtures/DuckDbJdbcFixture.java new file mode 100644 index 0000000..bc8c1d4 --- /dev/null +++ b/src/main/java/com/benchmark/fixtures/DuckDbJdbcFixture.java @@ -0,0 +1,80 @@ +package com.benchmark.fixtures; + +import com.benchmark.model.User; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.List; + +public class DuckDbJdbcFixture implements DataFixtures { + + private Connection connection; + private PreparedStatement selectStmt; + + @Override + public void setup(List users) throws Exception { + connection = DriverManager.getConnection("jdbc:duckdb:"); + + try (Statement stmt = connection.createStatement()) { + stmt.execute(""" + CREATE TABLE IF NOT EXISTS users ( + id BIGINT PRIMARY KEY, + name VARCHAR NOT NULL, + email VARCHAR NOT NULL, + age INTEGER NOT NULL, + created_at BIGINT NOT NULL + ) + """); + } + + connection.setAutoCommit(false); + try (PreparedStatement ps = connection.prepareStatement( + "INSERT INTO users(id, name, email, age, created_at) VALUES(?,?,?,?,?)")) { + for (User user : users) { + ps.setLong(1, user.getId()); + ps.setString(2, user.getName()); + ps.setString(3, user.getEmail()); + ps.setInt(4, user.getAge()); + ps.setLong(5, user.getCreatedAt()); + ps.addBatch(); + } + ps.executeBatch(); + } + connection.commit(); + connection.setAutoCommit(true); + + selectStmt = connection.prepareStatement( + "SELECT id, name, email, age, created_at FROM users WHERE id = ?"); + } + + @Override + public User findById(long id) throws SQLException { + selectStmt.setLong(1, id); + try (ResultSet rs = selectStmt.executeQuery()) { + if (!rs.next()) { + return null; + } + return new User( + rs.getLong("id"), + rs.getString("name"), + rs.getString("email"), + rs.getInt("age"), + rs.getLong("created_at") + ); + } + } + + @Override + public void teardown() throws Exception { + if (selectStmt != null) { + selectStmt.close(); + } + if (connection != null) { + connection.close(); + } + } +} diff --git a/src/main/java/com/benchmark/fixtures/FixtureFactory.java b/src/main/java/com/benchmark/fixtures/FixtureFactory.java index 8e1c681..ec20585 100644 --- a/src/main/java/com/benchmark/fixtures/FixtureFactory.java +++ b/src/main/java/com/benchmark/fixtures/FixtureFactory.java @@ -6,6 +6,9 @@ public class FixtureFactory { return switch (type) { case "in-memory" -> new InMemoryFixture(); case "memory-mapped-file" -> new MemoryMappedFileFixture(); + case "lmdb" -> new LmdbFixture(); + case "mapdb" -> new MapDbFixture(); + case "duckdb-jdbc" -> new DuckDbJdbcFixture(); case "chronicle-map" -> new ChronicleMapFixture(); case "gson" -> new GsonFileFixture(); case "kryo" -> new KryoFileFixture(); diff --git a/src/main/java/com/benchmark/fixtures/LmdbFixture.java b/src/main/java/com/benchmark/fixtures/LmdbFixture.java new file mode 100644 index 0000000..f544b84 --- /dev/null +++ b/src/main/java/com/benchmark/fixtures/LmdbFixture.java @@ -0,0 +1,108 @@ +package com.benchmark.fixtures; + +import com.benchmark.model.User; +import com.esotericsoftware.kryo.Kryo; +import com.esotericsoftware.kryo.io.Input; +import com.esotericsoftware.kryo.io.Output; +import org.lmdbjava.Dbi; +import org.lmdbjava.DbiFlags; +import org.lmdbjava.Env; +import org.lmdbjava.Txn; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Comparator; +import java.util.List; + +public class LmdbFixture implements DataFixtures { + + private Env env; + private Dbi db; + private Kryo kryo; + private Path envDir; + + @Override + public void setup(List users) throws IOException { + kryo = new Kryo(); + kryo.setRegistrationRequired(false); + kryo.register(User.class); + + envDir = Files.createTempDirectory("benchmark-lmdb-"); + long mapSize = Math.max(16L * 1024 * 1024, users.size() * 2048L); + env = Env.create() + .setMapSize(mapSize) + .setMaxDbs(1) + .setMaxReaders(1) + .open(envDir.toFile()); + + db = env.openDbi("users", DbiFlags.MDB_CREATE); + + try (Txn txn = env.txnWrite()) { + for (User user : users) { + byte[] bytes = serialize(user); + db.put(txn, keyBuffer(user.getId()), valueBuffer(bytes)); + } + txn.commit(); + } + } + + @Override + public User findById(long id) { + try (Txn txn = env.txnRead()) { + ByteBuffer value = db.get(txn, keyBuffer(id)); + if (value == null) { + return null; + } + ByteBuffer copy = value.duplicate(); + byte[] bytes = new byte[copy.remaining()]; + copy.get(bytes); + return deserialize(bytes); + } + } + + @Override + public void teardown() throws IOException { + if (db != null) { + db.close(); + } + if (env != null) { + env.close(); + } + if (envDir != null && Files.exists(envDir)) { + Files.walk(envDir) + .sorted(Comparator.reverseOrder()) + .forEach(path -> path.toFile().delete()); + } + } + + private ByteBuffer keyBuffer(long id) { + ByteBuffer key = ByteBuffer.allocateDirect(Long.BYTES).order(ByteOrder.BIG_ENDIAN); + key.putLong(id); + key.flip(); + return key; + } + + private ByteBuffer valueBuffer(byte[] bytes) { + ByteBuffer value = ByteBuffer.allocateDirect(bytes.length); + value.put(bytes); + value.flip(); + return value; + } + + private byte[] serialize(User user) { + Output output = new Output(256, -1); + kryo.writeObject(output, user); + byte[] bytes = output.toBytes(); + output.close(); + return bytes; + } + + private User deserialize(byte[] bytes) { + try (Input input = new Input(bytes)) { + return kryo.readObject(input, User.class); + } + } +} diff --git a/src/main/java/com/benchmark/fixtures/MapDbFixture.java b/src/main/java/com/benchmark/fixtures/MapDbFixture.java new file mode 100644 index 0000000..a570b43 --- /dev/null +++ b/src/main/java/com/benchmark/fixtures/MapDbFixture.java @@ -0,0 +1,83 @@ +package com.benchmark.fixtures; + +import com.benchmark.model.User; +import com.esotericsoftware.kryo.Kryo; +import com.esotericsoftware.kryo.io.Input; +import com.esotericsoftware.kryo.io.Output; +import org.mapdb.DB; +import org.mapdb.DBMaker; +import org.mapdb.HTreeMap; +import org.mapdb.Serializer; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Comparator; +import java.util.List; + +public class MapDbFixture implements DataFixtures { + + private DB db; + private HTreeMap usersById; + private Kryo kryo; + private Path dbDir; + private Path dbFile; + + @Override + public void setup(List users) throws IOException { + kryo = new Kryo(); + kryo.setRegistrationRequired(false); + kryo.register(User.class); + + dbDir = Files.createTempDirectory("benchmark-mapdb-"); + dbFile = dbDir.resolve("data.db"); + db = DBMaker + .fileDB(dbFile.toFile()) + .fileMmapEnableIfSupported() + .closeOnJvmShutdown() + .make(); + + usersById = db + .hashMap("users", Serializer.LONG, Serializer.BYTE_ARRAY) + .createOrOpen(); + + for (User user : users) { + usersById.put(user.getId(), serialize(user)); + } + } + + @Override + public User findById(long id) { + byte[] bytes = usersById.get(id); + return bytes == null ? null : deserialize(bytes); + } + + @Override + public void teardown() throws IOException { + if (db != null) { + db.close(); + } + if (dbFile != null) { + Files.deleteIfExists(dbFile); + } + if (dbDir != null && Files.exists(dbDir)) { + Files.walk(dbDir) + .sorted(Comparator.reverseOrder()) + .forEach(path -> path.toFile().delete()); + } + } + + private byte[] serialize(User user) { + Output output = new Output(256, -1); + kryo.writeObject(output, user); + byte[] bytes = output.toBytes(); + output.close(); + return bytes; + } + + private User deserialize(byte[] bytes) { + try (Input input = new Input(bytes)) { + return kryo.readObject(input, User.class); + } + } +} diff --git a/src/main/java/com/benchmark/runner/BenchmarkMain.java b/src/main/java/com/benchmark/runner/BenchmarkMain.java index 5ee4a64..41c881c 100644 --- a/src/main/java/com/benchmark/runner/BenchmarkMain.java +++ b/src/main/java/com/benchmark/runner/BenchmarkMain.java @@ -1,12 +1,21 @@ package com.benchmark.runner; import com.benchmark.benchmarks.DataAccessBenchmark; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.results.Result; +import org.openjdk.jmh.results.RunResult; import org.openjdk.jmh.runner.options.CommandLineOptions; import org.openjdk.jmh.runner.Runner; import org.openjdk.jmh.runner.options.ChainedOptionsBuilder; import org.openjdk.jmh.runner.options.Options; import org.openjdk.jmh.runner.options.OptionsBuilder; +import java.util.ArrayList; +import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + public class BenchmarkMain { public static void main(String[] args) throws Exception { @@ -23,6 +32,9 @@ public class BenchmarkMain { "--enable-native-access=ALL-UNNAMED", "--sun-misc-unsafe-memory-access=allow", "--add-opens=java.base/java.lang=ALL-UNNAMED", + "--add-opens=java.base/java.nio=ALL-UNNAMED", + "--add-opens=java.base/sun.nio.ch=ALL-UNNAMED", + "--add-exports=java.base/sun.nio.ch=ALL-UNNAMED", "--add-opens=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED", "--add-opens=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED", "--add-opens=jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED", @@ -50,7 +62,46 @@ public class BenchmarkMain { } Options opt = builder.build(); - new Runner(opt).run(); + Collection runResults = new Runner(opt).run(); + printSortedSummary(runResults); + } + + private static void printSortedSummary(Collection runResults) { + if (runResults == null || runResults.isEmpty()) { + return; + } + + Map> byMode = new LinkedHashMap<>(); + for (RunResult runResult : runResults) { + byMode.computeIfAbsent(runResult.getParams().getMode(), m -> new ArrayList<>()).add(runResult); + } + + System.out.println("\n=== Sorted summary by performance ==="); + for (Map.Entry> entry : byMode.entrySet()) { + Mode mode = entry.getKey(); + List sorted = new ArrayList<>(entry.getValue()); + boolean lowerIsBetter = mode == Mode.AverageTime || mode == Mode.SampleTime || mode == Mode.SingleShotTime; + + sorted.sort((a, b) -> { + double scoreA = a.getPrimaryResult().getScore(); + double scoreB = b.getPrimaryResult().getScore(); + return lowerIsBetter ? Double.compare(scoreA, scoreB) : Double.compare(scoreB, scoreA); + }); + + System.out.printf("%n[%s]%n", mode); + System.out.printf("%-24s %14s %14s %s%n", "fixtureType", "score", "error", "unit"); + for (RunResult runResult : sorted) { + Result result = runResult.getPrimaryResult(); + String fixtureType = runResult.getParams().getParam("fixtureType"); + System.out.printf( + "%-24s %14.3f %14.3f %s%n", + fixtureType, + result.getScore(), + result.getScoreError(), + result.getScoreUnit() + ); + } + } } private static boolean isJmhListingOrHelpCommand(String[] args) {