Add LMDB/MapDB/DuckDB fixtures and update benchmark runner
This commit is contained in:
@@ -1 +1,2 @@
|
||||
target
|
||||
dependency-reduced-pom.xml
|
||||
|
||||
@@ -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
|
||||
```
|
||||
|
||||
@@ -35,21 +35,21 @@
|
||||
<dependency>
|
||||
<groupId>com.google.code.gson</groupId>
|
||||
<artifactId>gson</artifactId>
|
||||
<version>2.10.1</version>
|
||||
<version>2.13.2</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Kryo binary serialization -->
|
||||
<dependency>
|
||||
<groupId>com.esotericsoftware</groupId>
|
||||
<artifactId>kryo</artifactId>
|
||||
<version>5.6.0</version>
|
||||
<version>5.6.2</version>
|
||||
</dependency>
|
||||
|
||||
<!-- SQLite JDBC driver -->
|
||||
<dependency>
|
||||
<groupId>org.xerial</groupId>
|
||||
<artifactId>sqlite-jdbc</artifactId>
|
||||
<version>3.45.1.0</version>
|
||||
<version>3.51.3.0</version>
|
||||
</dependency>
|
||||
|
||||
<!-- ORMLite -->
|
||||
@@ -66,11 +66,32 @@
|
||||
<version>2026.1</version>
|
||||
</dependency>
|
||||
|
||||
<!-- LMDB Java binding -->
|
||||
<dependency>
|
||||
<groupId>org.lmdbjava</groupId>
|
||||
<artifactId>lmdbjava</artifactId>
|
||||
<version>0.9.3</version>
|
||||
</dependency>
|
||||
|
||||
<!-- MapDB -->
|
||||
<dependency>
|
||||
<groupId>org.mapdb</groupId>
|
||||
<artifactId>mapdb</artifactId>
|
||||
<version>3.1.0</version>
|
||||
</dependency>
|
||||
|
||||
<!-- DuckDB JDBC -->
|
||||
<dependency>
|
||||
<groupId>org.duckdb</groupId>
|
||||
<artifactId>duckdb_jdbc</artifactId>
|
||||
<version>1.5.1.0</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Silence SLF4J "no binding" warning -->
|
||||
<dependency>
|
||||
<groupId>org.slf4j</groupId>
|
||||
<artifactId>slf4j-nop</artifactId>
|
||||
<version>2.0.13</version>
|
||||
<version>2.0.17</version>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
@@ -20,6 +20,9 @@ public class DataAccessBenchmark {
|
||||
@Param({
|
||||
"in-memory",
|
||||
"memory-mapped-file",
|
||||
"lmdb",
|
||||
"mapdb",
|
||||
"duckdb-jdbc",
|
||||
"gson",
|
||||
"kryo",
|
||||
"sqlite-jdbc-memory",
|
||||
|
||||
@@ -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<User> 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -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<ByteBuffer> env;
|
||||
private Dbi<ByteBuffer> db;
|
||||
private Kryo kryo;
|
||||
private Path envDir;
|
||||
|
||||
@Override
|
||||
public void setup(List<User> 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<ByteBuffer> 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<ByteBuffer> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<Long, byte[]> usersById;
|
||||
private Kryo kryo;
|
||||
private Path dbDir;
|
||||
private Path dbFile;
|
||||
|
||||
@Override
|
||||
public void setup(List<User> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<RunResult> runResults = new Runner(opt).run();
|
||||
printSortedSummary(runResults);
|
||||
}
|
||||
|
||||
private static void printSortedSummary(Collection<RunResult> runResults) {
|
||||
if (runResults == null || runResults.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
Map<Mode, List<RunResult>> 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<Mode, List<RunResult>> entry : byMode.entrySet()) {
|
||||
Mode mode = entry.getKey();
|
||||
List<RunResult> 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) {
|
||||
|
||||
Reference in New Issue
Block a user